Merge pull request #176 from simple-login/spf2
Alert user when SPF fails
This commit is contained in:
commit
c308e9f9bf
|
@ -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 >>>>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
182
email_handler.py
182
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)
|
mailbox: Mailbox = Mailbox.get_by(email=mailbox_email)
|
||||||
if ENFORCE_SPF and mailb.force_spf:
|
if ENFORCE_SPF and mailbox.force_spf:
|
||||||
if msg[_IP_HEADER]:
|
ip = msg[_IP_HEADER]
|
||||||
LOG.d("Enforce SPF")
|
if not spf_pass(ip, envelope, mailbox, user, alias, address):
|
||||||
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"
|
return False, "451 SL E11"
|
||||||
else:
|
|
||||||
LOG.warning(
|
|
||||||
"Could not find %s header %s -> %s", _IP_HEADER, mailbox_email, address,
|
|
||||||
)
|
|
||||||
|
|
||||||
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(
|
handle_unknown_mailbox(envelope, msg, mailbox, reply_email, user, alias)
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
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
templates/emails/transactional/spf-fail.html
Normal file
25
templates/emails/transactional/spf-fail.html
Normal 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 %}
|
13
templates/emails/transactional/spf-fail.txt
Normal file
13
templates/emails/transactional/spf-fail.txt
Normal 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.
|
Loading…
Reference in a new issue