DisplayEmailHtmlThread

Dynamic-section script for 'Email Threads' layout. This script renders all email messages with the thread number specified in the "Email Selected Thread" field and outputs them as a single HTML output.

python · Email Communication

Details

IDDisplayEmailHtmlThread
Languagepython
From Version6.2.0
Docker Imagedemisto/btfl-soup:1.0.1.10120494
Tagsdynamic-section

README

Renders all email messages in the selected thread as a single HTML entry, with the oldest message appearing first and all subsequent messages below.

Script Data


Name Description
Script Type python3
Tags dynamic-section

Inputs


There are no inputs for this script.

Outputs


There are no outputs for this script.

import json

import demistomock as demisto
import pytest


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


def test_set_email_reply(mocker):
    """Unit test
    Given
    - Email message details
    When
    - Function is called with all arguments provided
    Then
    - Validate the function returns HTML that will properly render the email message
    """
    from DisplayEmailHtmlThread import set_email_reply

    expected_html = util_open_file("test_data/single_html_doc.txt")

    input_html = "<!DOCTYPE html><html><body><p>Test email body.</p></body></html>"
    test_message = {
        "email_from": "soc_sender@company.com",
        "email_to": "end_user@company.com",
        "email_cc": "cc_user@company.com",
        "email_subject": "Test Email #1",
        "html_body": input_html,
        "email_time": "2022-04-06T17:53:46UTC",
        "attachment_names": "File1.txt, File2.txt",
    }

    result = set_email_reply(**test_message)
    assert result == expected_html


def test_html_cleanup(mocker):
    """Unit test
    Given
    - Input HTML content
    When
    - Input html contains multiple separate HTML documents
    Then
    - Validate that the function returns a single HTML document
    """
    from DisplayEmailHtmlThread import html_cleanup

    input_html = util_open_file("test_data/multiple_html_docs.txt")
    expected_html = util_open_file("test_data/cleaned_html.txt")

    result = html_cleanup(input_html)
    assert result == expected_html


no_entries_message = """<!DOCTYPE html>
<html>
<body>
<h3>This Incident does not contain any email threads yet.</h3>
</body>
</html>
"""


@pytest.mark.parametrize(
    "emailselectedthread, email_threads, expected_result_type",
    [
        (1, {}, "no_threads"),
        (1, {"EmailThreads": util_load_json("test_data/email_threads.json")}, "good_result"),
        (5, {"EmailThreads": util_load_json("test_data/email_threads.json")}, "error_result"),
    ],
)
def test_main(emailselectedthread, email_threads, expected_result_type, mocker):
    """
    Unit test Scenario - No email threads present
        Given
        - Script is called to render an HTML thread
        When
        - The incident where the script is being run contains no email threads
        Then
        - Validate that the script returns message that no threads are present
    Unit test Scenario - Threads present and thread selection valid
        Given
        - Script is called to render an HTML thread
        When
        - The incident where the script is being run contains email threads
        - The 'emailselectedthread' field is set to a value corresponding to an email thread that is present
        Then
        - Validate that the script returns properly rendered HTML for the email thread
    Unit test Scenario - Threads present but thread selection not valid
        Given
        - Script is called to render an HTML thread
        When
        - The incident where the script is being run contains email threads
        - The 'emailselectedthread' field is set to a value which does not correspond to any of the present threads
        Then
        - Validate that the script returns an appropriate error
    """
    import DisplayEmailHtmlThread
    from DisplayEmailHtmlThread import main

    mock_incident = {"CustomFields": {"emailselectedthread": emailselectedthread}}
    mocker.patch.object(demisto, "incident", return_value=mock_incident)
    mocker.patch.object(demisto, "context", return_value=email_threads)
    return_results_mocker = mocker.patch.object(DisplayEmailHtmlThread, "return_results", return_value=True)
    return_error_mocker = mocker.patch.object(DisplayEmailHtmlThread, "return_error", return_value=True)
    main()
    results_call_args = return_results_mocker.call_args
    error_call_args = return_error_mocker.call_args
    if expected_result_type == "no_threads":
        assert results_call_args.args[0]["Contents"] == no_entries_message
    elif expected_result_type == "good_result":
        expected_result = util_open_file("test_data/good_result.txt")
        assert results_call_args.args[0]["Contents"] == expected_result
    elif expected_result_type == "error_result":
        expected_result = "An email thread of 5 was not found. Please make sure this thread number is correct."
        assert error_call_args.args[0] == expected_result


def test_remove_color_from_html_text():
    from DisplayEmailHtmlThread import remove_color_from_html_text

    html_message = (
        '<html>\r\n<head>\r\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">\r\n'
        '<meta name="Generator" content="Microsoft Exchange Server">\r\n<!-- converted from text --><style>'
        "<!-- .EmailQuote { margin-left: 1pt; padding-left: 4pt; border-left: #800000 2px solid; } --></style>\r\n"
        '</head>\r\n<body>\r\n<meta content="text/html; charset=UTF-8">\r\n<style type="text/css" style="">\r\n'
        '<!--\r\np\r\n\t{margin-top:0;\r\n\tmargin-bottom:0}\r\n-->\r\n</style>\r\n<div dir="ltr">\r\n'
        '<div id="x_divtagdefaultwrapper" dir="ltr" style="font-size:12pt; color:#000000; font-family:Calibri,'
        'Helvetica,sans-serif">\r\nreply to a thread from outlook</div>\r\n<hr tabindex="-1" '
        'style="display:inline-block; width:98%">\r\n<div id="x_divRplyFwdMsg" dir="ltr"><font face="Calibri, '
        'sans-serif" color="#000000" style="font-size:11pt"><b>From:</b> Administrator<br>\r\n<b>Sent:</b> Tuesday, '
        "January 9, 2024 3:01:34 PM<br>\r\n<b>To:</b> Administrator<br>\r\n<b>Subject:</b> &lt;04352911&gt; "
        'test 9.1 15:00</font>\r\n<div>&nbsp;</div>\r\n</div>\r\n</div>\r\n<font size="2">'
        '<span style="font-size:10pt;">\r\n<div class="PlainText">testing again from xsoar</div>\r\n</span></font>'
        "\r\n</body>\r\n</html>\r\n"
    )
    expected_html_message = (
        '<html>\n<head>\n<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>\n<meta '
        'content="Microsoft Exchange Server" name="Generator"/>\n<!-- converted from text --><style>'
        "<!-- .EmailQuote { margin-left: 1pt; padding-left: 4pt; border-left: #800000 2px solid; } -->"
        '</style>\n</head>\n<body>\n<meta content="text/html; charset=UTF-8"/>\n<style style="" '
        'type="text/css">\r\n<!--\r\np\r\n\t{margin-top:0;\r\n\tmargin-bottom:0}\r\n-->\r\n</style>'
        '\n<div dir="ltr">\n<div dir="ltr" id="x_divtagdefaultwrapper" style="font-size:12pt; font-family:'
        'Calibri,Helvetica,sans-serif;">\r\nreply to a thread from outlook</div>\n<hr style="display:'
        'inline-block; width:98%" tabindex="-1"/>\n<div dir="ltr" id="x_divRplyFwdMsg"><font '
        'face="Calibri, sans-serif" style="font-size:11pt"><b>From:</b> Administrator<br/>\n<b>Sent:</b>'
        " Tuesday, January 9, 2024 3:01:34 PM<br/>\n<b>To:</b> Administrator<br/>\n<b>Subject:</b> "
        '&lt;04352911&gt; test 9.1 15:00</font>\n<div>\xa0</div>\n</div>\n</div>\n<font size="2">'
        '<span style="font-size:10pt;">\n<div class="PlainText">testing again from xsoar</div>\n</span>'
        "</font>\n</body>\n</html>\n"
    )

    result = remove_color_from_html_text(html_message)
    assert result == expected_html_message


def test_main_styled_html(mocker):
    """
    Given
    - Script is called to render an HTML thread. The html contains styling attributes such as color.
    When
    - The incident where the script is being run contains email threads
    Then
    - Validate that the script returns an appropriate html, after the removal of the styling.
    """
    import DisplayEmailHtmlThread
    from DisplayEmailHtmlThread import main

    email_threads = {"EmailThreads": util_load_json("test_data/email_thread_with_html_styling.json")}

    mock_incident = {"CustomFields": {"emailselectedthread": 0}}
    mocker.patch.object(demisto, "incident", return_value=mock_incident)
    mocker.patch.object(demisto, "context", return_value=email_threads)
    return_results_mocker = mocker.patch.object(DisplayEmailHtmlThread, "return_results", return_value=True)
    main()
    results_call_args = return_results_mocker.call_args
    assert " color" not in results_call_args.args[0]["Contents"]


def test_html_cleanup_with_account_name(mocker):
    """Unit test
    Given
    - Input HTML content with image tags and an account name
    When
    - html_cleanup is called with an account_name
    Then
    - Validate that the function rewrites image src paths to include the account name
    """
    from DisplayEmailHtmlThread import html_cleanup

    input_html = '<html><body><img src="xsoar/entry/download/abc123"><p>Hello</p></body></html>'
    result = html_cleanup(input_html, account_name="myaccount")
    assert 'src="xsoar/myaccount/entry/download/abc123"' in result
    assert result.startswith("<!DOCTYPE html>")


def test_main_with_single_dict_email_thread(mocker):
    """
    Given
    - Script is called to render an HTML thread
    When
    - The incident context contains email threads as a single dict (not a list)
    Then
    - Validate that the script handles the dict-to-list conversion and renders properly
    """
    import DisplayEmailHtmlThread
    from DisplayEmailHtmlThread import main

    single_thread = {
        "EmailBCC": "",
        "EmailBody": "Test message",
        "EmailCC": "",
        "EmailCommsThreadId": "12345",
        "EmailCommsThreadNumber": "1",
        "EmailFrom": "sender@test.com",
        "EmailHTML": "<p>Test message</p>",
        "EmailTo": "recipient@test.com",
        "EmailSubject": "Test Subject",
        "EmailAttachments": "None",
        "MessageTime": "2022-01-01T00:00:00UTC",
    }
    email_threads = {"EmailThreads": single_thread}

    mock_incident = {"CustomFields": {"emailselectedthread": 1}}
    mocker.patch.object(demisto, "incident", return_value=mock_incident)
    mocker.patch.object(demisto, "context", return_value=email_threads)
    mocker.patch.object(demisto, "args", return_value={})
    return_results_mocker = mocker.patch.object(DisplayEmailHtmlThread, "return_results", return_value=True)
    main()
    results_call_args = return_results_mocker.call_args
    assert "Test message" in results_call_args.args[0]["Contents"]


def test_main_with_account_name(mocker):
    """
    Given
    - Script is called with an account_name argument
    When
    - The incident contains email threads with image tags
    Then
    - Validate that the account_name is used to rewrite image src paths
    """
    import DisplayEmailHtmlThread
    from DisplayEmailHtmlThread import main

    email_thread = {
        "EmailBCC": "",
        "EmailBody": "Test",
        "EmailCC": "",
        "EmailCommsThreadId": "12345",
        "EmailCommsThreadNumber": "1",
        "EmailFrom": "sender@test.com",
        "EmailHTML": '<p>Hello</p><img src="xsoar/entry/download/abc123">',
        "EmailTo": "recipient@test.com",
        "EmailSubject": "Test",
        "EmailAttachments": "None",
        "MessageTime": "2022-01-01T00:00:00UTC",
    }
    email_threads = {"EmailThreads": [email_thread]}

    mock_incident = {"CustomFields": {"emailselectedthread": 1}}
    mocker.patch.object(demisto, "incident", return_value=mock_incident)
    mocker.patch.object(demisto, "context", return_value=email_threads)
    mocker.patch.object(demisto, "args", return_value={"account_name": "testaccount"})
    return_results_mocker = mocker.patch.object(DisplayEmailHtmlThread, "return_results", return_value=True)
    main()
    results_call_args = return_results_mocker.call_args
    assert 'src="xsoar/testaccount/entry/download/abc123"' in results_call_args.args[0]["Contents"]


def test_main_with_no_email_html(mocker):
    """
    Given
    - Script is called to render an HTML thread
    When
    - A thread entry has no EmailHTML field
    Then
    - Validate that the script handles the missing EmailHTML gracefully
    """
    import DisplayEmailHtmlThread
    from DisplayEmailHtmlThread import main

    email_thread = {
        "EmailBCC": "",
        "EmailBody": "Test",
        "EmailCC": "",
        "EmailCommsThreadId": "12345",
        "EmailCommsThreadNumber": "1",
        "EmailFrom": "sender@test.com",
        "EmailTo": "recipient@test.com",
        "EmailSubject": "Test",
        "EmailAttachments": "None",
        "MessageTime": "2022-01-01T00:00:00UTC",
    }
    email_threads = {"EmailThreads": [email_thread]}

    mock_incident = {"CustomFields": {"emailselectedthread": 1}}
    mocker.patch.object(demisto, "incident", return_value=mock_incident)
    mocker.patch.object(demisto, "context", return_value=email_threads)
    mocker.patch.object(demisto, "args", return_value={})
    return_results_mocker = mocker.patch.object(DisplayEmailHtmlThread, "return_results", return_value=True)
    main()
    results_call_args = return_results_mocker.call_args
    assert results_call_args.args[0]["Contents"] is not None
    assert "sender@test.com" in results_call_args.args[0]["Contents"]


def test_rewrites_single_img_src():
    """
    Given
    - HTML contains image tag with src values in the form:
      src="xsoar/entry/download/<id>"
    When
    - The rewrite_img_src function is executed with a valid account name
    Then
    - Validate that the function returns appropriate HTML with the account name
      correctly inserted into the image src path.
    """
    from DisplayEmailHtmlThread import rewrite_img_src

    html = '<img src="xsoar/entry/download/12345">'
    result = rewrite_img_src(html, "myaccount")
    assert result == '<img src="xsoar/myaccount/entry/download/12345">'


def test_rewrites_single_img_src_without_account():
    """
    Given
    - HTML contains image tag with src values in the form:
      src="xsoar/entry/download/<id>"
    When
    - The rewrite_img_src function is executed with no account name
    Then
    - Validate that the function returns the HTML input with no changes
    """
    from DisplayEmailHtmlThread import rewrite_img_src

    html = '<img src="xsoar/entry/download/12345">'
    result = rewrite_img_src(html)
    assert result == html


class TestSetEmailReplyXSSPrevention:
    """Tests for XSS prevention in set_email_reply header fields."""

    def test_xss_in_email_from(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(
            "<script>alert(1)</script>",
            "to@test.com",
            "cc@test.com",
            "Subject",
            "<p>body</p>",
            "2024-01-01T00:00:00Z",
            "file.txt",
        )
        assert "&lt;script&gt;" in result
        assert "<script>alert(1)</script>" not in result

    def test_xss_in_email_to(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(
            "from@test.com",
            "<img src=x onerror=alert(1)>",
            "cc@test.com",
            "Subject",
            "<p>body</p>",
            "2024-01-01T00:00:00Z",
            "file.txt",
        )
        assert "&lt;img src=x" in result
        assert "<img src=x onerror=" not in result

    def test_xss_in_email_cc(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(
            "from@test.com", "to@test.com", "<iframe src=evil.com>", "Subject", "<p>body</p>", "2024-01-01T00:00:00Z", "file.txt"
        )
        assert "&lt;iframe" in result

    def test_xss_in_email_subject(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(
            "from@test.com",
            "to@test.com",
            "cc@test.com",
            "<img src=x onerror=alert(1)>",
            "<p>body</p>",
            "2024-01-01T00:00:00Z",
            "file.txt",
        )
        assert "&lt;img src=x onerror=alert(1)&gt;" in result
        assert "<img src=x onerror=" not in result

    def test_xss_in_attachment_names(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(
            "from@test.com",
            "to@test.com",
            "cc@test.com",
            "Subject",
            "<p>body</p>",
            "2024-01-01T00:00:00Z",
            "<script>alert(1)</script>",
        )
        assert "&lt;script&gt;" in result
        assert "<script>alert(" not in result

    def test_none_fields_render_as_empty(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(None, None, None, None, "<p>body</p>", None, None)
        assert "<b>From:</b> " in result
        assert "None" not in result

    def test_ampersand_in_subject(self):
        from DisplayEmailHtmlThread import set_email_reply

        result = set_email_reply(
            "from@test.com", "to@test.com", "", "Tom & Jerry <together>", "<p>body</p>", "2024-01-01T00:00:00Z", ""
        )
        assert "Tom &amp; Jerry &lt;together&gt;" in result


class TestSanitizeHtmlBody:
    """Tests for HTML body sanitization in DisplayEmailHtmlThread."""

    def test_strips_script_tags(self):
        pytest.importorskip("nh3", reason="nh3 not installed in Docker image")
        from DisplayEmailHtmlThread import sanitize_html_body

        result = sanitize_html_body("<p>Hello</p><script>alert(1)</script><p>World</p>")
        assert "<script>" not in result
        assert "Hello" in result

    def test_strips_onerror_attribute(self):
        pytest.importorskip("nh3", reason="nh3 not installed in Docker image")
        from DisplayEmailHtmlThread import sanitize_html_body

        result = sanitize_html_body('<img src="x" onerror="alert(1)">')
        assert "onerror" not in result

    def test_preserves_safe_formatting(self):
        pytest.importorskip("nh3", reason="nh3 not installed in Docker image")
        from DisplayEmailHtmlThread import sanitize_html_body

        safe_html = "<p>Hello <b>World</b></p><table><tr><td>Cell</td></tr></table>"
        result = sanitize_html_body(safe_html)
        assert "<p>" in result
        assert "<b>" in result
        assert "<table>" in result

    def test_empty_input(self):
        from DisplayEmailHtmlThread import sanitize_html_body

        assert sanitize_html_body("") == ""

    def test_fallback_when_nh3_unavailable(self, mocker):
        """Validate graceful fallback when nh3 is not available."""
        from DisplayEmailHtmlThread import sanitize_html_body

        mocker.patch.dict("sys.modules", {"nh3": None})
        malicious_html = "<script>alert(1)</script>"
        result = sanitize_html_body(malicious_html)
        assert result == malicious_html