From bc971deb371e5f7847958815d057b7fffd100116 Mon Sep 17 00:00:00 2001 From: tamakyi Date: Mon, 7 Sep 2026 18:26:09 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=80=E4=BF=A1=E6=A0=B8=E6=9F=A5=E6=8C=89?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=E5=88=86=E7=B1=BB=EF=BC=9A=E5=8F=AA=E6=9C=89?= =?UTF-8?q?=E9=99=90=E6=B5=81/=E4=B8=B4=E6=97=B6=E6=80=A7=E9=80=80?= =?UTF-8?q?=E4=BF=A1=E6=89=8D=E5=89=94=E9=99=A4=E8=A1=A5=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:退信核查此前凡是退信一律从断点清单剔除、下次重跑补发。 但退信分两类——「您的账号外发频率超过邮件系统限制」这类限流退信重发 可成功;「地址不存在」这类硬退信重发也发不出去,只会浪费每日发信额度、 拖垮发信信誉。 改动: - 新增退信分类 rate/hard/unknown:优先用 DSN 状态码 5.x.x/4.x.x 判定, 同一封退信里的多个收件人可分别归类;无 DSN 时回退正文关键词匹配 (硬退信优先于限流,宁可不补发可疑地址) - --prune-state 只剔除「可重试」退信;永久失败不剔除,即不再补发 - 新增无效地址清单 broadcast_invalid.json:硬退信地址记入后每次运行 直接跳过(含 --reset-state),附 --reset-invalid / --no-invalid-list - 新增 --prune-unknown / --invalid-file / --bounce-report 参数 - 新增退信明细报告 broadcast_bounce_report.json(含每个收件人判定依据) - 修正 _part_text 取正文的两个解码坑:utf-8 正文默认 base64 传输编码未 解码、str 形态 payload 走 raw-unicode-escape 致中文变 \uXXXX,两者都会 让中文关键词匹配静默失效 - 补充退信主题关键词(delivery failed、未能送达、无法送达 等) - README 补充退信分类表与无效地址清单说明 - 新增 .gitignore,移除误入库的 __pycache__/broadcast.cpython-313.pyc --- .gitignore | 11 + README.md | 48 ++- __pycache__/broadcast.cpython-313.pyc | Bin 97225 -> 0 bytes broadcast.py | 465 ++++++++++++++++++++++---- 4 files changed, 449 insertions(+), 75 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/broadcast.cpython-313.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb4e5b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python +__pycache__/ +*.py[cod] + +# 运行时生成的状态/报告文件(含真实用户邮箱,不入库) +broadcast_state.json +broadcast_report.json +broadcast_invalid.json +broadcast_bounce_report.json +broadcast_bounces_seen.json +*.tmp diff --git a/README.md b/README.md index 47eb288..dc736fc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ README — TamaBox 站内信群发工具(mail-broadcast) single.subject.txt 单文件模式主题(一行) broadcast_state.json (运行后生成)断点续发记录,已发邮箱重跑自动跳过 broadcast_report.json (运行后生成)每次运行的监控报告 + broadcast_invalid.json (运行后生成)无效地址清单:硬退信地址,永久跳过 + broadcast_bounce_report.json (运行后生成)退信核查明细(含分类与判定依据) + broadcast_bounces_seen.json (运行后生成)已核查退信的 Message-ID,避免重复报告 参数文件 params.ini ------------------- @@ -135,22 +138,40 @@ conf/app.ini 路径的解析优先级: # 单文件模式:所有人发 templates/single.html(不按语言) python3 broadcast.py --single --send --to you@example.com -退信核查(验证真实送达) ------------------------- +退信核查(验证真实送达 · 按原因分类处理) +---------------------------------------- 背景:SMTP 返回 250 只代表服务器收下了,不代表送达。阿里云企业邮箱等对 「发送频率超限」「收件人不存在」等情况往往是先收下、再异步把退信通知投到 发件账号的收件箱——脚本当时的「发送成功」并不真实。 - # 发送完过几分钟,扫描近 3 天的退信通知,列出实际未送达的收件人(仅核查) +**退信不是一类,必须分类处理**(v3 起): + +| 类别 | 典型原因 | 脚本动作 | +| ---- | -------- | -------- | +| 可重试 `rate` | 「您的账号外发频率超过邮件系统限制」、系统繁忙、DSN 4.x.x | 从断点清单剔除 → 下次/本次补发 | +| 永久失败 `hard` | 地址不存在、用户不存在、DSN 5.x.x | **不补发**;记入无效地址清单,永久跳过 | +| 未分类 `unknown` | 判不出来 | 默认不补发(保守);`--prune-unknown` 可强制补发 | + +以前是「凡是退信一律剔除补发」,结果**地址不存在的死信也会被反复重发**—— +既浪费每日发信额度,又拖垮发信信誉。现在只有真正可重试的才会补发。 + +判定优先级:DSN 状态码(5.x.x / 4.x.x,最权威,且**同一封退信里的多个收件人 +可分别归类**)→ 正文硬退信关键词 → 限流/临时性关键词 → 正文 SMTP 状态码 → 未分类。 +硬退信优先于限流:宁可少补发一个可疑地址,也不给死信反复重发。 + + # 发送完过几分钟,扫描退信并分类报告(仅核查,不改动任何清单) python3 broadcast.py --check-bounces - # 仅剔除:把这些用户从断点续发清单剔除后结束,本次不发送 - #(下次任意一次重跑会自动补上这批人) + # 仅剔除:把「可重试」的退信从断点续发清单剔除后结束,本次不发送 + #(下次任意一次重跑会自动补上这批人;地址不存在的不会补发) python3 broadcast.py --check-bounces --prune-state - # 剔除并补发:剔除后继续正常发送流程,本次就把退信用户补上 + # 剔除并补发:剔除后继续正常发送流程,本次就把可重试的用户补上 python3 broadcast.py --check-bounces --prune-state --send + # 未分类的也按可重试一并剔除补发(确认不是死信后再用) + python3 broadcast.py --check-bounces --prune-state --prune-unknown + # 可选:--imap-host imap.xxx.com(默认由 SMTP 域名推导 smtp.→imap.) # --imap-port 993;扫描起点 --since auto(默认,见下) 扫描起点(避免把群发无关的退信算进来): @@ -158,11 +179,24 @@ conf/app.ini 路径的解析优先级: 时间开始——即只核查本次群发发出的那些邮件的退信 - --since 2026-09-07 可显式指定起点日期 - 断点清单为空时回退为 --since-days N(默认 3 天) + +无效地址清单 broadcast_invalid.json +----------------------------------- +硬退信(地址不存在等)的地址会写进这里,之后**每次运行都直接跳过**, +即使加了 --reset-state 也不会再发——重发也发不出去,只会浪费额度。 +条目里保留了退信原因、主题与时间,便于核对到底是哪些地址失效了。 + + python3 broadcast.py --reset-invalid # 清空清单(确认地址已修正后用) + python3 broadcast.py --no-invalid-list ... # 本次忽略清单(既不跳过也不写入) + python3 broadcast.py --invalid-file /path/to/other.json # 换一个清单文件 + 说明: - 用的是 conf/app.ini [mail] 的账号与密码(需邮箱已开启 IMAP,密码为邮箱登录密码) - - 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发 + - 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发, + 选择「仅剔除 / 剔除并补发」时会再问一次是否连未分类的一起补发 - 已核查过的退信(按 Message-ID)记录在 broadcast_bounces_seen.json, 重复核查不会重复报告/重复剔除 + - 每次核查另写一份明细报告 broadcast_bounce_report.json(含每个收件人的类别与判定依据) 安全机制 -------- diff --git a/__pycache__/broadcast.cpython-313.pyc b/__pycache__/broadcast.cpython-313.pyc deleted file mode 100644 index d0b5d0d76d5f67171a56210cac6a1b54ffd58ce5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97225 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 硬退信关键词 > 限流关键词 > 未知 +CAT_RATE = "rate" # 限流/临时性失败 → 可重试,剔除后补发 +CAT_HARD = "hard" # 地址不存在等永久失败 → 不补发,记入无效地址清单 +CAT_UNKNOWN = "unknown" # 判不出来 → 默认不剔除(保守),--prune-unknown 可强制 + +CAT_LABEL = { + CAT_RATE: "可重试 · 限流/临时性", + CAT_HARD: "永久失败 · 地址无效", + CAT_UNKNOWN: "未分类(保守:不补发)", +} + +# 限流 / 临时性失败(可稍后重试) +RATE_LIMIT_KEYWORDS = ( + "您的账号外发频率超过邮件系统限制", # 阿里云企业邮箱限流退信原文 + "外发频率超过", "发送频率超过", "超过邮件系统限制", "发送频率受限", + "发送流量超限", "超出发送限额", "发送数量超限", "超出邮件系统限制", + "系统繁忙", "稍后重试", "稍后重新发送", "请稍后再试", "请稍后重新发送", + "rate limit", "too many", "throttl", "try again later", "try later", + "too many recipients", "temporarily", "temporary failure", "temporary local", + "greylist", "grey list", "deferred", "deferral", "connection timed out", + "try again", "please retry", "later time", +) + +# 永久失败:收件人/邮箱根本不存在 +HARD_BOUNCE_KEYWORDS = ( + "地址不存在", "用户不存在", "无此用户", "账号不存在", "帐户不存在", + "收件人不存在", "不存在该用户", "邮箱不存在", "没有这个邮箱", + "没有此邮箱", "查无此用户", "无效地址", "无效的收件人", "无效的收件地址", + "收件人地址错误", "该邮箱不存在", "未知的用户", + "no such user", "no such recipient", "no such mailbox", "no such address", + "user unknown", "unknown user", "unknown recipient", "unknown address", + "recipient address rejected", "recipient not found", "recipient rejected", + "mailbox unavailable", "mailbox not found", "mailbox does not exist", + "address does not exist", "address not found", "address rejected", + "invalid recipient", "invalid address", "invalid mailbox", + "bad destination", "bad address syntax", "user not found", + "does not exist", "550 5.1.1", "no mailbox here", "mailbox disabled", +) + +# SMTP 状态码识别:增强状态码 5.1.1 / 三位码 550 +_SMTP_ENHANCED_RE = re.compile(r"\b([245])\.(\d{1,3})\.(\d{1,3})\b") +_SMTP_CODE_RE = re.compile(r"(? limit: break else: - try: - payload = msg.get_payload(decode=True) - if payload: - chunks.append(payload.decode(msg.get_content_charset() or "utf-8", "replace")) - except Exception: # noqa: BLE001 - pass - return "\n".join(chunks)[:limit] + chunks.append(_part_payload_text(msg)) + return "\n".join(c for c in chunks if c)[:limit] -def _dsn_failed_recipients(msg): - """标准 DSN(message/delivery-status)中 Action: failed 的收件人。""" +# DSN 里表示「已送达」的 Action,遇到就跳过(其余 failed/delayed/expired 都算失败) +DSN_OK_ACTIONS = ("delivered", "relayed", "expanded") + + +def _dsn_recipient_details(msg): + """标准 DSN(message/delivery-status)中每个收件人的失败详情。 + + 返回 [{email, action, status, diagnostic}],逐个带自己的 Status / + Diagnostic-Code,因此一封退信里的多个收件人可以分别归类。 + """ out = [] for part in msg.walk(): if part.get_content_type() != "message/delivery-status": @@ -751,15 +830,69 @@ def _dsn_failed_recipients(msg): for blk in blocks: try: final = str(blk.get("Final-Recipient") or blk.get("Original-Recipient") or "") - action = str(blk.get("Action") or "failed").lower() + action = str(blk.get("Action") or "failed").strip().lower() + status = str(blk.get("Status") or "").strip() + diagnostic = str(blk.get("Diagnostic-Code") or "").strip() except Exception: # noqa: BLE001 continue + if action in DSN_OK_ACTIONS: + continue m = EMAIL_RE.search(final) - if m and "failed" in action: - out.append(m.group(0).strip(".").lower()) + if not m: + continue + out.append({ + "email": m.group(0).strip(".").lower(), + "action": action, + "status": status, + "diagnostic": diagnostic, + }) return out +def _smtp_code_class(text): + """从 DSN Status / Diagnostic-Code 取 SMTP 状态码首位:5=永久、4=临时、2=成功。""" + if not text: + return None + m = _SMTP_ENHANCED_RE.search(text) # 增强状态码,如 5.1.1 / 4.4.7 + if m: + return m.group(1) + m = _SMTP_CODE_RE.search(text) # 三位码,如 550 / 451 + if m: + return m.group(0)[0] + return None + + +def classify_bounce(status="", diagnostic="", text=""): + """判定退信类别,返回 (类别, 判定依据)。 + + 顺序:DSN 状态码(最权威)→ 正文/诊断信息里的硬退信关键词 → 限流关键词 + → 正文里的 SMTP 状态码 → unknown。 + 硬退信优先于限流:宁可不补发一个可疑地址,也不要反复给死信浪费额度。 + """ + for src in (status, diagnostic): + c = _smtp_code_class(src or "") + if c == "5": + return CAT_HARD, f"DSN 状态码 {(src or '').strip()[:40]}" + if c == "4": + return CAT_RATE, f"DSN 状态码 {(src or '').strip()[:40]}" + + body = "\n".join(x for x in (text, diagnostic) if x) + low = body.lower() + for kw in HARD_BOUNCE_KEYWORDS: + if kw.lower() in low: + return CAT_HARD, f"关键词「{kw}」" + for kw in RATE_LIMIT_KEYWORDS: + if kw.lower() in low: + return CAT_RATE, f"关键词「{kw}」" + + c = _smtp_code_class(text or "") + if c == "5": + return CAT_HARD, "正文 SMTP 5xx" + if c == "4": + return CAT_RATE, "正文 SMTP 4xx" + return CAT_UNKNOWN, "" + + def _own_ok(addr, own_addr): a = addr.strip(".").lower() if not a or a == own_addr.lower(): @@ -781,28 +914,40 @@ def is_bounce_message(msg): def extract_bounced_recipients(msg, own_addr): - """从退信中解析投递失败的收件人列表(已去重、剔除发件人与系统地址)。""" + """从退信中解析投递失败的收件人,并逐个分类。 + + 返回 [{email, category, reason, detail}](已去重、剔除发件人与系统地址)。 + """ if not is_bounce_message(msg): return [] - recipients = [] - # 1) 标准 DSN 最可靠 - for r in _dsn_failed_recipients(msg): - if _own_ok(r, own_addr) and r not in recipients: - recipients.append(r) - # 2) 中文退信的「标签:地址」写法 - if not recipients: + + found = [] + + def add(em, category, reason, detail=""): + em = (em or "").strip().strip(".").lower() + if not _own_ok(em, own_addr): + return + if any(x["email"] == em for x in found): + return + found.append({"email": em, "category": category, + "reason": reason, "detail": (detail or "")[:160]}) + + # 1) 标准 DSN 最可靠:每个收件人带自己的 Status/Diagnostic-Code,可逐个分类 + for d in _dsn_recipient_details(msg): + cat, reason = classify_bounce(d["status"], d["diagnostic"], "") + add(d["email"], cat, reason, d["status"] or d["diagnostic"]) + + # 2) 中文退信的「标签:地址」写法 / 3) 正文兜底:整封共用一个分类结果 + if not found: text = _part_text(msg) + cat, reason = classify_bounce("", "", text) for pat in LABELED_PATTERNS: for m in re.finditer(pat, text, re.IGNORECASE): - e = m.group("e") - if _own_ok(e, own_addr) and e.lower() not in recipients: - recipients.append(e.lower()) - # 3) 兜底:正文里出现的其他地址(剔除发件人自身与 daemon/postmaster) - if not recipients: - for e in EMAIL_RE.findall(_part_text(msg)): - if _own_ok(e, own_addr) and e.lower() not in recipients: - recipients.append(e.lower()) - return recipients + add(m.group("e"), cat, reason, "") + if not found: + for e in EMAIL_RE.findall(text): + add(e, cat, reason, "") + return found def prune_state(state_path, emails): @@ -834,6 +979,88 @@ def prune_state(state_path, emails): return removed +# --------------------------------------------------------------------------- +# 无效地址清单:硬退信(收件人不存在 / DSN 5.x.x)的地址记在这里。 +# 这些地址重发也发不出去,所以: +# - 不从断点续发清单剔除(即不会「补发」) +# - 记入本清单,后续每次运行(即使 --reset-state)都会直接跳过 +# 只有确实要恢复发送时,才手动删条目或 --reset-invalid 清空。 +# --------------------------------------------------------------------------- +def load_invalid(path): + """读取无效地址清单,返回 {email: {"reason","detail","subject","date","ts"}}。""" + if not os.path.isfile(path): + return {} + try: + with open(path, "r", encoding="utf-8") as f: + raw = json.load(f) + except Exception as e: # noqa: BLE001 + print(f"[警告] 读取无效地址清单失败,按空清单处理:{e}") + return {} + out = {} + if isinstance(raw, dict): + items = raw.items() + elif isinstance(raw, list): + items = [(it.get("email"), it) for it in raw if isinstance(it, dict)] + else: + return out + for em, info in items: + em = str(em or "").strip().lower() + if not em: + continue + info = info if isinstance(info, dict) else {"reason": str(info)} + out[em] = { + "reason": str(info.get("reason") or ""), + "detail": str(info.get("detail") or ""), + "subject": str(info.get("subject") or ""), + "date": str(info.get("date") or ""), + "ts": str(info.get("ts") or ""), + } + return out + + +def record_invalid(path, entries): + """合并写入无效地址清单。entries: {email: {reason, detail, subject, date}}。 + 返回新增条数(已有条目只更新原因)。""" + if not entries: + return 0 + data = load_invalid(path) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + added = 0 + for em, info in entries.items(): + em = (em or "").strip().lower() + if not em: + continue + if em not in data: + added += 1 + data[em] = { + "reason": str(info.get("reason") or ""), + "detail": str(info.get("detail") or ""), + "subject": str(info.get("subject") or ""), + "date": str(info.get("date") or ""), + "ts": now, + } + try: + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump({k: data[k] for k in sorted(data)}, f, ensure_ascii=False, indent=2) + os.replace(tmp, path) + except Exception as e: # noqa: BLE001 + print(f"[警告] 写入无效地址清单失败:{e}") + return 0 + return added + + +def reset_invalid(path): + """清空无效地址清单(慎用:这些地址多半仍是不可达的)。""" + try: + if os.path.isfile(path): + os.remove(path) + return True + except Exception as e: # noqa: BLE001 + print(f"[警告] 清除无效地址清单失败:{e}") + return False + + def resolve_scan_start(args): """退信扫描起点:--since 显式日期 > auto 时取断点清单最早一条发送记录的 时间(精确到该时刻,不加余量)> 无记录时回退 --since-days。 @@ -857,8 +1084,10 @@ def resolve_scan_start(args): def check_bounces(cfg, args): """登录发件邮箱 IMAP,扫描近 N 天的退信并解析失败收件人。 - 返回 {email: (subject, date_str)};已处理过的邮件(Message-ID)记录在 - broadcast_bounces_seen.json,重复核查不会重复报告。""" + + 返回 {email: {"subject","date","category","reason","detail"}};已处理过的 + 邮件(Message-ID)记录在 broadcast_bounces_seen.json,重复核查不会重复报告。 + category 见 CAT_RATE / CAT_HARD / CAT_UNKNOWN。""" smtp_host = cfg["mail_smtp"] or "" imap_host = (args.imap_host or "").strip() if not imap_host: @@ -893,7 +1122,7 @@ def check_bounces(cfg, args): except Exception: # noqa: BLE001 seen = set() - bounced = {} # email -> (subject, date) + bounced = {} # email -> {"subject","date","category","reason","detail"} scanned, bounce_cnt, newly_seen = 0, 0, [] for i, mid in enumerate(ids, 1): uid = mid.decode() if isinstance(mid, bytes) else str(mid) @@ -924,33 +1153,93 @@ def check_bounces(cfg, args): bounce_cnt += 1 newly_seen.append(msg_id) for r in rcpts: - bounced.setdefault(r, (subject, date)) - print(f" [退信 {i}/{len(ids)}] {subject[:40]} → {', '.join(rcpts) if rcpts else '(未解析出收件人)'}") + bounced.setdefault(r["email"], { + "subject": subject, "date": date, + "category": r["category"], "reason": r["reason"], "detail": r["detail"], + }) + if rcpts: + desc = " ".join( + f"{r['email']}[{CAT_LABEL.get(r['category'], r['category'])}]" for r in rcpts + ) + else: + desc = "(未解析出收件人)" + print(f" [退信 {i}/{len(ids)}] {subject[:40]} → {desc}") try: conn.logout() except Exception: # noqa: BLE001 pass + # ---- 按类别分组处理:只有「可重试」的才剔除补发 ---- + groups = {CAT_RATE: [], CAT_HARD: [], CAT_UNKNOWN: []} + for em, info in bounced.items(): + groups.setdefault(info["category"], []).append(em) + prune_cats = [CAT_RATE] + ([CAT_UNKNOWN] if getattr(args, "prune_unknown", False) else []) + print("\n========================================================") print(f"退信核查完成:扫描 {scanned} 封未处理邮件,其中退信 {bounce_cnt} 封," f"涉及 {len(bounced)} 个收件人。") - if bounced: - for r, (subj, dt) in sorted(bounced.items()): - print(f" - {r} ({dt} · {subj[:36]})") - print("\n这些用户实际未收到邮件。") + for cat in (CAT_RATE, CAT_HARD, CAT_UNKNOWN): + ems = sorted(groups.get(cat, [])) + if not ems: + continue + print(f"\n [{CAT_LABEL[cat]}] {len(ems)} 个:") + for em in ems: + info = bounced[em] + why = info["reason"] or "未识别到明确原因" + meta = " · ".join(x for x in (info["date"], info["subject"][:32], why) if x) + print(f" - {em} ({meta})") + + if not bounced: + print("未发现新的退信,此前发送均正常送达(至少未被退回)。") + else: + # 硬退信:不补发 + 记入无效地址清单永久跳过 + hard = sorted(groups.get(CAT_HARD, [])) + if hard and not getattr(args, "no_invalid_list", False): + n = record_invalid( + args.invalid_file, + {em: bounced[em] for em in hard}, + ) + print(f"\n[无效地址] {len(hard)} 个永久失败地址已记入 {args.invalid_file}" + f"(新增 {n} 条);这些地址后续运行(含 --reset-state)会直接跳过,不再浪费额度。") + print(" 确认地址已修正后,删掉该文件或 --reset-invalid 可恢复发送。") + elif hard: + print(f"\n[无效地址] {len(hard)} 个永久失败地址未写入清单(--no-invalid-list)," + f"但它们不会被补发。") + + # 可重试:从断点清单剔除,下次/本次补发 + retryable = [em for cat in prune_cats for em in groups.get(cat, [])] if args.prune_state: - n = prune_state(args.state, bounced.keys()) - print(f"[剔除] 已从断点续发清单剔除 {n} 条记录:{args.state}") + n = prune_state(args.state, retryable) + print(f"[剔除] 已从断点续发清单剔除 {n} 条「可重试」记录:{args.state}") if getattr(args, "send", False): print(" 本次发送将继续进行,这些用户会被包含在内(剔除并补发)。") else: print(" 本次不发送;下次重跑(同参数)会自动给这些用户补发。") - else: - print("可选后续:--prune-state 仅剔除不发送;--prune-state --send 剔除后立即补发;") - print(" 或交互模式运行方式 4 里选择对应核查方式。") - else: - print("未发现新的退信,此前发送均正常送达(至少未被退回)。") + elif retryable: + print(f"\n[提示] 有 {len(retryable)} 个「可重试」退信等待补发。可选:" + f"--prune-state 仅剔除;--prune-state --send 剔除后立即补发。") + unknown = groups.get(CAT_UNKNOWN, []) + if unknown and not getattr(args, "prune_unknown", False): + print(f"[提示] 有 {len(unknown)} 个退信未能分类,默认不补发(保守)。" + f"确认是限流后可加 --prune-unknown 一并剔除补发。") + + # 退信核查明细报告(便于事后核对,不受 --no-report 影响) + try: + with open(args.bounce_report, "w", encoding="utf-8") as f: + json.dump({ + "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "script_version": SCRIPT_VERSION, + "scan_from": start_dt.strftime("%Y-%m-%d %H:%M:%S"), + "scan_from_desc": start_desc, + "scanned": scanned, + "bounce_messages": bounce_cnt, + "counts": {c: len(groups.get(c, [])) for c in (CAT_RATE, CAT_HARD, CAT_UNKNOWN)}, + "recipients": {em: bounced[em] for em in sorted(bounced)}, + }, f, ensure_ascii=False, indent=2) + print(f"\n[退信报告] 已写入:{args.bounce_report}") + except Exception as e: # noqa: BLE001 + print(f"[警告] 写入退信报告失败(不影响本次结果):{e}") if newly_seen: try: @@ -1106,10 +1395,11 @@ def run_interactive(cfg, templates, extra_vars, args, config_path): if mode == "4": args.check_bounces = True - print("\n退信核查方式:") - print(" 1) 仅核查 —— 只报告哪些用户没收到,不改动断点清单(默认)") - print(" 2) 仅剔除 —— 把退信用户从断点清单剔除后结束,本次不发送") - print(" 3) 剔除并补发 —— 剔除后继续正常发送流程(发送前仍有统计与确认)") + print("\n退信核查方式(会先按原因分类:限流/临时性 → 可重试;地址不存在 → 永久失败):") + print(" 1) 仅核查 —— 只报告哪些用户没收到、各自什么原因,不改动任何清单(默认)") + print(" 2) 仅剔除 —— 只把「限流等可重试」的退信从断点清单剔除后结束,本次不发送") + print(" (「地址不存在」的不会剔除,另记入无效地址清单永久跳过)") + print(" 3) 剔除并补发 —— 同上剔除后继续正常发送流程(发送前仍有统计与确认)") while True: try: sub = input("核查方式 [1/2/3](直接回车 = 1): ").strip() @@ -1118,11 +1408,16 @@ def run_interactive(cfg, templates, extra_vars, args, config_path): if sub in ("", "1", "2", "3"): break print(" 请输入 1、2 或 3") - if sub == "2": - args.prune_state = True - elif sub == "3": + if sub in ("2", "3"): args.prune_state = True + if sub == "3": args.send = True + if args.prune_state: + try: + unk = input("未分类的退信是否也按「可重试」一并剔除补发?(y/N): ").strip().lower() + except EOFError: + unk = "" + args.prune_unknown = unk in ("y", "yes") return site_url, box_prefix, extra_vars # ---- 模板模式:多语言 / 单文件通用 ---- @@ -1238,8 +1533,21 @@ def main(): help="退信核查:登录发件邮箱 IMAP 扫描退信通知,找出实际未送达的收件人" "(SMTP 250 不代表送达;限流等退信是异步投到发件箱的)") parser.add_argument("--prune-state", action="store_true", - help="配合 --check-bounces:把退信用户从断点续发清单剔除后结束(仅剔除," - "本次不发送);再加 --send 则剔除后立即补发") + help="配合 --check-bounces:把「可重试」退信(限流/临时性)从断点续发清单" + "剔除后结束(仅剔除,本次不发送);再加 --send 则剔除后立即补发。" + "「地址不存在」等永久失败不会被剔除(不补发)") + parser.add_argument("--prune-unknown", action="store_true", + help="配合 --check-bounces:无法分类的退信也按「可重试」处理,一并剔除补发" + "(默认不剔除,避免给死信地址反复重发)") + parser.add_argument("--invalid-file", default=DEFAULT_INVALID, metavar="FILE", + help=f"无效地址清单(硬退信地址)路径;默认脚本同目录 broadcast_invalid.json。" + f"清单里的地址每次运行都会跳过,即使 --reset-state") + parser.add_argument("--reset-invalid", action="store_true", + help="清空无效地址清单(慎用:这些地址多半仍不可达)") + parser.add_argument("--no-invalid-list", action="store_true", + help="本次不使用无效地址清单:既不跳过已知无效地址,也不写入新的硬退信") + parser.add_argument("--bounce-report", default=DEFAULT_BOUNCE_REPORT, metavar="FILE", + help="退信核查明细报告路径(JSON);默认脚本同目录 broadcast_bounce_report.json") parser.add_argument("--imap-host", default="", help="IMAP 服务器地址(默认由 SMTP 域名推导:smtp.xxx → imap.xxx)") parser.add_argument("--imap-port", type=int, default=993, @@ -1267,6 +1575,13 @@ def main(): args.interactive = not (args.send or args.to or args.yes or args.dry_run or args.check_bounces) + # ---- 无效地址清单维护(--reset-invalid):手动恢复发送硬退信地址时用 ---- + if args.reset_invalid: + if reset_invalid(args.invalid_file): + print(f"[无效地址清单] 已清空:{args.invalid_file}") + else: + print(f"[无效地址清单] 文件不存在,无需清空:{args.invalid_file}") + # 载入模板 templates, lang_list = load_templates(args.templates_dir) single_tpl = load_single_template(args.templates_dir) @@ -1401,6 +1716,19 @@ def main(): recipients = fetch_recipients(cfg, args, lang_expr=lang_expr, tbl=users_table) counts = fetch_counts(cfg, args, tbl=users_table) + # 跳过历史硬退信地址(收件人不存在 / DSN 5.x.x)——重发也发不出去, + # 且不受 --reset-state 影响;只有 --no-invalid-list 或清空清单才会恢复。 + invalid_skipped = 0 + if not args.no_invalid_list: + invalid_map = load_invalid(args.invalid_file) + if invalid_map: + before = len(recipients) + recipients = [r for r in recipients + if (r["email"] or "").strip().lower() not in invalid_map] + invalid_skipped = before - len(recipients) + print(f" [无效地址清单] 已载入 {len(invalid_map)} 条" + f"({args.invalid_file}),本次跳过 {invalid_skipped} 个地址") + print("数据库用户统计:") print(f" 总用户数(含已注销) : {counts['total']}") print(f" 已注销用户数 : {counts['deactivated']}") @@ -1496,6 +1824,7 @@ def main(): "failed": 0, "smtp_rejected": 0, "skipped_already_sent": skipped, + "skipped_invalid": invalid_skipped, "failed_list": [], "missing_placeholders": [], "test_mode": bool(args.to), @@ -1646,9 +1975,9 @@ def main(): print(f"[注意] 模板中有未提供值的占位符(已替换为空串):{', '.join(sorted(all_missing))}") if not args.to and args.send: print(f"断点续发状态已写入:{args.state}(重跑脚本会自动跳过已发用户)") - print("[提示] SMTP 250 不代表一定送达(限流常被异步退回发件箱)。" + print("[提示] SMTP 250 不代表一定送达(限流/无效地址常被异步退回发件箱)。" "发送完过几分钟可运行「python3 broadcast.py --check-bounces --prune-state」" - "核查退信并让失败用户下次自动补发。") + "核查退信:限流类会自动剔除补发,地址不存在类记入无效清单不再重发。") if failed: print("失败清单:") for e, err in failed: