Compare commits
6
Commits
5aad4a97e4
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1407fa9fb6 | ||
|
|
d4e5c611b0 | ||
|
|
6dc33604d1 | ||
|
|
5823b5fbb3 | ||
|
|
f83ea3e29a | ||
|
|
1cae63b671 |
@@ -41,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
|
||||||
@@ -48,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` 即进入交互模式(无需加任何参数);
|
||||||
@@ -180,6 +199,42 @@ 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
|
无效地址清单 broadcast_invalid.json
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
硬退信(地址不存在等)的地址会写进这里,之后**每次运行都直接跳过**,
|
硬退信(地址不存在等)的地址会写进这里,之后**每次运行都直接跳过**,
|
||||||
@@ -194,9 +249,61 @@ conf/app.ini 路径的解析优先级:
|
|||||||
- 用的是 conf/app.ini [mail] 的账号与密码(需邮箱已开启 IMAP,密码为邮箱登录密码)
|
- 用的是 conf/app.ini [mail] 的账号与密码(需邮箱已开启 IMAP,密码为邮箱登录密码)
|
||||||
- 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发,
|
- 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发,
|
||||||
选择「仅剔除 / 剔除并补发」时会再问一次是否连未分类的一起补发
|
选择「仅剔除 / 剔除并补发」时会再问一次是否连未分类的一起补发
|
||||||
- 已核查过的退信(按 Message-ID)记录在 broadcast_bounces_seen.json,
|
- 已核查过的退信(按 Message-ID)与**上次扫描位置**(INBOX UID 游标 +
|
||||||
重复核查不会重复报告/重复剔除
|
UIDVALIDITY)都记录在 broadcast_bounces_seen.json:
|
||||||
- 每次核查另写一份明细报告 broadcast_bounce_report.json(含每个收件人的类别与判定依据)
|
默认从上次位置续扫(UID n+1:*),只拉新邮件的头部,重复核查不会
|
||||||
|
重复报告/重复剔除,也不会随邮箱邮件增多越扫越慢
|
||||||
|
- 以下情况自动回退为按日期窗口从头扫(扫完位置照常更新):
|
||||||
|
首次核查(无位置记录)、邮箱 UIDVALIDITY 变化(换号/重建邮箱)、
|
||||||
|
显式指定 --since、加 --full-scan
|
||||||
|
- 发送中巡检不推进扫描位置(它只看本次运行之后新到的退信,
|
||||||
|
范围内的历史邮件不标记「已扫」),位置只由 --check-bounces 推进
|
||||||
|
- 每次核查另写一份明细报告 broadcast_bounce_report.json(含每个收件人的
|
||||||
|
类别与判定依据,以及 scan_mode / scan_cursor)
|
||||||
|
|
||||||
|
发信账号来源:app.ini / 程序自带 SMTP
|
||||||
|
------------------------------------
|
||||||
|
默认用 conf/app.ini [mail] 的 SMTP(站内信跟随站点自己的邮箱)。切到程序
|
||||||
|
自带的一套后,发信与退信核查(IMAP)都走这套账号:
|
||||||
|
|
||||||
|
[mail]
|
||||||
|
source = app ; app(默认)= conf/app.ini [mail];own = 下面这套
|
||||||
|
account = bot@other.com
|
||||||
|
password = env:MY_SMTP_PASSWORD ; 支持 env:变量名,推荐
|
||||||
|
smtp = smtp.other.com
|
||||||
|
port = 465
|
||||||
|
skip_tls_verify = 0
|
||||||
|
|
||||||
|
临时切换(优先级高于 params.ini):`--mail-source own` / `--mail-source app`
|
||||||
|
交互模式里若 [mail] 配齐了,会多问一次选哪个来源。
|
||||||
|
|
||||||
|
安全提醒:
|
||||||
|
- params.ini 会被 git 跟踪,**不要把密码明文写进去**;用 `env:变量名`
|
||||||
|
从环境变量读取,或把 params.ini 加入 .gitignore
|
||||||
|
- 明文写死时脚本会打印警告,但不会阻止运行
|
||||||
|
- 切到 own 但 [mail] 缺 smtp/account/password 时直接 FATAL,不会用半套配置
|
||||||
|
|
||||||
|
合并信封批量发送(可选)
|
||||||
|
------------------------
|
||||||
|
默认逐人一封(一个 SMTP 信封 = 1 个 RCPT TO)。开启 batch_size 后,
|
||||||
|
「渲染后内容完全相同」的收件人合并发送:一个信封 = 1×MAIL FROM +
|
||||||
|
N×RCPT TO + 1×DATA,发送次数从「人数」降到「信封数」(如 305 人、
|
||||||
|
每 50 人一封 → 约 7 次发信)。
|
||||||
|
|
||||||
|
开启方式(0=关闭):
|
||||||
|
broadcast.py --send --single --batch-size 25
|
||||||
|
或 params.ini [send] batch_size = 25
|
||||||
|
|
||||||
|
与限流的关系(要点):
|
||||||
|
- 服务商按「发信次数」计频率 → 合并后成倍降低触发概率(主要收益)
|
||||||
|
- 服务商按「单位时间收件人总数」计数 → 无缓解,仍靠 delay/pause 控制节奏
|
||||||
|
- 单封收件人数上限常见 50~100:超限的 RCPT 会被 452 拒收,脚本自动把
|
||||||
|
batch_size 砍半、被拒者重试,不会中止也不会误判为限流
|
||||||
|
- 信封收件人对其他收件人不可见;批量信封的 To: 头显示为「站点名+发件邮箱」
|
||||||
|
|
||||||
|
适用的前提是内容逐字相同:模板含 {{name}}/{{box_link}} 等个人化占位符时,
|
||||||
|
渲染结果逐人不同,会自动落回逐封,不会错合。断点续发按人记录(DATA 被
|
||||||
|
服务器接收即整批入账,被拒的除外);发送中限流巡检照常按收件人数计数。
|
||||||
|
|
||||||
安全机制
|
安全机制
|
||||||
--------
|
--------
|
||||||
@@ -204,7 +311,12 @@ 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)计入失败并写入报告
|
||||||
|
- 批量信封遇 452「收件人数超限」自动砍半拆批重试;限流类拒收仍立即中止
|
||||||
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
|
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
|
||||||
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
|
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
|
||||||
- 语言列自动试跑探测(language → lang → NULL 兜底),老库没有该列也能发
|
- 语言列自动试跑探测(language → lang → NULL 兜底),老库没有该列也能发
|
||||||
|
|||||||
+862
-134
File diff suppressed because it is too large
Load Diff
+53
@@ -18,3 +18,56 @@ 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
|
||||||
|
|
||||||
|
; 发信账号来源:app = 用 conf/app.ini [mail] 的 SMTP(默认,站内信跟随站点邮箱)
|
||||||
|
; own = 用下面这套程序自带 SMTP(命令行 --mail-source own 可临时覆盖)
|
||||||
|
; 切到 own 后,发信与退信核查(IMAP)都走这套账号
|
||||||
|
[mail]
|
||||||
|
source = app
|
||||||
|
account =
|
||||||
|
; password 支持 env:变量名(推荐),避免明文写进会被 git 跟踪的文件
|
||||||
|
password =
|
||||||
|
smtp =
|
||||||
|
port = 465
|
||||||
|
skip_tls_verify = 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