Compare commits

..
6 Commits
Author SHA1 Message Date
tamakyi f83ea3e29a 退信/巡检开关移入 params.ini [bounce],命令行仅作临时覆盖
原来这些开关只能靠命令行传,每次群发都得敲一长串,也容易漏。现在统一在
params.ini 的 [bounce] 小节长期配置,优先级为 命令行 > params.ini > 内置默认。

改动:
- load_params 新增 [bounce] 解析:watch_bounces / watch_every / prune_unknown /
  no_invalid_list / invalid_file / bounce_report / imap_host / imap_port /
  since / since_days;新增 _parse_bool 支持 1/0、yes/no、true/false、on/off,
  留空或非法值忽略并回退默认(带警告)
- 相关 argparse 默认值改为 None,以便区分「用户显式传了」还是「没传」——
  没传才用 params.ini 的值,保证命令行优先级
- save_params 回写 [bounce](用当前生效值),并用 _preserve_extra_sections
  原样保留文件里其它手写小节,避免保存时把配置抹掉
- 交互模式参数摘要里显示巡检开关的当前值与来源
- 把 argparse 构造抽成 build_arg_parser(),便于测试
- params.ini 补上 [bounce] 小节的默认配置与注释;README 同步
2026-09-07 19:25:20 +08:00
tamakyi 1cae63b671 发送中巡检限流退信:发现即中止发送
背景:限流退信是异步投到发件箱的——SMTP 当场返回 250,几十秒后收件箱
才收到「您的账号外发频率超过邮件系统限制」。只等发完再核查,等发现时
被限流的那一批早就全废了,白耗额度。

改动:
- 把 IMAP 扫描抽成 _scan_imap_bounces(),供退信核查与发送中巡检共用
  (新增 not_before 参数:只看指定时刻之后到达的退信)
- 发送过程中每 --watch-every 封(默认 10)巡检一次收件箱,命中限流类
  退信立即中止;只看本次运行开始后的新退信,历史退信不误触发
- SMTP 当场返回限流(如 450 MI:CEL)也立即中止;硬退信则照旧记入
  无效地址清单并继续发下一封
- 中止后把限流地址从断点清单剔除,等限制恢复重跑即自动补发
- IMAP 连续两轮不可用则自动关闭本次巡检并提示,不影响发送
- 新增 --no-watch-bounces / --watch-every,报告新增 aborted 等字段
- 顺带:分类依据里的状态码改为只显示匹配到的码(原来是整串原文)
2026-09-07 19:11:14 +08:00
tamakyi 5aad4a97e4 fix path error 2026-09-07 18:38:52 +08:00
tamakyi 909e853500 gitignore: 忽略 .workbuddy 项目数据目录 2026-09-07 18:30:04 +08:00
tamakyi bc971deb37 退信核查按原因分类:只有限流/临时性退信才剔除补发
问题:退信核查此前凡是退信一律从断点清单剔除、下次重跑补发。
但退信分两类——「您的账号外发频率超过邮件系统限制」这类限流退信重发
可成功;「地址不存在」这类硬退信重发也发不出去,只会浪费每日发信额度、
拖垮发信信誉。

改动:
- 新增退信分类 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
2026-09-07 18:26:09 +08:00
tamakyi 912facaf2c first init 2026-09-07 18:04:51 +08:00
15 changed files with 2996 additions and 191 deletions
+10 -172
View File
@@ -1,176 +1,14 @@
# ---> Python
# Byte-compiled / optimized / DLL files
# Python
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# WorkBuddy 项目数据(会话记忆等)
.workbuddy/
# 运行时生成的状态/报告文件(含真实用户邮箱,不入库)
broadcast_state.json
broadcast_report.json
broadcast_invalid.json
broadcast_bounce_report.json
broadcast_bounces_seen.json
*.tmp
-18
View File
@@ -1,18 +0,0 @@
MIT License
Copyright (c) 2026 tamakyi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+262 -1
View File
@@ -1,2 +1,263 @@
# tamabox-broadcast-mailsystem
README — TamaBox 站内信群发工具(mail-broadcast
=================================================
独立项目:只读生产 conf/app.ini 的数据库与 SMTP 配置,向「未注销且有邮箱」的用户
按语言群发邮件。零 pip 依赖(数据库走系统 psql/mysql 客户端,SMTP 走 Python 标准库)。
目录结构
--------
broadcast.py 主脚本
params.ini 参数文件(运行时自动读取;命令行参数优先级更高)
templates/ 模板目录(想改文案只动这里)
zh-CN.html 简体中文正文
zh-CN.subject.txt 简体中文主题(一行)
zh-CN.plain.txt 简体中文纯文本(可选;没有就自动从 HTML 抽取)
zh-TW.* / en.* / ja.* 其余三种语言同构
single.html 单文件通用模板(--single 模式;所有人不论语言都发这封)
single.subject.txt 单文件模式主题(一行)
broadcast_state.json (运行后生成)断点续发记录,已发邮箱重跑自动跳过
broadcast_report.json (运行后生成)每次运行的监控报告
broadcast_invalid.json (运行后生成)无效地址清单:硬退信地址,永久跳过
broadcast_bounce_report.json (运行后生成)退信核查明细(含分类与判定依据)
broadcast_bounces_seen.json (运行后生成)已核查退信的 Message-ID,避免重复报告
参数文件 params.ini
-------------------
脚本每次运行自动读取(不存在则跳过,不报错),可用 --params 指定其他路径、
--no-params 关闭。命令行参数 > params.ini > 内置默认。
[path]
config = ./auto/conf/app.ini ; conf/app.ini 路径(相对路径按运行时所在目录解析)
[site]
site_url = https://box.shiroko.one ; 对应 {{site}},也是 box_link 的前缀
box_prefix = /_/ ; 个性域名链接前缀
[vars] ; 自定义占位符:模板里用 {{键名}} 引用
old_domain = box.tama.guru
expire_date = 2026-12-24
[send]
to = ; 可选:默认测试收件邮箱
delay = 1.0
group_pause = 3.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
安全边界:params.ini 不提供 --send / --yes——群发只能在命令行显式指定
(或走交互模式在会话中确认),防止改配置文件时误发全量邮件。
优先级:**命令行参数 > params.ini > 内置默认**。所有 [bounce] 项都有同名命令行
参数可临时覆盖(如 `--watch-every 5``--no-watch-bounces`)。布尔值写
`1/0``yes/no``true/false``on/off` 均可,留空或写错会忽略并回退默认
(控制台会给出警告)。
交互模式(默认)
--------
直接运行 `python3 broadcast.py` 即进入交互模式(无需加任何参数);
命令行带了 --send/--to/--yes/--dry-run 之一、或显式 --no-interactive 时不进入,
按参数直接执行(脚本化/定时任务用)。
**只问缺的,有值不问**:交互开头先打印一份「参数确认」摘要,凡已有值的参数
params.ini / 命令行 / app.ini external_url 兜底)直接沿用、不再逐项询问;
要改值请编辑 params.ini,或干脆把一次性内容(旧域名、到期日等)直接写死到
模板 html 里并删掉对应 {{占位符}}。只有「模板用到但还没有值」的占位符和
完全缺失的 site_url 才会补问。交互流程:
- 参数摘要(有值直接沿用)+ 补问缺失项
- 模板模式:
1) 多语言 —— 按 users.language 使用各自语言模板(默认)
2) 单文件 —— 所有人发送 templates/single.html 通用模板
- 运行方式直接在交互中选择(不需要命令行 --send/--to/--yes):
1) 演练 —— 只列收件人、统计与渲染预览,不发信(默认)
2) 测试 —— 只给一个邮箱发一封,验证模板与发信链路
3) 群发 —— 向全体未注销用户发送;可顺手设 --limit 小量试水,
并可选择是否保留发送前的最终确认(默认保留)
- 最后可选把补全的参数保存回 params.ini,下次运行直接生效
单文件模式(通用模板)
--------
不按用户语言发送,所有人都收到 templates/single.html 这一封:
# 命令行
python3 broadcast.py --single --send --to you@example.com
# 或交互模式里"模板模式"选 2
用途:以后再做站内通知(维护公告、功能上线等)时,直接编辑 single.html
的「正文内容区」,样式无需重做;每个用户依然按其个性域名收到专属 box_link。
若存在 single.subject.txt 则用其主题,否则回退「【{{site_title}}】站内通知」。
模板风格
--------
四语言模板已对齐原程序 templates/mail/ 的样式(Google 风格邮件):
白底居中卡片、#dadce0 细边框 8px 圆角、标题 24px 带下边框分隔、
正文 Roboto 14px rgba(0,0,0,0.87)、次要信息 rgba(0,0,0,0.54)、
操作按钮 #4184F3 蓝底白字、页脚 11px 浅灰居中。
改动配色/排版时保持内联 style 写法即可(邮件客户端兼容)。
修改文案
--------
直接编辑 templates/ 下对应语言的 .html / .subject.txt / .plain.txt
保存后下次运行即生效,不需要改任何 Python 代码。
新增语言:放一份 <新语言码>.html + .subject.txt 进 templates/ 即会被自动识别。
占位符(发送时逐用户替换)
--------------------------
{{name}} 用户名(空则显示「用户」)
{{domain}} 用户个性域名(可能为空)
{{box_link}} 该用户的提问箱链接 = site-url + box-prefix + domain
无个性域名时退回站点首页
{{email}} 收件邮箱
{{site}} 站点地址(--site-url
{{site_title}} 站点名(app.ini [app] title
{{year}} 当前年份(版权行用)
{{任意自定义}} 用 --var key=value 传入,如 {{old_domain}}、{{expire_date}}
注意:模板里写了但没有提供值的占位符会被替换为空串,
结束时会在控制台与报告中列出缺失键,方便发现笔误。
个性域名链接规则
----------------
box_link = {--site-url}{--box-prefix}{domain}
默认 box-prefix 为 /_/,即 https://box.shiroko.one/_/某人的域名
用户没有个性域名时,{{box_link}} 退回站点首页。
常用命令
--------
# 默认即交互模式:逐项确认参数,并选择模板模式与运行方式
python3 broadcast.py
# 演练(不发信,跳过交互)
python3 broadcast.py --dry-run --no-interactive
# 测试:给单个邮箱发一封(命中数据库用户则套用其真实数据)
python3 broadcast.py --send --to you@example.com
# 正式群发(建议先 --limit 50 试水;中断后重跑自动断点续发)
python3 broadcast.py --send --yes
# 临时覆盖 params.ini 的值(CLI 优先级更高)
python3 broadcast.py -c /别的/conf/app.ini --send --var expire_date=2027-01-01
# 从头重发(清空断点续发记录,慎用)
python3 broadcast.py --send --reset-state
# 单文件模式:所有人发 templates/single.html(不按语言)
python3 broadcast.py --single --send --to you@example.com
退信核查(验证真实送达 · 按原因分类处理)
----------------------------------------
背景:SMTP 返回 250 只代表服务器收下了,不代表送达。阿里云企业邮箱等对
「发送频率超限」「收件人不存在」等情况往往是先收下、再异步把退信通知投到
发件账号的收件箱——脚本当时的「发送成功」并不真实。
**退信不是一类,必须分类处理**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(默认,见下)
扫描起点(避免把群发无关的退信算进来):
- 默认 auto:从断点续发清单(broadcast_state.json)最早一条发送记录的
时间开始——即只核查本次群发发出的那些邮件的退信
- --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) 退信核查」里同样三选一:仅核查 / 仅剔除 / 剔除并补发,
选择「仅剔除 / 剔除并补发」时会再问一次是否连未分类的一起补发
- 已核查过的退信(按 Message-ID)记录在 broadcast_bounces_seen.json
重复核查不会重复报告/重复剔除
- 每次核查另写一份明细报告 broadcast_bounce_report.json(含每个收件人的类别与判定依据)
安全机制
--------
- 默认 dry-run--send 需终端输入 yes 确认(--yes 跳过)
- 发送前打印数据库统计与语言分布,人工核对
- 每封间隔 --delay 秒(默认 1.0);--pause-every/--pause-for 防限流
- 按语言分群发送,组间 --group-pause 秒(默认 3.0
- SMTP 拒收(refused)计入失败并写入报告
- From/Subject 头自动做 RFC2047 编码(中文显示名不会被 QQ 邮箱 550 拒收)
- 自动定位 users 表所在 schema(避免 psql 命中别的同名空表)
- 语言列自动试跑探测(language → lang → NULL 兜底),老库没有该列也能发
+2257
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
; broadcast.py 参数文件:每次运行自动读取;命令行参数优先级更高
; 注意:本文件不控制 --send/--yes,正式发送仍需命令行显式指定
[path]
; conf/app.ini 路径(相对路径按运行脚本时的当前目录解析)
config = ../auto/conf/app.ini
[site]
site_url = https://box.shiroko.one
box_prefix = /_/
; 自定义占位符:模板里用 {{键名}} 引用
[vars]
expire_date = 2026-12-24
old_domain = box.tama.guru
[send]
; to = 可选:默认测试收件邮箱(正式群发用命令行 --send,不加 --to
delay = 1.0
group_pause = 3.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
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="format-detection" content="email=no"/>
<meta name="format-detection" content="date=no"/>
</head>
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0" lang="en">
<tbody>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
<tr align="center">
<td>
<table border="0" cellspacing="0" cellpadding="0"
style="padding-bottom: 20px;max-width: 516px;min-width: 220px;">
<tbody>
<tr>
<td width="8" style="width: 8px;"></td>
<td>
<div style="border-style: solid; border-width: thin; border-color:#dadce0; border-radius: 8px; padding: 40px 20px;"
align="center">
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif;border-bottom: thin solid #dadce0; color: rgba(0,0,0,0.87); line-height: 32px; padding-bottom: 24px;text-align: center; word-break: break-word;">
<div style="font-size: 24px;">
{{site_title}} domain change notice
</div>
<div style="font-size: 13px; color: rgba(0,0,0,0.54); margin-top: 6px;">
Hi {{name}}, this is a bulk notification sent to all registered users.
</div>
</div>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 14px; color: rgba(0,0,0,0.87); line-height: 22px; padding-top: 20px; text-align: left;">
<div style="padding: 12px 14px; border-radius: 6px; background-color: #e8f0fe; border: 1px solid #c6dafc; margin-bottom: 14px; text-align: center;">
<div style="font-size: 13px; color: rgba(0,0,0,0.54);">Our new domain</div>
<div style="font-size: 18px; font-weight: bold; color: #1a73e8; margin-top: 4px; word-break: break-all;">{{site}}</div>
</div>
<p style="margin: 0 0 14px;">First of all, thank you for using {{site_title}}. Our site has officially moved to the new domain <b>{{site}}</b> (previously {{old_domain}}).</p>
<p style="margin: 0 0 6px;"><b>Why the change?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">The .guru domain has kept raising its renewal price in recent years, so we have migrated to the more stable .one domain.</p>
<p style="margin: 0 0 6px;"><b>What happens to the old domain?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">The old domain will expire on <b>{{expire_date}}</b>. Until then, visits to the old domain will be redirected automatically to the new one via a permanent 301 redirect — your account and data stay unchanged. After that date, the old domain will be retired completely.</p>
<p style="margin: 0 0 6px;"><b>What should I do?</b></p>
<p style="margin: 0 0 4px; color: rgba(0,0,0,0.54);">1. Remember the new domain {{site}} and use it from now on.</p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">2. If you have shared your question box via a QR code before, please replace it with a new QR code generated for the new domain.</p>
<p style="margin: 0 0 10px;">Your personal question box link (bookmark it):</p>
</div>
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif; line-height: 16px; font-size: 14px; text-align: center;">
<a href="{{box_link}}" target="_blank"
style="color: #ffffff; font-weight: 400; text-decoration: none; display: inline-block; padding: 10px 24px; background-color: #4184F3; border-radius: 5px; min-width: 90px;">
Open my question box
</a>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: rgba(0,0,0,0.54); padding-top: 10px; word-break: break-all;">
{{box_link}}
</div>
</div>
</div>
<div style="text-align: left;">
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;color: rgba(0,0,0,0.54);font-size: 11px; line-height: 18px; padding-top: 12px; text-align: center;">
<div>
This is an automated notification sent to all registered users. Please do not reply directly.<br/>
Thank you for your continued support.<br/>
© {{year}} {{site_title}}
</div>
</div>
</div>
</td>
<td width="8" style="width: 8px;"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
【{{site_title}}】Important: we've moved to {{site}}
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta name="format-detection" content="email=no"/>
<meta name="format-detection" content="date=no"/>
</head>
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0" lang="ja">
<tbody>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
<tr align="center">
<td>
<table border="0" cellspacing="0" cellpadding="0"
style="padding-bottom: 20px;max-width: 516px;min-width: 220px;">
<tbody>
<tr>
<td width="8" style="width: 8px;"></td>
<td>
<div style="border-style: solid; border-width: thin; border-color:#dadce0; border-radius: 8px; padding: 40px 20px;"
align="center">
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif;border-bottom: thin solid #dadce0; color: rgba(0,0,0,0.87); line-height: 32px; padding-bottom: 24px;text-align: center; word-break: break-word;">
<div style="font-size: 24px;">
{{site_title}} ドメイン変更のお知らせ
</div>
<div style="font-size: 13px; color: rgba(0,0,0,0.54); margin-top: 6px;">
{{name}} 様、これは全登録ユーザーへの一括通知メールです。
</div>
</div>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 14px; color: rgba(0,0,0,0.87); line-height: 22px; padding-top: 20px; text-align: left;">
<div style="padding: 12px 14px; border-radius: 6px; background-color: #e8f0fe; border: 1px solid #c6dafc; margin-bottom: 14px; text-align: center;">
<div style="font-size: 13px; color: rgba(0,0,0,0.54);">新しいドメイン</div>
<div style="font-size: 18px; font-weight: bold; color: #1a73e8; margin-top: 4px; word-break: break-all;">{{site}}</div>
</div>
<p style="margin: 0 0 14px;">日頃より {{site_title}} をご利用いただき、ありがとうございます。当サイトは新しいドメイン <b>{{site}}</b>(旧ドメイン:{{old_domain}})へ正式に移行しました。</p>
<p style="margin: 0 0 6px;"><b>なぜドメインを変更するのですか?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">.guru ドメインは近年更新料金が連続して値上げしており、維持コストが高騰しているため、より安定した .one ドメインへ移行しました。</p>
<p style="margin: 0 0 6px;"><b>旧ドメインはどうなりますか?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">旧ドメインは <b>{{expire_date}}</b> に失効します。失効までの間、旧ドメインへのアクセスは 301 永久リダイレクトにより自動的に新ドメインへ転送されます。アカウントやデータに変更はありません。失効後、旧ドメインは完全に使用されなくなります。</p>
<p style="margin: 0 0 6px;"><b>何をすればよいですか?</b></p>
<p style="margin: 0 0 4px; color: rgba(0,0,0,0.54);">1. 新しいドメイン {{site}} を覚えて、今後はそちらにアクセスしてください。</p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">2. 以前 QR コードで質問箱を共有していた場合は、新しいドメインに対応した QR コードに張り替えてください。</p>
<p style="margin: 0 0 10px;">あなたの質問箱のアドレス(ブックマーク推奨):</p>
</div>
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif; line-height: 16px; font-size: 14px; text-align: center;">
<a href="{{box_link}}" target="_blank"
style="color: #ffffff; font-weight: 400; text-decoration: none; display: inline-block; padding: 10px 24px; background-color: #4184F3; border-radius: 5px; min-width: 90px;">
マイ質問箱を開く
</a>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: rgba(0,0,0,0.54); padding-top: 10px; word-break: break-all;">
{{box_link}}
</div>
</div>
</div>
<div style="text-align: left;">
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;color: rgba(0,0,0,0.54);font-size: 11px; line-height: 18px; padding-top: 12px; text-align: center;">
<div>
このメールは全登録ユーザーへ一括送信されたシステム通知です。直接の返信はできません。<br/>
これからも応援よろしくお願いいたします。<br/>
© {{year}} {{site_title}}
</div>
</div>
</div>
</td>
<td width="8" style="width: 8px;"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
【{{site_title}}】重要:サイトは {{site}} に移転しました
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="format-detection" content="email=no"/>
<meta name="format-detection" content="date=no"/>
</head>
<!--
单文件通用模板(--single 模式):所有人不论语言都收到这封邮件。
每次新的群发活动,直接编辑下面「正文内容区」即可,样式已对齐站内系统邮件。
可用占位符:{{name}} {{domain}} {{email}} {{box_link}} {{site}} {{site_title}} {{year}}
以及 --var 键=值 / params.ini [vars] 传入的自定义占位符。
-->
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0" lang="zh-CN">
<tbody>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
<tr align="center">
<td>
<table border="0" cellspacing="0" cellpadding="0"
style="padding-bottom: 20px;max-width: 516px;min-width: 220px;">
<tbody>
<tr>
<td width="8" style="width: 8px;"></td>
<td>
<div style="border-style: solid; border-width: thin; border-color:#dadce0; border-radius: 8px; padding: 40px 20px;"
align="center">
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif;border-bottom: thin solid #dadce0; color: rgba(0,0,0,0.87); line-height: 32px; padding-bottom: 24px;text-align: center; word-break: break-word;">
<div style="font-size: 24px;">
{{site_title}} 站内通知
</div>
<div style="font-size: 13px; color: rgba(0,0,0,0.54); margin-top: 6px;">
你好,{{name}}!这是一封群发的站内通知邮件
</div>
</div>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 14px; color: rgba(0,0,0,0.87); line-height: 22px; padding-top: 20px; text-align: left;">
<!-- ==================== 正文内容区(编辑这里) ==================== -->
<p style="margin: 0 0 14px;">感谢你一直以来使用 {{site_title}}。这里是本次通知的正文内容——发送前请编辑 templates/single.html,把本段替换成你想要告知用户的内容。</p>
<p style="margin: 0 0 6px;"><b>小标题(可选,可删)</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">需要分段就复制这些 &lt;p&gt; 段落;需要强调框可以复制下面这个浅灰框。</p>
<div style="padding: 12px 14px; border-radius: 6px; background-color: #f1f3f4; border: 1px solid #dadce0; margin-bottom: 14px;">
强调框示例(可删):重要信息放在这里,比如时间、地址或注意事项。
</div>
<!-- ==================== 正文内容区结束 ==================== -->
<p style="margin: 0 0 10px;">你的提问箱地址(建议收藏):</p>
</div>
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif; line-height: 16px; font-size: 14px; text-align: center;">
<a href="{{box_link}}" target="_blank"
style="color: #ffffff; font-weight: 400; text-decoration: none; display: inline-block; padding: 10px 24px; background-color: #4184F3; border-radius: 5px; min-width: 90px;">
访问我的提问箱
</a>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: rgba(0,0,0,0.54); padding-top: 10px; word-break: break-all;">
{{box_link}}
</div>
</div>
</div>
<div style="text-align: left;">
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;color: rgba(0,0,0,0.54);font-size: 11px; line-height: 18px; padding-top: 12px; text-align: center;">
<div>
这是一封系统通知邮件,由站点统一发送给全体注册用户,请勿直接回复。<br/>
© {{year}} {{site_title}}
</div>
</div>
</div>
</td>
<td width="8" style="width: 8px;"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
【{{site_title}}】站内通知
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="format-detection" content="email=no"/>
<meta name="format-detection" content="date=no"/>
</head>
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0" lang="zh-CN">
<tbody>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
<tr align="center">
<td>
<table border="0" cellspacing="0" cellpadding="0"
style="padding-bottom: 20px;max-width: 516px;min-width: 220px;">
<tbody>
<tr>
<td width="8" style="width: 8px;"></td>
<td>
<div style="border-style: solid; border-width: thin; border-color:#dadce0; border-radius: 8px; padding: 40px 20px;"
align="center">
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif;border-bottom: thin solid #dadce0; color: rgba(0,0,0,0.87); line-height: 32px; padding-bottom: 24px;text-align: center; word-break: break-word;">
<div style="font-size: 24px;">
{{site_title}} 域名变更通知
</div>
<div style="font-size: 13px; color: rgba(0,0,0,0.54); margin-top: 6px;">
你好,{{name}}!这是一封群发的站内通知邮件
</div>
</div>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 14px; color: rgba(0,0,0,0.87); line-height: 22px; padding-top: 20px; text-align: left;">
<div style="padding: 12px 14px; border-radius: 6px; background-color: #e8f0fe; border: 1px solid #c6dafc; margin-bottom: 14px; text-align: center;">
<div style="font-size: 13px; color: rgba(0,0,0,0.54);">本站新域名</div>
<div style="font-size: 18px; font-weight: bold; color: #1a73e8; margin-top: 4px; word-break: break-all;">{{site}}</div>
</div>
<p style="margin: 0 0 14px;">首先,感谢你一直以来使用 {{site_title}}。本站已正式切换到新域名 <b>{{site}}</b>(原域名:{{old_domain}})。</p>
<p style="margin: 0 0 6px;"><b>为什么换域名?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">.guru 域名近年来连续涨价,续费成本越来越高,因此我们迁移到了更稳定的 .one 域名。</p>
<p style="margin: 0 0 6px;"><b>原域名会怎样?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">原域名将在 <b>{{expire_date}}</b> 到期。到期前访问原域名会通过 301 永久重定向自动跳转到新域名,你的账号和数据都不变;到期之后,旧域名将彻底不再使用。</p>
<p style="margin: 0 0 6px;"><b>你该做什么?</b></p>
<p style="margin: 0 0 4px; color: rgba(0,0,0,0.54);">1. 记住新域名 {{site}},以后直接访问这里。</p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">2. 如果之前通过二维码分享过自己的提问箱,记得把二维码替换成新域名对应的新二维码。</p>
<p style="margin: 0 0 10px;">你的提问箱地址(建议收藏):</p>
</div>
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif; line-height: 16px; font-size: 14px; text-align: center;">
<a href="{{box_link}}" target="_blank"
style="color: #ffffff; font-weight: 400; text-decoration: none; display: inline-block; padding: 10px 24px; background-color: #4184F3; border-radius: 5px; min-width: 90px;">
打开我的提问箱
</a>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: rgba(0,0,0,0.54); padding-top: 10px; word-break: break-all;">
{{box_link}}
</div>
</div>
</div>
<div style="text-align: left;">
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;color: rgba(0,0,0,0.54);font-size: 11px; line-height: 18px; padding-top: 12px; text-align: center;">
<div>
这是一封系统通知邮件,由站点统一发送给全体注册用户,请勿直接回复。<br/>
感谢你一直以来的陪伴与支持。<br/>
© {{year}} {{site_title}}
</div>
</div>
</div>
</td>
<td width="8" style="width: 8px;"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
【{{site_title}}】重要通知:本站已更换域名为 {{site}}
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta name="format-detection" content="email=no"/>
<meta name="format-detection" content="date=no"/>
</head>
<body style="margin: 0; padding: 0;" bgcolor="#FFFFFF">
<table width="100%" height="100%" style="min-width: 348px;" border="0" cellspacing="0" cellpadding="0" lang="zh-TW">
<tbody>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
<tr align="center">
<td>
<table border="0" cellspacing="0" cellpadding="0"
style="padding-bottom: 20px;max-width: 516px;min-width: 220px;">
<tbody>
<tr>
<td width="8" style="width: 8px;"></td>
<td>
<div style="border-style: solid; border-width: thin; border-color:#dadce0; border-radius: 8px; padding: 40px 20px;"
align="center">
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif;border-bottom: thin solid #dadce0; color: rgba(0,0,0,0.87); line-height: 32px; padding-bottom: 24px;text-align: center; word-break: break-word;">
<div style="font-size: 24px;">
{{site_title}} 網域變更通知
</div>
<div style="font-size: 13px; color: rgba(0,0,0,0.54); margin-top: 6px;">
你好,{{name}}!這是一封群發的站內通知郵件
</div>
</div>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 14px; color: rgba(0,0,0,0.87); line-height: 22px; padding-top: 20px; text-align: left;">
<div style="padding: 12px 14px; border-radius: 6px; background-color: #e8f0fe; border: 1px solid #c6dafc; margin-bottom: 14px; text-align: center;">
<div style="font-size: 13px; color: rgba(0,0,0,0.54);">本站新網域</div>
<div style="font-size: 18px; font-weight: bold; color: #1a73e8; margin-top: 4px; word-break: break-all;">{{site}}</div>
</div>
<p style="margin: 0 0 14px;">首先,感謝你一直以來使用 {{site_title}}。本站已正式切換到新網域 <b>{{site}}</b>(原網域:{{old_domain}})。</p>
<p style="margin: 0 0 6px;"><b>為什麼換網域?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">.guru 網域近年來連續漲價,續費成本越來越高,因此我們遷移到了更穩定的 .one 網域。</p>
<p style="margin: 0 0 6px;"><b>原網域會怎樣?</b></p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">原網域將於 <b>{{expire_date}}</b> 到期。到期前造訪原網域會透過 301 永久重新導向自動跳轉到新網域,你的帳號和資料都不變;到期之後,舊網域將徹底不再使用。</p>
<p style="margin: 0 0 6px;"><b>你該做什麼?</b></p>
<p style="margin: 0 0 4px; color: rgba(0,0,0,0.54);">1. 記住新網域 {{site}},以後直接造訪這裡。</p>
<p style="margin: 0 0 14px; color: rgba(0,0,0,0.54);">2. 如果之前透過 QR Code 分享過自己的提問箱,記得把 QR Code 換成新網域對應的新 QR Code。</p>
<p style="margin: 0 0 10px;">你的提問箱地址(建議收藏):</p>
</div>
<div style="font-family: 'Google Sans',Roboto,RobotoDraft,Helvetica,Arial,sans-serif; line-height: 16px; font-size: 14px; text-align: center;">
<a href="{{box_link}}" target="_blank"
style="color: #ffffff; font-weight: 400; text-decoration: none; display: inline-block; padding: 10px 24px; background-color: #4184F3; border-radius: 5px; min-width: 90px;">
開啟我的提問箱
</a>
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: rgba(0,0,0,0.54); padding-top: 10px; word-break: break-all;">
{{box_link}}
</div>
</div>
</div>
<div style="text-align: left;">
<div style="font-family: Roboto-Regular,Helvetica,Arial,sans-serif;color: rgba(0,0,0,0.54);font-size: 11px; line-height: 18px; padding-top: 12px; text-align: center;">
<div>
這是一封系統通知郵件,由站點統一發送給全體註冊用戶,請勿直接回覆。<br/>
感謝你一直以來的陪伴與支持。<br/>
© {{year}} {{site_title}}
</div>
</div>
</div>
</td>
<td width="8" style="width: 8px;"></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr height="32" style="height: 32px;">
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
【{{site_title}}】重要通知:本站已更換域名為 {{site}}