def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR try: check_function = get_expression_list(config.name_expression_list).pop() except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR try: exec(check_function, globals()) return check_email(email, args.log) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR
def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR if "Sensitivity" in email and str(email.get("Sensitivity")) == "private": if len(email.as_bytes()) > config.max_size_kb * 1024: write_log(args.log, "Mail exceeds max size") return ReturnCode.INVALID for part in email.walk(): if part.is_attachment(): write_log(args.log, "Mail has attachment") return ReturnCode.INVALID return ReturnCode.PRIVATE return ReturnCode.NOT_PRIVATE
def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR num_recipients = 0 # count number of email addresses in To and Cc headers for header_keyword in ["To", "Cc"]: if header_keyword in email: for header in email.get_all(header_keyword): email_addresses = extract_email_addresses(str(header)) if email_addresses: num_recipients += len(email_addresses) if num_recipients > config.recipient_limit: return ReturnCode.LIMIT_EXCEEDED return ReturnCode.LIMIT_OK
def main(args): try: email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR for part in email.walk(): if part.get_content_type() == "text/html": charset_mime = part.get_content_charset() if charset_mime: content = part.get_payload(decode=True).decode(charset_mime, errors="ignore") match = re.search(r"<meta [^>]*charset=\"?([^;\"> ]+)", content, flags=re.IGNORECASE) if match is not None: charset_meta = match.group(1).lower() if charset_mime != charset_meta: content = re.sub( r"(<meta [^>]*charset=\"?)({})".format( charset_meta), r"\1{}".format(charset_mime), content, flags=re.IGNORECASE) if HEADER_CTE in part: del part[HEADER_CTE] part.set_payload(content, charset=charset_mime) try: with open(args.input, "wb") as f: f.write(email.as_bytes()) except Exception: write_log(args.log, "Error writing '{}'".format(args.input)) return ReturnCode.ERROR return ReturnCode.CHARSET_CHANGED return ReturnCode.NOT_MODIFIED
def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR email = email.as_bytes() for list_string in config.search_strings: for string in list_string: if email.find(string.encode(CHARSET_UTF8)) == -1: break else: return ReturnCode.STRING_FOUND return ReturnCode.NOT_FOUND
def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR if "Subject" not in email: return ReturnCode.ENCRYPTION_SKIPPED header_subject = str(email.get("Subject")) keyword_escaped = re.escape(config.keyword_encryption) if not re.match(r"^{}".format(keyword_escaped), header_subject, re.IGNORECASE): return ReturnCode.ENCRYPTION_SKIPPED if "From" not in email: write_log(args.log, "Header from does not exist") return ReturnCode.ERROR header_from = str(email.get("From")) if not header_from: write_log(args.log, "Header from is empty") return ReturnCode.ERROR address_sender = extract_email_address(header_from) if not address_sender: write_log(args.log, "Cannot find sender address") return ReturnCode.ERROR address_recipient = dict() # collect email addresses in To and Cc headers for header_keyword in ["To", "Cc"]: address_recipient[header_keyword] = set() if header_keyword in email: for header in email.get_all(header_keyword): email_addresses = extract_email_addresses(str(header)) if email_addresses: address_recipient[header_keyword] |= email_addresses if not address_recipient["To"]: write_log(args.log, "Cannot find recipient address") return ReturnCode.ERROR # remove encryption keyword from subject header email = re.sub(r"(\n|^)Subject: *{} *".format(keyword_escaped), r"\1Subject: ", email.as_string(), count=1, flags=re.IGNORECASE) header_subject = re.sub(r"^{} *".format(keyword_escaped), "", header_subject, flags=re.IGNORECASE) password_characters = string.ascii_letters + string.digits if config.password_punctuation: password_characters += string.punctuation password = "".join( random.choice(password_characters) for i in range(config.password_length)) try: zip_archive = zip_encrypt({("email.eml", email)}, password) except Exception: write_log(args.log, "Error zip-encrypting email") return ReturnCode.ERROR # send email with encrypted original mail attached to recipients email_message = EmailMessage() email_message["Subject"] = header_subject email_message["From"] = address_sender email_message["To"] = ", ".join(address_recipient["To"]) if address_recipient["Cc"]: email_message["Cc"] = ", ".join(address_recipient["Cc"]) email_message.set_content(MESSAGE_RECIPIENT.format(address_sender)) email_message.add_attachment(zip_archive, maintype="application", subtype="zip", filename="email.zip") try: with smtplib.SMTP("localhost", port=PORT_SMTP) as s: s.send_message(email_message) except Exception: write_log(args.log, "Cannot send recipient email") return ReturnCode.ERROR # send email with password to sender email_message = EmailMessage() email_message["Subject"] = "Re: {}".format(header_subject) email_message["From"] = address_sender email_message["To"] = address_sender email_message.set_content(MESSAGE_SENDER.format(password)) try: with smtplib.SMTP("localhost", port=PORT_SMTP) as s: s.send_message(email_message) except Exception: write_log(args.log, "Cannot send sender email") return ReturnCode.MAIL_ENCRYPTED
def main(args): HEADER_CTE = "Content-Transfer-Encoding" try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR sys.setrecursionlimit(RECURSION_LIMIT) if config.text_tag or (args.remove and config.address_tag and config.clean_text): text_part = None for part in email.walk(): if part.get_content_type( ) == "text/plain" and not part.is_attachment(): text_part = part text_charset = python_charset(text_part.get_content_charset()) if text_charset is None: text_charset = CHARSET_UTF8 try: text_content = text_part.get_payload(decode=True).decode( text_charset, errors="ignore").replace("\r", "") except Exception: write_log( args.log, "Cannot decode text part with charset '{}'".format( text_charset)) return ReturnCode.INVALID_ENCODING break if config.html_tag or (args.remove and config.address_tag and config.clean_html): html_part = None for part in email.walk(): if part.get_content_type( ) == "text/html" and not part.is_attachment(): html_part = part html_charset = python_charset(html_part.get_content_charset()) if html_charset is None: html_charset = CHARSET_UTF8 try: html_content = html_part.get_payload(decode=True).decode( html_charset, errors="ignore") except Exception: write_log( args.log, "Cannot decode html part with charset '{}'".format( html_charset)) return ReturnCode.INVALID_ENCODING break if config.calendar_tag: calendar_part = None for part in email.walk(): if part.get_content_type( ) == "text/calendar" and not part.is_attachment(): calendar_part = part calendar_charset = python_charset( calendar_part.get_content_charset()) if calendar_charset is None: calendar_charset = CHARSET_UTF8 try: calendar_content = calendar_part.get_payload( decode=True).decode(calendar_charset, errors="ignore") except Exception: write_log( args.log, "Cannot decode calendar part with charset '{}'".format( calendar_charset)) return ReturnCode.INVALID_ENCODING break email_modified = False if args.remove: if config.address_tag: # remove address tag string_tag = "{} ".format(config.address_tag) pattern_tag = re.compile(r'^"?{} '.format( re.escape(config.address_tag))) pattern_quote = re.compile(r'^".*"$') for header_keyword in ["To", "Cc"]: if header_keyword in email: header = "".join([ part if isinstance(part, str) else part.decode(python_charset(encoding), errors="ignore") if encoding else part.decode(CHARSET_UTF8, errors="ignore") for (part, encoding) in decode_header(", ".join([ str(header) for header in email.get_all(header_keyword) ]).replace("\n", "")) ]) list_address = extract_addresses(header) if list_address: header = "" header_modified = False for (prefix, address, suffix) in list_address: if prefix.startswith(";") or prefix.startswith( ","): prefix = prefix[1:] if prefix.endswith("<") and suffix.startswith(">"): prefix = prefix[:-1] suffix = suffix[1:] prefix = prefix.strip() suffix = suffix.strip() if re.search(pattern_tag, prefix): if prefix.startswith('"'): prefix = '"' + prefix[ len(config.address_tag) + 2:] else: prefix = prefix[len(config.address_tag) + 1:] header_modified = True if not string_ascii(prefix): if re.search(pattern_quote, prefix): prefix = prefix[1:-1] prefix = make_header([ (prefix.encode(CHARSET_UTF8), CHARSET_UTF8) ]).encode() if prefix: header += prefix + " " header += "<" + address + ">" if suffix: header += " " + suffix header += ", " if header_modified: del email[header_keyword] email[header_keyword] = header[:-2] email_modified = True if config.subject_tag and "Subject" in email: # remove subject tag header = "".join([ part if isinstance(part, str) else part.decode(python_charset(encoding), errors="ignore") if encoding else part.decode(CHARSET_UTF8, errors="ignore") for (part, encoding) in decode_header( str(email.get("Subject")).strip().replace("\n", "")) ]) match = re.search(r"{} ".format(re.escape(config.subject_tag)), header) if match is not None: del email["Subject"] email["Subject"] = header[:match.start()] + header[match.end( ):] email_modified = True if (config.text_tag or (config.address_tag and config.clean_text)) and text_part is not None: # remove text body tag body_modified = False if config.text_tag: split_tag = config.text_tag.split("\n") while not split_tag[-1]: del split_tag[-1] pattern_tag = re.compile("\\n".join( [r"(>+ )*" + re.escape(item) for item in split_tag]) + r"\n") match = re.search(pattern_tag, text_content) if match is not None: text_content = re.sub(pattern_tag, "", text_content) body_modified = True if config.address_tag and config.clean_text and string_tag in text_content: text_content = text_content.replace(string_tag, "") body_modified = True if body_modified: if HEADER_CTE in text_part: del text_part[HEADER_CTE] text_part.set_payload(text_content, charset=text_charset) email_modified = True if (config.html_tag or (config.address_tag and config.clean_html)) and html_part is not None: # remove html body tag body_modified = False soup = bs4.BeautifulSoup(html_content, features="html5lib") list_tag = soup.find_all("div", id=re.compile(r".*{}.*".format( re.escape(config.html_tag_id)))) if list_tag: for tag in list_tag: tag.decompose() try: html_content = soup.encode(html_charset).decode( html_charset) except Exception: write_log(args.log, "Error converting soup to string") return ReturnCode.ERROR body_modified = True if config.address_tag and config.clean_html and string_tag in html_content: html_content = html_content.replace(string_tag, "") body_modified = True if body_modified: if HEADER_CTE in html_part: del html_part[HEADER_CTE] html_part.set_payload(html_content, charset=html_charset) email_modified = True if config.calendar_tag and calendar_part is not None: # remove calendar tag match = re.search(r"\nORGANIZER;.*CN=([^:;\r\n]+)", calendar_content) if match is not None: organizer = match.group(1) organizer_start = match.start(1) organizer_end = match.end(1) match = re.search( r"^{} ".format(re.escape(config.calendar_tag)), organizer) if match is not None: if HEADER_CTE in calendar_part: del calendar_part[HEADER_CTE] if calendar_charset != CHARSET_UTF8 and not string_ascii( config.calendar_tag): calendar_charset = CHARSET_UTF8 calendar_part.set_payload( calendar_content[:organizer_start] + organizer[match.end():] + calendar_content[organizer_end:], charset=calendar_charset) email_modified = True else: if config.address_tag and "From" in email: # add address tag pattern_tag = re.compile(r'^"?{} '.format( re.escape(config.address_tag))) pattern_quote = re.compile(r'^".*"$') list_address = extract_addresses("".join([ part if isinstance(part, str) else part.decode(python_charset(encoding), errors="ignore") if encoding else part.decode(CHARSET_UTF8, errors="ignore") for (part, encoding) in decode_header( str(email.get("From")).replace("\n", "")) ])) if list_address: (prefix, address, suffix) = list_address[0] if prefix.endswith("<") and suffix.startswith(">"): prefix = prefix[:-1] suffix = suffix[1:] prefix = prefix.strip() suffix = suffix.strip() if not re.search(pattern_tag, prefix): prefix_new = "" for (index, char) in enumerate(prefix): if char == '"': if end_escape(prefix[:index]): prefix_new += char else: prefix_new += char if prefix_new: prefix = '"{} {}"'.format(config.address_tag, prefix_new) else: prefix = '"{} {}"'.format(config.address_tag, address) if not string_ascii(prefix): if re.search(pattern_quote, prefix): prefix = prefix[1:-1] prefix = make_header([(prefix.encode(CHARSET_UTF8), CHARSET_UTF8)]).encode() del email["From"] email["From"] = prefix + " <" + address + "> " + suffix email_modified = True if config.name_domain_list: # add address tag to external addresses in To/Cc header try: set_address = get_address_list(config.name_domain_list) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR pattern_domain = re.compile(r"^\S+@(\S+)") set_domain = { match.group(1).lower() for match in [ re.search(pattern_domain, address) for address in set_address ] if match is not None } for header_keyword in ["To", "Cc"]: if header_keyword in email: header = "".join([ part if isinstance(part, str) else part.decode(python_charset(encoding), errors="ignore") if encoding else part.decode(CHARSET_UTF8, errors="ignore") for (part, encoding) in decode_header(", ".join([ str(header) for header in email.get_all(header_keyword) ]).replace("\n", "")) ]) list_address = extract_addresses(header) if list_address: header = "" header_modified = False for (prefix, address, suffix) in list_address: if prefix.startswith(";") or prefix.startswith( ","): prefix = prefix[1:] if prefix.endswith("<") and suffix.startswith( ">"): prefix = prefix[:-1] suffix = suffix[1:] prefix = prefix.strip() suffix = suffix.strip() match = re.search(pattern_domain, address) if match and match.group(1).lower( ) not in set_domain and not re.search( pattern_tag, prefix): prefix_new = "" for (index, char) in enumerate(prefix): if char == '"': if end_escape(prefix[:index]): prefix_new += char else: prefix_new += char if prefix_new: prefix = '"{} {}"'.format( config.address_tag, prefix_new) else: prefix = '"{} {}"'.format( config.address_tag, address) header_modified = True if not string_ascii(prefix): if re.search(pattern_quote, prefix): prefix = prefix[1:-1] prefix = make_header([ (prefix.encode(CHARSET_UTF8), CHARSET_UTF8) ]).encode() if prefix: header += prefix + " " header += "<" + address + ">" if suffix: header += " " + suffix header += ", " if header_modified: del email[header_keyword] email[header_keyword] = header[:-2] email_modified = True if config.subject_tag and "Subject" in email: # add subject tag header = "".join([ part if isinstance(part, str) else part.decode(python_charset(encoding), errors="ignore") if encoding else part.decode(CHARSET_UTF8, errors="ignore") for (part, encoding) in decode_header( str(email.get("Subject")).strip().replace("\n", "")) ]) if not re.search(r"^{} ".format(re.escape(config.subject_tag)), header): header = "{} {}".format(config.subject_tag, header) del email["Subject"] email["Subject"] = header email_modified = True if config.text_tag and text_part is not None: # add text body tag if config.text_top: text_content = config.text_tag + text_content else: text_content += config.text_tag if text_charset != CHARSET_UTF8 and not string_ascii( config.text_tag): text_charset = CHARSET_UTF8 if HEADER_CTE in text_part: del text_part[HEADER_CTE] text_part.set_payload(text_content, charset=text_charset) email_modified = True if config.html_tag and html_part is not None: # add html body tag if config.html_top: match = re.search(r"<body[^>]*>", html_content) if match is not None: index = match.end() else: match = re.search(r"<html[^>]*>", html_content) if match is not None: index = match.end() else: index = 0 else: index = html_content.find("</body>") if index < 0: index = html_content.find("</html>") if index < 0: index = len(html_content) - 1 if HEADER_CTE in html_part: del html_part[HEADER_CTE] if html_charset != CHARSET_UTF8 and not string_ascii( config.html_tag): html_charset = CHARSET_UTF8 html_part.set_payload(html_content[:index] + '<div id="{}">{}</div>'.format( config.html_tag_id, config.html_tag) + html_content[index:], charset=html_charset) email_modified = True if config.calendar_tag and calendar_part is not None: # add calendar tag match = re.search(r"\nORGANIZER;.*CN=([^:;\r\n]+)", calendar_content) if match is not None: organizer = match.group(1) if not re.search( r"^{} ".format(re.escape(config.calendar_tag)), organizer): if HEADER_CTE in calendar_part: del calendar_part[HEADER_CTE] if calendar_charset != CHARSET_UTF8 and not string_ascii( config.calendar_tag): calendar_charset = CHARSET_UTF8 calendar_part.set_payload( calendar_content[:match.start(1)] + config.calendar_tag + " " + organizer + calendar_content[match.end(1):], charset=calendar_charset) email_modified = True if email_modified: try: with open(args.input, "wb") as f: f.write(email.as_bytes()) except Exception: write_log(args.log, "Error writing '{}'".format(args.input)) return ReturnCode.ERROR return ReturnCode.TAG_ADDED return ReturnCode.NOT_MODIFIED
def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR try: set_expression = get_expression_list(config.name_expression_list) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR if not set_expression: write_log(args.log, "Expression list is empty") return ReturnCode.ERROR part_text = None part_html = None for part in email.walk(): if part_text is None and part.get_content_type() == "text/plain": part_text = part elif part_html is None and part.get_content_type() == "text/html": part_html = part if part_text is not None and part_html is not None: break list_pattern = list() for expression in set_expression: list_pattern.append(re.compile(expression, re.IGNORECASE)) expression_found = False if part_text is not None: content_text = part_text.get_payload(decode=True).decode( "utf-8", errors="ignore") for pattern in list_pattern: match = re.search(pattern, content_text) if match: expression_found = True break if part_html is not None: content_html = part_html.get_payload(decode=True).decode( "utf-8", errors="ignore") if not expression_found: text_html = html2text(content_html) for pattern in list_pattern: match = re.search(pattern, text_html) if match: expression_found = True break if expression_found: if part_text is not None: set_url = { url for (_, url, _, _) in re.findall(PATTERN_URL, content_text) } for url in set_url: content_text = content_text.replace(url, config.url_replacement) part_text.set_payload(content_text) if part_html is not None: soup = BeautifulSoup(content_html, features="html5lib") for a in soup.findAll("a", href=True): a["href"] = config.url_replacement content_html = str(soup) part_html.set_payload(content_html) try: with open(args.input, "wb") as f: f.write(email.as_bytes()) except Exception: write_log(args.log, "Error writing '{}'".format(args.input)) return ReturnCode.ERROR return ReturnCode.URL_REPLACED return ReturnCode.NOT_MODIFIED
def main(args): try: config = get_config(args.config, CONFIG_PARAMETERS) email = read_email(args.input) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR if "Received" not in email: write_log(args.log, "Header received does not exist") return ReturnCode.ERROR header_received = str(email.get("Received")) if not header_received: write_log(args.log, "Header received is empty") return ReturnCode.ERROR sender_ip = re.search(r"\[([0-9.]+)\]", header_received) if not sender_ip: write_log(args.log, "Cannot find sender IP") return ReturnCode.ERROR sender_ip = sender_ip.group(1) try: ipv4_address = IPv4Address(sender_ip) except Exception: write_log(args.log, "Invalid sender IP") return ReturnCode.ERROR for network in config.internal_networks: try: ipv4_network = IPv4Network(network) except Exception: write_log(args.log, "Invalid CIDR '{}'".format(network)) return ReturnCode.ERROR if ipv4_address in ipv4_network: break else: return ReturnCode.SENDER_EXTERNAL if "From" not in email: write_log(args.log, "Header does not exist") return ReturnCode.ERROR header_from = str(email.get("From")) if not header_from: write_log(args.log, "Header from is empty") return ReturnCode.ERROR sender_address = extract_email_address(header_from) if not sender_address: write_log(args.log, "Cannot find sender address") return ReturnCode.ERROR sender_address = sender_address.group(1) index_at = sender_address.find("@") if index_at == -1: write_log(args.log, "Invalid sender address") return ReturnCode.ERROR sender_domain = sender_address[index_at + 1:] if not sender_domain: write_log(args.log, "Empty sender domain") return ReturnCode.ERROR try: set_address = get_address_list(config.name_address_list) except Exception as ex: write_log(args.log, ex) return ReturnCode.ERROR if not set_address: write_log(args.log, "Address list is empty") return ReturnCode.ERROR set_domain = { email_address[email_address.find("@") + 1:] for email_address in set_address } if sender_domain not in set_domain: return ReturnCode.SENDER_EXTERNAL return ReturnCode.SENDER_INTERNAL