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.