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

View File

@ -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]}"

View File

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

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