aboutsummaryrefslogblamecommitdiffstats
path: root/modules/cal-config-caldav/e-caldav-chooser.c
blob: 77035c10858a47c97473d3aa20b8177a0dd05fc2 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11


                     


                                                                           
  



                                                                             
  

                                                                           








                             




                                  
                          



























                                                              

                                  




















                                    

                                 
                                   
                          












                                                            
                                                                       


                                                      
                                                          

                                                            









                                                     












                                                                














                                                                 

                                                                   

















                                                                       





                                                   























                                                               
                                                  

































                                                          

                              
































































                                                                             

                                                                            

                                                             
                                 
















































                                                                    









                                                                 

                        



                                                          
 




































































                                                                      














































































































































































































                                                                       
                                                             
                                               
                                                                 
                                              
                                                                    









































































































































































































































                                                                              
                                                                

                                                                  


                          













                                                                          











                                                                   

                                                                                                     
























                                                                        




                                                                             

                                                                     
                                                                
































































































































                                                                          
                                                                      




















                                                                            

                                                                                                     























































































































































                                                                             
                                      

















































































                                                                         
                                      






















                                                                               
                                                                                         
 
                                                     
















































































                                                                                             
                                                                          
 
                                                                    




































































































                                                                           

                                                                                            































































































































                                                                              
/*
 * e-caldav-chooser.c
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "e-caldav-chooser.h"

#include <config.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>

#define E_CALDAV_CHOOSER_GET_PRIVATE(obj) \
    (G_TYPE_INSTANCE_GET_PRIVATE \
    ((obj), E_TYPE_CALDAV_CHOOSER, ECaldavChooserPrivate))

#define XC(string) ((xmlChar *) string)

/* Standard Namespaces */
#define NS_WEBDAV  "DAV:"
#define NS_CALDAV  "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 _ECaldavChooserPrivate {
    ESourceRegistry *registry;
    ESource *source;
    ECalClientSourceType source_type;
    SoupSession *session;
    GList *user_address_set;
    gchar *password;
};

struct _Context {
    SoupSession *session;
    ESourceRegistry *registry;
    ESource *source;

    GCancellable *cancellable;
    gulong cancelled_handler_id;

    GList *user_address_set;
};

enum {
    PROP_0,
    PROP_REGISTRY,
    PROP_SOURCE,
    PROP_SOURCE_TYPE
};

/* Mainly for readability. */
enum {
    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
};

/* Forward Declarations */
static void e_caldav_chooser_authenticator_init
                (ESourceAuthenticatorInterface *iface);
static void caldav_chooser_get_collection_details
                (SoupSession *session,
                 SoupMessage *message,
                 const gchar *path_or_uri,
                 GSimpleAsyncResult *simple,
                 Context *context);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (
    ECaldavChooser,
    e_caldav_chooser,
    GTK_TYPE_TREE_VIEW,
    0,
    G_IMPLEMENT_INTERFACE_DYNAMIC (
        E_TYPE_SOURCE_AUTHENTICATOR,
        e_caldav_chooser_authenticator_init))

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_cancel_message (GCancellable *cancellable,
                        Context *context)
{
    soup_session_abort (context->session);
}

static Context *
context_new (ECaldavChooser *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);
    }

    return context;
}

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 void
caldav_chooser_redirect (SoupMessage *message,
                         SoupSession *session)
{
    SoupURI *soup_uri;
    const gchar *location;

    if (!SOUP_STATUS_IS_REDIRECTION (message->status_code))
        return;

    location = soup_message_headers_get_list (
        message->response_headers, "Location");

    if (location == NULL)
        return;

    soup_uri = soup_uri_new_with_base (
        soup_message_get_uri (message), location);

    if (soup_uri == NULL) {
        soup_message_set_status_full (
            message, SOUP_STATUS_MALFORMED,
            "Invalid Redirect URL");
        return;
    }

    soup_message_set_uri (message, soup_uri);
    soup_session_requeue_message (session, message);

    soup_uri_free (soup_uri);
}

static G_GNUC_NULL_TERMINATED SoupMessage *
caldav_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_CALDAV), XC ("C"));
    g_hash_table_insert (namespaces, (gpointer) NS_CALDAV, 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);

    g_hash_table_destroy (namespaces);

    /* Construct the SoupMessage. */

    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);

    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 (caldav_chooser_redirect), session);

    return message;
}

static void
caldav_chooser_authenticate_cb (SoupSession *session,
                                SoupMessage *message,
                                SoupAuth *auth,
                                gboolean retrying,
                                ECaldavChooser *chooser)
{
    ESource *source;
    ESourceAuthentication *extension;
    const gchar *extension_name;
    const gchar *username;
    const gchar *password;

    source = e_caldav_chooser_get_source (chooser);
    extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
    extension = e_source_get_extension (source, extension_name);

    username = e_source_authentication_get_user (extension);
    password = chooser->priv->password;

    /* If our password was rejected, let the operation fail. */
    if (retrying)
        return;

    /* If we don't have a username, let the operation fail. */
    if (username == NULL || *username == '\0')
        return;

    /* If we don't have a password, let the operation fail. */
    if (password == NULL || *password == '\0')
        return;

    soup_auth_authenticate (auth, username, password);
}

static void
caldav_chooser_configure_session (ECaldavChooser *chooser,
                                  SoupSession *session)
{
    if (g_getenv ("CALDAV_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 (caldav_chooser_authenticate_cb), chooser);
}

static gboolean
caldav_chooser_check_successful (SoupMessage *message,
                                 GError **error)
{
    GIOErrorEnum error_code;

    /* 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;
        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
caldav_chooser_parse_xml (SoupMessage *message,
                          const gchar *expected_name,
                          GError **error)
{
    xmlDocPtr doc;
    xmlNodePtr root;

    if (!caldav_chooser_check_successful (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
caldav_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 *
caldav_chooser_get_xpath_string (xmlXPathContextPtr xp_ctx,
                                 const gchar *path_format,
                                 ...)
{
    xmlXPathObjectPtr xp_obj;
    va_list va;
    gchar *path;
    gchar *expression;
    gchar *string = NULL;

    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;

    if (xp_obj->type == XPATH_STRING)
        string = g_strdup ((gchar *) xp_obj->stringval);

    /* If the string is empty, return NULL. */
    if (string != NULL && *string == '\0') {
        g_free (string);
        string = NULL;
    }

    xmlXPathFreeObject (xp_obj);

    return string;
}

static void
caldav_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 = caldav_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 = caldav_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);
    }

    xmlXPathFreeObject (xp_obj);
}

static SupportedComponentSet
caldav_chooser_get_supported_component_set (xmlXPathContextPtr xp_ctx,
                                            gint index)
{
    xmlXPathObjectPtr xp_obj;
    SupportedComponentSet set = 0;
    gint ii, length;

    xp_obj = caldav_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 = caldav_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
caldav_chooser_process_response (SoupSession *session,
                                 SoupMessage *message,
                                 GSimpleAsyncResult *simple,
                                 xmlXPathContextPtr xp_ctx,
                                 gint index)
{
    GObject *object;
    xmlXPathObjectPtr xp_obj;
    SupportedComponentSet comp_set;
    ECaldavChooser *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_CALDAV_CHOOSER (object);
    tree_model = gtk_tree_view_get_model (GTK_TREE_VIEW (object));

    g_object_unref (object);

    status_line = caldav_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 = caldav_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 = caldav_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 = caldav_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 = caldav_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);
    else
        has_color = FALSE;

    g_free (color_spec);

    /* Which calendar component types are supported? */

    comp_set = caldav_chooser_get_supported_component_set (xp_ctx, index);

    switch (e_caldav_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
caldav_chooser_collection_details_cb (SoupSession *session,
                                      SoupMessage *message,
                                      GSimpleAsyncResult *simple)
{
    xmlDocPtr doc;
    xmlXPathContextPtr xp_ctx;
    xmlXPathObjectPtr xp_obj;
    GError *error = NULL;

    doc = caldav_chooser_parse_xml (message, "multistatus", &error);

    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_CALDAV));
    xmlXPathRegisterNs (xp_ctx, XC ("CS"), XC (NS_CALSRV));
    xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));

    xp_obj = caldav_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++)
            caldav_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
caldav_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 = caldav_chooser_new_propfind (
        session, soup_uri, DEPTH_1,
        NS_WEBDAV, XC ("displayname"),
        NS_WEBDAV, XC ("resourcetype"),
        NS_CALDAV, XC ("calendar-description"),
        NS_CALDAV, XC ("supported-calendar-component-set"),
        NS_CALDAV, XC ("calendar-user-address-set"),
        NS_CALSRV, XC ("getctag"),
        NS_ICAL,   XC ("calendar-color"),
        NULL);

    e_soup_ssl_trust_connect (message, context->source, context->registry, context->cancellable);

    /* This takes ownership of the message. */
    soup_session_queue_message (
        session, message, (SoupSessionCallback)
        caldav_chooser_collection_details_cb, simple);

    soup_uri_free (soup_uri);
}

static void
caldav_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;
    GError *error = NULL;

    context = g_simple_async_result_get_op_res_gpointer (simple);

    doc = caldav_chooser_parse_xml (message, "multistatus", &error);

    /* 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_CALDAV));

    /* Record any "C:calendar-user-address-set" properties. */
    caldav_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 = caldav_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 = caldav_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 = caldav_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 = caldav_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);

    caldav_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 = caldav_chooser_new_propfind (
        session, soup_uri, DEPTH_1,
        NS_CALDAV, XC ("calendar-home-set"),
        NS_CALDAV, XC ("calendar-user-address-set"),
        NULL);

    e_soup_ssl_trust_connect (message, context->source, context->registry, context->cancellable);

    /* This takes ownership of the message. */
    soup_session_queue_message (
        session, message, (SoupSessionCallback)
        caldav_chooser_calendar_home_set_cb, simple);

    soup_uri_free (soup_uri);

    g_free (calendar_home_set);
}

static void
caldav_chooser_set_registry (ECaldavChooser *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
caldav_chooser_set_source (ECaldavChooser *chooser,
                           ESource *source)
{
    g_return_if_fail (E_IS_SOURCE (source));
    g_return_if_fail (chooser->priv->source == NULL);

    chooser->priv->source = g_object_ref (source);
}

static void
caldav_chooser_set_source_type (ECaldavChooser *chooser,
                                ECalClientSourceType source_type)
{
    chooser->priv->source_type = source_type;
}

static void
caldav_chooser_set_property (GObject *object,
                             guint property_id,
                             const GValue *value,
                             GParamSpec *pspec)
{
    switch (property_id) {
        case PROP_REGISTRY:
            caldav_chooser_set_registry (
                E_CALDAV_CHOOSER (object),
                g_value_get_object (value));
            return;

        case PROP_SOURCE:
            caldav_chooser_set_source (
                E_CALDAV_CHOOSER (object),
                g_value_get_object (value));
            return;

        case PROP_SOURCE_TYPE:
            caldav_chooser_set_source_type (
                E_CALDAV_CHOOSER (object),
                g_value_get_enum (value));
            return;
    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
caldav_chooser_get_property (GObject *object,
                             guint property_id,
                             GValue *value,
                             GParamSpec *pspec)
{
    switch (property_id) {
        case PROP_REGISTRY:
            g_value_set_object (
                value, e_caldav_chooser_get_registry (
                E_CALDAV_CHOOSER (object)));
            return;

        case PROP_SOURCE:
            g_value_set_object (
                value, e_caldav_chooser_get_source (
                E_CALDAV_CHOOSER (object)));
            return;

        case PROP_SOURCE_TYPE:
            g_value_set_enum (
                value, e_caldav_chooser_get_source_type (
                E_CALDAV_CHOOSER (object)));
            return;
    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
caldav_chooser_dispose (GObject *object)
{
    ECaldavChooserPrivate *priv;

    priv = E_CALDAV_CHOOSER_GET_PRIVATE (object);

    if (priv->registry != NULL) {
        g_object_unref (priv->registry);
        priv->registry = NULL;
    }

    if (priv->source != NULL) {
        g_object_unref (priv->source);
        priv->source = NULL;
    }

    if (priv->session != NULL) {
        g_object_unref (priv->session);
        priv->session = NULL;
    }

    /* Chain up to parent's dispose() method. */
    G_OBJECT_CLASS (e_caldav_chooser_parent_class)->dispose (object);
}

static void
caldav_chooser_finalize (GObject *object)
{
    ECaldavChooserPrivate *priv;

    priv = E_CALDAV_CHOOSER_GET_PRIVATE (object);

    g_list_free_full (
        priv->user_address_set,
        (GDestroyNotify) g_free);

    g_free (priv->password);

    /* Chain up to parent's finalize() method. */
    G_OBJECT_CLASS (e_caldav_chooser_parent_class)->finalize (object);
}

static void
caldav_chooser_constructed (GObject *object)
{
    ECaldavChooser *chooser;
    GtkTreeView *tree_view;
    GtkListStore *list_store;
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;
    SoupSession *session;

    /* Chain up to parent's constructed() method. */
    G_OBJECT_CLASS (e_caldav_chooser_parent_class)->constructed (object);

    chooser = E_CALDAV_CHOOSER (object);
    session = soup_session_new ();
    caldav_chooser_configure_session (chooser, session);
    chooser->priv->session = session;

    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_BOOLEAN);    /* COLUMN_HAS_COLOR */

    gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (list_store));

    column = gtk_tree_view_column_new ();
    gtk_tree_view_column_set_expand (column, TRUE);
    gtk_tree_view_column_set_title (column, _("Name"));
    gtk_tree_view_insert_column (tree_view, column, -1);

    renderer = e_cell_renderer_color_new ();
    gtk_tree_view_column_pack_start (column, renderer, FALSE);
    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_set_attributes (
        column, renderer,
        "text", COLUMN_DISPLAY_NAME,
        NULL);

    column = gtk_tree_view_column_new ();
    gtk_tree_view_column_set_expand (column, FALSE);
    gtk_tree_view_column_set_title (column, _("Path"));
    gtk_tree_view_insert_column (tree_view, column, -1);

    renderer = gtk_cell_renderer_text_new ();
    gtk_tree_view_column_pack_start (column, renderer, TRUE);
    gtk_tree_view_column_set_attributes (
        column, renderer,
        "text", COLUMN_PATH_DECODED,
        NULL);
}

/* Helper for caldav_chooser_try_password_sync() */
static void
caldav_chooser_try_password_cancelled_cb (GCancellable *cancellable,
                                          SoupSession *session)
{
    soup_session_abort (session);
}

static ESourceAuthenticationResult
caldav_chooser_try_password_sync (ESourceAuthenticator *auth,
                                  const GString *password,
                                  GCancellable *cancellable,
                                  GError **error)
{
    ECaldavChooser *chooser;
    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;

    chooser = E_CALDAV_CHOOSER (auth);

    /* Cache the password for later use in our
     * SoupSession::authenticate signal handler. */
    g_free (chooser->priv->password);
    chooser->priv->password = g_strdup (password->str);

    /* Create our own SoupSession so we
     * can try the password synchronously. */
    session = soup_session_new ();
    caldav_chooser_configure_session (chooser, session);

    source = e_caldav_chooser_get_source (chooser);
    extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
    extension = e_source_get_extension (source, extension_name);

    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 = caldav_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 (caldav_chooser_try_password_cancelled_cb),
            g_object_ref (session),
            (GDestroyNotify) g_object_unref);

    e_soup_ssl_trust_connect (message, source, chooser->priv->registry, cancellable);

    soup_session_send_message (session, message);

    if (cancel_id > 0)
        g_cancellable_disconnect (cancellable, cancel_id);

    if (caldav_chooser_check_successful (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;
        g_clear_error (&local_error);

    } 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
e_caldav_chooser_class_init (ECaldavChooserClass *class)
{
    GObjectClass *object_class;

    g_type_class_add_private (class, sizeof (ECaldavChooserPrivate));

    object_class = G_OBJECT_CLASS (class);
    object_class->set_property = caldav_chooser_set_property;
    object_class->get_property = caldav_chooser_get_property;
    object_class->dispose = caldav_chooser_dispose;
    object_class->finalize = caldav_chooser_finalize;
    object_class->constructed = caldav_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",
            "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
e_caldav_chooser_class_finalize (ECaldavChooserClass *class)
{
}

static void
e_caldav_chooser_authenticator_init (ESourceAuthenticatorInterface *iface)
{
    iface->try_password_sync = caldav_chooser_try_password_sync;
}

static void
e_caldav_chooser_init (ECaldavChooser *chooser)
{
    chooser->priv = E_CALDAV_CHOOSER_GET_PRIVATE (chooser);
}

void
e_caldav_chooser_type_register (GTypeModule *type_module)
{
    /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
     *     function, so we have to wrap it with a public function in
     *     order to register types from a separate compilation unit. */
    e_caldav_chooser_register_type (type_module);
}

GtkWidget *
e_caldav_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_CALDAV_CHOOSER,
        "registry", registry, "source", source,
        "source-type", source_type, NULL);
}

ESourceRegistry *
e_caldav_chooser_get_registry (ECaldavChooser *chooser)
{
    g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), NULL);

    return chooser->priv->registry;
}

ESource *
e_caldav_chooser_get_source (ECaldavChooser *chooser)
{
    g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), NULL);

    return chooser->priv->source;
}

ECalClientSourceType
e_caldav_chooser_get_source_type (ECaldavChooser *chooser)
{
    g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), 0);

    return chooser->priv->source_type;
}

void
e_caldav_chooser_populate (ECaldavChooser *chooser,
                           GCancellable *cancellable,
                           GAsyncReadyCallback callback,
                           gpointer user_data)
{
    Context *context;
    ESource *source;
    SoupURI *soup_uri;
    SoupMessage *message;
    ESourceWebdav *extension;
    GtkTreeModel *tree_model;
    GSimpleAsyncResult *simple;
    const gchar *extension_name;

    g_return_if_fail (E_IS_CALDAV_CHOOSER (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);

    source = e_caldav_chooser_get_source (chooser);
    extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
    extension = e_source_get_extension (source, extension_name);

    soup_uri = e_source_webdav_dup_soup_uri (extension);
    g_return_if_fail (soup_uri != NULL);

    context = context_new (chooser, cancellable);

    simple = g_simple_async_result_new (
        G_OBJECT (chooser), callback,
        user_data, e_caldav_chooser_populate);

    g_simple_async_result_set_op_res_gpointer (
        simple, context, (GDestroyNotify) context_free);

    message = caldav_chooser_new_propfind (
        context->session, soup_uri, DEPTH_0,
        NS_WEBDAV, XC ("resourcetype"),
        NS_CALDAV, XC ("calendar-home-set"),
        NS_CALDAV, XC ("calendar-user-address-set"),
        NS_WEBDAV, XC ("current-user-principal"),
        NS_WEBDAV, XC ("principal-URL"),
        NULL);

    e_soup_ssl_trust_connect (message, source, context->registry, context->cancellable);

    /* This takes ownership of the message. */
    soup_session_queue_message (
        context->session, message, (SoupSessionCallback)
        caldav_chooser_calendar_home_set_cb, simple);

    soup_uri_free (soup_uri);
}

gboolean
e_caldav_chooser_populate_finish (ECaldavChooser *chooser,
                                  GAsyncResult *result,
                                  GError **error)
{
    GSimpleAsyncResult *simple;
    Context *context;

    g_return_val_if_fail (
        g_simple_async_result_is_valid (
        result, G_OBJECT (chooser),
        e_caldav_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;

    /* 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_caldav_chooser_apply_selected (ECaldavChooser *chooser)
{
    ESourceWebdav *webdav_extension;
    GtkTreeSelection *selection;
    GtkTreeModel *model;
    GtkTreeIter iter;
    ESource *source;
    GdkColor *color;
    gboolean has_color;
    gchar *display_name;
    gchar *path_encoded;

    g_return_val_if_fail (E_IS_CALDAV_CHOOSER (chooser), FALSE);

    source = e_caldav_chooser_get_source (chooser);
    selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (chooser));

    if (!gtk_tree_selection_get_selected (selection, &model, &iter))
        return FALSE;

    gtk_tree_model_get (
        model, &iter,
        COLUMN_DISPLAY_NAME, &display_name,
        COLUMN_PATH_ENCODED, &path_encoded,
        COLUMN_HAS_COLOR, &has_color,
        COLUMN_COLOR, &color,
        -1);

    /* 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, 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_caldav_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);
    }

    g_free (display_name);
    g_free (path_encoded);

    return TRUE;
}