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

IDEWS Mail Sender
ProviderMicrosoft
CategoryEmail
From Version5.0.0
Docker Imagedemisto/py3ews:1.0.0.47270
Supported ModulesAgentix 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 settings
  • impersonation — Has impersonation rights
  • mailbox — Sender Mailbox

Commands (2)

  • reply-mail

    Replies to an email using EWS.

  • send-mail

    Sends 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()