Use msmtp for relaying mails to Gmail with OAUTH2

I got tired that my Android phone keep prompting me that my account is insecure since I still use app password, because my local servers need that to send alert emails to myself via Gmail, and OpenSMTPD doesn’t like OAUTH.

msmtp is a promising candidate, since it supports AUTH OAUTHBEARER, and password can be supplied by a script. However, by default it’s not running a daemon like OpenSMTPD does, thus not listening to requests on my local network, and I don’t plan to install msmtp on every host - I still would like others simply use dma to point to this server and send mails.

For Debian, it has a package of msmtp-mta, which provides a daemon of msmtpd that can listen on port 25, and that’s pretty much all I need.

For OAUTH token part, it’s fairly standard procedure - go to Google Cloud Console https://console.developers.google.com/, create a project, and activate Gmail API. Note that, since 2023 Google no longer allows OOB flow for OAuth, and the only way AFAIK allow to use loopback ip address auth flow is to create a desktop app when creating the credential, thus the type has to be that when the OAuth credential is created. Also, add your own gmail address to test users list.

A script is required by passwordeval option of msmtp, and getmail-gmail-xoauth-tokens is a decent one to start - I did modified it slightly to make it flow more naturally with Gmail, namely, automatically decide whether currently it should be refresh the token, or acquire one if access token is not present. Also, when requesting for access token, use PKCE as Google suggested.

To ensure that only msmtpd can do the token acquire and refresh, I created a /etc/msmtp and put both the script and token file into the folder, chown to msmtp and chmod to 700/600 respectively so that only the daemon itself can read/write. A sample /etc/msmtprc is like:

account default

host smtp.gmail.com

port 587
protocol smtp

auth oauthbearer
user your@gmail.com

tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt

passwordeval /etc/msmtp/oauth.py /etc/msmtp/your-token-file

auto_from on
aliases /etc/aliases

I am still not quite clear about AppArmor rules on how to include external script called by msmtp yet, so for now I simply disabled rule for msmtp, by creating a /etc/apparmor.d/disable and symlink /etc/apparmor.d/usr.bin.msmtp there, then restart apparmor.

Starting msmtpd, and echo "hello world" | mailx -s "test mail" to@gmail.com from other hosts that have dma pointing to this server should work. Did I say other hosts? This setup works for any hosts than the one running msmtp itself (because requests are handled by msmtpd), but on the localhost, mailx will attempt to use msmtp directly, without talking to the daemon. For root that’s ok, but for any other accounts, one will fail since it can’t read /etc/msmtprc or executing the script that gets the token, nor it has a local ~/.msmtprc to run, and I don’t want to create ~/.msmtprc for every possible account.

dma should be a good candidate, but it conflicts with msmtp-mta. msmtp-mta actually serves 2 purposes: provides a msmtpd and a symlink for /usr/bin/sendmail to /usr/bin/msmtp. We do need the msmtpd daemon, but we don’t really need the MTA side of thing. So we can backup msmtpd, purge msmtp-mta, install dma and have it pointing to the listening IP of msmtpd. In this way, all local mails will still be routed via msmtpd, just like any mails from other hosts, and a single /etc/msmtprc plus msmtpd handle everything, and only the daemon can read/write the token file.

TODOs:

Will need to figure out an AppArmor profile to limit the scope of msmtp scripts for passwordeval, AND

Check if Google requires the token of app in testing mode to be manually refreshed every 7 days (that is, requiring to click thorugh the OAuth authorization pages again). I do have mails sending at least weekly, so hopefully access tokens can be refreshed by the script itself, without the need of getting a new one manually.

If the app is in testing mode, the recovery token will expire in 7 days, and reqiures user to go through OAuth consent page again. Switching to production mode and re-request the token once more will resolve the issue.

标签:

更新时间: