sublime插件发邮件卡住主线程是因ui线程单线程限制,须用subprocess.popen异步调外部脚本,禁用run/wait;模板用string.template防嵌套污染;状态存唯一id命名的json文件;smtp需显式check starttls并捕获特定异常。

为什么直接用 subprocess 调 Python 脚本发邮件会卡住主线程
Sublime Text 本身不支持原生多线程或异步 I/O,所有插件运行在主线程(UI 线程)。一旦你在插件里用 smtplib.sendmail() 同步发信,界面立刻冻结,尤其群发时更明显——这不是代码写错了,是模型限制。
实操建议:
- 必须把发信逻辑剥离到独立进程,推荐用
subprocess.Popen启动外部 Python 脚本,传入 JSON 参数文件路径(避免命令行长度限制和特殊字符转义问题) - 不要用
subprocess.run()或.wait(),否则又阻塞;改用.poll()配合定时器轮询状态 - 发信脚本自身要加超时控制:
SMTP(timeout=15),否则 DNS 卡死或服务器无响应会让子进程挂起
Template.replace() 无法处理嵌套变量?用 string.Template 更稳
很多人用 str.replace() 手动替换 ${name}、${email},结果遇到 ${user.name} 或重复字段(如 ${count} 和 ${count_total})就乱套——顺序替换导致二次污染。
实操建议:
- 改用 Python 标准库
string.Template,它只认完整占位符,且支持安全的safe_substitute() - 模板文件统一用
$name语法(不加花括号),比${name}更少歧义,也兼容更多邮件模板引擎 - 变量字典必须预处理:把原始数据中的空值/None 显式转成空字符串,否则
safe_substitute()会留着未替换的$xxx
from string import Template
with open("template.txt") as f:
t = Template(f.read())
result = t.safe_substitute({
"name": "张三",
"email": "zhang@example.com" or ""
})
发送状态怎么存?别碰 Sublime 的 view.settings() 做持久化
view.settings() 是内存级的,关掉文件或重启 Sublime 就丢;而用本地 SQLite 或 JSON 文件存日志,又得自己加锁防并发写入冲突——群发时多个子进程同时写一个文件极易损坏。
实操建议:
- 每个发信任务生成唯一 ID(如
uuid.uuid4().hex[:8]),状态文件名带该 ID,例如status_abc123de.json - 状态只写不读:子进程结束前追加一行 JSON(含时间戳、收件人、状态码、错误信息),主进程用
tail -f类思路按行读取 - Sublime 插件侧用
sublime.set_timeout_async()定期扫描status_*.json文件,解析最新行更新面板状态栏
SMTP 登录失败却显示“发送成功”?检查 ehlo() 后的 has_extn("starttls")
很多企业邮箱(如腾讯企业邮、阿里云邮箱)强制 STARTTLS,但默认 smtplib.SMTP() 不自动升级加密。脚本看似连上并调了 sendmail(),实际底层被服务器静默断开,错误只在 ehlo() 返回码里藏了一眼。
实操建议:
- 发信脚本开头必须显式检查 TLS 支持:
if server.has_extn("starttls"):,不支持就直接报错退出 - 支持则立即调
server.starttls(),再login();注意starttls()后要重新ehlo() - 捕获
smtplib.SMTPAuthenticationError和smtplib.SMTPRecipientsRefused两类异常,分别提示“账号密码错”和“收件人格式非法”,别全吞成“未知错误”











