neomutt 多邮箱配置笔记(live.cn + QQ邮箱)
涉及两种完全不同的认证机制:
- live.cn:微软已强制 OAuth2(Modern Auth),流程复杂,见第一部分
- QQ邮箱:传统授权码机制,简单很多,见第二部分
之后又给两个账户都加上了 mbsync 本地缓存(秒开体验,避免每次在线拉取),live.cn 这边因为 OAuth2 走了不少弯路,最终用 email-oauth2-proxy 绕过了 mbsync 原生 SASL XOAUTH2 支持的坑,见第五部分。
一、neomutt 配置 live.cn (Outlook/Hotmail) 邮箱
微软邮箱 (outlook.com / live.com / live.cn) 已停用基础认证,IMAP/POP/SMTP 必须走 OAuth2 (微软称之为 “Modern Auth”)。neomutt 本身不处理 OAuth2 对话,需要靠外部脚本
mutt_oauth2.py拿 token。
环境
- Arch Linux + fish shell
- 代理:Clash,
mixed-port: 7890(HTTP/SOCKS5 混用)
依赖安装
sudo pacman -S neomutt isync msmtp gnupg python python-requests jq第一步:下载 OAuth2 脚本
⚠️ 官方脚本路径是 contrib/oauth2/mutt_oauth2.py(旧教程常写错路径,注意别漏了仓库名 neomutt/neomutt)。
mkdir -p ~/.config/neomutt
curl -Lo ~/.config/neomutt/mutt_oauth2.py \
'https://raw.githubusercontent.com/neomutt/neomutt/main/contrib/oauth2/mutt_oauth2.py'
chmod +x ~/.config/neomutt/mutt_oauth2.py第二步:生成 GPG 密钥(用于加密本地 token 文件)
gpg --gen-key
# 标识随便填,例如邮箱本身: z_fun@live.cn,记住设置的密码fish 下设置 GPG_TTY(不能用 bash 的 export 语法):
set -Ux GPG_TTY (tty)第三步:拿 client_id
踩坑记录:live.cn 这类纯个人 Microsoft 账户,Azure Portal 里默认没有可用的 Entra 目录,无法自己创建应用注册(“应用注册”和”应用服务”是两个完全不同的东西,别搞混)。尝试过的路径:
- ❌ Microsoft 365 Developer Program(个人账户申请被拒:“You don’t currently qualify”)
- ❌ 自己在 Azure 建租户(需要信用卡等额外流程)
- ✅ 直接借用 Mozilla Thunderbird 公开的 client_id(README 官方认可的备用方案之一,Thunderbird 是开源软件,公开注册的多租户 client_id,任何人都能用)
client_id = 9e5f94bc-e8a4-4e73-b8be-63364c29d753
无 client_secret(公开客户端,脚本问 Client secret 时直接回车留空)。
第四步:跑授权(device code 流程)
推荐用 devicecode 而不是 authcode,因为 authcode 依赖 http://localhost 重定向,用 Thunderbird 的 client_id 时容易报重定向 URI 不匹配。
~/.config/neomutt/mutt_oauth2.py ~/.config/neomutt/live.tokens \
--verbose \
--authorize \
--provider microsoft \
--encryption-pipe "gpg --encrypt --recipient z_fun@live.cn" \
--client-id 9e5f94bc-e8a4-4e73-b8be-63364c29d753 \
--authflow devicecode终端会打印类似:
To sign in, use a web browser to open the page https://login.microsoft.com/device and enter the code XXXXXXXXX to authenticate.
Polling...
必须尽快(几分钟内)手动打开浏览器访问该网址,输入验证码,登录 live.cn 账号并同意授权,否则会超时(“too much time has elapsed”)。
网络问题
脚本请求 login.microsoftonline.com 时如果报 ssl.SSLEOFError: UNEXPECTED_EOF_WHILE_READING,是网络被墙,需要给当前终端进程(不只是浏览器)挂代理:
set -x https_proxy http://127.0.0.1:7890
set -x http_proxy http://127.0.0.1:7890
set -x all_proxy http://127.0.0.1:7890想永久生效改用 set -Ux(universal,写入所有 fish 会话)。
授权成功后会打印 NOTICE: Obtained new access token... 和一串 access token(一小时后过期,refresh token 由脚本自动续期,几个月后才需要重新授权)。
第五步:测试连接
~/.config/neomutt/mutt_oauth2.py ~/.config/neomutt/live.tokens --provider microsoft --verbose --test第六步:neomutt 配置
~/.config/neomutt/neomuttrc:
set realname = "你的名字"
set imap_user = "z_fun@live.cn"
set from = ${imap_user}
set folder = "imap://outlook.office365.com/"
set spoolfile = "+INBOX"
set imap_authenticators = "oauthbearer:xoauth2"
set imap_oauth_refresh_command = "~/.config/neomutt/mutt_oauth2.py ~/.config/neomutt/live.tokens --provider microsoft"
set smtp_url = "smtp://${imap_user}@smtp.office365.com:587/"
set smtp_authenticators = ${imap_authenticators}
set smtp_oauth_refresh_command = ${imap_oauth_refresh_command}
set ssl_starttls = yes
set ssl_force_tls = yes
set editor = "nvim"
set sort = "reverse-date"
启动
neomutt
# 或指定配置文件路径
neomutt -F ~/.config/neomutt/neomuttrc常用键位(vim 风格):j/k 移动,Enter 打开邮件,m 写新邮件,r 回复,d 标记删除,y 同步到服务器,q 退出。
二、neomutt 配置 QQ 邮箱
QQ 邮箱走传统”授权码”机制,不涉及 OAuth2,配置量小很多。授权码本地存进 Bitwarden,用脚本动态读取,避免明文写死在配置文件里。
第一步:QQ 邮箱开启 IMAP/SMTP,拿授权码
- 网页版 QQ 邮箱 → 「设置」→「账户」
- 找到 POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,开启 IMAP/SMTP服务
- 手机验证后会弹出授权码(16位字符),当场存起来,不会再显示第二次
第二步:把授权码存进 Bitwarden
用自定义字段方式存(不是标准的 login.password),例如:
- 条目名:
qq_mail_app_password - 自定义字段名:
app_password - 字段值:授权码本身
命名建议用英文下划线风格,方便脚本调用时不用处理转义。
第三步:写取值脚本
~/.config/neomutt/get-qq-pass.fish:
#!/usr/bin/env fish
bw get item "qq_mail_app_password" | jq -j '.fields[] | select(.name=="app_password") | .value'⚠️ 用 jq -j(join,不加尾部换行)而不是 jq -r,避免密码字符串末尾混入 \n,虽然 fish 的反引号替换通常会自动去掉尾部换行,但用 -j 更保险。
chmod +x ~/.config/neomutt/get-qq-pass.fish测试脚本输出是否干净:
set -x BW_SESSION (bw unlock --raw)
~/.config/neomutt/get-qq-pass.fish | xxd | tail -5 # 确认末尾没有多余的 0a(换行)或空格第四步:neomutt 配置
~/.config/neomutt/qq-neomuttrc:
set realname = "你的名字"
set imap_user = "你的QQ号@qq.com"
set from = ${imap_user}
set imap_pass = "`~/.config/neomutt/get-qq-pass.fish`"
set smtp_pass = ${imap_pass}
set folder = "imaps://imap.qq.com:993/"
set spoolfile = "+INBOX"
set smtp_url = "smtp://${imap_user}@smtp.qq.com:465/"
set ssl_starttls = no
set ssl_force_tls = yes
set editor = "nvim"
set sort = "reverse-date"
QQ 邮箱 SMTP 用 465 端口(SSL 直连),不是 STARTTLS,所以 ssl_starttls = no。
启动
set -x BW_SESSION (bw unlock --raw)
neomutt -F ~/.config/neomutt/qq-neomuttrc踩坑记录
登录报错:
NO Login fail. Account is abnormal, service is not open, password is incorrect, login frequency limited, or system is busy.
这是 QQ 邮箱的万能错误码,把好几种可能原因打包在一起,遇到时按顺序排查:
- 授权码是不是旧的/过期的 —— 最常见的原因。如果 Bitwarden 里存的授权码是很久之前配别的客户端时生成的,很可能已经失效。回 QQ 邮箱设置页重新生成一个新的,覆盖更新到 Bitwarden。
- 手机端有没有”异常登录”确认通知 —— QQ 邮箱第一次被新客户端连接时,经常会给手机 QQ / QQ邮箱App 推一条确认通知,不点确认所有登录会被拒绝。去手机上翻一下通知栏。
- IMAP/SMTP 开关是否真的是开启状态 —— 网页设置页确认。
- 连续失败触发限流 —— 排查过程中如果连续认证失败好几次,服务器可能临时限制一小段时间(日志表现为
tls_socket_read异常终止/连接被服务器主动断开),等 10-15 分钟再试。
调试方法(一闪而过的错误抓不住时):
neomutt -F ~/.config/neomutt/qq-neomuttrc -d 5 -l ~/.config/neomutt/debug.log
tail -50 ~/.config/neomutt/debug.log三、Bitwarden CLI 基本用法备忘(首次在 Linux 用)
bw login # 登录,问邮箱+主密码(+2FA)
set -x BW_SESSION (bw unlock --raw) # 解锁,塞进环境变量,每个新终端都要做一次
bw list items --search "关键词" # 搜索条目
bw get item "条目名" | jq # 查看条目完整结构,确认字段名
bw get item "条目名" | jq -r '.fields[] | select(.name=="字段名") | .value' # 精确取自定义字段安全注意:
- 授权码类信息调试时会打印在终端,测完记得
clear清屏(不影响 Bitwarden 里存的东西) - session key(
BW_SESSION的值)本身等同于密码库的钥匙,不要粘贴分享出去
四、本地缓存加速(mbsync)+ live.cn 的 OAuth2 死胡同与破解
两个邮箱直连 IMAP 每次打开都要重新拉取,速度慢。用 isync (mbsync) 把邮件同步到本地 Maildir,neomutt 只读本地文件,秒开,后台定时增量同步。
4.1 QQ邮箱(顺利,直接用 mbsync 原生认证)
sudo pacman -S isync
mkdir -p ~/Maildir/qq~/.mbsyncrc 里 QQ 部分:
IMAPAccount qq
Host imap.qq.com
User 你的QQ号@qq.com
PassCmd "~/.config/neomutt/get-qq-pass.fish"
AuthMechs LOGIN
SSLType IMAPS
IMAPStore qq-remote
Account qq
MaildirStore qq-local
Path ~/Maildir/qq/
Inbox ~/Maildir/qq/INBOX
SubFolders Verbatim
Channel qq
Far :qq-remote:
Near :qq-local:
Patterns *
Create Both
Expunge Both
SyncState *
set -x BW_SESSION (bw unlock --raw)
mbsync qqqq-neomuttrc 里 folder 改成指向本地:
set folder = "~/Maildir/qq"
set spoolfile = "+INBOX"
4.2 live.cn(大坑:mbsync 原生 XOAUTH2 支持在 Linux 上普遍不可靠)
死胡同:SASL XOAUTH2 插件路线,放弃
mbsync 默认走 Cyrus SASL 库协商认证机制,遇到 XOAUTH2 需要额外装 SASL 插件:
IMAP error: selected SASL mechanism(s) not available;
selected: XOAUTH2
available: ... PLAIN LOGIN ...
Arch 官方仓库没有现成包,AUR 上有两个实现:
cyrus-sasl-xoauth2-git(moriyoshi 维护)—— 社区反馈配合 mbsync 会卡死在登录sasl-xoauth2-git(tarickb 维护)—— 更常被推荐,装上后(依赖python-msal,编译还需要cmake、pandoc-cli)遇到新报错:
Error performing SASL authentication step: SASL(-1): generic failure: Unable to find a callback: 32775
这是 mbsync 与各种 SASL XOAUTH2 插件之间广为人知、长期未解决的兼容性问题,Arch论坛、GitHub issue、邮件列表里大量类似报错,没有统一稳定的修法,跟 isync 版本、插件版本、操作系统组合都有关系。结论:不要在这条路上继续投入时间,直接换方案。
(事后把装的这些依赖都清理掉了:sudo pacman -Rs sasl-xoauth2-git sasl-xoauth2-git-debug python-msal pandoc-cli,cmake 因为以后还有用留着了)
破解方案:email-oauth2-proxy(本地 OAuth2 转译代理)
原理:本地起一个”假装普通”的 IMAP/SMTP 服务,mbsync 用普通账号密码连接这个本地代理,代理在背后帮你跟微软做真正的 OAuth2 认证。mbsync 完全不需要碰 SASL XOAUTH2。
sudo pacman -S python-pip
pip install --user --break-system-packages requests pystray pillow pyasyncore prompt_toolkit
mkdir -p ~/.config/email-oauth2-proxy
cd ~/.config/email-oauth2-proxy
curl -Lo emailproxy.py 'https://raw.githubusercontent.com/simonrob/email-oauth2-proxy/main/emailproxy.py'
curl -Lo emailproxy.config 'https://raw.githubusercontent.com/simonrob/email-oauth2-proxy/main/emailproxy.config'⚠️ 踩坑:Python 3.12+ 移除了标准库 asyncore 模块,这个项目还依赖它,装 pyasyncore(官方提供的兼容包)补回来才能跑。
配置文件结构(emailproxy.config),容易搞错的地方
这个工具是两层结构,不是简单的”一个账户一段”:
[Server setup]段:本地端口 → 远程服务器映射,live.cn / outlook.com 用模板自带的[IMAP-1993]、[SMTP-1587]就够了,不用改[Account setup]段:用邮箱地址本身当段名,写 OAuth2 参数,这才是需要新增的部分
正确的账户配置:
[z_fun@live.cn]
permission_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
token_url = https://login.microsoftonline.com/consumers/oauth2/v2.0/token
oauth2_scope = https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access
redirect_uri = http://localhost:8080
client_id = 9e5f94bc-e8a4-4e73-b8be-63364c29d753(继续复用之前的 Thunderbird 公开 client_id,不需要 client_secret 这一行;个人账户用 consumers 端点而不是 common)
Gmail 那几个示例段([IMAP-2993] 等)和占位符账户段([your.xxx@example.com])用不到,可以删掉。
首次授权:用 --external-auth 模式,手动复制粘贴链接
不要只用 --no-gui——这会尝试自动弹浏览器,在纯终端/无桌面环境下会静默卡在 “Authorisation request received…” 不动。
正确启动方式:
python ~/.config/email-oauth2-proxy/emailproxy.py --external-auth --no-gui --config-file ~/.config/email-oauth2-proxy/emailproxy.config--external-auth 需要额外依赖:
pip install --user --break-system-packages prompt_toolkit保持这个终端窗口开着,另开一个新终端跑 mbsync live 触发登录请求。回到第一个窗口,会打印出一条完整授权 URL,操作步骤:
- 复制这条 URL 到浏览器打开
- 登录 live.cn 账号,同意授权
- 浏览器会跳转到
http://localhost:8080/?code=...,显示”连接失败”是正常的(本地没有真的监听 8080 接收回调),只需要地址栏里这条完整 URL - 复制这条跳转后的 URL,粘贴回终端(提示”then paste here the full post-authentication URL…”的地方),回车
⚠️ 踩坑:mbsync 默认连接超时时间太短,等不到手动完成浏览器授权就会报 Socket error: timeout。给 live 账户加长超时:
Timeout 300
⚠️ 授权码 (code=...) 是一次性、短时效凭证,复制到粘贴这一步要尽快操作,拖久了会过期需要重新走一遍。
.mbsyncrc 里 live.cn 部分改成指向本地代理
IMAPAccount live
Host 127.0.0.1
Port 1993
User z_fun@live.cn
Pass "dummy123"
AuthMechs LOGIN
TLSType None
Timeout 300
IMAPStore live-remote
Account live
MaildirStore live-local
Path ~/Maildir/live/
Inbox ~/Maildir/live/INBOX
SubFolders Verbatim
Channel live
Far :live-remote:
Near :live-local:
Patterns *
Create Both
Expunge Both
SyncState *
关键点:
Host指向127.0.0.1,Port是代理本地监听端口(不是微软的 993 了)Pass随便填一个字符串就行(这个”密码”只是本地用来加密缓存 token 的,不是真实密码)AuthMechs LOGIN(普通登录,OAuth2 那层已经被代理接管)TLSType None(本地回环连接不需要加密;mbsync 会警告”密码明文发送”,这是预期的,忽略即可)
首次授权成功后,代理会自动缓存 token 到配置文件里,之后不用重新走授权
把代理设成 systemd 用户服务常驻后台:
~/.config/systemd/user/emailproxy.service:
[Unit]
Description=Email OAuth2 Proxy for live.cn
[Service]
Type=simple
ExecStart=/usr/bin/python /home/viso/.config/email-oauth2-proxy/emailproxy.py --no-gui --config-file /home/viso/.config/email-oauth2-proxy/emailproxy.config
Restart=on-failure
[Install]
WantedBy=default.target⚠️ 这里不要再带 --external-auth,那个只是首次交互授权用的,token 缓存进配置文件后普通 --no-gui 模式就够用了,会自动用 refresh token 续期。
systemctl --user daemon-reload
systemctl --user enable --now emailproxy.service
systemctl --user status emailproxy.service # 确认 active (running)neomuttrc(live.cn)改成指向本地 Maildir + 本地代理 SMTP:
set imap_user = "z_fun@live.cn"
set folder = "~/Maildir/live"
set spoolfile = "+INBOX"
set imap_pass = "dummy123"
set smtp_url = "smtp://z_fun@live.cn@127.0.0.1:1587/"
set smtp_pass = "dummy123"
set ssl_starttls = no
set ssl_force_tls = no
4.3 两个账户一起定时后台同步
~/.config/systemd/user/mbsync.service:
[Unit]
Description=mbsync mail sync
[Service]
Type=oneshot
ExecStart=/usr/bin/mbsync -a~/.config/systemd/user/mbsync.timer:
[Unit]
Description=Run mbsync every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.targetsystemctl --user daemon-reload
systemctl --user enable --now mbsync.timer-a 会同步 .mbsyncrc 里定义的所有账户(live + qq)。
五、换机器迁移
需要搬的东西
live.cn 相关(在线连接方式,OAuth2 脚本,目前 neomuttrc 已改用本地代理,这几个文件留作备用/理解原理用):
~/.config/neomutt/mutt_oauth2.py~/.config/neomutt/live.tokens(已被 GPG 加密,可以放公开仓库)- GPG 私钥(唯一敏感项,
live.tokens靠它解密,必须单独妥善搬运,不进 git)
live.cn 相关(本地缓存 + 代理方式,目前实际使用的架构):
4. ~/.config/email-oauth2-proxy/emailproxy.py
5. ~/.config/email-oauth2-proxy/emailproxy.config(⚠️ 含已授权的 OAuth2 token 缓存,敏感文件,不要放公开仓库,处理方式见下)
6. ~/.config/systemd/user/emailproxy.service
QQ 邮箱相关:
7. ~/.config/neomutt/get-qq-pass.fish
8. Bitwarden 里的 qq_mail_app_password 条目本身不用搬——新机器上 bw login 后自动能访问同一个云端密码库
两账户共用:
9. ~/.config/neomutt/neomuttrc(live.cn,指向本地 Maildir + 代理端口)
10. ~/.config/neomutt/qq-neomuttrc
11. ~/.mbsyncrc
12. ~/.config/systemd/user/mbsync.service、~/.config/systemd/user/mbsync.timer
chezmoi 管理方案
可以直接 chezmoi add(不含敏感信息):
neomuttrc, qq-neomuttrc, mutt_oauth2.py, get-qq-pass.fish, live.tokens,
emailproxy.py, emailproxy.service, mbsyncrc, mbsync.service, mbsync.timer
.mbsyncrc 里 QQ 账户用 PassCmd 动态取密码、live.cn 账户用固定占位密码 dummy123,都不是真实凭证,可以放公开仓库。
不要放 chezmoi/git:
- GPG 私钥(live.tokens 解密用)
emailproxy.config(因为里面缓存了实际的 OAuth2 access/refresh token,这个文件在首次授权成功后会被程序自动写回,含真实凭证。走 Bitwarden 单独存取,或者干脆新机器上重新走一次--external-auth首次授权流程,不搬这个文件也可以,反正只是重新登录一次的成本)
Bitwarden CLI 基本用法(Linux 首次使用备忘)
bw login # 登录,问邮箱+主密码(+2FA)
set -x BW_SESSION (bw unlock --raw) # 解锁并把 session 塞进环境变量,每个新终端都要做
bw list items --search "test" # 验证解锁成功(能返回结果,哪怕是空数组)存私钥(旧机器)
gpg --export-secret-keys --armor z_fun@live.cn > mutt-gpg-key.asc
# 把内容粘贴进 Bitwarden 某个条目的自定义字段(比如字段名 "key")
shred -u mutt-gpg-key.asc # 本地明文用完即删取私钥(新机器)
bw login
set -x BW_SESSION (bw unlock --raw)
# 查看条目结构,确认字段名
bw get item "条目名" | jq
# 精确取出自定义字段 "key" 的值并导入
bw get item "条目名" | jq -r '.fields[] | select(.name=="key") | .value' > mutt-gpg-key.asc
gpg --import mutt-gpg-key.asc
shred -u mutt-gpg-key.asc
# 给密钥设置信任等级
gpg --edit-key z_fun@live.cn
# 输入 trust -> 选 5 (ultimate) -> save新机器完整流程
# 1. 装依赖
sudo pacman -S neomutt gnupg python python-requests python-pip isync jq
pip install --user --break-system-packages requests pystray pillow pyasyncore prompt_toolkit
# 2. 导入 GPG 私钥(如果打算保留在线直连备用方案)
# 3. chezmoi 拉配置
chezmoi init <dotfiles仓库地址>
chezmoi apply
# 4. 配代理(live.cn 认证需要,QQ邮箱国内直连不需要代理)
set -Ux https_proxy http://127.0.0.1:7890
set -Ux http_proxy http://127.0.0.1:7890
# 5. Bitwarden 登录解锁(QQ邮箱需要)
bw login
set -x BW_SESSION (bw unlock --raw)
# 6. email-oauth2-proxy 首次授权(如果没有搬 emailproxy.config,这里要重新走一遍)
python ~/.config/email-oauth2-proxy/emailproxy.py --external-auth --no-gui \
--config-file ~/.config/email-oauth2-proxy/emailproxy.config
# 另开终端跑 mbsync live 触发登录,跟着提示走完浏览器授权+粘贴URL那一套
# 7. 启动常驻服务
systemctl --user daemon-reload
systemctl --user enable --now emailproxy.service
systemctl --user enable --now mbsync.timer
# 8. 首次全量同步(走后台 timer 也行,手动跑一次更快看到结果)
mbsync -a
# 9. 测试
~/.config/neomutt/get-qq-pass.fish | wc -c # 确认QQ密码能读到
# 10. 启动
neomutt -F ~/.config/neomutt/neomuttrc # live.cn
neomutt -F ~/.config/neomutt/qq-neomuttrc # QQ邮箱QQ邮箱这边全程不需要重新生成授权码,Bitwarden 云端同步是关键。live.cn 如果搬了 emailproxy.config(含 token 缓存)就也不用重新授权;如果没搬(因为它含敏感 token,建议不搬),第 6 步走一次新授权即可,之后同样自动续期,不用每次都重新登录。