[evolution] Prototype an online-accounts module.



commit dd6e64ad305e1ab181c6276ed4c77d910a36a4fa
Author: Matthew Barnes <mbarnes redhat com>
Date:   Thu Jun 9 16:28:36 2011 -0400

    Prototype an online-accounts module.
    
    Integrates with the GNOME Online Accounts service.
    
    Creates Evolution sources for a GOA Google account and keeps them
    synchronized.  Also registers a new CamelSaslXOAuth class for use
    with GMail.
    
    Authentication of Google Calendars and Google Contacts using OAuth
    is still under development.

 configure.ac                                       |   22 +-
 modules/Makefile.am                                |    5 +
 modules/online-accounts/Makefile.am                |   32 ++
 modules/online-accounts/camel-sasl-xoauth.c        |  504 +++++++++++++++++++
 modules/online-accounts/camel-sasl-xoauth.h        |   63 +++
 modules/online-accounts/e-online-accounts-google.c |  455 +++++++++++++++++
 modules/online-accounts/e-online-accounts-google.h |   32 ++
 .../online-accounts/evolution-online-accounts.c    |  520 ++++++++++++++++++++
 8 files changed, 1632 insertions(+), 1 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 903e50e..d3bcd50 100644
--- a/configure.ac
+++ b/configure.ac
@@ -54,6 +54,7 @@ m4_define([champlain_minimum_version], [0.10])
 m4_define([clutter_gtk_minimum_version], [0.90])
 m4_define([geoclue_minimum_version], [0.11.1])
 m4_define([gladeui_minimum_version], [3.10.0])
+m4_define([goa_minimum_version], [3.1.1])
 m4_define([gweather_minimum_version], [2.90.0])
 m4_define([libcanberra_gtk_minimum_version], [0.25])
 m4_define([nm_minimum_version], [0.7])
@@ -1085,6 +1086,23 @@ else
 fi
 AC_SUBST(TNEF_CFLAGS)
 
+dnl *******************************
+dnl Check for GNOME Online Accounts
+dnl *******************************
+AC_ARG_ENABLE([goa],
+	[AS_HELP_STRING([--enable-goa],
+	[enable GNOME Online Accounts support (default=yes)])],
+	[enable_goa=$enableval], [enable_goa=yes])
+AC_MSG_CHECKING([if GNOME Online Accounts support is enabled])
+AC_MSG_RESULT([$enable_goa])
+if test "x$enable_goa" = xyes; then
+	PKG_CHECK_MODULES([GOA], [goa-1.0 >= goa_minimum_version],,
+	[AC_MSG_ERROR([goa-1.0 not found (or version < goa_minimum_version),
+	If you want to disable GNOME Online Accounts support,
+	please append --disable-goa to configure.])])
+fi
+AM_CONDITIONAL([ENABLE_ONLINE_ACCOUNTS], [test "x$enable_goa" = xyes])
+
 dnl ******************************
 dnl Check for NetworkManager
 dnl ******************************
@@ -1732,6 +1750,7 @@ modules/composer-autosave/Makefile
 modules/connman/Makefile
 modules/mailto-handler/Makefile
 modules/network-manager/Makefile
+modules/online-accounts/Makefile
 modules/offline-alert/Makefile
 modules/plugin-lib/Makefile
 modules/plugin-manager/Makefile
@@ -1801,8 +1820,9 @@ echo "
 	LDAP support:		$msg_ldap
 	Clutter support:	$with_clutter
 	NetworkManager:		$enable_nm
-	Windows SENS:           $enable_sens
+	Windows SENS:		$enable_sens
 	ConnMan:		$enable_connman
+	GNOME Online Accounts:	$enable_goa
 	Libnotify:		$HAVE_LIBNOTIFY
 	Kerberos 5:		$msg_krb5
 	SSL support:		$msg_ssl
diff --git a/modules/Makefile.am b/modules/Makefile.am
index ae95614..9a32c72 100644
--- a/modules/Makefile.am
+++ b/modules/Makefile.am
@@ -6,6 +6,10 @@ if ENABLE_PYTHON
 PYTHON_DIR = plugin-python
 endif
 
+if ENABLE_ONLINE_ACCOUNTS
+ONLINE_ACCOUNTS_DIR = online-accounts
+endif
+
 if ENABLE_NETWORK_MANAGER
 NETWORK_MANAGER_DIR = network-manager
 endif
@@ -30,6 +34,7 @@ SUBDIRS = \
 	startup-wizard \
 	$(MONO_DIR) \
 	$(PYTHON_DIR) \
+	$(ONLINE_ACCOUNTS_DIR) \
 	$(NETWORK_MANAGER_DIR) \
 	$(WINDOWS_SENS_DIR) \
 	$(CONNMAN_DIR)
diff --git a/modules/online-accounts/Makefile.am b/modules/online-accounts/Makefile.am
new file mode 100644
index 0000000..a5dfd97
--- /dev/null
+++ b/modules/online-accounts/Makefile.am
@@ -0,0 +1,32 @@
+module_LTLIBRARIES = libevolution-module-online-accounts.la
+
+libevolution_module_online_accounts_la_CPPFLAGS =		\
+	$(AM_CPPFLAGS)						\
+	-I$(top_srcdir)						\
+	-DG_LOG_DOMAIN=\"evolution-online-accounts\"		\
+	$(GNOME_PLATFORM_CFLAGS)				\
+	$(EVOLUTION_ADDRESSBOOK_CFLAGS)				\
+	$(EVOLUTION_CALENDAR_CFLAGS)				\
+	$(EVOLUTION_MAIL_CFLAGS)				\
+	$(GOA_CFLAGS)
+
+libevolution_module_online_accounts_la_SOURCES =		\
+	evolution-online-accounts.c				\
+	e-online-accounts-google.c				\
+	e-online-accounts-google.h				\
+	camel-sasl-xoauth.c					\
+	camel-sasl-xoauth.h
+
+libevolution_module_online_accounts_la_LIBADD =			\
+	$(top_builddir)/e-util/libeutil.la			\
+	$(top_builddir)/shell/libeshell.la			\
+	$(GNOME_PLATFORM_LIBS)					\
+	$(EVOLUTION_ADDRESSBOOK_LIBS)				\
+	$(EVOLUTION_CALENDAR_LIBS)				\
+	$(EVOLUTION_MAIL_LIBS)					\
+	$(GOA_LIBS)
+
+libevolution_module_online_accounts_la_LDFLAGS =		\
+	-module -avoid-version $(NO_UNDEFINED)
+
+-include $(top_srcdir)/git.mk
diff --git a/modules/online-accounts/camel-sasl-xoauth.c b/modules/online-accounts/camel-sasl-xoauth.c
new file mode 100644
index 0000000..9ce3912
--- /dev/null
+++ b/modules/online-accounts/camel-sasl-xoauth.c
@@ -0,0 +1,504 @@
+/*
+ * camel-sasl-xoauth.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+#include "camel-sasl-xoauth.h"
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <goa/goa.h>
+
+#define CAMEL_SASL_XOAUTH_GET_PRIVATE(obj) \
+	(G_TYPE_INSTANCE_GET_PRIVATE \
+	((obj), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuthPrivate))
+
+/* This is the property name or URL parameter under which we
+ * embed the GoaAccount ID into an EAccount or ESource object. */
+#define GOA_KEY "goa-account-id"
+
+struct _CamelSaslXOAuthPrivate {
+	gint placeholder;
+};
+
+G_DEFINE_DYNAMIC_TYPE (CamelSaslXOAuth, camel_sasl_xoauth, CAMEL_TYPE_SASL)
+
+/*****************************************************************************
+ * This is based on an old revision of gnome-online-accounts
+ * which demonstrated OAuth authentication with an IMAP server.
+ *
+ * See commit 5bcbe2a3eac4821892680e0655b27ab8c128ab15
+ *****************************************************************************/
+
+#include <libsoup/soup.h>
+
+#define OAUTH_ENCODE_STRING(str) \
+	(str ? soup_uri_encode ((str), "!$&'()*+,;=@") : g_strdup (""))
+
+#define SHA1_BLOCK_SIZE 64
+#define SHA1_LENGTH 20
+
+/*
+ * hmac_sha1:
+ * @key: The key
+ * @message: The message
+ *
+ * Given the key and message, compute the HMAC-SHA1 hash and return the base-64
+ * encoding of it.  This is very geared towards OAuth, and as such both key and
+ * message must be NULL-terminated strings, and the result is base-64 encoded.
+ */
+static gchar *
+hmac_sha1 (const gchar *key,
+           const gchar *message)
+{
+	GChecksum *checksum;
+	gchar *real_key;
+	guchar ipad[SHA1_BLOCK_SIZE];
+	guchar opad[SHA1_BLOCK_SIZE];
+	guchar inner[SHA1_LENGTH];
+	guchar digest[SHA1_LENGTH];
+	gsize key_length, inner_length, digest_length;
+	int i;
+
+	g_return_val_if_fail (key, NULL);
+	g_return_val_if_fail (message, NULL);
+
+	checksum = g_checksum_new (G_CHECKSUM_SHA1);
+
+	/* If the key is longer than the block size, hash it first */
+	if (strlen (key) > SHA1_BLOCK_SIZE) {
+		guchar new_key[SHA1_LENGTH];
+
+		key_length = sizeof (new_key);
+
+		g_checksum_update (checksum, (guchar*)key, strlen (key));
+		g_checksum_get_digest (checksum, new_key, &key_length);
+		g_checksum_reset (checksum);
+
+		real_key = g_memdup (new_key, key_length);
+	} else {
+		real_key = g_strdup (key);
+		key_length = strlen (key);
+	}
+
+	/* Sanity check the length */
+	g_assert (key_length <= SHA1_BLOCK_SIZE);
+
+	/* Protect against use of the provided key by NULLing it */
+	key = NULL;
+
+	/* Stage 1 */
+	memset (ipad, 0, sizeof (ipad));
+	memset (opad, 0, sizeof (opad));
+
+	memcpy (ipad, real_key, key_length);
+	memcpy (opad, real_key, key_length);
+
+	/* Stage 2 and 5 */
+	for (i = 0; i < sizeof (ipad); i++) {
+		ipad[i] ^= 0x36;
+		opad[i] ^= 0x5C;
+	}
+
+	/* Stage 3 and 4 */
+	g_checksum_update (checksum, ipad, sizeof (ipad));
+	g_checksum_update (checksum, (guchar*)message, strlen (message));
+	inner_length = sizeof (inner);
+	g_checksum_get_digest (checksum, inner, &inner_length);
+	g_checksum_reset (checksum);
+
+	/* Stage 6 and 7 */
+	g_checksum_update (checksum, opad, sizeof (opad));
+	g_checksum_update (checksum, inner, inner_length);
+
+	digest_length = sizeof (digest);
+	g_checksum_get_digest (checksum, digest, &digest_length);
+
+	g_checksum_free (checksum);
+	g_free (real_key);
+
+	return g_base64_encode (digest, digest_length);
+}
+
+static char *
+sign_plaintext (const gchar *consumer_secret,
+                const gchar *token_secret)
+{
+	gchar *cs;
+	gchar *ts;
+	gchar *rv;
+
+	cs = OAUTH_ENCODE_STRING (consumer_secret);
+	ts = OAUTH_ENCODE_STRING (token_secret);
+	rv = g_strconcat (cs, "&", ts, NULL);
+
+	g_free (cs);
+	g_free (ts);
+
+	return rv;
+}
+
+static char *
+sign_hmac (const gchar *consumer_secret,
+           const gchar *token_secret,
+           const gchar *http_method,
+           const gchar *request_uri,
+           const gchar *encoded_params)
+{
+	GString *text;
+	gchar *signature;
+	gchar *key;
+
+	text = g_string_new (NULL);
+	g_string_append (text, http_method);
+	g_string_append_c (text, '&');
+	g_string_append_uri_escaped (text, request_uri, NULL, FALSE);
+	g_string_append_c (text, '&');
+	g_string_append_uri_escaped (text, encoded_params, NULL, FALSE);
+
+	/* PLAINTEXT signature value is the HMAC-SHA1 key value */
+	key = sign_plaintext (consumer_secret, token_secret);
+	signature = hmac_sha1 (key, text->str);
+	g_free (key);
+
+	g_string_free (text, TRUE);
+
+	return signature;
+}
+
+static GHashTable *
+calculate_xoauth_params (const gchar *request_uri,
+                         const gchar *consumer_key,
+                         const gchar *consumer_secret,
+                         const gchar *access_token,
+                         const gchar *access_token_secret)
+{
+	gchar *signature;
+	GHashTable *params;
+	gchar *nonce;
+	gchar *timestamp;
+	GList *keys;
+	GList *iter;
+	GString *normalized;
+	gpointer key;
+
+	nonce = g_strdup_printf ("%u", g_random_int ());
+	timestamp = g_strdup_printf (
+		"%" G_GINT64_FORMAT, (gint64) time (NULL));
+
+	params = g_hash_table_new_full (
+		(GHashFunc) g_str_hash,
+		(GEqualFunc) g_str_equal,
+		(GDestroyNotify) NULL,
+		(GDestroyNotify) g_free);
+
+	key = (gpointer) "oauth_consumer_key";
+	g_hash_table_insert (params, key, g_strdup (consumer_key));
+
+	key = (gpointer) "oauth_nonce";
+	g_hash_table_insert (params, key, nonce); /* takes ownership */
+
+	key = (gpointer) "oauth_timestamp";
+	g_hash_table_insert (params, key, timestamp); /* takes ownership */
+
+	key = (gpointer) "oauth_version";
+	g_hash_table_insert (params, key, g_strdup ("1.0"));
+
+	key = (gpointer) "oauth_signature_method";
+	g_hash_table_insert (params, key, g_strdup ("HMAC-SHA1"));
+
+	key = (gpointer) "oauth_token";
+	g_hash_table_insert (params, key, g_strdup (access_token));
+
+	normalized = g_string_new (NULL);
+	keys = g_hash_table_get_keys (params);
+	keys = g_list_sort (keys, (GCompareFunc) g_strcmp0);
+	for (iter = keys; iter != NULL; iter = iter->next) {
+		const gchar *key = iter->data;
+		const gchar *value;
+		gchar *k;
+		gchar *v;
+
+		value = g_hash_table_lookup (params, key);
+		if (normalized->len > 0)
+			g_string_append_c (normalized, '&');
+
+		k = OAUTH_ENCODE_STRING (key);
+		v = OAUTH_ENCODE_STRING (value);
+
+		g_string_append_printf (normalized, "%s=%s", k, v);
+
+		g_free (k);
+		g_free (v);
+	}
+	g_list_free (keys);
+
+	signature = sign_hmac (
+		consumer_secret, access_token_secret,
+		"GET", request_uri, normalized->str);
+
+	key = (gpointer) "oauth_signature";
+	g_hash_table_insert (params, key, signature); /* takes ownership */
+
+	g_string_free (normalized, TRUE);
+
+	return params;
+}
+
+static gchar *
+calculate_xoauth_param (const gchar *request_uri,
+                        const gchar *consumer_key,
+                        const gchar *consumer_secret,
+                        const gchar *access_token,
+                        const gchar *access_token_secret)
+{
+	GString *str;
+	GHashTable *params;
+	GList *keys;
+	GList *iter;
+
+	params = calculate_xoauth_params (
+		request_uri,
+		consumer_key,
+		consumer_secret,
+		access_token,
+		access_token_secret);
+
+	str = g_string_new ("GET ");
+	g_string_append (str, request_uri);
+	g_string_append_c (str, ' ');
+	keys = g_hash_table_get_keys (params);
+	keys = g_list_sort (keys, (GCompareFunc) g_strcmp0);
+	for (iter = keys; iter != NULL; iter = iter->next) {
+		const gchar *key = iter->data;
+		const gchar *value;
+		gchar *k;
+		gchar *v;
+
+		value = g_hash_table_lookup (params, key);
+		if (iter != keys)
+			g_string_append_c (str, ',');
+
+		k = OAUTH_ENCODE_STRING (key);
+		v = OAUTH_ENCODE_STRING (value);
+		g_string_append_printf (str, "%s=\"%s\"", k, v);
+		g_free (k);
+		g_free (v);
+	}
+	g_list_free (keys);
+
+	g_hash_table_unref (params);
+
+	return g_string_free (str, FALSE);
+}
+
+/****************************************************************************/
+
+static GoaObject *
+sasl_xoauth_get_account_by_id (GoaClient *client,
+                               const gchar *account_id)
+{
+	GoaObject *match = NULL;
+	GList *list, *iter;
+
+	list = goa_client_get_accounts (client);
+
+	for (iter = list; iter != NULL; iter = g_list_next (iter)) {
+		GoaObject *goa_object;
+		GoaAccount *goa_account;
+		const gchar *candidate_id;
+
+		goa_object = GOA_OBJECT (iter->data);
+		goa_account = goa_object_get_account (goa_object);
+		candidate_id = goa_account_get_id (goa_account);
+
+		if (g_strcmp0 (account_id, candidate_id) == 0)
+			match = g_object_ref (goa_object);
+
+		g_object_unref (goa_account);
+
+		if (match != NULL)
+			break;
+	}
+
+	g_list_free_full (list, (GDestroyNotify) g_object_unref);
+
+	return match;
+}
+
+static GByteArray *
+sasl_xoauth_challenge_sync (CamelSasl *sasl,
+                            GByteArray *token,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+	GoaClient *goa_client;
+	GoaObject *goa_object;
+	GoaAccount *goa_account;
+	GByteArray *parameters = NULL;
+	CamelService *service;
+	CamelURL *url;
+	const gchar *account_id;
+	gchar *xoauth_param = NULL;
+	gboolean success;
+
+	service = camel_sasl_get_service (sasl);
+	url = camel_service_get_camel_url (service);
+	account_id = camel_url_get_param (url, GOA_KEY);
+	g_return_val_if_fail (account_id != NULL, NULL);
+
+	goa_client = goa_client_new_sync (cancellable, error);
+	if (goa_client == NULL)
+		return NULL;
+
+	goa_object = sasl_xoauth_get_account_by_id (goa_client, account_id);
+	if (goa_object == NULL) {
+		g_set_error_literal (
+			error, CAMEL_SERVICE_ERROR,
+			CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE,
+			_("Cannot find a corresponding account in "
+			  "the org.gnome.OnlineAccounts service from "
+			  "which to obtain an authentication token."));
+		g_object_unref (goa_client);
+		return NULL;
+	}
+
+	goa_account = goa_object_get_account (goa_object);
+
+	success = goa_account_call_ensure_credentials_sync (
+		goa_account, NULL, cancellable, error);
+
+	if (success) {
+		GoaOAuthBased *goa_oauth_based;
+		const gchar *identity;
+		const gchar *consumer_key;
+		const gchar *consumer_secret;
+		const gchar *service_type;
+		gchar *access_token = NULL;
+		gchar *access_token_secret = NULL;
+		gchar *request_uri;
+
+		goa_oauth_based = goa_object_get_oauth_based (goa_object);
+
+		identity = goa_account_get_identity (goa_account);
+		service_type = CAMEL_IS_STORE (service) ? "imap" : "smtp";
+
+		/* FIXME This should probably be generalized. */
+		request_uri = g_strdup_printf (
+			"https://mail.google.com/mail/b/%s/%s/";,
+			identity, service_type);
+
+		consumer_key =
+			goa_oauth_based_get_consumer_key (goa_oauth_based);
+		consumer_secret =
+			goa_oauth_based_get_consumer_secret (goa_oauth_based);
+
+		success = goa_oauth_based_call_get_access_token_sync (
+			goa_oauth_based,
+			&access_token,
+			&access_token_secret,
+			NULL,
+			cancellable,
+			error);
+
+		if (success)
+			xoauth_param = calculate_xoauth_param (
+				request_uri,
+				consumer_key,
+				consumer_secret,
+				access_token,
+				access_token_secret);
+
+		g_free (access_token);
+		g_free (access_token_secret);
+		g_free (request_uri);
+
+		g_object_unref (goa_oauth_based);
+	}
+
+	g_object_unref (goa_account);
+	g_object_unref (goa_object);
+	g_object_unref (goa_client);
+
+	if (success) {
+		/* Sanity check. */
+		g_return_val_if_fail (xoauth_param != NULL, NULL);
+
+		parameters = g_byte_array_new ();
+		g_byte_array_append (
+			parameters, (guint8 *) xoauth_param,
+			strlen (xoauth_param) + 1);
+		g_free (xoauth_param);
+	}
+
+	/* IMAP and SMTP services will Base64-encode the XOAUTH parameters. */
+
+	return parameters;
+}
+
+static gpointer
+camel_sasl_xoauth_auth_type_init (gpointer unused)
+{
+	CamelServiceAuthType *auth_type;
+
+	/* This is a one-time allocation, never freed. */
+	auth_type = g_malloc0 (sizeof (CamelServiceAuthType));
+	auth_type->name = _("OAuth");
+	auth_type->description =
+		_("This option will connect to the server by "
+		  "way of the GNOME Online Accounts service");
+	auth_type->authproto = "XOAUTH";
+	auth_type->need_password = FALSE;
+
+	return auth_type;
+}
+
+static void
+camel_sasl_xoauth_class_init (CamelSaslXOAuthClass *class)
+{
+	static GOnce auth_type_once = G_ONCE_INIT;
+	CamelSaslClass *sasl_class;
+
+	g_once (&auth_type_once, camel_sasl_xoauth_auth_type_init, NULL);
+
+	g_type_class_add_private (class, sizeof (CamelSaslXOAuthPrivate));
+
+	sasl_class = CAMEL_SASL_CLASS (class);
+	sasl_class->auth_type = auth_type_once.retval;
+	sasl_class->challenge_sync = sasl_xoauth_challenge_sync;
+}
+
+static void
+camel_sasl_xoauth_class_finalize (CamelSaslXOAuthClass *class)
+{
+}
+
+static void
+camel_sasl_xoauth_init (CamelSaslXOAuth *sasl)
+{
+	sasl->priv = CAMEL_SASL_XOAUTH_GET_PRIVATE (sasl);
+}
+
+void
+camel_sasl_xoauth_type_register (GTypeModule *type_module)
+{
+	/* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
+	 *     function, so we have to wrap it with a public function in
+	 *     order to register types from a separate compilation unit. */
+	camel_sasl_xoauth_register_type (type_module);
+}
diff --git a/modules/online-accounts/camel-sasl-xoauth.h b/modules/online-accounts/camel-sasl-xoauth.h
new file mode 100644
index 0000000..3e78547
--- /dev/null
+++ b/modules/online-accounts/camel-sasl-xoauth.h
@@ -0,0 +1,63 @@
+/*
+ * camel-sasl-xoauth.h
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+#ifndef CAMEL_SASL_XOAUTH_H
+#define CAMEL_SASL_XOAUTH_H
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_SASL_XOAUTH \
+	(camel_sasl_xoauth_get_type ())
+#define CAMEL_SASL_XOAUTH(obj) \
+	(G_TYPE_CHECK_INSTANCE_CAST \
+	((obj), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuth))
+#define CAMEL_SASL_XOAUTH_CLASS(cls) \
+	(G_TYPE_CHECK_CLASS_CAST \
+	((cls), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuthClass))
+#define CAMEL_IS_SASL_XOAUTH(obj) \
+	(G_TYPE_CHECK_INSTANCE_TYPE \
+	((obj), CAMEL_TYPE_SASL_XOAUTH))
+#define CAMEL_IS_SASL_XOAUTH_CLASS(cls) \
+	(G_TYPE_CHECK_CLASS_TYPE \
+	((cls), CAMEL_TYPE_SASL_XOAUTH))
+#define CAMEL_SASL_XOAUTH_GET_CLASS(obj) \
+	(G_TYPE_INSTANCE_GET_CLASS \
+	((obj), CAMEL_TYPE_SASL_XOAUTH, CamelSaslXOAuthClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelSaslXOAuth CamelSaslXOAuth;
+typedef struct _CamelSaslXOAuthClass CamelSaslXOAuthClass;
+typedef struct _CamelSaslXOAuthPrivate CamelSaslXOAuthPrivate;
+
+struct _CamelSaslXOAuth {
+	CamelSasl parent;
+	CamelSaslXOAuthPrivate *priv;
+};
+
+struct _CamelSaslXOAuthClass {
+	CamelSaslClass parent_class;
+};
+
+GType		camel_sasl_xoauth_get_type	(void);
+void		camel_sasl_xoauth_type_register	(GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* CAMEL_SASL_XOAUTH_H */
diff --git a/modules/online-accounts/e-online-accounts-google.c b/modules/online-accounts/e-online-accounts-google.c
new file mode 100644
index 0000000..3228079
--- /dev/null
+++ b/modules/online-accounts/e-online-accounts-google.c
@@ -0,0 +1,455 @@
+/*
+ * e-online-accounts-google.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+#include "e-online-accounts-google.h"
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+/* XXX Just use the deprecated APIs for now.
+ *     We'll be switching away soon enough. */
+#undef E_CAL_DISABLE_DEPRECATED
+#undef E_BOOK_DISABLE_DEPRECATED
+
+#include <libecal/e-cal.h>
+#include <libebook/e-book.h>
+
+#include <e-util/e-account-utils.h>
+
+/* This is the property name or URL parameter under which we
+ * embed the GoaAccount ID into an EAccount or ESource object. */
+#define GOA_KEY "goa-account-id"
+
+#define GOOGLE_BASE_URI "google://"
+
+/**
+ * XXX Once the key-file based ESource API is merged, I'd
+ *     like to structure the ESources something like this:
+ *
+ *     * Maybe add an "Enabled" key to the [Data Source] group,
+ *       so we have an easy way to hide/show individual sources
+ *       without destroying custom settings.  Would replace the
+ *       "Enabled" key in [Mail Account], and rename the same
+ *       key in ESourceSelectable to "Active".
+ *
+ *     +---------------------------------------------------+
+ *     | [Data Source]                                     |
+ *     | DisplayName: <<GoaAccount:presentation-identity>> |
+ *     | Backend: google                                   |
+ *     | Enabled: true  # What would 'false' mean?         |
+ *     |                                                   |
+ *     | [GNOME Online Accounts]                           |
+ *     | Id: <<GoaAccount:id>>                             |
+ *     +---------------------------------------------------+
+ *         |
+ *         | (child ESources)
+ *         |
+ *         |    +------------------------------------------+
+ *         |    | [Data Source]                            |
+ *         |    | DisplayName: (same as parent)            |
+ *         |    | Enabled: true                            |
+ *         |    |                                          |
+ *         |    | [Authentication]                         |
+ *         |    | Host: imap.gmail.com                     |
+ *         |    | blah, blah, blah...                      |
+ *         |    |                                          |
+ *         +----| [Mail Account]                           |
+ *         |    | blah, blah, blah...                      |
+ *         |    |                                          |
+ *         |    | [Mail Identity]                          |
+ *         |    | Name: Matthew Barnes                     |
+ *         |    | Address: <<my-gmail-address>>            |
+ *         |    | blah, blah, blah...                      |
+ *         |    +------------------------------------------+
+ *         |
+ *         |    +------------------------------------------+
+ *         |    | [Data Source]                            |
+ *         |    | DisplayName: GMail SMTP Server           |
+ *         |    | Enabled: true                            |
+ *         |    |                                          |
+ *         |    | [Authentication]                         |
+ *         |    | Host: smtp.gmail.com                     |
+ *         +----| blah, blah, blah...                      |
+ *         |    |                                          |
+ *         |    | [Mail Transport]                         |
+ *         |    | blah, blah, blah...                      |
+ *         |    +------------------------------------------+
+ *         |
+ *         |    +------------------------------------------+
+ *         |    | [Data Source]                            |
+ *         |    | DisplayName: Contacts                    |
+ *         |    | Enabled: true                            |
+ *         |    |                                          |
+ *         |    | [Authentication]                         |
+ *         |    | blah, blah, blah...                      |
+ *         +----|                                          |
+ *         |    | [Address Book]                           |
+ *         |    | blah, blah, blah...                      |
+ *         |    +------------------------------------------+
+ *         |
+ *         |    +------------------------------------------+
+ *         |    | [Data Source]                            |
+ *         |    | DisplayName: Calendar                    |
+ *         |    | Backend: caldav                          |
+ *         |    | Enabled: true                            |
+ *         |    |                                          |
+ *         |    | [Authentication]                         |
+ *         |    | blah, blah, blah...                      |
+ *         +----|                                          |
+ *              | [Calendar]                               |
+ *              | blah, blah, blah...                      |
+ *              +------------------------------------------+
+ */
+
+/* XXX Copy part of the private struct here so we can set our own UID.
+ *     Since EAccountList and ESourceList forces the different aspects
+ *     of the Google account to be disjoint, we can reuse the UID to
+ *     link them back together. */
+struct _ESourcePrivate {
+	ESourceGroup *group;
+
+	gchar *uid;
+	/* ... yadda, yadda, yadda ... */
+};
+
+static void
+online_accounts_google_sync_mail (GoaObject *goa_object,
+                                  const gchar *evo_id)
+{
+	GoaMail *goa_mail;
+	GoaAccount *goa_account;
+	EAccountList *account_list;
+	EAccount *account;
+	CamelURL *url;
+	const gchar *string;
+	gboolean new_account = FALSE;
+
+	/* XXX There's nothing particularly GMail-specific about this.
+	 *     Maybe break this off into a more generic IMAP/SMTP sync
+	 *     function and then apply any GMail-specific tweaks. */
+
+	goa_mail = goa_object_get_mail (goa_object);
+	goa_account = goa_object_get_account (goa_object);
+
+	account_list = e_get_account_list ();
+	account = e_get_account_by_uid (evo_id);
+
+	if (account == NULL) {
+		account = g_object_new (E_TYPE_ACCOUNT, NULL);
+		account->uid = g_strdup (evo_id);
+		account->enabled = TRUE;
+		new_account = TRUE;
+	}
+
+	/*** Account Name ***/
+
+	g_free (account->name);
+	string = goa_account_get_presentation_identity (goa_account);
+	account->name = g_strdup (string);
+
+	/*** Mail Identity ***/
+
+	if (account->id->name == NULL)
+		account->id->name = g_strdup (g_get_real_name ());
+
+	g_free (account->id->address);
+	string = goa_mail_get_email_address (goa_mail);
+	account->id->address = g_strdup (string);
+
+	/*** Mail Storage ***/
+
+	/* This quietly handles NULL strings sanely. */
+	url = camel_url_new (account->source->url, NULL);
+
+	if (url == NULL)
+		url = g_new0 (CamelURL, 1);
+
+	camel_url_set_protocol (url, "imapx");
+
+	string = goa_account_get_identity (goa_account);
+	camel_url_set_user (url, string);
+
+	string = goa_mail_get_imap_host (goa_mail);
+	camel_url_set_host (url, string);
+
+	/* Use CamelSaslXOAuth. */
+	camel_url_set_authmech (url, "XOAUTH");
+
+	/* Always == SSL (port 993) */
+	if (goa_mail_get_imap_use_tls (goa_mail))
+		camel_url_set_param (url, "use_ssl", "always");
+	else
+		camel_url_set_param (url, "use_ssl", "never");
+
+	string = goa_account_get_id (goa_account);
+	camel_url_set_param (url, GOA_KEY, string);
+
+	g_free (account->source->url);
+	account->source->url = camel_url_to_string (url, 0);
+
+	camel_url_free (url);
+
+	/*** Mail Transport ***/
+
+	/* This quietly handles NULL strings sanely. */
+	url = camel_url_new (account->transport->url, NULL);
+
+	if (url == NULL)
+		url = g_new0 (CamelURL, 1);
+
+	camel_url_set_protocol (url, "smtp");
+
+	string = goa_account_get_identity (goa_account);
+	camel_url_set_user (url, string);
+
+	string = goa_mail_get_smtp_host (goa_mail);
+	camel_url_set_host (url, string);
+
+	/* Message Submission port */
+	camel_url_set_port (url, 587);
+
+	/* Use CamelSaslXOAuth. */
+	camel_url_set_authmech (url, "XOAUTH");
+
+	/* When-Possible == STARTTLS */
+	if (goa_mail_get_smtp_use_tls (goa_mail))
+		camel_url_set_param (url, "use_ssl", "when-possible");
+	else
+		camel_url_set_param (url, "use_ssl", "never");
+
+	string = goa_account_get_id (goa_account);
+	camel_url_set_param (url, GOA_KEY, string);
+
+	g_free (account->transport->url);
+	account->transport->url = camel_url_to_string (url, 0);
+
+	camel_url_free (url);
+
+	/* Clean up. */
+
+	if (new_account) {
+		e_account_list_add (account_list, account);
+		g_object_unref (account);
+	}
+
+	g_object_unref (goa_account);
+	g_object_unref (goa_mail);
+}
+
+static void
+online_accounts_google_sync_calendar (GoaObject *goa_object,
+                                      const gchar *evo_id)
+{
+	GoaAccount *goa_account;
+	ESourceList *source_list = NULL;
+	ESourceGroup *source_group;
+	ECalSourceType source_type;
+	ESource *source;
+	const gchar *string;
+	gchar *encoded;
+	gchar *uri_string;
+	gboolean new_source = FALSE;
+	GError *error = NULL;
+
+	source_type = E_CAL_SOURCE_TYPE_EVENT;
+
+	if (!e_cal_get_sources (&source_list, source_type, &error)) {
+		g_warn_if_fail (source_list == NULL);
+		g_warn_if_fail (error != NULL);
+		g_warning ("%s", error->message);
+		g_error_free (error);
+		return;
+	}
+
+	g_return_if_fail (E_IS_SOURCE_LIST (source_list));
+
+	goa_account = goa_object_get_account (goa_object);
+
+	/* This returns a new reference to the source group. */
+	source_group = e_source_list_ensure_group (
+		source_list, _("Google"), GOOGLE_BASE_URI, TRUE);
+
+	source = e_source_group_peek_source_by_uid (source_group, evo_id);
+
+	if (source == NULL) {
+		source = g_object_new (E_TYPE_SOURCE, NULL);
+		source->priv->uid = g_strdup (evo_id);
+		e_source_set_name (source, _("Calendar"));
+		new_source = TRUE;
+	}
+
+	string = goa_account_get_identity (goa_account);
+
+	encoded = camel_url_encode (string, "@");
+	uri_string = g_strdup_printf (
+		"caldav://%s www google com/calendar/dav/%s/events",
+		encoded, string);
+	e_source_set_absolute_uri (source, uri_string);
+	g_free (uri_string);
+	g_free (encoded);
+
+	e_source_set_property (source, "ssl", "1");
+	e_source_set_property (source, "username", string);
+	e_source_set_property (source, "setup-username", string);
+
+	/* XXX Not sure this needs to be set since the backend
+	 *     will authenticate itself if it sees a GOA ID. */
+	e_source_set_property (source, "auth", "1");
+
+	if (new_source) {
+		e_source_group_add_source (source_group, source, -1);
+		g_object_unref (source);
+	}
+
+	g_object_unref (source_group);
+	g_object_unref (goa_account);
+}
+
+static void
+online_accounts_google_sync_contacts (GoaObject *goa_object,
+                                      const gchar *evo_id)
+{
+	GoaAccount *goa_account;
+	ESourceList *source_list = NULL;
+	ESourceGroup *source_group;
+	ESource *source;
+	const gchar *string;
+	gboolean new_source = FALSE;
+	GError *error = NULL;
+
+	if (!e_book_get_addressbooks (&source_list, &error)) {
+		g_warn_if_fail (source_list == NULL);
+		g_warn_if_fail (error != NULL);
+		g_warning ("%s", error->message);
+		g_error_free (error);
+		return;
+	}
+
+	g_return_if_fail (E_IS_SOURCE_LIST (source_list));
+
+	goa_account = goa_object_get_account (goa_object);
+
+	/* This returns a new reference to the source group. */
+	source_group = e_source_list_ensure_group (
+		source_list, _("Google"), GOOGLE_BASE_URI, TRUE);
+
+	source = e_source_group_peek_source_by_uid (source_group, evo_id);
+
+	if (source == NULL) {
+		source = g_object_new (E_TYPE_SOURCE, NULL);
+		source->priv->uid = g_strdup (evo_id);
+		e_source_set_name (source, _("Contacts"));
+		new_source = TRUE;
+	}
+
+	string = goa_account_get_identity (goa_account);
+
+	e_source_set_relative_uri (source, string);
+
+	e_source_set_property (source, "use-ssl", "true");
+	e_source_set_property (source, "username", string);
+
+	/* XXX Not sure this needs to be set since the backend
+	 *     will authenticate itself if it sees a GOA ID. */
+	e_source_set_property (source, "auth", "plain/password");
+
+	if (new_source) {
+		e_source_group_add_source (source_group, source, -1);
+		g_object_unref (source);
+	}
+
+	g_object_unref (source_group);
+	g_object_unref (goa_account);
+}
+
+void
+e_online_accounts_google_sync (GoaObject *goa_object,
+                               const gchar *evo_id)
+{
+	GoaMail *goa_mail;
+	GoaCalendar *goa_calendar;
+	GoaContacts *goa_contacts;
+
+	g_return_if_fail (GOA_IS_OBJECT (goa_object));
+	g_return_if_fail (evo_id != NULL);
+
+	/*** Google Mail ***/
+
+	goa_mail = goa_object_get_mail (goa_object);
+	if (goa_mail != NULL) {
+		online_accounts_google_sync_mail (goa_object, evo_id);
+		g_object_unref (goa_mail);
+	} else {
+		EAccountList *account_list;
+		EAccount *account;
+
+		account_list = e_get_account_list ();
+		account = e_get_account_by_uid (evo_id);
+
+		if (account != NULL)
+			e_account_list_remove (account_list, account);
+	}
+
+	/*** Google Calendar ***/
+
+	goa_calendar = goa_object_get_calendar (goa_object);
+	if (goa_calendar != NULL) {
+		online_accounts_google_sync_calendar (goa_object, evo_id);
+		g_object_unref (goa_calendar);
+	} else {
+		ESourceList *source_list = NULL;
+		ECalSourceType source_type;
+		GError *error = NULL;
+
+		source_type = E_CAL_SOURCE_TYPE_EVENT;
+		if (e_cal_get_sources (&source_list, source_type, &error)) {
+			e_source_list_remove_source_by_uid (
+				source_list, evo_id);
+			g_object_unref (source_list);
+		} else {
+			g_warn_if_fail (source_list == NULL);
+			g_warn_if_fail (error != NULL);
+			g_warning ("%s", error->message);
+			g_error_free (error);
+		}
+
+		/* XXX Would be nice to support Google Tasks as well. */
+	}
+
+	/*** Google Contacts ***/
+
+	goa_contacts = goa_object_get_contacts (goa_object);
+	if (goa_contacts != NULL) {
+		online_accounts_google_sync_contacts (goa_object, evo_id);
+		g_object_unref (goa_contacts);
+	} else {
+		ESourceList *source_list = NULL;
+		GError *error = NULL;
+
+		if (e_book_get_addressbooks (&source_list, &error)) {
+			e_source_list_remove_source_by_uid (
+				source_list, evo_id);
+			g_object_unref (source_list);
+		} else {
+			g_warn_if_fail (source_list == NULL);
+			g_warn_if_fail (error != NULL);
+			g_warning ("%s", error->message);
+			g_error_free (error);
+		}
+	}
+}
diff --git a/modules/online-accounts/e-online-accounts-google.h b/modules/online-accounts/e-online-accounts-google.h
new file mode 100644
index 0000000..10973f3
--- /dev/null
+++ b/modules/online-accounts/e-online-accounts-google.h
@@ -0,0 +1,32 @@
+/*
+ * e-online-accounts-google.h
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+#ifndef E_ONLINE_ACCOUNTS_GOOGLE_H
+#define E_ONLINE_ACCOUNTS_GOOGLE_H
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <goa/goa.h>
+
+G_BEGIN_DECLS
+
+void		e_online_accounts_google_sync	(GoaObject *goa_object,
+						 const gchar *evo_id);
+
+G_END_DECLS
+
+#endif /* E_ONLINE_ACCOUNTS_GOOGLE_H */
diff --git a/modules/online-accounts/evolution-online-accounts.c b/modules/online-accounts/evolution-online-accounts.c
new file mode 100644
index 0000000..d127f4d
--- /dev/null
+++ b/modules/online-accounts/evolution-online-accounts.c
@@ -0,0 +1,520 @@
+/*
+ * evolution-online-accounts.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+/* XXX Just use the deprecated APIs for now.
+ *     We'll be switching away soon enough. */
+#undef E_CAL_DISABLE_DEPRECATED
+#undef E_BOOK_DISABLE_DEPRECATED
+
+#include <libecal/e-cal.h>
+#include <libebook/e-book.h>
+#include <libedataserver/e-uid.h>
+#include <libedataserver/e-account-list.h>
+
+#include <shell/e-shell.h>
+#include <e-util/e-account-utils.h>
+
+#include "camel-sasl-xoauth.h"
+#include "e-online-accounts-google.h"
+
+/* Standard GObject macros */
+#define E_TYPE_ONLINE_ACCOUNTS \
+	(e_online_accounts_get_type ())
+#define E_ONLINE_ACCOUNTS(obj) \
+	(G_TYPE_CHECK_INSTANCE_CAST \
+	((obj), E_TYPE_ONLINE_ACCOUNTS, EOnlineAccounts))
+
+/* This is the property name or URL parameter under which we
+ * embed the GoaAccount ID into an EAccount or ESource object. */
+#define GOA_KEY "goa-account-id"
+
+typedef struct _EOnlineAccounts EOnlineAccounts;
+typedef struct _EOnlineAccountsClass EOnlineAccountsClass;
+
+typedef struct _AccountNode AccountNode;
+
+struct _EOnlineAccounts {
+	EExtension parent;
+
+	/* GoaAccount ID -> EAccount/ESource ID */
+	GHashTable *accounts;
+
+	GoaClient *goa_client;
+	EActivity *connecting;
+};
+
+struct _EOnlineAccountsClass {
+	EExtensionClass parent_class;
+};
+
+struct _AccountNode {
+	gchar *goa_id;		/* GoaAccount ID */
+	gchar *evo_id;		/* EAccount/ESource ID */
+};
+
+/* Module Entry Points */
+void e_module_load (GTypeModule *type_module);
+void e_module_unload (GTypeModule *type_module);
+
+/* Forward Declarations */
+GType e_online_accounts_get_type (void);
+
+G_DEFINE_DYNAMIC_TYPE (EOnlineAccounts, e_online_accounts, E_TYPE_EXTENSION)
+
+static EShell *
+online_accounts_get_shell (EOnlineAccounts *extension)
+{
+	EExtensible *extensible;
+
+	extensible = e_extension_get_extensible (E_EXTENSION (extension));
+
+	return E_SHELL (extensible);
+}
+
+static void
+online_accounts_account_added_cb (GoaClient *goa_client,
+                                  GoaObject *goa_object,
+                                  EOnlineAccounts *extension)
+{
+	GoaAccount *goa_account;
+	const gchar *provider_type;
+	const gchar *goa_id;
+	const gchar *evo_id;
+
+	goa_account = goa_object_get_account (goa_object);
+	provider_type = goa_account_get_provider_type (goa_account);
+
+	goa_id = goa_account_get_id (goa_account);
+	evo_id = g_hash_table_lookup (extension->accounts, goa_id);
+
+	if (g_strcmp0 (provider_type, "google") == 0) {
+		if (evo_id == NULL) {
+			gchar *uid = e_uid_new ();
+			g_hash_table_insert (
+				extension->accounts,
+				g_strdup (goa_id), uid);
+			evo_id = uid;
+		}
+
+		e_online_accounts_google_sync (goa_object, evo_id);
+	}
+
+	g_object_unref (goa_account);
+}
+
+static void
+online_accounts_account_changed_cb (GoaClient *goa_client,
+                                    GoaObject *goa_object,
+                                    EOnlineAccounts *extension)
+{
+	/* XXX We'll be able to handle changes more sanely once we have
+	 *     key-file based ESources with proper change notifications. */
+	online_accounts_account_added_cb (goa_client, goa_object, extension);
+}
+
+static void
+online_accounts_account_removed_cb (GoaClient *goa_client,
+                                    GoaObject *goa_object,
+                                    EOnlineAccounts *extension)
+{
+	GoaAccount *goa_account;
+	EAccountList *account_list;
+	ESourceList *source_list;
+	ECalSourceType type;
+	EAccount *account;
+	const gchar *goa_id;
+	const gchar *evo_id;
+
+	goa_account = goa_object_get_account (goa_object);
+	goa_id = goa_account_get_id (goa_account);
+	evo_id = g_hash_table_lookup (extension->accounts, goa_id);
+
+	if (evo_id == NULL)
+		goto exit;
+
+	/* Remove the mail account. */
+
+	account_list = e_get_account_list ();
+	account = e_get_account_by_uid (evo_id);
+
+	if (account != NULL)
+		e_account_list_remove (account_list, account);
+
+	/* Remove the address book. */
+
+	if (e_book_get_addressbooks (&source_list, NULL)) {
+		e_source_list_remove_source_by_uid (source_list, evo_id);
+		g_object_unref (source_list);
+	}
+
+	/* Remove the calendar. */
+
+	for (type = 0; type < E_CAL_SOURCE_TYPE_LAST; type++) {
+		if (e_cal_get_sources (&source_list, type, NULL)) {
+			e_source_list_remove_source_by_uid (
+				source_list, evo_id);
+			g_object_unref (source_list);
+		}
+	}
+
+exit:
+	g_object_unref (goa_account);
+}
+
+static gint
+online_accounts_compare_id (GoaObject *goa_object,
+                            const gchar *goa_id)
+{
+	GoaAccount *goa_account;
+	gint result;
+
+	goa_account = goa_object_get_account (goa_object);
+	result = g_strcmp0 (goa_account_get_id (goa_account), goa_id);
+	g_object_unref (goa_account);
+
+	return result;
+}
+
+static void
+online_accounts_handle_uid (EOnlineAccounts *extension,
+                            const gchar *goa_id,
+                            const gchar *evo_id)
+{
+	const gchar *match;
+
+	/* If the GNOME Online Account ID is already registered, the
+	 * corresponding Evolution ID better match what was passed in. */
+	match = g_hash_table_lookup (extension->accounts, goa_id);
+	g_return_if_fail (match == NULL || g_strcmp0 (match, evo_id) == 0);
+
+	if (match == NULL)
+		g_hash_table_insert (
+			extension->accounts,
+			g_strdup (goa_id),
+			g_strdup (evo_id));
+}
+
+static void
+online_accounts_search_source_list (EOnlineAccounts *extension,
+                                    GList *goa_objects,
+                                    ESourceList *source_list)
+{
+	GSList *list_a;
+
+	list_a = e_source_list_peek_groups (source_list);
+
+	while (list_a != NULL) {
+		ESourceGroup *source_group;
+		GQueue trash = G_QUEUE_INIT;
+		GSList *list_b;
+
+		source_group = E_SOURCE_GROUP (list_a->data);
+		list_a = g_slist_next (list_a);
+
+		list_b = e_source_group_peek_sources (source_group);
+
+		while (list_b != NULL) {
+			ESource *source;
+			const gchar *property;
+			const gchar *uid;
+			GList *match;
+
+			source = E_SOURCE (list_b->data);
+			list_b = g_slist_next (list_b);
+
+			uid = e_source_peek_uid (source);
+			property = e_source_get_property (source, GOA_KEY);
+
+			if (property == NULL)
+				continue;
+
+			/* Verify the GOA account still exists. */
+			match = g_list_find_custom (
+				goa_objects, property, (GCompareFunc)
+				online_accounts_compare_id);
+
+			/* If a matching GoaObject was found, add its ID
+			 * to our accounts hash table.  Otherwise remove
+			 * the ESource after we finish looping. */
+			if (match != NULL)
+				online_accounts_handle_uid (
+					extension, property, uid);
+			else
+				g_queue_push_tail (&trash, source);
+		}
+
+		/* Empty the trash. */
+		while (!g_queue_is_empty (&trash)) {
+			ESource *source = g_queue_pop_head (&trash);
+			e_source_group_remove_source (source_group, source);
+		}
+	}
+}
+
+static void
+online_accounts_populate_accounts_table (EOnlineAccounts *extension,
+                                         GList *goa_objects)
+{
+	EAccountList *account_list;
+	ESourceList *source_list;
+	EIterator *iterator;
+	ECalSourceType type;
+	GQueue trash = G_QUEUE_INIT;
+
+	/* XXX All this messy logic will be much prettier once the new
+	 *     key-file based ESource API is merged.  For now though,
+	 *     we trudge through it the old and cumbersome way. */
+
+	/* Search mail accounts. */
+
+	account_list = e_get_account_list ();
+	iterator = e_list_get_iterator (E_LIST (account_list));
+
+	while (e_iterator_is_valid (iterator)) {
+		EAccount *account;
+		CamelURL *url;
+		const gchar *param;
+
+		/* XXX EIterator misuses const. */
+		account = (EAccount *) e_iterator_get (iterator);
+		e_iterator_next (iterator);
+
+		if (account->source == NULL)
+			continue;
+
+		if (account->source->url == NULL)
+			continue;
+
+		url = camel_url_new (account->source->url, NULL);
+		if (url == NULL)
+			continue;
+
+		param = camel_url_get_param (url, GOA_KEY);
+		if (param != NULL) {
+			GList *match;
+
+			/* Verify the GOA account still exists. */
+			match = g_list_find_custom (
+				goa_objects, param, (GCompareFunc)
+				online_accounts_compare_id);
+
+			/* If a matching GoaObject was found, add its ID
+			 * to our accounts hash table.  Otherwise remove
+			 * the EAccount after we finish looping. */
+			if (match != NULL)
+				online_accounts_handle_uid (
+					extension, param, account->uid);
+			else
+				g_queue_push_tail (&trash, account);
+		}
+
+		camel_url_free (url);
+	}
+
+	g_object_unref (iterator);
+
+	/* Empty the trash. */
+	while (!g_queue_is_empty (&trash)) {
+		EAccount *account = g_queue_pop_head (&trash);
+		e_account_list_remove (account_list, account);
+	}
+
+	/* Search address book sources. */
+
+	if (e_book_get_addressbooks (&source_list, NULL)) {
+		online_accounts_search_source_list (
+			extension, goa_objects, source_list);
+		g_object_unref (source_list);
+	}
+
+	/* Search calendar-related sources. */
+
+	for (type = 0; type < E_CAL_SOURCE_TYPE_LAST; type++) {
+		if (e_cal_get_sources (&source_list, type, NULL)) {
+			online_accounts_search_source_list (
+				extension, goa_objects, source_list);
+			g_object_unref (source_list);
+		}
+	}
+}
+
+static void
+online_accounts_connect_done (GObject *source_object,
+                              GAsyncResult *result,
+                              EOnlineAccounts *extension)
+{
+	GList *list, *link;
+	GError *error = NULL;
+
+	extension->goa_client = goa_client_new_finish (result, &error);
+
+	/* FIXME Add an EAlert for this? */
+	if (error != NULL) {
+		g_warning ("%s", error->message);
+		g_error_free (error);
+		return;
+	}
+
+	list = goa_client_get_accounts (extension->goa_client);
+
+	/* This populates a hash table of GOA ID -> Evo ID strings by
+	 * searching through all Evolution sources for ones tagged with
+	 * a GOA ID.  If a GOA ID tag is found, but no corresponding GOA
+	 * account (presumably meaning the GOA account was deleted between
+	 * Evo sessions), then the EAccount or ESource on which the tag was
+	 * found gets deleted. */
+	online_accounts_populate_accounts_table (extension, list);
+
+	for (link = list; link != NULL; link = g_list_next (link))
+		online_accounts_account_added_cb (
+			extension->goa_client,
+			GOA_OBJECT (link->data),
+			extension);
+
+	g_list_free_full (list, (GDestroyNotify) g_object_unref);
+
+	/* Listen for Online Account changes. */
+	g_signal_connect (
+		extension->goa_client, "account-added",
+		G_CALLBACK (online_accounts_account_added_cb), extension);
+	g_signal_connect (
+		extension->goa_client, "account-changed",
+		G_CALLBACK (online_accounts_account_changed_cb), extension);
+	g_signal_connect (
+		extension->goa_client, "account-removed",
+		G_CALLBACK (online_accounts_account_removed_cb), extension);
+
+	/* This will allow the Evolution Setup Assistant to proceed. */
+	g_object_unref (extension->connecting);
+	extension->connecting = NULL;
+}
+
+static void
+online_accounts_connect (EShell *shell,
+                         EActivity *activity,
+                         EOnlineAccounts *extension)
+{
+	/* This will inhibit the Evolution Setup Assistant until
+	 * we've synchronized with the OnlineAccounts service. */
+	extension->connecting = g_object_ref (activity);
+
+	/* We don't really need to reference the extension in the
+	 * async closure since its lifetime is bound to the EShell. */
+	goa_client_new (
+		NULL, (GAsyncReadyCallback)
+		online_accounts_connect_done, extension);
+}
+
+static void
+online_accounts_dispose (GObject *object)
+{
+	EOnlineAccounts *extension;
+
+	extension = E_ONLINE_ACCOUNTS (object);
+
+	/* This should never fail... in theory. */
+	g_warn_if_fail (extension->connecting == NULL);
+
+	if (extension->goa_client != NULL) {
+		g_signal_handlers_disconnect_matched (
+			extension->goa_client, G_SIGNAL_MATCH_DATA,
+			0, 0, NULL, NULL, object);
+		g_object_unref (extension->goa_client);
+		extension->goa_client = NULL;
+	}
+
+	/* Chain up to parent's dispose() method. */
+	G_OBJECT_CLASS (e_online_accounts_parent_class)->dispose (object);
+}
+
+static void
+online_accounts_finalize (GObject *object)
+{
+	EOnlineAccounts *extension;
+
+	extension = E_ONLINE_ACCOUNTS (object);
+
+	g_hash_table_destroy (extension->accounts);
+
+	/* Chain up to parent's finalize() method. */
+	G_OBJECT_CLASS (e_online_accounts_parent_class)->finalize (object);
+}
+
+static void
+online_accounts_constructed (GObject *object)
+{
+	EOnlineAccounts *extension;
+	EShell *shell;
+
+	extension = E_ONLINE_ACCOUNTS (object);
+	shell = online_accounts_get_shell (extension);
+
+	/* This event is emitted from the "startup-wizard" module. */
+	g_signal_connect (
+		shell, "event::load-accounts",
+		G_CALLBACK (online_accounts_connect), extension);
+
+	/* Chain up to parent's constructed() method. */
+	G_OBJECT_CLASS (e_online_accounts_parent_class)->constructed (object);
+}
+
+static void
+e_online_accounts_class_init (EOnlineAccountsClass *class)
+{
+	GObjectClass *object_class;
+	EExtensionClass *extension_class;
+
+	object_class = G_OBJECT_CLASS (class);
+	object_class->dispose = online_accounts_dispose;
+	object_class->finalize = online_accounts_finalize;
+	object_class->constructed = online_accounts_constructed;
+
+	extension_class = E_EXTENSION_CLASS (class);
+	extension_class->extensible_type = E_TYPE_SHELL;
+}
+
+static void
+e_online_accounts_class_finalize (EOnlineAccountsClass *class)
+{
+}
+
+static void
+e_online_accounts_init (EOnlineAccounts *extension)
+{
+	extension->accounts = g_hash_table_new_full (
+		(GHashFunc) g_str_hash,
+		(GEqualFunc) g_str_equal,
+		(GDestroyNotify) g_free,
+		(GDestroyNotify) g_free);
+}
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+	e_online_accounts_register_type (type_module);
+	camel_sasl_xoauth_type_register (type_module);
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+}
+



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