SendEmailReply

Send email reply This automation runs using the default Limited User role, unless you explicitly change the permissions. For more information, see the section about permissions here: - For Cortex XSOAR 6 see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.x/Cortex-XSOAR-Playbook-Design-Guide/Automations - For Cortex XSOAR 8 Cloud see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Create-a-script - For Cortex XSOAR 8.7 On-prem see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Create-a-script

python · Email Communication

Details

IDSendEmailReply
Languagepython
From Version5.0.0
Docker Imagedemisto/bs4-py3:1.0.0.10120494

README

The script is a part of the Email Communication pack.

The script sends the email to the recipient’s list, including the following information:

  • The original email.
  • The name of the sender.
  • The person to reply to.
  • CC addresses, if they exist.
  • BCC addresses, if they exist (Email Threads layout only)
  • Attachments, if they exist.

For the “Email Communication” layout:

  • The email body is taken from the incident ‘notes,’ and the email subject will contain the incident ID and the incident name.

For the “Email Threads” layout:

  • Email message details (recipients, subject, email body, etc.) are set in Incident fields, as well as extracted from any previous messages on the same email thread.
  • Outbound email subjects will include a unique message ID and a custom subject line set when the first message in the thread is created.
  • If you include any Markdown in the body, the script will send it as HTML even if body_type argument is set to “text”. To send the body without applying Markdown formatting, check the Send Body as Raw Text (No Markdown) checkbox.

Permissions


This automation runs using the default Limited User role, unless you explicitly change the permissions.
For more information, see the section about permissions here: For Cortex XSOAR 6, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.x/Cortex-XSOAR-Playbook-Design-Guide/Automations for Cortex XSOAR 8 Cloud, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Create-a-script for Cortex XSOAR 8 On-prem, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Create-a-script.

Script Data


Name Description
Script Type python3
Tags  
Cortex XSOAR Version 0.0.0

Inputs


Argument Name Description
files The context path for files.
attachment The context path for attachments.
service_mail The email address the emails are sent from.
mail_sender_instance Name of the mail sender instance name for transmitting emails
new_thread Specify whether to reply to an existing thread or start a new one. Default value of ‘n/a’ is for ‘Email Communication’ type incidents only
body_type The type of the email body. Can be either HTML or plain text. Default is HTML.
reputation_calc_async Whether to calculate the reputation asynchronously. Default is false.

Outputs


There are no outputs for this script.

Troubleshooting

Multi-tenant environments should be configured with the Cortex Rest API instance when using this
automation. Make sure the Use tenant parameter (in the Cortex Rest API integration) is checked
to ensure that API calls are made to the current tenant instead of the master tenant.

import pytest
from CommonServerPython import *
from freezegun import freeze_time
from SendEmailReply import (
    get_unique_code,
    apply_direction,
    process_directions,
    ensure_markdown_tables_have_spacing,
    replace_atlassian_tags,
)


def util_open_file(path):
    with open(path) as f:
        return f.read()


def util_load_json(path):
    with open(path) as f:
        return json.loads(f.read())


@pytest.mark.parametrize(
    "inputted_body, expected_body",
    [
        pytest.param(
            '<html><body style="font-family: Arial">Formatted HTML message.\r\n</body></html>',
            '<html><body style="font-family: Arial">Formatted HTML message.\r\n\r\nTest email signature.\r\n</body></html>',
            id="HTML Body",
        ),
        pytest.param(
            "<p>Plain text message.</p>",
            "<p>Plain text message.</p>\r\nTest email signature.\r\n",
            id="Plain Text Body",
        ),
    ],
)
def test_append_email_signature(mocker, inputted_body: str, expected_body: str):
    """
    Given
    - Email signature stored in XSOAR List
    When
    - List content is returned correctly
    Then
    - Validate that the returned message includes the appended signature content
    """
    from SendEmailReply import append_email_signature

    signature_list = util_load_json("test_data/getList_signature_success.json")
    mocker.patch.object(demisto, "executeCommand", return_value=signature_list)
    email_body = append_email_signature(inputted_body)
    assert email_body == expected_body


def test_append_email_signature_fails(mocker):
    """
    Given
    - Email signature stored in XSOAR List
    When
    - List content results in error when fetched
    Then
    - Validate that the returned message remains unchanged
    - Validate that a debug message is saved indicating the list couldn't be fetched
    """
    from SendEmailReply import append_email_signature

    get_list_error_response = util_load_json("test_data/getList_signature_error.json")
    mocker.patch.object(demisto, "executeCommand", return_value=get_list_error_response)
    debug_mocker = mocker.patch.object(demisto, "debug")
    html_body = append_email_signature("<html><body>Simple HTML message.\r\n</body></html>")
    debug_mocker_call_args = debug_mocker.call_args
    assert (
        debug_mocker_call_args.args[0] == "Error occurred while trying to load the `XSOAR - Email Communication "
        "Signature` list. No signature added to email. Error: Item not found (8)."
    )
    assert html_body == "<html><body>Simple HTML message.\r\n</body></html>"


@pytest.mark.parametrize(
    "email_cc, email_bcc, expected_result",
    [
        ("", "", "Mail sent successfully. To: test1@gmail.com,test2@gmail.com"),
        ("cc_user@company.com", "", "Mail sent successfully. To: test1@gmail.com,test2@gmail.com Cc: cc_user@company.com"),
        (
            "cc_user@company.com",
            "bcc_user@company.com",
            "Mail sent successfully. To: test1@gmail.com,test2@gmail.com Cc: cc_user@company.com Bcc: bcc_user@company.com",
        ),
    ],
)
def test_validate_email_sent(email_cc, email_bcc, expected_result, mocker):
    """
    Given
    - Raw response of an email reply.
    When
    - The result is a successful reply with email recipients, cc, and bcc.
    Then
    - Validate that the successful message is returned.
    """
    from SendEmailReply import validate_email_sent

    email_reply_response = util_load_json("test_data/email_reply.json")
    mocker.patch("SendEmailReply.execute_reply_mail", return_value=email_reply_response)
    result = validate_email_sent(
        "123",
        "email_subject",
        False,
        "test1@gmail.com,test2@gmail.com",
        "reply_body",
        "html",
        "test.onmicrosoft.com",
        email_cc,
        email_bcc,
        "reply_html_body",
        {},
        "item_id",
        "12345678",
        "test.onmicrosoft.com",
        "test.from.mail",
    )
    assert result == expected_result


def test_validate_email_sent_fails(mocker):
    """
    Given -
        a random error message which is returned from reply-mail executed command.
    When -
        executing the 'send_reply' function
    Then -
        an error message would be returned.
    """
    from SendEmailReply import validate_email_sent

    False, get_error(util_load_json("test_data/reply_mail_error.json"))
    mocker.patch("SendEmailReply.execute_command", side_effect=Exception())
    mocker.patch.object(demisto, "debug")
    return_error_mock = mocker.patch("SendEmailReply.return_error")
    with pytest.raises(Exception):
        validate_email_sent("", "", False, "", "", "html", "", "", "", "", {}, "", "", "", "")
    assert return_error_mock.call_count == 1
    assert (
        return_error_mock.call_args[0][0]
        == 'SetIncident Failed."emailsubject" field was not updated with <>  value for incident: '
    )


@pytest.mark.parametrize(
    "test_args, expected_response",
    [
        (
            (
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Reply body.",
                "html",
                "soc_sender@company.com",
                "cc_user@company.com",
                "bcc_user@company.com",
                "<html><body>Reply body.</body></html",
                ["10", "12"],
                "5",
                "12345678",
                "soc_sender@company.com",
                "from_mail",
            ),
            {
                "to": "end_user@company.com",
                "inReplyTo": "5",
                "subject": "<12345678> Email Subject",
                "cc": "cc_user@company.com",
                "from": "from_mail",
                "bcc": "bcc_user@company.com",
                "htmlBody": "<html><body>Reply body.</body></html",
                "body": "Reply body.",
                "attachIDs": "10,12",
                "replyTo": "soc_sender@company.com",
                "using": "soc_sender@company.com",
                "bodyType": "html",
            },
        ),
        (
            (
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Reply body.",
                "html",
                "soc_sender@company.com",
                "cc_user@company.com",
                "bcc_user@company.com",
                "<html><body>Reply body.</body></html",
                ["10", "12"],
                "5",
                "12345678",
                "",
                "",
            ),
            {
                "to": "end_user@company.com",
                "inReplyTo": "5",
                "subject": "<12345678> Email Subject",
                "cc": "cc_user@company.com",
                "bcc": "bcc_user@company.com",
                "htmlBody": "<html><body>Reply body.</body></html",
                "body": "Reply body.",
                "attachIDs": "10,12",
                "replyTo": "soc_sender@company.com",
                "bodyType": "html",
            },
        ),
        (
            (
                2,
                "Email Subject",
                True,
                "end_user@company.com",
                "Reply body.",
                "html",
                "soc_sender@company.com",
                "cc_user@company.com",
                "bcc_user@company.com",
                "<html><body>Reply body.</body></html",
                ["10", "12"],
                "5",
                "12345678",
                "soc_sender@company.com",
                "from_mail",
            ),
            {
                "to": "end_user@company.com",
                "inReplyTo": "5",
                "subject": "<12345678> [2] Email Subject",
                "cc": "cc_user@company.com",
                "from": "from_mail",
                "bcc": "bcc_user@company.com",
                "htmlBody": "<html><body>Reply body.</body></html",
                "body": "Reply body.",
                "attachIDs": "10,12",
                "replyTo": "soc_sender@company.com",
                "using": "soc_sender@company.com",
                "bodyType": "html",
            },
        ),
        (
            (
                2,
                "Email Subject",
                True,
                "end_user@company.com",
                "Reply body.",
                "html",
                "soc_sender@company.com",
                "cc_user@company.com",
                "bcc_user@company.com",
                "<html><body>Reply body.</body></html",
                ["10", "12"],
                "5",
                "12345678",
                "",
                "",
            ),
            {
                "to": "end_user@company.com",
                "inReplyTo": "5",
                "subject": "<12345678> [2] Email Subject",
                "cc": "cc_user@company.com",
                "bcc": "bcc_user@company.com",
                "htmlBody": "<html><body>Reply body.</body></html",
                "body": "Reply body.",
                "attachIDs": "10,12",
                "replyTo": "soc_sender@company.com",
                "bodyType": "html",
            },
        ),
    ],
)
def test_execute_reply_mail(test_args, expected_response, mocker):
    """Unit Test
    Given
    - function is called to send an email reply
    When
    - All input arguments are correctly set
    Then
    - Validate that 'reply-mail' command is called and supplied with a correctly formatted set of mail content
    """
    import SendEmailReply
    from SendEmailReply import execute_reply_mail

    execute_command_mocker = mocker.patch.object(demisto, "executeCommand", return_value=True)
    mocker.patch.object(SendEmailReply, "return_error", return_value=True)
    execute_reply_mail(*test_args)
    execute_command_call_args = execute_command_mocker.call_args
    assert execute_command_call_args.args[1] == expected_response


GET_EMAIL_RECIPIENTS = [
    # Both service mail and mailbox are configured as different addresses, should remove only mailbox.
    (
        '["avishai@demistodev.onmicrosoft.com", "test test <\'test@test.com\'>"]',
        "test123@gmail.com",
        "avishai@demistodev.onmicrosoft.com",
        "test@test.com",
        {"test123@gmail.com", "avishai@demistodev.onmicrosoft.com"},
    ),
    # Only mailbox is configured, should be removed.
    (
        '["avishai@demistodev.onmicrosoft.com", "test test <\'test@test.com\'>"]',
        "test123@gmail.com",
        "",
        "test@test.com",
        {"test123@gmail.com", "avishai@demistodev.onmicrosoft.com"},
    ),
    # Only service mail is configured, should be removed.
    (
        '["avishai@demistodev.onmicrosoft.com", "test1@gmail.com"]',
        "test123@gmail.com",
        "avishai@demistodev.onmicrosoft.com",
        "",
        {"test123@gmail.com", "test1@gmail.com"},
    ),
    # Neither service mail nor mailbox is configured, make sure nothing is removed.
    (
        '["avishai@demistodev.onmicrosoft.com", "test1@gmail.com"]',
        "test123@gmail.com",
        "",
        "",
        {"test123@gmail.com", "test1@gmail.com", "avishai@demistodev.onmicrosoft.com"},
    ),
    # Mailbox is configured with different capitalization, but should still be removed.
    (
        '["avishai@demistodev.onmicrosoft.com", "test1@gmail.com"]',
        "test123@gmail.com",
        "",
        "AVISHAI@DEMISTODEV.ONMICROSOFT.COM",
        {"test123@gmail.com", "test1@gmail.com"},
    ),
    # Service mail is configured with different capitalization, but should still be removed.
    (
        '["avishai@demistodev.onmicrosoft.com", "test1@gmail.com"]',
        "test123@gmail.com",
        "AVISHAI@DEMISTODEV.ONMICROSOFT.COM",
        "",
        {"test123@gmail.com", "test1@gmail.com"},
    ),
]


@pytest.mark.parametrize("email_to, email_from, service_mail, mailbox, excepted", GET_EMAIL_RECIPIENTS)
def test_get_email_recipients(email_to, email_from, service_mail, mailbox, excepted):
    """Unit test
    Given
    - Single email recipient, single email author, service mail and mailbox.
    - Multiple email recipients, single email author, service mail and mailbox.
    When
    - Getting the email recipients.
    Then
    - validate that the correct email recipients are returned.
    """
    from SendEmailReply import get_email_recipients

    result = set(get_email_recipients(email_to, email_from, service_mail, mailbox).split(","))
    assert result == excepted


@pytest.mark.parametrize(
    "list_response, expected_result",
    [
        (util_load_json("test_data/getList_querywindow_success.json"), "success"),
        (util_load_json("test_data/getList_querywindow_error.json"), "fail"),
    ],
)
def test_get_query_window(list_response, expected_result, mocker):
    """
    Unit Test Scenario 1 - List exists
        Given
        - Query window value stored in XSOAR List
        When
        - List content is returned successfully
        Then
        - Validate that the function returns the correct window based on the list response
    Unit Test Scenario 2 - List retrieval fails
        Given
        - Query window value stored in XSOAR List
        When
        - List retrieval results in an error
        Then
        - Validate that the function returns the default '60 days' window
        - Validate that a debug message is saved indicating the list couldn't be fetched
    """
    from SendEmailReply import get_query_window

    mocker.patch.object(demisto, "executeCommand", return_value=list_response)
    debug_mocker = mocker.patch.object(demisto, "debug")
    result = get_query_window()
    debug_mocker_call_args = debug_mocker.call_args
    if expected_result == "success":
        assert result == "90 days"
    elif expected_result == "fail":
        assert result == "60 days"
        assert (
            debug_mocker_call_args.args[0] == "Error occurred while trying to load the `XSOAR - Email "
            "Communication Days To Query` list. Using the default query time - "
            "60 days"
        )


@pytest.mark.parametrize(
    "notes, attachments, expected_results",
    [
        (
            [{"Metadata": {"user": "DBot"}, "Contents": "note1"}, {"Metadata": {"user": "DBot"}, "Contents": "note2"}],
            [{"name": "attachment1.png"}, {"name": "attachment2.png"}],
            "DBot: \n\nnote1\n\nDBot: \n\nnote2\n\nAttachments: ['attachment1.png', 'attachment2.png']\n\n",
        ),
        (
            [{"Metadata": {"user": "DBot"}, "Contents": "note1"}, {"Metadata": {"user": "DBot"}, "Contents": "note2"}],
            [],
            "DBot: \n\nnote1\n\nDBot: \n\nnote2\n\n",
        ),
        (
            [{"Metadata": {"user": "DBot"}, "Contents": "note1"}, {"Metadata": {"user": "DBot"}, "Contents": "note2"}],
            "[]",
            "DBot: \n\nnote1\n\nDBot: \n\nnote2\n\n",
        ),
    ],
)
def test_get_reply_body(mocker, notes, attachments, expected_results):
    """Unit test
    Given
    - List of notes and list of attachments.
    When
    - Getting the email reply body.
    Then
    - validate that the correct reply is returned.
    """
    import CommonServerPython
    from SendEmailReply import get_reply_body

    mocker.patch.object(demisto, "executeCommand", return_value=[{"EntryContext": {"replyhtmlbody": ""}, "Type": ""}])
    mocker.patch.object(CommonServerPython, "dict_safe_get", return_value=None)
    mocker.patch.object(CommonServerPython, "is_error", return_value=False)
    reply_body = get_reply_body(notes=notes, incident_id="1", attachments=attachments)[0]
    assert reply_body == expected_results


def test_create_file_data_json():
    """Unit test
    Given
    - Raw response of an attachment in email reply.
    When
    - There is an attachment in the email reply.
    Then
    - Validate that the file data is in the right json format.
    """
    from SendEmailReply import create_file_data_json

    attachment_response = util_load_json("test_data/attachment_example.json")
    expected_result = util_open_file("test_data/file_data.txt")
    result = create_file_data_json(attachment_response, "attachment")
    assert result == expected_result


@pytest.mark.parametrize(
    "current_cc, additional_cc, excepted",
    [
        (
            "test1@gmail.com, test2@gmail.com",
            "test3@gmail.com,test4@gmail.com",
            "test1@gmail.com,test2@gmail.com,test3@gmail.com,test4@gmail.com",
        ),
        ("test1@gmail.com", "", "test1@gmail.com"),
        ("", "", ""),
    ],
)
def test_get_email_cc(current_cc, additional_cc, excepted):
    """Unit test
    Given
    - multiple current email cc and multiple additional email cc.
    - single current email cc and empty additional email cc.
    - empty current email cc and empty additional email cc.
    When
    - Getting email cc.
    Then
    - validate that the correct email cc are being returned.
    """
    from SendEmailReply import get_email_cc

    result = get_email_cc(current_cc, additional_cc)
    assert result == excepted


def test_get_email_threads(mocker):
    """Unit Test
    Given
    - Function is called to fetch email threads from current incident
    When
    - Context contains email threads
    Then
    - Validate that function returns email thread data
    """
    import SendEmailReply
    from SendEmailReply import get_email_threads

    email_threads = util_load_json("test_data/email_threads.json")
    mocker.patch.object(demisto, "executeCommand")
    mocker.patch.object(SendEmailReply, "dict_safe_get", return_value=email_threads)
    result = get_email_threads("1")
    assert result == email_threads


@pytest.mark.parametrize(
    "email_code, email_threads, scenario",
    [
        ("69433507", util_load_json("test_data/email_threads.json"), "thread_found"),
        ("123", util_load_json("test_data/email_threads.json"), "thread_notfound"),
        (
            "69433507",
            [
                {
                    "EmailCommsThreadId": "69433507",
                    "EmailCommsThreadNumber": "0",
                    "EmailCC": "cc_user@company.com",
                    "EmailBCC": "bcc_user@company.com",
                    "EmailBody": "Email body.",
                    "EmailFrom": "soc_sender@company.com",
                    "EmailHTML": "<html><body>Email body.</body></html>",
                    "MessageID": "5",
                    "EmailReceived": "soc_sender@company.com",
                    "EmailReplyTo": "soc_sender@company.com",
                    "EmailSubject": "Email Subject",
                    "EmailTo": "end_user@company.com",
                    "EmailAttachments": "None",
                    "MessageDirection": "outbound",
                    "MessageTime": "2022-02-04T20:56:53UTC",
                }
            ],
            "thread_found",
        ),
    ],
)
def test_create_thread_context(email_code, email_threads, scenario, mocker):
    """Unit test
    Given:
    - all required function arguments are provided
    When:
    - creating new context entry to store email thread data
    Then
    - validate that function calls appendContext() with all arguments and data needed to properly create
      the required context entry
    """
    import SendEmailReply
    from SendEmailReply import create_thread_context

    # Mock function to get current time string to match the expected result
    mocker.patch(
        "SendEmailReply.get_current_time_in_timezone",
        return_value="2022-02-04T20:58:20UTC",
    )
    mocker.patch.object(SendEmailReply, "get_email_threads", return_value=email_threads)
    append_context_mocker = mocker.patch.object(SendEmailReply, "appendContext", return_value=True)
    create_thread_context(
        email_code,
        "cc_user@company.com",
        "bcc_user@company.com",
        "Email body.",
        "soc_sender@company.com",
        "<html>body><Email body.</body></html>",
        "10",
        "soc_sender@company.com",
        "soc_sender@company.com",
        "Email Subject",
        "end_user@company.com",
        "123",
        "",
        time_zone="UTC",
    )
    call_args = append_context_mocker.call_args
    if scenario == "thread_found":
        expected = {
            "EmailCommsThreadId": "69433507",
            "EmailCommsThreadNumber": "0",
            "EmailCC": "cc_user@company.com",
            "EmailBCC": "bcc_user@company.com",
            "EmailBody": "Email body.",
            "EmailFrom": "soc_sender@company.com",
            "EmailHTML": "<html>body><Email body.</body></html>",
            "MessageID": "10",
            "EmailReceived": "soc_sender@company.com",
            "EmailReplyTo": "soc_sender@company.com",
            "EmailSubject": "Email Subject",
            "EmailTo": "end_user@company.com",
            "EmailAttachments": "",
            "MessageDirection": "outbound",
            "MessageTime": "2022-02-04T20:58:20UTC",
        }

        assert call_args.args[1] == expected
    elif scenario == "thread_notfound":
        expected = {
            "EmailCommsThreadId": "123",
            "EmailCommsThreadNumber": "2",
            "EmailCC": "cc_user@company.com",
            "EmailBCC": "bcc_user@company.com",
            "EmailBody": "Email body.",
            "EmailFrom": "soc_sender@company.com",
            "EmailHTML": "<html>body><Email body.</body></html>",
            "MessageID": "10",
            "EmailReceived": "soc_sender@company.com",
            "EmailReplyTo": "soc_sender@company.com",
            "EmailSubject": "Email Subject",
            "EmailTo": "end_user@company.com",
            "EmailAttachments": "",
            "MessageDirection": "outbound",
            "MessageTime": "2022-02-04T20:58:20UTC",
        }
        assert call_args.args[1] == expected


@pytest.mark.parametrize(
    "test_args, expected_result, expected_message",
    [
        (
            (  # Test arguments for send_new_email
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Email Body",
                "soc_sender@company.com",
                "",
                "",
                "<html><body>Email Body</body></html>",  # email_html_body
                "html",
                [],
                "12345678",
                "soc_sender@company.com",
                "attachment.txt",
                "<html><body>Email Body</body></html>",  # context_html_body
                "from.mail",
                "UTC",
            ),
            (  # Expected result for send_new_mail_request
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Email Body",
                "soc_sender@company.com",
                "",
                "",
                "Email Body + Signature",  # Updated email_html_body
                "html",
                [],
                "attachment.txt",
                "12345678",
                "soc_sender@company.com",
                "Email Body + Signature",  # Updated context_html_body
                "from.mail",
                "UTC",
            ),
            "Mail sent successfully. To: end_user@company.com",
        ),
        (  # Test arguments for send_new_email
            (
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Email Body",
                "soc_sender@company.com",
                "cc_user@company.com",
                "",
                "<html><body>Email Body</body></html>",
                "html",
                [],
                "12345678",
                "soc_sender@company.com",
                "attachment.txt",
                "<html><body>Email Body</body></html>",
                "from.mail",
                "UTC",
            ),
            (  # Expected result for send_new_mail_request
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Email Body",
                "soc_sender@company.com",
                "cc_user@company.com",
                "",
                "Email Body + Signature",
                "html",
                [],
                "attachment.txt",
                "12345678",
                "soc_sender@company.com",
                "Email Body + Signature",
                "from.mail",
                "UTC",
            ),
            "Mail sent successfully. To: end_user@company.com Cc: cc_user@company.com",
        ),
        (  # Test arguments for send_new_email
            (
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Email Body",
                "soc_sender@company.com",
                "",
                "bcc_user@company.com",
                "<html><body>Email Body</body></html>",
                "html",
                [],
                "12345678",
                "soc_sender@company.com",
                "attachment.txt",
                "<html><body>Email Body</body></html>",
                "from_mail",
                "UTC",
            ),
            (  # Expected result for send_new_mail_request
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Email Body",
                "soc_sender@company.com",
                "",
                "bcc_user@company.com",
                "Email Body + Signature",
                "html",
                [],
                "attachment.txt",
                "12345678",
                "soc_sender@company.com",
                "Email Body + Signature",
                "from_mail",
                "UTC",
            ),
            "Mail sent successfully. To: end_user@company.com Bcc: bcc_user@company.com",
        ),
    ],
)
def test_send_new_email(test_args, expected_result, expected_message, mocker):
    """Unit test
    Given:
    - All required function arguments are provided
    When:
    - Appending email signature and calling send_new_mail_request function
    Then
    - Validate that the email signature is appended to 'email_html_body' and arguments are correctly
      passed to 'send_new_mail_request'
    """
    import SendEmailReply
    from SendEmailReply import send_new_email

    mocker.patch.object(SendEmailReply, "append_email_signature", return_value="Email Body + Signature")
    send_new_mail_request_mocker = mocker.patch.object(
        SendEmailReply, "send_new_mail_request", return_value=[{"Contents": "Success", "Type": 1}]
    )
    message = send_new_email(*test_args)
    send_new_mail_request_args = send_new_mail_request_mocker.call_args
    assert message == expected_message
    assert send_new_mail_request_args.args == expected_result


@pytest.mark.parametrize(
    "email_selected_thread, email_thread, expected_result",
    [
        ("0", util_load_json("test_data/email_threads.json")[0], "success"),
        ("42", util_load_json("test_data/email_threads.json")[0], "fail"),
    ],
)
def test_resend_first_contact(email_selected_thread, email_thread, expected_result, mocker):
    """Unit Test
    Given
    - function is called to re-send a first-contact email message
    When
    - All input arguments are  set
    Then
    - Validate that 'send_new_email' is called if selected thread is found
    - Validate that an error is returned if selected thread is not found
    """
    import SendEmailReply
    from SendEmailReply import resend_first_contact

    mocker.patch.object(SendEmailReply, "get_entry_id_list", return_value=["5", "10"])
    mocker.patch.object(
        SendEmailReply,
        "format_body",
        return_value=("<html><body>Resending email.</body></html>", "<html><body>Resending email.</body></html>"),
    )
    mocker.patch.object(SendEmailReply, "get_email_cc", return_value="")
    send_new_email_mocker = mocker.patch.object(SendEmailReply, "send_new_email", return_value=True)
    return_error_mocker = mocker.patch.object(SendEmailReply, "return_error", return_value=True)
    result = resend_first_contact(
        email_selected_thread,
        email_thread,
        1,
        "",
        "",
        "Resending email.",
        "html",
        "",
        "",
        "soc_sender@company.com",
        "soc_sender@company.com",
        "",
        False,
        "",
        time_zone="UTC",
    )
    send_new_email_args = send_new_email_mocker.call_args
    return_error_args = return_error_mocker.call_args
    if expected_result == "success":
        assert result is True
        assert send_new_email_args.args == (
            1,
            "<69433507> Test Email 2",
            False,
            "end_user@company.com",
            "Resending email.",
            "soc_sender@company.com",
            "",
            "",
            "<html><body>Resending email.</body></html>",
            "html",
            ["5", "10"],
            "69433507",
            "soc_sender@company.com",
            "",
            "<html><body>Resending email.</body></html>",
            "",
            "UTC",
        )
    if expected_result == "fail":
        assert return_error_args.args[0] == (
            "The selected Thread Number to respond to (42) does not exist.  Please choose a valid Thread Number and re-try."
        )


@pytest.mark.parametrize("email_code", ["12345678", ""])
def test_single_thread_reply(email_code, mocker):
    """Unit Test
    Given
    - function is called to send a new email message
    When
    - All input arguments are correctly set
    Then
    - Validate that 'validate_email_sent' function is called and supplied with a correctly formatted set of mail content
    - Validate that if no email_code is provided, 'get_unique_code' is called to generate one
    """

    def get_reply_body_side_effect(notes, incident_id, attachments, reputation_calc_async):  # noqa
        return "Email body.", "<html><body>Email body.</body></html>", "<html><body>Email body.</body></html>"

    import SendEmailReply
    from SendEmailReply import single_thread_reply

    mocker.patch.object(SendEmailReply, "append_email_signature", side_effect=lambda body: body + " - signature")
    mocker.patch.object(SendEmailReply, "get_unique_code", return_value="12345678")
    execute_command_mocker = mocker.patch.object(demisto, "executeCommand", return_value=True)
    mocker.patch.object(SendEmailReply, "get_entry_id_list", return_value=["5", "10"])
    mocker.patch.object(SendEmailReply, "get_email_cc", return_value="")
    mocker.patch.object(SendEmailReply, "get_reply_body", side_effect=get_reply_body_side_effect)
    validate_email_sent_mocker = mocker.patch.object(SendEmailReply, "validate_email_sent", return_value=True)
    single_thread_reply(
        email_code,
        1,
        "cc_user@company.com",
        "",
        ["5"],
        "html",
        "",
        "",
        "Email Subject",
        False,
        "end_user@company.com",
        "soc_sender@company.com",
        10,
        "soc_sender@company.com",
        "",
    )
    validate_mail_sent_call_args = validate_email_sent_mocker.call_args
    execute_command_call_args = execute_command_mocker.call_args
    assert validate_mail_sent_call_args.args == (
        1,
        "Email Subject",
        False,
        "end_user@company.com",
        "Email body.",
        "html",
        "soc_sender@company.com",
        "",
        "",
        "<html><body>Email body.</body></html> - signature",  # The expected value is now a signed body
        ["5", "10"],
        10,
        "12345678",
        "soc_sender@company.com",
        "",
    )
    if not email_code:
        assert execute_command_call_args.args == ("setIncident", {"id": 1, "customFields": {"emailgeneratedcode": "12345678"}})


@pytest.mark.parametrize(
    "test_args, expected_result",
    [
        (
            (
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Message body.",
                "soc_sender@company.com",
                "cc_user@company.com",
                "bcc_user@company.com",
                "<html><body>Reply body.</body></html",
                "html",
                ["10", "12"],
                "",
                "12345678",
                "soc_sender@company.com",
                "",
                "",
                "UTC",
            ),
            {
                "to": "end_user@company.com",
                "subject": "<12345678> Email Subject",
                "cc": "cc_user@company.com",
                "bcc": "bcc_user@company.com",
                "htmlBody": "<html><body>Reply body.</body></html",
                "body": "Message body.",
                "bodyType": "html",
                "attachIDs": "10,12",
                "replyTo": "soc_sender@company.com",
                "using": "soc_sender@company.com",
            },
        ),
        (
            (
                1,
                "Email Subject",
                False,
                "end_user@company.com",
                "Message body.",
                "soc_sender@company.com",
                "cc_user@company.com",
                "bcc_user@company.com",
                "<html><body>Reply body.</body></html",
                "html",
                ["10", "12"],
                "",
                "12345678",
                "",
                "",
                "from.mail",
                "UTC",
            ),
            {
                "to": "end_user@company.com",
                "subject": "<12345678> Email Subject",
                "cc": "cc_user@company.com",
                "bcc": "bcc_user@company.com",
                "htmlBody": "<html><body>Reply body.</body></html",
                "body": "Message body.",
                "bodyType": "html",
                "attachIDs": "10,12",
                "replyTo": "soc_sender@company.com",
                "from": "from.mail",
            },
        ),
    ],
)
def test_send_new_mail_request(test_args, expected_result, mocker):
    """Unit Test
    Given
    - function is called to send a new email message
    When
    - All input arguments are correctly set
    Then
    - Validate that 'send-mail' command is called and supplied with a correctly formatted set of mail content
    """
    import SendEmailReply
    from SendEmailReply import send_new_mail_request

    execute_command_mocker = mocker.patch.object(demisto, "executeCommand", return_value="Success")
    mocker.patch.object(SendEmailReply, "return_error", return_value=True)
    mocker.patch.object(SendEmailReply, "create_thread_context", return_value=True)
    result = send_new_mail_request(*test_args)
    execute_command_call_args = execute_command_mocker.call_args
    assert execute_command_call_args.args[1] == expected_result
    assert result == "Success"


@pytest.mark.parametrize("scenario", ["required_fields_missing", "no_codes_present", "codes_present"])
def test_multi_thread_new(scenario, mocker):
    """
    Unit test scenario 1 - Required fields are missing
        Given
         - Script is called to send a new first-contact email message
        When
         - Required fields 'new_email_subject', 'new_email_recipients', and 'new_email_body' are not provided
        Then
         - Validate that the function returns a meaningful error message
    Unit test scenario 2 - No other email identifier codes are present
        Given
         - Script is called to send a new first-contact email message
        When
         - All required fields are provided
         - There are no prior email identifier codes present on the incident
        Then
         - Validate that the function sets 'emailgeneratedcodes' to the new identifier value
         - Validate that the function calls 'send_reply' with correct arguments set
         - Validate that 'reset_fields' is called
    Unit test scenario 3 - Incident already contains other unique identifier codes for other threads
        Given
         - Script is called to send a new first-contact email message
        When
         - All required fields are provided
         - There are at least one prior email identifier codes present on the incident
        Then
         - Validate that the function sets 'emailgeneratedcodes' with the new code appended to the prior one
         - Validate that the function calls 'send_reply' with correct arguments set
         - Validate that 'reset_fields' is called
    """
    import SendEmailReply
    from SendEmailReply import multi_thread_new

    return_error_mocker = mocker.patch.object(SendEmailReply, "return_error", return_value=True)
    mocker.patch.object(SendEmailReply, "get_unique_code", return_value="87654321")
    set_incident_mocker = mocker.patch.object(demisto, "executeCommand", return_value=True)
    mocker.patch.object(SendEmailReply, "get_entry_id_list", return_value=[])
    mocker.patch.object(SendEmailReply, "format_body", return_value=("<html>Some HTML</html>", "<html>Some HTML</html>"))
    send_new_email_mocker = mocker.patch.object(SendEmailReply, "send_new_email", return_value=True)
    reset_fields_mocker = mocker.patch.object(SendEmailReply, "reset_fields", return_value=True)
    if scenario == "required_fields_missing":
        # Test Scenario 1
        expected = (
            "The following required fields have not been set. Please set them and try again. "
            "['New Email Subject', 'New Email Recipients', 'New Email Body']"
        )
        multi_thread_new(
            "",
            False,
            "",
            "",
            "html",
            1,
            "12345678",
            "",
            "",
            "soc_sender@company.com",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "",
            "",
            time_zone="UTC",
        )
        call_args = return_error_mocker.call_args
        assert call_args.args[0] == expected
    if scenario == "no_codes_present":
        # Test Scenario 2
        multi_thread_new(
            "New Subject",
            False,
            "end_user@company.com",
            "Email Body",
            "html",
            1,
            "",
            "",
            "",
            "soc_sender@company.com",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "",
            "",
            time_zone="UTC",
        )
        set_incident_call_args = set_incident_mocker.call_args
        send_new_email_mocker_args = send_new_email_mocker.call_args
        assert set_incident_call_args.args[1] == {"id": 1, "customFields": {"emailgeneratedcodes": "87654321"}}
        valid_args = (
            1,
            "New Subject",
            False,
            "end_user@company.com",
            "Email Body",
            "soc_sender@company.com",
            "cc_user@company.com",
            "bcc_user@company.com",
            "<html>Some HTML</html>",
            "html",
            [],
            "87654321",
            "soc_sender@company.com",
            "",
            "<html>Some HTML</html>",
            "",
            "UTC",
        )
        assert send_new_email_mocker_args.args == valid_args
        assert reset_fields_mocker.called is True
    if scenario == "codes_present":
        # Test Scenario 3
        multi_thread_new(
            "New Subject",
            False,
            "end_user@company.com",
            "Email Body",
            "html",
            1,
            "12345678",
            "",
            "",
            "soc_sender@company.com",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "",
            "",
            time_zone="UTC",
        )
        set_incident_call_args = set_incident_mocker.call_args
        send_new_email_mocker_args = send_new_email_mocker.call_args
        assert set_incident_call_args.args[1] == {"id": 1, "customFields": {"emailgeneratedcodes": "12345678,87654321"}}
        valid_args = (
            1,
            "New Subject",
            False,
            "end_user@company.com",
            "Email Body",
            "soc_sender@company.com",
            "cc_user@company.com",
            "bcc_user@company.com",
            "<html>Some HTML</html>",
            "html",
            [],
            "87654321",
            "soc_sender@company.com",
            "",
            "<html>Some HTML</html>",
            "",
            "UTC",
        )
        assert send_new_email_mocker_args.args == valid_args
        assert reset_fields_mocker.called is True


def test_collect_thread_details():
    """
    Unit test scenario
        Given
         - Function is called to gather details from existing email thread
        When
         - Multiple emails are present on the thread
        Then
         - Validate that the function returns the correct email details for use in an email reply
    """
    from SendEmailReply import collect_thread_details

    expected = (
        True,
        "AAMkAGRcOGZlZTEzLTkyZGDtNGJkNy1iOWMxLYM0NTAwODZhZjlxNABGAAAAAAAP2ksrJ8icRL4Zhadm7iVXBwAkkBJXBb0"
        "sRJWC0zdXEMqsAAAAAAEMAAAkkBJFBb0fRJWC0zdXEMqsABApcWVYAAA=",
        False,
        "87692312",
        "Re: <87692312> Test Email 4",
        "end_user@company.com, soc_sender@company.com",
        "soc_sender@company.com",
        "",
        "",
        3,
    )
    actual = collect_thread_details(util_load_json("test_data/email_threads.json"), "1")
    assert actual == expected


@pytest.mark.parametrize("scenario", ["single_outbound", "multiple_outbound", "replies_present"])
def test_multi_thread_reply(scenario, mocker):
    """
    Unit test scenario 1 - Resend First-Contact - Single Outbound
        Given
         - Function is called to send a reply to an existing email thread
        When
         - The selected thread contains only an outbound message (no replies from recipients)
        Then
         - Validate that the function calls 'resend_first_contact' to send the message as a new outbound email
         - Validate that 'first_contact_resent' is set to True
    Unit test scenario 2 - Resend First-Contact - Multiple Outbound
        Given
         - Function is called to send a reply to an existing email thread
        When
         - The selected thread contains multiple outbound messages (no replies from recipients)
        Then
         - Validate that the function calls 'resend_first_contact' to send the message as a new outbound email
         - Validate that 'first_contact_resent' is set to True
    Unit test scenario 3 - Replies Present
        Given
         - Function is called to send a reply to an existing email thread
        When
         - The selected thread contains both outbound and inbound messages (there are replies from recipients)
        Then
         - Validate that the function calls 'send_reply' to reply to existing email thread
         - Validate that the function calls 'create_thread_context' with correct arguments
         - Validate that 'first_contact_resent' is set to False
         - Validate that 'reset_fields' is called
    """
    import SendEmailReply
    from SendEmailReply import multi_thread_reply

    email_threads = util_load_json("test_data/email_threads.json")
    resend_first_contact_mocker = mocker.patch.object(SendEmailReply, "resend_first_contact", return_value=True)
    mocker.patch.object(SendEmailReply, "reset_fields", return_value=True)
    mocker.patch.object(SendEmailReply, "return_results", return_value=True)
    mocker.patch.object(
        SendEmailReply,
        "format_body",
        return_value=("<html><body>Email body</body></html>", "<html><body>Email body</body></html>"),
    )
    mocker.patch.object(SendEmailReply, "append_email_signature", return_value="<html><body>Email body+signature</body></html>")
    reset_fields_mocker = mocker.patch.object(SendEmailReply, "reset_fields", return_value=True)
    validate_email_sent_mocker = mocker.patch.object(SendEmailReply, "validate_email_sent", return_value=True)
    create_context_mocker = mocker.patch.object(SendEmailReply, "create_thread_context", return_value=True)

    if scenario == "single_outbound":
        # Return only a single email thread entry
        mocker.patch.object(SendEmailReply, "get_email_threads", return_value=email_threads[0])
        multi_thread_reply(
            "Email body",
            "html",
            1,
            0,
            "",
            "",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "soc_sender@company.com",
            "",
            False,
            "",
            time_zone="UTC",
        )
        expected = (
            0,
            {
                "EmailBCC": "",
                "EmailBody": "Outbound test message from XSOAR to User.",
                "EmailCC": "",
                "EmailCommsThreadId": "69433507",
                "EmailCommsThreadNumber": "0",
                "EmailFrom": "soc_sender@company.com",
                "EmailHTML": "Outbound test message from XSOAR to User.",
                "EmailReceived": "",
                "EmailReplyTo": "soc_sender@company.com",
                "EmailSubject": "<69433507> Test Email 2",
                "EmailTo": "end_user@company.com",
                "EmailAttachments": "None",
                "MessageDirection": "outbound",
                "MessageID": "",
                "MessageTime": "2022-02-04T20:56:53UTC",
            },
            1,
            "",
            "",
            "Email body",
            "html",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "soc_sender@company.com",
            "",
            False,
            "",
            "UTC",
        )
        resend_first_contact_call_args = resend_first_contact_mocker.call_args
        assert resend_first_contact_call_args.args == expected
        assert reset_fields_mocker.called is True
    elif scenario == "multiple_outbound":
        # Return all email thread entries
        mocker.patch.object(SendEmailReply, "get_email_threads", return_value=email_threads)
        thread_details = (
            True,
            "AAMkAGRcOGZlZTEzLTkyZGDtNGJkNy1iOWMxLYM0NTAwODZhZjlxNABGAAAAAAAP2ksrJ8icRL4Zhadm7iVXBwAkkBJXBb0"
            "sRJWC0zdXEMqsAAAAAAEMAAAkkBJFBb0fRJWC0zdXEMqsABApcWVYAAA=",
            True,
            "69433507",
            "Re: <69433507> Test Email 2",
            "end_user@company.com, soc_sender@company.com",
            "soc_sender@company.com",
            "",
            "",
            2,
        )
        mocker.patch.object(SendEmailReply, "collect_thread_details", return_value=thread_details)

        # Execute the tested function
        multi_thread_reply(
            "Email body",
            "html",
            1,
            0,
            "",
            "",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "soc_sender@company.com",
            "",
            False,
            "",
            time_zone="UTC",
        )

        expected = (
            0,
            {
                "EmailBCC": "",
                "EmailBody": "Outbound test message from XSOAR to User.",
                "EmailCC": "",
                "EmailCommsThreadId": "87692312",
                "EmailCommsThreadNumber": "1",
                "EmailFrom": "soc_sender@company.com",
                "EmailHTML": "Outbound test message from XSOAR to User.",
                "EmailReceived": "",
                "EmailReplyTo": "soc_sender@company.com",
                "EmailSubject": "<87692312> Test Email 4",
                "EmailTo": "end_user@company.com",
                "EmailAttachments": "None",
                "MessageDirection": "outbound",
                "MessageID": "",
                "MessageTime": "2022-02-04T20:56:53UTC",
            },
            1,
            "",
            "",
            "Email body",
            "html",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "soc_sender@company.com",
            "",
            False,
            "",
            "UTC",
        )
        resend_first_contact_call_args = resend_first_contact_mocker.call_args
        assert resend_first_contact_call_args.args == expected
        assert reset_fields_mocker.called is True
    elif scenario == "replies_present":
        thread_details = (
            True,
            "AAMkAGRcOGZlZTEzLTkyZGDtNGJkNy1iOWMxLYM0NTAwODZhZjlxNABGAAAAAAAP2ksrJ8icRL4Zhadm7iVXBwAkkBJXBb0"
            "sRJWC0zdXEMqsAAAAAAEMAAAkkBJFBb0fRJWC0zdXEMqsABApcWVYAAA=",
            False,
            "87692312",
            "RE: <87692312> Test Email 4",
            "end_user@company.com, soc_sender@company.com",
            "soc_sender@company.com",
            "",
            "",
            2,
        )
        mocker.patch.object(SendEmailReply, "collect_thread_details", return_value=thread_details)
        # Return all email thread entries
        mocker.patch.object(SendEmailReply, "get_email_threads", return_value=email_threads)
        validate_email_sent_expected = (
            1,
            "<87692312> Test Email 4",
            False,
            "end_user@company.com",
            "Email body",
            "html",
            "soc_sender@company.com",
            "cc_user@company.com",
            "bcc_user@company.com",
            "<html><body>Email body+signature</body></html>",
            [],
            "AAMkAGRcOGZlZTEzLTkyZGDtNGJkNy1iOWMxLYM0NTAwODZhZjlxNABGAAAAAAAP2ksrJ8icRL4Zha"
            "dm7iVXBwAkkBJXBb0sRJWC0zdXEMqsAAAAAAEMAAAkkBJFBb0fRJWC0zdXEMqsABApcWVYAAA=",
            "87692312",
            "soc_sender@company.com",
            "",
        )
        create_context_expected = (
            "87692312",
            "cc_user@company.com",
            "bcc_user@company.com",
            "Email body",
            "soc_sender@company.com",
            "<html><body>Email body+signature</body></html>",
            "",
            "",
            "soc_sender@company.com",
            "<87692312> Test Email 4",
            "end_user@company.com",
            1,
            "",
            "UTC",
        )

        # Execute the tested function
        multi_thread_reply(
            "Email body",
            "html",
            1,
            1,
            "",
            "",
            "cc_user@company.com",
            "bcc_user@company.com",
            "soc_sender@company.com",
            "soc_sender@company.com",
            "",
            False,
            "",
            time_zone="UTC",
        )

        validate_email_sent_call_args = validate_email_sent_mocker.call_args
        create_context_call_args = create_context_mocker.call_args
        assert validate_email_sent_call_args.args == validate_email_sent_expected
        assert create_context_call_args.args == create_context_expected
        assert reset_fields_mocker.called is True


@pytest.mark.parametrize("new_thread", ["n/a", "true", "false"])
def test_main(new_thread, mocker):
    """
    Unit Test Scenario 1 new_thread = 'n/a'
        Given
         - Script is called to send an email message
        When
         - new_thread = 'n/a', indicating this script is being called from the Email Communication layout
        Then
         - Validate that function single_thread_reply is called with all correct arguments
    Unit Test Scenario 1 new_thread = 'true'
        Given
         - Script is called to send an email message
        When
         - new_thread = 'true', indicating this script is being called from the Email Threads layout and the message
           to be sent is a new first-contact email
        Then
         - Validate that function multi_thread_new is called with all correct arguments
    Unit Test Scenario 1 new_thread = 'false'
        Given
         - Script is called to send an email message
        When
         - new_thread = 'false', indicating this script is being called from the Email Threads layout and the message
           to be sent is a reply to an existing email chain
        Then
         - Validate that function multi_thread_reply is called with all correct arguments
    """
    import SendEmailReply
    from SendEmailReply import main

    incident = util_load_json("test_data/incident_data.json")
    input_args = {
        "service_mail": "soc_sender@company.com",
        "files": {},
        "mail_sender_instance": "mail-sender-instance-1",
        "new_thread": new_thread,
        "bodyType": "html",
    }
    mocker.patch.object(demisto, "args", return_value=input_args)
    mocker.patch.object(demisto, "incident", return_value=incident)
    mocker.patch.object(SendEmailReply, "get_mailbox_from_incident_labels", return_value="")
    mocker.patch.object(SendEmailReply, "get_email_recipients", return_value="end_user@company.com")
    mocker.patch.object(demisto, "executeCommand", return_value="")
    single_thread_reply_mocker = mocker.patch.object(SendEmailReply, "single_thread_reply", return_value=True)
    multi_thread_new_mocker = mocker.patch.object(SendEmailReply, "multi_thread_new", return_value=True)
    multi_thread_reply_mocker = mocker.patch.object(SendEmailReply, "multi_thread_reply", return_value=True)
    main()
    if new_thread == "n/a":
        single_thread_reply_args = single_thread_reply_mocker.call_args
        expected_args = (
            "87654321",
            "10",
            "",
            "test_cc@example.com",
            "",
            "html",
            [],
            {},
            None,
            False,
            "end_user@company.com",
            "soc_sender@company.com",
            "123456",
            "mail-sender-instance-1",
            None,
            False,
        )
        assert single_thread_reply_args.args == expected_args
    elif new_thread == "true":
        multi_thread_new_args = multi_thread_new_mocker.call_args
        expected_args = (
            "Test Email Subject.",
            False,
            "test_recipient@example.com",
            "This is a test email.",
            "html",
            "10",
            None,
            {},
            {},
            "soc_sender@company.com",
            "test_cc@example.com",
            "test_bcc@example.com",
            "mail-sender-instance-1",
            "None",
            None,
            "UTC",
        )
        assert multi_thread_new_args.args == expected_args
    elif new_thread == "false":
        multi_thread_reply_args = multi_thread_reply_mocker.call_args
        expected_args = (
            "This is a test email.",
            "html",
            "10",
            1,
            {},
            {},
            "test_cc@example.com",
            "test_bcc@example.com",
            "soc_sender@company.com",
            "mail-sender-instance-1",
            "None",
            False,
            None,
            "UTC",
        )
        assert multi_thread_reply_args.args == expected_args


# Parametrized test for happy path scenarios with various realistic markdown inputs
@pytest.mark.parametrize(
    "input_md, expected_html, test_id",
    [
        # Test ID: #1 - Simple text conversion
        ("Hello, World!", "<p>Hello, World!</p>", "simple_text"),
        # Test ID: #2 - Header conversion
        ("# Header 1", "<h1>Header 1</h1>", "header_conversion"),
        # Test ID: #3 - Table conversion
        (
            "| Header1 | Header2 |\n| ------- | ------- |\n| Cell1   | Cell2   |",
            '<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse; border: 1px solid #999;">\n'
            "<thead>\n<tr>\n<th>Header1</th>\n<th>Header2</th>\n</tr>\n</thead>\n"
            "<tbody>\n<tr>\n<td>Cell1</td>\n<td>Cell2</td>\n</tr>\n</tbody>\n</table>",
            "table_conversion",
        ),
        # Test ID: #4 - Emphasis conversion using legacy syntax
        ("_italic_ **bold**", "<p><em>italic</em> <strong>bold</strong></p>", "emphasis_conversion"),
        # Test ID: #5 - List conversion
        ("- Item 1\n- Item 2", "<ul>\n<li>Item 1</li>\n<li>Item 2</li>\n</ul>", "list_conversion"),
        # Test ID: #6 - New lines to <br> conversion
        ("Line 1\nLine 2", "<p>Line 1<br />\nLine 2</p>", "newline_to_br_conversion"),
    ],
    ids=lambda test_id: test_id,
)
def test_format_body_happy_path(input_md, expected_html, test_id):
    # Act
    from SendEmailReply import format_body

    result = format_body(input_md)

    # Assert
    assert result[1] == expected_html, f"Test failed for {test_id}"


# Parametrized test for edge cases
@pytest.mark.parametrize(
    "input_md, expected_html, test_id",
    [
        # Test ID: #1 - Empty string
        ("", ("", ""), "empty_string"),
        # Test ID: #2 - Markdown with only special characters
        ("# $%^&*()", ("<h1>$%^&amp;*()</h1>", "<h1>$%^&amp;*()</h1>"), "special_characters_only"),
    ],
    ids=lambda test_id: test_id,
)
def test_format_body_edge_cases(input_md, expected_html, test_id):
    # Act
    from SendEmailReply import format_body

    result = format_body(input_md)

    # Assert
    assert result == expected_html, f"Test failed for {test_id}"


# Parametrized test for edge cases
@pytest.mark.parametrize(
    "input_md, expected_html, test_id",
    [
        # Test ID: #1 - Demisto custom markdown underline syntax.
        ("+underline+", ("<p><u>underline</u></p>", "<p><u>underline</u></p>"), "underline"),
        # Test ID: #2 - Demisto custom markdown strikethrough syntax.
        ("~~strikethrough~~", ("<p><s>strikethrough</s></p>", "<p><s>strikethrough</s></p>"), "strikethrough"),
    ],
    ids=lambda test_id: test_id,
)
def test_demisto_custom_markdown_syntax(input_md, expected_html, test_id):
    # Act
    from SendEmailReply import format_body

    result = format_body(input_md)

    # Assert
    assert result == expected_html, f"Test failed for {test_id}"


def test_format_body_raw_checkbox_true(mocker):
    """
    Given
    - An incident with the checkbox 'Send Body as Raw Text (No Markdown)' checked
    When
    - The format_body function is called with a markdown-formatted body
    Then
    - Validate that the body is returned as-is, without any markdown conversion
    """
    from SendEmailReply import format_body

    mock_incident = {"CustomFields": {"sendbodyasrawnomarkdown": True}}
    mocker.patch("SendEmailReply.demisto.incident", return_value=mock_incident)

    result = format_body("**bold**")
    assert result == ("**bold**", "**bold**")

    result = format_body("_using italic_")
    assert result == ("_using italic_", "_using italic_")


@freeze_time("2024-02-22 10:00:00 UTC")
def test_get_unique_code_happy_path(mocker):
    # Arrange
    incident_id = "123"
    max_tries = 1000
    expected_code = "1231708596000000"
    mocker.patch("SendEmailReply.demisto", return_value=None)
    mocker.patch("SendEmailReply.get_incident_by_query", return_value=None)

    # Act
    code = get_unique_code(incident_id, max_tries)

    # Assert
    assert code == expected_code


@freeze_time("2024-02-22 10:00:00 UTC")
def test_get_unique_code_edge_cases(mocker):
    # Arrange
    incident_id = "1"
    max_tries = 1000
    expected_code = "0011708596000000"
    mocker.patch("SendEmailReply.get_incident_by_query", return_value=None)

    # Act
    code = get_unique_code(incident_id, max_tries)

    # Assert
    assert code == expected_code


@freeze_time("2024-02-22 10:00:00 UTC")
def test_get_unique_code_error_case(mocker):
    # Arrange
    incident_id = "123"
    max_tries = 0
    mocker.patch("SendEmailReply.get_incident_by_query", return_value=[{"id": "existing_incident"}])
    mocked_return_error = mocker.patch("SendEmailReply.return_error", side_effect=Exception("Error"))

    # Act and Assert
    with pytest.raises(Exception):
        get_unique_code(incident_id, max_tries)
    mocked_return_error.assert_called_once_with(
        f"Failed to generate unique code for incident {incident_id} after {max_tries} tries"
    )


def test_format_body(mocker):
    from SendEmailReply import format_body

    html_body = "![image](/markdown/image/aljhgfghdjakldvygi)"
    mocker.patch("SendEmailReply.execute_command", return_value=(True, [{"FileID": "111"}]))
    mocker.patch.object(demisto, "investigation", return_value={"id": "1234"})
    open_mock = mocker.mock_open(read_data=b"some binary data")
    mocker.patch("builtins.open", open_mock)
    result = format_body(html_body)
    expected_result = (
        '<p><img alt="image" src="/markdown/image/aljhgfghdjakldvygi" /></p>',
        '<p><img alt="image" src="data:image/png;base64,c29tZSBiaW5hcnkgZGF0YQ==" /></p>',
    )
    assert result == expected_result


def test_apply_direction():
    """
    Test that apply_direction correctly transforms lines with direction tags
    into the expected HTML div wrappers, and leaves other lines unchanged.
    """
    # Center alignment
    assert apply_direction("<-:->Centered text") == '<div style="text-align:center;">Centered text</div>'
    # Leading spaces should be ignored for detecting tags
    assert apply_direction("   <-:->Indented center") == '<div style="text-align:center;">Indented center</div>'
    # Left-to-right direction
    assert apply_direction("<--:>LTR text") == '<div dir="ltr">LTR text</div>'
    # Right-to-left direction
    assert apply_direction("<:-->RTL text") == '<div dir="rtl">RTL text</div>'
    # No direction tag, returns original line
    assert apply_direction("No special tag") == "No special tag"
    # Tag-like but not at start after stripping
    assert apply_direction("  some text <-:-> not a tag") == "  some text <-:-> not a tag"


def test_process_directions():
    """
    Test that process_directions applies apply_direction line-by-line on multiline text.
    """
    md = """Line 1
<-:->Center
Some text
<--:>LTR line
<:-->RTL line"""

    expected = (
        "Line 1\n"
        '<div style="text-align:center;">Center</div>\n'
        "Some text\n"
        '<div dir="ltr">LTR line</div>\n'
        '<div dir="rtl">RTL line</div>'
    )
    assert process_directions(md) == expected


def test_ensure_markdown_tables_have_spacing():
    """
    Test that ensure_markdown_tables_have_spacing adds blank lines before and after markdown tables.
    It should not add extra blank lines if already present.
    """
    # Single table without blank lines
    md = "Paragraph\n| Header |\n|--------|\n| Row    |\nParagraph2"
    expected = "Paragraph\n\n| Header |\n|--------|\n| Row    |\n\nParagraph2"
    assert ensure_markdown_tables_have_spacing(md) == expected

    # Table already has blank lines
    md2 = "Para\n\n| H |\n|---|\n| R |\n\nPara2"
    expected2 = md2
    assert ensure_markdown_tables_have_spacing(md2) == expected2

    # Multiple tables with some blank lines missing
    md3 = "| H1 |\n|----|\n| R1 |\nPara\n| H2 |\n|----|\n| R2 |"
    expected3 = "| H1 |\n" "|----|\n" "| R1 |\n" "\n" "Para\n" "\n" "| H2 |\n" "|----|\n" "| R2 |"
    assert ensure_markdown_tables_have_spacing(md3).rstrip() == expected3.rstrip()


def test_replace_atlassian_tags():
    """
    Test that replace_atlassian_tags converts Atlassian color and background tags into HTML span tags.
    """
    # Color tag
    input_md = "{{color:#FF0000}}(red text)"
    expected = '<span style="color:#FF0000;">red text</span>'
    assert replace_atlassian_tags(input_md) == expected

    # Background tag
    input_md2 = "{{background:#00FF00}}(green background)"
    expected2 = '<span style="background-color:#00FF00;">green background</span>'
    assert replace_atlassian_tags(input_md2) == expected2

    # Both tags in one string
    input_md3 = "Text {{color:#123456}}(colored) and {{background:#654321}}(background)"
    expected3 = 'Text <span style="color:#123456;">colored</span> and <span style="background-color:#654321;">background</span>'
    assert replace_atlassian_tags(input_md3) == expected3

    # No tags remain unchanged
    input_md4 = "No tags here"
    assert replace_atlassian_tags(input_md4) == input_md4


class TestGetReplyBodyXSSPrevention:
    """Tests for XSS prevention in attachment names within get_reply_body."""

    def test_attachment_names_escaped_in_reply_body(self, mocker):
        """
        Given
        - A note with content and attachments containing XSS payloads in filenames
        When
        - get_reply_body is called
        Then
        - Validate that attachment names in the reply body are HTML-escaped
        """
        from SendEmailReply import get_reply_body

        notes = [{"Metadata": {"user": "admin"}, "Contents": "Test reply", "ID": "1"}]
        malicious_attachments = [
            {"name": "<script>alert(1)</script>"},
            {"name": "<img src=x onerror=alert(1)>"},
        ]

        mocker.patch(
            "SendEmailReply.execute_command",
            side_effect=[
                (True, [{"Contents": {"name": "Admin User"}}]),  # getUserByUsername
                (True, None),  # core-api-post (entry/note)
                (True, None),  # addEntries
            ],
        )
        mocker.patch.object(demisto, "incident", return_value={"CustomFields": {"sendbodyasrawnomarkdown": False}})

        reply_body, context_html, reply_html = get_reply_body(notes, "123", malicious_attachments)

        # Attachment names must be escaped in the reply body
        assert "&lt;script&gt;" in reply_body
        assert "<script>alert(1)</script>" not in reply_body
        assert "&lt;img src=x onerror=alert(1)&gt;" in reply_body
        assert "<img src=x onerror=" not in reply_body