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.
- Type
- python
- Pack
- EmailCommunication
Source
import html as html_module
import re
import demistomock as demisto # noqa: F401
from bs4 import BeautifulSoup
from CommonServerPython import * # noqa: F401
ALLOWED_EMAIL_TAGS = {
"p",
"br",
"div",
"span",
"b",
"i",
"u",
"a",
"img",
"table",
"tr",
"td",
"th",
"thead",
"tbody",
"ul",
"ol",
"li",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"pre",
"code",
"blockquote",
"strong",
"em",
"hr",
"font",
"center",
"small",
"big",
"sub",
"sup",
"dl",
"dt",
"dd",
"caption",
}
ALLOWED_EMAIL_ATTRIBUTES: dict[str, set[str]] = {
"*": {"style", "class", "id", "dir", "align", "width", "height", "bgcolor", "valign"},
"a": {"href", "target"},
"img": {"src", "alt", "title"},
"td": {"colspan", "rowspan"},
"th": {"colspan", "rowspan", "scope"},
"font": {"color", "face", "size"},
"table": {"border", "cellpadding", "cellspacing"},
}
def sanitize_html_body(html_body: str) -> str:
"""Sanitize email body HTML using an allowlist of tags and attributes.
When nh3 is available, strips disallowed tags and attributes while preserving safe ones.
When nh3 is not available, returns the HTML as-is since full escaping
would break legitimate formatting in an HTML rendering context.
"""
try:
import nh3
return nh3.clean(html_body, tags=ALLOWED_EMAIL_TAGS, attributes=ALLOWED_EMAIL_ATTRIBUTES) # pylint: disable=no-member
except ImportError:
demisto.debug("nh3 is not available; HTML sanitization skipped")
return html_body
no_entries_message = """<!DOCTYPE html>
<html>
<body>
<h3>This Incident does not contain any email threads yet.</h3>
</body>
</html>
"""
def set_email_reply(email_from, email_to, email_cc, email_subject, html_body, email_time, attachment_names):
"""Set the email reply from the given details.
Args:
email_from: The email author mail.
email_to: The email recipients.
email_cc: The email cc.
email_subject: The email subject.
html_body: The email HTML body.
email_time: The time the email was received
attachment_names: String of attachment names
Returns:
str. Email reply.
"""
safe_from = html_module.escape(email_from or "")
safe_to = html_module.escape(email_to or "")
safe_cc = html_module.escape(email_cc or "")
safe_subject = html_module.escape(email_subject or "")
safe_time = html_module.escape(email_time or "")
safe_attachments = html_module.escape(attachment_names or "")
single_reply = (
f"<html><body><b>From:</b> {safe_from}<br><b>To:</b> {safe_to}<br><b>CC:</b> {safe_cc}<br>"
f"<b>Subject:</b> {safe_subject}<br><b>Email Time:</b> {safe_time}<br>"
f"<b>Attachments:</b> {safe_attachments}</body></html>"
)
single_reply += (
f'\n{sanitize_html_body(html_body or "")}\n'
f'<hr style="width:98%;text-align:center;height:3px;border-width:0;background-color:#cccccc">\n\n'
)
return single_reply
def rewrite_img_src(html: str, account_name: str = None) -> str:
"""
Replace:
src="xsoar/entry/download/<id>"
With:
src="xsoar/<account_name>/entry/download/<id>"
"""
if not account_name:
return html
pattern = r'src="xsoar/entry/download/([^"]+)"'
replacement = rf'src="xsoar/{account_name}/entry/download/\1"'
return re.sub(pattern, replacement, html)
def html_cleanup(full_thread_html, account_name=None):
"""
Moves various HTML tags so the final output is a single HTML document
Args:
full_thread_html (string): The concatenated string of all email HTML
Returns:
str. Full email thread HTML
"""
# Remove HTML tags within the string that should only occur once
full_thread_html = re.sub("(?i)<!DOCTYPE html>", "", full_thread_html)
full_thread_html = re.sub("(?i)<.?html>", "", full_thread_html)
full_thread_html = re.sub("(?i)<.?body>", "", full_thread_html)
# Place needed HTML tags in their appropriate locations
final_html_result = f"<!DOCTYPE html>\n<html>\n<body>\n{full_thread_html}\n</body>\n</html>"
if account_name:
final_html_result = rewrite_img_src(final_html_result, account_name)
return final_html_result
def remove_color_from_html_text(html_message):
"""Remove the color from the html text, so the color will be determined by the front-end.
Args:
html_message: The content of the HTML that the color attribute should be removed from.
Returns:
str. The updated HTML, without the color attribute.
"""
parsed_html_body = BeautifulSoup(html_message, "html.parser")
# Remove style attributes of color
for tag in parsed_html_body.find_all(True):
if "style" in tag.attrs and tag.attrs["style"] and "color" in tag.attrs["style"]:
demisto.debug(f"The original style att {tag.attrs['style']=}")
new_style = ""
style_attr = str(tag.attrs["style"]).split(";")
for attr in style_attr:
if "color" not in attr:
new_style += f"{attr};"
else: # Can be color, background-color, etc. Remove only the color of the text.
color_attr = attr.split(":")
if not (color_attr[0] == "color" or color_attr[0] == " color"):
new_style += f"{attr};"
tag.attrs["style"] = new_style
demisto.debug(f"The new style att {tag.attrs['style']=}")
if "color" in tag.attrs:
demisto.debug(f"Removed the color att {tag.attrs['color']} from the tag {tag=}")
del tag.attrs["color"]
return str(parsed_html_body)
def main():
args = demisto.args()
incident = demisto.incident()
account_name = args.get("account_name", "")
custom_fields = incident.get("CustomFields")
thread_number = custom_fields.get("emailselectedthread", 0)
incident_context = demisto.context()
email_threads = incident_context.get("EmailThreads", {})
if len(email_threads) == 0:
return_results({"ContentsFormat": EntryFormat.HTML, "Type": EntryType.NOTE, "Contents": no_entries_message})
return
if isinstance(email_threads, dict):
email_threads = [email_threads]
thread_exists = False
thread_items = []
full_thread_html = ""
for thread in email_threads:
if str(thread["EmailCommsThreadNumber"]) == str(thread_number):
thread_exists = True
thread_dict = {
"email_from": thread.get("EmailFrom", None),
"email_cc": thread.get("EmailCC", None),
"email_to": thread.get("EmailTo", None),
"email_subject": thread.get("EmailSubject", None),
"email_html": remove_color_from_html_text(thread.get("EmailHTML")) if thread.get("EmailHTML") else None,
"email_time": thread.get("MessageTime", None),
"email_attachments": thread.get("EmailAttachments", None),
}
demisto.debug(f'DisplayEmailHtmlThread - {thread_dict.get("email_html")=}')
thread_items.append(thread_dict)
if thread_exists:
# Append all messages together, with the most recent on top
for thread in reversed(thread_items):
email_reply = set_email_reply(
thread.get("email_from", None),
thread.get("email_to", None),
thread.get("email_cc", None),
thread.get("email_subject", None),
thread.get("email_html", None),
thread.get("email_time", None),
thread.get("email_attachments", None),
)
full_thread_html += email_reply
final_html_result = html_cleanup(full_thread_html, account_name)
return_results({"ContentsFormat": EntryFormat.HTML, "Type": EntryType.NOTE, "Contents": final_html_result})
else:
return_error(f"An email thread of {thread_number} was not found. Please make sure this thread number is correct.")
if __name__ in ("__builtin__", "builtins", "__main__"):
main()
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.