From ab911fd55eeecaa7c4c7168fe4bed1ea208c5673 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Tue, 25 Aug 2020 12:51:05 +0200
Subject: [PATCH] do not forward cycle email: email sent to alias from its
mailbox
---
app/config.py | 3 ++
email_handler.py | 45 +++++++++++++++++++
.../emails/transactional/cycle-email.html | 30 +++++++++++++
.../emails/transactional/cycle-email.txt | 16 +++++++
4 files changed, 94 insertions(+)
create mode 100644 templates/emails/transactional/cycle-email.html
create mode 100644 templates/emails/transactional/cycle-email.txt
diff --git a/app/config.py b/app/config.py
index 75499b25..79636057 100644
--- a/app/config.py
+++ b/app/config.py
@@ -289,6 +289,9 @@ ALERT_BOUNCE_EMAIL = "bounce"
# When a forwarding email is detected as spam
ALERT_SPAM_EMAIL = "spam"
+# When an email is sent from a mailbox to an alias - a cycle
+ALERT_SEND_EMAIL_CYCLE = "cycle"
+
ALERT_SPF = "spf"
# <<<<< END ALERT EMAIL >>>>
diff --git a/email_handler.py b/email_handler.py
index 500e4065..f5ebc5ef 100644
--- a/email_handler.py
+++ b/email_handler.py
@@ -71,6 +71,7 @@ from app.config import (
SPAMASSASSIN_HOST,
MAX_SPAM_SCORE,
MAX_REPLY_PHASE_SPAM_SCORE,
+ ALERT_SEND_EMAIL_CYCLE,
)
from app.email_utils import (
send_email,
@@ -116,6 +117,7 @@ _MAILBOX_ID_HEADER = "X-SimpleLogin-Mailbox-ID"
_EMAIL_LOG_ID_HEADER = "X-SimpleLogin-EmailLog-ID"
_MESSAGE_ID = "Message-ID"
+
# fix the database connection leak issue
# use this method instead of create_app
def new_app():
@@ -374,6 +376,41 @@ def prepare_pgp_message(orig_msg: Message, pgp_fingerprint: str):
return msg
+def handle_email_sent_to_ourself(alias, mailbox, msg: Message, user):
+ # store the refused email
+ random_name = str(uuid.uuid4())
+ full_report_path = f"refused-emails/cycle-{random_name}.eml"
+ s3.upload_email_from_bytesio(full_report_path, BytesIO(msg.as_bytes()), random_name)
+ refused_email = RefusedEmail.create(
+ path=None, full_report_path=full_report_path, user_id=alias.user_id
+ )
+ db.session.commit()
+ LOG.d("Create refused email %s", refused_email)
+ # link available for 6 days as it gets deleted in 7 days
+ refused_email_url = refused_email.get_url(expires_in=518400)
+
+ send_email_with_rate_control(
+ user,
+ ALERT_SEND_EMAIL_CYCLE,
+ mailbox.email,
+ f"Warning: email sent from {mailbox.email} to {alias.email} forms a cycle",
+ render(
+ "transactional/cycle-email.txt",
+ name=user.name or "",
+ alias=alias,
+ mailbox=mailbox,
+ refused_email_url=refused_email_url,
+ ),
+ render(
+ "transactional/cycle-email.html",
+ name=user.name or "",
+ alias=alias,
+ mailbox=mailbox,
+ refused_email_url=refused_email_url,
+ ),
+ )
+
+
async def handle_forward(
envelope, smtp: SMTP, msg: Message, rcpt_to: str
) -> List[Tuple[bool, str]]:
@@ -390,6 +427,14 @@ async def handle_forward(
LOG.d("alias %s cannot be created on-the-fly, return 550", address)
return [(False, "550 SL E3 Email not exist")]
+ mail_from = envelope.mail_from.lower().strip()
+ for mb in alias.mailboxes:
+ # email send from a mailbox to alias
+ if mb.email.lower().strip() == mail_from:
+ LOG.exception("cycle email sent from %s to %s", mb, alias)
+ handle_email_sent_to_ourself(alias, mb, msg, alias.user)
+ return [(True, "250 Message accepted for delivery")]
+
contact = get_or_create_contact(msg["From"], envelope.mail_from, alias)
email_log = EmailLog.create(contact_id=contact.id, user_id=contact.user_id)
db.session.commit()
diff --git a/templates/emails/transactional/cycle-email.html b/templates/emails/transactional/cycle-email.html
new file mode 100644
index 00000000..3ac3183b
--- /dev/null
+++ b/templates/emails/transactional/cycle-email.html
@@ -0,0 +1,30 @@
+{% extends "base.html" %}
+
+{% block content %}
+ {% call text() %}
+ Hi {{ name }}
+ {% endcall %}
+
+ {% call text() %}
+ SimpleLogin has blocked an email that was sent to your alias {{ alias.email }} from its mailbox
+ {{ mailbox.email }}:
+ the email would be forwarded to the same mailbox {{ mailbox.email }}.
+
+ {% endcall %}
+
+ {% call text() %}
+ This creates a cycle and the email will probably be refused or hidden by your email service anyway.
+ {% endcall %}
+
+ {{ render_button("View the refused email", refused_email_url) }}
+
+ {% call text() %}
+ The email is automatically deleted in 7 days.
+ {% endcall %}
+
+ {% call text() %}
+ Please let us know if you have any question.
+ {% endcall %}
+
+ {{ render_text('Thanks,
SimpleLogin Team.') }}
+{% endblock %}
diff --git a/templates/emails/transactional/cycle-email.txt b/templates/emails/transactional/cycle-email.txt
new file mode 100644
index 00000000..0860c051
--- /dev/null
+++ b/templates/emails/transactional/cycle-email.txt
@@ -0,0 +1,16 @@
+Hi {{name}}
+
+SimpleLogin has blocked an email that was sent to your alias {{alias.email}} from its mailbox {{ mailbox.email }}:
+ the email would be forwarded to the same mailbox {{ mailbox.email }}.
+
+This creates a *cycle* and the email will probably be refused or hidden by your email service anyway.
+
+You can view this email here:
+{{ refused_email_url }}
+
+The email is automatically deleted in 7 days.
+
+Please let us know if you have any question.
+
+Best,
+SimpleLogin team.
\ No newline at end of file