Add disposable/high-risk email signup scrutiny via check-mail.org

Classifies each signup's email domain (domain-only, GDPR-friendly) alongside
the existing IP scrutiny signal, with matching held-welcome and auto
email_domain_block behavior. A report against an account with a flagged
domain suspends immediately (no reporter-count threshold) and blocks the
domain, classified live from the report payload rather than any signup-time
record so it also covers pre-existing accounts. Ships CHECK_MAIL_DRY_RUN=true
by default, independent of ABUSE_DRY_RUN, so the new report-triggered suspend
path stays inert until watched.
This commit is contained in:
waffle2k
2026-07-03 09:08:30 -07:00
parent 6ad9a93f38
commit 656eec09c0
5 changed files with 452 additions and 40 deletions
+99 -4
View File
@@ -96,8 +96,8 @@ def routing_tests():
})
assert r.status_code == 200, r.text
assert r.json().get("report") == "55", r.json()
# (report_id, target_id, acct, is_local, suspended, silenced, privileged)
assert reports == [("55", "777", "spammer", True, False, False, False)], reports
# (report_id, target_id, acct, is_local, suspended, silenced, privileged, email)
assert reports == [("55", "777", "spammer", True, False, False, False, "")], reports
# 5b. report against a staff account -> privileged=True extracted from role
r = post({
@@ -112,7 +112,7 @@ def routing_tests():
},
},
})
assert reports[-1] == ("56", "778", "waffles", True, False, False, True), reports[-1]
assert reports[-1] == ("56", "778", "waffles", True, False, False, True, ""), reports[-1]
# 6. account.approved also welcomes — but dedup means alice (id 1001,
# already welcomed via account.created above) is not welcomed twice.
@@ -267,7 +267,7 @@ def ip_scrutiny_tests():
main.process_signup("103", "dc2", "198.51.100.21")
assert ("103", "dc2") in sent, "dry-run must not hold the welcome"
assert ipblocks == [], "dry-run must not write an ip_block"
assert any(d.startswith("[DRY-RUN]") for d in dms), dms
assert any("[DRY-RUN]" in d for d in dms), dms
# D. abuse threshold override: a flagged account needs only
# IP_SCRUTINY_ABUSE_THRESHOLD (1) distinct reporter to silence, vs. the
@@ -293,8 +293,103 @@ def ip_scrutiny_tests():
assert ("101", "silence") not in actions, actions
def email_scrutiny_tests():
"""Drive process_signup's email-domain path + the report-triggered
suspend+domain-block override, network stubbed out."""
sent = []
dms = []
domain_blocks = []
main.send_welcome = lambda account_id, acct: sent.append((account_id, acct))
main.dm_moderator = lambda message: dms.append(message)
main.register_email_domain_block = lambda domain, acct: domain_blocks.append((domain, acct))
classifications = {
"gmail.com": (False, 5),
"temp-mail.org": (True, 99),
"sketchy-disposable.example": (True, 95),
}
main.classify_email_domain = lambda domain: classifications[domain]
main.CHECK_MAIL_ENABLED = True
main.CHECK_MAIL_API_KEY = "test-key" # required for the signup-path gate
main.IP_SCRUTINY_ENABLED = False # isolate the email-only signal
# A. clean domain -> welcomed immediately, no domain-block, not flagged.
main.CHECK_MAIL_DRY_RUN = False
main.CHECK_MAIL_HOLD_WELCOME = True
main.CHECK_MAIL_AUTO_DOMAIN_BLOCK = True
main.process_signup("201", "clean1", "", "clean1@gmail.com")
assert ("201", "clean1") in sent, sent
assert domain_blocks == [], domain_blocks
assert main.get_signup_email("201") == ("gmail.com", False)
# B. disposable domain, HOLD_WELCOME + AUTO_DOMAIN_BLOCK, live -> welcome
# held, domain blocked, moderator DM'd; account.approved later releases
# the welcome.
sent.clear(); dms.clear(); domain_blocks.clear()
main.process_signup("202", "spam1", "", "spam1@temp-mail.org")
assert ("202", "spam1") not in sent, "welcome should be held for a flagged signup"
assert domain_blocks == [("temp-mail.org", "spam1")], domain_blocks
assert any("spam1" in d and "held" in d for d in dms), dms
assert main.get_signup_email("202") == ("temp-mail.org", True)
main.send_welcome("202", "spam1") # simulate account.approved's unconditional welcome
assert ("202", "spam1") in sent, sent
# C. same idea, but CHECK_MAIL_DRY_RUN -> welcomed immediately (no hold),
# no domain-block write, DM carries the dry-run marker.
sent.clear(); dms.clear(); domain_blocks.clear()
main.CHECK_MAIL_DRY_RUN = True
main.process_signup("203", "spam2", "", "spam2@sketchy-disposable.example")
assert ("203", "spam2") in sent, "dry-run must not hold the welcome"
assert domain_blocks == [], "dry-run must not write a domain block"
assert any("[DRY-RUN]" in d for d in dms), dms
# D0. CHECK_MAIL_DRY_RUN alone holds the report-triggered suspend even
# though ABUSE_DRY_RUN=False in this test env (set in the module
# preamble) — the two dry-run switches are independent; either one
# holding is enough to keep this brand-new action path inert.
actions = []
main.apply_action = lambda target_id, action, text: actions.append((target_id, action))
main.dm_silenced_user = lambda acct: None
domain_blocks.clear()
main.handle_report("R9", "997", "heldbydryrun", True, False, False, False,
"heldbydryrun@temp-mail.org")
assert ("997", "suspend") not in actions, actions
assert domain_blocks == [], "CHECK_MAIL_DRY_RUN alone must prevent the domain block too"
# D. report-triggered override: ANY report against an account with a
# high-risk email domain suspends immediately (no distinct-reporter
# threshold) and blocks the domain — classified LIVE from the report
# payload's target_account.email, independent of whatever (if
# anything) process_signup recorded at signup time. This account was
# never seen by process_signup at all.
main.CHECK_MAIL_DRY_RUN = False
actions.clear()
domain_blocks.clear()
main.handle_report("R10", "999", "neversignedup", True, False, False, False,
"neversignedup@temp-mail.org")
assert ("999", "suspend") in actions, actions
assert domain_blocks == [("temp-mail.org", "neversignedup")], domain_blocks
# E. report against an account with a clean email domain -> untouched by
# this path, falls through to the normal reporter-count/tier policy.
actions.clear(); domain_blocks.clear()
noposts = {"has_posts": False, "young": False, "dormant": False,
"newest_at": None, "oldest_seen": None, "truncated": False}
main.classify_account = lambda tid: noposts
main.count_distinct_reporters = lambda tid: 1
main.handle_report("R11", "998", "clean2", True, False, False, False,
"clean2@gmail.com")
assert ("998", "suspend") not in actions, actions
assert domain_blocks == [], domain_blocks
if __name__ == "__main__":
routing_tests()
policy_tests()
ip_scrutiny_tests()
email_scrutiny_tests()
print("ALL TESTS PASSED")