Compare commits
6
Commits
912facaf2c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5823b5fbb3 | ||
|
|
f83ea3e29a | ||
|
|
1cae63b671 | ||
|
|
5aad4a97e4 | ||
|
|
909e853500 | ||
|
|
bc971deb37 |
+14
@@ -0,0 +1,14 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# WorkBuddy 项目数据(会话记忆等)
|
||||
.workbuddy/
|
||||
|
||||
# 运行时生成的状态/报告文件(含真实用户邮箱,不入库)
|
||||
broadcast_state.json
|
||||
broadcast_report.json
|
||||
broadcast_invalid.json
|
||||
broadcast_bounce_report.json
|
||||
broadcast_bounces_seen.json
|
||||
*.tmp
|
||||
@@ -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
|
||||
-------------------
|
||||
@@ -38,6 +41,19 @@ README — TamaBox 站内信群发工具(mail-broadcast)
|
||||
to = ; 可选:默认测试收件邮箱
|
||||
delay = 1.0
|
||||
group_pause = 3.0
|
||||
smtp_idle_reconnect = 30 ; SMTP 空闲超时秒数,超时就重连
|
||||
|
||||
[bounce] ; 退信核查 / 发送中限流巡检(见下节)
|
||||
watch_bounces = 1 ; 发送中巡检限流退信,发现即中止
|
||||
watch_every = 10 ; 每发 N 封巡检一次
|
||||
prune_unknown = 0 ; 未分类退信是否也按可重试补发
|
||||
no_invalid_list= 0 ; 停用无效地址清单
|
||||
invalid_file = ; 留空用默认 broadcast_invalid.json
|
||||
bounce_report = ; 留空用默认
|
||||
imap_host = ; 留空按 SMTP 域名推导
|
||||
imap_port = 993
|
||||
since = auto ; auto = 从断点清单最早记录开始扫
|
||||
since_days = 3
|
||||
|
||||
conf/app.ini 路径的解析优先级:
|
||||
命令行 -c > params.ini [path] config > 环境变量 TAMABOX_CONFIG_PATH > ./conf/app.ini
|
||||
@@ -45,6 +61,11 @@ conf/app.ini 路径的解析优先级:
|
||||
安全边界:params.ini 不提供 --send / --yes——群发只能在命令行显式指定
|
||||
(或走交互模式在会话中确认),防止改配置文件时误发全量邮件。
|
||||
|
||||
优先级:**命令行参数 > params.ini > 内置默认**。所有 [bounce] 项都有同名命令行
|
||||
参数可临时覆盖(如 `--watch-every 5`、`--no-watch-bounces`)。布尔值写
|
||||
`1/0`、`yes/no`、`true/false`、`on/off` 均可,留空或写错会忽略并回退默认
|
||||
(控制台会给出警告)。
|
||||
|
||||
交互模式(默认)
|
||||
--------
|
||||
直接运行 `python3 broadcast.py` 即进入交互模式(无需加任何参数);
|
||||
@@ -135,22 +156,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 +197,60 @@ conf/app.ini 路径的解析优先级:
|
||||
时间开始——即只核查本次群发发出的那些邮件的退信
|
||||
- --since 2026-09-07 可显式指定起点日期
|
||||
- 断点清单为空时回退为 --since-days N(默认 3 天)
|
||||
|
||||
发送中限流巡检(边发边看,及时止损)
|
||||
----------------------------------
|
||||
限流退信是**异步**投到发件箱的——SMTP 当场返回 250,几十秒后收件箱才收到
|
||||
「您的账号外发频率超过邮件系统限制」。如果只等发完再核查,等发现时限流那一批
|
||||
早就全废了。所以正式群发时会**边发边巡检收件箱**:
|
||||
|
||||
- 每 `--watch-every` 封(默认 10)登录一次 IMAP 查新退信
|
||||
- 只看**本次运行开始之后**到达的退信,历史退信不会误触发
|
||||
- 一旦出现限流类退信 → 立即停止发送
|
||||
- SMTP 当场就返回限流(如 `450 MI:CEL 发送频率超限`)→ 同样立即停止
|
||||
- 善后:把限流退信的地址从断点清单剔除,等限制恢复后重跑自动补发
|
||||
- 巡检到的「地址不存在」类退信照旧写入无效地址清单
|
||||
|
||||
这两个开关**优先写在 params.ini 的 [bounce] 小节**(长期生效),命令行只用来
|
||||
临时覆盖:
|
||||
|
||||
# params.ini
|
||||
[bounce]
|
||||
watch_bounces = 1 ; 0 关闭巡检
|
||||
watch_every = 10 ; 每 10 封查一次
|
||||
|
||||
# 命令行临时覆盖(优先级更高,只影响这一次)
|
||||
python3 broadcast.py --send --yes --watch-every 5
|
||||
python3 broadcast.py --send --yes --no-watch-bounces
|
||||
|
||||
(--to 单封测试时本就不巡检)
|
||||
|
||||
中止时的输出示例:
|
||||
|
||||
[中止发送] 收件箱出现 1 条限流退信(如 a@b.com:关键词「您的账号外发频率超过邮件系统限制」)
|
||||
[善后] 已将 1 个限流退信地址从断点清单剔除,等限制恢复后重跑同一条命令即可自动补发。
|
||||
发送中止:已成功 30 封,失败 0 封,未发送 30 封。
|
||||
|
||||
注意:巡检依赖 IMAP 可用。若连续两轮连不上 IMAP,会自动关闭本次巡检并提示
|
||||
(不影响发送),此时请发完手动跑一次 `--check-bounces`。
|
||||
|
||||
无效地址清单 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(含每个收件人的类别与判定依据)
|
||||
|
||||
安全机制
|
||||
--------
|
||||
@@ -170,6 +258,10 @@ conf/app.ini 路径的解析优先级:
|
||||
- 发送前打印数据库统计与语言分布,人工核对
|
||||
- 每封间隔 --delay 秒(默认 1.0);--pause-every/--pause-for 防限流
|
||||
- 按语言分群发送,组间 --group-pause 秒(默认 3.0)
|
||||
- SMTP 断线自动重连:服务器会掐掉空闲连接,--delay 调大(如 60s/封)时
|
||||
必现「Server not connected / please run connect() first」,整批失败。
|
||||
连接空闲超过 `smtp_idle_reconnect` 秒(默认 30,0 关闭)就主动重连;
|
||||
仍遇到断线则立即重试最多 3 次(间隔 2s/4s),失败才会记为该收件人失败
|
||||
- SMTP 拒收(refused)计入失败并写入报告
|
||||
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
|
||||
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
|
||||
|
||||
Binary file not shown.
+811
-139
File diff suppressed because it is too large
Load Diff
+33
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[path]
|
||||
; conf/app.ini 路径(相对路径按运行脚本时的当前目录解析)
|
||||
config = ./auto/conf/app.ini
|
||||
config = ../auto/conf/app.ini
|
||||
|
||||
[site]
|
||||
site_url = https://box.shiroko.one
|
||||
@@ -18,3 +18,35 @@ old_domain = box.tama.guru
|
||||
; to = 可选:默认测试收件邮箱(正式群发用命令行 --send,不加 --to)
|
||||
delay = 1.0
|
||||
group_pause = 3.0
|
||||
; SMTP 连接空闲超过该秒数就主动重连再发。
|
||||
; delay 调大时(比如 60s/封)服务器会掐掉空闲连接,出现整批
|
||||
; 「please run connect() first」;保持默认 30 即可自动重连,设 0 关闭
|
||||
smtp_idle_reconnect = 30
|
||||
|
||||
; 退信核查 / 发送中限流巡检开关
|
||||
; 这些都能在 params.ini 里长期配置,命令行同名参数可临时覆盖
|
||||
[bounce]
|
||||
; 发送中巡检收件箱:出现「外发频率超过邮件系统限制」类限流退信立即中止发送
|
||||
; 中止后会把限流地址从断点清单剔除,等限制恢复重跑即自动补发
|
||||
watch_bounces = 1
|
||||
|
||||
; 每发 N 封巡检一次退信。调小更及时,但 IMAP 登录更频繁(不建议 < 5)
|
||||
watch_every = 10
|
||||
|
||||
; 退信核查时,分类不明的退信是否也按「可重试」一并剔除补发(默认 0,保守不补发)
|
||||
prune_unknown = 0
|
||||
|
||||
; 停用无效地址清单:既不跳过已知无效地址,也不写入新的硬退信
|
||||
no_invalid_list = 0
|
||||
|
||||
; 留空则用脚本同目录的默认文件名
|
||||
invalid_file =
|
||||
bounce_report =
|
||||
|
||||
; IMAP(退信核查要用发件邮箱的 IMAP;留空则按 SMTP 域名推导 smtp.xxx → imap.xxx)
|
||||
imap_host =
|
||||
imap_port = 993
|
||||
|
||||
; since = auto 表示从断点清单最早一条发送记录的时间开始扫描,只查本次群发的退信
|
||||
since = auto
|
||||
since_days = 3
|
||||
|
||||
Reference in New Issue
Block a user