Subject: Experimental support for IMAP/SMTP/POP with XOAUTH2
Author: Yoshinari Nomura <nom@quickhack.net>
Origin: https://github.com/yoshinari-nomura/Mew/commit/eac7b88b8ab091eea53140dec0f7a76206239eaf
Origin: https://github.com/tats/Mew/tree/feature/xoauth2

cf. https://groups.google.com/g/mew-ja/c/Hn9L27ll-eY
    
    This patch is just tested with:
     + Gmail SMTP/IMAP XOAUTH2, Ubuntu Linux
    and requires oauth2.el from ELPA.
    
    Get your own OAuth cliend-id and client-secret and
    setup these variables in your init.el:
    
      (setq mew-auth-oauth-client-id "xxxxxxxxxapps.googleusercontent.com"
            mew-auth-oauth-client-secret "xxxxxxxxxxxxxxxxxxxxxxxxxx"
            plstore-cache-passphrase-for-symmetric-encryption t
            epg-pinentry-mode 'loopback)
    
    You may want to give priority to XOAUTH2:
    
      (setq mew-config-alist
            '(
              ("default"
               ; ...
               (smtp-auth-list '("XOAUTH2"))
               (pop-auth-list  '("XOAUTH2"))
               (imap-auth-list '("XOAUTH2")))
               ; ...
              ))
    
    If you want to another email service instead of Gmail, you will
    have to change these variables:
    
      (defvar mew-auth-oauth2-auth-url
        "https://accounts.google.com/o/oauth2/auth"
        "OAuth2 auth server URL.")
    
      (defvar mew-auth-oauth2-token-url
        "https://accounts.google.com/o/oauth2/token"
        "OAuth2 token server URL.")
    
      (defvar mew-auth-oauth2-resource-url
        "https://mail.google.com/"
        "URL used to request access to Mail Resources.")
    
      (defvar mew-auth-oauth2-redirect-url nil
        "URL used to OAuth redirect url.")

diff --git a/mew-auth.el b/mew-auth.el
index eaf6677..49d0f46 100644
--- a/mew-auth.el
+++ b/mew-auth.el
@@ -67,6 +67,80 @@
 (defun mew-keyed-md5 (key passwd)
   (mew-md5 (concat key passwd)))
 
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; XOAUTH2
+
+(defun mew-auth-xoauth2-auth-string (user token)
+  ;; base64(user=user@example.com^Aauth=Bearer ya29vF9dft4...^A^A)
+  (base64-encode-string (format "user=%s\1auth=Bearer %s\1\1" user token) t))
+
+(defun mew-auth-xoauth2-json-status (status-string)
+  ;; https://developers.google.com/gmail/imap/xoauth2-protocol#error_response_2
+  (require 'json)
+  (let ((json-status
+         (ignore-errors
+           (json-read-from-string
+            (base64-decode-string status-string)))))
+    (if json-status
+        (if (string-match "^2" (cdr (assoc 'status json-status)))
+            "OK" ;; 2XX
+          "NO" ;; XXX: Anyway NO?
+          )
+      "OK" ;; XXX: Maybe OK if not JSON.
+      )))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; OAuth2
+
+(defvar mew-auth-oauth2-client-id
+  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com")
+
+(defvar mew-auth-oauth2-client-secret
+  "xxxxxxxxxxxxxxxxxxxxxxxx")
+
+(defvar mew-auth-oauth2-auth-url "https://accounts.google.com/o/oauth2/auth"
+  "OAuth2 auth server URL.")
+
+(defvar mew-auth-oauth2-token-url "https://accounts.google.com/o/oauth2/token"
+  "OAuth2 token server URL.")
+
+(defvar mew-auth-oauth2-resource-url "https://mail.google.com/"
+  "URL used to request access to Mail Resources.")
+
+(defvar mew-auth-oauth2-redirect-url nil
+  "URL used to OAuth redirect url.")
+
+(declare-function oauth2-auth-and-store "oauth2")
+(declare-function oauth2-refresh-access "oauth2")
+(declare-function oauth2-token-access-token "oauth2")
+
+(defun mew-auth-oauth2-auth-and-store
+    (resource-url client-id client-secret &optional redirect-url)
+  "Request access to a mail resource and store it using `auth-source'."
+  (require 'oauth2)
+  (oauth2-auth-and-store
+   mew-auth-oauth2-auth-url
+   mew-auth-oauth2-token-url
+   resource-url
+   client-id
+   client-secret
+   redirect-url))
+
+(defun mew-auth-oauth2-token ()
+  "Get OAuth token for Mew to access mail service."
+  (require 'oauth2)
+  (let ((token (mew-auth-oauth2-auth-and-store
+                mew-auth-oauth2-resource-url
+                mew-auth-oauth2-client-id
+                mew-auth-oauth2-client-secret
+                mew-auth-oauth2-redirect-url)))
+    (oauth2-refresh-access token)
+    token))
+
+(defun mew-auth-oauth2-token-access-token ()
+  (require 'oauth2)
+  (ignore-errors (oauth2-token-access-token (mew-auth-oauth2-token))))
+
 (provide 'mew-auth)
 
 ;;; Copyright Notice:
diff --git a/mew-imap.el b/mew-imap.el
index c07dc93..558227f 100644
--- a/mew-imap.el
+++ b/mew-imap.el
@@ -56,6 +56,7 @@
 (defvar mew-imap-fsm
   '(("greeting"      ("OK" . "capability"))
     ("capability"    ("OK" . "post-capability"))
+    ("auth-xoauth2"  ("OK" .  "next") ("NO" . "xoauth2-wpwd"))
     ("auth-cram-md5" ("OK" . "pwd-cram-md5") ("NO" . "wpwd"))
     ("pwd-cram-md5"  ("OK" . "next") ("NO" . "wpwd"))
     ("auth-login"    ("OK" . "user-login") ("NO" . "wpwd"))
@@ -969,7 +970,8 @@
 
 (defvar mew-imap-auth-alist
   '(("CRAM-MD5" mew-imap-command-auth-cram-md5)
-    ("LOGIN"    mew-imap-command-auth-login)))
+    ("LOGIN"    mew-imap-command-auth-login)
+    ("XOAUTH2"  mew-imap-command-auth-xoauth2)))
 
 (defun mew-imap-auth-get-func (auth)
   (nth 1 (mew-assoc-case-equal auth mew-imap-auth-alist 0)))
@@ -1007,6 +1009,43 @@
 	 (epasswd (mew-base64-encode-string passwd)))
     (mew-imap-process-send-string2 pro epasswd)))
 
+;;;;;;;;;;;;;;;;
+;; XOAUTH2
+
+(defun mew-imap-command-auth-xoauth2 (pro pnm)
+  (let* ((user (mew-imap-get-user pnm))
+         (token (mew-auth-oauth2-token-access-token))
+         (auth-string (mew-auth-xoauth2-auth-string user token)))
+    ;; XXX: need to reset satus if token is nil.
+    (mew-imap-process-send-string pro pnm (format "AUTHENTICATE XOAUTH2 %s" auth-string))
+    (mew-imap-set-status pnm "auth-xoauth2")))
+
+;; XXX: defalias does not work!
+;; (defalias 'mew-imap2-command-auth-xoauth2 'mew-imap-command-auth-xoauth2)
+(defun mew-imap2-command-auth-xoauth2 (pro pnm)
+  (let* ((user (mew-imap2-get-user pnm))
+         (token (mew-auth-oauth2-token-access-token))
+         (auth-string (mew-auth-xoauth2-auth-string user token)))
+    ;; XXX: need to reset satus if token is nil.
+    (mew-imap2-process-send-string pro pnm (format "AUTHENTICATE XOAUTH2 %s" auth-string))
+    (mew-imap2-set-status pnm "auth-xoauth2")))
+
+(defun mew-imap-command-xoauth2-wpwd (pro pnm)
+  (mew-imap-set-done pnm t)
+  (mew-passwd-set-passwd (mew-imap-passtag pnm) nil)
+  (delete-process pro)
+  ;; XXX: Should be cared more! Clear process and filter without sending LOGOUT.
+  (error "IMAP XOAUTH2 token is wrong!"))
+
+;; XXX: defalias does not work!
+;; (defalias 'mew-imap2-command-xoauth2-wpwd 'mew-imap-command-xoauth2-wpwd)
+(defun mew-imap2-command-xoauth2-wpwd (pro pnm)
+  (mew-imap2-set-done pnm t)
+  (mew-passwd-set-passwd (mew-imap2-passtag pnm) nil)
+  (delete-process pro)
+  ;; XXX: Should be cared more! Clear process and filter without sending LOGOUT.
+  (error "IMAP XOAUTH2 token is wrong!"))
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;
 ;;; Sub functions
@@ -1477,8 +1516,12 @@
        (if (string= status "greeting")
 	   (setq next (mew-imap-fsm-next "greeting" "OK"))
 	 (setq stay t)))
-      ((and (goto-char (point-min)) (looking-at "\\+"))
-       (setq next (mew-imap-fsm-next status "OK")))
+      ((and (goto-char (point-min)) (looking-at "\\+ *\\(.*\\)"))
+       (setq next (mew-imap-fsm-next
+                   status
+                   (if (string= status "auth-xoauth2")
+                       (mew-auth-xoauth2-json-status (mew-match-string 1))
+                     "OK"))))
       ((and (goto-char (point-max)) (= (forward-line -1) 0) (looking-at eos))
        (mew-imap-set-tag pnm nil)
        (setq code (mew-match-string 1))
diff --git a/mew-imap2.el b/mew-imap2.el
index 49b8231..c579216 100644
--- a/mew-imap2.el
+++ b/mew-imap2.el
@@ -37,6 +37,7 @@
 (defvar mew-imap2-fsm
   '(("greeting"      ("OK" . "capability"))
     ("capability"    ("OK" . "post-capability"))
+    ("auth-xoauth2"  ("OK" .  "next") ("NO" . "xoauth2-wpwd"))
     ("auth-cram-md5" ("OK" . "pwd-cram-md5") ("NO" . "wpwd"))
     ("pwd-cram-md5"  ("OK" . "next") ("NO" . "wpwd"))
     ("auth-login"    ("OK" . "user-login") ("NO" . "wpwd"))
@@ -241,7 +242,8 @@
 
 (defvar mew-imap2-auth-alist
   '(("CRAM-MD5" mew-imap2-command-auth-cram-md5)
-    ("LOGIN"    mew-imap2-command-auth-login)))
+    ("LOGIN"    mew-imap2-command-auth-login)
+    ("XOAUTH2"  mew-imap2-command-auth-xoauth2)))
 
 (defun mew-imap2-auth-get-func (auth)
   (nth 1 (mew-assoc-case-equal auth mew-imap2-auth-alist 0)))
@@ -537,7 +539,7 @@ with '*' in the region are handled."
 	 (buf (process-buffer process))
 	 aux stay next func code resp)
     (save-excursion
-      (mew-imap2-debug (upcase status) string)
+      (mew-imap2-debug (upcase (format "%s" status)) string)
       (if (and buf (get-buffer buf)) (set-buffer buf))
       (while (string-match "^\\*[^\n]*\n" str)
 	(setq aux (substring str 0 (match-end 0)))
@@ -551,9 +553,13 @@ with '*' in the region are handled."
       (cond
        (next
 	nil)
-       ((string-match "^\\+" str)
+       ((string-match "^\\+ *\\(.*\\)" str)
 	(mew-imap2-set-aux pnm str)
-	(setq next (mew-imap2-fsm-next status "OK")))
+        (setq next (mew-imap2-fsm-next
+                    status
+                    (if (string= status "auth-xoauth2")
+                        (mew-auth-xoauth2-json-status (mew-match-string 1))
+                      "OK"))))
        ((string-match eos str)
 	(mew-imap2-set-tag pnm nil)
 	(setq code (mew-match-string 1 str))
diff --git a/mew-pop.el b/mew-pop.el
index 9e2db7c..75f7cb7 100644
--- a/mew-pop.el
+++ b/mew-pop.el
@@ -41,6 +41,7 @@
 (defvar mew-pop-fsm
   '(("greeting"      nil ("\\+OK" . "capa"))
     ("capa"          t   ("\\+OK" . "auth") ("-ERR" . "pswd"))
+    ("xoauth2"       nil ("\\+OK" . "list") ("-ERR" . "wpwd"))
     ("auth-cram-md5" nil ("\\+OK" . "pwd-cram-md5") ("-ERR" . "wpwd"))
     ("pwd-cram-md5"  nil ("\\+OK" . "list") ("-ERR" . "wpwd"))
     ("auth-plain"    nil ("\\+OK" . "pwd-plain") ("-ERR" . "wpwd"))
@@ -503,11 +504,19 @@
 
 (defvar mew-pop-auth-alist
   '(("CRAM-MD5" mew-pop-command-auth-cram-md5)
-    ("PLAIN"    mew-pop-command-auth-plain)))
+    ("PLAIN"    mew-pop-command-auth-plain)
+    ("XOAUTH2"  mew-pop-command-auth-xoauth2)))
 
 (defun mew-pop-auth-get-func (auth)
   (nth 1 (mew-assoc-case-equal auth mew-pop-auth-alist 0)))
 
+(defun mew-pop-command-auth-xoauth2 (pro pnm)
+  (let* ((user (mew-pop-get-user pnm))
+         (token (mew-auth-oauth2-token-access-token))
+         (auth-string (mew-auth-xoauth2-auth-string user token)))
+    (mew-pop-process-send-string pro "AUTH XOAUTH2 %s" auth-string)
+    (mew-smtp-set-status pnm "auth-xoauth2")))
+
 (defun mew-pop-command-auth-cram-md5 (pro pnm)
   (mew-pop-process-send-string pro "AUTH CRAM-MD5")
   (mew-pop-set-status pnm "auth-cram-md5"))
diff --git a/mew-smtp.el b/mew-smtp.el
index e3dbbdc..a4ea807 100644
--- a/mew-smtp.el
+++ b/mew-smtp.el
@@ -48,6 +48,10 @@
     ("user-login"    ("334" . "pwd-login") (t . "wpwd"))
     ("pwd-login"     ("235" . "next") (t . "wpwd"))
     ("auth-plain"    ("235" . "next") (t . "wpwd"))
+    ;; This is checked only for Gmail https://developers.google.com/gmail/imap/xoauth2-protocol
+    ;; XXX: MS Exchange Returns 334 like CRAM-MD5?
+    ;;  https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
+    ("auth-xoauth2"  ("235" . "next") (t . "wpwd"))
 ;; See blow
 ;;    ("auth-plain"    ("334" . "pwd-plain") (t . "wpwd"))
 ;;    ("pwd-plain"     ("235" . "next") (t . "wpwd"))
@@ -249,7 +253,8 @@
 (defvar mew-smtp-auth-alist
   '(("CRAM-MD5" mew-smtp-command-auth-cram-md5)  ;; RFC 2195
     ("PLAIN"    mew-smtp-command-auth-plain)     ;; RFC 2595
-    ("LOGIN"    mew-smtp-command-auth-login)))   ;; No spec
+    ("LOGIN"    mew-smtp-command-auth-login)     ;; No spec
+    ("XOAUTH2"  mew-smtp-command-auth-xoauth2)))
 
 (defun mew-smtp-auth-get-func (auth)
   (nth 1 (mew-assoc-case-equal auth mew-smtp-auth-alist 0)))
@@ -308,6 +313,13 @@
     (mew-smtp-process-send-string pro "AUTH PLAIN %s" plain)
     (mew-smtp-set-status pnm "auth-plain")))
 
+(defun mew-smtp-command-auth-xoauth2 (pro pnm)
+  (let* ((user (mew-smtp-get-auth-user pnm))
+         (token (mew-auth-oauth2-token-access-token))
+         (auth-string (mew-auth-xoauth2-auth-string user token)))
+    (mew-smtp-process-send-string pro "AUTH XOAUTH2 %s" auth-string)
+    (mew-smtp-set-status pnm "auth-xoauth2")))
+
 ;; (defun mew-smtp-command-auth-plain (pro pnm)
 ;;   (mew-smtp-process-send-string pro "AUTH PLAIN")
 ;;   (mew-smtp-set-status pnm "auth-plain"))
diff --git a/mew-vars.el b/mew-vars.el
index 3fa1af3..9801229 100644
--- a/mew-vars.el
+++ b/mew-vars.el
@@ -619,9 +619,9 @@ your e-mail address is automatically set"
 
 (defcustom mew-smtp-auth-list '("CRAM-MD5" "PLAIN" "LOGIN")
   "*A list of SMTP AUTH methods in the preferred order.
-Currently, \"CRAM-MD5\", \"PLAIN\", and \"LOGIN\" can be used."
+Currently, \"CRAM-MD5\", \"PLAIN\", \"LOGIN\", and \"XOAUTH2\" can be used."
   :group 'mew-smtp
-  :type '(repeat (choice (const "CRAM-MD5") (const "PLAIN") (const "LOGIN"))))
+  :type '(repeat (choice (const "CRAM-MD5") (const "PLAIN") (const "LOGIN") (const "XOAUTH2"))))
 
 (defcustom mew-smtp-helo-domain "localhost"
   "*An e-mail domain to tell a SMTP server with HELO/EHLO."
@@ -736,9 +736,9 @@ t means SASL according to 'mew-pop-auth-list'."
 
 (defcustom mew-pop-auth-list '("CRAM-MD5" "PLAIN")
   "*A list of SASL methods in the preferred order.
-Currently, \"CRAM-MD5\" can be used."
+Currently, \"CRAM-MD5\", \"PLAIN\", and \"XOAUTH2\" can be used."
   :group 'mew-pop
-  :type '(repeat (choice (const "CRAM-MD5") (const "PLAIN"))))
+  :type '(repeat (choice (const "CRAM-MD5") (const "PLAIN") (const "XOAUTH2"))))
 
 (defcustom mew-pop-delete t
   "*Whether or not delete messages on a POP server after retrieval by
@@ -826,11 +826,11 @@ t means SASL according to 'mew-imap-auth-list'."
   :group 'mew-imap
   :type 'boolean)
 
-(defcustom mew-imap-auth-list '("CRAM-MD5"  "LOGIN")
+(defcustom mew-imap-auth-list '("CRAM-MD5" "LOGIN")
   "*A list of SASL methods in the preferred order.
-Currently, \"CRAM-MD5\" and \"LOGIN\" can be used."
+Currently, \"CRAM-MD5\", \"LOGIN\", and \"XOAUTH2\" can be used."
   :group 'mew-imap
-  :type '(repeat (choice (const "CRAM-MD5") (const "LOGIN"))))
+  :type '(repeat (choice (const "CRAM-MD5") (const "LOGIN") (const "XOAUTH2"))))
 
 (defcustom mew-imap-delete t
   "*Whether or not delete messages on an IMAP server after retrieval by
