diff --git a/app/email_utils.py b/app/email_utils.py index ca9a8e44..2db45f8a 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -363,3 +363,18 @@ def mailbox_already_used(email: str, user) -> bool: return True return False + + +def get_orig_message_from_bounce(msg: Message) -> Message: + """parse the original email from Bounce""" + 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, Postfix log + # ... + # 7th is original message + if i == 7: + return part diff --git a/email_handler.py b/email_handler.py index e1f05306..238510bf 100644 --- a/email_handler.py +++ b/email_handler.py @@ -37,21 +37,19 @@ from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.parser import Parser from email.policy import SMTPUTF8 +from io import BytesIO from smtplib import SMTP from typing import Optional from aiosmtpd.controller import Controller -import gnupg +from app import pgp_utils, s3 from app.config import ( EMAIL_DOMAIN, POSTFIX_SERVER, URL, ALIAS_DOMAINS, - ADMIN_EMAIL, - SUPPORT_EMAIL, POSTFIX_SUBMISSION_TLS, - GNUPGHOME, ) from app.email_utils import ( get_email_name, @@ -65,6 +63,7 @@ from app.email_utils import ( send_cannot_create_domain_alias, email_belongs_to_alias_domains, render, + get_orig_message_from_bounce, ) from app.extensions import db from app.log import LOG @@ -76,10 +75,10 @@ from app.models import ( Directory, User, DeletedAlias, + RefusedEmail, ) from app.utils import random_string from server import create_app -from app import pgp_utils # fix the database connection leak issue @@ -406,7 +405,12 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: # in this case Postfix will try to send a bounce report to original sender, which is # the "reply email" if envelope.mail_from == "<>": - LOG.error("Bounce when sending to alias %s, user %s", alias, gen_email.user) + LOG.error( + "Bounce when sending to alias %s from %s, user %s", + alias, + forward_email.website_from, + gen_email.user, + ) handle_bounce( alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email @@ -513,7 +517,9 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> str: def handle_bounce( alias, envelope, forward_email, gen_email, msg, smtp, user, mailbox_email ): - ForwardEmailLog.create(forward_id=forward_email.id, bounced=True) + fel: ForwardEmailLog = ForwardEmailLog.create( + forward_id=forward_email.id, bounced=True + ) db.session.commit() nb_bounced = ForwardEmailLog.filter_by( @@ -521,6 +527,30 @@ def handle_bounce( ).count() disable_alias_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" + # Store the bounced email + random_name = random_string(50) + + full_report_path = f"refused-emails/full-{random_name}.eml" + s3.upload_from_bytesio(full_report_path, BytesIO(msg.as_bytes())) + + file_path = f"refused-emails/{random_name}.eml" + orig_msg = get_orig_message_from_bounce(msg) + s3.upload_from_bytesio(file_path, BytesIO(orig_msg.as_bytes())) + + refused_email = RefusedEmail.create( + path=file_path, full_report_path=full_report_path, user_id=user.id + ) + db.session.flush() + + fel.refused_email_id = refused_email.id + db.session.commit() + + LOG.d("Create refused email %s", refused_email) + + refused_email_url = ( + URL + f"/dashboard/refused_email?highlight_fel_id=" + str(fel.id) + ) + # inform user if this is the first bounced email if nb_bounced == 1: LOG.d( @@ -530,7 +560,9 @@ def handle_bounce( alias, ) send_email( - mailbox_email, + # TOOD: use mailbox_email instead + user.email, + # mailbox_email, f"Email from {forward_email.website_from} to {alias} cannot be delivered to your inbox", render( "transactional/bounced-email.txt", @@ -539,6 +571,7 @@ def handle_bounce( website_from=forward_email.website_from, website_email=forward_email.website_email, disable_alias_link=disable_alias_link, + refused_email_url=refused_email_url, ), render( "transactional/bounced-email.html", @@ -547,8 +580,10 @@ def handle_bounce( website_from=forward_email.website_from, website_email=forward_email.website_email, disable_alias_link=disable_alias_link, + refused_email_url=refused_email_url, ), - bounced_email=msg, + # cannot include bounce email as it can contain spammy text + # bounced_email=msg, ) # disable the alias the second time email is bounced elif nb_bounced >= 2: diff --git a/templates/emails/transactional/bounced-email.html b/templates/emails/transactional/bounced-email.html index e1e3361e..2358289d 100644 --- a/templates/emails/transactional/bounced-email.html +++ b/templates/emails/transactional/bounced-email.html @@ -4,7 +4,9 @@ {{ render_text("Hi " + name) }} {{ render_text("An email sent to your alias " + alias + " from " + website_email + " was refused (or bounced) by your email provider.") }} - {{ render_text("This is usually due to the email being considered as spam by your email provider. The email is included at the end of this message so you can take a look at its content.") }} + {{ render_text('This is usually due to the email being considered as spam by your email provider.') }} + + {{ render_button("View the refused email", refused_email_url) }} {{ render_text('To avoid spams forwarded by SimpleLogin server, please consider the following options:') }} diff --git a/templates/emails/transactional/bounced-email.txt b/templates/emails/transactional/bounced-email.txt index 0fd0c688..8b8bcb8f 100644 --- a/templates/emails/transactional/bounced-email.txt +++ b/templates/emails/transactional/bounced-email.txt @@ -3,7 +3,8 @@ Hi {{name}} An email sent to your alias {{alias}} from {{website_from}} was refused (or bounced) by your email provider. This is usually due to the email being considered as spam by your email provider. -The email is included at the end of this message so you can take a look at its content. +You can view this email here: +{{ refused_email_url }} To avoid spams forwarded by SimpleLogin server, please consider the following options: