Merge pull request #226 from simple-login/sender-report

Handle transactional bounce emails
This commit is contained in:
Son Nguyen Kim 2020-06-10 13:59:49 +02:00 committed by GitHub
commit 2034225a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 31 deletions

View File

@ -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

View File

@ -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
@ -180,9 +181,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 +199,12 @@ def send_email(
else:
smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT or 25)
if bounced_email:
msg = MIMEMultipart("mixed")
msg = MIMEMultipart("alternative")
msg.attach(MIMEText(plaintext, "text"))
# 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"))
if not html:
html = plaintext.replace("\n", "<br>")
msg.attach(MIMEText(html, "html"))
msg["Subject"] = subject
msg["From"] = f"{SUPPORT_NAME} <{SUPPORT_EMAIL}>"
@ -236,7 +221,10 @@ def send_email(
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(
@ -246,7 +234,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.
@ -273,7 +260,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

View File

@ -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,
@ -836,7 +839,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 +859,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 +876,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 +894,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,
)
@ -1025,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
@ -1032,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(

View File

@ -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