Merge pull request #176 from simple-login/spf2

Alert user when SPF fails
This commit is contained in:
Son Nguyen Kim 2020-05-09 23:16:14 +02:00 committed by GitHub
commit c308e9f9bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 197 additions and 105 deletions

View File

@ -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 >>>>

View File

@ -65,38 +65,6 @@
<!-- 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">
<form method="post">
<input type="hidden" name="form-name" value="pgp">
@ -137,6 +105,46 @@
</form>
</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>
{% endblock %}

View File

@ -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

View File

@ -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
):

View File

@ -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 %}

View File

@ -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.