From 4cd49b66c24cdb31e0f3e0dcb08c2947aeba64a9 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Tue, 26 Jan 2021 09:59:08 +0100 Subject: [PATCH] use VERP for transactional email: remove SENDER, SENDER_DIR --- app/config.py | 17 +++++++++----- app/dashboard/views/directory.py | 8 ++++++- app/email_utils.py | 15 ++++++++---- app/models.py | 3 +-- email_handler.py | 40 ++++++++++++++++---------------- example.env | 5 ---- 6 files changed, 49 insertions(+), 39 deletions(-) diff --git a/app/config.py b/app/config.py index 9ca7e24d..193725f6 100644 --- a/app/config.py +++ b/app/config.py @@ -60,6 +60,17 @@ BOUNCE_PREFIX = os.environ.get("BOUNCE_PREFIX") or "bounce+" BOUNCE_SUFFIX = os.environ.get("BOUNCE_SUFFIX") or f"+@{EMAIL_DOMAIN}" BOUNCE_EMAIL = BOUNCE_PREFIX + "{}" + BOUNCE_SUFFIX +# VERP for transactional email: mail_from set to BOUNCE_PREFIX + email_log.id + BOUNCE_SUFFIX +TRANSACTIONAL_BOUNCE_PREFIX = ( + os.environ.get("TRANSACTIONAL_BOUNCE_PREFIX") or "transactional+" +) +TRANSACTIONAL_BOUNCE_SUFFIX = ( + os.environ.get("TRANSACTIONAL_BOUNCE_SUFFIX") or f"+@{EMAIL_DOMAIN}" +) +TRANSACTIONAL_BOUNCE_EMAIL = ( + TRANSACTIONAL_BOUNCE_PREFIX + "{}" + TRANSACTIONAL_BOUNCE_SUFFIX +) + try: MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"]) except Exception: @@ -69,12 +80,6 @@ except Exception: # maximum number of directory a premium user can create MAX_NB_DIRECTORY = 50 -# transactional email sender -SENDER = os.environ.get("SENDER") - -# the directory to store bounce emails -SENDER_DIR = os.environ.get("SENDER_DIR") - ENFORCE_SPF = "ENFORCE_SPF" in os.environ # allow to override postfix server locally diff --git a/app/dashboard/views/directory.py b/app/dashboard/views/directory.py index 14745ba1..27270c46 100644 --- a/app/dashboard/views/directory.py +++ b/app/dashboard/views/directory.py @@ -120,7 +120,13 @@ def directory(): if Directory.get_by(name=new_dir_name): flash(f"{new_dir_name} already added", "warning") - elif new_dir_name in ("reply", "ra", "bounces", "bounce"): + elif new_dir_name in ( + "reply", + "ra", + "bounces", + "bounce", + "transactional", + ): flash( "this directory name is reserved, please choose another name", "warning", diff --git a/app/email_utils.py b/app/email_utils.py index 6f3a652c..a7f4c0ea 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -32,11 +32,11 @@ from app.config import ( DISPOSABLE_EMAIL_DOMAINS, MAX_ALERT_24H, POSTFIX_PORT, - SENDER, URL, LANDING_PAGE_URL, EMAIL_DOMAIN, ALERT_DIRECTORY_DISABLED_ALIAS_CREATION, + TRANSACTIONAL_BOUNCE_EMAIL, ) from app.dns_utils import get_mx_domains from app.extensions import db @@ -50,6 +50,7 @@ from app.models import ( Contact, Alias, EmailLog, + TransactionalEmail, ) from app.utils import ( random_string, @@ -230,6 +231,7 @@ def send_email( unsubscribe_link=None, unsubscribe_via_email=False, ): + to_email = sanitize_email(to_email) if NOT_SEND_EMAIL: LOG.d( "send email with subject '%s' to '%s', plaintext: %s", @@ -277,10 +279,13 @@ def send_email( add_dkim_signature(msg, email_domain) msg_raw = to_bytes(msg) - if SENDER: - smtp.sendmail(SENDER, to_email, msg_raw) - else: - smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw) + + transaction = TransactionalEmail.get_by(email=to_email) + if not transaction: + transaction = TransactionalEmail.create(email=to_email, commit=True) + + # use a different envelope sender for each transactional email (aka VERP) + smtp.sendmail(TRANSACTIONAL_BOUNCE_EMAIL.format(transaction.id), to_email, msg_raw) def send_email_with_rate_control( diff --git a/app/models.py b/app/models.py index 66d1e626..25c6b28a 100644 --- a/app/models.py +++ b/app/models.py @@ -2114,8 +2114,7 @@ class Metric(db.Model, ModelMixin): class Bounce(db.Model, ModelMixin): - """Record all bounces. Deleted after 7 days - """ + """Record all bounces. Deleted after 7 days""" email = db.Column(db.String(256), nullable=False, index=True) diff --git a/email_handler.py b/email_handler.py index 8ca87ab1..5064be88 100644 --- a/email_handler.py +++ b/email_handler.py @@ -68,8 +68,6 @@ from app.config import ( ALERT_SPAM_EMAIL, ALERT_SPF, POSTFIX_PORT, - SENDER, - SENDER_DIR, SPAMASSASSIN_HOST, MAX_SPAM_SCORE, MAX_REPLY_PHASE_SPAM_SCORE, @@ -81,6 +79,8 @@ from app.config import ( BOUNCE_EMAIL, BOUNCE_PREFIX, BOUNCE_SUFFIX, + TRANSACTIONAL_BOUNCE_PREFIX, + TRANSACTIONAL_BOUNCE_SUFFIX, ) from app.email_utils import ( send_email, @@ -1502,26 +1502,21 @@ def handle_unsubscribe_user(user_id: int, mail_from: str) -> str: return "250 Unsubscribe request accepted" -def handle_sender_email(envelope: Envelope): - filename = ( - arrow.now().format("YYYY-MM-DD_HH-mm-ss") + "_" + random_string(10) + ".eml" - ) - filepath = os.path.join(SENDER_DIR, filename) +def handle_transactional_bounce(envelope: Envelope, rcpt_to): + LOG.d("handle transactional bounce sent to %s", rcpt_to) - with open(filepath, "wb") as f: - f.write(envelope.original_content) + # parse the TransactionalEmail + transactional_id = parse_id_from_bounce(rcpt_to) + transactional = TransactionalEmail.get(transactional_id) - LOG.d("Write email to sender at %s", filepath) - - msg = email.message_from_bytes(envelope.original_content) - orig = get_orig_message_from_bounce(msg) - if orig: - LOG.warning( - "Original message %s -> %s saved at %s", orig["From"], orig["To"], filepath + if transactional: + LOG.info("Create bounce for %s", transactional.email) + Bounce.create(email=transactional.email, commit=True) + else: + LOG.exception( + "Cannot find transactional email for %s %s", transactional_id, rcpt_to ) - return "250 email to sender accepted" - def handle(envelope: Envelope) -> str: """Return SMTP status""" @@ -1538,9 +1533,14 @@ def handle(envelope: Envelope) -> str: return handle_unsubscribe(envelope) # emails sent to sender. Probably bounce emails - if SENDER and rcpt_tos == [SENDER]: + if ( + len(rcpt_tos) == 1 + and rcpt_tos[0].startswith(TRANSACTIONAL_BOUNCE_PREFIX) + and rcpt_tos[0].endswith(TRANSACTIONAL_BOUNCE_SUFFIX) + ): LOG.d("Handle email sent to sender from %s", mail_from) - return handle_sender_email(envelope) + handle_transactional_bounce(envelope, rcpt_tos[0]) + return "250 bounce handled" if ( len(rcpt_tos) == 1 diff --git a/example.env b/example.env index bff764e5..1ffb8c85 100644 --- a/example.env +++ b/example.env @@ -36,8 +36,6 @@ PREMIUM_ALIAS_DOMAINS=["premium.com"] # transactional email is sent from this email address SUPPORT_EMAIL=support@sl.local SUPPORT_NAME=Son from SimpleLogin -# in case sender is different than SUPPORT_EMAIL -SENDER=sender@sl.local # To use VERP # prefix must end with + and suffix must start with + @@ -45,9 +43,6 @@ SENDER=sender@sl.local # BOUNCE_SUFFIX = "+@sl.local" -# all emails sent to sender are stored in this folder -SENDER_DIR=/tmp - # to receive general stats. # ADMIN_EMAIL=admin@sl.local