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
| ID | DisplayEmailHtmlThread |
|---|---|
| Language | python |
| From Version | 6.2.0 |
| Docker Image | demisto/btfl-soup:1.0.1.10120494 |
| Tags | dynamic-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> <04352911> " 'test 9.1 15:00</font>\r\n<div> </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> " '<04352911> 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 "<script>" 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 "<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 "<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 "<img src=x onerror=alert(1)>" 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 "<script>" 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 & Jerry <together>" 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