mirror of
https://github.com/simple-login/app.git
synced 2024-09-30 05:31:30 +02:00
Merge pull request #226 from simple-login/sender-report
Handle transactional bounce emails
This commit is contained in:
commit
2034225a37
@ -62,6 +62,12 @@ 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
|
||||||
|
@ -27,6 +27,7 @@ from app.config import (
|
|||||||
DISPOSABLE_EMAIL_DOMAINS,
|
DISPOSABLE_EMAIL_DOMAINS,
|
||||||
MAX_ALERT_24H,
|
MAX_ALERT_24H,
|
||||||
POSTFIX_PORT,
|
POSTFIX_PORT,
|
||||||
|
SENDER,
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -180,9 +181,7 @@ def send_cannot_create_domain_alias(user, alias, domain):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_email(
|
def send_email(to_email, subject, plaintext, html=None):
|
||||||
to_email, subject, plaintext, html=None, bounced_email: Optional[Message] = None
|
|
||||||
):
|
|
||||||
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",
|
||||||
@ -200,25 +199,11 @@ def send_email(
|
|||||||
else:
|
else:
|
||||||
smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT or 25)
|
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 = MIMEMultipart("alternative")
|
||||||
msg.attach(MIMEText(plaintext, "text"))
|
msg.attach(MIMEText(plaintext, "text"))
|
||||||
if html:
|
|
||||||
|
if not html:
|
||||||
|
html = plaintext.replace("\n", "<br>")
|
||||||
msg.attach(MIMEText(html, "html"))
|
msg.attach(MIMEText(html, "html"))
|
||||||
|
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
@ -236,6 +221,9 @@ def send_email(
|
|||||||
add_dkim_signature(msg, email_domain)
|
add_dkim_signature(msg, email_domain)
|
||||||
|
|
||||||
msg_raw = msg.as_bytes()
|
msg_raw = msg.as_bytes()
|
||||||
|
if SENDER:
|
||||||
|
smtp.sendmail(SENDER, to_email, msg_raw)
|
||||||
|
else:
|
||||||
smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw)
|
smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw)
|
||||||
|
|
||||||
|
|
||||||
@ -246,7 +234,6 @@ def send_email_with_rate_control(
|
|||||||
subject,
|
subject,
|
||||||
plaintext,
|
plaintext,
|
||||||
html=None,
|
html=None,
|
||||||
bounced_email: Optional[Message] = None,
|
|
||||||
max_alert_24h=MAX_ALERT_24H,
|
max_alert_24h=MAX_ALERT_24H,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Same as send_email with rate control over alert_type.
|
"""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)
|
SentAlert.create(user_id=user.id, alert_type=alert_type, to_email=to_email)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
send_email(to_email, subject, plaintext, html, bounced_email)
|
send_email(to_email, subject, plaintext, html)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ It should contain the following info:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import email
|
import email
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from email import encoders
|
from email import encoders
|
||||||
@ -63,6 +64,8 @@ from app.config import (
|
|||||||
ALERT_SPAM_EMAIL,
|
ALERT_SPAM_EMAIL,
|
||||||
ALERT_SPF,
|
ALERT_SPF,
|
||||||
POSTFIX_PORT,
|
POSTFIX_PORT,
|
||||||
|
SENDER,
|
||||||
|
SENDER_DIR,
|
||||||
)
|
)
|
||||||
from app.email_utils import (
|
from app.email_utils import (
|
||||||
send_email,
|
send_email,
|
||||||
@ -836,7 +839,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
|||||||
send_email_with_rate_control(
|
send_email_with_rate_control(
|
||||||
user,
|
user,
|
||||||
ALERT_BOUNCE_EMAIL,
|
ALERT_BOUNCE_EMAIL,
|
||||||
# use user mail here as only user is authenticated to see the refused email
|
|
||||||
user.email,
|
user.email,
|
||||||
f"Email from {contact.website_email} to {address} cannot be delivered to your inbox",
|
f"Email from {contact.website_email} to {address} cannot be delivered to your inbox",
|
||||||
render(
|
render(
|
||||||
@ -857,8 +859,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
|||||||
refused_email_url=refused_email_url,
|
refused_email_url=refused_email_url,
|
||||||
mailbox_email=mailbox.email,
|
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
|
# disable the alias the second time email is bounced
|
||||||
elif nb_bounced >= 2:
|
elif nb_bounced >= 2:
|
||||||
@ -876,7 +876,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
|||||||
send_email_with_rate_control(
|
send_email_with_rate_control(
|
||||||
user,
|
user,
|
||||||
ALERT_BOUNCE_EMAIL,
|
ALERT_BOUNCE_EMAIL,
|
||||||
# use user mail here as only user is authenticated to see the refused email
|
|
||||||
user.email,
|
user.email,
|
||||||
f"Alias {address} has been disabled due to second undelivered email from {contact.website_email}",
|
f"Alias {address} has been disabled due to second undelivered email from {contact.website_email}",
|
||||||
render(
|
render(
|
||||||
@ -895,8 +894,6 @@ def handle_bounce(contact: Contact, alias: Alias, msg: Message, user: User):
|
|||||||
refused_email_url=refused_email_url,
|
refused_email_url=refused_email_url,
|
||||||
mailbox_email=mailbox.email,
|
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"
|
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:
|
def handle(envelope: Envelope, smtp: SMTP) -> str:
|
||||||
"""Return SMTP status"""
|
"""Return SMTP status"""
|
||||||
# unsubscribe request
|
# unsubscribe request
|
||||||
@ -1032,6 +1050,11 @@ def handle(envelope: Envelope, smtp: SMTP) -> str:
|
|||||||
LOG.d("Handle unsubscribe request from %s", envelope.mail_from)
|
LOG.d("Handle unsubscribe request from %s", envelope.mail_from)
|
||||||
return handle_unsubscribe(envelope)
|
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
|
# Whether it's necessary to apply greylisting
|
||||||
if greylisting_needed(envelope.mail_from, envelope.rcpt_tos):
|
if greylisting_needed(envelope.mail_from, envelope.rcpt_tos):
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
|
@ -33,6 +33,11 @@ ALIAS_DOMAINS=["domain1.com", "domain2.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
|
||||||
|
|
||||||
|
# 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
Block a user