EWS Mail Sender Deprecated
Exchange Web Services mail sender. Note: this integration supports Office 365 basic authentication only. If you are using Office 365, we recommend using the EWS O365 Integration instead, which supports modern authentication (oauth2). Deprecated. Use EWS v2 instead
Email · Microsoft Exchange On-Premise
Details
| ID | EWS Mail Sender |
|---|---|
| Provider | Microsoft |
| Category | |
| From Version | 5.0.0 |
| Docker Image | demisto/py3ews:1.0.0.47270 |
| Supported Modules | Agentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud |
README
Exchange Web Services and Office 365 Email sender.
Configure EWS Mail Sender in Cortex
| Parameter | Description | Required |
|---|---|---|
| ewsServer | Exchange URL or Server IP address | True |
| credentials | Authentication: Email address (for Office 365) or DOMAIN\USERNAME (e.g. XSOAR.INT\admin) | True |
| defaultServerVersion | Server Version (2007, 2010, 2010_SP2, 2013, or 2016) | True |
| authType | Authentication Type (NTLM, Basic, or Digest). For Office 365 use Basic. | True |
| insecure | Trust any certificate (not secure) | False |
| proxy | Use system proxy settings | False |
| impersonation | Has impersonation rights | False |
| mailbox | Sender Mailbox | False |
| Single engine | If relevant, select the engine that acts as a proxy to the server. Engines are used when you need to access a remote network segments and there are network devices such as proxies, firewalls, etc. that prevent the Cortex XSOAR server from accessing the remote networks. For more information on Cortex XSOAR engines see: Cortex XSOAR 6.13 - Engines Cortex XSOAR 8 Cloud- Engines Cortex XSOAR 8.7 On-prem - Engines |
False |
Top Use-cases
- Send notifications to external users.
- Send an email asking for a response to be returned as part of a playbook. See Receiving an email reply
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
send-mail
Sends an email using EWS.
Base Command
send-mail
Input
| Argument Name | Description | Required |
|---|---|---|
| to | A CSV list of email addresses for the ‘to’ field. | Required |
| cc | A CSV list of email addresses for the ‘cc’ field. | Optional |
| bcc | A CSV list of email addresses for the ‘bcc’ field. | Optional |
| subject | Subject for the email to be sent. | Required |
| replyTo | The email address specified in the ‘reply to’ field. | Optional |
| body | The contents (body) of the email to send. | Optional |
| htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the “body” argument. | Optional |
| attachIDs | A CSV list of War Room entry IDs that contain files, and are used to attach files to the outgoing email. For example: attachIDs=15@8,19@8. | Optional |
| attachNames | A CSV list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional |
| attachCIDs | A CSV list of CIDs to embed attachments within the email itself. | Optional |
| from_address | The email address from which to reply. | Optional |
Context Output
There is no context output for this command.
Command Example
!send-mail body="hello this is a test" subject=Hi to=avishai@demistodev.onmicrosoft.com
Human Readable Output
Sent email
attachments from subject to avishai@demistodev.onmicrosoft.com Hi avishai@demistodev.onmicrosoft.com
reply-mail
Replies to an email using EWS.
Base Command
reply-mail
Input
| Argument Name | Description | Required |
|---|---|---|
| inReplyTo | ID of the item to reply to. | Required |
| to | A CSV list of email addresses for the ‘to’ field. | Required |
| cc | A CSV list of email addresses for the ‘cc’ field. | Optional |
| bcc | A CSV list of email addresses for the ‘bcc’ field. | Optional |
| subject | Subject for the email to be sent. | Optional |
| body | The contents (body) of the email to send. | Optional |
| htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the “body” argument. | Optional |
| attachIDs | A CSV list of War Room entry IDs that contain files, and are used to attach files to the outgoing email. For example: attachIDs=15@8,19@8. | Optional |
| attachNames | A CSV list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional |
| attachCIDs | A CSV list of CIDs to embed attachments within the email itself. | Optional |
Context Output
There is no context output for this command.
Command Example
!reply-mail item_id=AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NmZhLWQ5MGY1YjIyNzBkNABGAAAAAACYCKjWAnXBTrnhgWJCcLX7BwDrxRwRjq/zTrN6vWSzK4OWAAAAAAEMAADrxRwRjq/zTrN6vWSzK4OWAAPYQGFeAAA= body=hello subject=hi to="avishai@demistodev.onmicrosoft.com"
Human Readable Output
Sent email
attachments from subject to avishai@demistodev.onmicrosoft.com hi avishai@demistodev.onmicrosoft.com
Configuration parameters
ewsServer— Exchange URL or Server IP address (required)credentials— Authentication: Email address (for Office 365) or DOMAIN\USERNAME (e.g. DEMISTO.INT\admin) (required)defaultServerVersion— Server Version (2007, 2010, 2010_SP2, 2013, or 2016) (required)authType— Authentication Type (NTLM, Basic, or Digest). For Office 365 use Basic. (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsimpersonation— Has impersonation rightsmailbox— Sender Mailbox
Commands (2)
-
reply-mailReplies to an email using EWS.
-
send-mailSends an email using EWS.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * from io import StringIO import logging import warnings import traceback import getpass # workaround for bug in exchangelib: https://github.com/ecederstrand/exchangelib/issues/448 class FixGetPass(object): def __init__(self): self.getpass_getuser_org = getpass.getuser def getuser_no_fail(): # getuser() fails on some systems. Provide a sane default. user = 'ews' try: if self.getpass_getuser_org: user = self.getpass_getuser_org() except KeyError: pass return user getpass.getuser = getuser_no_fail def __del__(self): if self.getpass_getuser_org and getpass: getpass.getuser = self.getpass_getuser_org _fix_getpass = FixGetPass() warnings.filterwarnings("ignore") import exchangelib # noqa: E402 from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter # noqa: E402 from exchangelib.version import EXCHANGE_2007, EXCHANGE_2010, EXCHANGE_2010_SP2, EXCHANGE_2013, \ EXCHANGE_2016 # noqa: E402 from exchangelib import HTMLBody, Message, FileAttachment, Account, IMPERSONATION, Credentials, Configuration, NTLM, \ BASIC, DIGEST, Version, DELEGATE # noqa: E402 from exchangelib.errors import ErrorItemNotFound, UnauthorizedError # noqa: E402 IS_TEST_MODULE = False # load arguments USE_PROXY = demisto.params().get('proxy', False) NON_SECURE = demisto.params().get('insecure', False) AUTH_METHOD_STR = demisto.params().get('authType', 'Basic').lower() EWS_SERVER = demisto.params().get('ewsServer', 'https://outlook.office365.com/EWS/Exchange.asmx/') VERSION_STR = demisto.params().get('defaultServerVersion', '2013') FOLDER_NAME = demisto.params().get('folder', 'Inbox') ACCESS_TYPE = IMPERSONATION if demisto.params().get('impersonation', False) else DELEGATE # initialized in main() USERNAME = "" PASSWORD = "" ACCOUNT_EMAIL = "" VERSIONS = { '2007': EXCHANGE_2007, '2010': EXCHANGE_2010, '2010_SP2': EXCHANGE_2010_SP2, '2013': EXCHANGE_2013, '2016': EXCHANGE_2016 } config = None # type: ignore # LOGGING log_stream = None log_handler = None def start_logging(): logging.raiseExceptions = False global log_stream global log_handler if log_stream is None: log_stream = StringIO() log_handler = logging.StreamHandler(stream=log_stream) log_handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) logger = logging.getLogger() logger.addHandler(log_handler) logger.setLevel(logging.DEBUG) # NOTE: Same method used in EWSv2 # If you are modifying this probably also need to modify in the other file def exchangelib_cleanup(): # pragma: no cover key_protocols = exchangelib.protocol.CachingProtocol._protocol_cache.items() try: exchangelib.close_connections() except Exception as ex: demisto.error("Error was found in exchangelib cleanup, ignoring: {}".format(ex)) for key, protocol in key_protocols: try: if "thread_pool" in protocol.__dict__: demisto.debug('terminating thread pool key{} id: {}'.format(key, id(protocol.thread_pool))) protocol.thread_pool.terminate() del protocol.__dict__["thread_pool"] else: demisto.info( 'Thread pool not found (ignoring terminate) in protcol dict: {}'.format(dir(protocol.__dict__))) except Exception as ex: demisto.error("Error with thread_pool.terminate, ignoring: {}".format(ex)) def get_account(account_email): for i in range(1, 4): response = Account( primary_smtp_address=account_email, autodiscover=False, config=config, access_type=ACCESS_TYPE, ) try: response.root # Check if you have access to root directory return response except UnauthorizedError: demisto.debug("Got unauthorized error, This is attempt number {}".format(i)) continue return response def send_email_to_mailbox( account: Account, to: List[str], subject: str, body: str, bcc: List[str], cc: List[str], reply_to: List[str], html_body: Optional[str] = None, attachments: Optional[List[str]] = None, raw_message: Optional[str] = None, from_address: Optional[str] = None ): # pragma: no cover """ Send an email to a mailbox. Args: account (Account): account from which to send an email. to (list[str]): a list of emails to send an email. subject (str): subject of the mail. body (str): body of the email. reply_to (list[str]): list of emails of which to reply to from the sent email. bcc (list[str]): list of email addresses for the 'bcc' field. cc (list[str]): list of email addresses for the 'cc' field. html_body (str): HTML formatted content (body) of the email to be sent. This argument overrides the "body" argument. attachments (list[str]): list of names of attachments to send. raw_message (str): Raw email message from MimeContent type. from_address (str): the email address from which to reply. """ if not attachments: attachments = [] message_body = HTMLBody(html_body) if html_body else body m = Message( account=account, mime_content=raw_message.encode('UTF-8') if raw_message else None, folder=account.sent, cc_recipients=cc, bcc_recipients=bcc, subject=subject, body=message_body, to_recipients=to, reply_to=reply_to, author=from_address ) if account.protocol.version.build <= EXCHANGE_2010_SP2: m.save() for attachment in attachments: m.attach(attachment) m.send() else: for attachment in attachments: m.attach(attachment) m.send_and_save() return m def send_email_reply_to_mailbox(account, inReplyTo, to, body, subject=None, bcc=None, cc=None, html_body=None, attachments=[]): # pragma: no cover item_to_reply_to = account.inbox.get(id=inReplyTo) if isinstance(item_to_reply_to, ErrorItemNotFound): raise Exception(item_to_reply_to) subject = subject or item_to_reply_to.subject message_body = HTMLBody(html_body) if html_body else body reply = item_to_reply_to.create_reply(subject='Re: ' + subject, body=message_body, to_recipients=to, cc_recipients=cc, bcc_recipients=bcc) reply = reply.save(account.drafts) m = account.inbox.get(id=reply.id) for attachment in attachments: m.attach(attachment) m.send() return m def get_auth_method(auth_method): # pragma: no cover auth_method = auth_method.lower() if auth_method == 'ntlm': return NTLM elif auth_method == 'basic': return BASIC elif auth_method == 'digest': return DIGEST raise Exception("%s auth method is not supported. Choose one of %s" % (auth_method, 'ntlm\\basic\\digest')) def get_version(version_str): if version_str not in VERSIONS: raise Exception("%s is unsupported version: %s. Choose one of" % (version_str, "\\".join(VERSIONS.keys()))) return Version(VERSIONS[version_str]) def collect_manual_attachments(manualAttachObj): # pragma: no cover attachments = [] for attachment in manualAttachObj: res = demisto.getFilePath(os.path.basename(attachment['RealFileName'])) file_path = res["path"] with open(file_path, 'rb') as f: attachments.append(FileAttachment(content=f.read(), name=attachment['FileName'])) return attachments def get_none_empty_addresses(addresses_ls): return [adress for adress in addresses_ls if adress] def send_email(to, subject, body="", bcc=None, cc=None, replyTo=None, htmlBody=None, attachIDs="", attachCIDs="", attachNames="", from_mailbox=None, manualAttachObj=None, raw_message=None, from_address=None): account = get_account(from_mailbox or ACCOUNT_EMAIL) bcc: List[str] = get_none_empty_addresses(argToList(bcc)) cc: List[str] = get_none_empty_addresses(argToList(cc)) to: List[str] = get_none_empty_addresses(argToList(to)) reply_to: List[str] = argToList(replyTo) manualAttachObj = manualAttachObj if manualAttachObj is not None else [] subject = subject[:252] + '...' if len(subject) > 255 else subject attachments, attachments_names = process_attachments(attachCIDs, attachIDs, attachNames, manualAttachObj) send_email_to_mailbox( account=account, to=to, subject=subject, body=body, bcc=bcc, cc=cc, reply_to=reply_to, html_body=htmlBody, attachments=attachments, raw_message=raw_message, from_address=from_address ) result_object = { 'from': account.primary_smtp_address, 'to': to, 'subject': subject, 'attachments': attachments_names } return { 'Type': entryTypes['note'], 'Contents': result_object, 'ContentsFormat': formats['json'], 'ReadableContentsFormat': formats['markdown'], 'HumanReadable': tableToMarkdown('Sent email', result_object), } def process_attachments(attachCIDs="", attachIDs="", attachNames="", manualAttachObj=None): # pragma: no cover if manualAttachObj is None: manualAttachObj = [] file_entries_for_attachments = [] # type: list attachments_names = [] # type: list if attachIDs: file_entries_for_attachments = attachIDs if isinstance(attachIDs, list) else attachIDs.split(",") if attachNames: attachments_names = attachNames if isinstance(attachNames, list) else attachNames.split(",") else: for att_id in file_entries_for_attachments: att_name = demisto.getFilePath(att_id)['name'] if isinstance(att_name, list): att_name = att_name[0] attachments_names.append(att_name) if len(file_entries_for_attachments) != len(attachments_names): raise Exception("attachIDs and attachNames lists should be the same length") attachments = collect_manual_attachments(manualAttachObj) if attachCIDs: file_entries_for_attachments_inline = attachCIDs if isinstance(attachCIDs, list) else attachCIDs.split(",") for att_id_inline in file_entries_for_attachments_inline: try: file_info = demisto.getFilePath(att_id_inline) except Exception as ex: demisto.info("EWS error from getFilePath: {}".format(ex)) raise Exception("entry %s does not contain a file" % att_id_inline) att_name_inline = file_info["name"] with open(file_info["path"], 'rb') as f: attachments.append(FileAttachment(content=f.read(), name=att_name_inline, is_inline=True, content_id=att_name_inline)) for i in range(0, len(file_entries_for_attachments)): entry_id = file_entries_for_attachments[i] attachment_name = attachments_names[i] try: res = demisto.getFilePath(entry_id) except Exception as ex: raise Exception("entry {} does not contain a file: {}".format(entry_id, str(ex))) file_path = res["path"] with open(file_path, 'rb') as f: attachments.append(FileAttachment(content=f.read(), name=attachment_name)) return attachments, attachments_names def reply_email(to, inReplyTo, body="", subject="", bcc=None, cc=None, htmlBody=None, attachIDs="", attachCIDs="", attachNames="", from_mailbox=None, manualAttachObj=None): # pragma: no cover account = get_account(from_mailbox or ACCOUNT_EMAIL) bcc = bcc.split(",") if bcc else None cc = cc.split(",") if cc else None to = to.split(",") if to else None manualAttachObj = manualAttachObj if manualAttachObj is not None else [] subject = subject[:252] + '...' if len(subject) > 255 else subject attachments, attachments_names = process_attachments(attachCIDs, attachIDs, attachNames, manualAttachObj) send_email_reply_to_mailbox(account, inReplyTo, to, body, subject, bcc, cc, htmlBody, attachments) result_object = { 'from': account.primary_smtp_address, 'to': to, 'subject': subject, 'attachments': attachments_names } return { 'Type': entryTypes['note'], 'Contents': result_object, 'ContentsFormat': formats['json'], 'ReadableContentsFormat': formats['markdown'], 'HumanReadable': tableToMarkdown('Sent email', result_object), } def prepare(): if NON_SECURE: BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter if not USE_PROXY: def remove_from_dict(d, key): if key in d: del d[key] import os remove_from_dict(os.environ, 'HTTP_PROXY') remove_from_dict(os.environ, 'http_proxy') remove_from_dict(os.environ, 'HTTPS_PROXY') remove_from_dict(os.environ, 'https_proxy') os.environ['NO_PROXY'] = EWS_SERVER version = get_version(VERSION_STR) credentials = Credentials(username=USERNAME, password=PASSWORD) config_args = { 'credentials': credentials, 'auth_type': get_auth_method(AUTH_METHOD_STR), 'version': version } if 'http' in EWS_SERVER.lower(): config_args['service_endpoint'] = EWS_SERVER else: config_args['server'] = EWS_SERVER config = Configuration(**config_args) return config def prepare_args(d): # pragma: no cover if "from" in d: d['from_address'] = d.pop('from') return dict((k.replace("-", "_"), v) for k, v in d.items()) def test_module(): # pragma: no cover global IS_TEST_MODULE IS_TEST_MODULE = True BaseProtocol.TIMEOUT = 20 get_account(ACCOUNT_EMAIL).root demisto.results('ok') def main(): # pragma: no cover global USERNAME, PASSWORD, ACCOUNT_EMAIL, log_stream, config USERNAME = demisto.params()['credentials']['identifier'] PASSWORD = demisto.params()['credentials']['password'] ACCOUNT_EMAIL = demisto.params().get('mailbox', None) if not ACCOUNT_EMAIL: if "@" in USERNAME: ACCOUNT_EMAIL = USERNAME if ACCOUNT_EMAIL is None: raise Exception("Provide a valid email address in the mailbox field") try: start_logging() config = prepare() args = prepare_args(demisto.args()) if demisto.command() == 'test-module': test_module() elif demisto.command() == 'send-mail': demisto.results(send_email(**args)) elif demisto.command() == 'reply-mail': demisto.results(reply_email(**args)) except Exception as e: import time time.sleep(2) debug_log = "=== DEBUG LOG ===\n" + (log_stream.getvalue() if log_stream else "") error_message = "" if "Status code: 401" in debug_log: error_message = ("Got unauthorized from the server. " "Check credentials are correct and authentication" " method are supported. ") error_message += ("You can try using 'domain\\username' as username for authentication. " if AUTH_METHOD_STR.lower() == 'ntlm' else '') if "Status code: 503" in debug_log: error_message = "Got timeout from the server. " \ "Probably the server is not reachable with the current settings. " \ "Check proxy parameter. If you are using server URL - change to server IP address. " error_message = error_message + "\n" + str(e) stacktrace = traceback.format_exc() if stacktrace: debug_log += "\nFull stacktrace:\n" + stacktrace demisto.error( "EWS Mail Sender failed {}. Error: {}. Debug: {}".format(demisto.command(), error_message, debug_log)) if IS_TEST_MODULE: demisto.results(error_message) else: return_error(error_message + '\n' + debug_log) finally: exchangelib_cleanup() if log_stream: try: logging.getLogger().removeHandler(log_handler) # type: ignore log_stream.close() log_stream = None except Exception as ex: demisto.error("EWS Mail Sender: unexpected exception when trying to remove log handler: {}".format(ex)) # python2 uses __builtin__ python3 uses builtins if __name__ == "__builtin__" or __name__ == "builtins" or __name__ == "__main__": main()