This article is English version of
Overview and References
After using emacs for a long time, I began to want to write emails in emacs as well. I used emacs + notmuch a long time ago, after that I switched to Gnus, which is built into emacs. Then, when Gmail stopped accepting standard password authentication and required OAuth2 authentication, I made significant changes to my Gnus configuration. I'd like to explain the setup.
I used the following article as a reference:
The following is my environment:
- Linux 6.18.7 (Arch Linux)
- emacs 30.2
- gnupg 2.4.9
gnupg is required for the emacs oauth2 package.
Preparation
Install the emacs package and elisp file.
| Package Name or File Name | Summary |
|---|---|
| oauth2 | Basic library for the oauth2 authorization protocol |
| google-contacts | Dependency of gnus-gmail-oauth.el below. Library for accessing Google via oauth2. |
| gnus-gmail-oauth.el | Library for Gnus' authenticating with oauth2 and collecting emails in Gmail. |
Put gnus-gmail-oauth.el in your path.
As a brief explanation, the oauth2 package performs the oauth2 authorization process using emacs as a client application, and securely stores the obtained access token, refresh token, etc. locally. For storing, it uses the emacs built-in plstore, which requires the emacs built-in EasyPG and gnupg as dependencies. Using EasyPG or plstore will help you understand what's going on internally. gnus-gmail-oauth.el is an elisp that uses the oauth2 package to perform OAuth2 authentication when getting emails with imap in Gnus.
Configuring OAuth2 authentication in my Google account
To use Gmail from Gnus, the Gmail API needs to be accessed via OAuth2, so configure OAuth2 settings in your Google account.
- Access the Google Cloud Console with your Google account and create a project.
- Enter the app name and other authentication information required to access Gmail. I chose external as the target user, since I'm using this as a test user.
- Create an OAuth2.0 client. I selected the desktop app. Keep the client ID and client secret, as they will be used when accessing from Emacs.
- Add a Gmail account to the test user. Since I'm using it myself, I added my own account.
- Enable the Gmail API
I'm not a Google Workspace user, and Google's app verification is too demanding, so I'll use this in a test environment. However, the refresh token has a 7-day expiration.
Getting emails with IMAP and seeing them with gnus
Add the following settings to perform OAuth2 authentication for IMAP login and getting emails. Replace the ID and secret with what you recorded earlier.
(setq gnus-select-method
'(nnimap "imap.gmail.com"))
;; imap
(setq gnus-gmail-oauth-client-id "your_client_id"
gnus-gmail-oauth-client-secret "your_client_secret")
(require 'gnus-gmail-oauth)
(advice-add 'nnimap-login :before-until #'gnus-gmail-oauth2-imap-authenticator)
It might be a good idea to configure other Gnus settings referencing the Gnus info page.
When you log in to IMAP using M-x gnus or something other, a Google consent screen appear. Following the process, it gives you an authorization code. Enter the code into Emacs to obtain an access token, and enter your passphrase to save the tokens in plstore. You should then be able to log in to IMAP.
Sending emails with SMTP
gnus-gmail-oauth.el does not include settings for sending emails with SMTP, so add the code for it to .gnus.el. Change the values of user-mail-address and user-full-name.
;; smtp
(setq user-mail-address "your_email@gmail.com"
user-full-name "your name"
send-mail-function 'smtpmail-send-it
smtpmail-smtp-server "smtp.gmail.com"
smtpmail-stream-type 'starttls
smtpmail-smtp-service 587)
(require 'smtpmail)
(cl-defmethod smtpmail-try-auth-method
(process (_mech (eql xoauth2)) user _password)
(let ((token (google-oauth-auth-and-store
gnus-gmail-resource-url
gnus-gmail-oauth-client-id
gnus-gmail-oauth-client-secret))
access-token
ret)
(setq access-token (oauth2-token-access-token token))
(setq ret (smtpmail-command-or-throw
process
(concat "AUTH XOAUTH2 "
(base64-encode-string
(format "user=%s\001auth=Bearer %s\001\001"
(nnimap-quote-specials user)
(nnimap-quote-specials access-token)) t))
))
(when (not (eq (car ret) 235))
(smtpmail-send-command process "NOOP")
(smtpmail-read-response process)
(setq token (gnus-gmail-oauth-token))
(setq access-token (oauth2-token-access-token token))
(smtpmail-command-or-throw
process
(concat "AUTH XOAUTH2 "
(base64-encode-string
(format "user=%s\001auth=Bearer %s\001\001"
(nnimap-quote-specials user)
(nnimap-quote-specials access-token)) t))
235))))
(add-to-list 'smtpmail-auth-supported 'xoauth2)
You also need to add your username and password to .authinfo. This password will not be used in actual OAuth2 authentication, so just enter something here.
machine smtp.gmail.com login your_email@gmail.com port 587 password notrecord
You should now be able to send emails.
Multi-account configuration
I have multiple Gmail accounts, so I want to be able to see emails from those accounts in Gnus. To do this, when accessing Google, I need to change the client ID and client secret by each user.
Saving the client ID and secret, and loading them by the user as a key
gnus-gmail-oauth2-imap-authenticator needs modification, so rename the file:
gnus-gmail-oauth.el → my-gnus-gmail-oauth.el
imap settings
(defvar gnus-gmail-id-and-secret-file (expand-file-name "~/.emacs.d/oauth2.plstore")
"File in which the client id and secret are stored")
;; for id and secret update
(defun gnus-gmail-secure-id-and-secret (user id secret)
"Read the client id and secret, and save them after encryption."
(interactive
(list (read-string "Input user: ")
(read-passwd "Input client id: ")
(read-passwd "Input client secret: ")))
(let ((store (plstore-open gnus-gmail-id-and-secret-file)))
(plstore-put store user nil (list :id id :secret secret))
(plstore-save store)
(plstore-close store)))
;; load id and secret
(defun gnus-gmail-load-id-and-secret (user)
"Set `gnus-gmail-oauth-client-id' and `gnus-gmail-oauth-client-secret'
according to user"
(let* ((store (plstore-open gnus-gmail-id-and-secret-file))
(id-secret (cdr (plstore-get store user))))
(if id-secret
(setq gnus-gmail-oauth-client-id (plist-get id-secret :id)
gnus-gmail-oauth-client-secret (plist-get id-secret :secret))
(display-warning 'gnus-oauth "no id, no secret" :error))
(plstore-close store)))
;;;; imap
(defun gnus-gmail-oauth2-imap-authenticator (user password)
"Authenticator for GMail OAuth2. Use as before-until advice for nnimap-login
See: https://developers.google.com/gmail/xoauth2_protocol"
(if (nnimap-capability "AUTH=XOAUTH2")
(progn
(gnus-gmail-load-id-and-secret user)
(let ((token (gnus-gmail-oauth-token))
access-token)
(setq access-token (oauth2-token-access-token token))
(if (or (null token)
(null access-token))
nil
(let (sequence challenge)
(erase-buffer)
(setq sequence (nnimap-send-command
"AUTHENTICATE XOAUTH2 %s"
(base64-encode-string
(format "user=%s\001auth=Bearer %s\001\001"
(nnimap-quote-specials user)
(nnimap-quote-specials access-token)))))
(setq challenge (nnimap-wait-for-line "^\\(.*\\)\n"))
;; on successful authentication, first line is capabilities,
;; next line is response
(if (string-match "^\\* CAPABILITY" challenge)
(let (response (nnimap-get-response sequence))
(cons t response))
;; send empty response on error
(let (response)
(erase-buffer)
(process-send-string
(get-buffer-process (current-buffer))
"\r\n")
(setq response (nnimap-get-response sequence))
(nnheader-report 'nnimap "%s"
(mapconcat (lambda (a)
(format "%s" a))
(car response) " "))
nil))))))))
Save the user (gmail address), client ID, and secret in advance using the gnus-gmail-secure-id-and-secret command. The file to save them in is specified by gnus-gmail-id-and-secret-file. gnus-gmail-oauth2-imap-authenticator is modified to set the client ID and secret from the user. Also, remove the gnus-gmail-oauth-client-id and gnus-gmail-oauth-client-secret settings that were added to .gnus.el in the Getting emails with IMAP and seeing them with gnus.
(setq gnus-select-method '(nnnil "")
gnus-secondary-select-methods
'((nnimap "name1"
(nnimap-address "imap.gmail.com"))
(nnimap "name2"
(nnimap-address "imap.gmail.com"))
))
To use multiple accounts, I'm using gnus-secondary-select-methods instead of gnus-select-method. name1 and name2 are the names used by Gnus to identify the groups. If nothing is written in .authinfo, you will be asked for the username and password for each group, so please enter your Gmail address as the username. Since the password is not used for OAuth2 connection, just enter something.
SMTP Settings
We need to change the SMTP settings because we need to switch accounts when replying to emails, etc. We want to change the server and login name used for email transmission by the group we're viewing emails in, so we'll use X-Message-SMTP-Method from gnus-posting-styles.
(setq gnus-posting-styles
'((".*"
("X-Message-SMTP-Method" "smtp smtp.gmail.com 587 your_account1@gmail.com"))
("name2" ; Matches Gnus group called "name2"
(address "your_account2@gmail.com")
("X-Message-SMTP-Method" "smtp smtp.gmail.com 587 your_account2@gmail.com"))
))
Furthermore, if there is no entry in .authinfo, an error occurs without being asked for a username and password for some reason, so add entries to the file.
machine smtp.gmail.com login your_account1@gmail.com port 587 password foo
machine smtp.gmail.com login your_account2@gmail.com port 587 password bar
Then, modify smtpmail-try-auth-method. Previously, this was in .gnus.el, but since it no longer seemed like a Gnus setting, I moved it to my-gnus-gmail-oauth.el.
(require 'smtpmail)
(cl-defmethod smtpmail-try-auth-method
(process (_mech (eql xoauth2)) user _password)
(gnus-gmail-load-id-and-secret user)
(let ((token (google-oauth-auth-and-store
gnus-gmail-resource-url
gnus-gmail-oauth-client-id
gnus-gmail-oauth-client-secret))
access-token
ret)
(setq access-token (oauth2-token-access-token token))
(setq ret (smtpmail-command-or-throw
process
(concat "AUTH XOAUTH2 "
(base64-encode-string
(format "user=%s\001auth=Bearer %s\001\001"
(nnimap-quote-specials user)
(nnimap-quote-specials access-token)) t))
))
(when (not (eq (car ret) 235))
(smtpmail-send-command process "NOOP")
(smtpmail-read-response process)
(setq token (gnus-gmail-oauth-token))
(setq access-token (oauth2-token-access-token token))
(smtpmail-command-or-throw
process
(concat "AUTH XOAUTH2 "
(base64-encode-string
(format "user=%s\001auth=Bearer %s\001\001"
(nnimap-quote-specials user)
(nnimap-quote-specials access-token)) t))
235))))
gnus-gmail-load-id-and-secret is called.
Using public key authentication
When using multiple accounts, you are prompted for a password every time you load and save a token, which I feel causes too much password input. Referring to the early comments in plstore.el, which is built into emacs, it'd be better to create a GPG key and switch to public key authentication. I added the following settings to emacs:
;;; plstore for oauth and gnus
(setq plstore-encrypt-to "your_gpg_email")
;;; easypg assistant (epa)
(setq epg-pinentry-mode 'loopback)
The second setting is for entering the password in emacs instead of using pinentry.
More efficient refresh token update
Since we are using test environment of Google OAuth2, the refresh token expires in 7 days. After 7 days, you would need to manually delete the token information stored in oauth2.plstore, obtain the authorization code again, enter it into Emacs, and go through the process of obtaining the token again. Since manually editing oauth2.plstore is troublesome, let's automate this. Modify my-gnus-gmail-oauth.el as follows:
(defun gnus-gmail-oauth-token ()
"Get OAuth token for Gnus to access GMail."
(let ((token (google-oauth-auth-and-store
gnus-gmail-resource-url
gnus-gmail-oauth-client-id
gnus-gmail-oauth-client-secret)))
;; HACK -- always refresh
(oauth2-refresh-access token)
(when (null (oauth2-token-access-token token))
;; delete token and try again
(if (gnus-gmail-oauth2-token-delete)
(setq token
(google-oauth-auth-and-store
gnus-gmail-resource-url
gnus-gmail-oauth-client-id
gnus-gmail-oauth-client-secret)
))
)
token))
(defun gnus-gmail-oauth2-token-delete ()
"Delete a stored oauth2 token. After deletion, if no token associated
with the client id is found, return t, otherwise nil."
(let ((id (oauth2-compute-id
google-oauth-auth-url google-oauth-token-url
gnus-gmail-resource-url gnus-gmail-oauth-client-id))
(plstore (plstore-open oauth2-token-file)))
(plstore--decrypt plstore)
(plstore-delete plstore id)
(prog1 (not (plstore-get plstore id))
(plstore-save plstore)
(plstore-close plstore))))
If the access token cannot be obtained with oauth-refresh-access due to the expired refresh token, the stored tokens are deleted with gnus-gmail-oauth2-token-delete, and google-oauth-auth-and-store is run again to re-authenticate. This eliminates the need to manually edit oauth2.plstore.
Afterword
This article introduced how to use Gnus for Gmail with OAuth 2.0. It took much time, but thanks to the references, I was able to complete this setup. Other email clients such as Thunderbird make it much easier to set up authenticattion with Gmail OAuth2, so I wonder how they do it.