优化 AWS 邮件验证码过滤与有效字段输出

This commit is contained in:
2026-10-08 15:29:35 +08:00
parent d1398b9475
commit 2174b61606
7 changed files with 1169 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
"""Offline classification and extraction regression tests."""
import email
from email import policy
from email.message import EmailMessage
from email.utils import parsedate_to_datetime
from pathlib import Path
import sys
import unittest
from zoneinfo import ZoneInfo
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import check_mail
def sample(name, folder="参考邮件"):
path = next((ROOT / folder).glob(name), None)
if path is None:
raise unittest.SkipTest("本地真实邮件样本未随仓库分发")
message = email.message_from_bytes(path.read_bytes(), policy=policy.default)
received = parsedate_to_datetime(check_mail.header_value(message, "Date"))
return check_mail.extract(message, received)
def synthetic(subject, body, rich=False):
message = EmailMessage()
message["Subject"] = subject
message.set_content(body, subtype="html" if rich else "plain")
return check_mail.extract(message, parsedate_to_datetime("Mon, 28 Sep 2026 12:00:00 +0800"))
class ExtractionTests(unittest.TestCase):
def test_four_forwarded_reference_messages(self):
expected = {
"*Account_on_Hold*": "账户验证或暂停",
"*账户已暂停*": "账户验证或暂停",
"*Abuse_Report*": "资源滥用报告",
"*存在可疑活动*": "账号可疑活动警告",
}
for pattern, kind in expected.items():
with self.subTest(pattern=pattern):
record = sample(pattern)
self.assertIsNotNone(record)
self.assertEqual(record["type"], kind)
self.assertNotEqual(record["warning"], check_mail.EMPTY)
def test_forwarded_abuse_report_keeps_explicit_account_and_resources(self):
record = sample("*Abuse_Report*")
self.assertRegex(record["account"], r"^\d{12}$")
self.assertRegex(record["resources"], r"\bi-[0-9a-f]{8,17}\b")
self.assertEqual(record["deadline"], check_mail.EMPTY)
def test_permanent_closure_confirmation_does_not_invent_body_facts(self):
record = synthetic(
"AWS Account Permanent Closure Confirmation",
"This confirms your account is permanently closed.",
)
self.assertIsNotNone(record)
self.assertEqual(record["type"], "账户永久关闭确认")
self.assertEqual(record["account"], check_mail.EMPTY)
self.assertEqual(record["warning"], check_mail.EMPTY)
self.assertEqual(record["action"], check_mail.EMPTY)
self.assertEqual(record["deadline"], check_mail.EMPTY)
def test_identity_verification_footer_does_not_imply_compromise(self):
record = synthetic(
"验证您的身份",
"AWS 身份验证通知。为保护您的安全,如果您发现未授权访问,请联系支持团队。",
)
self.assertEqual(record["type"], "身份验证通知")
self.assertEqual(record["warning"], check_mail.EMPTY)
self.assertEqual(record["action"], check_mail.EMPTY)
self.assertEqual(record["account"], check_mail.EMPTY)
self.assertEqual(record["deadline"], check_mail.EMPTY)
def test_specific_subject_outweighs_quoted_phishing_report(self):
record = synthetic(
"AWS Account on Hold: Response Required",
"Your AWS account has been placed on hold.\n--- Forwarded history ---\n"
"An earlier AWS resource was implicated in hosting content that resembles a phishing website.",
)
self.assertEqual(record["type"], "账户验证或暂停")
def test_local_forwarded_login_code_is_filtered(self):
self.assertIsNone(sample("*.eml", "过滤邮件"))
def test_login_codes_with_security_boilerplate_are_filtered(self):
cases = [
("FW: 验证您的身份", "AWS 登录验证。您的验证码是 123456,10 分钟后过期。"
"您是否发现异常活动?如果您没有发起登录,请更换密码并启用 MFA。", False),
("Fwd: AWS Verify your identity", "Your verification code is 123456. "
"Use this code to sign in. If you notice suspicious activity or unauthorized access, "
"change your password and enable multi-factor authentication (MFA).", False),
("AWS authentication", "AWS one-time passcode: 654321. Use this code to log in. "
"It expires in 10 minutes. Verify your identity.", False),
("AWS 验证您的身份", "<div>请输入验证码</div><strong>123456</strong>"
"<p>以完成登录。如果发现异常活动,请更换密码。</p>", True),
("AWS verification code", "<p>Your security code:</p><b>123456</b>"
"<p>It expires in 10 minutes. Use this code to sign in.</p>", True),
]
for subject, body, rich in cases:
with self.subTest(subject=subject, rich=rich):
self.assertIsNone(synthetic(subject, body, rich))
def test_real_risk_with_verification_or_mfa_is_retained(self):
cases = [
("AWS Account on Hold: Verification Required", "Your account has been placed on hold. "
"Please upload your identity documents. MFA security code: 123456; use this code to sign in."),
("AWS 身份验证", "您的账户已暂停。请上传身份证核验材料。验证码 123456,可用于登录。"),
("AWS verify your identity", "Please submit proof of identity and a bank statement. "
"Your verification code is 123456. Use this code to sign in."),
("AWS Account Permanent Closure Confirmation", "Your account is permanently closed. "
"Verification code: 123456. Use this code to sign in."),
("AWS Security verification", "We have limited your ability to access AWS services. "
"Your security code is 123456. Use this code to sign in."),
("AWS Security warning", "Your access key has been compromised. "
"Verification code: 123456. Use this code to sign in."),
("AWS Account Suspended", "Verification code: 123456. Use this code to sign in."),
("AWS Account Restricted", "Verification code: 123456. Use this code to sign in."),
("AWS 账号受限", "验证码 123456,用于登录。"),
]
for subject, body in cases:
with self.subTest(subject=subject):
self.assertIsNotNone(synthetic(subject, body))
def test_conditional_security_tips_do_not_make_login_codes_into_alerts(self):
cases = [
("AWS verification code", "Your verification code is 987654. Use this code to sign in.\n"
"If you think your access key has been compromised, change your password and enable MFA."),
("AWS 验证码", "您的验证码是 987654,用于登录。\n如果您的账户已暂停,请联系支持。"),
("AWS verification code", "Your verification code is 987654. Use this code to sign in. "
"If your account is suspended, please submit identity documents to support."),
("AWS verification code", "Your verification code is 987654. Use this code to sign in. "
"If you need help, please provide identity documents to support."),
("AWS 验证码", "验证码 987654,用于登录。如果您需要帮助,请提交身份证材料给支持团队。"),
]
for subject, body in cases:
with self.subTest(subject=subject):
self.assertIsNone(synthetic(subject, body))
def test_explicit_risk_and_nonresponse_consequences_survive_conditional_tips(self):
english_code = "Your verification code is 987654. Use this code to sign in. "
cases = [
("AWS verification code", english_code +
"If you think your access key has been compromised, enable MFA. "
"Your account has been placed on hold."),
("AWS verification code", english_code +
"Your account has been placed on hold, if you think your access key has been compromised, enable MFA."),
("AWS verification code", english_code +
"If you think your access key has been compromised, enable MFA, your account has been suspended."),
("AWS 验证码", "验证码 987654,用于登录。如果您的账户已暂停,请联系支持。您的账户已受限。"),
("AWS verification code", english_code +
"If you fail to respond by 2026-10-10 your account will be suspended."),
("AWS verification code", english_code +
"If you do not upload documents by 2026-10-10, your account will be suspended."),
("AWS 验证码", "验证码 987654,用于登录。如果您未能在截止日期回复您的账号将被暂停。"),
("AWS verification code", english_code +
"Your access key has been compromised. If your account is suspended, contact support."),
("AWS verification code", english_code +
"If you need help, please provide identity documents to support, your account has been suspended."),
("AWS verification code", english_code +
"If you need help, please provide identity documents to support. Please upload proof of identity."),
]
for subject, body in cases:
with self.subTest(subject=subject, body=body):
self.assertIsNotNone(synthetic(subject, body))
def test_report_omits_missing_fields_without_reordering(self):
record = synthetic("AWS Account Permanent Closure Confirmation", "Account permanently closed.")
report = check_mail.format_report(record, ZoneInfo("Asia/Shanghai"))
self.assertEqual([line.split(":", 1)[0] for line in report.splitlines()],
["【AWS账号风控邮件】", "收件时间", "邮件主题", "风险类型"])
self.assertNotIn(check_mail.EMPTY, report)
self.assertNotIn("AWS账号ID", report)
def test_report_omits_none_empty_and_absent_fields(self):
report = check_mail.format_report({
"subject": "", "account": None, "type": "账户验证或暂停",
"warning": " ", "resources": check_mail.EMPTY, "deadline": "2026-10-10",
}, ZoneInfo("Asia/Shanghai"))
self.assertEqual(report.splitlines(), ["【AWS账号风控邮件】", "风险类型:账户验证或暂停",
"截止时间:2026-10-10"])
def test_complete_report_keeps_field_order(self):
record = synthetic("AWS Account on Hold: Response Required",
"AWS Account ID: 123456789012\nYour account has been placed on hold.\n"
"Resource i-12345678\nPlease reply before 2026-10-10.")
self.assertEqual(record["account"], "123456789012")
self.assertEqual(record["resources"], "i-12345678")
report = check_mail.format_report(record, ZoneInfo("Asia/Shanghai"))
self.assertEqual([line.split(":", 1)[0] for line in report.splitlines()],
["【AWS账号风控邮件】", "收件时间", "邮件主题", "AWS账号ID", "风险类型",
"警告信息", "涉及资源", "要求操作", "截止时间"])
if __name__ == "__main__":
unittest.main()
+143
View File
@@ -0,0 +1,143 @@
"""Offline protocol tests for the IMAP ID handshake."""
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from unittest.mock import patch
from zoneinfo import ZoneInfo
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import check_mail
class FakeIMAP:
def __init__(self, id_status="OK", id_data=None, select_status="OK", select_data=None,
id_error=None, login_error=None, login_status="OK", login_data=None):
self.calls = []
self.id_status = id_status
self.id_data = id_data if id_data is not None else [b"ID completed"]
self.select_status = select_status
self.select_data = select_data if select_data is not None else [b"0"]
self.id_error = id_error
self.login_error = login_error
self.login_status = login_status
self.login_data = login_data if login_data is not None else [b"logged in"]
def __enter__(self):
return self
def __exit__(self, *_args):
pass
def xatom(self, name, arguments):
self.calls.append(("ID", name, arguments))
if self.id_error:
raise self.id_error
return self.id_status, self.id_data
def login(self, username, authorization_code):
self.calls.append(("LOGIN", username, authorization_code))
if self.login_error:
raise self.login_error
return self.login_status, self.login_data
def select(self, mailbox, readonly=False):
self.calls.append(("SELECT", mailbox, readonly))
return self.select_status, self.select_data
def response(self, name):
self.calls.append(("RESPONSE", name))
return "OK", [b"123"]
def uid(self, command, *args):
self.calls.append(("UID", command, args))
return "OK", [b""]
class ImapIdTests(unittest.TestCase):
def setUp(self):
self.settings = {
"host": "imap.163.com",
"username": "reader@example.com",
"authorization_code": "PRIVATE-AUTH-CODE",
}
self.zone = ZoneInfo("Asia/Shanghai")
self.start, self.end = check_mail.day_window(self.zone, "2026-09-28")
def run_check(self, fake, settings=None):
with TemporaryDirectory() as temp, patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=fake):
check_mail._check_imap_unlocked(
{"imap": settings or self.settings}, self.zone, self.start, self.end,
Path(temp) / "processed.json",
)
def test_163_sends_id_before_login_and_select(self):
fake = FakeIMAP()
self.run_check(fake)
self.assertEqual([call[0] for call in fake.calls[:3]], ["ID", "LOGIN", "SELECT"])
self.assertEqual(fake.calls[0][1], "ID")
self.assertIn('"name" "IMAPClient"', fake.calls[0][2])
def test_unknown_host_skips_id_by_default(self):
fake = FakeIMAP()
self.run_check(fake, {**self.settings, "host": "imap.example.com"})
self.assertEqual([call[0] for call in fake.calls[:2]], ["LOGIN", "SELECT"])
def test_explicit_toggle_overrides_host_default(self):
fake = FakeIMAP()
self.run_check(fake, {**self.settings, "send_id": False})
self.assertEqual(fake.calls[0][0], "LOGIN")
fake = FakeIMAP()
self.run_check(fake, {**self.settings, "host": "imap.example.com", "send_id": True})
self.assertEqual(fake.calls[0][0], "ID")
def test_id_no_aborts_before_login_with_sanitized_reason(self):
fake = FakeIMAP(id_status="NO", id_data=[b"Unsafe Login PRIVATE-AUTH-CODE"])
with self.assertRaisesRegex(RuntimeError, "Unsafe Login") as error:
self.run_check(fake)
self.assertNotIn("PRIVATE-AUTH-CODE", str(error.exception))
self.assertEqual([call[0] for call in fake.calls], ["ID"])
def test_id_exception_is_reported_without_credentials(self):
fake = FakeIMAP(id_error=OSError("socket failure PRIVATE-AUTH-CODE"))
with self.assertRaisesRegex(RuntimeError, "IMAP ID 命令失败") as error:
self.run_check(fake)
self.assertNotIn("PRIVATE-AUTH-CODE", str(error.exception))
self.assertEqual([call[0] for call in fake.calls], ["ID"])
def test_select_no_preserves_safe_server_reason(self):
fake = FakeIMAP(select_status="NO", select_data=[b"SELECT Unsafe Login PRIVATE-AUTH-CODE"])
with self.assertRaisesRegex(RuntimeError, "SELECT Unsafe Login") as error:
self.run_check(fake)
self.assertNotIn("PRIVATE-AUTH-CODE", str(error.exception))
self.assertEqual([call[0] for call in fake.calls[:3]], ["ID", "LOGIN", "SELECT"])
def test_invalid_send_id_mode_rejected(self):
fake = FakeIMAP()
with self.assertRaisesRegex(ValueError, "send_id"):
self.run_check(fake, {**self.settings, "send_id": "always"})
self.assertEqual(fake.calls, [])
def test_login_exception_cannot_echo_credentials(self):
fake = FakeIMAP(login_error=OSError(
"authentication failed for reader@example.com: PRIVATE-AUTH-CODE"
))
with self.assertRaisesRegex(RuntimeError, "IMAP 登录失败") as error:
self.run_check(fake)
self.assertNotIn(self.settings["username"], str(error.exception))
self.assertNotIn(self.settings["authorization_code"], str(error.exception))
def test_login_no_cannot_echo_credentials(self):
fake = FakeIMAP(login_status="NO", login_data=[
b"reader@example.com PRIVATE-AUTH-CODE rejected"
])
with self.assertRaisesRegex(RuntimeError, "IMAP 登录失败") as error:
self.run_check(fake)
self.assertNotIn(self.settings["username"], str(error.exception))
self.assertNotIn(self.settings["authorization_code"], str(error.exception))
if __name__ == "__main__":
unittest.main()
+209
View File
@@ -0,0 +1,209 @@
"""Offline tests for explicit recovery of the last generated report."""
from contextlib import redirect_stdout
from email.message import EmailMessage
from io import StringIO
import json
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
import unittest
from unittest.mock import patch
from zoneinfo import ZoneInfo
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import check_mail
class FakeIMAP:
def __init__(self, message, uid=b"42"):
self.message = message
self.uid_value = uid
self.fetches = 0
def __enter__(self):
return self
def __exit__(self, *_args):
pass
def login(self, *_args):
return "OK", [b"logged in"]
def select(self, *_args, **_kwargs):
return "OK", [b"1"]
def response(self, name):
assert name == "UIDVALIDITY"
return "OK", [b"123"]
def uid(self, command, *_args):
if command == "SEARCH":
return "OK", [self.uid_value]
if command == "FETCH":
self.fetches += 1
return "OK", [
(b'1 (UID 42 INTERNALDATE "28-Sep-2026 10:00:00 +0800" BODY[] {1}', self.message),
b")",
]
raise AssertionError(command)
class ReplayTests(unittest.TestCase):
def setUp(self):
self.temp = TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.config = self.root / "config.json"
self.state = self.root / "state" / "processed.json"
self.config.write_text(json.dumps({
"imap": {
"host": "imap.example.com",
"username": "reader@example.com",
"authorization_code": "test-only",
},
"timezone": "Asia/Shanghai",
"state_file": "state/processed.json",
}), encoding="utf-8")
mail = EmailMessage()
mail["Subject"] = "AWS Account on Hold: Response Required"
mail["Message-ID"] = "<report-42@example.com>"
mail.set_content("AWS Account ID: 123456789012\nYour account has been placed on hold.")
self.fake = FakeIMAP(mail.as_bytes())
# Keep the fake INTERNALDATE within the test's day without changing production filtering.
window = check_mail.day_window(ZoneInfo("Asia/Shanghai"), "2026-09-28")
fixed_day = patch.object(check_mail, "day_window", return_value=window)
fixed_day.start()
self.addCleanup(fixed_day.stop)
def run_main(self, *args):
output = StringIO()
with redirect_stdout(output):
check_mail.main(["--config", str(self.config), *args])
return output.getvalue()
def test_dedup_and_explicit_replay_without_connection(self):
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=self.fake):
first = self.run_main()
second = self.run_main()
self.assertIn("【AWS账号风控邮件】", first)
self.assertIn("AWS账号ID:123456789012", first)
self.assertEqual(second, check_mail.NO_NEW + "\n")
self.assertEqual(self.fake.fetches, 1)
before = self.state.read_bytes()
with patch.object(check_mail.imaplib, "IMAP4_SSL", side_effect=AssertionError("unexpected connection")):
replay = self.run_main("--replay-last")
self.assertEqual(replay, first)
self.assertEqual(self.state.read_bytes(), before)
self.assertEqual(json.loads(before)["last_report"], first.removesuffix("\n"))
def test_empty_scan_does_not_replace_previous_report(self):
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=self.fake):
first = self.run_main()
empty = FakeIMAP(self.fake.message, uid=b"")
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=empty):
self.assertEqual(self.run_main(), check_mail.NO_NEW + "\n")
self.assertEqual(self.run_main("--replay-last"), first)
def test_older_batch_survives_next_report_and_empty_scan(self):
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=self.fake):
first = self.run_main()
newer_mail = EmailMessage()
newer_mail["Subject"] = "AWS Account on Hold: Second Notice"
newer_mail["Message-ID"] = "<report-43@example.com>"
newer_mail.set_content("AWS Account ID: 999999999999\nYour account has been placed on hold.")
newer = FakeIMAP(newer_mail.as_bytes(), uid=b"43")
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=newer):
second = self.run_main()
self.assertEqual(self.run_main(), check_mail.NO_NEW + "\n")
state_before = self.state.read_bytes()
with patch.object(check_mail.imaplib, "IMAP4_SSL", side_effect=AssertionError("unexpected connection")):
ids = self.run_main("--list-report-batches").splitlines()
self.assertEqual(len(ids), 2)
self.assertEqual(self.run_main("--replay-id", ids[1]), first)
self.assertEqual(self.run_main("--replay-id", ids[0]), second)
self.assertEqual(self.run_main("--replay-last"), second)
self.assertEqual(self.state.read_bytes(), state_before)
def test_history_is_bounded(self):
state = {"mailboxes": {}}
for number in range(check_mail.REPORT_HISTORY_LIMIT + 4):
check_mail.save_report_batch(state, f"report {number}")
history = check_mail.report_history(state)
self.assertEqual(len(history), check_mail.REPORT_HISTORY_LIMIT)
self.assertEqual(history[0]["report"], "report 4")
self.assertEqual(history[-1]["report"], f"report {check_mail.REPORT_HISTORY_LIMIT + 3}")
self.assertEqual(len({item["id"] for item in history}), len(history))
def test_legacy_last_report_remains_replayable(self):
self.state.parent.mkdir(parents=True, exist_ok=True)
self.state.write_text(json.dumps({"mailboxes": {}, "last_report": "旧报告"}), encoding="utf-8")
with patch.object(check_mail.imaplib, "IMAP4_SSL", side_effect=AssertionError("unexpected connection")):
self.assertEqual(self.run_main("--list-report-batches"), "legacy\n")
self.assertEqual(self.run_main("--replay-id", "legacy"), "旧报告\n")
self.assertEqual(self.run_main("--replay-last"), "旧报告\n")
def test_missing_batch_and_invalid_history_are_clear_errors(self):
with patch.object(check_mail.imaplib, "IMAP4_SSL", side_effect=AssertionError("unexpected connection")):
with self.assertRaisesRegex(RuntimeError, "报告批次 ID 不存在"):
self.run_main("--replay-id", "missing")
self.state.parent.mkdir(parents=True, exist_ok=True)
self.state.write_text('{"mailboxes": {}, "report_history": [42]}', encoding="utf-8")
with self.assertRaisesRegex(ValueError, "报告历史格式无效"):
self.run_main("--list-report-batches")
def test_no_saved_report_has_clear_error(self):
with patch.object(check_mail.imaplib, "IMAP4_SSL", side_effect=AssertionError("unexpected connection")):
with self.assertRaisesRegex(RuntimeError, "没有可重放的上次报告"):
self.run_main("--replay-last")
self.state.parent.mkdir(parents=True, exist_ok=True)
self.state.write_text('{"mailboxes": {}}', encoding="utf-8")
with self.assertRaisesRegex(RuntimeError, "没有可重放的上次报告"):
self.run_main("--replay-last")
def test_local_eml_mode_does_not_write_state(self):
folder = self.root / "samples"
folder.mkdir()
mail = EmailMessage()
mail["Subject"] = "AWS Account on Hold: Response Required"
mail["Date"] = "Mon, 28 Sep 2026 10:00:00 +0800"
mail.set_content("AWS Account ID: 123456789012\nYour account has been placed on hold.")
(folder / "sample.eml").write_bytes(mail.as_bytes())
self.assertIn("【AWS账号风控邮件】", self.run_main("--eml-dir", str(folder), "--date", "2026-09-28"))
self.assertFalse(self.state.exists())
def test_filtered_code_is_deduplicated_without_report_history(self):
mail = EmailMessage()
mail["Subject"] = "AWS 验证您的身份"
mail["Message-ID"] = "<code-42@example.com>"
mail.set_content("AWS 验证码 123456,用于登录。如果发现异常活动,请启用 MFA。")
fake = FakeIMAP(mail.as_bytes())
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=fake):
self.assertEqual(self.run_main(), check_mail.NO_NEW + "\n")
self.assertEqual(self.run_main(), check_mail.NO_NEW + "\n")
self.assertEqual(fake.fetches, 1)
state = json.loads(self.state.read_text(encoding="utf-8"))
entry = next(iter(state["mailboxes"].values()))
self.assertEqual(entry["uids"], ["42"])
self.assertEqual(entry["message_ids"], ["<code-42@example.com>"])
self.assertNotIn("report_history", state)
self.assertNotIn("last_report", state)
def test_filtered_code_preserves_previous_report_batch(self):
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=self.fake):
first = self.run_main()
before = json.loads(self.state.read_text(encoding="utf-8"))
mail = EmailMessage()
mail["Subject"] = "AWS verification code"
mail["Message-ID"] = "<code-43@example.com>"
mail.set_content("Verification code: 123456. Use this code to sign in.")
with patch.object(check_mail.imaplib, "IMAP4_SSL", return_value=FakeIMAP(mail.as_bytes(), uid=b"43")):
self.assertEqual(self.run_main(), check_mail.NO_NEW + "\n")
after = json.loads(self.state.read_text(encoding="utf-8"))
self.assertEqual(after["report_history"], before["report_history"])
self.assertEqual(self.run_main("--replay-last"), first)
if __name__ == "__main__":
unittest.main()