优化 AWS 邮件验证码过滤与有效字段输出
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user