Gmailについて考える
Eメールを使用する時、メールサーバーを自前で運用している猛者達もいるだろうけど、多くの人は、Gmail、Outlookなどのサービスを使っているだろう。僕は、主にGmailを使っている。Webブラウザでhttps://mail.google.comにアクセスしログインすれば、メールの閲覧や送信ができる。殆どそれで問題はない。だけれど僕はGmailもEmacsから使いたい。そんな事を考えていたけれど、設定が面倒な事や、そもそもGmail自体にも不満はなかったので、今まで手が出ないでいた。
Emacsでメールを送受信するためのメールクライアントは幾つかある。Mewなども候補になったけれど、今回はGnusを使う事にした。Gnusは元はニュースリーダーではあるものの、バックエンドにIMAP4を使用できるため、メールリーダーとしても使用できる。基本的にはこの記事を参考に設定し、いくつか変更したり、必要な関数を追加したりしている。
https://qiita.com/waiseiningenchokon/items/faca52752c2f78284ecd
gmail.el
(require 'smtpmail)
(require 'cl)
(require 'gnus-gmail-oauth)
(require 'gnus-x-gm-raw)
(require 'oauth2)
(require 'nnir)
(require 'url)
(defvar gmail-current-oauth2-application nil)
(defvar gmail-current-email nil)
(defvar gmail-current-token nil)
(defvar gmail-base-token nil)
(defvar gmail-current-authorize-url nil
"Gmail用認証URL
通常はnilを設定する。必要に応じてこの変数をシャドーイングし使用する。")
(defun gmail-oauth2-param-get (sym)
(alist-get sym
(cdr (car gmail-current-oauth2-application))))
(defun gmail-base-token-param-get (sym)
(alist-get sym gmail-base-token))
(defun gmail-token-param-get (sym)
(alist-get sym gmail-current-token))
(defun gmail-convert-json-to-elisp ()
(interactive)
(let ((data (json-read-from-string (buffer-string)))
(buf (get-buffer-create "*Google OAuth2 Parameters*")))
(prin1 data buf)
(display-buffer buf)))
(defun gmail-format-authorize-url ()
(format
(string-join '("%s"
"?redirect_uri=urn:ietf:wg:oauth:2.0:oob"
"&scope=https://mail.google.com/"
"&response_type=code"
"&client_id=%s"))
(gmail-oauth2-param-get 'auth_uri)
(gmail-oauth2-param-get 'client_id)))
(defun gmail-login ()
(interactive)
(let ((gmail-authorize-url (gmail-format-authorize-url)))
(browse-url gmail-authorize-url)))
(defun gmail-get-access-token-from-code (code)
(interactive "sCode: ")
(make-process
:name "*Gmail Get Access Token From Code*"
:buffer "*Gmail Get Access Token From Code*"
:command
`("curl" ,(gmail-oauth2-param-get 'token_uri)
"--request" "POST"
"--header" "Content-Type: application/x-www-form-urlencoded"
"--data"
,(format
"redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s"
(gmail-oauth2-param-get 'client_id)
(gmail-oauth2-param-get 'client_secret)
code))))
(defun gmail-get-access-token-from-refres-token ()
"Gmail API用のアクセストークンをリフレッシュする"
(interactive)
(make-process
:name "*Gmail Refresh Token*"
:buffer "*Gmail Refresh Token*"
:command
`("curl" ,(gmail-oauth2-param-get 'token_uri)
"--request" "POST"
"--header" "Content-Type: application/x-www-form-urlencoded"
"--data"
,(format
"redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=refresh_token&client_id=%s&client_secret=%s&refresh_token=%s"
(gmail-oauth2-param-get 'client_id)
(gmail-oauth2-param-get 'client_secret)
(gmail-base-token-param-get 'refresh_token)))
:sentinel
(lambda (process event)
(when (string-equal event "finished\n")
(setq gmail-current-token (json-read-from-string
(with-current-buffer "*Gmail Refresh Token*"
(buffer-string))))))))
(defun gnus-gmail-oauth-token ()
"既存の関数を上書きする"
(make-oauth2-token :access-token (gmail-token-param-get 'access_token)))
(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")
(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)))))))
(setq gnutls-min-prime-bits 1024)
(setq gnutls-algorithm-priority "SECURE128:-VERS-SSL3.0:-VERS-TLS1.3")
(setq gnus-select-method '(nnnil ""))
(setq gnus-secondary-select-methods
`((nnimap "gmail"
(nnimap-address "imap.gmail.com")
(nnimap-user ,gmail-current-email)
(nnimap-server-port 993)
(nnimap-stream ssl)
;; Search
(nnir-search-engine imap))))
(add-to-list 'nnir-imap-search-arguments '("gmail" . "X-GM-RAW"))
(setq nnir-imap-default-search-key "gmail")
(defadvice nnir-run-imap (before decode-group activate)
(ad-set-arg 2 (mapcar 'gnus-group-decoded-name (ad-get-arg 2))))
(advice-add 'nnimap-login :before-until #'gnus-gmail-oauth2-imap-authenticator)
(setq gnus-permanently-visible-groups "^nnimap\\+gmail:INBOX\\|^nnimap\\+gmail:\\[Gmail\\]")
(setq gnus-gmail-oauth-client-id (gmail-oauth2-param-get 'client_id))
(setq gnus-gmail-oauth-client-secret (gmail-oauth2-param-get 'client_secret))
;; SMTMP XOAUTH2
(setq smtpmail-smtp-server "smtp.gmail.com"
smtpmail-smtp-service 587)
(defvar %smtpmail-try-auth-method (symbol-function 'smtpmail-try-auth-method))
(defun smtpmail-try-auth-method (process mech user password)
(if (eql mech 'xoauth2)
(let ((token (gnus-gmail-oauth-token))
access-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))
(funcall (symbol-value '%smtpmail-try-auth-method) process mech user password)))
(add-to-list 'smtpmail-auth-supported 'xoauth2)
(provide 'gmail)
以下、実装時のメモを書く。
Gmail APIでメールを送信する
Gmail APIを使用したメールの操作はOAuth2によりアクセストークンを取得し、そのアクセストークンを使用してGMail APIを呼び出すことでメールを操作する。今回はこの方法を用いてメールの送信を確認する。
プロジェクトを有効にする
(setq dummy-project-name "DUMMY_PROJECT")
(setq dummy-project-user-id "1")
OAuth 2.0 クライアント IDを作成する
(concat "https://console.cloud.google.com/apis/credentials/consent"
"?"
(format "authuser=%s" dummy-project-user-id)
"?"
(format "project=%s" dummy-project-name)
)
https://console.cloud.google.com/apis/credentials/consent?authuser=1?project=DUMMY_PROJECT
この値は重要な値である。今回は以下の値を例として扱うことにする。
Name | Value |
---|---|
Client ID | DUMMY_CLIENT_ID |
Client Secret | DUMMY_CLIENT_SECRET |
(setq dummy-client-id "DUMMY_CLIENT_ID")
(setq dummy-client-secret "DUMMY_CLIENT_SECRET")
Gmail APIを有効にする
(concat "https://console.cloud.google.com/apis/dashboard"
"?"
(format "authuser=%s" dummy-project-user-id)
"?"
(format "project=%s" dummy-project-name)
)
https://console.cloud.google.com/apis/dashboard?authuser=1?project=DUMMY_PROJECT
(concat "https://console.cloud.google.com/apis/api/gmail.googleapis.com/metrics"
"?"
(format "authuser=%s" dummy-project-user-id)
"?"
(format "project=%s" dummy-project-name)
)
https://console.cloud.google.com/apis/api/gmail.googleapis.com/metrics?authuser=1?project=DUMMY_PROJECT
OAuth2のアクセストークンを取得する
認可ページへのURLを組み立てる
Webブラウザで認可ページへアクセスし認可を行う
認可が正常に完了するとコードが表示される。 このコードはアクセストークンの初回の生成時に使用する。
(setq dummy-authorization-code "DUMMY_CODE")
OAuth2のアクセストークンを生成する
先ほど生成したコードを利用してアクセストークンを生成する。
:CLIENT_ID := dummy-client-id
:CLIENT_SECRET := dummy-client-secret
:CODE := dummy-authorization-code
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=authorization_code&code=:CODE&client_id=:CLIENT_ID&client_secret=:CLIENT_SECRET
アクセストークンには有効期限がある。有効期限の切れたアクセストークンは当然API呼び出しに失敗する。 アクセストークンの生成時にリクレッシュトークンも共に返される。 このリクレッシュトークンはアクセストークンを更新し期限を延長するために使用する。
(setq dummy-access-token "DUMMY_ACCESS_TOKEN")
(setq dummy-refresh-token "DUMMY_REFRESH_TOKEN")
期限の切れたOAuth2のアクセストークンをリフレッシュする
:CLIENT_ID := (plist-get gmail-secret-plist :gmail-client-id)
:CLIENT_SECRET := (plist-get gmail-secret-plist :gmail-client-secret)
:REFRESH_TOKEN := (plist-get gmail-secret-plist :gmail-oauth2-refresh-token)
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
client_id=:CLIENT_ID&client_secret=:CLIENT_SECRET&redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=refresh_token&refresh_token=:REFRESH_TOKEN
メールを送信する
送信するメールを作成する
from email.mime.text import MIMEText
m = MIMEText('This is automated draft mail')
m['To'] = '[email protected]'
m['From'] = '[email protected]'
m['Subject'] = 'succeed!!'
return m.as_string()
Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit To: [email protected] From: [email protected] Subject: succeed!! This is automated draft mail
メールのデータをbase64形式にエンコードする
Q29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJ1cy1hc2NpaSIKTUlNRS1WZXJzaW9u OiAxLjAKQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdApUbzogdG9AZXhhbXBsZS5jb20K RnJvbTogZnJvbUBleGFtcGxlLmNvbQpTdWJqZWN0OiBzdWNjZWVkISEKClRoaXMgaXMgYXV0b21h dGVkIGRyYWZ0IG1haWw=
上記のデータを生成できればよい。base64コマンドを使用すればbase64形式に エンコードできる。Pythonでbase64形式にエンコードする場合、 base64.b64encode()関数を使用する。
メール送信APIを呼び出す
:USER_ID = me
:ACCESS_TOKEN := dummy-access-token
POST https://gmail.googleapis.com/gmail/v1/users/:USER_ID/messages/send
Content-Type: application/json
Authorization: Bearer :ACCESS_TOKEN
{
"id": "string",
"labelIds": [],
"snippet": "string",
"payload": {
"partId": "string",
"mimeType": "string",
"filename": "string",
"headers": [
{
"name": "To",
"value": "[email protected]"
},
{
"name": "From",
"value": "[email protected]"
}
],
"body": {
"attachmentId": "",
"size": 10,
"data": "YWJjZGU="
},
"parts": []
},
"sizeEstimate": 10,
"raw": "Q29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PSJ1cy1hc2NpaSIKTUlNRS1WZXJzaW9uOiAxLjAKQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdApUbzogdG9AZXhhbXBsZS5jb20KRnJvbTogZnJvbUBleGFtcGxlLmNvbQpTdWJqZWN0OiBzdWNjZWVkISEKClRoaXMgaXMgYXV0b21hdGVkIGRyYWZ0IG1haWw=",
}
スレッドの取得
:USER_ID = me
:ACCESS_TOKEN := gmail-oauth2-access_token
GET https://gmail.googleapis.com/gmail/v1/users/:USER_ID/threads
Content-Type: application/json
Authorization: Bearer :ACCESS_TOKEN
メッセージの一覧を取得する
:USER_ID = me
:ACCESS_TOKEN := gmail-oauth2-access_token
GET https://gmail.googleapis.com/gmail/v1/users/:USER_ID/messages
Content-Type: application/json
Authorization: Bearer :ACCESS_TOKEN