Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc33604d1 | ||
|
|
5823b5fbb3 | ||
|
|
f83ea3e29a | ||
|
|
1cae63b671 | ||
|
|
5aad4a97e4 | ||
|
|
909e853500 | ||
|
|
bc971deb37 | ||
|
|
912facaf2c |
+10
-172
@@ -1,176 +1,14 @@
|
|||||||
# ---> Python
|
# Python
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
# WorkBuddy 项目数据(会话记忆等)
|
||||||
*.so
|
.workbuddy/
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
# 运行时生成的状态/报告文件(含真实用户邮箱,不入库)
|
||||||
|
broadcast_state.json
|
||||||
|
broadcast_report.json
|
||||||
|
broadcast_invalid.json
|
||||||
|
broadcast_bounce_report.json
|
||||||
|
broadcast_bounces_seen.json
|
||||||
|
*.tmp
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -1,2 +1,269 @@
|
|||||||
# 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
|
||||||
|
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
|
||||||
|
|
||||||
|
安全边界: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 断线自动重连:服务器会掐掉空闲连接,--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 命中别的同名空表)
|
||||||
|
- 语言列自动试跑探测(language → lang → NULL 兜底),老库没有该列也能发
|
||||||
|
|||||||
+2493
File diff suppressed because it is too large
Load Diff
+61
@@ -0,0 +1,61 @@
|
|||||||
|
; 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
|
||||||
|
; 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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
【{{site_title}}】Important: we've moved to {{site}}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
【{{site_title}}】重要:サイトは {{site}} に移転しました
|
||||||
@@ -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);">需要分段就复制这些 <p> 段落;需要强调框可以复制下面这个浅灰框。</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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
【{{site_title}}】站内通知
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
【{{site_title}}】重要通知:本站已更换域名为 {{site}}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
【{{site_title}}】重要通知:本站已更換域名為 {{site}}
|
||||||
Reference in New Issue
Block a user