do not quarantine an email if fails DMARC but has a small rspamd score (#1337)
* do not quarantine an email if fails DMARC but has a small rspamd score * use 0 when cannot parse rspamd score * use -1 as default value
This commit is contained in:
parent
5088604bb8
commit
1c5a547cd0
|
@ -504,6 +504,15 @@ if not RECOVERY_CODE_HMAC_SECRET or len(RECOVERY_CODE_HMAC_SECRET) < 16:
|
||||||
"Please define RECOVERY_CODE_HMAC_SECRET in your configuration with a random string at least 16 chars long"
|
"Please define RECOVERY_CODE_HMAC_SECRET in your configuration with a random string at least 16 chars long"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# the minimum rspamd spam score above which emails that fail DMARC should be quarantined
|
||||||
|
if "MIN_RSPAMD_SCORE_FOR_FAILED_DMARC" in os.environ:
|
||||||
|
MIN_RSPAMD_SCORE_FOR_FAILED_DMARC = float(
|
||||||
|
os.environ["MIN_RSPAMD_SCORE_FOR_FAILED_DMARC"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
MIN_RSPAMD_SCORE_FOR_FAILED_DMARC = None
|
||||||
|
|
||||||
# run over all reverse alias for an alias and replace them with sender address
|
# run over all reverse alias for an alias and replace them with sender address
|
||||||
ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT = (
|
ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT = (
|
||||||
"ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT" in os.environ
|
"ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT" in os.environ
|
||||||
|
|
|
@ -5,7 +5,7 @@ from typing import Optional, Tuple
|
||||||
from aiosmtpd.handlers import Message
|
from aiosmtpd.handlers import Message
|
||||||
from aiosmtpd.smtp import Envelope
|
from aiosmtpd.smtp import Envelope
|
||||||
|
|
||||||
from app import s3
|
from app import s3, config
|
||||||
from app.config import (
|
from app.config import (
|
||||||
DMARC_CHECK_ENABLED,
|
DMARC_CHECK_ENABLED,
|
||||||
ALERT_QUARANTINE_DMARC,
|
ALERT_QUARANTINE_DMARC,
|
||||||
|
@ -34,6 +34,37 @@ def apply_dmarc_policy_for_forward_phase(
|
||||||
|
|
||||||
from_header = get_header_unicode(msg[headers.FROM])
|
from_header = get_header_unicode(msg[headers.FROM])
|
||||||
|
|
||||||
|
warning_plain_text = f"""This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
||||||
|
More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
||||||
|
"""
|
||||||
|
warning_html = f"""
|
||||||
|
<p style="color:red">
|
||||||
|
This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
||||||
|
More info on <a href="https://simplelogin.io/docs/getting-started/anti-phishing/">anti-phishing measure</a>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# do not quarantine an email if fails DMARC but has a small rspamd score
|
||||||
|
if (
|
||||||
|
config.MIN_RSPAMD_SCORE_FOR_FAILED_DMARC is not None
|
||||||
|
and spam_result.rspamd_score < config.MIN_RSPAMD_SCORE_FOR_FAILED_DMARC
|
||||||
|
and spam_result.dmarc
|
||||||
|
in (
|
||||||
|
DmarcCheckResult.quarantine,
|
||||||
|
DmarcCheckResult.reject,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
LOG.w(
|
||||||
|
f"email fails DMARC but has a small rspamd score, from contact {contact.email} to alias {alias.email}."
|
||||||
|
f"mail_from:{envelope.mail_from}, from_header: {from_header}"
|
||||||
|
)
|
||||||
|
changed_msg = add_header(
|
||||||
|
msg,
|
||||||
|
warning_plain_text,
|
||||||
|
warning_html,
|
||||||
|
)
|
||||||
|
return changed_msg, None
|
||||||
|
|
||||||
if spam_result.dmarc == DmarcCheckResult.soft_fail:
|
if spam_result.dmarc == DmarcCheckResult.soft_fail:
|
||||||
LOG.w(
|
LOG.w(
|
||||||
f"dmarc forward: soft_fail from contact {contact.email} to alias {alias.email}."
|
f"dmarc forward: soft_fail from contact {contact.email} to alias {alias.email}."
|
||||||
|
@ -41,15 +72,8 @@ def apply_dmarc_policy_for_forward_phase(
|
||||||
)
|
)
|
||||||
changed_msg = add_header(
|
changed_msg = add_header(
|
||||||
msg,
|
msg,
|
||||||
f"""This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
warning_plain_text,
|
||||||
More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
warning_html,
|
||||||
""",
|
|
||||||
f"""
|
|
||||||
<p style="color:red">
|
|
||||||
This email failed anti-phishing checks when it was received by SimpleLogin, be careful with its content.
|
|
||||||
More info on <a href="https://simplelogin.io/docs/getting-started/anti-phishing/">anti-phishing measure</a>
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
return changed_msg, None
|
return changed_msg, None
|
||||||
|
|
||||||
|
@ -133,6 +157,7 @@ def apply_dmarc_policy_for_reply_phase(
|
||||||
DmarcCheckResult.soft_fail,
|
DmarcCheckResult.soft_fail,
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
LOG.w(
|
LOG.w(
|
||||||
f"dmarc reply: Put email from {alias_from.email} to {contact_recipient} into quarantine. {spam_result.event_data()}, "
|
f"dmarc reply: Put email from {alias_from.email} to {contact_recipient} into quarantine. {spam_result.event_data()}, "
|
||||||
f"mail_from:{envelope.mail_from}, from_header: {msg[headers.FROM]}"
|
f"mail_from:{envelope.mail_from}, from_header: {msg[headers.FROM]}"
|
||||||
|
|
|
@ -4,6 +4,7 @@ from typing import Dict, Optional
|
||||||
import newrelic.agent
|
import newrelic.agent
|
||||||
|
|
||||||
from app.email import headers
|
from app.email import headers
|
||||||
|
from app.log import LOG
|
||||||
from app.models import EnumE, Phase
|
from app.models import EnumE, Phase
|
||||||
from email.message import Message
|
from email.message import Message
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ class SpamdResult:
|
||||||
self.phase: Phase = phase
|
self.phase: Phase = phase
|
||||||
self.dmarc: DmarcCheckResult = DmarcCheckResult.not_available
|
self.dmarc: DmarcCheckResult = DmarcCheckResult.not_available
|
||||||
self.spf: SPFCheckResult = SPFCheckResult.not_available
|
self.spf: SPFCheckResult = SPFCheckResult.not_available
|
||||||
|
self.rspamd_score = -1
|
||||||
|
|
||||||
def set_dmarc_result(self, dmarc_result: DmarcCheckResult):
|
def set_dmarc_result(self, dmarc_result: DmarcCheckResult):
|
||||||
self.dmarc = dmarc_result
|
self.dmarc = dmarc_result
|
||||||
|
@ -85,6 +87,7 @@ class SpamdResult:
|
||||||
spam_entries = [
|
spam_entries = [
|
||||||
entry.strip() for entry in str(spam_result_header[-1]).split("\n")
|
entry.strip() for entry in str(spam_result_header[-1]).split("\n")
|
||||||
]
|
]
|
||||||
|
|
||||||
for entry_pos in range(len(spam_entries)):
|
for entry_pos in range(len(spam_entries)):
|
||||||
sep = spam_entries[entry_pos].find("(")
|
sep = spam_entries[entry_pos].find("(")
|
||||||
if sep > -1:
|
if sep > -1:
|
||||||
|
@ -101,6 +104,17 @@ class SpamdResult:
|
||||||
spamd_result.set_spf_result(spf_result)
|
spamd_result.set_spf_result(spf_result)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# parse the rspamd score
|
||||||
|
try:
|
||||||
|
score_line = spam_entries[0] # e.g. "default: False [2.30 / 13.00];"
|
||||||
|
spamd_result.rspamd_score = float(
|
||||||
|
score_line[(score_line.find("[") + 1) : score_line.find("]")]
|
||||||
|
.split("/")[0]
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
LOG.e("cannot parse rspamd score")
|
||||||
|
|
||||||
cls._store_in_message(spamd_result, msg)
|
cls._store_in_message(spamd_result, msg)
|
||||||
return spamd_result
|
return spamd_result
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||||
|
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||||
|
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||||
|
receiver=<UNKNOWN>
|
||||||
|
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||||
|
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||||
|
(No client certificate requested)
|
||||||
|
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||||
|
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||||
|
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||||
|
To: {{ alias_email }}
|
||||||
|
From: spoofedemailsource@gmail.com
|
||||||
|
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||||
|
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||||
|
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||||
|
X-Rspamd-Queue-Id: 6D8C13F069
|
||||||
|
X-Rspamd-Server: staging1
|
||||||
|
X-Spamd-Result: default: False [WRONGLY_FORMATTED / 13.00];
|
||||||
|
MID_RHS_NOT_FQDN(0.50)[];
|
||||||
|
DMARC_POLICY_SOFTFAIL(0.10)[gmail.com : No valid SPF, No valid DKIM,none];
|
||||||
|
MIME_GOOD(-0.10)[text/plain];
|
||||||
|
MIME_TRACE(0.00)[0:+];
|
||||||
|
FROM_EQ_ENVFROM(0.00)[];
|
||||||
|
ASN(0.00)[asn:16276, ipnet:34.59.0.0/16, country:FR];
|
||||||
|
R_DKIM_NA(0.00)[];
|
||||||
|
RCVD_COUNT_ZERO(0.00)[0];
|
||||||
|
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||||
|
FROM_NO_DN(0.00)[];
|
||||||
|
R_SPF_SOFTFAIL(0.00)[~all];
|
||||||
|
FORCE_ACTION_SL_SPF_FAIL_ADD_HEADER(0.00)[add header];
|
||||||
|
RCPT_COUNT_ONE(0.00)[1];
|
||||||
|
FREEMAIL_FROM(0.00)[gmail.com];
|
||||||
|
TO_DN_NONE(0.00)[];
|
||||||
|
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||||
|
ARC_NA(0.00)[]
|
||||||
|
X-Rspamd-Pre-Result: action=add header;
|
||||||
|
module=force_actions;
|
||||||
|
unknown reason
|
||||||
|
X-Spam: Yes
|
||||||
|
|
||||||
|
This is a test mailing
|
|
@ -5,6 +5,7 @@ from tests.utils import load_eml_file
|
||||||
def test_dmarc_result_softfail():
|
def test_dmarc_result_softfail():
|
||||||
msg = load_eml_file("dmarc_gmail_softfail.eml")
|
msg = load_eml_file("dmarc_gmail_softfail.eml")
|
||||||
assert DmarcCheckResult.soft_fail == SpamdResult.extract_from_headers(msg).dmarc
|
assert DmarcCheckResult.soft_fail == SpamdResult.extract_from_headers(msg).dmarc
|
||||||
|
assert SpamdResult.extract_from_headers(msg).rspamd_score == 0.5
|
||||||
|
|
||||||
|
|
||||||
def test_dmarc_result_quarantine():
|
def test_dmarc_result_quarantine():
|
||||||
|
@ -32,3 +33,14 @@ def test_dmarc_result_bad_policy():
|
||||||
assert SpamdResult._get_from_message(msg) is None
|
assert SpamdResult._get_from_message(msg) is None
|
||||||
assert DmarcCheckResult.bad_policy == SpamdResult.extract_from_headers(msg).dmarc
|
assert DmarcCheckResult.bad_policy == SpamdResult.extract_from_headers(msg).dmarc
|
||||||
assert SpamdResult._get_from_message(msg) is not None
|
assert SpamdResult._get_from_message(msg) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_rspamd_score():
|
||||||
|
msg = load_eml_file("dmarc_gmail_softfail.eml")
|
||||||
|
assert SpamdResult.extract_from_headers(msg).rspamd_score == 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_parse_rspamd_score():
|
||||||
|
msg = load_eml_file("dmarc_cannot_parse_rspamd_score.eml")
|
||||||
|
# use the default score when cannot parse
|
||||||
|
assert SpamdResult.extract_from_headers(msg).rspamd_score == -1
|
||||||
|
|
Loading…
Reference in New Issue