use VERP for transactional email: remove SENDER, SENDER_DIR

This commit is contained in:
Son NK 2021-01-26 09:59:08 +01:00
parent 3e1ef3358b
commit 4cd49b66c2
6 changed files with 49 additions and 39 deletions

View File

@ -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_SUFFIX = os.environ.get("BOUNCE_SUFFIX") or f"+@{EMAIL_DOMAIN}"
BOUNCE_EMAIL = BOUNCE_PREFIX + "{}" + BOUNCE_SUFFIX 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: try:
MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"]) MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"])
except Exception: except Exception:
@ -69,12 +80,6 @@ except Exception:
# maximum number of directory a premium user can create # maximum number of directory a premium user can create
MAX_NB_DIRECTORY = 50 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 ENFORCE_SPF = "ENFORCE_SPF" in os.environ
# allow to override postfix server locally # allow to override postfix server locally

View File

@ -120,7 +120,13 @@ def directory():
if Directory.get_by(name=new_dir_name): if Directory.get_by(name=new_dir_name):
flash(f"{new_dir_name} already added", "warning") 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( flash(
"this directory name is reserved, please choose another name", "this directory name is reserved, please choose another name",
"warning", "warning",

View File

@ -32,11 +32,11 @@ from app.config import (
DISPOSABLE_EMAIL_DOMAINS, DISPOSABLE_EMAIL_DOMAINS,
MAX_ALERT_24H, MAX_ALERT_24H,
POSTFIX_PORT, POSTFIX_PORT,
SENDER,
URL, URL,
LANDING_PAGE_URL, LANDING_PAGE_URL,
EMAIL_DOMAIN, EMAIL_DOMAIN,
ALERT_DIRECTORY_DISABLED_ALIAS_CREATION, ALERT_DIRECTORY_DISABLED_ALIAS_CREATION,
TRANSACTIONAL_BOUNCE_EMAIL,
) )
from app.dns_utils import get_mx_domains from app.dns_utils import get_mx_domains
from app.extensions import db from app.extensions import db
@ -50,6 +50,7 @@ from app.models import (
Contact, Contact,
Alias, Alias,
EmailLog, EmailLog,
TransactionalEmail,
) )
from app.utils import ( from app.utils import (
random_string, random_string,
@ -230,6 +231,7 @@ def send_email(
unsubscribe_link=None, unsubscribe_link=None,
unsubscribe_via_email=False, unsubscribe_via_email=False,
): ):
to_email = sanitize_email(to_email)
if NOT_SEND_EMAIL: if NOT_SEND_EMAIL:
LOG.d( LOG.d(
"send email with subject '%s' to '%s', plaintext: %s", "send email with subject '%s' to '%s', plaintext: %s",
@ -277,10 +279,13 @@ def send_email(
add_dkim_signature(msg, email_domain) add_dkim_signature(msg, email_domain)
msg_raw = to_bytes(msg) msg_raw = to_bytes(msg)
if SENDER:
smtp.sendmail(SENDER, to_email, msg_raw) transaction = TransactionalEmail.get_by(email=to_email)
else: if not transaction:
smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw) 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( def send_email_with_rate_control(

View File

@ -2114,8 +2114,7 @@ class Metric(db.Model, ModelMixin):
class Bounce(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) email = db.Column(db.String(256), nullable=False, index=True)

View File

@ -68,8 +68,6 @@ from app.config import (
ALERT_SPAM_EMAIL, ALERT_SPAM_EMAIL,
ALERT_SPF, ALERT_SPF,
POSTFIX_PORT, POSTFIX_PORT,
SENDER,
SENDER_DIR,
SPAMASSASSIN_HOST, SPAMASSASSIN_HOST,
MAX_SPAM_SCORE, MAX_SPAM_SCORE,
MAX_REPLY_PHASE_SPAM_SCORE, MAX_REPLY_PHASE_SPAM_SCORE,
@ -81,6 +79,8 @@ from app.config import (
BOUNCE_EMAIL, BOUNCE_EMAIL,
BOUNCE_PREFIX, BOUNCE_PREFIX,
BOUNCE_SUFFIX, BOUNCE_SUFFIX,
TRANSACTIONAL_BOUNCE_PREFIX,
TRANSACTIONAL_BOUNCE_SUFFIX,
) )
from app.email_utils import ( from app.email_utils import (
send_email, send_email,
@ -1502,26 +1502,21 @@ def handle_unsubscribe_user(user_id: int, mail_from: str) -> str:
return "250 Unsubscribe request accepted" return "250 Unsubscribe request accepted"
def handle_sender_email(envelope: Envelope): def handle_transactional_bounce(envelope: Envelope, rcpt_to):
filename = ( LOG.d("handle transactional bounce sent to %s", rcpt_to)
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: # parse the TransactionalEmail
f.write(envelope.original_content) transactional_id = parse_id_from_bounce(rcpt_to)
transactional = TransactionalEmail.get(transactional_id)
LOG.d("Write email to sender at %s", filepath) if transactional:
LOG.info("Create bounce for %s", transactional.email)
msg = email.message_from_bytes(envelope.original_content) Bounce.create(email=transactional.email, commit=True)
orig = get_orig_message_from_bounce(msg) else:
if orig: LOG.exception(
LOG.warning( "Cannot find transactional email for %s %s", transactional_id, rcpt_to
"Original message %s -> %s saved at %s", orig["From"], orig["To"], filepath
) )
return "250 email to sender accepted"
def handle(envelope: Envelope) -> str: def handle(envelope: Envelope) -> str:
"""Return SMTP status""" """Return SMTP status"""
@ -1538,9 +1533,14 @@ def handle(envelope: Envelope) -> str:
return handle_unsubscribe(envelope) return handle_unsubscribe(envelope)
# emails sent to sender. Probably bounce emails # 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) 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 ( if (
len(rcpt_tos) == 1 len(rcpt_tos) == 1

View File

@ -36,8 +36,6 @@ PREMIUM_ALIAS_DOMAINS=["premium.com"]
# transactional email is sent from this email address # transactional email is sent from this email address
SUPPORT_EMAIL=support@sl.local SUPPORT_EMAIL=support@sl.local
SUPPORT_NAME=Son from SimpleLogin SUPPORT_NAME=Son from SimpleLogin
# in case sender is different than SUPPORT_EMAIL
SENDER=sender@sl.local
# To use VERP # To use VERP
# prefix must end with + and suffix must start with + # prefix must end with + and suffix must start with +
@ -45,9 +43,6 @@ SENDER=sender@sl.local
# BOUNCE_SUFFIX = "+@sl.local" # BOUNCE_SUFFIX = "+@sl.local"
# all emails sent to sender are stored in this folder
SENDER_DIR=/tmp
# to receive general stats. # to receive general stats.
# ADMIN_EMAIL=admin@sl.local # ADMIN_EMAIL=admin@sl.local