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,拿授权码

  1. 网页版 QQ 邮箱 → 「设置」→「账户」
  2. 找到 POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,开启 IMAP/SMTP服务
  3. 手机验证后会弹出授权码(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 邮箱的万能错误码,把好几种可能原因打包在一起,遇到时按顺序排查:

  1. 授权码是不是旧的/过期的 —— 最常见的原因。如果 Bitwarden 里存的授权码是很久之前配别的客户端时生成的,很可能已经失效。回 QQ 邮箱设置页重新生成一个新的,覆盖更新到 Bitwarden。
  2. 手机端有没有”异常登录”确认通知 —— QQ 邮箱第一次被新客户端连接时,经常会给手机 QQ / QQ邮箱App 推一条确认通知,不点确认所有登录会被拒绝。去手机上翻一下通知栏。
  3. IMAP/SMTP 开关是否真的是开启状态 —— 网页设置页确认。
  4. 连续失败触发限流 —— 排查过程中如果连续认证失败好几次,服务器可能临时限制一小段时间(日志表现为 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 qq

qq-neomuttrcfolder 改成指向本地:

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,编译还需要 cmakepandoc-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-clicmake 因为以后还有用留着了)

破解方案: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,操作步骤:

  1. 复制这条 URL 到浏览器打开
  2. 登录 live.cn 账号,同意授权
  3. 浏览器会跳转到 http://localhost:8080/?code=...显示”连接失败”是正常的(本地没有真的监听 8080 接收回调),只需要地址栏里这条完整 URL
  4. 复制这条跳转后的 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.1Port 是代理本地监听端口(不是微软的 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.target
systemctl --user daemon-reload
systemctl --user enable --now mbsync.timer

-a 会同步 .mbsyncrc 里定义的所有账户(live + qq)。


五、换机器迁移

需要搬的东西

live.cn 相关(在线连接方式,OAuth2 脚本,目前 neomuttrc 已改用本地代理,这几个文件留作备用/理解原理用):

  1. ~/.config/neomutt/mutt_oauth2.py
  2. ~/.config/neomutt/live.tokens(已被 GPG 加密,可以放公开仓库)
  3. 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 步走一次新授权即可,之后同样自动续期,不用每次都重新登录。