Compare commits
7
Commits
909e853500
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1407fa9fb6 | ||
|
|
d4e5c611b0 | ||
|
|
6dc33604d1 | ||
|
|
5823b5fbb3 | ||
|
|
f83ea3e29a | ||
|
|
1cae63b671 | ||
|
|
5aad4a97e4 |
@@ -41,6 +41,20 @@ README — TamaBox 站内信群发工具(mail-broadcast)
|
||||
to = ; 可选:默认测试收件邮箱
|
||||
delay = 1.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 路径的解析优先级:
|
||||
命令行 -c > params.ini [path] config > 环境变量 TAMABOX_CONFIG_PATH > ./conf/app.ini
|
||||
@@ -48,6 +62,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` 即进入交互模式(无需加任何参数);
|
||||
@@ -180,6 +199,42 @@ 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
|
||||
-----------------------------------
|
||||
硬退信(地址不存在等)的地址会写进这里,之后**每次运行都直接跳过**,
|
||||
@@ -194,9 +249,61 @@ conf/app.ini 路径的解析优先级:
|
||||
- 用的是 conf/app.ini [mail] 的账号与密码(需邮箱已开启 IMAP,密码为邮箱登录密码)
|
||||
- 交互模式的运行方式「4) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发,
|
||||
选择「仅剔除 / 剔除并补发」时会再问一次是否连未分类的一起补发
|
||||
- 已核查过的退信(按 Message-ID)记录在 broadcast_bounces_seen.json,
|
||||
重复核查不会重复报告/重复剔除
|
||||
- 每次核查另写一份明细报告 broadcast_bounce_report.json(含每个收件人的类别与判定依据)
|
||||
- 已核查过的退信(按 Message-ID)与**上次扫描位置**(INBOX UID 游标 +
|
||||
UIDVALIDITY)都记录在 broadcast_bounces_seen.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 防限流
|
||||
- 按语言分群发送,组间 --group-pause 秒(默认 3.0)
|
||||
- SMTP 断线自动重连:服务器会掐掉空闲连接,--delay 调大(如 60s/封)时
|
||||
必现「Server not connected / please run connect() first」,整批失败。
|
||||
连接空闲超过 `smtp_idle_reconnect` 秒(默认 30,0 关闭)就主动重连;
|
||||
仍遇到断线则立即重试最多 3 次(间隔 2s/4s),失败才会记为该收件人失败
|
||||
- SMTP 拒收(refused)计入失败并写入报告
|
||||
- 批量信封遇 452「收件人数超限」自动砍半拆批重试;限流类拒收仍立即中止
|
||||
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
|
||||
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
|
||||
- 语言列自动试跑探测(language → lang → NULL 兜底),老库没有该列也能发
|
||||
|
||||
+862
-134
File diff suppressed because it is too large
Load Diff
+54
-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,56 @@ 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
|
||||
|
||||
; 合并信封批量发送(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