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:
Son Nguyen Kim 2022-10-10 10:13:07 +02:00 committed by GitHub
parent 5088604bb8
commit 1c5a547cd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 10 deletions

View File

@ -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"
)
# 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
ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT = (
"ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT" in os.environ

View File

@ -5,7 +5,7 @@ from typing import Optional, Tuple
from aiosmtpd.handlers import Message
from aiosmtpd.smtp import Envelope
from app import s3
from app import s3, config
from app.config import (
DMARC_CHECK_ENABLED,
ALERT_QUARANTINE_DMARC,
@ -34,6 +34,37 @@ def apply_dmarc_policy_for_forward_phase(
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:
LOG.w(
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(
msg,
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/
""",
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>
""",
warning_plain_text,
warning_html,
)
return changed_msg, None
@ -133,6 +157,7 @@ def apply_dmarc_policy_for_reply_phase(
DmarcCheckResult.soft_fail,
):
return None
LOG.w(
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]}"

View File

@ -4,6 +4,7 @@ from typing import Dict, Optional
import newrelic.agent
from app.email import headers
from app.log import LOG
from app.models import EnumE, Phase
from email.message import Message
@ -55,6 +56,7 @@ class SpamdResult:
self.phase: Phase = phase
self.dmarc: DmarcCheckResult = DmarcCheckResult.not_available
self.spf: SPFCheckResult = SPFCheckResult.not_available
self.rspamd_score = -1
def set_dmarc_result(self, dmarc_result: DmarcCheckResult):
self.dmarc = dmarc_result
@ -85,6 +87,7 @@ class SpamdResult:
spam_entries = [
entry.strip() for entry in str(spam_result_header[-1]).split("\n")
]
for entry_pos in range(len(spam_entries)):
sep = spam_entries[entry_pos].find("(")
if sep > -1:
@ -101,6 +104,17 @@ class SpamdResult:
spamd_result.set_spf_result(spf_result)
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)
return spamd_result

View File

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

View File

@ -5,6 +5,7 @@ from tests.utils import load_eml_file
def test_dmarc_result_softfail():
msg = load_eml_file("dmarc_gmail_softfail.eml")
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():
@ -32,3 +33,14 @@ def test_dmarc_result_bad_policy():
assert SpamdResult._get_from_message(msg) is None
assert DmarcCheckResult.bad_policy == SpamdResult.extract_from_headers(msg).dmarc
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