use VERP for transactional email: remove SENDER, SENDER_DIR
This commit is contained in:
parent
3e1ef3358b
commit
4cd49b66c2
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue