From 9500cc6cee7a3732fc13314a7431aef41d96a2b5 Mon Sep 17 00:00:00 2001 From: Son NK Date: Mon, 30 Mar 2020 22:05:31 +0200 Subject: [PATCH] Take into account spamassassin spam report --- app/email_utils.py | 33 +++++++ email_handler.py | 92 ++++++++++++++++++- .../emails/transactional/spam-email.html | 21 +++++ templates/emails/transactional/spam-email.txt | 19 ++++ 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 templates/emails/transactional/spam-email.html create mode 100644 templates/emails/transactional/spam-email.txt diff --git a/app/email_utils.py b/app/email_utils.py index aec4de69..c18c47be 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -365,6 +365,21 @@ def get_orig_message_from_bounce(msg: Message) -> Message: return part +def get_orig_message_from_spamassassin_report(msg: Message) -> Message: + """parse the original email from Spamassassin report""" + i = 0 + for part in msg.walk(): + i += 1 + + # the original message is the 4th part + # 1st part is the root part, multipart/report + # 2nd is text/plain, SpamAssassin part + # 3rd is the original message in message/rfc822 content type + # 4th is original message + if i == 4: + return part + + def new_addr(old_addr, new_email, user: User) -> str: """replace First Last by first@example.com by SimpleLogin @@ -399,3 +414,21 @@ def get_addrs_from_header(msg: Message, header) -> [str]: # do not return empty string return [r for r in ret if r] + + +def get_spam_info(msg: Message) -> (bool, str): + """parse SpamAssassin header to detect whether a message is classified as spam. + Return (is spam, spam status detail) + The header format is + ```X-Spam-Status: No, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID, + DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS, + URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2``` + """ + spamassassin_status = msg["X-Spam-Status"] + if not spamassassin_status: + return False, "" + + # yes or no + spamassassin_answer = spamassassin_status[: spamassassin_status.find(",")] + + return spamassassin_answer.lower() == "yes", spamassassin_status diff --git a/email_handler.py b/email_handler.py index f72b4693..26d46c46 100644 --- a/email_handler.py +++ b/email_handler.py @@ -68,6 +68,8 @@ from app.email_utils import ( delete_all_headers_except, new_addr, get_addrs_from_header, + get_spam_info, + get_orig_message_from_spamassassin_report, ) from app.extensions import db from app.log import LOG @@ -424,12 +426,25 @@ def handle_forward(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, s LOG.d("Forward from %s to %s, nothing to do", envelope.mail_from, mailbox_email) return False, "550 SL ignored" + contact = get_or_create_contact(msg["From"], alias) + + spam_check = True + # create PGP email if needed if mailbox.pgp_finger_print and user.is_premium(): LOG.d("Encrypt message using mailbox %s", mailbox) msg = prepare_pgp_message(msg, mailbox.pgp_finger_print) - contact = get_or_create_contact(msg["From"], alias) + # no need to spam check for encrypted message + spam_check = False + + if spam_check: + is_spam, spam_status = get_spam_info(msg) + if is_spam: + LOG.warning("Email detected as spam. Alias: %s, from: %s", alias, contact) + handle_spam(contact, alias, msg, user, mailbox_email, spam_status) + return False, "550 SL ignored" + forward_log = EmailLog.create(contact_id=contact.id, user_id=contact.user_id) if alias.enabled: @@ -750,6 +765,81 @@ def handle_bounce( ) +def handle_spam( + contact: Contact, + alias: Alias, + msg: Message, + user: User, + mailbox_email: str, + spam_status: str, +): + email_log: EmailLog = EmailLog.create( + contact_id=contact.id, + user_id=contact.user_id, + is_spam=True, + spam_status=spam_status, + ) + db.session.commit() + + # Store the report & original email + orig_msg = get_orig_message_from_spamassassin_report(msg) + # generate a name for the email + random_name = str(uuid.uuid4()) + + full_report_path = f"refused-emails/full-{random_name}.eml" + s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()), random_name) + + file_path = None + if orig_msg: + file_path = f"refused-emails/{random_name}.eml" + s3.upload_email_from_bytesio( + file_path, BytesIO(orig_msg.as_bytes()), random_name + ) + + refused_email = RefusedEmail.create( + path=file_path, full_report_path=full_report_path, user_id=user.id + ) + db.session.flush() + + email_log.refused_email_id = refused_email.id + db.session.commit() + + LOG.d("Create spam email %s", refused_email) + + refused_email_url = URL + f"/dashboard/refused_email?highlight_id=" + str(email_log.id) + disable_alias_link = f"{URL}/dashboard/unsubscribe/{alias.id}" + + # inform user + LOG.d( + "Inform user %s about spam email sent by %s to alias %s", + user, + contact.website_from, + alias.email, + ) + send_email( + mailbox_email, + f"Email from {contact.website_from} to {alias.email} is detected as spam", + render( + "transactional/spam-email.txt", + name=user.name, + alias=alias, + website_from=contact.website_from, + website_email=contact.website_email, + disable_alias_link=disable_alias_link, + refused_email_url=refused_email_url, + ), + render( + "transactional/spam-email.html", + name=user.name, + alias=alias, + website_from=contact.website_from, + website_email=contact.website_email, + disable_alias_link=disable_alias_link, + refused_email_url=refused_email_url, + ), + ) + + def handle_unsubscribe(envelope): message_data = envelope.content.decode("utf8", errors="replace") msg = Parser(policy=SMTPUTF8).parsestr(message_data) diff --git a/templates/emails/transactional/spam-email.html b/templates/emails/transactional/spam-email.html new file mode 100644 index 00000000..9bc2cf3b --- /dev/null +++ b/templates/emails/transactional/spam-email.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block content %} + {{ render_text("Hi " + name) }} + {{ render_text("An email sent to your alias " + alias.email + " from " + website_email + " is detected as spam by our Spam Detection Engine (SpamAssassin).") }} + + {{ render_text('In most of the cases, the email will be refused by your email provider.') }} + + {{ render_button("View the email", refused_email_url) }} + + {{ render_text('The email is automatically deleted in 7 days.') }} + + {{ render_text('Your alias ' + alias.email + ' is probably in the hands of a spammer now. In this case, you should disable or delete the alias immediately.') }} + + {{ render_button("Disable alias", disable_alias_link) }} + + {{ render_text('Please let us know if you have any question by replying to this email.') }} + + {{ render_text('Thanks,
SimpleLogin Team.') }} + {{ raw_url(disable_alias_link) }} +{% endblock %} diff --git a/templates/emails/transactional/spam-email.txt b/templates/emails/transactional/spam-email.txt new file mode 100644 index 00000000..c11b1a76 --- /dev/null +++ b/templates/emails/transactional/spam-email.txt @@ -0,0 +1,19 @@ +Hi {{name}} + +An email sent to your alias {{alias.email}} from {{website_from}} is detected as spam by our Spam Detection Engine (SpamAssassin). + +In most of the cases, the email will be refused by your email provider. + +You can view this email here: +{{ refused_email_url }} + +The email is automatically deleted in 7 days. + +Your alias {{alias}} is probably in the hands of a spammer now. In this case, you should disable or delete the alias immediately. + +{{disable_alias_link}} + +Please let us know if you have any question by replying to this email. + +Best, +SimpleLogin team. \ No newline at end of file