Browse Source

Merge pull request #176 from simple-login/spf2

Alert user when SPF fails
Son Nguyen Kim 5 years ago
parent
commit
c308e9f9bf

+ 6 - 0
app/config.py

@@ -255,6 +255,8 @@ APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET")
 # for Mac App
 # for Mac App
 MACAPP_APPLE_API_SECRET = os.environ.get("MACAPP_APPLE_API_SECRET")
 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
 # maximal number of alerts that can be sent to the same email in 24h
 MAX_ALERT_24H = 4
 MAX_ALERT_24H = 4
 
 
@@ -266,3 +268,7 @@ ALERT_BOUNCE_EMAIL = "bounce"
 
 
 # When a forwarding email is detected as spam
 # When a forwarding email is detected as spam
 ALERT_SPAM_EMAIL = "spam"
 ALERT_SPAM_EMAIL = "spam"
+
+ALERT_SPF = "spf"
+
+# <<<<< END ALERT EMAIL >>>>

+ 40 - 32
app/dashboard/templates/dashboard/mailbox_detail.html

@@ -65,38 +65,6 @@
       <!-- END Change email -->
       <!-- END Change email -->
 
 
 
 
-      {% if spf_available %}
-        <!--
-      <div class="card">
-        <form method="post">
-          <input type="hidden" name="form-name" value="force-spf">
-
-          <div class="card-body">
-            <div class="card-title">
-              Enforce SPF
-              <div class="small-text">
-                Block emails to reverse alias if sender is not validated by SPF,
-                even when SPF is configured as soft-fail.
-              </div>
-            </div>
-            <label class="custom-switch cursor mt-2 pl-0"
-                   data-toggle="tooltip"
-                   {% if mailbox.force_spf %}
-                   title="Disable SPF enforcement"
-                   {% else %}
-                   title="Enable SPF enforcement"
-                   {% endif %}
-            >
-              <input type="checkbox" name="spf-status" class="custom-switch-input"
-                     {{ "checked" if mailbox.force_spf else "" }}>
-              <span class="custom-switch-indicator"></span>
-            </label>
-          </div>
-        </form>
-      </div>
-      -->
-      {% endif %}
-
       <div class="card">
       <div class="card">
         <form method="post">
         <form method="post">
           <input type="hidden" name="form-name" value="pgp">
           <input type="hidden" name="form-name" value="pgp">
@@ -137,6 +105,46 @@
         </form>
         </form>
 
 
       </div>
       </div>
+
+      <hr>
+      <h2 class="h4">Advanced Options</h2>
+
+      {% if spf_available %}
+        <div class="card" id="spf">
+          <form method="post">
+            <input type="hidden" name="form-name" value="force-spf">
+
+            <div class="card-body">
+              <div class="card-title">
+                Enforce SPF
+                <div class="small-text">
+                  To avoid email-spoofing, SimpleLogin blocks email that
+                  <em data-toggle="tooltip"
+                      title="Email that has your mailbox as envelope-sender address">seems</em> to come from your
+                  mailbox
+                  but sent from <em data-toggle="tooltip"
+                                    title="IP Address that is not known by your mailbox email service">unknown</em>
+                  IP address.
+                  <br>
+                  Only turn off this option if you know what you're doing :).
+                </div>
+              </div>
+              <label class="custom-switch cursor mt-2 pl-0"
+                     data-toggle="tooltip"
+                  {% if mailbox.force_spf %}
+                     title="Disable SPF enforcement"
+                  {% else %}
+                     title="Enable SPF enforcement"
+                  {% endif %}
+              >
+                <input type="checkbox" name="spf-status" class="custom-switch-input"
+                    {{ "checked" if mailbox.force_spf else "" }}>
+                <span class="custom-switch-indicator"></span>
+              </label>
+            </div>
+          </form>
+        </div>
+      {% endif %}
     </div>
     </div>
   </div>
   </div>
 {% endblock %}
 {% endblock %}

+ 2 - 0
app/paddle_utils.py

@@ -10,11 +10,13 @@ import collections
 import phpserialize
 import phpserialize
 import requests
 import requests
 from Crypto.Hash import SHA1
 from Crypto.Hash import SHA1
+
 # Crypto can be found at https://pypi.org/project/pycryptodome/
 # Crypto can be found at https://pypi.org/project/pycryptodome/
 from Crypto.PublicKey import RSA
 from Crypto.PublicKey import RSA
 from Crypto.Signature import PKCS1_v1_5
 from Crypto.Signature import PKCS1_v1_5
 
 
 from app.config import PADDLE_PUBLIC_KEY_PATH, PADDLE_VENDOR_ID, PADDLE_AUTH_CODE
 from app.config import PADDLE_PUBLIC_KEY_PATH, PADDLE_VENDOR_ID, PADDLE_AUTH_CODE
+
 # Your Paddle public key.
 # Your Paddle public key.
 from app.log import LOG
 from app.log import LOG
 
 

+ 111 - 73
email_handler.py

@@ -59,6 +59,7 @@ from app.config import (
     ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX,
     ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX,
     ALERT_BOUNCE_EMAIL,
     ALERT_BOUNCE_EMAIL,
     ALERT_SPAM_EMAIL,
     ALERT_SPAM_EMAIL,
+    ALERT_SPF,
 )
 )
 from app.email_utils import (
 from app.email_utils import (
     send_email,
     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)
         handle_bounce(contact, alias, msg, user, mailbox_email)
         return False, "550 SL E6"
         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)
     delete_header(msg, _IP_HEADER)
 
 
     # only mailbox can send email to the reply-email
     # only mailbox can send email to the reply-email
     if envelope.mail_from.lower() != mailbox_email.lower():
     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"
         return False, "550 SL E7"
 
 
     delete_header(msg, "DKIM-Signature")
     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"
     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(
 def handle_bounce(
     contact: Contact, alias: Alias, msg: Message, user: User, mailbox_email: str
     contact: Contact, alias: Alias, msg: Message, user: User, mailbox_email: str
 ):
 ):

+ 25 - 0
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 <b>{{ alias }}</b> from an unknown IP address
+    <b>{{ ip }}</b>.
+  {% endcall %}
+
+  {% call text() %}
+    To prevent email-spoofing, SimpleLogin enforces the SPF (Sender Policy Framework).
+    Emails sent from an IP address that is <b>unknown</b> 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, <br />SimpleLogin Team.') }}
+{% endblock %}

+ 13 - 0
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.