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

View File

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

View File

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

View File

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

View File

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

View File

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