diff --git a/app/config.py b/app/config.py index 05bb7f46..6f35484d 100644 --- a/app/config.py +++ b/app/config.py @@ -255,6 +255,8 @@ APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET") # for Mac App MACAPP_APPLE_API_SECRET = os.environ.get("MACAPP_APPLE_API_SECRET") +# <<<<< ALERT EMAIL >>>> + # maximal number of alerts that can be sent to the same email in 24h MAX_ALERT_24H = 4 @@ -266,3 +268,7 @@ ALERT_BOUNCE_EMAIL = "bounce" # When a forwarding email is detected as spam ALERT_SPAM_EMAIL = "spam" + +ALERT_SPF = "spf" + +# <<<<< END ALERT EMAIL >>>> diff --git a/app/dashboard/templates/dashboard/mailbox_detail.html b/app/dashboard/templates/dashboard/mailbox_detail.html index 87b6e4a6..816b1ca8 100644 --- a/app/dashboard/templates/dashboard/mailbox_detail.html +++ b/app/dashboard/templates/dashboard/mailbox_detail.html @@ -65,38 +65,6 @@ - {% if spf_available %} - - {% endif %} -
@@ -137,6 +105,46 @@
+ +
+

Advanced Options

+ + {% if spf_available %} +
+
+ + +
+
+ Enforce SPF +
+ To avoid email-spoofing, SimpleLogin blocks email that + seems to come from your + mailbox + but sent from unknown + IP address. +
+ Only turn off this option if you know what you're doing :). +
+
+ +
+
+
+ {% endif %} {% endblock %} diff --git a/app/paddle_utils.py b/app/paddle_utils.py index a7a35155..cb782e90 100644 --- a/app/paddle_utils.py +++ b/app/paddle_utils.py @@ -10,11 +10,13 @@ import collections import phpserialize import requests from Crypto.Hash import SHA1 + # Crypto can be found at https://pypi.org/project/pycryptodome/ from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 from app.config import PADDLE_PUBLIC_KEY_PATH, PADDLE_VENDOR_ID, PADDLE_AUTH_CODE + # Your Paddle public key. from app.log import LOG diff --git a/email_handler.py b/email_handler.py index d94df13e..658c6a65 100644 --- a/email_handler.py +++ b/email_handler.py @@ -59,6 +59,7 @@ from app.config import ( ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, ALERT_BOUNCE_EMAIL, ALERT_SPAM_EMAIL, + ALERT_SPF, ) from app.email_utils import ( send_email, @@ -476,84 +477,17 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, str handle_bounce(contact, alias, msg, user, mailbox_email) return False, "550 SL E6" - mailb: Mailbox = Mailbox.get_by(email=mailbox_email) - if ENFORCE_SPF and mailb.force_spf: - if msg[_IP_HEADER]: - LOG.d("Enforce SPF") - try: - r = spf.check2(i=msg[_IP_HEADER], s=envelope.mail_from.lower(), h=None) - except Exception: - LOG.error( - "SPF error, mailbox %s, ip %s", mailbox_email, msg[_IP_HEADER] - ) - else: - # TODO: Handle temperr case (e.g. dns timeout) - # only an absolute pass, or no SPF policy at all is 'valid' - if r[0] not in ["pass", "none"]: - LOG.error( - "SPF fail for mailbox %s, reason %s, failed IP %s", - mailbox_email, - r[0], - msg[_IP_HEADER], - ) - return False, "451 SL E11" - else: - LOG.warning( - "Could not find %s header %s -> %s", _IP_HEADER, mailbox_email, address, - ) + mailbox: Mailbox = Mailbox.get_by(email=mailbox_email) + if ENFORCE_SPF and mailbox.force_spf: + ip = msg[_IP_HEADER] + if not spf_pass(ip, envelope, mailbox, user, alias, address): + return False, "451 SL E11" delete_header(msg, _IP_HEADER) # only mailbox can send email to the reply-email if envelope.mail_from.lower() != mailbox_email.lower(): - LOG.warning( - f"Reply email can only be used by mailbox. " - f"Actual mail_from: %s. msg from header: %s, Mailbox %s. reply_email %s", - envelope.mail_from, - msg["From"], - mailbox_email, - reply_email, - ) - - send_email_with_rate_control( - user, - ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, - mailbox_email, - f"Reply from your alias {alias.email} only works from your mailbox", - render( - "transactional/reply-must-use-personal-email.txt", - name=user.name, - alias=alias.email, - sender=envelope.mail_from, - mailbox_email=mailbox_email, - ), - render( - "transactional/reply-must-use-personal-email.html", - name=user.name, - alias=alias.email, - sender=envelope.mail_from, - mailbox_email=mailbox_email, - ), - ) - - # Notify sender that they cannot send emails to this address - send_email_with_rate_control( - user, - ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, - envelope.mail_from, - f"Your email ({envelope.mail_from}) is not allowed to send emails to {reply_email}", - render( - "transactional/send-from-alias-from-unknown-sender.txt", - sender=envelope.mail_from, - reply_email=reply_email, - ), - render( - "transactional/send-from-alias-from-unknown-sender.html", - sender=envelope.mail_from, - reply_email=reply_email, - ), - ) - + handle_unknown_mailbox(envelope, msg, mailbox, reply_email, user, alias) return False, "550 SL E7" delete_header(msg, "DKIM-Signature") @@ -619,6 +553,110 @@ def handle_reply(envelope, smtp: SMTP, msg: Message, rcpt_to: str) -> (bool, str return True, "250 Message accepted for delivery" +def spf_pass( + ip: str, envelope, mailbox: Mailbox, user: User, alias: Alias, contact_email: str +) -> bool: + if ip: + LOG.d("Enforce SPF") + try: + r = spf.check2(i=ip, s=envelope.mail_from.lower(), h=None) + except Exception: + LOG.error("SPF error, mailbox %s, ip %s", mailbox.email, ip) + else: + # TODO: Handle temperr case (e.g. dns timeout) + # only an absolute pass, or no SPF policy at all is 'valid' + if r[0] not in ["pass", "none"]: + LOG.error( + "SPF fail for mailbox %s, reason %s, failed IP %s", + mailbox.email, + r[0], + ip, + ) + send_email_with_rate_control( + user, + ALERT_SPF, + mailbox.email, + f"SimpleLogin Alert: attempt to send emails from your alias {alias.email} from unknown IP Address", + render( + "transactional/spf-fail.txt", + name=user.name, + alias=alias.email, + ip=ip, + mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", + ), + render( + "transactional/spf-fail.html", + name=user.name, + alias=alias.email, + ip=ip, + mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", + ), + ) + return False + + else: + LOG.warning( + "Could not find %s header %s -> %s", + _IP_HEADER, + mailbox.email, + contact_email, + ) + + return True + + +def handle_unknown_mailbox( + envelope, msg, mailbox: Mailbox, reply_email: str, user: User, alias: Alias +): + LOG.warning( + f"Reply email can only be used by mailbox. " + f"Actual mail_from: %s. msg from header: %s, Mailbox %s. reply_email %s", + envelope.mail_from, + msg["From"], + mailbox.email, + reply_email, + ) + + send_email_with_rate_control( + user, + ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, + mailbox.email, + f"Reply from your alias {alias.email} only works from your mailbox", + render( + "transactional/reply-must-use-personal-email.txt", + name=user.name, + alias=alias.email, + sender=envelope.mail_from, + mailbox_email=mailbox.email, + ), + render( + "transactional/reply-must-use-personal-email.html", + name=user.name, + alias=alias.email, + sender=envelope.mail_from, + mailbox_email=mailbox.email, + ), + ) + + # Notify sender that they cannot send emails to this address + send_email_with_rate_control( + user, + ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, + envelope.mail_from, + f"Your email ({envelope.mail_from}) is not allowed to send emails to {reply_email}", + render( + "transactional/send-from-alias-from-unknown-sender.txt", + sender=envelope.mail_from, + reply_email=reply_email, + ), + render( + "transactional/send-from-alias-from-unknown-sender.html", + sender=envelope.mail_from, + reply_email=reply_email, + ), + ) + + def handle_bounce( contact: Contact, alias: Alias, msg: Message, user: User, mailbox_email: str ): diff --git a/templates/emails/transactional/spf-fail.html b/templates/emails/transactional/spf-fail.html new file mode 100644 index 00000000..d8de6264 --- /dev/null +++ b/templates/emails/transactional/spf-fail.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} + {{ render_text("Hi " + name) }} + + {% call text() %} + We have recorded an attempt to send an email from your alias {{ alias }} from an unknown IP address + {{ ip }}. + {% endcall %} + + {% call text() %} + To prevent email-spoofing, SimpleLogin enforces the SPF (Sender Policy Framework). + Emails sent from an IP address that is unknown by your email service are refused by default. + {% endcall %} + + {% call text() %} + However you can turn off this option by going to {{ mailbox_url }}. + {% endcall %} + + {% call text() %} + Please only turn this protection off this if you know what you're doing :). + {% endcall %} + + {{ render_text('Thanks,
SimpleLogin Team.') }} +{% endblock %} diff --git a/templates/emails/transactional/spf-fail.txt b/templates/emails/transactional/spf-fail.txt new file mode 100644 index 00000000..e7ba08e7 --- /dev/null +++ b/templates/emails/transactional/spf-fail.txt @@ -0,0 +1,13 @@ +Hi {{name}} + +We have recorded an attempt to send an email from your alias {{ alias }} from an unknown IP address {{ ip }}. + +To prevent email-spoofing, SimpleLogin enforces the SPF (Sender Policy Framework). +Emails sent from an IP address that is unknown by your email service are refused by default. + +However you can turn off this option by going to {{mailbox_url}}. + +Please only turn this protection off this if you know what you're doing :). + +Best, +SimpleLogin team.