Compare commits
7
Commits
912facaf2c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc33604d1 | ||
|
|
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 单文件模式主题(一行)
|
single.subject.txt 单文件模式主题(一行)
|
||||||
broadcast_state.json (运行后生成)断点续发记录,已发邮箱重跑自动跳过
|
broadcast_state.json (运行后生成)断点续发记录,已发邮箱重跑自动跳过
|
||||||
broadcast_report.json (运行后生成)每次运行的监控报告
|
broadcast_report.json (运行后生成)每次运行的监控报告
|
||||||
|
broadcast_invalid.json (运行后生成)无效地址清单:硬退信地址,永久跳过
|
||||||
|
broadcast_bounce_report.json (运行后生成)退信核查明细(含分类与判定依据)
|
||||||
|
broadcast_bounces_seen.json (运行后生成)已核查退信的 Message-ID,避免重复报告
|
||||||
|
|
||||||
参数文件 params.ini
|
参数文件 params.ini
|
||||||
-------------------
|
-------------------
|
||||||
@@ -38,6 +41,20 @@ README — TamaBox 站内信群发工具(mail-broadcast)
|
|||||||
to = ; 可选:默认测试收件邮箱
|
to = ; 可选:默认测试收件邮箱
|
||||||
delay = 1.0
|
delay = 1.0
|
||||||
group_pause = 3.0
|
group_pause = 3.0
|
||||||
|
smtp_idle_reconnect = 30 ; SMTP 空闲超时秒数,超时就重连
|
||||||
|
batch_size = 0 ; 合并信封:内容相同的收件人每 N 人一封(0=逐封)
|
||||||
|
|
||||||
|
[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 路径的解析优先级:
|
conf/app.ini 路径的解析优先级:
|
||||||
命令行 -c > params.ini [path] config > 环境变量 TAMABOX_CONFIG_PATH > ./conf/app.ini
|
命令行 -c > params.ini [path] config > 环境变量 TAMABOX_CONFIG_PATH > ./conf/app.ini
|
||||||
@@ -45,6 +62,11 @@ conf/app.ini 路径的解析优先级:
|
|||||||
安全边界:params.ini 不提供 --send / --yes——群发只能在命令行显式指定
|
安全边界:params.ini 不提供 --send / --yes——群发只能在命令行显式指定
|
||||||
(或走交互模式在会话中确认),防止改配置文件时误发全量邮件。
|
(或走交互模式在会话中确认),防止改配置文件时误发全量邮件。
|
||||||
|
|
||||||
|
优先级:**命令行参数 > params.ini > 内置默认**。所有 [bounce] 项都有同名命令行
|
||||||
|
参数可临时覆盖(如 `--watch-every 5`、`--no-watch-bounces`)。布尔值写
|
||||||
|
`1/0`、`yes/no`、`true/false`、`on/off` 均可,留空或写错会忽略并回退默认
|
||||||
|
(控制台会给出警告)。
|
||||||
|
|
||||||
交互模式(默认)
|
交互模式(默认)
|
||||||
--------
|
--------
|
||||||
直接运行 `python3 broadcast.py` 即进入交互模式(无需加任何参数);
|
直接运行 `python3 broadcast.py` 即进入交互模式(无需加任何参数);
|
||||||
@@ -135,22 +157,40 @@ conf/app.ini 路径的解析优先级:
|
|||||||
# 单文件模式:所有人发 templates/single.html(不按语言)
|
# 单文件模式:所有人发 templates/single.html(不按语言)
|
||||||
python3 broadcast.py --single --send --to you@example.com
|
python3 broadcast.py --single --send --to you@example.com
|
||||||
|
|
||||||
退信核查(验证真实送达)
|
退信核查(验证真实送达 · 按原因分类处理)
|
||||||
------------------------
|
----------------------------------------
|
||||||
背景:SMTP 返回 250 只代表服务器收下了,不代表送达。阿里云企业邮箱等对
|
背景: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
|
||||||
|
|
||||||
# 仅剔除:把这些用户从断点续发清单剔除后结束,本次不发送
|
# 仅剔除:把「可重试」的退信从断点续发清单剔除后结束,本次不发送
|
||||||
#(下次任意一次重跑会自动补上这批人)
|
#(下次任意一次重跑会自动补上这批人;地址不存在的不会补发)
|
||||||
python3 broadcast.py --check-bounces --prune-state
|
python3 broadcast.py --check-bounces --prune-state
|
||||||
|
|
||||||
# 剔除并补发:剔除后继续正常发送流程,本次就把退信用户补上
|
# 剔除并补发:剔除后继续正常发送流程,本次就把可重试的用户补上
|
||||||
python3 broadcast.py --check-bounces --prune-state --send
|
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-host imap.xxx.com(默认由 SMTP 域名推导 smtp.→imap.)
|
||||||
# --imap-port 993;扫描起点 --since auto(默认,见下)
|
# --imap-port 993;扫描起点 --since auto(默认,见下)
|
||||||
扫描起点(避免把群发无关的退信算进来):
|
扫描起点(避免把群发无关的退信算进来):
|
||||||
@@ -158,11 +198,60 @@ conf/app.ini 路径的解析优先级:
|
|||||||
时间开始——即只核查本次群发发出的那些邮件的退信
|
时间开始——即只核查本次群发发出的那些邮件的退信
|
||||||
- --since 2026-09-07 可显式指定起点日期
|
- --since 2026-09-07 可显式指定起点日期
|
||||||
- 断点清单为空时回退为 --since-days N(默认 3 天)
|
- 断点清单为空时回退为 --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,密码为邮箱登录密码)
|
- 用的是 conf/app.ini [mail] 的账号与密码(需邮箱已开启 IMAP,密码为邮箱登录密码)
|
||||||
- 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发
|
- 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发,
|
||||||
|
选择「仅剔除 / 剔除并补发」时会再问一次是否连未分类的一起补发
|
||||||
- 已核查过的退信(按 Message-ID)记录在 broadcast_bounces_seen.json,
|
- 已核查过的退信(按 Message-ID)记录在 broadcast_bounces_seen.json,
|
||||||
重复核查不会重复报告/重复剔除
|
重复核查不会重复报告/重复剔除
|
||||||
|
- 每次核查另写一份明细报告 broadcast_bounce_report.json(含每个收件人的类别与判定依据)
|
||||||
|
|
||||||
安全机制
|
安全机制
|
||||||
--------
|
--------
|
||||||
@@ -170,6 +259,10 @@ conf/app.ini 路径的解析优先级:
|
|||||||
- 发送前打印数据库统计与语言分布,人工核对
|
- 发送前打印数据库统计与语言分布,人工核对
|
||||||
- 每封间隔 --delay 秒(默认 1.0);--pause-every/--pause-for 防限流
|
- 每封间隔 --delay 秒(默认 1.0);--pause-every/--pause-for 防限流
|
||||||
- 按语言分群发送,组间 --group-pause 秒(默认 3.0)
|
- 按语言分群发送,组间 --group-pause 秒(默认 3.0)
|
||||||
|
- SMTP 断线自动重连:服务器会掐掉空闲连接,--delay 调大(如 60s/封)时
|
||||||
|
必现「Server not connected / please run connect() first」,整批失败。
|
||||||
|
连接空闲超过 `smtp_idle_reconnect` 秒(默认 30,0 关闭)就主动重连;
|
||||||
|
仍遇到断线则立即重试最多 3 次(间隔 2s/4s),失败才会记为该收件人失败
|
||||||
- SMTP 拒收(refused)计入失败并写入报告
|
- SMTP 拒收(refused)计入失败并写入报告
|
||||||
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
|
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
|
||||||
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
|
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
|
||||||
|
|||||||
Binary file not shown.
+966
-133
File diff suppressed because it is too large
Load Diff
+42
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
[path]
|
[path]
|
||||||
; conf/app.ini 路径(相对路径按运行脚本时的当前目录解析)
|
; conf/app.ini 路径(相对路径按运行脚本时的当前目录解析)
|
||||||
config = ./auto/conf/app.ini
|
config = ../auto/conf/app.ini
|
||||||
|
|
||||||
[site]
|
[site]
|
||||||
site_url = https://box.shiroko.one
|
site_url = https://box.shiroko.one
|
||||||
@@ -18,3 +18,44 @@ old_domain = box.tama.guru
|
|||||||
; to = 可选:默认测试收件邮箱(正式群发用命令行 --send,不加 --to)
|
; to = 可选:默认测试收件邮箱(正式群发用命令行 --send,不加 --to)
|
||||||
delay = 1.0
|
delay = 1.0
|
||||||
group_pause = 3.0
|
group_pause = 3.0
|
||||||
|
; SMTP 连接空闲超过该秒数就主动重连再发。
|
||||||
|
; delay 调大时(比如 60s/封)服务器会掐掉空闲连接,出现整批
|
||||||
|
; 「please run connect() first」;保持默认 30 即可自动重连,设 0 关闭
|
||||||
|
smtp_idle_reconnect = 30
|
||||||
|
|
||||||
|
; 合并信封批量发送(0=逐封)。渲染内容完全相同的收件人每 N 人共用一个
|
||||||
|
; SMTP 信封:发送次数从「人数」降到「信封数」(如 305 人、每 50 人一封
|
||||||
|
; → 约 7 次发信),可显著降低触发「外发频率超过邮件系统限制」类限流的
|
||||||
|
; 概率;若服务商按收件人计数则无缓解。
|
||||||
|
; 注意:单封收件人有服务商上限(常见 50~100),超限被 452 拒收时脚本会
|
||||||
|
; 自动砍半拆批重试,不会中止;delay 在批量模式下的语义是「每批之间」间隔。
|
||||||
|
; 模板含 {{name}} 等个人化占位符时内容逐人不同,自动回退逐封,不会错合。
|
||||||
|
batch_size = 0
|
||||||
|
|
||||||
|
; 退信核查 / 发送中限流巡检开关
|
||||||
|
; 这些都能在 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