退信核查按原因分类:只有限流/临时性退信才剔除补发
问题:退信核查此前凡是退信一律从断点清单剔除、下次重跑补发。 但退信分两类——「您的账号外发频率超过邮件系统限制」这类限流退信重发 可成功;「地址不存在」这类硬退信重发也发不出去,只会浪费每日发信额度、 拖垮发信信誉。 改动: - 新增退信分类 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
This commit is contained in:
@@ -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(含每个收件人的类别与判定依据)
|
||||
|
||||
安全机制
|
||||
--------
|
||||
|
||||
Reference in New Issue
Block a user