From da7b46ef97be1be5b07b3c1eb42dc3c4fc7e6bb0 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 10 Jun 2020 12:15:57 +0200 Subject: [PATCH 1/6] remove bounced_email param from send_email --- app/email_utils.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/app/email_utils.py b/app/email_utils.py index 63678ecb..4e4938a9 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -180,9 +180,7 @@ def send_cannot_create_domain_alias(user, alias, domain): ) -def send_email( - to_email, subject, plaintext, html=None, bounced_email: Optional[Message] = None -): +def send_email(to_email, subject, plaintext, html=None): if NOT_SEND_EMAIL: LOG.d( "send email with subject %s to %s, plaintext: %s", @@ -200,26 +198,10 @@ def send_email( else: smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT or 25) - if bounced_email: - msg = MIMEMultipart("mixed") - - # add email main body - body = MIMEMultipart("alternative") - body.attach(MIMEText(plaintext, "text")) - if html: - body.attach(MIMEText(html, "html")) - - msg.attach(body) - - # add attachment - rfcmessage = MIMEBase("message", "rfc822") - rfcmessage.attach(bounced_email) - msg.attach(rfcmessage) - else: - msg = MIMEMultipart("alternative") - msg.attach(MIMEText(plaintext, "text")) - if html: - msg.attach(MIMEText(html, "html")) + msg = MIMEMultipart("alternative") + msg.attach(MIMEText(plaintext, "text")) + if html: + msg.attach(MIMEText(html, "html")) msg["Subject"] = subject msg["From"] = f"{SUPPORT_NAME} <{SUPPORT_EMAIL}>" @@ -273,7 +255,7 @@ def send_email_with_rate_control( SentAlert.create(user_id=user.id, alert_type=alert_type, to_email=to_email) db.session.commit() - send_email(to_email, subject, plaintext, html, bounced_email) + send_email(to_email, subject, plaintext, html) return True From 0c4e48c906fe5f8f1f2108de357a7c89fd07cdd2 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 10 Jun 2020 12:17:04 +0200 Subject: [PATCH 2/6] remove bounced_email param from send_email_with_rate_control --- app/email_utils.py | 1 - email_handler.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/app/email_utils.py b/app/email_utils.py index 4e4938a9..52f436bc 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -228,7 +228,6 @@ def send_email_with_rate_control( subject, plaintext, html=None, - bounced_email: Optional[Message] = None, max_alert_24h=MAX_ALERT_24H, ) -> bool: """Same as send_email with rate control over alert_type. diff --git a/email_handler.py b/email_handler.py index 29e09ed9..f7a61fc8 100644 --- a/email_handler.py +++ b/email_handler.py @@ -836,7 +836,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User): send_email_with_rate_control( user, ALERT_BOUNCE_EMAIL, - # use user mail here as only user is authenticated to see the refused email user.email, f"Email from {contact.website_email} to {address} cannot be delivered to your inbox", render( @@ -857,8 +856,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User): refused_email_url=refused_email_url, mailbox_email=mailbox.email, ), - # 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: @@ -876,7 +873,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User): send_email_with_rate_control( user, ALERT_BOUNCE_EMAIL, - # use user mail here as only user is authenticated to see the refused email user.email, f"Alias {address} has been disabled due to second undelivered email from {contact.website_email}", render( @@ -895,8 +891,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User): refused_email_url=refused_email_url, mailbox_email=mailbox.email, ), - # cannot include bounce email as it can contain spammy text - # bounced_email=msg, ) From b47d95226d07c8e0062c39b8ad5d7dbe2ee46c9b Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 10 Jun 2020 12:18:39 +0200 Subject: [PATCH 3/6] generate html from plaintext if not set --- app/email_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/email_utils.py b/app/email_utils.py index 52f436bc..9f5a614e 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -200,8 +200,10 @@ def send_email(to_email, subject, plaintext, html=None): msg = MIMEMultipart("alternative") msg.attach(MIMEText(plaintext, "text")) - if html: - msg.attach(MIMEText(html, "html")) + + if not html: + html = plaintext.replace("\n", "
") + msg.attach(MIMEText(html, "html")) msg["Subject"] = subject msg["From"] = f"{SUPPORT_NAME} <{SUPPORT_EMAIL}>" From 9abfa3e98cd93c7e61685a53b7324610fba5940c Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 10 Jun 2020 13:54:42 +0200 Subject: [PATCH 4/6] Add new param SENDER, SENDER_DIR --- app/config.py | 6 ++++++ example.env | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/app/config.py b/app/config.py index 048dbca2..7177dbce 100644 --- a/app/config.py +++ b/app/config.py @@ -62,6 +62,12 @@ 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/example.env b/example.env index 53d38731..ca5c4c19 100644 --- a/example.env +++ b/example.env @@ -33,6 +33,11 @@ ALIAS_DOMAINS=["domain1.com", "domain2.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 + +# all emails sent to sender are stored in this folder +SENDER_DIR=/tmp # to receive general stats. # ADMIN_EMAIL=admin@sl.local From d0c65ea37811eaa14c6a1e5f7e40063a33cee5f4 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 10 Jun 2020 13:55:47 +0200 Subject: [PATCH 5/6] send transactional email from SENDER if set --- app/email_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/email_utils.py b/app/email_utils.py index 9f5a614e..ff0df649 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -27,6 +27,7 @@ from app.config import ( DISPOSABLE_EMAIL_DOMAINS, MAX_ALERT_24H, POSTFIX_PORT, + SENDER, ) from app.dns_utils import get_mx_domains from app.extensions import db @@ -220,7 +221,10 @@ def send_email(to_email, subject, plaintext, html=None): add_dkim_signature(msg, email_domain) msg_raw = msg.as_bytes() - smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw) + if SENDER: + smtp.sendmail(SENDER, to_email, msg_raw) + else: + smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw) def send_email_with_rate_control( From 9c9319c94ef295e2ffc81a8db667b517aec19bdd Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 10 Jun 2020 13:57:23 +0200 Subject: [PATCH 6/6] handle emails sent to sender --- email_handler.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/email_handler.py b/email_handler.py index f7a61fc8..b4770bde 100644 --- a/email_handler.py +++ b/email_handler.py @@ -31,6 +31,7 @@ It should contain the following info: """ import email +import os import time import uuid from email import encoders @@ -63,6 +64,8 @@ from app.config import ( ALERT_SPAM_EMAIL, ALERT_SPF, POSTFIX_PORT, + SENDER, + SENDER_DIR, ) from app.email_utils import ( send_email, @@ -1019,6 +1022,27 @@ def handle_unsubscribe(envelope: Envelope): 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) + + with open(filepath, "wb") as f: + f.write(envelope.original_content) + + 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 + ) + + return "250 email to sender accepted" + + def handle(envelope: Envelope, smtp: SMTP) -> str: """Return SMTP status""" # unsubscribe request @@ -1026,6 +1050,11 @@ def handle(envelope: Envelope, smtp: SMTP) -> str: LOG.d("Handle unsubscribe request from %s", envelope.mail_from) return handle_unsubscribe(envelope) + # emails sent to sender. Probably bounce emails + if SENDER and envelope.rcpt_tos == [SENDER]: + LOG.d("Handle email sent to sender from %s", envelope.mail_from) + return handle_sender_email(envelope) + # Whether it's necessary to apply greylisting if greylisting_needed(envelope.mail_from, envelope.rcpt_tos): LOG.warning(