概要と参考文献
emacs を永らく使用していると、僕はメールもemacs で書きたくなってきました。ずいぶん前は emacs + notmuch を使用していましたが、emacs にbuilt-in で入っているgnus にけっこう前に乗り換えました。そしてgmail が通常のパスワード認証を受け付けなくなり、oauth2 認証が必須になったため、gnus 周りの設定をかなり変えて対応しました。その設定を説明したいと思います。
以下の記事を参考にしました。
以下は僕の環境です。
- Linux 6.18.7 (Arch Linux)
- emacs 30.2
- gnupg 2.4.9
gnupg はemacs のoauth2 パッケージで必要です。
準備
emacs のパッケージあるいはelisp ファイルをインストールします。
| パッケージ名 or ファイル名 | 概要 |
|---|---|
| oauth2 | oauth2 認可プロトコル用基礎ライブラリ |
| google-contacts | 下の gnus-gmail-oauth.el の依存パッケージ。google に oauth2 でアクセスするためのライブラリ |
| gnus-gmail-oauth.el | gnus から oauth2 認証を行い、gmail のメールを取得するためのライブラリ |
gnus-gmail-oauth.el はパスを通しておきます。
少し解説すると、 oauth2 パッケージはemacs をクライアントアプリケーションとしてoauth2 認可プロセスを行い、取得したアクセストークン、リフレッシュトークンなどをローカルに安全に保存します。保存のために、emacs built-in の plstore を使用し、plstore の依存パッケージとしてemacs built-in の EasyPG、と gnupg が必要になります。EasyPG や plstore を使ってみると内部でやっていることがわかりやすくなると思います。gnus-gmail-oauth.el はgnus でimap でメールを取得するときに oauth2 パッケージを用いてoauth2 認証を行うelisp です。
google アカウントでのoauth2 認証の設定
gmail をgnus から使用するには、gmail API に oauth2 でアクセスする必要が有り、google アカウントにoauth2 などの設定を行います。
- 自分のgoogle アカウントで google cloud console にアクセスし、プロジェクトを作成します
- gmail にアクセスするため、認証に必要なアプリ名などを入れます。対象ユーザは、私は、テストユーザーで使用するため、外部にしました
- oauth2.0 クライアントを作成します。デスクトップアプリを選択しました。クライアントID とクライアントシークレットをemacs からのアクセス時に使用するので控えておきます
- テストユーザーにgmail の使用のアカウントを追加します。私は自分で使用するので、自分のアカウントを追加しました
- gmail API を有効にします
私はgoogle workspace ユーザーでなく、アプリの検証もハードルが高いので、テスト環境で使用しますが、リフレッシュトークンの有効期限が7日間という制限があります。
imap でメールを取得、gnus で閲覧する
imap ログイン時にoauth2 認証を行い、メールを取得するため以下の設定を追加します。idとシークレットは、先程記録した値で置き換えます。
(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)
gnus の他の設定は gnus の info などを見て設定するのが良いと思います。
これでM-x gnus などでimap ログインすると、google の同意画面などが出て、進んでいくと認可コードを取得できます。それをemacs に入力して、アクセストークンなどを取得、plstore で保存するためにパスフレーズを入力します。そしてimap ログインできるはずです。
smtp でメールを送信する
gnus-gmail-oauth.el では、smtp でのメール送信の設定は入っていないため、メール送信のためのコードを .gnus.el に追加します。user-mail-address, 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)
また、 .authinfo にユーザー名とパスワードを追加する必要があります。実際のoauth2 認証ではこのパスワードは使用しないので適当に入力します。
machine smtp.gmail.com login your_email@gmail.com port 587 password notrecord
これでメール送信ができるはずです。
マルチアカウントの設定
僕はgmail のアカウントを複数持っているため、gnus でそれら複数のアカウントのメールを見たくなりました。そのためには、google アクセス時に、クライアントID とクライアントシークレットを、アクセスするユーザーごとに切り替える必要があります。
クライアントIDとシークレットを保存、ユーザーをキーにロードする
gnus-gmail-oauth2-imap-authenticator を修正する必要が出てくるので、ファイル名を変え、
gnus-gmail-oauth.el → my-gnus-gmail-oauth.el
とします。
imap の設定
(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))))))))
あらかじめgnus-gmail-secure-id-and-secretコマンドを用いてユーザー(gmailアドレス)、クライアントIDとシークレットを保存します。保存するファイルはgnus-gmail-id-and-secret-file で指定します。gnus-gmail-oauth2-imap-authenticator を修正して、ユーザーからクライアントIDとシークレットをセットするようにしています。また、imap でメールを取得、gnus で閲覧する で.gnus.el に追加したgnus-gmail-oauth-client-id,gnus-gmail-oauth-client-secretの設定は削除しましょう。
(setq gnus-select-method '(nnnil "")
gnus-secondary-select-methods
'((nnimap "name1"
(nnimap-address "imap.gmail.com"))
(nnimap "name2"
(nnimap-address "imap.gmail.com"))
))
複数アカウントにするため、gnus-select-methodはメール取得に使用しないよう変更し、gnus-secondary-select-methodsを使用しています。name1,name2 がgnus でグループを識別する名前です。.authinfoに何も書いていなければ、それぞれのグループ名で、ユーザー名、パスワードをどうするか聞かれますので、そのときにユーザー名としてgmail のアドレスを入力してください。oauth2 接続のためパスワードは使われないため適当に入力してください。
smtp の設定
メールに返信するときなどアカウントを切り替える必要があるので、smtp の設定を変更します。メールを見ているときのグループで、作成するメールで使用するサーバーやログイン名を変えたいので、gnus-posting-stylesのX-Message-SMTP-Methodを使います。
(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"))
))
さらに.authinfoにエントリーがないとなぜかユーザー名とパスワードを聞かれることなくエラーになってしまうので、エントリーを書きます。
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
そして、smtpmail-try-auth-method を修正します。今までは、.gnus.elにこれを書いていましたが、gnus の設定ではないように思えてきたので、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 を呼び出すようにしています。
公開鍵認証方式を使用する
複数アカウントを使用すると、トークンを読み込み、保存するたびにパスワードを聞かれるため、パスワードの入力が多すぎるように思えてきました。emacs built-in のplstore.elの最初の方のコメントを参考に、gpg鍵を作り、公開鍵認証方式に切り替えるのがよいです。emacsに以下の設定を追加しました。
;;; plstore for oauth and gnus
(setq plstore-encrypt-to "your_gpg_email")
;;; easypg assistant (epa)
(setq epg-pinentry-mode 'loopback)
2つ目の設定は、パスワードをpinentry の代わりにemacs 上で入力するための設定です。
リフレッシュトークン更新の効率化
google のoauth2 をテスト環境で使用していますので、リフレッシュトークンの有効期限が7日間です。7日後は、oauth2.plstoreに保存しているトークンの情報を手動で消して、再度認可コードを取得、emacsに入力し、トークンを取得する手続きを行う必要があります。oauth2.plstoreを手動で編集する、というのが面倒なのでこれを自動化してみます。my-gnus-gmail-oauth.elを以下のように修正します。
(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))))
oauth-refresh-accessでアクセストークンがリフレッシュトークンの有効期限切れで得られない場合に、gnus-gmail-oauth2-token-deleteで保存しているトークンを削除し、再度google-oauth-auth-and-storeを行い、再認証をするようにしています。これでoauth2.plstoreを手動で編集する必要はなくなります。
あとがき
gnus でoauth2.0 を用いてgmail を使用する方法を紹介しました。時間はかかりましたが、参考文献のおかげもありこの設定を完成させることができました。Thunderbird などの他のメーラはもっと簡単にgmail のoauth2 認証ができるようになっていますが、どうやっているのだろうと思います。