[evolution] Use CalDAV to get list of available Google calendars



commit 219c5f7b4b334fcce3bb379c68eb5c1c46fe81e3
Author: Milan Crha <mcrha redhat com>
Date:   Thu Feb 19 11:50:57 2015 +0100

    Use CalDAV to get list of available Google calendars
    
    There used to be used libgdata for this, but it stopped working
    since some Google server change, thus better to use CalDAV interface,
    which provides the data the calendar config requires.

 modules/cal-config-caldav/e-caldav-chooser.c       |   10 +-
 modules/cal-config-google/e-cal-config-google.c    |    2 +-
 .../cal-config-google/e-google-chooser-button.c    |  115 +-
 .../cal-config-google/e-google-chooser-button.h    |    7 +-
 .../cal-config-google/e-google-chooser-dialog.c    |  210 ++-
 modules/cal-config-google/e-google-chooser.c       | 1867 ++++++++++++++++----
 modules/cal-config-google/e-google-chooser.h       |   45 +-
 7 files changed, 1901 insertions(+), 355 deletions(-)
---
diff --git a/modules/cal-config-caldav/e-caldav-chooser.c b/modules/cal-config-caldav/e-caldav-chooser.c
index c167834..9cfe510 100644
--- a/modules/cal-config-caldav/e-caldav-chooser.c
+++ b/modules/cal-config-caldav/e-caldav-chooser.c
@@ -792,9 +792,15 @@ caldav_chooser_process_response (SoupSession *session,
                "/IC:calendar-color",
                index);
 
-       if (color_spec != NULL)
+       if (color_spec != NULL) {
                has_color = gdk_color_parse (color_spec, &color);
-       else
+               if (!has_color && strlen (color_spec) == 9) {
+                       /* It can parse only #rrggbb, but servers like Google can return #rrggbbaa,
+                          thus strip the alpha channel and try again */
+                       color_spec[7] = '\0';
+                       has_color = gdk_color_parse (color_spec, &color);
+               }
+       } else
                has_color = FALSE;
 
        g_free (color_spec);
diff --git a/modules/cal-config-google/e-cal-config-google.c b/modules/cal-config-google/e-cal-config-google.c
index 9cba60b..85c45a8 100644
--- a/modules/cal-config-google/e-cal-config-google.c
+++ b/modules/cal-config-google/e-cal-config-google.c
@@ -88,7 +88,7 @@ cal_config_google_insert_widgets (ESourceConfigBackend *backend,
 
        e_source_config_add_user_entry (config, scratch_source);
 
-       widget = e_google_chooser_button_new (scratch_source);
+       widget = e_google_chooser_button_new (scratch_source, config);
        e_source_config_insert_widget (
                config, scratch_source, _("Calendar:"), widget);
        context->google_button = g_object_ref (widget);
diff --git a/modules/cal-config-google/e-google-chooser-button.c 
b/modules/cal-config-google/e-google-chooser-button.c
index a6dc703..98e6b1d 100644
--- a/modules/cal-config-google/e-google-chooser-button.c
+++ b/modules/cal-config-google/e-google-chooser-button.c
@@ -28,13 +28,14 @@
 
 struct _EGoogleChooserButtonPrivate {
        ESource *source;
+       ESourceConfig *config;
        GtkWidget *label;
 };
 
 enum {
        PROP_0,
-       PROP_DISPLAY_NAME,
-       PROP_SOURCE
+       PROP_SOURCE,
+       PROP_CONFIG
 };
 
 G_DEFINE_DYNAMIC_TYPE (
@@ -53,6 +54,16 @@ google_chooser_button_set_source (EGoogleChooserButton *button,
 }
 
 static void
+google_chooser_button_set_config (EGoogleChooserButton *button,
+                                  ESourceConfig *config)
+{
+       g_return_if_fail (E_IS_SOURCE_CONFIG (config));
+       g_return_if_fail (button->priv->config == NULL);
+
+       button->priv->config = g_object_ref (config);
+}
+
+static void
 google_chooser_button_set_property (GObject *object,
                                     guint property_id,
                                     const GValue *value,
@@ -64,6 +75,12 @@ google_chooser_button_set_property (GObject *object,
                                E_GOOGLE_CHOOSER_BUTTON (object),
                                g_value_get_object (value));
                        return;
+
+               case PROP_CONFIG:
+                       google_chooser_button_set_config (
+                               E_GOOGLE_CHOOSER_BUTTON (object),
+                               g_value_get_object (value));
+                       return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -82,6 +99,13 @@ google_chooser_button_get_property (GObject *object,
                                e_google_chooser_button_get_source (
                                E_GOOGLE_CHOOSER_BUTTON (object)));
                        return;
+
+               case PROP_CONFIG:
+                       g_value_set_object (
+                               value,
+                               e_google_chooser_button_get_config (
+                               E_GOOGLE_CHOOSER_BUTTON (object)));
+                       return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -94,15 +118,9 @@ google_chooser_button_dispose (GObject *object)
 
        priv = E_GOOGLE_CHOOSER_BUTTON_GET_PRIVATE (object);
 
-       if (priv->source != NULL) {
-               g_object_unref (priv->source);
-               priv->source = NULL;
-       }
-
-       if (priv->label != NULL) {
-               g_object_unref (priv->label);
-               priv->label = NULL;
-       }
+       g_clear_object (&priv->source);
+       g_clear_object (&priv->config);
+       g_clear_object (&priv->label);
 
        /* Chain up to parent's dispose() method. */
        G_OBJECT_CLASS (e_google_chooser_button_parent_class)->dispose (object);
@@ -142,23 +160,55 @@ google_chooser_button_constructed (GObject *object)
                binding_flags);
 }
 
+static GtkWindow *
+google_config_get_dialog_parent_cb (ECredentialsPrompter *prompter,
+                                   GtkWindow *dialog)
+{
+       return dialog;
+}
+
 static void
 google_chooser_button_clicked (GtkButton *button)
 {
        EGoogleChooserButtonPrivate *priv;
-       GtkWidget *chooser;
-       GtkWidget *dialog;
        gpointer parent;
+       ESourceRegistry *registry;
+       ECalClientSourceType source_type;
+       ECredentialsPrompter *prompter;
+       ESourceWebdav *webdav_extension;
+       ESourceAuthentication *authentication_extension;
+       SoupURI *uri;
+       GtkWidget *dialog;
+       GtkWidget *widget;
+       gulong handler_id;
 
        priv = E_GOOGLE_CHOOSER_BUTTON_GET_PRIVATE (button);
 
        parent = gtk_widget_get_toplevel (GTK_WIDGET (button));
        parent = gtk_widget_is_toplevel (parent) ? parent : NULL;
 
-       chooser = e_google_chooser_new (priv->source);
+       registry = e_source_config_get_registry (priv->config);
 
-       dialog = e_google_chooser_dialog_new (
-               E_GOOGLE_CHOOSER (chooser), parent);
+       source_type = e_cal_source_config_get_source_type (E_CAL_SOURCE_CONFIG (priv->config));
+
+       authentication_extension = e_source_get_extension (priv->source, E_SOURCE_EXTENSION_AUTHENTICATION);
+       webdav_extension = e_source_get_extension (priv->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+
+       uri = e_source_webdav_dup_soup_uri (webdav_extension);
+
+       e_google_chooser_construct_default_uri (uri, e_source_authentication_get_user 
(authentication_extension));
+
+       /* The host name is fixed, obviously. */
+       soup_uri_set_host (uri, "www.google.com");
+
+       /* Google's CalDAV interface requires a secure connection. */
+       soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTPS);
+
+       e_source_webdav_set_soup_uri (webdav_extension, uri);
+
+       widget = e_google_chooser_new (registry, priv->source, source_type);
+
+       dialog = e_google_chooser_dialog_new (E_GOOGLE_CHOOSER (widget), parent);
 
        if (parent != NULL)
                g_object_bind_property (
@@ -166,9 +216,18 @@ google_chooser_button_clicked (GtkButton *button)
                        dialog, "icon-name",
                        G_BINDING_SYNC_CREATE);
 
+       prompter = e_google_chooser_get_prompter (E_GOOGLE_CHOOSER (widget));
+
+       handler_id = g_signal_connect (prompter, "get-dialog-parent",
+               G_CALLBACK (google_config_get_dialog_parent_cb), dialog);
+
        gtk_dialog_run (GTK_DIALOG (dialog));
 
+       g_signal_handler_disconnect (prompter, handler_id);
+
        gtk_widget_destroy (dialog);
+
+       soup_uri_free (uri);
 }
 
 static void
@@ -198,6 +257,17 @@ e_google_chooser_button_class_init (EGoogleChooserButtonClass *class)
                        E_TYPE_SOURCE,
                        G_PARAM_READWRITE |
                        G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_CONFIG,
+               g_param_spec_object (
+                       "config",
+                       NULL,
+                       NULL,
+                       E_TYPE_SOURCE_CONFIG,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY));
 }
 
 static void
@@ -221,13 +291,15 @@ e_google_chooser_button_type_register (GTypeModule *type_module)
 }
 
 GtkWidget *
-e_google_chooser_button_new (ESource *source)
+e_google_chooser_button_new (ESource *source,
+                            ESourceConfig *config)
 {
        g_return_val_if_fail (E_IS_SOURCE (source), NULL);
 
        return g_object_new (
                E_TYPE_GOOGLE_CHOOSER_BUTTON,
-               "source", source, NULL);
+               "source", source,
+               "config", config, NULL);
 }
 
 ESource *
@@ -238,3 +310,10 @@ e_google_chooser_button_get_source (EGoogleChooserButton *button)
        return button->priv->source;
 }
 
+ESourceConfig *
+e_google_chooser_button_get_config (EGoogleChooserButton *button)
+{
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER_BUTTON (button), NULL);
+
+       return button->priv->config;
+}
diff --git a/modules/cal-config-google/e-google-chooser-button.h 
b/modules/cal-config-google/e-google-chooser-button.h
index 12f14d6..ea04fc9 100644
--- a/modules/cal-config-google/e-google-chooser-button.h
+++ b/modules/cal-config-google/e-google-chooser-button.h
@@ -18,6 +18,8 @@
 #ifndef E_GOOGLE_CHOOSER_BUTTON_H
 #define E_GOOGLE_CHOOSER_BUTTON_H
 
+#include <e-util/e-util.h>
+
 #include "e-google-chooser.h"
 
 /* Standard GObject macros */
@@ -57,9 +59,12 @@ struct _EGoogleChooserButtonClass {
 GType          e_google_chooser_button_get_type (void);
 void           e_google_chooser_button_type_register
                                                (GTypeModule *type_module);
-GtkWidget *    e_google_chooser_button_new     (ESource *source);
+GtkWidget *    e_google_chooser_button_new     (ESource *source,
+                                                ESourceConfig *config);
 ESource *      e_google_chooser_button_get_source
                                                (EGoogleChooserButton *button);
+ESourceConfig *        e_google_chooser_button_get_config
+                                               (EGoogleChooserButton *button);
 
 G_END_DECLS
 
diff --git a/modules/cal-config-google/e-google-chooser-dialog.c 
b/modules/cal-config-google/e-google-chooser-dialog.c
index 68ff2a9..d01dcc5 100644
--- a/modules/cal-config-google/e-google-chooser-dialog.c
+++ b/modules/cal-config-google/e-google-chooser-dialog.c
@@ -37,25 +37,26 @@ enum {
        PROP_CHOOSER
 };
 
+/* Forward Declarations */
+static void    google_chooser_dialog_populated_cb
+                                               (GObject *source_object,
+                                                GAsyncResult *result,
+                                                gpointer user_data);
+static void    google_chooser_dialog_credentials_prompt_cb
+                                               (GObject *source_object,
+                                                GAsyncResult *result,
+                                                gpointer user_data);
+
 G_DEFINE_DYNAMIC_TYPE (
        EGoogleChooserDialog,
        e_google_chooser_dialog,
        GTK_TYPE_DIALOG)
 
 static void
-google_chooser_dialog_populated_cb (EGoogleChooser *chooser,
-                                    GAsyncResult *result,
-                                    EGoogleChooserDialog *dialog)
+google_chooser_dialog_done (EGoogleChooserDialog *dialog,
+                            const GError *error)
 {
        GdkWindow *window;
-       GError *error = NULL;
-
-       e_google_chooser_populate_finish (chooser, result, &error);
-
-       /* Ignore cancellations, and leave the mouse cursor alone
-        * since the GdkWindow may have already been destroyed. */
-       if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-               goto exit;
 
        /* Reset the mouse cursor to normal. */
        window = gtk_widget_get_window (GTK_WIDGET (dialog));
@@ -67,12 +68,172 @@ google_chooser_dialog_populated_cb (EGoogleChooser *chooser,
                label = GTK_LABEL (dialog->priv->info_bar_label);
                gtk_label_set_text (label, error->message);
                gtk_widget_show (dialog->priv->info_bar);
+       }
+}
+
+/* It's a little weird to have the callback called on the #ESource,
+   but it's simpler than writing a proxy around the e-trust-prompt
+   async call, which would be unnecessary anyway. */
+static void
+google_chooser_dialog_trust_prompt_done_cb (GObject *source_object,
+                                           GAsyncResult *result,
+                                           gpointer user_data)
+{
+       EGoogleChooserDialog *dialog;
+       EGoogleChooser *chooser;
+       ETrustPromptResponse response = E_TRUST_PROMPT_RESPONSE_UNKNOWN;
+       GError *error = NULL;
+
+       g_return_if_fail (E_IS_SOURCE (source_object));
+       g_return_if_fail (E_IS_GOOGLE_CHOOSER_DIALOG (user_data));
+
+       dialog = E_GOOGLE_CHOOSER_DIALOG (user_data);
+       chooser = e_google_chooser_dialog_get_chooser (dialog);
+
+       if (!e_trust_prompt_run_for_source_finish (E_SOURCE (source_object), result, &response, &error)) {
+               if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                       /* close also the dialog */
+                       gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
+               } else {
+                       google_chooser_dialog_done (dialog, error);
+               }
+       } else if (response == E_TRUST_PROMPT_RESPONSE_ACCEPT ||
+                  response == E_TRUST_PROMPT_RESPONSE_ACCEPT_TEMPORARILY) {
+               e_google_chooser_populate (
+                       chooser, dialog->priv->cancellable,
+                       google_chooser_dialog_populated_cb,
+                       g_object_ref (dialog));
+       } else {
+               g_warn_if_fail (error == NULL);
+
+               error = e_google_chooser_new_ssl_trust_error (chooser);
+
+               google_chooser_dialog_done (dialog, error);
+       }
+
+       g_clear_error (&error);
+       g_object_unref (dialog);
+}
 
-               g_error_free (error);
-               goto exit;
+static void
+google_chooser_dialog_authenticate_cb (GObject *source_object,
+                                      GAsyncResult *result,
+                                      gpointer user_data)
+{
+       EGoogleChooserDialog *dialog = user_data;
+       EGoogleChooser *chooser;
+       GError *error = NULL;
+
+       g_return_if_fail (E_IS_GOOGLE_CHOOSER (source_object));
+
+       chooser = E_GOOGLE_CHOOSER (source_object);
+
+       if (!e_google_chooser_authenticate_finish (chooser, result, &error)) {
+               if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                       /* close also the dialog */
+                       gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
+               } else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) {
+                       e_google_chooser_run_credentials_prompt (
+                               chooser,
+                               google_chooser_dialog_credentials_prompt_cb,
+                               g_object_ref (dialog));
+
+               } else if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
+                       e_google_chooser_run_trust_prompt (chooser, GTK_WINDOW (dialog),
+                               dialog->priv->cancellable,
+                               google_chooser_dialog_trust_prompt_done_cb,
+                               g_object_ref (dialog));
+
+               /* We were either successful or got an unexpected error. */
+               } else {
+                       google_chooser_dialog_done (dialog, error);
+               }
+       } else {
+               g_warn_if_fail (error == NULL);
+
+               e_google_chooser_populate (
+                       chooser, dialog->priv->cancellable,
+                       google_chooser_dialog_populated_cb,
+                       g_object_ref (dialog));
        }
 
-exit:
+       g_clear_error (&error);
+       g_object_unref (dialog);
+}
+
+static void
+google_chooser_dialog_credentials_prompt_cb (GObject *source_object,
+                                            GAsyncResult *result,
+                                            gpointer user_data)
+{
+       EGoogleChooser *chooser;
+       EGoogleChooserDialog *dialog = user_data;
+       ENamedParameters *credentials = NULL;
+       GError *error = NULL;
+
+       g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (source_object));
+
+       chooser = e_google_chooser_dialog_get_chooser (dialog);
+       g_return_if_fail (chooser != NULL);
+
+       if (!e_google_chooser_run_credentials_prompt_finish (chooser, result, &credentials, &error)) {
+               if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+                       /* close also the dialog */
+                       gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
+               } else {
+                       google_chooser_dialog_done (dialog, error);
+               }
+       } else {
+               e_google_chooser_authenticate (chooser, credentials, dialog->priv->cancellable,
+                       google_chooser_dialog_authenticate_cb, g_object_ref (dialog));
+       }
+
+       e_named_parameters_free (credentials);
+       g_clear_error (&error);
+       g_object_unref (dialog);
+}
+
+static void
+google_chooser_dialog_populated_cb (GObject *source_object,
+                                    GAsyncResult *result,
+                                    gpointer user_data)
+{
+       EGoogleChooserDialog *dialog;
+       EGoogleChooser *chooser;
+       GError *error = NULL;
+
+       chooser = E_GOOGLE_CHOOSER (source_object);
+       dialog = E_GOOGLE_CHOOSER_DIALOG (user_data);
+
+       e_google_chooser_populate_finish (chooser, result, &error);
+
+       /* Ignore cancellations, and leave the mouse cursor alone
+        * since the GdkWindow may have already been destroyed. */
+       if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+               /* do nothing */
+
+       /* We will likely get this error on the first try, since WebDAV
+        * servers generally require authentication.  It means we waste a
+        * round-trip to the server, but we don't want to risk prompting
+        * for authentication unnecessarily. */
+       } else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) {
+               e_google_chooser_run_credentials_prompt (
+                       chooser,
+                       google_chooser_dialog_credentials_prompt_cb,
+                       g_object_ref (dialog));
+
+       } else if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
+               e_google_chooser_run_trust_prompt (chooser, GTK_WINDOW (dialog),
+                       dialog->priv->cancellable,
+                       google_chooser_dialog_trust_prompt_done_cb,
+                       g_object_ref (dialog));
+
+       /* We were either successful or got an unexpected error. */
+       } else {
+               google_chooser_dialog_done (dialog, error);
+       }
+
+       g_clear_error (&error);
        g_object_unref (dialog);
 }
 
@@ -182,6 +343,21 @@ google_chooser_dialog_constructed (GObject *object)
        /* Chain up to parent's constructed() method. */
        G_OBJECT_CLASS (e_google_chooser_dialog_parent_class)->constructed (object);
 
+       switch (e_google_chooser_get_source_type (dialog->priv->chooser)) {
+               case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
+                       title = _("Choose a Calendar");
+                       break;
+               case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
+                       title = _("Choose a Memo List");
+                       break;
+               case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
+                       title = _("Choose a Task List");
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       title = "";
+       }
+
        gtk_dialog_add_button (
                GTK_DIALOG (dialog),
                _("_Cancel"), GTK_RESPONSE_CANCEL);
@@ -195,7 +371,6 @@ google_chooser_dialog_constructed (GObject *object)
        gtk_dialog_set_response_sensitive (
                GTK_DIALOG (dialog), GTK_RESPONSE_APPLY, FALSE);
 
-       title = _("Choose a Calendar");
        gtk_window_set_title (GTK_WINDOW (dialog), title);
        gtk_window_set_default_size (GTK_WINDOW (dialog), 400, 400);
        gtk_container_set_border_width (GTK_CONTAINER (dialog), 5);
@@ -293,8 +468,9 @@ google_chooser_dialog_realize (GtkWidget *widget)
        g_object_unref (cursor);
 
        e_google_chooser_populate (
-               priv->chooser, priv->cancellable, (GAsyncReadyCallback)
-               google_chooser_dialog_populated_cb, g_object_ref (widget));
+               priv->chooser, priv->cancellable,
+               google_chooser_dialog_populated_cb,
+               g_object_ref (widget));
 }
 
 static void
diff --git a/modules/cal-config-google/e-google-chooser.c b/modules/cal-config-google/e-google-chooser.c
index 0435611..c4d9e01 100644
--- a/modules/cal-config-google/e-google-chooser.c
+++ b/modules/cal-config-google/e-google-chooser.c
@@ -18,135 +18,1140 @@
 #include "e-google-chooser.h"
 
 #include <config.h>
-#include <string.h>
-#include <gdata/gdata.h>
 #include <glib/gi18n-lib.h>
 
+#include <libsoup/soup.h>
+
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+
 #include <e-util/e-util.h>
+#include <libedataserverui/libedataserverui.h>
 
 #define E_GOOGLE_CHOOSER_GET_PRIVATE(obj) \
        (G_TYPE_INSTANCE_GET_PRIVATE \
        ((obj), E_TYPE_GOOGLE_CHOOSER, EGoogleChooserPrivate))
 
+#define XC(string) ((xmlChar *) string)
+
 #define CALDAV_EVENTS_PATH_FORMAT "/calendar/dav/%s/events"
 
+/* Standard Namespaces */
+#define NS_WEBDAV  "DAV:"
+#define NS_GOOGLE  "urn:ietf:params:xml:ns:caldav"
+
+/* Application-Specific Namespaces */
+#define NS_CALSRV  "http://calendarserver.org/ns/";
+#define NS_ICAL    "http://apple.com/ns/ical/";
+
 typedef struct _Context Context;
 
 struct _EGoogleChooserPrivate {
+       ESourceRegistry *registry;
+       ECredentialsPrompter *prompter;
        ESource *source;
+       ECalClientSourceType source_type;
+       SoupSession *session;
+       GList *user_address_set;
+       gchar *username;
+       gchar *password;
+       gchar *certificate_pem;
+       GTlsCertificateFlags certificate_errors;
+       gchar *error_text;
+       gboolean first_auth_request;
 };
 
 struct _Context {
-       GCancellable *cancellable;
-       GDataCalendarService *service;
-       GDataClientLoginAuthorizer *authorizer;
+       SoupSession *session;
+       ESourceRegistry *registry;
        ESource *source;
+
+       GCancellable *cancellable;
+       gulong cancelled_handler_id;
+
+       GList *user_address_set;
 };
 
 enum {
        PROP_0,
-       PROP_SOURCE
+       PROP_REGISTRY,
+       PROP_SOURCE,
+       PROP_SOURCE_TYPE
 };
 
+/* Mainly for readability. */
 enum {
-       COLUMN_COLOR,
-       COLUMN_PATH,
-       COLUMN_TITLE,
-       COLUMN_WRITABLE,
+       DEPTH_0,
+       DEPTH_1
+};
+
+typedef enum {
+       SUPPORTS_VEVENT = 1 << 0,
+       SUPPORTS_VTODO = 1 << 1,
+       SUPPORTS_VJOURNAL = 1 << 2,
+       SUPPORTS_ALL = 0x7
+} SupportedComponentSet;
+
+enum {
+       COLUMN_DISPLAY_NAME,            /* G_TYPE_STRING */
+       COLUMN_PATH_ENCODED,            /* G_TYPE_STRING */
+       COLUMN_PATH_DECODED,            /* G_TYPE_STRING */
+       COLUMN_COLOR,                   /* GDK_TYPE_COLOR */
+       COLUMN_HAS_COLOR,               /* G_TYPE_BOOLEAN */
        NUM_COLUMNS
 };
 
-G_DEFINE_DYNAMIC_TYPE (
-       EGoogleChooser,
-       e_google_chooser,
-       GTK_TYPE_TREE_VIEW)
+/* Forward Declarations */
+static void    google_chooser_get_collection_details
+                               (SoupSession *session,
+                                SoupMessage *message,
+                                const gchar *path_or_uri,
+                                GSimpleAsyncResult *simple,
+                                Context *context);
+
+G_DEFINE_DYNAMIC_TYPE (EGoogleChooser, e_google_chooser, GTK_TYPE_TREE_VIEW)
+
+static gconstpointer
+compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
+                                         gsize *out_len)
+{
+#ifdef LIBXML2_NEW_BUFFER
+       *out_len = xmlOutputBufferGetSize (buf);
+       return xmlOutputBufferGetContent (buf);
+#else
+       *out_len = buf->buffer->use;
+       return buf->buffer->content;
+#endif
+}
 
 static void
-context_free (Context *context)
+context_cancel_message (GCancellable *cancellable,
+                        Context *context)
 {
-       if (context->cancellable != NULL)
-               g_object_unref (context->cancellable);
+       soup_session_abort (context->session);
+}
+
+static Context *
+context_new (EGoogleChooser *chooser,
+             GCancellable *cancellable)
+{
+       Context *context;
+
+       context = g_slice_new0 (Context);
+       context->session = g_object_ref (chooser->priv->session);
+       context->registry = g_object_ref (chooser->priv->registry);
+       context->source = g_object_ref (chooser->priv->source);
+
+       if (cancellable != NULL) {
+               context->cancellable = g_object_ref (cancellable);
+               context->cancelled_handler_id = g_cancellable_connect (
+                       context->cancellable,
+                       G_CALLBACK (context_cancel_message),
+                       context, (GDestroyNotify) NULL);
+       }
 
-       if (context->service != NULL)
-               g_object_unref (context->service);
+       return context;
+}
 
-       if (context->authorizer != NULL)
-               g_object_unref (context->authorizer);
+static void
+context_free (Context *context)
+{
+       if (context->session != NULL)
+               g_object_unref (context->session);
+
+       if (context->registry != NULL)
+               g_object_unref (context->registry);
 
        if (context->source != NULL)
                g_object_unref (context->source);
 
+       if (context->cancellable != NULL) {
+               g_cancellable_disconnect (
+                       context->cancellable,
+                       context->cancelled_handler_id);
+               g_object_unref (context->cancellable);
+       }
+
+       g_list_free_full (
+               context->user_address_set,
+               (GDestroyNotify) g_free);
+
        g_slice_free (Context, context);
 }
 
-static gchar *
-google_chooser_extract_caldav_events_path (const gchar *uri)
+static void
+google_chooser_redirect (SoupMessage *message,
+                         SoupSession *session)
 {
        SoupURI *soup_uri;
-       gchar *resource_name;
-       gchar *path;
-       gchar *cp;
+       const gchar *location;
 
-       soup_uri = soup_uri_new (uri);
-       g_return_val_if_fail (soup_uri != NULL, NULL);
+       if (!SOUP_STATUS_IS_REDIRECTION (message->status_code))
+               return;
 
-       /* Isolate the resource name in the "feeds" URI. */
+       location = soup_message_headers_get_list (
+               message->response_headers, "Location");
 
-       cp = strstr (soup_uri->path, "/feeds/");
-       g_return_val_if_fail (cp != NULL, NULL);
+       if (location == NULL)
+               return;
 
-       /* strlen("/feeds/) == 7 */
-       resource_name = g_strdup (cp + 7);
-       cp = strchr (resource_name, '/');
-       if (cp != NULL)
-               *cp = '\0';
+       soup_uri = soup_uri_new_with_base (
+               soup_message_get_uri (message), location);
 
-       /* Decode any encoded 'at' symbols ('%40' -> '@'). */
-       if (strstr (resource_name, "%40") != NULL) {
-               gchar **segments;
+       if (soup_uri == NULL) {
+               soup_message_set_status_full (
+                       message, SOUP_STATUS_MALFORMED,
+                       "Invalid Redirect URL");
+               return;
+       }
 
-               segments = g_strsplit (resource_name, "%40", 0);
-               g_free (resource_name);
-               resource_name = g_strjoinv ("@", segments);
-               g_strfreev (segments);
+       soup_message_set_uri (message, soup_uri);
+       soup_session_requeue_message (session, message);
+
+       soup_uri_free (soup_uri);
+}
+
+static G_GNUC_NULL_TERMINATED SoupMessage *
+google_chooser_new_propfind (SoupSession *session,
+                             SoupURI *soup_uri,
+                             gint depth,
+                             ...)
+{
+       GHashTable *namespaces;
+       SoupMessage *message;
+       xmlDocPtr doc;
+       xmlNodePtr root;
+       xmlNodePtr node;
+       xmlNsPtr ns;
+       xmlOutputBufferPtr output;
+       gconstpointer content;
+       gsize length;
+       gpointer key;
+       va_list va;
+
+       /* Construct the XML content. */
+
+       doc = xmlNewDoc (XC ("1.0"));
+       node = xmlNewDocNode (doc, NULL, XC ("propfind"), NULL);
+
+       /* Build a hash table of namespace URIs to xmlNs structs. */
+       namespaces = g_hash_table_new (NULL, NULL);
+
+       ns = xmlNewNs (node, XC (NS_GOOGLE), XC ("C"));
+       g_hash_table_insert (namespaces, (gpointer) NS_GOOGLE, ns);
+
+       ns = xmlNewNs (node, XC (NS_CALSRV), XC ("CS"));
+       g_hash_table_insert (namespaces, (gpointer) NS_CALSRV, ns);
+
+       ns = xmlNewNs (node, XC (NS_ICAL), XC ("IC"));
+       g_hash_table_insert (namespaces, (gpointer) NS_ICAL, ns);
+
+       /* Add WebDAV last since we use it below. */
+       ns = xmlNewNs (node, XC (NS_WEBDAV), XC ("D"));
+       g_hash_table_insert (namespaces, (gpointer) NS_WEBDAV, ns);
+
+       xmlSetNs (node, ns);
+       xmlDocSetRootElement (doc, node);
+
+       node = xmlNewTextChild (node, ns, XC ("prop"), NULL);
+
+       va_start (va, depth);
+       while ((key = va_arg (va, gpointer)) != NULL) {
+               xmlChar *name;
+
+               ns = g_hash_table_lookup (namespaces, key);
+               name = va_arg (va, xmlChar *);
+
+               if (ns != NULL && name != NULL)
+                       xmlNewTextChild (node, ns, name, NULL);
+               else
+                       g_warn_if_reached ();
        }
+       va_end (va);
 
-       /* Use the decoded resource name in the CalDAV events path. */
-       path = g_strdup_printf (CALDAV_EVENTS_PATH_FORMAT, resource_name);
+       g_hash_table_destroy (namespaces);
 
-       g_free (resource_name);
+       /* Construct the SoupMessage. */
 
-       soup_uri_free (soup_uri);
+       message = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, soup_uri);
+
+       soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
+
+       soup_message_headers_append (
+               message->request_headers,
+               "User-Agent", "Evolution/" VERSION);
+
+       soup_message_headers_append (
+               message->request_headers,
+               "Depth", (depth == 0) ? "0" : "1");
+
+       output = xmlAllocOutputBuffer (NULL);
+
+       root = xmlDocGetRootElement (doc);
+       xmlNodeDumpOutput (output, doc, root, 0, 1, NULL);
+       xmlOutputBufferFlush (output);
+
+       content = compat_libxml_output_buffer_get_content (output, &length);
 
-       return path;
+       soup_message_set_request (
+               message, "application/xml", SOUP_MEMORY_COPY,
+               content, length);
+
+       xmlOutputBufferClose (output);
+
+       soup_message_add_header_handler (
+               message, "got-body", "Location",
+               G_CALLBACK (google_chooser_redirect), session);
+
+       return message;
+}
+
+static void
+google_chooser_authenticate_cb (SoupSession *session,
+                                SoupMessage *message,
+                                SoupAuth *auth,
+                                gboolean retrying,
+                                EGoogleChooser *chooser)
+{
+       ESource *source;
+       ESourceAuthentication *extension;
+
+       source = e_google_chooser_get_source (chooser);
+       extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+       /* If our password was rejected, let the operation fail. */
+       if (retrying)
+               return;
+
+       if (!chooser->priv->username)
+               chooser->priv->username = e_source_authentication_dup_user (extension);
+
+       /* If we don't have a username, let the operation fail. */
+       if (chooser->priv->username == NULL || *chooser->priv->username == '\0')
+               return;
+
+       /* If we don't have a password, let the operation fail. */
+       if (chooser->priv->password == NULL || *chooser->priv->password == '\0')
+               return;
+
+       soup_auth_authenticate (auth, chooser->priv->username, chooser->priv->password);
+}
+
+static void
+google_chooser_configure_session (EGoogleChooser *chooser,
+                                  SoupSession *session)
+{
+       if (g_getenv ("GOOGLE_DEBUG") != NULL) {
+               SoupLogger *logger;
+
+               logger = soup_logger_new (
+                       SOUP_LOGGER_LOG_BODY, 100 * 1024 * 1024);
+               soup_session_add_feature (
+                       session, SOUP_SESSION_FEATURE (logger));
+               g_object_unref (logger);
+       }
+
+       g_object_set (
+               session,
+               SOUP_SESSION_TIMEOUT, 90,
+               SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
+               SOUP_SESSION_SSL_STRICT, TRUE,
+               NULL);
+
+       g_signal_connect (
+               session, "authenticate",
+               G_CALLBACK (google_chooser_authenticate_cb), chooser);
+}
+
+static gboolean
+google_chooser_check_successful (EGoogleChooser *chooser,
+                                SoupMessage *message,
+                                 GError **error)
+{
+       GIOErrorEnum error_code;
+       GTlsCertificate *certificate = NULL;
+
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), FALSE);
+
+       /* Loosely copied from the GVFS DAV backend. */
+
+       if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code))
+               return TRUE;
+
+       switch (message->status_code) {
+               case SOUP_STATUS_CANCELLED:
+                       error_code = G_IO_ERROR_CANCELLED;
+                       break;
+               case SOUP_STATUS_NOT_FOUND:
+                       error_code = G_IO_ERROR_NOT_FOUND;
+                       break;
+               case SOUP_STATUS_UNAUTHORIZED:
+               case SOUP_STATUS_PAYMENT_REQUIRED:
+               case SOUP_STATUS_FORBIDDEN:
+                       error_code = G_IO_ERROR_PERMISSION_DENIED;
+                       break;
+               case SOUP_STATUS_REQUEST_TIMEOUT:
+                       error_code = G_IO_ERROR_TIMED_OUT;
+                       break;
+               case SOUP_STATUS_CANT_RESOLVE:
+                       error_code = G_IO_ERROR_HOST_NOT_FOUND;
+                       break;
+               case SOUP_STATUS_NOT_IMPLEMENTED:
+                       error_code = G_IO_ERROR_NOT_SUPPORTED;
+                       break;
+               case SOUP_STATUS_INSUFFICIENT_STORAGE:
+                       error_code = G_IO_ERROR_NO_SPACE;
+                       break;
+               case SOUP_STATUS_SSL_FAILED:
+                       g_free (chooser->priv->certificate_pem);
+                       chooser->priv->certificate_pem = NULL;
+
+                       g_object_get (G_OBJECT (message),
+                               "tls-certificate", &certificate,
+                               "tls-errors", &chooser->priv->certificate_errors,
+                               NULL);
+                       if (certificate) {
+                               g_object_get (certificate, "certificate-pem", 
&chooser->priv->certificate_pem, NULL);
+                               g_object_unref (certificate);
+                       }
+
+                       g_free (chooser->priv->error_text);
+                       chooser->priv->error_text = g_strdup (message->reason_phrase);
+
+                       g_set_error (
+                               error, SOUP_HTTP_ERROR, message->status_code,
+                               _("HTTP Error: %s"), message->reason_phrase);
+                       return FALSE;
+               default:
+                       error_code = G_IO_ERROR_FAILED;
+                       break;
+       }
+
+       g_set_error (
+               error, G_IO_ERROR, error_code,
+               _("HTTP Error: %s"), message->reason_phrase);
+
+       return FALSE;
+}
+
+static xmlDocPtr
+google_chooser_parse_xml (EGoogleChooser *chooser,
+                         SoupMessage *message,
+                          const gchar *expected_name,
+                          GError **error)
+{
+       xmlDocPtr doc;
+       xmlNodePtr root;
+
+       if (!google_chooser_check_successful (chooser, message, error))
+               return NULL;
+
+       doc = xmlReadMemory (
+               message->response_body->data,
+               message->response_body->length,
+               "response.xml", NULL,
+               XML_PARSE_NONET |
+               XML_PARSE_NOWARNING |
+               XML_PARSE_NOCDATA |
+               XML_PARSE_COMPACT);
+
+       if (doc == NULL) {
+               g_set_error_literal (
+                       error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       _("Could not parse response"));
+               return NULL;
+       }
+
+       root = xmlDocGetRootElement (doc);
+
+       if (root == NULL || root->children == NULL) {
+               g_set_error_literal (
+                       error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       _("Empty response"));
+               xmlFreeDoc (doc);
+               return NULL;
+       }
+
+       if (g_strcmp0 ((gchar *) root->name, expected_name) != 0) {
+               g_set_error_literal (
+                       error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       _("Unexpected reply from server"));
+               xmlFreeDoc (doc);
+               return NULL;
+       }
+
+       return doc;
+}
+
+static xmlXPathObjectPtr
+google_chooser_get_xpath (xmlXPathContextPtr xp_ctx,
+                          const gchar *path_format,
+                          ...)
+{
+       xmlXPathObjectPtr xp_obj;
+       va_list va;
+       gchar *path;
+
+       va_start (va, path_format);
+       path = g_strdup_vprintf (path_format, va);
+       va_end (va);
+
+       xp_obj = xmlXPathEvalExpression (XC (path), xp_ctx);
+
+       g_free (path);
+
+       if (xp_obj == NULL)
+               return NULL;
+
+       if (xp_obj->type != XPATH_NODESET) {
+               xmlXPathFreeObject (xp_obj);
+               return NULL;
+       }
+
+       if (xmlXPathNodeSetGetLength (xp_obj->nodesetval) == 0) {
+               xmlXPathFreeObject (xp_obj);
+               return NULL;
+       }
+
+       return xp_obj;
 }
 
 static gchar *
-google_chooser_decode_user (const gchar *user)
+google_chooser_get_xpath_string (xmlXPathContextPtr xp_ctx,
+                                 const gchar *path_format,
+                                 ...)
 {
-       gchar *decoded_user;
+       xmlXPathObjectPtr xp_obj;
+       va_list va;
+       gchar *path;
+       gchar *expression;
+       gchar *string = NULL;
 
-       if (user == NULL || *user == '\0')
+       va_start (va, path_format);
+       path = g_strdup_vprintf (path_format, va);
+       va_end (va);
+
+       expression = g_strdup_printf ("string(%s)", path);
+       xp_obj = xmlXPathEvalExpression (XC (expression), xp_ctx);
+       g_free (expression);
+
+       g_free (path);
+
+       if (xp_obj == NULL)
                return NULL;
 
-       /* Decode any encoded 'at' symbols ('%40' -> '@'). */
-       if (strstr (user, "%40") != NULL) {
-               gchar **segments;
+       if (xp_obj->type == XPATH_STRING)
+               string = g_strdup ((gchar *) xp_obj->stringval);
 
-               segments = g_strsplit (user, "%40", 0);
-               decoded_user = g_strjoinv ("@", segments);
-               g_strfreev (segments);
+       /* If the string is empty, return NULL. */
+       if (string != NULL && *string == '\0') {
+               g_free (string);
+               string = NULL;
+       }
 
-       /* If no domain is given, append "@gmail.com". */
-       } else if (strstr (user, "@") == NULL) {
-               decoded_user = g_strconcat (user, "@gmail.com", NULL);
+       xmlXPathFreeObject (xp_obj);
 
-       /* Otherwise the user name should be fine as is. */
-       } else {
-               decoded_user = g_strdup (user);
+       return string;
+}
+
+static void
+google_chooser_process_user_address_set (xmlXPathContextPtr xp_ctx,
+                                         Context *context)
+{
+       xmlXPathObjectPtr xp_obj;
+       gint ii, length;
+
+       /* XXX Is response[1] safe to assume? */
+       xp_obj = google_chooser_get_xpath (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response"
+               "/D:propstat"
+               "/D:prop"
+               "/C:calendar-user-address-set");
+
+       if (xp_obj == NULL)
+               return;
+
+       length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
+
+       for (ii = 0; ii < length; ii++) {
+               GList *duplicate;
+               const gchar *address;
+               gchar *href;
+
+               href = google_chooser_get_xpath_string (
+                       xp_ctx,
+                       "/D:multistatus"
+                       "/D:response"
+                       "/D:propstat"
+                       "/D:prop"
+                       "/C:calendar-user-address-set"
+                       "/D:href[%d]", ii + 1);
+
+               if (href == NULL)
+                       continue;
+
+               if (!g_str_has_prefix (href, "mailto:";)) {
+                       g_free (href);
+                       continue;
+               }
+
+               /* strlen("mailto:";) == 7 */
+               address = href + 7;
+
+               /* Avoid duplicates. */
+               duplicate = g_list_find_custom (
+                       context->user_address_set,
+                       address, (GCompareFunc) strdup);
+
+               if (duplicate != NULL) {
+                       g_free (href);
+                       continue;
+               }
+
+               context->user_address_set = g_list_append (
+                       context->user_address_set, g_strdup (address));
+
+               g_free (href);
        }
 
-       return decoded_user;
+       xmlXPathFreeObject (xp_obj);
+}
+
+static SupportedComponentSet
+google_chooser_get_supported_component_set (xmlXPathContextPtr xp_ctx,
+                                            gint index)
+{
+       xmlXPathObjectPtr xp_obj;
+       SupportedComponentSet set = 0;
+       gint ii, length;
+
+       xp_obj = google_chooser_get_xpath (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response[%d]"
+               "/D:propstat"
+               "/D:prop"
+               "/C:supported-calendar-component-set"
+               "/C:comp", index);
+
+       /* If the property is not present, assume all component
+        * types are supported.  (RFC 4791, Section 5.2.3) */
+       if (xp_obj == NULL)
+               return SUPPORTS_ALL;
+
+       length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
+
+       for (ii = 0; ii < length; ii++) {
+               gchar *name;
+
+               name = google_chooser_get_xpath_string (
+                       xp_ctx,
+                       "/D:multistatus"
+                       "/D:response[%d]"
+                       "/D:propstat"
+                       "/D:prop"
+                       "/C:supported-calendar-component-set"
+                       "/C:comp[%d]"
+                       "/@name", index, ii + 1);
+
+               if (name == NULL)
+                       continue;
+
+               if (g_ascii_strcasecmp (name, "VEVENT") == 0)
+                       set |= SUPPORTS_VEVENT;
+               else if (g_ascii_strcasecmp (name, "VTODO") == 0)
+                       set |= SUPPORTS_VTODO;
+               else if (g_ascii_strcasecmp (name, "VJOURNAL") == 0)
+                       set |= SUPPORTS_VJOURNAL;
+
+               g_free (name);
+       }
+
+       xmlXPathFreeObject (xp_obj);
+
+       return set;
+}
+
+static void
+google_chooser_process_response (SoupSession *session,
+                                 SoupMessage *message,
+                                 GSimpleAsyncResult *simple,
+                                 xmlXPathContextPtr xp_ctx,
+                                 gint index)
+{
+       GObject *object;
+       xmlXPathObjectPtr xp_obj;
+       SupportedComponentSet comp_set;
+       EGoogleChooser *chooser;
+       GtkTreeModel *tree_model;
+       GtkTreeIter iter;
+       GdkColor color;
+       gchar *color_spec;
+       gchar *display_name;
+       gchar *href_decoded;
+       gchar *href_encoded;
+       gchar *status_line;
+       guint status;
+       gboolean has_color;
+       gboolean success;
+
+       /* This returns a new reference, for reasons passing understanding. */
+       object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
+
+       chooser = E_GOOGLE_CHOOSER (object);
+       tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (object));
+
+       g_object_unref (object);
+
+       status_line = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response[%d]"
+               "/D:propstat"
+               "/D:status",
+               index);
+
+       if (status_line == NULL)
+               return;
+
+       success = soup_headers_parse_status_line (
+               status_line, NULL, &status, NULL);
+
+       g_free (status_line);
+
+       if (!success || status != SOUP_STATUS_OK)
+               return;
+
+       href_encoded = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response[%d]"
+               "/D:href",
+               index);
+
+       if (href_encoded == NULL)
+               return;
+
+       href_decoded = soup_uri_decode (href_encoded);
+
+       /* Get the display name or fall back to the href. */
+
+       display_name = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response[%d]"
+               "/D:propstat"
+               "/D:prop"
+               "/D:displayname",
+               index);
+
+       if (display_name == NULL) {
+               gchar *href_copy, *cp;
+
+               href_copy = g_strdup (href_decoded);
+
+               /* Use the last non-empty path segment. */
+               while ((cp = strrchr (href_copy, '/')) != NULL) {
+                       if (*(cp + 1) == '\0')
+                               *cp = '\0';
+                       else {
+                               display_name = g_strdup (cp + 1);
+                               break;
+                       }
+               }
+
+               g_free (href_copy);
+       }
+
+       /* Make sure the resource is a calendar. */
+
+       xp_obj = google_chooser_get_xpath (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response[%d]"
+               "/D:propstat"
+               "/D:prop"
+               "/D:resourcetype"
+               "/C:calendar",
+               index);
+
+       if (xp_obj == NULL)
+               goto exit;
+
+       xmlXPathFreeObject (xp_obj);
+
+       /* Get the color specification string. */
+
+       color_spec = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response[%d]"
+               "/D:propstat"
+               "/D:prop"
+               "/IC:calendar-color",
+               index);
+
+       if (color_spec != NULL) {
+               has_color = gdk_color_parse (color_spec, &color);
+               if (!has_color && strlen (color_spec) == 9) {
+                       /* It can parse only #rrggbb, but servers like Google can return #rrggbbaa,
+                          thus strip the alpha channel and try again */
+                       color_spec[7] = '\0';
+                       has_color = gdk_color_parse (color_spec, &color);
+               }
+       } else
+               has_color = FALSE;
+
+       g_free (color_spec);
+
+       /* Which calendar component types are supported? */
+
+       comp_set = google_chooser_get_supported_component_set (xp_ctx, index);
+
+       switch (e_google_chooser_get_source_type (chooser)) {
+               case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
+                       if ((comp_set & SUPPORTS_VEVENT) == 0)
+                               goto exit;
+                       break;
+               case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
+                       if ((comp_set & SUPPORTS_VJOURNAL) == 0)
+                               goto exit;
+                       break;
+               case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
+                       if ((comp_set & SUPPORTS_VTODO) == 0)
+                               goto exit;
+                       break;
+               default:
+                       goto exit;
+       }
+
+       /* Append a new tree model row. */
+
+       gtk_list_store_append (GTK_LIST_STORE (tree_model), &iter);
+
+       gtk_list_store_set (
+               GTK_LIST_STORE (tree_model), &iter,
+               COLUMN_DISPLAY_NAME, display_name,
+               COLUMN_PATH_ENCODED, href_encoded,
+               COLUMN_PATH_DECODED, href_decoded,
+               COLUMN_COLOR, has_color ? &color : NULL,
+               COLUMN_HAS_COLOR, has_color,
+               -1);
+
+exit:
+       g_free (display_name);
+       g_free (href_decoded);
+       g_free (href_encoded);
+}
+
+static void
+google_chooser_collection_details_cb (SoupSession *session,
+                                      SoupMessage *message,
+                                      GSimpleAsyncResult *simple)
+{
+       xmlDocPtr doc;
+       xmlXPathContextPtr xp_ctx;
+       xmlXPathObjectPtr xp_obj;
+       GObject *chooser_obj;
+       GError *error = NULL;
+
+       chooser_obj = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
+
+       doc = google_chooser_parse_xml (E_GOOGLE_CHOOSER (chooser_obj), message, "multistatus", &error);
+
+       g_clear_object (&chooser_obj);
+
+       if (error != NULL) {
+               g_warn_if_fail (doc == NULL);
+               g_simple_async_result_set_from_error (simple, error);
+               g_error_free (error);
+               goto exit;
+       }
+
+       xp_ctx = xmlXPathNewContext (doc);
+       xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
+       xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_GOOGLE));
+       xmlXPathRegisterNs (xp_ctx, XC ("CS"), XC (NS_CALSRV));
+       xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
+
+       xp_obj = google_chooser_get_xpath (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response");
+
+       if (xp_obj != NULL) {
+               gint length, ii;
+
+               length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
+
+               for (ii = 0; ii < length; ii++)
+                       google_chooser_process_response (
+                               session, message, simple, xp_ctx, ii + 1);
+
+               xmlXPathFreeObject (xp_obj);
+       }
+
+       xmlXPathFreeContext (xp_ctx);
+       xmlFreeDoc (doc);
+
+exit:
+       /* If we were cancelled then we're in a GCancellable::cancelled
+        * signal handler right now and GCancellable has its mutex locked,
+        * which means calling g_cancellable_disconnect() now will deadlock
+        * when it too tries to acquire the mutex.  So defer the GAsyncResult
+        * completion to an idle callback to avoid this deadlock. */
+       g_simple_async_result_complete_in_idle (simple);
+       g_object_unref (simple);
+}
+
+static void
+google_chooser_get_collection_details (SoupSession *session,
+                                       SoupMessage *message,
+                                       const gchar *path_or_uri,
+                                       GSimpleAsyncResult *simple,
+                                       Context *context)
+{
+       SoupURI *soup_uri;
+
+       soup_uri = soup_uri_new (path_or_uri);
+       if (!soup_uri ||
+           !soup_uri_get_scheme (soup_uri) ||
+           !soup_uri_get_host (soup_uri) ||
+           !soup_uri_get_path (soup_uri) ||
+           !*soup_uri_get_scheme (soup_uri) ||
+           !*soup_uri_get_host (soup_uri) ||
+           !*soup_uri_get_path (soup_uri)) {
+               /* it's a path only, not full uri */
+               if (soup_uri)
+                       soup_uri_free (soup_uri);
+               soup_uri = soup_uri_copy (soup_message_get_uri (message));
+               soup_uri_set_path (soup_uri, path_or_uri);
+       }
+
+       message = google_chooser_new_propfind (
+               session, soup_uri, DEPTH_1,
+               NS_WEBDAV, XC ("displayname"),
+               NS_WEBDAV, XC ("resourcetype"),
+               NS_GOOGLE, XC ("calendar-description"),
+               NS_GOOGLE, XC ("supported-calendar-component-set"),
+               NS_GOOGLE, XC ("calendar-user-address-set"),
+               NS_CALSRV, XC ("getctag"),
+               NS_ICAL,   XC ("calendar-color"),
+               NULL);
+
+       e_soup_ssl_trust_connect (message, context->source);
+
+       /* This takes ownership of the message. */
+       soup_session_queue_message (
+               session, message, (SoupSessionCallback)
+               google_chooser_collection_details_cb, simple);
+
+       soup_uri_free (soup_uri);
+}
+
+static void
+google_chooser_calendar_home_set_cb (SoupSession *session,
+                                     SoupMessage *message,
+                                     GSimpleAsyncResult *simple)
+{
+       Context *context;
+       SoupURI *soup_uri;
+       xmlDocPtr doc;
+       xmlXPathContextPtr xp_ctx;
+       xmlXPathObjectPtr xp_obj;
+       gchar *calendar_home_set;
+       GObject *chooser_obj;
+       GError *error = NULL;
+
+       context = g_simple_async_result_get_op_res_gpointer (simple);
+       chooser_obj = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
+
+       doc = google_chooser_parse_xml (E_GOOGLE_CHOOSER (chooser_obj), message, "multistatus", &error);
+
+       g_clear_object (&chooser_obj);
+
+       /* If we were cancelled then we're in a GCancellable::cancelled
+        * signal handler right now and GCancellable has its mutex locked,
+        * which means calling g_cancellable_disconnect() now will deadlock
+        * when it too tries to acquire the mutex.  So defer the GAsyncResult
+        * completion to an idle callback to avoid this deadlock. */
+       if (error != NULL) {
+               g_simple_async_result_set_from_error (simple, error);
+               g_simple_async_result_complete_in_idle (simple);
+               g_object_unref (simple);
+               g_error_free (error);
+               return;
+       }
+
+       g_return_if_fail (doc != NULL);
+
+       xp_ctx = xmlXPathNewContext (doc);
+       xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
+       xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_GOOGLE));
+
+       /* Record any "C:calendar-user-address-set" properties. */
+       google_chooser_process_user_address_set (xp_ctx, context);
+
+       /* Try to find the calendar home URL using the
+        * following properties in order of preference:
+        *
+        *   "C:calendar-home-set"
+        *   "D:current-user-principal"
+        *   "D:principal-URL"
+        *
+        * If the second or third URL preference is used, rerun
+        * the PROPFIND method on that URL at Depth=1 in hopes
+        * of getting a proper "C:calendar-home-set" property.
+        */
+
+       /* FIXME There can be multiple "D:href" elements for a
+        *       "C:calendar-home-set".  We're only processing
+        *       the first one.  Need to iterate over them. */
+
+       calendar_home_set = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response"
+               "/D:propstat"
+               "/D:prop"
+               "/C:calendar-home-set"
+               "/D:href");
+
+       if (calendar_home_set != NULL)
+               goto get_collection_details;
+
+       g_free (calendar_home_set);
+
+       calendar_home_set = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response"
+               "/D:propstat"
+               "/D:prop"
+               "/D:current-user-principal"
+               "/D:href");
+
+       if (calendar_home_set != NULL)
+               goto retry_propfind;
+
+       g_free (calendar_home_set);
+
+       calendar_home_set = google_chooser_get_xpath_string (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response"
+               "/D:propstat"
+               "/D:prop"
+               "/D:principal-URL"
+               "/D:href");
+
+       if (calendar_home_set != NULL)
+               goto retry_propfind;
+
+       g_free (calendar_home_set);
+       calendar_home_set = NULL;
+
+       /* None of the aforementioned properties are present.  If the
+        * user-supplied CalDAV URL is a calendar resource, use that. */
+
+       xp_obj = google_chooser_get_xpath (
+               xp_ctx,
+               "/D:multistatus"
+               "/D:response"
+               "/D:propstat"
+               "/D:prop"
+               "/D:resourcetype"
+               "/C:calendar");
+
+       if (xp_obj != NULL) {
+               soup_uri = soup_message_get_uri (message);
+
+               if (soup_uri->path != NULL && *soup_uri->path != '\0') {
+                       gchar *slash;
+
+                       soup_uri = soup_uri_copy (soup_uri);
+
+                       slash = strrchr (soup_uri->path, '/');
+                       while (slash != NULL && slash != soup_uri->path) {
+
+                               if (slash[1] != '\0') {
+                                       slash[1] = '\0';
+                                       calendar_home_set =
+                                               g_strdup (soup_uri->path);
+                                       break;
+                               }
+
+                               slash[0] = '\0';
+                               slash = strrchr (soup_uri->path, '/');
+                       }
+
+                       soup_uri_free (soup_uri);
+               }
+
+               xmlXPathFreeObject (xp_obj);
+       }
+
+       if (calendar_home_set == NULL || *calendar_home_set == '\0') {
+               g_free (calendar_home_set);
+               g_simple_async_result_set_error (
+                       simple, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       _("Could not locate user's calendars"));
+               g_simple_async_result_complete (simple);
+               g_object_unref (simple);
+               return;
+       }
+
+get_collection_details:
+
+       xmlXPathFreeContext (xp_ctx);
+       xmlFreeDoc (doc);
+
+       google_chooser_get_collection_details (
+               session, message, calendar_home_set, simple, context);
+
+       g_free (calendar_home_set);
+
+       return;
+
+retry_propfind:
+
+       xmlXPathFreeContext (xp_ctx);
+       xmlFreeDoc (doc);
+
+       soup_uri = soup_uri_copy (soup_message_get_uri (message));
+       soup_uri_set_path (soup_uri, calendar_home_set);
+
+       /* Note that we omit "D:resourcetype", "D:current-user-principal"
+        * and "D:principal-URL" in order to short-circuit the recursion. */
+       message = google_chooser_new_propfind (
+               session, soup_uri, DEPTH_1,
+               NS_GOOGLE, XC ("calendar-home-set"),
+               NS_GOOGLE, XC ("calendar-user-address-set"),
+               NULL);
+
+       e_soup_ssl_trust_connect (message, context->source);
+
+       /* This takes ownership of the message. */
+       soup_session_queue_message (
+               session, message, (SoupSessionCallback)
+               google_chooser_calendar_home_set_cb, simple);
+
+       soup_uri_free (soup_uri);
+
+       g_free (calendar_home_set);
+}
+
+static void
+google_chooser_set_registry (EGoogleChooser *chooser,
+                             ESourceRegistry *registry)
+{
+       g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
+       g_return_if_fail (chooser->priv->registry == NULL);
+
+       chooser->priv->registry = g_object_ref (registry);
 }
 
 static void
@@ -160,17 +1165,36 @@ google_chooser_set_source (EGoogleChooser *chooser,
 }
 
 static void
+google_chooser_set_source_type (EGoogleChooser *chooser,
+                                ECalClientSourceType source_type)
+{
+       chooser->priv->source_type = source_type;
+}
+
+static void
 google_chooser_set_property (GObject *object,
                              guint property_id,
                              const GValue *value,
                              GParamSpec *pspec)
 {
        switch (property_id) {
+               case PROP_REGISTRY:
+                       google_chooser_set_registry (
+                               E_GOOGLE_CHOOSER (object),
+                               g_value_get_object (value));
+                       return;
+
                case PROP_SOURCE:
                        google_chooser_set_source (
                                E_GOOGLE_CHOOSER (object),
                                g_value_get_object (value));
                        return;
+
+               case PROP_SOURCE_TYPE:
+                       google_chooser_set_source_type (
+                               E_GOOGLE_CHOOSER (object),
+                               g_value_get_enum (value));
+                       return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -183,11 +1207,23 @@ google_chooser_get_property (GObject *object,
                              GParamSpec *pspec)
 {
        switch (property_id) {
+               case PROP_REGISTRY:
+                       g_value_set_object (
+                               value, e_google_chooser_get_registry (
+                               E_GOOGLE_CHOOSER (object)));
+                       return;
+
                case PROP_SOURCE:
                        g_value_set_object (
                                value, e_google_chooser_get_source (
                                E_GOOGLE_CHOOSER (object)));
                        return;
+
+               case PROP_SOURCE_TYPE:
+                       g_value_set_enum (
+                               value, e_google_chooser_get_source_type (
+                               E_GOOGLE_CHOOSER (object)));
+                       return;
        }
 
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -200,34 +1236,65 @@ google_chooser_dispose (GObject *object)
 
        priv = E_GOOGLE_CHOOSER_GET_PRIVATE (object);
 
-       if (priv->source != NULL) {
-               g_object_unref (priv->source);
-               priv->source = NULL;
-       }
+       g_clear_object (&priv->registry);
+       g_clear_object (&priv->prompter);
+       g_clear_object (&priv->source);
+       g_clear_object (&priv->session);
 
-       /* Chain up parent's dispose() method. */
+       /* Chain up to parent's dispose() method. */
        G_OBJECT_CLASS (e_google_chooser_parent_class)->dispose (object);
 }
 
 static void
+google_chooser_finalize (GObject *object)
+{
+       EGoogleChooserPrivate *priv;
+
+       priv = E_GOOGLE_CHOOSER_GET_PRIVATE (object);
+
+       g_list_free_full (
+               priv->user_address_set,
+               (GDestroyNotify) g_free);
+
+       g_free (priv->username);
+       g_free (priv->password);
+       g_free (priv->certificate_pem);
+       g_free (priv->error_text);
+
+       /* Chain up to parent's finalize() method. */
+       G_OBJECT_CLASS (e_google_chooser_parent_class)->finalize (object);
+}
+
+static void
 google_chooser_constructed (GObject *object)
 {
+       EGoogleChooser *chooser;
        GtkTreeView *tree_view;
        GtkListStore *list_store;
        GtkCellRenderer *renderer;
        GtkTreeViewColumn *column;
-
-       tree_view = GTK_TREE_VIEW (object);
+       SoupSession *session;
 
        /* Chain up to parent's constructed() method. */
        G_OBJECT_CLASS (e_google_chooser_parent_class)->constructed (object);
 
+       chooser = E_GOOGLE_CHOOSER (object);
+       session = soup_session_new ();
+       google_chooser_configure_session (chooser, session);
+       chooser->priv->session = session;
+
+       chooser->priv->prompter = e_credentials_prompter_new (chooser->priv->registry);
+       e_credentials_prompter_set_auto_prompt (chooser->priv->prompter, FALSE);
+
+       tree_view = GTK_TREE_VIEW (object);
+
        list_store = gtk_list_store_new (
                NUM_COLUMNS,
+               G_TYPE_STRING,          /* COLUMN_DISPLAY_NAME */
+               G_TYPE_STRING,          /* COLUMN_PATH_ENCODED */
+               G_TYPE_STRING,          /* COLUMN_PATH_DECODED */
                GDK_TYPE_COLOR,         /* COLUMN_COLOR */
-               G_TYPE_STRING,          /* COLUMN_PATH */
-               G_TYPE_STRING,          /* COLUMN_TITLE */
-               G_TYPE_BOOLEAN);        /* COLUMN_WRITABLE */
+               G_TYPE_BOOLEAN);        /* COLUMN_HAS_COLOR */
 
        gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (list_store));
 
@@ -238,13 +1305,120 @@ google_chooser_constructed (GObject *object)
 
        renderer = e_cell_renderer_color_new ();
        gtk_tree_view_column_pack_start (column, renderer, FALSE);
-       gtk_tree_view_column_add_attribute (
-               column, renderer, "color", COLUMN_COLOR);
+       gtk_tree_view_column_set_attributes (
+               column, renderer,
+               "color", COLUMN_COLOR,
+               "visible", COLUMN_HAS_COLOR,
+               NULL);
 
        renderer = gtk_cell_renderer_text_new ();
        gtk_tree_view_column_pack_start (column, renderer, TRUE);
-       gtk_tree_view_column_add_attribute (
-               column, renderer, "text", COLUMN_TITLE);
+       gtk_tree_view_column_set_attributes (
+               column, renderer,
+               "text", COLUMN_DISPLAY_NAME,
+               NULL);
+}
+
+/* Helper for google_chooser_try_password_sync() */
+static void
+google_chooser_try_password_cancelled_cb (GCancellable *cancellable,
+                                          SoupSession *session)
+{
+       soup_session_abort (session);
+}
+
+static ESourceAuthenticationResult
+google_chooser_try_password_sync (EGoogleChooser *chooser,
+                                  const ENamedParameters *credentials,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       ESourceAuthenticationResult result;
+       SoupMessage *message;
+       SoupSession *session;
+       SoupURI *soup_uri;
+       ESource *source;
+       ESourceWebdav *extension;
+       const gchar *extension_name;
+       gulong cancel_id = 0;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), E_SOURCE_AUTHENTICATION_ERROR);
+       g_return_val_if_fail (credentials != NULL, E_SOURCE_AUTHENTICATION_ERROR);
+
+       source = e_google_chooser_get_source (chooser);
+       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+       extension = e_source_get_extension (source, extension_name);
+
+       /* Cache the password for later use in our
+        * SoupSession::authenticate signal handler. */
+       g_free (chooser->priv->username);
+       chooser->priv->username = g_strdup (e_named_parameters_get (credentials, 
E_SOURCE_CREDENTIAL_USERNAME));
+
+       g_free (chooser->priv->password);
+       chooser->priv->password = g_strdup (e_named_parameters_get (credentials, 
E_SOURCE_CREDENTIAL_PASSWORD));
+
+       if (e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_SSL_TRUST))
+               e_source_webdav_set_ssl_trust (extension, e_named_parameters_get (credentials, 
E_SOURCE_CREDENTIAL_SSL_TRUST));
+
+       g_free (chooser->priv->certificate_pem);
+       chooser->priv->certificate_pem = NULL;
+       chooser->priv->certificate_errors = 0;
+       g_free (chooser->priv->error_text);
+       chooser->priv->error_text = NULL;
+
+       /* Create our own SoupSession so we
+        * can try the password synchronously. */
+       session = soup_session_new ();
+       google_chooser_configure_session (chooser, session);
+
+       soup_uri = e_source_webdav_dup_soup_uri (extension);
+       g_return_val_if_fail (soup_uri != NULL, E_SOURCE_AUTHENTICATION_ERROR);
+
+       /* Try some simple PROPFIND query.  We don't care about the query
+        * result, only whether the CalDAV server will accept our password. */
+       message = google_chooser_new_propfind (
+               session, soup_uri, DEPTH_0,
+               NS_WEBDAV, XC ("resourcetype"),
+               NULL);
+
+       if (G_IS_CANCELLABLE (cancellable))
+               cancel_id = g_cancellable_connect (
+                       cancellable,
+                       G_CALLBACK (google_chooser_try_password_cancelled_cb),
+                       g_object_ref (session),
+                       (GDestroyNotify) g_object_unref);
+
+       e_soup_ssl_trust_connect (message, source);
+
+       soup_session_send_message (session, message);
+
+       if (cancel_id > 0)
+               g_cancellable_disconnect (cancellable, cancel_id);
+
+       if (google_chooser_check_successful (chooser, message, &local_error)) {
+               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+
+       } else if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) {
+               result = E_SOURCE_AUTHENTICATION_REJECTED;
+               /* Return also the error here. */
+
+       } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
+               result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
+
+       } else {
+               result = E_SOURCE_AUTHENTICATION_ERROR;
+       }
+
+       if (local_error != NULL)
+               g_propagate_error (error, local_error);
+
+       g_object_unref (message);
+       g_object_unref (session);
+
+       soup_uri_free (soup_uri);
+
+       return result;
 }
 
 static void
@@ -258,18 +1432,42 @@ e_google_chooser_class_init (EGoogleChooserClass *class)
        object_class->set_property = google_chooser_set_property;
        object_class->get_property = google_chooser_get_property;
        object_class->dispose = google_chooser_dispose;
+       object_class->finalize = google_chooser_finalize;
        object_class->constructed = google_chooser_constructed;
 
        g_object_class_install_property (
                object_class,
+               PROP_REGISTRY,
+               g_param_spec_object (
+                       "registry",
+                       "Registry",
+                       "Data source registry",
+                       E_TYPE_SOURCE_REGISTRY,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property (
+               object_class,
                PROP_SOURCE,
                g_param_spec_object (
                        "source",
                        "Source",
-                       "Google data source",
+                       "CalDAV data source",
                        E_TYPE_SOURCE,
                        G_PARAM_READWRITE |
                        G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_SOURCE_TYPE,
+               g_param_spec_enum (
+                       "source-type",
+                       "Source Type",
+                       "The iCalendar object type",
+                       E_TYPE_CAL_CLIENT_SOURCE_TYPE,
+                       E_CAL_CLIENT_SOURCE_TYPE_EVENTS,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY));
 }
 
 static void
@@ -281,6 +1479,7 @@ static void
 e_google_chooser_init (EGoogleChooser *chooser)
 {
        chooser->priv = E_GOOGLE_CHOOSER_GET_PRIVATE (chooser);
+       chooser->priv->first_auth_request = TRUE;
 }
 
 void
@@ -293,173 +1492,50 @@ e_google_chooser_type_register (GTypeModule *type_module)
 }
 
 GtkWidget *
-e_google_chooser_new (ESource *source)
+e_google_chooser_new (ESourceRegistry *registry,
+                      ESource *source,
+                      ECalClientSourceType source_type)
 {
+       g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL);
        g_return_val_if_fail (E_IS_SOURCE (source), NULL);
 
-       return g_object_new (E_TYPE_GOOGLE_CHOOSER, "source", source, NULL);
+       return g_object_new (
+               E_TYPE_GOOGLE_CHOOSER,
+               "registry", registry,
+               "source", source,
+               "source-type", source_type, NULL);
 }
 
-ESource *
-e_google_chooser_get_source (EGoogleChooser *chooser)
+ESourceRegistry *
+e_google_chooser_get_registry (EGoogleChooser *chooser)
 {
        g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
 
-       return chooser->priv->source;
+       return chooser->priv->registry;
 }
 
-gchar *
-e_google_chooser_get_decoded_user (EGoogleChooser *chooser)
+ECredentialsPrompter *
+e_google_chooser_get_prompter (EGoogleChooser *chooser)
 {
-       ESource *source;
-       ESourceAuthentication *authentication_extension;
-       const gchar *user;
-
        g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
 
-       source = e_google_chooser_get_source (chooser);
-
-       authentication_extension = e_source_get_extension (
-               source, E_SOURCE_EXTENSION_AUTHENTICATION);
-
-       user = e_source_authentication_get_user (authentication_extension);
-       return google_chooser_decode_user (user);
+       return chooser->priv->prompter;
 }
 
-static void
-google_chooser_query_cb (GDataService *service,
-                         GAsyncResult *result,
-                         GSimpleAsyncResult *simple)
+ESource *
+e_google_chooser_get_source (EGoogleChooser *chooser)
 {
-       GObject *object;
-       GDataFeed *feed;
-       GList *list, *link;
-       GtkTreeView *tree_view;
-       GtkListStore *list_store;
-       GtkTreeModel *tree_model;
-       GError *error = NULL;
-
-       feed = gdata_service_query_finish (service, result, &error);
-
-       if (error != NULL) {
-               g_warn_if_fail (feed == NULL);
-               g_simple_async_result_set_from_error (simple, error);
-               g_simple_async_result_complete (simple);
-               g_object_unref (simple);
-               return;
-       }
-
-       g_return_if_fail (GDATA_IS_FEED (feed));
-
-       list = gdata_feed_get_entries (feed);
-
-       /* This returns a new reference, for reasons passing understanding. */
-       object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));
-
-       tree_view = GTK_TREE_VIEW (object);
-       tree_model = gtk_tree_view_get_model (tree_view);
-       list_store = GTK_LIST_STORE (tree_model);
-
-       gtk_list_store_clear (list_store);
-
-       for (link = list; link != NULL; link = g_list_next (link)) {
-               GDataCalendarCalendar *calendar;
-               GDataEntry *entry;
-               GDataLink *alt;
-               GDataColor color;
-               GdkColor gdkcolor;
-               GtkTreeIter iter;
-               const gchar *uri;
-               const gchar *title;
-               const gchar *access;
-               gboolean writable;
-               gchar *path;
-
-               entry = GDATA_ENTRY (link->data);
-               calendar = GDATA_CALENDAR_CALENDAR (entry);
-
-               /* Skip hidden entries. */
-               if (gdata_calendar_calendar_is_hidden (calendar))
-                       continue;
-
-               /* Look up the alternate link, skip if there is none. */
-               alt = gdata_entry_look_up_link (entry, GDATA_LINK_ALTERNATE);
-               if (alt == NULL)
-                       continue;
-
-               uri = gdata_link_get_uri (alt);
-               title = gdata_entry_get_title (entry);
-               gdata_calendar_calendar_get_color (calendar, &color);
-               access = gdata_calendar_calendar_get_access_level (calendar);
-
-               if (uri == NULL || *uri == '\0')
-                       continue;
-
-               if (title == NULL || *title == '\0')
-                       continue;
-
-               path = google_chooser_extract_caldav_events_path (uri);
-
-               gdkcolor.pixel = 0;
-               gdkcolor.red = color.red * 256;
-               gdkcolor.green = color.green * 256;
-               gdkcolor.blue = color.blue * 256;
-
-               if (access == NULL)
-                       writable = TRUE;
-               else if (g_ascii_strcasecmp (access, "owner") == 0)
-                       writable = TRUE;
-               else if (g_ascii_strcasecmp (access, "contributor") == 0)
-                       writable = TRUE;
-               else
-                       writable = FALSE;
-
-               gtk_list_store_append (list_store, &iter);
-
-               gtk_list_store_set (
-                       list_store, &iter,
-                       COLUMN_COLOR, &gdkcolor,
-                       COLUMN_PATH, path,
-                       COLUMN_TITLE, title,
-                       COLUMN_WRITABLE, writable,
-                       -1);
-
-               g_free (path);
-       }
-
-       g_object_unref (object);
-       g_object_unref (feed);
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
 
-       g_simple_async_result_complete (simple);
-       g_object_unref (simple);
+       return chooser->priv->source;
 }
 
-static void
-google_chooser_authenticate_cb (GDataClientLoginAuthorizer *authorizer,
-                                GAsyncResult *result,
-                                GSimpleAsyncResult *simple)
+ECalClientSourceType
+e_google_chooser_get_source_type (EGoogleChooser *chooser)
 {
-       Context *context;
-       GError *error = NULL;
-
-       context = g_simple_async_result_get_op_res_gpointer (simple);
-
-       gdata_client_login_authorizer_authenticate_finish (
-               authorizer, result, &error);
-
-       if (error != NULL) {
-               g_simple_async_result_set_from_error (simple, error);
-               g_simple_async_result_complete (simple);
-               g_object_unref (simple);
-               return;
-       }
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), 0);
 
-       /* We're authenticated, now query for all calendars. */
-
-       gdata_calendar_service_query_all_calendars_async (
-               context->service, NULL, context->cancellable,
-               NULL, NULL, NULL, (GAsyncReadyCallback)
-               google_chooser_query_cb, simple);
+       return chooser->priv->source_type;
 }
 
 void
@@ -468,33 +1544,29 @@ e_google_chooser_populate (EGoogleChooser *chooser,
                            GAsyncReadyCallback callback,
                            gpointer user_data)
 {
-       GDataClientLoginAuthorizer *authorizer;
-       GDataCalendarService *service;
-       GSimpleAsyncResult *simple;
        Context *context;
        ESource *source;
-       gpointer parent;
-       gchar *password;
-       gchar *prompt;
-       gchar *user;
+       SoupURI *soup_uri;
+       SoupMessage *message;
+       ESourceWebdav *extension;
+       GtkTreeModel *tree_model;
+       GSimpleAsyncResult *simple;
+       const gchar *extension_name;
 
        g_return_if_fail (E_IS_GOOGLE_CHOOSER (chooser));
 
-       source = e_google_chooser_get_source (chooser);
+       tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser));
+       gtk_list_store_clear (GTK_LIST_STORE (tree_model));
+       soup_session_abort (chooser->priv->session);
 
-       authorizer = gdata_client_login_authorizer_new (
-               PACKAGE_NAME, GDATA_TYPE_CALENDAR_SERVICE);
+       source = e_google_chooser_get_source (chooser);
+       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+       extension = e_source_get_extension (source, extension_name);
 
-       service = gdata_calendar_service_new (GDATA_AUTHORIZER (authorizer));
+       soup_uri = e_source_webdav_dup_soup_uri (extension);
+       g_return_if_fail (soup_uri != NULL);
 
-       context = g_slice_new0 (Context);
-       context->service = service;  /* takes ownership */
-       context->source = g_object_ref (source);
-
-       if (G_IS_CANCELLABLE (cancellable))
-               context->cancellable = g_object_ref (cancellable);
-       else
-               context->cancellable = g_cancellable_new ();
+       context = context_new (chooser, cancellable);
 
        simple = g_simple_async_result_new (
                G_OBJECT (chooser), callback,
@@ -503,50 +1575,23 @@ e_google_chooser_populate (EGoogleChooser *chooser,
        g_simple_async_result_set_op_res_gpointer (
                simple, context, (GDestroyNotify) context_free);
 
-       /* Prompt for a password. */
-
-       user = e_google_chooser_get_decoded_user (chooser);
-
-       parent = gtk_widget_get_toplevel (GTK_WIDGET (chooser));
-       parent = gtk_widget_is_toplevel (parent) ? parent : NULL;
-
-       prompt = g_strdup_printf (
-               _("Enter Google password for user '%s'."), user);
+       message = google_chooser_new_propfind (
+               context->session, soup_uri, DEPTH_0,
+               NS_WEBDAV, XC ("resourcetype"),
+               NS_GOOGLE, XC ("calendar-home-set"),
+               NS_GOOGLE, XC ("calendar-user-address-set"),
+               NS_WEBDAV, XC ("current-user-principal"),
+               NS_WEBDAV, XC ("principal-URL"),
+               NULL);
 
-       /* XXX The 'key' (2nd) argument doesn't matter since we're
-        *     passing E_PASSWORDS_REMEMBER_NEVER, it just needs to
-        *     be non-NULL.  This API is degenerating rapidly. */
-       password = e_passwords_ask_password (
-               "", "bogus key", prompt,
-               E_PASSWORDS_REMEMBER_NEVER |
-               E_PASSWORDS_DISABLE_REMEMBER |
-               E_PASSWORDS_SECRET, NULL, parent);
+       e_soup_ssl_trust_connect (message, source);
 
-       g_free (prompt);
+       /* This takes ownership of the message. */
+       soup_session_queue_message (
+               context->session, message, (SoupSessionCallback)
+               google_chooser_calendar_home_set_cb, simple);
 
-       if (password == NULL) {
-               g_cancellable_cancel (context->cancellable);
-               g_simple_async_result_set_error (
-                       simple, G_IO_ERROR, G_IO_ERROR_CANCELLED,
-                       "%s", _("User declined to provide a password"));
-               g_simple_async_result_complete (simple);
-               g_object_unref (authorizer);
-               g_object_unref (simple);
-               g_free (user);
-               return;
-       }
-
-       /* Try authenticating. */
-
-       gdata_client_login_authorizer_authenticate_async (
-               authorizer, user, password,
-               context->cancellable, (GAsyncReadyCallback)
-               google_chooser_authenticate_cb, simple);
-
-       g_free (password);
-       g_free (user);
-
-       g_object_unref (authorizer);
+       soup_uri_free (soup_uri);
 }
 
 gboolean
@@ -555,6 +1600,7 @@ e_google_chooser_populate_finish (EGoogleChooser *chooser,
                                   GError **error)
 {
        GSimpleAsyncResult *simple;
+       Context *context;
 
        g_return_val_if_fail (
                g_simple_async_result_is_valid (
@@ -562,25 +1608,35 @@ e_google_chooser_populate_finish (EGoogleChooser *chooser,
                e_google_chooser_populate), FALSE);
 
        simple = G_SIMPLE_ASYNC_RESULT (result);
+       context = g_simple_async_result_get_op_res_gpointer (simple);
+
+       if (g_simple_async_result_propagate_error (simple, error))
+               return FALSE;
 
-       /* Assume success unless a GError is set. */
-       return !g_simple_async_result_propagate_error (simple, error);
+       /* Transfer user addresses to the private struct. */
+
+       g_list_free_full (
+               chooser->priv->user_address_set,
+               (GDestroyNotify) g_free);
+
+       chooser->priv->user_address_set = context->user_address_set;
+       context->user_address_set = NULL;
+
+       return TRUE;
 }
 
 gboolean
 e_google_chooser_apply_selected (EGoogleChooser *chooser)
 {
-       ESourceSelectable *selectable_extension;
        ESourceWebdav *webdav_extension;
        GtkTreeSelection *selection;
        GtkTreeModel *model;
        GtkTreeIter iter;
        ESource *source;
        GdkColor *color;
-       SoupURI *soup_uri;
-       gchar *color_spec;
-       gchar *title;
-       gchar *path;
+       gboolean has_color;
+       gchar *display_name;
+       gchar *path_encoded;
 
        g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), FALSE);
 
@@ -592,36 +1648,98 @@ e_google_chooser_apply_selected (EGoogleChooser *chooser)
 
        gtk_tree_model_get (
                model, &iter,
+               COLUMN_DISPLAY_NAME, &display_name,
+               COLUMN_PATH_ENCODED, &path_encoded,
+               COLUMN_HAS_COLOR, &has_color,
                COLUMN_COLOR, &color,
-               COLUMN_PATH, &path,
-               COLUMN_TITLE, &title,
                -1);
 
-       selectable_extension = e_source_get_extension (
-               source, E_SOURCE_EXTENSION_CALENDAR);
+       /* Sanity check. */
+       g_warn_if_fail (
+               (has_color && color != NULL) ||
+               (!has_color && color == NULL));
 
        webdav_extension = e_source_get_extension (
                source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
 
-       e_source_set_display_name (source, title);
+       e_source_set_display_name (source, display_name);
+
+       e_source_webdav_set_display_name (webdav_extension, display_name);
+       e_source_webdav_set_resource_path (webdav_extension, path_encoded);
+
+       /* XXX For now just pick the first user address in the list.
+        *     Might be better to compare the list against our own mail
+        *     accounts and give preference to matches (especially if an
+        *     address matches the default mail account), but I'm not sure
+        *     if multiple user addresses are common enough to justify the
+        *     extra effort. */
+       if (chooser->priv->user_address_set != NULL)
+               e_source_webdav_set_email_address (
+                       webdav_extension,
+                       chooser->priv->user_address_set->data);
+
+       if (has_color) {
+               ESourceSelectable *selectable_extension;
+               const gchar *extension_name;
+               gchar *color_spec;
+
+               switch (e_google_chooser_get_source_type (chooser)) {
+                       case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
+                               extension_name = E_SOURCE_EXTENSION_CALENDAR;
+                               break;
+                       case E_CAL_CLIENT_SOURCE_TYPE_MEMOS:
+                               extension_name = E_SOURCE_EXTENSION_MEMO_LIST;
+                               break;
+                       case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
+                               extension_name = E_SOURCE_EXTENSION_TASK_LIST;
+                               break;
+                       default:
+                               g_return_val_if_reached (TRUE);
+               }
+
+               selectable_extension =
+                       e_source_get_extension (source, extension_name);
+
+               color_spec = gdk_color_to_string (color);
+               e_source_selectable_set_color (
+                       selectable_extension, color_spec);
+               g_free (color_spec);
+
+               gdk_color_free (color);
+       }
 
-       e_source_webdav_set_display_name (webdav_extension, title);
+       g_free (display_name);
+       g_free (path_encoded);
 
-       /* XXX Might be easier to expose get/set_path functions? */
-       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
-       soup_uri_set_path (soup_uri, path);
-       e_source_webdav_set_soup_uri (webdav_extension, soup_uri);
-       soup_uri_free (soup_uri);
+       return TRUE;
+}
 
-       color_spec = gdk_color_to_string (color);
-       e_source_selectable_set_color (selectable_extension, color_spec);
-       g_free (color_spec);
+static gchar *
+google_chooser_decode_user (const gchar *user)
+{
+       gchar *decoded_user;
 
-       gdk_color_free (color);
-       g_free (title);
-       g_free (path);
+       if (user == NULL || *user == '\0')
+               return NULL;
 
-       return TRUE;
+       /* Decode any encoded 'at' symbols ('%40' -> '@'). */
+       if (strstr (user, "%40") != NULL) {
+               gchar **segments;
+
+               segments = g_strsplit (user, "%40", 0);
+               decoded_user = g_strjoinv ("@", segments);
+               g_strfreev (segments);
+
+       /* If no domain is given, append "@gmail.com". */
+       } else if (strstr (user, "@") == NULL) {
+               decoded_user = g_strconcat (user, "@gmail.com", NULL);
+
+       /* Otherwise the user name should be fine as is. */
+       } else {
+               decoded_user = g_strdup (user);
+       }
+
+       return decoded_user;
 }
 
 void
@@ -642,3 +1760,132 @@ e_google_chooser_construct_default_uri (SoupURI *soup_uri,
        g_free (decoded_user);
        g_free (path);
 }
+
+void
+e_google_chooser_run_trust_prompt (EGoogleChooser *chooser,
+                                  GtkWindow *parent,
+                                  GCancellable *cancellable,
+                                  GAsyncReadyCallback callback,
+                                  gpointer user_data)
+{
+       g_return_if_fail (E_IS_GOOGLE_CHOOSER (chooser));
+
+       e_trust_prompt_run_for_source (parent,
+               chooser->priv->source,
+               chooser->priv->certificate_pem,
+               chooser->priv->certificate_errors,
+               chooser->priv->error_text,
+               FALSE,
+               cancellable,
+               callback,
+               user_data);
+}
+
+/* Free returned pointer with g_error_free() or g_clear_error() */
+GError *
+e_google_chooser_new_ssl_trust_error (EGoogleChooser *chooser)
+{
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), NULL);
+       g_return_val_if_fail (chooser->priv->error_text != NULL, NULL);
+
+       return g_error_new_literal (SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED, chooser->priv->error_text);
+}
+
+/* The callback has an ECredentialsPrompter as the source_object. */
+void
+e_google_chooser_run_credentials_prompt (EGoogleChooser *chooser,
+                                        GAsyncReadyCallback callback,
+                                        gpointer user_data)
+{
+       g_return_if_fail (E_IS_GOOGLE_CHOOSER (chooser));
+       g_return_if_fail (callback != NULL);
+
+       e_credentials_prompter_prompt (chooser->priv->prompter, chooser->priv->source, 
chooser->priv->error_text,
+               chooser->priv->first_auth_request ? 
E_CREDENTIALS_PROMPTER_PROMPT_FLAG_ALLOW_STORED_CREDENTIALS : 0,
+               callback, user_data);
+
+       chooser->priv->first_auth_request = FALSE;
+}
+
+gboolean
+e_google_chooser_run_credentials_prompt_finish (EGoogleChooser *chooser,
+                                               GAsyncResult *result,
+                                               ENamedParameters **out_credentials,
+                                               GError **error)
+{
+       ESource *source = NULL;
+
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), FALSE);
+       g_return_val_if_fail (out_credentials != NULL, FALSE);
+
+       if (!e_credentials_prompter_prompt_finish (chooser->priv->prompter, result,
+               &source, out_credentials, error))
+               return FALSE;
+
+       g_return_val_if_fail (source == chooser->priv->source, FALSE);
+
+       return TRUE;
+}
+
+static void
+e_google_chooser_authenticate_thread (GTask *task,
+                                     gpointer source_object,
+                                     gpointer task_data,
+                                     GCancellable *cancellable)
+{
+       EGoogleChooser *chooser = source_object;
+       const ENamedParameters *credentials = task_data;
+       gboolean success;
+       GError *local_error = NULL;
+
+       if (google_chooser_try_password_sync (chooser, credentials, cancellable, &local_error)
+           != E_SOURCE_AUTHENTICATION_ACCEPTED && !local_error) {
+               local_error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_FAILED, _("Unknown error"));
+       }
+
+       if (local_error != NULL) {
+               g_task_return_error (task, local_error);
+       } else {
+               g_task_return_boolean (task, success);
+       }
+}
+
+void
+e_google_chooser_authenticate (EGoogleChooser *chooser,
+                              const ENamedParameters *credentials,
+                              GCancellable *cancellable,
+                              GAsyncReadyCallback callback,
+                              gpointer user_data)
+{
+       ENamedParameters *credentials_copy;
+       GTask *task;
+
+       g_return_if_fail (E_IS_GOOGLE_CHOOSER (chooser));
+       g_return_if_fail (credentials != NULL);
+       g_return_if_fail (callback != NULL);
+
+       credentials_copy = e_named_parameters_new_clone (credentials);
+
+       task = g_task_new (chooser, cancellable, callback, user_data);
+       g_task_set_source_tag (task, e_google_chooser_authenticate);
+       g_task_set_task_data (task, credentials_copy, (GDestroyNotify) e_named_parameters_free);
+
+       g_task_run_in_thread (task, e_google_chooser_authenticate_thread);
+
+       g_object_unref (task);
+}
+
+gboolean
+e_google_chooser_authenticate_finish (EGoogleChooser *chooser,
+                                     GAsyncResult *result,
+                                     GError **error)
+{
+       g_return_val_if_fail (E_IS_GOOGLE_CHOOSER (chooser), FALSE);
+       g_return_val_if_fail (g_task_is_valid (result, chooser), FALSE);
+
+       g_return_val_if_fail (
+               g_async_result_is_tagged (
+               result, e_google_chooser_authenticate), FALSE);
+
+       return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/modules/cal-config-google/e-google-chooser.h b/modules/cal-config-google/e-google-chooser.h
index 1983795..c273697 100644
--- a/modules/cal-config-google/e-google-chooser.h
+++ b/modules/cal-config-google/e-google-chooser.h
@@ -19,7 +19,8 @@
 #define E_GOOGLE_CHOOSER_H
 
 #include <gtk/gtk.h>
-#include <libedataserver/libedataserver.h>
+#include <libecal/libecal.h>
+#include <libedataserverui/libedataserverui.h>
 
 /* Standard GObject macros */
 #define E_TYPE_GOOGLE_CHOOSER \
@@ -56,11 +57,17 @@ struct _EGoogleChooserClass {
 };
 
 GType          e_google_chooser_get_type       (void);
-void           e_google_chooser_type_register
-                                               (GTypeModule *type_module);
-GtkWidget *    e_google_chooser_new            (ESource *source);
+void           e_google_chooser_type_register  (GTypeModule *type_module);
+GtkWidget *    e_google_chooser_new            (ESourceRegistry *registry,
+                                                ESource *source,
+                                                ECalClientSourceType source_type);
+ESourceRegistry *
+               e_google_chooser_get_registry   (EGoogleChooser *chooser);
+ECredentialsPrompter *
+               e_google_chooser_get_prompter   (EGoogleChooser *chooser);
 ESource *      e_google_chooser_get_source     (EGoogleChooser *chooser);
-gchar *                e_google_chooser_get_decoded_user
+ECalClientSourceType
+               e_google_chooser_get_source_type
                                                (EGoogleChooser *chooser);
 void           e_google_chooser_populate       (EGoogleChooser *chooser,
                                                 GCancellable *cancellable,
@@ -71,9 +78,35 @@ gboolean     e_google_chooser_populate_finish
                                                 GAsyncResult *result,
                                                 GError **error);
 gboolean       e_google_chooser_apply_selected (EGoogleChooser *chooser);
-
 void           e_google_chooser_construct_default_uri
                                                (SoupURI *soup_uri,
                                                 const gchar *username);
 
+void           e_google_chooser_run_trust_prompt
+                                               (EGoogleChooser *chooser,
+                                                GtkWindow *parent,
+                                                GCancellable *cancellable,
+                                                GAsyncReadyCallback callback,
+                                                gpointer user_data);
+GError *       e_google_chooser_new_ssl_trust_error
+                                               (EGoogleChooser *chooser);
+void           e_google_chooser_run_credentials_prompt
+                                               (EGoogleChooser *chooser,
+                                                GAsyncReadyCallback callback,
+                                                gpointer user_data);
+gboolean       e_google_chooser_run_credentials_prompt_finish
+                                               (EGoogleChooser *chooser,
+                                                GAsyncResult *result,
+                                                ENamedParameters **out_credentials,
+                                                GError **error);
+void           e_google_chooser_authenticate   (EGoogleChooser *chooser,
+                                                const ENamedParameters *credentials,
+                                                GCancellable *cancellable,
+                                                GAsyncReadyCallback callback,
+                                                gpointer user_data);
+gboolean       e_google_chooser_authenticate_finish
+                                               (EGoogleChooser *chooser,
+                                                GAsyncResult *result,
+                                                GError **error);
+
 #endif /* E_GOOGLE_CHOOSER_H */


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