[balsa/oauth2-support] draft implementation of OAUTHBEARER support for SMTP, POP3 and IMAP



commit 9788c42ef13479ffc77d7960ad65177cd2ce6f9c
Author: Albrecht Dreß <albrecht dress arcor de>
Date:   Tue Dec 22 13:35:45 2020 +0100

    draft implementation of OAUTHBEARER support for SMTP, POP3 and IMAP
    
    See also Balsa issue #40 - a first limited implementation of RFC 7628
    "OAUTHBEARER" authentication, for the time being limited to GMail and
    Yahoo accounts.
    
    Changes:
    - libbalsa/imap:
      - imap-auth.c: implement OAUTHBEARER auth for IMAP
      - imap-handle.[ch]: catch "AUTH=OAUTHBEARER" capability, fix doc
    - libbalsa:
      - oauth2.[ch]: OAuth2 authorisation core and authentication helpers
      - Makefile.am, meson.build: add oauth2.[ch]
      - server.c: add OAuth object reference to server object; use OAuth in
        libbalsa_server_get_auth()
    - libnetclient:
      - net-client.[ch]: add method to get the remote address of a connected
        client
      - net-client-utils.[hc]: calculate OAUTHBEARER auth string instead of
        XOAUTH2
      - net-client-pop.c: use OAUTHBEARER sasl auth instead of XOAUTH2
      - net-client-smtp.c: use OAUTHBEARER sasl auth instead of XOAUTH2; be
        more strict checking the required server response codes
    - README: add OAuth2 config and debug options
    
    Signed-off-by: Albrecht Dreß <albrecht dress arcor de>

 README                          |    7 +-
 libbalsa/Makefile.am            |    2 +
 libbalsa/imap/imap-auth.c       |   67 ++-
 libbalsa/imap/imap-handle.c     |    2 +-
 libbalsa/imap/imap-handle.h     |    2 +-
 libbalsa/meson.build            |    2 +
 libbalsa/oauth2.c               | 1085 +++++++++++++++++++++++++++++++++++++++
 libbalsa/oauth2.h               |   72 +++
 libbalsa/server.c               |   25 +-
 libnetclient/net-client-pop.c   |   10 +-
 libnetclient/net-client-smtp.c  |  115 +++--
 libnetclient/net-client-utils.c |   10 +-
 libnetclient/net-client-utils.h |    9 +-
 libnetclient/net-client.c       |   18 +
 libnetclient/net-client.h       |   12 +-
 15 files changed, 1358 insertions(+), 80 deletions(-)
---
diff --git a/README b/README
index a18ab3b35..cde3d7a5a 100644
--- a/README
+++ b/README
@@ -89,7 +89,7 @@ address book is in the works but needs some finishing touches.
 
 --with-osmo
        Enable experimental support for read-only DBus access to the Osmo
-       contacts.  Note that Osmo svn rev. 1099 or later is required.
+contacts.  Note that Osmo svn rev. 1099 or later is required.
 
 --with-canberra
        Use libcanberra-gtk3 for filter sounds.
@@ -102,6 +102,9 @@ authors in a mail header.
        Use GtkSourceview for highlighting structured phrases in
 messages, and for syntax highlighting in attachments.
 
+--with-oauth2
+       Enable OAuth2 authentication (needs libsoup and json-glib)
+
 
 Libraries:
 ---------
@@ -250,7 +253,9 @@ following custom domains are implemented in Balsa:
 - mbox-mbox: MBox mailbox operations
 - mbox-mh: MH mailbox operations
 - mbox-pop3: POP3 mailbox operations
+- oauth: OAuth2 authentication operations
 - send: message transmission
+- libbalsa-server: server operations
 - spell-check: internal spell checker
 
 
diff --git a/libbalsa/Makefile.am b/libbalsa/Makefile.am
index 2792472d0..90a1c6b86 100644
--- a/libbalsa/Makefile.am
+++ b/libbalsa/Makefile.am
@@ -114,6 +114,8 @@ libbalsa_a_SOURCES =                \
        mime-stream-shared.h    \
        misc.c                  \
        misc.h                  \
+       oauth2.c                \
+       oauth2.h                \
        rfc2445.c               \
        rfc2445.h               \
        rfc3156.c               \
diff --git a/libbalsa/imap/imap-auth.c b/libbalsa/imap/imap-auth.c
index b3091f9a2..4c55599f9 100644
--- a/libbalsa/imap/imap-auth.c
+++ b/libbalsa/imap/imap-auth.c
@@ -34,6 +34,7 @@
 static ImapResult imap_auth_anonymous(ImapMboxHandle* handle);
 static ImapResult imap_auth_plain(ImapMboxHandle* handle);
 static ImapResult imap_auth_login(ImapMboxHandle* handle);
+static ImapResult imap_auth_oauth2(ImapMboxHandle* handle);
 
 typedef ImapResult (*ImapAuthenticator)(ImapMboxHandle* handle);
 
@@ -57,25 +58,20 @@ imap_authenticate(ImapMboxHandle* handle)
     return IMAP_SUCCESS;
 
   if ((handle->auth_mode & NET_CLIENT_AUTH_OAUTH2) != 0U) {
-         /* FIXME!! */
-  }
-
-  if ((r != IMAP_SUCCESS) && (handle->auth_mode & NET_CLIENT_AUTH_KERBEROS) != 0U) {
+         r = imap_auth_oauth2(handle);
+  } else if ((handle->auth_mode & NET_CLIENT_AUTH_KERBEROS) != 0U) {
          r = imap_auth_gssapi(handle);
-  }
-
-  if ((r != IMAP_SUCCESS) && (handle->auth_mode & NET_CLIENT_AUTH_NONE_ANON) != 0U) {
+  } else if ((handle->auth_mode & NET_CLIENT_AUTH_NONE_ANON) != 0U) {
          r = imap_auth_anonymous(handle);
-  }
-
-  for (authenticator = imap_authenticators_arr; (r != IMAP_SUCCESS) && *authenticator; authenticator++) {
-         r = (*authenticator)(handle);
-         if (r == IMAP_SUCCESS) {
-                 imap_mbox_handle_set_state(handle, IMHS_AUTHENTICATED);
+  } else {
+         for (authenticator = imap_authenticators_arr; (r != IMAP_SUCCESS) && *authenticator; 
authenticator++) {
+                 r = (*authenticator)(handle);
          }
   }
 
-  if (r != IMAP_SUCCESS) {
+  if (r == IMAP_SUCCESS) {
+         imap_mbox_handle_set_state(handle, IMHS_AUTHENTICATED);
+  } else if (r == IMAP_AUTH_UNAVAIL) {
          imap_mbox_handle_set_msg(handle, _("No way to authenticate is known"));
   }
   return r;
@@ -226,3 +222,46 @@ imap_auth_anonymous(ImapMboxHandle* handle)
                        getmsg_anonymous);
 }
 
+
+/* =================================================================== */
+/* SASL OAUTHBEARER (RFC 7628)                                         */
+/* =================================================================== */
+
+#if defined(HAVE_OAUTH2)
+static gboolean
+getmsg_oauth2(ImapMboxHandle *h, char **retmsg, int *retmsglen)
+{
+       gchar **auth_data;
+       gboolean result;
+
+       g_signal_emit_by_name(h->sio, "auth", NET_CLIENT_AUTH_OAUTH2, &auth_data);
+       if ((auth_data == NULL) || (auth_data[0] == NULL) || (auth_data[1] == NULL)) {
+               result = FALSE;
+       } else {
+               *retmsg = net_client_auth_oauth2_calc(auth_data[0], NET_CLIENT(h->sio), auth_data[1]);
+               *retmsglen = strlen(*retmsg);
+               result = TRUE;
+       }
+       if (auth_data != NULL) {
+               net_client_free_authstr(auth_data[0]);
+               net_client_free_authstr(auth_data[1]);
+               g_free(auth_data);
+       }
+       return result;
+}
+
+static ImapResult
+imap_auth_oauth2(ImapMboxHandle* handle)
+{
+       return imap_auth_sasl(handle, IMCAP_AOAUTH2, "AUTHENTICATE OAUTHBEARER", getmsg_oauth2);
+}
+
+#else
+
+static ImapResult
+imap_auth_oauth2(ImapMboxHandle* handle)
+{
+       return IMAP_AUTH_UNAVAIL;
+}
+
+#endif /* defined(HAVE_OAUTH2) */
diff --git a/libbalsa/imap/imap-handle.c b/libbalsa/imap/imap-handle.c
index b8e5643aa..2f83d739a 100644
--- a/libbalsa/imap/imap-handle.c
+++ b/libbalsa/imap/imap-handle.c
@@ -2107,7 +2107,7 @@ ir_capability_data(ImapMboxHandle *handle)
   /* ordered identically as ImapCapability constants */
   static const char* capabilities[] = {
     "IMAP4", "IMAP4rev1", "STATUS",
-    "AUTH=ANONYMOUS", "AUTH=CRAM-MD5", "AUTH=GSSAPI", "AUTH=PLAIN", "AUTH=XOAUTH2",
+    "AUTH=ANONYMOUS", "AUTH=CRAM-MD5", "AUTH=GSSAPI", "AUTH=PLAIN", "AUTH=OAUTHBEARER",
     "ACL", "RIGHTS=", "BINARY", "CHILDREN",
     "COMPRESS=DEFLATE",
     "ESEARCH", "IDLE", "LITERAL+",
diff --git a/libbalsa/imap/imap-handle.h b/libbalsa/imap/imap-handle.h
index d79d54841..716fb4a1c 100644
--- a/libbalsa/imap/imap-handle.h
+++ b/libbalsa/imap/imap-handle.h
@@ -62,7 +62,7 @@ typedef enum
   IMCAP_ACRAM_MD5,             /* RFC 2195: CRAM-MD5 authentication */
   IMCAP_AGSSAPI,               /* RFC 1731: GSSAPI authentication */
   IMCAP_APLAIN,                 /* RFC 2595: */
-  IMCAP_AOAUTH2,                               /* RFC 6749: OAUTH2 authentication */
+  IMCAP_AOAUTH2,                               /* RFC 7628: OAUTHBEARER authentication */
   IMCAP_ACL,                   /* RFC 2086: IMAP4 ACL extension */
   IMCAP_RIGHTS,                 /* RFC 4314: IMAP4 RIGHTS= extension */
   IMCAP_BINARY,                 /* RFC 3516 */
diff --git a/libbalsa/meson.build b/libbalsa/meson.build
index 24c37f76c..a15c24fcd 100644
--- a/libbalsa/meson.build
+++ b/libbalsa/meson.build
@@ -111,6 +111,8 @@ libbalsa_a_sources = [
   'mime-stream-shared.h',
   'misc.c',
   'misc.h',
+  'oauth2.c',
+  'oauth2.h',
   'rfc2445.c',
   'rfc2445.h',
   'rfc3156.c',
diff --git a/libbalsa/oauth2.c b/libbalsa/oauth2.c
new file mode 100644
index 000000000..83d60e7ee
--- /dev/null
+++ b/libbalsa/oauth2.c
@@ -0,0 +1,1085 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2020 Stuart Parmenter and others,
+ *                         See the file AUTHORS for a list.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "oauth2.h"
+
+
+#if defined(HAVE_OAUTH2)
+
+#include <glib/gi18n.h>
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+#include "libbalsa-conf.h"
+#include "net-client-utils.h"
+
+
+#ifdef G_LOG_DOMAIN
+#  undef G_LOG_DOMAIN
+#endif
+#define G_LOG_DOMAIN "oauth"
+
+
+#if defined (HAVE_LIBSECRET)
+#include <libsecret/secret.h>
+#endif                          /* defined(HAVE_LIBSECRET) */
+
+
+/* define the following to enable verbose HTTP (soup) session logging */
+#define HTTP_VERBOSE                   1
+
+
+/** Web page template for displaying a message in the web browser that the authorisation code has been 
received. */
+#define OAUTH_AUTH_DONE_TEMPLATE                                                                             
                          \
+       "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n"                    \
+       "<html>\n"                                                                                            
                                                  \
+       "<head>\n"                                                                                            
                                                  \
+       "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>\n"    \
+       "<title>%s</title>\n"                                                                                 
                                  \
+       "</head>\n"                                                                                           
                                                  \
+       "<body>%s</p>%s</p></body></html>"
+
+
+/** @brief Provider data
+ *
+ * This struct collects all information about the OAuth2-capable providers implemented here.
+ */
+typedef struct {
+       gchar *id;                                                      /**< Provider identifier. */
+       gchar *display_name;                            /**< Provider display name. */
+       gchar *email_re;                                        /**< Regular Expression for e-mail addresses. 
*/
+       gchar *client_id;                                       /**< Balsa client ID. */
+       gchar *client_secret;                           /**< Balsa client "secret" (not really secret, as its 
hard-coded in the sources). */
+       gchar *auth_uri;                                        /**< URI to call for authentication. */
+       gchar *token_uri;                                       /**< URI to call for for receiving an access 
token. */
+       gchar *scope;                                           /**< OAuth2 scope, not used by some 
providers. */
+       gboolean oob_mode;                                      /**< Indicates if the provider cannot 
redirect to an arbitrary URI with port. */
+} oauth2_provider_t;
+
+
+/** @brief Authorisation context
+ *
+ * This struct collects the data required for performing the initial OAuth2 authorisation of Balsa for a 
particular account.
+ */
+typedef struct {
+       LibBalsaOauth2 *oauth;                          /**< Related OAuth2 object. */
+       GtkWidget *auth_dialog;                         /**< Authorisation dialogue. */
+       GtkWidget *code_entry;                          /**< Entry for authorisation code in OOB mode, NULL 
otherwise. */
+       GtkWidget *spinner;                                     /**< Spinner and... */
+       GtkWidget *spinner_label;                       /**< ...related label. */
+       gchar *auth_request_uri;                        /**< URI for the authorisation request, to be opened 
in a web browser. */
+       gchar *listen_uri;                                      /**< Local listen URI, or 'oob' if 
oauth2_provider_t::oob_mode is TRUE. */
+       SoupServer *server;                                     /**< Locally listening server object, NULL if 
oauth2_provider_t::oob_mode is TRUE. */
+       GError *auth_err;                                       /**< Location for propagating any error. */
+} oauth2_auth_ctx_t;
+
+
+struct _LibBalsaOauth2 {
+       GObject parent;
+
+       const oauth2_provider_t *provider;      /**< The provider information for this OAuth object. */
+       gchar *account;                                         /**< The account name. */
+
+       GMutex mutex;                                           /**< Mutex to avoid multiple auth server 
transactions in parallel. */
+       gchar *access_token;                            /**< Access token. */
+       gint64 valid_until;                                     /**< Access token validity end, in UTC. */
+       gchar *refresh_token;                           /**< Refresh token. */
+       gchar *scope;                                           /**< OAuth2 scope, may be NULL for some 
providers. */
+};
+
+
+G_DEFINE_TYPE(LibBalsaOauth2, libbalsa_oauth2, G_TYPE_OBJECT)
+
+
+// FIXME:
+// - not all OAuth2 providers are listed here
+static const oauth2_provider_t providers[] = {
+       {       // FIXME - limited account, users must be added explicitly for the time being
+               // alternative: create your own account, and modify the client credentials below
+               .id            = "gmail.com",
+               .display_name  = "Gmail",
+               .email_re      = "^.*@(gmail|googlemail|google)\\.com$",                // FIXME - are there 
more?
+               .client_id     = "855255990023-n0gsa2lgg5ubudrce69kcvu0bpmi1m2q.apps.googleusercontent.com",
+               .client_secret = "0Od0RKXFtomBoOgDidCyRxKs",
+               .auth_uri      = "https://accounts.google.com/o/oauth2/v2/auth";,
+               .token_uri     = "https://oauth2.googleapis.com/token";,
+               .scope         = "https://mail.google.com/";,
+               .oob_mode      = FALSE },
+       {       // see https://developer.verizonmedia.com/mail/mail-api-access/ and
+               // https://developer.verizonmedia.com/mail/imap-smtp-documentation/
+               // FIXME - Thunderbird client id & secret
+               .id            = "yahoo.com",
+               .display_name  = "Yahoo Mail",
+               .email_re      = "^.*@yahoo\\.[a-z]{2,3}$",                                             // 
FIXME - are there more?
+               .client_id     = 
"dj0yJmk9NUtCTWFMNVpTaVJmJmQ9WVdrOVJ6UjVTa2xJTXpRbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD0yYw--",
+               .client_secret = "f2de6a30ae123cdbc258c15e0812799010d589cc",
+               .auth_uri      = "https://api.login.yahoo.com/oauth2/request_auth";,
+               .token_uri     = "https://api.login.yahoo.com/oauth2/get_token";,
+               .scope         = "mail-w",
+               .oob_mode      = TRUE }
+};
+
+
+#if defined(HAVE_LIBSECRET)
+
+/** @brief Balsa OAuth2 tokens in Secret Service
+ *
+ * The OAuth2 tokens stored in the Secret Service are identified by @em provider and @em user (i.e. the 
email address).  The value
+ * is stored as binary data with the signature <c>(s(sx))</c>, containing the refresh token and the latest 
access token and its UTC
+ * validity end time stamp.
+ *
+ * If Secret Service support is disabled, or if accessing it fails, Balsa falls back to the private 
configuration file.  The group
+ * containing the tokes and validity is identified by the sha256 hash of provider id and user name.
+ */
+static const SecretSchema oauth2_schema = {
+    "org.gnome.Balsa.OAuth2Token",
+       SECRET_SCHEMA_NONE,
+    { { "provider", SECRET_SCHEMA_ATTRIBUTE_STRING },
+      { "user",     SECRET_SCHEMA_ATTRIBUTE_STRING },
+         { NULL,       0 } }
+};
+#endif  /* defined(HAVE_LIBSECRET) */
+
+
+static void on_oauth_toggle_notify(gpointer  data,
+                                                                  GObject  *object,
+                                                                  gboolean  is_last_ref);
+static void libbalsa_oauth2_finalise(GObject *object);
+static const oauth2_provider_t *libbalsa_oauth2_find_provider(const gchar *mailbox);
+
+#if defined(HAVE_LIBSECRET)
+static void load_oauth2_token_libsecret(LibBalsaOauth2 *oauth);
+static void save_oauth2_token_libsecret(const LibBalsaOauth2 *oauth);
+#endif  /* defined(HAVE_LIBSECRET) */
+static void load_oauth2_token_config(LibBalsaOauth2 *oauth);
+static void save_oauth2_token_config(const LibBalsaOauth2 *oauth);
+
+static gboolean oauth2_refresh(LibBalsaOauth2  *oauth,
+                                                          GError         **error);
+static gboolean eval_json_auth_reply(LibBalsaOauth2     *oauth,
+                                                                        const SoupMessage  *message,
+                                                                        gboolean            is_auth,
+                                                                        GError            **error);
+static void eval_json_error(const gchar        *prefix,
+                                                       const SoupMessage  *message,
+                                                       GError            **error);
+
+static gboolean oauth2_authorise(LibBalsaOauth2  *oauth,
+                                                                GtkWindow       *parent,
+                                                                GError         **error);
+static gboolean oauth2_authorise_idle(gpointer data);
+static gboolean oauth2_authorise_real(LibBalsaOauth2  *oauth,
+                                                                         GtkWindow       *parent,
+                                                                         GError         **error);
+static oauth2_auth_ctx_t *oauth2_authorize_init(const oauth2_provider_t  *provider,
+                                                                                               GError        
          **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+static void oauth2_listener_cb(SoupServer        *server,
+                                                          SoupMessage       *msg,
+                                                          const char        *path,
+                                                          GHashTable        *query,
+                                                          SoupClientContext *client,
+                                                          gpointer           user_data);
+static void oauth2_authorize_finish(oauth2_auth_ctx_t *auth_ctx,
+                                                                       const gchar       *auth_code);
+static gboolean run_oauth2_dialog(oauth2_auth_ctx_t              *auth_ctx,
+                                                                 const oauth2_provider_t *provider,
+                                                                 const gchar             *account,
+                                                                 GtkWindow                       *parent);
+static SoupSession *oauth_soup_session_new(void);
+
+
+/** @brief List of "known" @ref LibBalsaOauth2 items. */
+static GList *oauth_list = NULL;
+G_LOCK_DEFINE_STATIC(oauth_list);
+
+
+/* == OAuth2 object implementation 
============================================================================================== */
+
+gboolean
+libbalsa_oauth2_supported(const gchar *mailbox)
+{
+       return libbalsa_oauth2_find_provider(mailbox) != NULL;
+}
+
+
+LibBalsaOauth2 *
+libbalsa_oauth2_new(LibBalsaServer *server, GError **error)
+{
+       const oauth2_provider_t *provider;
+       const gchar *user;
+       LibBalsaOauth2 *oauth = NULL;
+
+       /* the user name is a mailbox, for which we can look up the suitable provider data */
+       user = libbalsa_server_get_user(server);
+       provider = libbalsa_oauth2_find_provider(user);
+       if (provider == NULL) {
+               g_set_error(error, LIBBALSA_OAUTH2_ERROR_QUARK, -1, _("OAuth2 is not supported for “%s”: 
provider unknown"),
+                       libbalsa_server_get_user(server));
+       } else {
+               GList *p;
+
+               g_debug("%s: user='%s', provider '%s'", __func__, user, provider->display_name);
+               G_LOCK(oauth_list);
+               for (p = oauth_list;
+                        (p != NULL) && (LIBBALSA_OAUTH2(p->data)->provider != provider) &&
+                        (g_ascii_strcasecmp(LIBBALSA_OAUTH2(p->data)->account, user) != 0);
+                        p = p->next) {
+                       /* noop */
+               }
+
+               if (p != NULL) {
+                       oauth = g_object_ref(LIBBALSA_OAUTH2(p->data));
+                       g_debug("%s: ref oauth %p", __func__, oauth);
+               } else {
+                       oauth = g_object_new(LIBBALSA_OAUTH2_TYPE, NULL);
+                       oauth->provider = provider;
+                       oauth->account = g_strdup(user);
+                       g_debug("%s: new oauth %p", __func__, oauth);
+                       g_object_add_toggle_ref(G_OBJECT(oauth), on_oauth_toggle_notify, NULL);
+                       oauth_list = g_list_prepend(oauth_list, oauth);
+               }
+               G_UNLOCK(oauth_list);
+
+               /* try to load stored tokens and validity */
+#if defined(HAVE_LIBSECRET)
+               load_oauth2_token_libsecret(oauth);
+#else
+               load_oauth2_token_config(oauth);
+#endif /* defined(HAVE_LIBSECRET) */
+       }
+
+       return oauth;
+}
+
+
+gchar *
+libbalsa_oauth2_token(LibBalsaOauth2 *oauth, GtkWindow *parent, GError **error)
+{
+       gchar *result = NULL;
+       gboolean save = FALSE;
+
+       g_return_val_if_fail(LIBBALSA_IS_OAUTH2(oauth), NULL);
+
+       g_mutex_lock(&oauth->mutex);
+
+       if (oauth->refresh_token == NULL) {
+               if (oauth2_authorise(oauth, parent, error)) {
+                       result = g_strdup(oauth->access_token);
+                       save = TRUE;
+               }
+       } else if ((oauth->access_token == NULL) || (oauth->valid_until <= time(NULL))) {
+               if (oauth2_refresh(oauth, error)) {
+                       result = g_strdup(oauth->access_token);
+                       save = TRUE;
+               }
+       } else {
+               result = g_strdup(oauth->access_token);
+       }
+
+       if (save) {
+#if defined(HAVE_LIBSECRET)
+               save_oauth2_token_libsecret(oauth);
+#else
+               save_oauth2_token_config(oauth);
+#endif /* defined(HAVE_LIBSECRET) */
+       }
+
+       g_mutex_unlock(&oauth->mutex);
+
+       return result;
+}
+
+
+static void
+libbalsa_oauth2_class_init(LibBalsaOauth2Class *klass)
+{
+       GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+       gobject_class->finalize = libbalsa_oauth2_finalise;
+}
+
+
+static void
+libbalsa_oauth2_init(LibBalsaOauth2 *self)
+{
+       g_mutex_init(&self->mutex);
+}
+
+
+static void
+libbalsa_oauth2_finalise(GObject *object)
+{
+       LibBalsaOauth2 *oauth = LIBBALSA_OAUTH2(object);
+       const GObjectClass *parent_class = G_OBJECT_CLASS(libbalsa_oauth2_parent_class);
+
+       g_debug("%s: %p", __func__, object);
+       g_free(oauth->account);
+       g_free(oauth->access_token);
+       g_free(oauth->refresh_token);
+       g_free(oauth->scope);
+       g_mutex_clear(&oauth->mutex);
+       (*parent_class->finalize)(object);
+}
+
+
+static void
+on_oauth_toggle_notify(gpointer G_GNUC_UNUSED data, GObject *object, gboolean is_last_ref)
+{
+       g_debug("%s: %p %d", __func__, object, is_last_ref);
+       if (is_last_ref) {
+               G_LOCK(oauth_list);
+               oauth_list = g_list_remove(oauth_list, object);
+               G_UNLOCK(oauth_list);
+               g_object_remove_toggle_ref(object, on_oauth_toggle_notify, NULL);
+       }
+}
+
+
+/** @brief Identify the provider from the mailbox address
+ *
+ * @param[in] mailbox user's email address
+ * @return the provider item from @ref providers belonging to the passed email address, or NULL if it is not 
supported
+ */
+static const oauth2_provider_t *
+libbalsa_oauth2_find_provider(const gchar *mailbox)
+{
+       guint n;
+
+       for (n = 0U; n < G_N_ELEMENTS(providers); ++n) {
+               if (g_regex_match_simple(providers[n].email_re, mailbox, G_REGEX_CASELESS, 0)) {
+                       return &providers[n];
+               }
+       }
+       return NULL;
+}
+
+
+/* == OAuth2 token storage 
====================================================================================================== */
+
+#if defined(HAVE_LIBSECRET)
+
+/** @brief Load the OAuth2 tokens from the Secret Service
+ *
+ * @param[in] oauth OAuth2 context
+ *
+ * Try to load the refresh and access tokens as well as the validity end time of the latter of the passed 
OAuth2 object from the
+ * Secret Service.  If this fails, load_oauth2_token_config() is called to try the local private 
configuration file.
+ */
+static void
+load_oauth2_token_libsecret(LibBalsaOauth2 *oauth)
+{
+       static GVariantType *token_gv_type = NULL;
+       SecretValue *secret_value;
+       GError *error = NULL;
+       gboolean try_cfgfile;
+
+       if (g_once_init_enter(&token_gv_type)) {
+               g_once_init_leave(&token_gv_type, g_variant_type_new("(s(sx))"));
+       }
+
+       secret_value = secret_password_lookup_binary_sync(&oauth2_schema, NULL, &error, "provider", 
oauth->provider->id,
+               "user", oauth->account, NULL);
+       if (secret_value != NULL) {
+               const gchar *sec_data;
+               gsize sec_length;
+               GVariant *tok_data;
+
+               sec_data = secret_value_get(secret_value, &sec_length);
+               tok_data = g_variant_new_from_data(token_gv_type, sec_data, sec_length, FALSE, NULL, NULL);
+               if (tok_data != NULL) {
+                       g_free(oauth->access_token);
+                       g_free(oauth->refresh_token);
+                       g_variant_get(tok_data, "(s(sx))", &oauth->refresh_token, &oauth->access_token, 
&oauth->valid_until);
+                       g_variant_unref(tok_data);
+                       try_cfgfile = FALSE;
+                       g_debug("%s: loaded token for %s/%s from secret service", __func__, oauth->account, 
oauth->provider->id);
+               } else {
+                       secret_password_clear_sync(&oauth2_schema, NULL, NULL, "provider", 
oauth->provider->id, "user", oauth->account, NULL);
+                       g_info("invalid OAuth2 data in secret service, trying private config file");
+                       try_cfgfile = TRUE;
+               }
+               secret_value_unref(secret_value);
+       } else {
+               g_info("failed to load OAuth2 data from secret service, trying private config file: %s",
+                       (error != NULL) ? error->message : "unknown");
+               try_cfgfile = TRUE;
+               g_clear_error(&error);
+
+       }
+
+       if (try_cfgfile) {
+               load_oauth2_token_config(oauth);
+       }
+}
+
+
+/** @brief Store the OAuth2 tokens in the Secret Service
+ *
+ * @param[in] oauth OAuth2 context
+ *
+ * Try to save the refresh and access tokens as well as the validity end time of the latter of the passed 
OAuth2 object in the
+ * Secret Service.  If this fails, save_oauth2_token_config() is called to try the local private 
configuration file.
+ */
+static void
+save_oauth2_token_libsecret(const LibBalsaOauth2 *oauth)
+{
+       GVariant *tok_data;
+       SecretValue *secret_value;
+       gboolean result;
+       GError *error = NULL;
+
+       tok_data = g_variant_new("(s(sx))", oauth->refresh_token, oauth->access_token, oauth->valid_until);
+       secret_value = secret_value_new(g_variant_get_data(tok_data), g_variant_get_size(tok_data), 
"application/octet-stream");
+       g_variant_unref(tok_data);
+       result = secret_password_store_binary_sync(&oauth2_schema, NULL, _("Balsa OAuth2"), secret_value, 
NULL, &error,
+               "provider", oauth->provider->id, "user", oauth->account, NULL);
+       secret_value_unref(secret_value);
+
+       if (!result) {
+               libbalsa_information(LIBBALSA_INFORMATION_WARNING,
+                       _("cannot store OAuth2 access data in secret service, fall back to private config 
file: %s"),
+                       (error != NULL) ? error->message : _("unknown"));
+               save_oauth2_token_config(oauth);
+               g_clear_error(&error);
+       } else {
+               g_debug("%s: saved token for %s/%s in secret service", __func__, oauth->account, 
oauth->provider->id);
+       }
+}
+
+#endif  /* defined(HAVE_LIBSECRET) */
+
+
+/** @brief Load the OAuth2 tokens from the private config file
+ *
+ * @param[in] oauth OAuth2 context
+ *
+ * Try to load the refresh and access tokens as well as the validity end time of the latter of the passed 
OAuth2 object from the
+ * local private configuration file.
+ */
+static void
+load_oauth2_token_config(LibBalsaOauth2 *oauth)
+{
+       GChecksum *hash;
+       const gchar *group_name;
+
+       hash = g_checksum_new(G_CHECKSUM_SHA256);
+       g_checksum_update(hash, (const guchar *) oauth->provider->id, strlen(oauth->provider->id));
+       g_checksum_update(hash, (const guchar *) oauth->account, strlen(oauth->account));
+       group_name = g_checksum_get_string(hash);
+       libbalsa_conf_push_group(group_name);
+       oauth->refresh_token = libbalsa_conf_private_get_string("Refresh", TRUE);
+       oauth->access_token = libbalsa_conf_private_get_string("Access", TRUE);
+       oauth->valid_until = libbalsa_conf_get_int_with_default_("Valid", NULL, TRUE);
+       libbalsa_conf_pop_group();
+       g_checksum_free(hash);
+}
+
+
+/** @brief Store the OAuth2 tokens in the private config file
+ *
+ * @param[in] oauth OAuth2 context
+ *
+ * Try to save the refresh and access tokens as well as the validity end time of the latter of the passed 
OAuth2 object in the
+ * local private configuration file.
+ */
+static void
+save_oauth2_token_config(const LibBalsaOauth2 *oauth)
+{
+       GChecksum *hash;
+       const gchar *group_name;
+
+       hash = g_checksum_new(G_CHECKSUM_SHA256);
+       g_checksum_update(hash, (const guchar *) oauth->provider->id, strlen(oauth->provider->id));
+       g_checksum_update(hash, (const guchar *) oauth->account, strlen(oauth->account));
+       group_name = g_checksum_get_string(hash);
+       libbalsa_conf_push_group(group_name);
+       libbalsa_conf_private_set_string("Refresh", oauth->refresh_token, TRUE);
+       libbalsa_conf_private_set_string("Access", oauth->access_token, TRUE);
+       libbalsa_conf_set_int_("Valid", oauth->valid_until, TRUE);
+       libbalsa_conf_pop_group();
+       g_checksum_free(hash);
+}
+
+
+/* == OAuth2 token refresh related functions 
==================================================================================== */
+
+/** @brief Refresh a OAuth2 access token
+ *
+ * @param[in] oauth OAuth2 context
+ * @param[out] error location for error, may be NULL
+ * @return TRUE on success, FALSE on error
+ *
+ * Send a @c refresh_token request to the provider's oauth2_provider_t::token_uri, and call 
eval_json_auth_reply() to extract the
+ * new access token.
+ */
+static gboolean
+oauth2_refresh(LibBalsaOauth2 *oauth, GError **error)
+{
+       gboolean result;
+       SoupSession *session;
+       SoupMessage *message;
+       guint status;
+
+       g_return_val_if_fail((oauth->refresh_token != NULL) && (oauth->provider != NULL), FALSE);
+
+       /* send the refresh token */
+       g_debug("%s: post refresh request for %s: uri=%s id=%s secret=%s token=%s", __func__, oauth->account,
+               oauth->provider->token_uri, oauth->provider->client_id, oauth->provider->client_secret, 
oauth->refresh_token);
+       session = oauth_soup_session_new();
+       message = soup_form_request_new("POST", oauth->provider->token_uri,
+               "grant_type", "refresh_token",
+               "client_id", oauth->provider->client_id,
+               "client_secret", oauth->provider->client_secret,
+               "refresh_token", oauth->refresh_token,
+               NULL);
+       status = soup_session_send_message(session, message);
+       g_debug("%s: status=%u, code=%u, reason=%s", __func__, status, message->status_code, 
message->reason_phrase);
+
+       g_free(oauth->access_token);
+       oauth->access_token = NULL;
+       if ((status != SOUP_STATUS_OK) || (message->status_code != SOUP_STATUS_OK) || (message->response_body 
== NULL)) {
+               eval_json_error(_("OAuth2 refresh token request failed"), message, error);
+               result = FALSE;
+       } else {
+               result = eval_json_auth_reply(oauth, message, FALSE, error);
+       }
+       g_object_unref(message);
+       g_object_unref(session);
+       return result;
+}
+
+
+/** @brief Evaluate the JSON success reply from an OAuth2 server
+ *
+ * @param[in] oauth OAuth2 context
+ * @param[in] message message received from the remote OAuth2 server which @em must have a non-NULL @c 
response_body
+ * @param[in] is_auth TRUE if the JSON string @em must contain a refresh token (i.e. the reply to a 
authorisation request)
+ * @param[out] error location for error, may be NULL
+ * @return TRUE on success, FALSE on error
+ *
+ * Parse the passed JSON string, extract the <c>access_token</c>, <c>scope</c> (if present, see below), 
<c>expires_in</c> and, for
+ * an authorisation reply, the <c>refresh_token</c> elements, and set the respective values in the passed 
OAuth2 context.  Any
+ * missing or malformed JSON element will result in an error.
+ *
+ * Apparently, some providers (e.g. Yahoo) do not include the scope in the response.  If this field is 
missing in the response,
+ * just copy the scope from @ref oauth2_provider_t::scope.
+ */
+static gboolean
+eval_json_auth_reply(LibBalsaOauth2 *oauth, const SoupMessage *message, gboolean is_auth, GError **error)
+{
+       gboolean result = FALSE;
+       JsonParser *parser;
+
+       parser = json_parser_new();
+       if (json_parser_load_from_data(parser, message->response_body->data, message->response_body->length, 
error)) {
+               JsonNode *root;
+
+               root = json_parser_get_root(parser);
+               if (JSON_NODE_HOLDS_OBJECT(root)) {
+                       JsonObject *reply;
+                       gint64 valid_secs;
+
+                       reply = json_node_get_object(root);
+                       oauth->access_token = g_strdup(json_object_get_string_member(reply, "access_token"));
+                       if (is_auth) {
+                               oauth->refresh_token = g_strdup(json_object_get_string_member(reply, 
"refresh_token"));
+                       }
+                       /* some providers don't include the scope in the reply */
+                       if (json_object_has_member(reply, "scope")) {
+                               oauth->scope = g_strdup(json_object_get_string_member(reply, "scope"));
+                       } else {
+                               oauth->scope = g_strdup(oauth->provider->scope);
+                       }
+                       valid_secs = json_object_get_int_member(reply, "expires_in");
+                       if ((oauth->access_token == NULL) || (oauth->refresh_token == NULL) || (oauth->scope 
== NULL) ||
+                               (valid_secs <= 0LL)) {
+                               g_set_error(error, LIBBALSA_OAUTH2_ERROR_QUARK, -1, _("OAuth2 authorization 
request failed: incomplete reply"));
+                               g_debug("%s: malformed json reply for %s/%s", __func__, oauth->account, 
oauth->provider->id);
+                       } else {
+                               oauth->valid_until = time(NULL) + valid_secs - 5;
+                               g_debug("%s: got token for %s/%s", __func__, oauth->account, 
oauth->provider->id);
+                               result = TRUE;
+                       }
+               } else {
+                       g_set_error(error, LIBBALSA_OAUTH2_ERROR_QUARK, -1, _("OAuth2 authorization request 
failed: malformed reply"));
+               }
+       }
+       g_object_unref(parser);
+
+       return result;
+}
+
+
+/** @brief Evaluate the JSON error reply from an OAuth2 server
+ *
+ * @param[in] prefix error message prefix string
+ * @param[in] message message received from the remote OAuth2 server
+ * @param[out] error filled with the error information
+ *
+ * Fill the passed error location with the prefix and the @c error_description item received in the passed 
JSON message, or with
+ * the status code and reason phrase if parsing the JSON body fails.
+ */
+static void
+eval_json_error(const gchar *prefix, const SoupMessage *message, GError **error)
+{
+       gboolean assigned = FALSE;
+
+       if (message->response_body != NULL) {
+               JsonParser *parser;
+
+               parser = json_parser_new();
+               if (json_parser_load_from_data(parser, message->response_body->data, 
message->response_body->length, NULL)) {
+                       JsonNode *root;
+
+                       root = json_parser_get_root(parser);
+                       if (JSON_NODE_HOLDS_OBJECT(root)) {
+                               JsonObject *reply;
+                               const gchar *err_desc;
+
+                               reply = json_node_get_object(root);
+                               err_desc = json_object_get_string_member(reply, "error_description");
+                               if (err_desc != NULL) {
+                                       g_set_error(error, LIBBALSA_OAUTH2_ERROR_QUARK, -1, "%s: %s", prefix, 
err_desc);
+                                       assigned = TRUE;
+                               }
+                       }
+                       g_object_unref(parser);
+               }
+       }
+       if (!assigned) {
+               g_set_error(error, LIBBALSA_OAUTH2_ERROR_QUARK, message->status_code, "%s: %u: %s", prefix, 
message->status_code,
+                       message->reason_phrase);
+       }
+}
+
+
+/* == OAuth2 authorisation related functions 
==================================================================================== */
+
+/** @brief OAuth2 authorisation idle callback data */
+typedef struct {
+    GCond cond;                                                /**< Condition for signalling that the 
authorisation has been finished. */
+    gboolean done;                                     /**< Done flag (see oauth2_auth_idle_t::cond) */
+    LibBalsaOauth2 *oauth;                     /**< OAuth2 context. */
+    GtkWindow *parent;                         /**< Transient parent window for the authorisation dialogue. 
*/
+    gboolean result;                           /**< TRUE if the authorisation process has been successful, 
FALSE if not. */
+    GError **error;                                    /**< Error location for any error if the 
authorisation process failed. */
+} oauth2_auth_idle_t;
+
+
+/** @brief Authorise Balsa for OAuth2
+ *
+ * @param[in] oauth OAuth2 context
+ * @param[in] parent transient parent of the authorisation dialogue
+ * @param[out] error location for error, may be NULL
+ * @return TRUE on success, FALSE if any error occurred
+ *
+ * Authorise Balsa for the passed OAuth2 context.  If this function is not called from the main thread, 
oauth2_authorise_idle() is
+ * scheduled as idle callback to perform the user interaction and authorication process.
+ */
+static gboolean
+oauth2_authorise(LibBalsaOauth2 *oauth, GtkWindow *parent, GError **error)
+{
+       gboolean result;
+
+    if (libbalsa_am_i_subthread()) {
+       static GMutex oauth2_auth_lock;
+       oauth2_auth_idle_t idle_data;
+
+       g_mutex_lock(&oauth2_auth_lock);
+       g_cond_init(&idle_data.cond);
+       idle_data.oauth = g_object_ref(oauth);
+       idle_data.parent = parent;
+       idle_data.error = error;
+       idle_data.done = FALSE;
+       g_idle_add(oauth2_authorise_idle, &idle_data);
+       while (!idle_data.done) {
+               g_cond_wait(&idle_data.cond, &oauth2_auth_lock);
+       }
+       g_cond_clear(&idle_data.cond);
+       g_mutex_unlock(&oauth2_auth_lock);
+       g_object_unref(oauth);
+       result = idle_data.result;
+    } else {
+       result = oauth2_authorise_real(oauth, parent, error);
+    }
+    return result;
+}
+
+
+/** @brief OAuth2 authorisation idle callback
+ *
+ * @param data OAuth2 authorisation idle callback data, cast'ed to @ref oauth2_auth_idle_t *
+ * @return always FALSE
+ *
+ * Simply run oauth2_authorise_real(), set oauth2_auth_idle_t::done and signal the condition 
oauth2_auth_idle_t::cond.
+ */
+static gboolean
+oauth2_authorise_idle(gpointer data)
+{
+       oauth2_auth_idle_t *idle_data = (oauth2_auth_idle_t *) data;
+
+       idle_data->result = oauth2_authorise_real(idle_data->oauth, idle_data->parent, idle_data->error);
+       idle_data->done = TRUE;
+    g_cond_signal(&idle_data->cond);
+       return FALSE;
+}
+
+
+/** \brief Perform the OAuth2 authorisation process for Balsa
+ *
+ * @param[in] oauth OAuth2 context
+ * @param[in] parent transient parent of the authorisation dialogue
+ * @param[out] error location for error, may be NULL
+ * @return TRUE on success, FALSE if any error occurred
+ *
+ * Create an authorisation context, and run the OAuth2 dialogue, guiding the user through the actual 
authorisation process.
+ */
+static gboolean
+oauth2_authorise_real(LibBalsaOauth2 *oauth, GtkWindow *parent, GError **error)
+{
+       oauth2_auth_ctx_t *auth_ctx;
+       gboolean result = FALSE;
+
+       auth_ctx = oauth2_authorize_init(oauth->provider, error);
+       if (auth_ctx != NULL) {
+               auth_ctx->oauth = oauth;
+               result = run_oauth2_dialog(auth_ctx, oauth->provider, oauth->account, parent);
+               if (auth_ctx->server != NULL) {
+                       g_object_unref(auth_ctx->server);
+               }
+               g_free(auth_ctx->listen_uri);
+               g_free(auth_ctx->auth_request_uri);
+               if (auth_ctx->auth_err != NULL) {
+                       g_propagate_error(error, auth_ctx->auth_err);
+               }
+               g_free(auth_ctx);
+       }
+
+       return result;
+}
+
+
+/** @brief Initialise a OAuth2 authorisation context
+ *
+ * @param[in] provider provider data
+ * @param[out] error location for error information may be NULL
+ * @return an initialised OAuth2 authorisation context on success, NULL on error
+ *
+ * Create a new OAuth2 authorisation context, and fill in:
+ * - oauth2_auth_ctx_t::server with a newly created listening soup server, unless 
oauth2_provider_t::oob_mode is set;
+ * - oauth2_auth_ctx_t::listen_uri
+ * - oauth2_auth_ctx_t::auth_request_uri
+ */
+static oauth2_auth_ctx_t *
+oauth2_authorize_init(const oauth2_provider_t *provider, GError **error)
+{
+       oauth2_auth_ctx_t *auth_ctx;
+
+       g_return_val_if_fail(provider != NULL, NULL);
+
+       auth_ctx = g_new0(oauth2_auth_ctx_t, 1U);
+
+       /* create the local listener unless the provider supports OOB mode only */
+       if (!provider->oob_mode) {
+               auth_ctx->server = soup_server_new(SOUP_SERVER_SERVER_HEADER, PACKAGE "/" BALSA_VERSION " ", 
NULL);
+               soup_server_add_handler(auth_ctx->server, NULL, oauth2_listener_cb, auth_ctx, NULL);
+               if (!soup_server_listen_local(auth_ctx->server, 0U, SOUP_SERVER_LISTEN_IPV4_ONLY, error)) {
+                       g_object_unref(auth_ctx->server);
+                       g_free(auth_ctx);
+                       auth_ctx = NULL;
+               } else {
+                       GSList *uris;
+                       SoupURI *uri;
+
+                       /* get the local listener uri */
+                       uris = soup_server_get_uris(auth_ctx->server);
+                       uri = (SoupURI *) uris->data;
+                       auth_ctx->listen_uri = g_strdup_printf("http://%s:%u";, uri->host, uri->port);
+                       g_slist_free_full(uris, (GDestroyNotify) soup_uri_free);
+                       g_debug("%s: listening on %s for auth reply", __func__, auth_ctx->listen_uri);
+               }
+       } else {
+               auth_ctx->listen_uri = g_strdup("oob");
+       }
+
+       if (auth_ctx != NULL) {
+               SoupURI *uri;
+
+               /* calculate the authentication uri */
+               uri = soup_uri_new(provider->auth_uri);
+               if (provider->scope == NULL) {
+                       soup_uri_set_query_from_fields(uri,
+                               "client_id", provider->client_id,
+                               "redirect_uri", auth_ctx->listen_uri,
+                               "response_type", "code",
+                               NULL);
+               } else {
+                       soup_uri_set_query_from_fields(uri,
+                               "client_id", provider->client_id,
+                               "redirect_uri", auth_ctx->listen_uri,
+                               "scope", provider->scope,
+                               "response_type", "code",
+                               NULL);
+               }
+               auth_ctx->auth_request_uri = soup_uri_to_string(uri, FALSE);
+               g_debug("%s: auth request uri %s", __func__, auth_ctx->auth_request_uri);
+               soup_uri_free(uri);
+       }
+
+       return auth_ctx;
+}
+
+
+/** @brief Local listening server callback
+ *
+ * @param[in] server server object, unused
+ * @param[in] msg received message
+ * @param[in] path path component of the received URI
+ * @param[in] query query component of the received URI
+ * @param[in] client additional context information, unused
+ * @param[in] user_data authorisation context, cast'ed to @ref oauth2_auth_ctx_t *
+ *
+ * Evaluate the authorisation message received from the remote server.  The message @em must be directed to 
path "/" and contain
+ * query data, including a "code" component containing the actual authorisation code.  
oauth2_authorize_finish() is called with this
+ * code to get the refresh and access tokens.  The status of the passed message is set to 200 (ok) in this 
case, or to a 4xy error
+ * code otherwise.
+ */
+static void
+oauth2_listener_cb(SoupServer G_GNUC_UNUSED *server, SoupMessage *msg, const char *path, GHashTable *query,
+       SoupClientContext G_GNUC_UNUSED *client, gpointer user_data)
+{
+       oauth2_auth_ctx_t *auth_ctx = (oauth2_auth_ctx_t *) user_data;
+
+       if ((strcmp(path, "/") == 0) && (query != NULL) && (auth_ctx->oauth->access_token == NULL) && 
(auth_ctx->auth_err == NULL)) {
+               gconstpointer code;
+
+               code = g_hash_table_lookup(query, "code");
+               if (code != NULL) {
+                       gchar *reply;
+
+                       g_debug("%s: got authentication code '%s'", __func__, (const gchar *) code);
+                       reply = g_strdup_printf(OAUTH_AUTH_DONE_TEMPLATE, _("Received Code"),
+                               _("The OAuth2 Authorization code for Balsa has been received."), _("You may 
close this window."));
+                       soup_message_set_status(msg, 200);
+                       soup_message_set_response(msg, "text/html", SOUP_MEMORY_TAKE, reply, strlen(reply));
+                       oauth2_authorize_finish(auth_ctx, (const gchar *) code);
+               } else {
+                       g_debug("%s: got request for '%s', but no 'code' parameter", __func__, path);
+                       soup_message_set_status(msg, 403);
+               }
+       } else {
+               g_debug("%s: ignore request for '%s'", __func__, path);
+               soup_message_set_status(msg, 404);
+       }
+}
+
+
+/** @brief Finish authorisation and receive refresh and access tokens
+ *
+ * @param[in] auth_ctx authorisation context
+ * @param[in] auth_code authorisation code
+ *
+ * Post the authorisation code to the provider's authorisation uri, and receive the JSON reply which shall 
contain the refresh and
+ * access tokens.  The reply is parsed by calling eval_json_auth_reply().  Any error is remembered in 
oauth2_auth_ctx_t::auth_err.
+ * Additionally, the function finally stops the spinner oauth2_auth_ctx_t::spinner in the authorisation 
dialogue, and updates the
+ * related label oauth2_auth_ctx_t::spinner_label accordingly.
+ */
+static void
+oauth2_authorize_finish(oauth2_auth_ctx_t *auth_ctx, const gchar *auth_code)
+{
+       const oauth2_provider_t *provider = auth_ctx->oauth->provider;
+       SoupSession *session;
+       SoupMessage *message;
+       guint status;
+
+       gtk_label_set_label(GTK_LABEL(auth_ctx->spinner_label), _("sending authorization code…"));
+       g_debug("%s: uri=%s, client_id=%s, client_secret=%s code=%s listen_uri=%s", __func__, 
provider->token_uri, provider->client_id,
+               provider->client_secret, auth_code, auth_ctx->listen_uri);
+
+       /* send the authorisation code */
+       session = oauth_soup_session_new();
+       message = soup_form_request_new("POST", provider->token_uri,
+               "grant_type", "authorization_code",
+               "client_id", provider->client_id,
+               "client_secret", provider->client_secret,
+               "code", auth_code,
+               "redirect_uri", auth_ctx->listen_uri,
+               NULL);
+       status = soup_session_send_message(session, message);
+       g_debug("%s: status=%u, code=%u, reason=%s", __func__, status, message->status_code, 
message->reason_phrase);
+
+       if ((status != SOUP_STATUS_OK) || (message->status_code != SOUP_STATUS_OK) || (message->response_body 
== NULL)) {
+               eval_json_error(_("OAuth2 authorization request failed"), message, &auth_ctx->auth_err);
+               gtk_label_set_label(GTK_LABEL(auth_ctx->spinner_label), _("send error"));
+       } else {
+               if (eval_json_auth_reply(auth_ctx->oauth, message, TRUE, &auth_ctx->auth_err)) {
+                       gtk_label_set_label(GTK_LABEL(auth_ctx->spinner_label), _("authorization 
successful"));
+                       gtk_dialog_set_response_sensitive(GTK_DIALOG(auth_ctx->auth_dialog), 
GTK_RESPONSE_ACCEPT, TRUE);
+               } else {
+                       gtk_label_set_label(GTK_LABEL(auth_ctx->spinner_label), _("authorization failed"));
+               }
+       }
+       gtk_spinner_stop(GTK_SPINNER(auth_ctx->spinner));
+       g_object_unref(message);
+       g_object_unref(session);
+}
+
+
+/** @brief Authorisation code entry change callback
+ *
+ * @param entry authorisation code entry
+ * @param button related button, enabled iff the entry is not empty
+ */
+static void
+on_code_entry_changed(GtkEntry *entry, GtkWidget *button)
+{
+       gtk_widget_set_sensitive(button, strlen(gtk_entry_get_text(entry)) > 0UL);
+}
+
+
+/** @brief Authorisation code entry accept button callback
+ *
+ * @param button source button, unused
+ * @param auth_ctx authorisation context
+ *
+ * Call oauth2_authorize_finish() with the code entered by the user to finish the authorisation process.
+ */
+static void
+on_code_button_clicked(GtkButton G_GNUC_UNUSED *button, oauth2_auth_ctx_t *auth_ctx)
+{
+       oauth2_authorize_finish(auth_ctx, gtk_entry_get_text(GTK_ENTRY(auth_ctx->code_entry)));
+}
+
+
+/** @brief Run the OAuth2 authorisation dialogue
+ *
+ * @param auth_ctx authorisation context
+ * @param provider provider data
+ * @param account user account (email address)
+ * @param parent transient parent window
+ * @return TRUE on success, FALSE if the user cancelled the process or if any error occurred
+ *
+ * Create and run the OAuth2 authorisation dialogue, guiding the user through the process.
+ */
+static gboolean
+run_oauth2_dialog(oauth2_auth_ctx_t *auth_ctx, const oauth2_provider_t *provider, const gchar *account, 
GtkWindow *parent)
+{
+       GtkWidget *content;
+       GtkWidget *widget;
+       GtkWidget *hbox;
+       gchar *message;
+       gint result;
+
+       auth_ctx->auth_dialog = gtk_dialog_new_with_buttons(_("Authorize Balsa"), parent,
+               GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | libbalsa_dialog_flags(),
+               _("Cancel"), GTK_RESPONSE_REJECT, _("OK"), GTK_RESPONSE_ACCEPT, NULL);
+       gtk_dialog_set_response_sensitive(GTK_DIALOG(auth_ctx->auth_dialog), GTK_RESPONSE_ACCEPT, FALSE);
+
+       content = gtk_dialog_get_content_area(GTK_DIALOG(auth_ctx->auth_dialog));
+       gtk_container_set_border_width(GTK_CONTAINER(content), 12U);
+       gtk_box_set_spacing(GTK_BOX(content), 6U);
+       if (provider->oob_mode) {
+               message = g_strdup_printf(_("Balsa must be authorized to access %s:\n"
+                                                                       "\342\200\242 click the link to open 
the authorization page\n"
+                                                                       "\342\200\242 log in as user %s\n"
+                                                                       "\342\200\242 follow the instructions 
on the web page and\n"
+                                                                       "\342\200\242 enter the authorization 
code."),
+                       provider->display_name, account);
+       } else {
+               message = g_strdup_printf(_("Balsa must be authorized to access %s:\n"
+                                                                   "\342\200\242 click the link to open the 
authorization page\n"
+                                                                       "\342\200\242 log in as user %s and\n"
+                                                                       "\342\200\242 follow the instructions 
on the web page."),
+                       provider->display_name, account);
+       }
+       widget = gtk_label_new(message);
+       g_free(message);
+       gtk_label_set_justify(GTK_LABEL(widget), GTK_JUSTIFY_LEFT);
+       gtk_label_set_xalign(GTK_LABEL(widget), 0.0);
+       gtk_box_pack_start(GTK_BOX(content), widget, FALSE, FALSE, 0U);
+
+       message = g_strdup_printf(_("Authorize Balsa for %s"), provider->display_name);
+       widget = gtk_link_button_new_with_label(auth_ctx->auth_request_uri, message);
+       g_free(message);
+       gtk_widget_grab_focus(widget);
+       gtk_box_pack_start(GTK_BOX(content), widget, FALSE, FALSE, 0U);
+
+       if (provider->oob_mode) {
+               GtkWidget *button;
+
+               hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+               gtk_box_pack_start(GTK_BOX(content), hbox, FALSE, FALSE, 0U);
+               gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("Code:")), FALSE, FALSE, 0U);
+               auth_ctx->code_entry = gtk_entry_new();
+               gtk_box_pack_start(GTK_BOX(hbox), auth_ctx->code_entry, TRUE, TRUE, 0U);
+               button = gtk_button_new_from_icon_name("gtk-ok", GTK_ICON_SIZE_BUTTON);
+               gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0U);
+               g_signal_connect(button, "clicked", G_CALLBACK(on_code_button_clicked), auth_ctx);
+               gtk_widget_set_sensitive(button, FALSE);
+               g_signal_connect(auth_ctx->code_entry, "changed", G_CALLBACK(on_code_entry_changed), button);
+       } else {
+               auth_ctx->code_entry = NULL;
+       }
+
+       hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+       gtk_box_pack_start(GTK_BOX(content), hbox, FALSE, FALSE, 0U);
+       auth_ctx->spinner = gtk_spinner_new();
+       gtk_spinner_start(GTK_SPINNER(auth_ctx->spinner));
+       gtk_box_pack_start(GTK_BOX(hbox), auth_ctx->spinner, FALSE, FALSE, 0U);
+       auth_ctx->spinner_label = gtk_label_new(_("…waiting for authorization code"));
+       gtk_box_pack_start(GTK_BOX(hbox), auth_ctx->spinner_label, FALSE, FALSE, 0U);
+
+       gtk_widget_show_all(auth_ctx->auth_dialog);
+       result = gtk_dialog_run(GTK_DIALOG(auth_ctx->auth_dialog));
+       gtk_widget_destroy(auth_ctx->auth_dialog);
+
+       return (result == GTK_RESPONSE_ACCEPT);
+}
+
+
+/** @brief Create a Soup session with optional debugging
+ *
+ * @return a new SoupSession
+ *
+ * @todo Check the environment variable @c G_MESSAGES_DEBUG for @c oauth instead of using the compile-time 
@ref HTTP_VERBOSE define.
+ */
+static SoupSession *
+oauth_soup_session_new(void)
+{
+#ifdef HTTP_VERBOSE
+       static SoupLogger *logger = NULL;
+#endif
+       SoupSession *session;
+
+       session = soup_session_new();
+       g_object_set(session, "user-agent", PACKAGE "/" BALSA_VERSION " ", NULL);
+
+#ifdef HTTP_VERBOSE
+       if (g_once_init_enter(&logger)) {
+               SoupLogger *_logger = soup_logger_new(SOUP_LOGGER_LOG_BODY, -1);
+               g_once_init_leave(&logger, _logger);
+       }
+       soup_session_add_feature(session, SOUP_SESSION_FEATURE(logger));
+#endif
+
+       return session;
+}
+
+#endif /* defined(HAVE_OAUTH2) */
diff --git a/libbalsa/oauth2.h b/libbalsa/oauth2.h
new file mode 100644
index 000000000..620e9b068
--- /dev/null
+++ b/libbalsa/oauth2.h
@@ -0,0 +1,72 @@
+/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
+/* Balsa E-Mail Client
+ *
+ * Copyright (C) 1997-2020 Stuart Parmenter and others,
+ *                         See the file AUTHORS for a list.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBBALSA_OAUTH2_H_
+#define LIBBALSA_OAUTH2_H_
+
+
+#include "config.h"
+#include "server.h"
+
+
+#if defined(HAVE_OAUTH2)
+
+
+#define LIBBALSA_OAUTH2_ERROR_QUARK                    (g_quark_from_static_string("libbalsa-oauth2"))
+
+
+#define LIBBALSA_OAUTH2_TYPE                           (libbalsa_oauth2_get_type())
+G_DECLARE_FINAL_TYPE(LibBalsaOauth2, libbalsa_oauth2, LIBBALSA, OAUTH2, GObject)
+
+
+/** @brief Check for a provider supporting OAuth2
+ *
+ * @param[in] mailbox email address
+ * @return TRUE if the passed email address belongs to a known provider supporting OAuth2, FALSE if not
+ */
+gboolean libbalsa_oauth2_supported(const gchar *mailbox);
+
+/** @brief Get the OAuth2 context for a POP3, IMAP or SMTP server
+ *
+ * @param[in] server server configuration
+ * @param[out] error location for error, may be NULL
+ * @return the OAuth2 context on success, NULL on error
+ */
+LibBalsaOauth2 *libbalsa_oauth2_new(LibBalsaServer  *server,
+                                                                       GError         **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/** @brief Get the OAuth2 access token
+ *
+ * @param[in] oauth OAuth2 context returned by calling libbalsa_oauth2_new()
+ * @param[in] parent transient parent of the authorisation dialogue
+ * @param[out] error location for error, may be NULL
+ * @return a newly allocated string containing the OAuth2 access token on success, NULL on error
+ */
+gchar *libbalsa_oauth2_token(LibBalsaOauth2  *oauth,
+                                                        GtkWindow       *parent,
+                                                        GError         **error)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+#endif /* defined(HAVE_OAUTH2) */
+
+
+#endif /* LIBBALSA_OAUTH2_H_ */
diff --git a/libbalsa/server.c b/libbalsa/server.c
index f1ffe6bd8..7c8632a13 100644
--- a/libbalsa/server.c
+++ b/libbalsa/server.c
@@ -34,6 +34,7 @@
 #include "libbalsa_private.h"
 #include "libbalsa-conf.h"
 #include "net-client-utils.h"
+#include "oauth2.h"
 #include <glib/gi18n.h>
 
 
@@ -79,6 +80,9 @@ struct _LibBalsaServerPrivate {
     gboolean remember_passwd;
     gboolean remember_cert_passphrase;
     NetClientAuthMode auth_mode;
+#if defined(HAVE_OAUTH2)
+    LibBalsaOauth2 *oauth;
+#endif
 };
 
 static void libbalsa_server_finalize(GObject * object);
@@ -179,6 +183,11 @@ libbalsa_server_finalize(GObject * object)
     g_free(priv->cert_file);
     priv->passwd = libbalsa_free_password(priv->passwd);
     priv->cert_passphrase = libbalsa_free_password(priv->cert_passphrase);
+#if defined(HAVE_OAUTH2)
+    if (priv->oauth != NULL) {
+       g_object_unref(priv->oauth);
+    }
+#endif
 
     G_OBJECT_CLASS(libbalsa_server_parent_class)->finalize(object);
 }
@@ -567,6 +576,7 @@ libbalsa_server_get_auth(NetClient         *client,
     LibBalsaServer *server = LIBBALSA_SERVER(user_data);
     LibBalsaServerPrivate *priv = libbalsa_server_get_instance_private(server);
     gchar **result = NULL;
+    GError *error = NULL;
 
     g_debug("%s: %p %d %p: encrypted = %d", __func__, client, mode, user_data, 
net_client_is_encrypted(client));
     if (priv->auth_mode != NET_CLIENT_AUTH_NONE_ANON) {
@@ -582,8 +592,21 @@ libbalsa_server_get_auth(NetClient         *client,
             break;
         case NET_CLIENT_AUTH_KERBEROS:
                break;                  /* only user name required */
+#if defined(HAVE_OAUTH2)
         case NET_CLIENT_AUTH_OAUTH2:
-               break;                  // FIXME!! get the OAuth2 token in result[1]
+               if (priv->oauth == NULL) {
+                       priv->oauth = libbalsa_oauth2_new(server, &error);
+               }
+               if (priv->oauth != NULL) {
+                       // FIXME - how to pass parent window?
+                       result[1] = libbalsa_oauth2_token(priv->oauth, NULL, &error);
+               }
+               if (error != NULL) {
+                       libbalsa_information(LIBBALSA_INFORMATION_ERROR, _("OAuth error: %s"), 
error->message);
+                       g_clear_error(&error);
+               }
+               break;
+#endif /* defined(HAVE_OAUTH2) */
         default:
                g_assert_not_reached();
         }
diff --git a/libnetclient/net-client-pop.c b/libnetclient/net-client-pop.c
index 883e8bee9..84b1ab1bb 100644
--- a/libnetclient/net-client-pop.c
+++ b/libnetclient/net-client-pop.c
@@ -52,7 +52,7 @@ struct _NetClientPop {
 #define NET_CLIENT_POP_AUTH_CRAM_SHA1          0x020U
 /** RFC 4752 "GSSAPI" authentication method. */
 #define NET_CLIENT_POP_AUTH_GSSAPI                     0x040U
-/** RFC 6749 "XOAUTH2" authentication method. */
+/** RFC 7628 "OAUTHBEARER" authentication method. */
 #define NET_CLIENT_POP_AUTH_OAUTH2                     0x080U
 /** RFC 4505 "ANONYMOUS" authentication method. */
 #define NET_CLIENT_POP_AUTH_ANONYMOUS          0x100U
@@ -252,7 +252,7 @@ net_client_pop_set_auth_mode(NetClientPop *client, NetClientAuthMode auth_mode,
                client->auth_enabled |= NET_CLIENT_POP_AUTH_GSSAPI;
        }
 #endif
-#if defined (HAVE_OAUTH2)
+#if defined(HAVE_OAUTH2)
        if ((auth_mode & NET_CLIENT_AUTH_OAUTH2) != 0U) {
                client->auth_enabled |= NET_CLIENT_POP_AUTH_OAUTH2;
        }
@@ -880,9 +880,9 @@ net_client_pop_auth_oauth2(NetClientPop *client, const gchar *user, const gchar
        gboolean result ;
        gchar *base64_buf;
 
-       base64_buf = net_client_auth_oauth2_calc(user, access_token);
+       base64_buf = net_client_auth_oauth2_calc(user, NET_CLIENT(client), access_token);
        if (base64_buf != NULL) {
-               result = net_client_pop_execute_sasl(client, "AUTH XOAUTH2", NULL, error);
+               result = net_client_pop_execute_sasl(client, "AUTH OAUTHBEARER", NULL, error);
                if (result) {
                        result = net_client_pop_execute(client, "%s", NULL, error, base64_buf);
                        // FIXME - grab the JSON response on error
@@ -981,7 +981,7 @@ net_client_pop_get_capa(NetClientPop *client, guint *auth_supported)
                                                *auth_supported |= NET_CLIENT_POP_AUTH_GSSAPI;
 #endif
 #if defined(HAVE_OAUTH2)
-                                       } else if (strcmp(auth[n], "XOAUTH2") == 0) {
+                                       } else if (strcmp(auth[n], "OAUTHBEARER") == 0) {
                                                *auth_supported |= NET_CLIENT_POP_AUTH_OAUTH2;
 #endif
                                        } else {
diff --git a/libnetclient/net-client-smtp.c b/libnetclient/net-client-smtp.c
index 925803749..6fcacc71e 100644
--- a/libnetclient/net-client-smtp.c
+++ b/libnetclient/net-client-smtp.c
@@ -61,7 +61,7 @@ typedef struct {
 #define NET_CLIENT_SMTP_AUTH_CRAM_SHA1         0x10U
 /** RFC 4752 "GSSAPI" authentication method. */
 #define NET_CLIENT_SMTP_AUTH_GSSAPI                    0x20U
-/** RFC 6749 "XOAUTH2" authentication method. */
+/** RFC 7628 "OAUTHBEARER" authentication method. */
 #define NET_CLIENT_SMTP_AUTH_OAUTH2                    0x40U
 
 /** Mask of all authentication methods requiring user name and password. */
@@ -87,8 +87,9 @@ G_DEFINE_TYPE(NetClientSmtp, net_client_smtp, NET_CLIENT_TYPE)
 static void net_client_smtp_finalise(GObject *object);
 static gboolean net_client_smtp_ehlo(NetClientSmtp *client, guint *auth_supported, gboolean *can_starttls, 
GError **error);
 static gboolean net_client_smtp_starttls(NetClientSmtp *client, GError **error);
-static gboolean net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gchar **last_reply, 
GError **error, ...)
-       G_GNUC_PRINTF(2, 5);
+static gboolean net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gint expect_code, 
gchar **last_reply,
+       GError **error, ...)
+       G_GNUC_PRINTF(2, 6);
 static gboolean net_client_smtp_auth(NetClientSmtp *client, guint auth_supported, GError **error);
 static gboolean net_client_smtp_auth_plain(NetClientSmtp *client, const gchar *user, const gchar *passwd, 
GError **error);
 static gboolean net_client_smtp_auth_login(NetClientSmtp *client, const gchar *user, const gchar *passwd, 
GError **error);
@@ -97,7 +98,7 @@ static gboolean net_client_smtp_auth_cram(NetClientSmtp *client, GChecksumType c
 static gboolean net_client_smtp_auth_gssapi(NetClientSmtp *client, const gchar *user, GError **error);
 static gboolean net_client_smtp_auth_oauth2(NetClientSmtp *client, const gchar *user, const gchar 
*access_token, GError **error);
 static gboolean net_client_smtp_read_reply(NetClientSmtp *client, gint expect_code, gchar **last_reply, 
GError **error);
-static gboolean net_client_smtp_eval_rescode(gint res_code, const gchar *reply, GError **error);
+static gboolean net_client_smtp_eval_rescode(gint res_code, gint expect_code, const gchar *reply, GError 
**error);
 static gchar *net_client_smtp_dsn_to_string(const NetClientSmtp *client, NetClientSmtpDsnMode dsn_mode);
 static void smtp_rcpt_free(smtp_rcpt_t *rcpt);
 
@@ -141,7 +142,7 @@ net_client_smtp_set_auth_mode(NetClientSmtp *client, NetClientAuthMode auth_mode
                client->auth_enabled |= NET_CLIENT_SMTP_AUTH_GSSAPI;
        }
 #endif
-#if defined (HAVE_OAUTH2)
+#if defined(HAVE_OAUTH2)
        if ((auth_mode & NET_CLIENT_AUTH_OAUTH2) != 0U) {
                client->auth_enabled |= NET_CLIENT_SMTP_AUTH_OAUTH2;
        }
@@ -268,7 +269,7 @@ net_client_smtp_connect(NetClientSmtp *client, gchar **greeting, GError **error)
                result = net_client_start_tls(NET_CLIENT(client), error);
        }
 
-       /* get the greeting */
+       /* get the greeting, RFC 5321 requires status 220 */
        if (result) {
                (void) net_client_set_timeout(NET_CLIENT(client), 5U * 60U);    /* RFC 5321, Sect. 
4.5.3.2.1.: 5 minutes timeout */
                result = net_client_smtp_read_reply(client, 220, greeting, error);
@@ -323,19 +324,19 @@ net_client_smtp_send_msg(NetClientSmtp *client, const NetClientSmtpMessage *mess
        g_return_val_if_fail(NET_IS_CLIENT_SMTP(client) && (message != NULL) && (message->sender != NULL) &&
                (message->recipients != NULL) && (message->data_callback != NULL), FALSE);
 
-       /* set the RFC 5321 sender and recipient(s) */
+       /* set the RFC 5321 sender and recipient(s); Sect. 3.3 requires status 250 */
        netclient = NET_CLIENT(client);         /* convenience pointer */
        (void) net_client_set_timeout(netclient, 5U * 60U);     /* RFC 5321, Sect. 4.5.3.2.2., 4.5.3.2.3.: 5 
minutes timeout */
        if (client->can_dsn && message->have_dsn_rcpt) {
                if (message->dsn_envid != NULL) {
-                       result = net_client_smtp_execute(client, "MAIL FROM:<%s> RET=%s ENVID=%s", NULL, 
error, message->sender,
+                       result = net_client_smtp_execute(client, "MAIL FROM:<%s> RET=%s ENVID=%s", 250, NULL, 
error, message->sender,
                                                                                         
(message->dsn_ret_full) ? "FULL" : "HDRS", message->dsn_envid);
                } else {
-                       result = net_client_smtp_execute(client, "MAIL FROM:<%s> RET=%s", NULL, error, 
message->sender,
+                       result = net_client_smtp_execute(client, "MAIL FROM:<%s> RET=%s", 250, NULL, error, 
message->sender,
                                                                                         
(message->dsn_ret_full) ? "FULL" : "HDRS");
                }
        } else {
-               result = net_client_smtp_execute(client, "MAIL FROM:<%s>", NULL, error, message->sender);
+               result = net_client_smtp_execute(client, "MAIL FROM:<%s>", 250, NULL, error, message->sender);
        }
        rcpt = message->recipients;
        while (result && (rcpt != NULL)) {
@@ -344,15 +345,15 @@ net_client_smtp_send_msg(NetClientSmtp *client, const NetClientSmtpMessage *mess
 
                /* create the RFC 3461 DSN string */
                dsn_opts = net_client_smtp_dsn_to_string(client, this_rcpt->dsn_mode);
-               result = net_client_smtp_execute(client, "RCPT TO:<%s>%s", NULL, error, 
this_rcpt->rfc5321_addr, dsn_opts);
+               result = net_client_smtp_execute(client, "RCPT TO:<%s>%s", 250, NULL, error, 
this_rcpt->rfc5321_addr, dsn_opts);
                g_free(dsn_opts);
                rcpt = rcpt->next;
        }
 
-       /* initialise sending the message data */
+       /* initialise sending the message data; Sect. 3.3 requires status 354 */
        if (result) {
                (void) net_client_set_timeout(netclient, 2U * 60U);     /* RFC 5321, Sect. 4.5.3.2.4.: 2 
minutes timeout */
-               result = net_client_smtp_execute(client, "DATA", NULL, error);
+               result = net_client_smtp_execute(client, "DATA", 354, NULL, error);
        }
 
        /* call the data callback until all data has been transmitted or an error occurs */
@@ -383,7 +384,7 @@ net_client_smtp_send_msg(NetClientSmtp *client, const NetClientSmtpMessage *mess
 
        if (result) {
                (void) net_client_set_timeout(netclient, 10U * 60U);    /* RFC 5321, Sect 4.5.3.2.6.: 10 
minutes timeout */
-               result = net_client_smtp_read_reply(client, -1, server_stat, error);
+               result = net_client_smtp_read_reply(client, -1, server_stat, error);    // FIXME - expect 250
                client->data_state = FALSE;
        }
 
@@ -501,7 +502,8 @@ net_client_smtp_starttls(NetClientSmtp *client, GError **error)
 {
        gboolean result;
 
-       result = net_client_smtp_execute(client, "STARTTLS", NULL, error);
+       /* RFC 3207, Sect. 4 requires status 220 */
+       result = net_client_smtp_execute(client, "STARTTLS", 220, NULL, error);
        if (result) {
                result = net_client_start_tls(NET_CLIENT(client), error);
        }
@@ -584,7 +586,8 @@ net_client_smtp_auth_plain(NetClientSmtp *client, const gchar *user, const gchar
 
        base64_buf = net_client_auth_plain_calc(user, passwd);
        if (base64_buf != NULL) {
-               result = net_client_smtp_execute(client, "AUTH PLAIN %s", NULL, error, base64_buf);
+               /* RFC 4954, Sect. 6 requires status 235 */
+               result = net_client_smtp_execute(client, "AUTH PLAIN %s", 235, NULL, error, base64_buf);
                net_client_free_authstr(base64_buf);
        } else {
                result = FALSE;
@@ -600,12 +603,13 @@ net_client_smtp_auth_login(NetClientSmtp *client, const gchar *user, const gchar
        gboolean result;
        gchar *base64_buf;
 
+       /* RFC 4954, Sect. 4 requires status 334 for the challenge; Sect. 6 requires status 235 */
        base64_buf = g_base64_encode((const guchar *) user, strlen(user));
-       result = net_client_smtp_execute(client, "AUTH LOGIN %s", NULL, error, base64_buf);
+       result = net_client_smtp_execute(client, "AUTH LOGIN %s", 334, NULL, error, base64_buf);
        net_client_free_authstr(base64_buf);
        if (result) {
                base64_buf = g_base64_encode((const guchar *) passwd, strlen(passwd));
-               result = net_client_smtp_execute(client, "%s", NULL, error, base64_buf);
+               result = net_client_smtp_execute(client, "%s", 235, NULL, error, base64_buf);
                net_client_free_authstr(base64_buf);
        }
 
@@ -619,13 +623,14 @@ net_client_smtp_auth_cram(NetClientSmtp *client, GChecksumType chksum_type, cons
        gboolean result;
        gchar *challenge = NULL;
 
-       result = net_client_smtp_execute(client, "AUTH CRAM-%s", &challenge, error, 
net_client_chksum_to_str(chksum_type));
+       /* RFC 4954, Sect. 4 requires status 334 for the challenge; Sect. 6 requires status 235 */
+       result = net_client_smtp_execute(client, "AUTH CRAM-%s", 334, &challenge, error, 
net_client_chksum_to_str(chksum_type));
        if (result) {
                gchar *auth_buf;
 
                auth_buf = net_client_cram_calc(challenge, chksum_type, user, passwd);
                if (auth_buf != NULL) {
-                       result = net_client_smtp_execute(client, "%s", NULL, error, auth_buf);
+                       result = net_client_smtp_execute(client, "%s", 235, NULL, error, auth_buf);
                        net_client_free_authstr(auth_buf);
                } else {
                        result = FALSE;
@@ -645,6 +650,7 @@ net_client_smtp_auth_gssapi(NetClientSmtp *client, const gchar *user, GError **e
        NetClientGssCtx *gss_ctx;
        gboolean result = FALSE;
 
+       /* RFC 4954, Sect. 4 requires status 334 for the challenges; Sect. 6 requires status 235 */
        gss_ctx = net_client_gss_ctx_new("smtp", net_client_get_host(NET_CLIENT(client)), user, error);
        if (gss_ctx != NULL) {
                gint state;
@@ -658,10 +664,10 @@ net_client_smtp_auth_gssapi(NetClientSmtp *client, const gchar *user, GError **e
                        input_token = NULL;
                        if (state >= 0) {
                                if (initial) {
-                                       result = net_client_smtp_execute(client, "AUTH GSSAPI %s", 
&input_token, error, output_token);
+                                       result = net_client_smtp_execute(client, "AUTH GSSAPI %s", 334, 
&input_token, error, output_token);
                                        initial = FALSE;
                                } else {
-                                       result = net_client_smtp_execute(client, "%s", &input_token, error, 
output_token);
+                                       result = net_client_smtp_execute(client, "%s", 334, &input_token, 
error, output_token);
                                }
                        }
                        g_free(output_token);
@@ -670,7 +676,7 @@ net_client_smtp_auth_gssapi(NetClientSmtp *client, const gchar *user, GError **e
                if (state == 1) {
                        output_token = net_client_gss_auth_finish(gss_ctx, input_token, error);
                        if (output_token != NULL) {
-                           result = net_client_smtp_execute(client, "%s", NULL, error, output_token);
+                           result = net_client_smtp_execute(client, "%s", 235, NULL, error, output_token);
                            g_free(output_token);
                        }
                }
@@ -702,10 +708,11 @@ net_client_smtp_auth_oauth2(NetClientSmtp *client, const gchar *user, const gcha
        gboolean result;
        gchar *base64_buf;
 
-       base64_buf = net_client_auth_oauth2_calc(user, access_token);
+       base64_buf = net_client_auth_oauth2_calc(user, NET_CLIENT(client), access_token);
        if (base64_buf != NULL) {
-               result = net_client_smtp_execute(client, "AUTH XOAUTH2 %s", NULL, error, base64_buf);
-               // FIXME - grab the JSON response on error
+               /* RFC 4954, Sect. 6 requires status 235 */
+               result = net_client_smtp_execute(client, "AUTH OAUTHBEARER %s", 235, NULL, error, base64_buf);
+               // FIXME - grab the JSON response on error?
                net_client_free_authstr(base64_buf);
        } else {
                result = FALSE;
@@ -730,7 +737,7 @@ net_client_smtp_auth_oauth2(NetClientSmtp G_GNUC_UNUSED *client, const gchar G_G
 
 /* note: if supplied, last_reply is never NULL on success */
 static gboolean
-net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gchar **last_reply, GError **error, 
...)
+net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gint expect_code, gchar 
**last_reply, GError **error, ...)
 {
        va_list args;
        gboolean result;
@@ -740,7 +747,7 @@ net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gchar *
        va_end(args);
 
        if (result) {
-               result = net_client_smtp_read_reply(client, -1, last_reply, error);
+               result = net_client_smtp_read_reply(client, expect_code, last_reply, error);
        }
 
        return result;
@@ -799,7 +806,7 @@ net_client_smtp_ehlo(NetClientSmtp *client, guint *auth_supported, gboolean *can
                                                        *auth_supported |= NET_CLIENT_SMTP_AUTH_GSSAPI;
 #endif
 #if defined (HAVE_OAUTH2)
-                                               } else if (strcmp(auth[n], "XOAUTH2") == 0) {
+                                               } else if (strcmp(auth[n], "OAUTHBEARER") == 0) {
                                                        *auth_supported |= NET_CLIENT_SMTP_AUTH_OAUTH2;
 #endif
                                                } else {
@@ -828,55 +835,67 @@ net_client_smtp_ehlo(NetClientSmtp *client, guint *auth_supported, gboolean *can
 static gboolean
 net_client_smtp_read_reply(NetClientSmtp *client, gint expect_code, gchar **last_reply, GError **error)
 {
-       gint rescode;
        gboolean done;
        gboolean result;
 
        done = FALSE;
-       rescode = expect_code;
        do {
                gchar *reply;
+               GError *this_error = NULL;
 
-               result = net_client_read_line(NET_CLIENT(client), &reply, error);
+               result = net_client_read_line(NET_CLIENT(client), &reply, &this_error);
                if (result) {
                        gint this_rescode;
-                       gchar *endptr;
 
-                       this_rescode = strtol(reply, &endptr, 10);
-                       if (rescode == -1) {
-                               rescode = this_rescode;
-                               result = net_client_smtp_eval_rescode(rescode, reply, error);
-                       } else if (rescode != this_rescode) {
-                               g_set_error(error, NET_CLIENT_SMTP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_SMTP_PROTOCOL,
-                                       _("bad server reply: %s"), reply);
-                               result = FALSE;
-                       } else {
-                               /* nothing to do (see MISRA C:2012, Rule 15.7) */
+                       this_rescode = strtol(reply, NULL, 10);
+                       result = net_client_smtp_eval_rescode(this_rescode, expect_code, reply, &this_error);
+
+                       if (!result) {
+                               if ((error != NULL) && (*error != NULL)) {
+                                       g_prefix_error(&this_error, "%s ", (*error)->message);
+                                       g_clear_error(error);
+                               }
+                               g_propagate_error(error, this_error);
                        }
+
+                       if (expect_code == -1) {
+                               expect_code = this_rescode;
+                       }
+
                        if (reply[3] == ' ') {
                                done = TRUE;
                                if (last_reply != NULL) {
                                        *last_reply = g_strdup(&reply[4]);
                                }
                        }
-
-                       g_free(reply);
+               } else {
+                       g_clear_error(error);
+                       g_propagate_error(error, this_error);
+                       done = TRUE;
                }
-       } while (result && !done);
+
+               g_free(reply);
+       } while (!done);
 
        return result;
 }
 
 
 static gboolean
-net_client_smtp_eval_rescode(gint res_code, const gchar *reply, GError **error)
+net_client_smtp_eval_rescode(gint res_code, gint expect_code, const gchar *reply, GError **error)
 {
        gboolean result;
 
        switch (res_code / 100) {
        case 2:
        case 3:
-               result = TRUE;
+               if ((expect_code == -1) || (res_code == expect_code)) {
+                       result = TRUE;
+               } else {
+                       g_set_error(error, NET_CLIENT_SMTP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_SMTP_TRANSIENT,
+                               _("unexpected reply: %s"), reply);
+                       result = FALSE;
+               }
                break;
        case 4:
                g_set_error(error, NET_CLIENT_SMTP_ERROR_QUARK, (gint) NET_CLIENT_ERROR_SMTP_TRANSIENT,
diff --git a/libnetclient/net-client-utils.c b/libnetclient/net-client-utils.c
index 0781956f0..d34c4ba20 100644
--- a/libnetclient/net-client-utils.c
+++ b/libnetclient/net-client-utils.c
@@ -375,13 +375,17 @@ gss_error_string(OM_uint32 err_maj, OM_uint32 err_min)
 #if defined(HAVE_OAUTH2)
 
 gchar *
-net_client_auth_oauth2_calc(const gchar *user, const gchar *access_token)
+net_client_auth_oauth2_calc(const gchar *user, NetClient *client, const gchar *access_token)
 {
        gchar *buffer;
        gchar *result;
+       GNetworkAddress *remote_address;
 
-       g_return_val_if_fail((user != NULL) && (access_token != NULL), NULL);
-       buffer = g_strdup_printf("user=%s\001auth=Bearer %s\001\001", user, access_token);
+       g_return_val_if_fail((user != NULL) && (client != NULL) && (access_token != NULL), NULL);
+       remote_address = net_client_get_remote_address(client);
+       g_return_val_if_fail(remote_address != NULL, NULL);
+       buffer = g_strdup_printf("n,a=%s,\001host=%s\001port=%hu\001auth=Bearer %s\001\001", user,
+               g_network_address_get_hostname(remote_address), g_network_address_get_port(remote_address), 
access_token);
        result = g_base64_encode(buffer, strlen(buffer));
        g_free(buffer);
        return result;
diff --git a/libnetclient/net-client-utils.h b/libnetclient/net-client-utils.h
index 4ca69f461..0db4333fb 100644
--- a/libnetclient/net-client-utils.h
+++ b/libnetclient/net-client-utils.h
@@ -183,15 +183,14 @@ void net_client_gss_ctx_free(NetClientGssCtx *gss_ctx);
 /** @brief Calculate a OAuth2 authentication string
  *
  * @param user user name
+ * @param client network client
  * @param access_token access token
  * @return a newly allocated string containing the base64-encoded authentication
  *
- * This helper function calculates the the base64-encoded authentication string from the user name and the 
access token.  The caller
- * shall free the returned string when it is not needed any more.
- *
- * \sa <a href="https://developers.google.com/gmail/imap/xoauth2-protocol";>Google Developers: OAuth 2.0 
Mechanism</a>.
+ * This helper function calculates the the base64-encoded authentication string from the user name, host and 
port, and the access
+ * token according to RFC 7628.  The caller shall free the returned string when it is not needed any more.
  */
-gchar *net_client_auth_oauth2_calc(const gchar *user, const gchar *access_token)
+gchar *net_client_auth_oauth2_calc(const gchar *user, NetClient *client, const gchar *access_token)
        G_GNUC_MALLOC;
 
 #endif         /* HAVE_OAUTH2 */
diff --git a/libnetclient/net-client.c b/libnetclient/net-client.c
index fae535bd1..bf92ffd23 100644
--- a/libnetclient/net-client.c
+++ b/libnetclient/net-client.c
@@ -132,6 +132,24 @@ net_client_get_host(NetClient *client)
 }
 
 
+GNetworkAddress *
+net_client_get_remote_address(NetClient *client)
+{
+       GNetworkAddress *result;
+
+       if (NET_IS_CLIENT(client)) {
+               const NetClientPrivate *priv;
+
+               /*lint -e{9079}         (MISRA C:2012 Rule 11.5) intended use of this function */
+               priv = net_client_get_instance_private(client);
+               result = G_NETWORK_ADDRESS(priv->remote_address);
+       } else {
+               result = NULL;
+       }
+       return result;
+}
+
+
 gboolean
 net_client_connect(NetClient *client, GError **error)
 {
diff --git a/libnetclient/net-client.h b/libnetclient/net-client.h
index 68392aaa8..299a757a6 100644
--- a/libnetclient/net-client.h
+++ b/libnetclient/net-client.h
@@ -55,7 +55,7 @@ enum _NetClientAuthMode {
        NET_CLIENT_AUTH_NONE_ANON = 1,                  /**< No authentication (SMTP); anonymous 
authentication (RFC 4505 for POP3, IMAP). */
        NET_CLIENT_AUTH_USER_PASS = 2,                  /**< Authenticate with user name and password. */
        NET_CLIENT_AUTH_KERBEROS = 4,                   /**< Authenticate with user name and Kerberos ticket. 
*/
-       NET_CLIENT_AUTH_OAUTH2 = 8                              /**< OAuth2 authentication (RFC 6749). */
+       NET_CLIENT_AUTH_OAUTH2 = 8                              /**< OAUTHBEARER authentication (RFC 7628). */
 };
 
 
@@ -113,6 +113,16 @@ gboolean net_client_configure(NetClient *client, const gchar *host_and_port, gui
 const gchar *net_client_get_host(NetClient *client);
 
 
+/** @brief Get the remote address of a connected network client
+ *
+ * @param client network client
+ * @return the remote address, or NULL if the client is not connected or on error
+ *
+ * @note The returned value is part of the passed network client object and shall not be modified or 
unref'ed.
+ */
+GNetworkAddress *net_client_get_remote_address(NetClient *client);
+
+
 /** @brief Connect a network client
  *
  * @param client network client


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]