我的 WSL 日常使用 msmtp 作为 MTA,它通过 starttls 连接到 Outlook 邮箱的 SMTP 服务器(smtp-mail.outlook.com
)。此前 Outlook 的安全策略强制要求多因素认证(MFA),但允许使用应用密码,因此我一直在使用传统的 GPG 加密应用密码的方法。但今天我尝试发送邮件时,得到了以下的错误:
535 5.7.139 Authentication unsuccessful, basic authentication is disabled. [SI2P153CA0032.APCP153.PROD.OUTLOOK.COM 2024-09-25T08:14:40.271Z 08DCDD1F9EAD5AFB]
在 TUNA 群中为此询问群友,获得了微软的公告链接。简而言之,从 2024/9/16 开始,Outlook 的服务器不再支持传统的 SMTP AUTH 方式进行认证,而必须使用 OAuth2。
对于邮件客户端来说,OAuth2 有两种常用的认证方式,bearer token 或者 SASL XOAUTH2;Outlook 使用后者,Gmail 是前者。msmtp 两种都支持,只要对应修改 auth
选项即可。然而,msmtp 和其他大多数命令行 MTA 一样,只能使用而不能管理 OAuth2 token(也就是说,用户需要负责获取和定期刷新)。因此,需要借助第三方工具来管理 OAuth 的状态。
Arch Wiki 的 msmtp 页面推荐使用的工具是 oama,这是一个 Haskell 写的小工具,可以用在 msmtp 的 password-eval
中。然而,使用起来遇到了一些棘手的问题。
具体来说,oama 需要自己配置 OAuth 使用的 client_id 和 client_secret。网上能公开获取到的几个 client_id 都需要配置对应的回调 URL(尽管其实最终客户端可以不真的通过回调获取 code,直接在最后的 302 请求中截获即可;但如果不按配置填写,根本无法进入授权流程),如 ThunderBird 的要求 http://localhost:8080
或者 https://localhost
。但 oama 的配置只支持形如 http://127.0.0.1[:port]
的格式,其他格式要么报无法解析的错误,要么无法启动 Web Server listen。我完全不会 Haskell,这个修不来。后续又找到了别的 client_id,但发现无法使用个人的 Outlook 账号登录,只能用组织账号(Microsoft 365),这显然也不是我想要的。
于是我只能选择在 Office 365 中创建了一个 App,配上所需的几个权限(用 OAuth 术语来说叫 scope),但发现微软接受的本地回调只能以 http://localhost
开头,看来 oama 是没法用了。又经过一番搜索,我找到了 mutt 带的 mutt_oauth2.py
。把这个文件复制出来,修改其中的 client 相关配置,然后执行:
user@~:$ python3 ~/.local/bin/mutt_oauth2.py -t ~/.local/var/email/outlook.gpg --authorize
OAuth2 registration: microsoft
Preferred OAuth2 flow ("authcode" or "localhostauthcode" or "devicecode"): localhostauthcode
Account e-mail address: [email protected]
后续在给出的链接完成认证即可。如果脚本报告 POP3 鉴权失败,很可能是 Outlook 的设置中没有打开 POP3 服务。最后,修改 msmtp 的配置文件:
account outlook
host smtp-mail.outlook.com
from [email protected]
user [email protected]
tls_starttls on
auth xoauth2
passwordeval python3 ~/.local/bin/mutt_oauth2.py ~/.local/var/outlook.gpg
在每次发送邮件时,mutt_oauth2.py
会检查 token 的可用性,并在需要时刷新。如果 refresh token 也过期了,则会自动重新发起授权。这样,我又可以愉快地使用 mail
和 git-sendmail
等工具了。
就在我以为完事大吉的时候,还是遇到了新的问题(但和 Outlook 没有关系):如果同时调用 gpg 和 msmtp(如 foo | gpg --clear-sign | msmtp [email protected]
),则只有第一个 gpg 可以正确调用 pinentry
来使用智能卡进行签名,而 msmtp 的 passwordeval
脚本调用的 gpg 无法正常触发 pinentry
,会卡在 gpg --decrypt
中。尝试使用 shell substitution 也遇到了同样的问题(msmtp [email protected] <(foo | gpg --clear-sign)
)。似乎是因为我使用了 gpg-agent
,而上述命令中都有两个 gpg 进程同时启动,就会有一个无法连接到 agent。目前我只能通过每次只执行一个 gpg 来绕过问题。
附录
为方便起见,公开我的 OAuth Application 配置(由于只在本地使用,公开 secret 没有太大的安全风险):
- 名称:
msmtp OAuth
- Client ID:
1ba11cc8-c6d1-4ae6-bd88-6becf878f8df
- Client Secret:
lBm8Q~_IfyNpFUZ6KydTc4QHjLl1IwcCxFhxqa7n
(过期日:2026/9/24) - Redirect URI:
http://localhost
(经测试,可以增加任意端口号) - Scope:
IMAP.AccessAsUser.All
,POP.AccessAsUser.All
,SMTP.Send
,User.Read
,此外offline_access
会由客户端额外请求
说明:上述配置仅供测试,我不以任何形式保证其可用性或对此负任何责任。如选择使用,您需要承担任何可能的后果。
在 Microsoft Entra 平台上配置 OAuth App 时,我还遇到了一个坑点:如果授权时指定的 tenate
是 common
,那么 signInAudience
必须设置成 AzureADandPersonalMicrosoftAccount
或者 AzureADMyOrg
(此选项会导致被标记成 Consumer
的 Outlook 用户无法登录,只有组织用户可以登录)。我原来设定的是 PersonalMicrosoftAccount
,但会引发授权的 invalid_request
错误。