diff --git a/calendar/backends/caldav/e-cal-backend-caldav.c b/calendar/backends/caldav/e-cal-backend-caldav.c
index 4e1693e..fa90ce1 100644
--- a/calendar/backends/caldav/e-cal-backend-caldav.c
+++ b/calendar/backends/caldav/e-cal-backend-caldav.c
@@ -5417,12 +5417,31 @@ caldav_authenticate_sync (EBackend *backend,
 		g_cond_signal (&cbdav->priv->cond);
 	} else if (g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed) ||
 		   g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationRequired)) {
-		if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD) ||
-		    g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationRequired))
-			result = E_SOURCE_AUTHENTICATION_REQUIRED;
-		else
-			result = E_SOURCE_AUTHENTICATION_REJECTED;
-		g_clear_error (&local_error);
+		gchar *auth_user = NULL;
+
+		if (!username || !*username) {
+			ESource *source;
+			ESourceAuthentication *auth_extension;
+
+			source = e_backend_get_source (backend);
+			auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+			auth_user = e_source_authentication_dup_user (auth_extension);
+
+			username = auth_user;
+		}
+
+		if (username && *username) {
+			if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+				result = E_SOURCE_AUTHENTICATION_REQUIRED;
+			else
+				result = E_SOURCE_AUTHENTICATION_REJECTED;
+			g_clear_error (&local_error);
+		} else {
+			result = E_SOURCE_AUTHENTICATION_ERROR;
+			g_propagate_error (error, local_error);
+		}
+
+		g_free (auth_user);
 	} else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
 		result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
 		g_propagate_error (error, local_error);
diff --git a/libebackend/Makefile.am b/libebackend/Makefile.am
index dd4633f..30a2cf5 100644
--- a/libebackend/Makefile.am
+++ b/libebackend/Makefile.am
@@ -65,8 +65,6 @@ libebackend_1_2_la_SOURCES = \
 	e-offline-listener.c \
 	e-server-side-source.c \
 	e-server-side-source-credentials-provider.c \
-	e-soup-auth-bearer.c \
-	e-soup-ssl-trust.c \
 	e-source-registry-server.c \
 	e-sqlite3-vfs.c \
 	e-subprocess-factory.c \
@@ -110,8 +108,6 @@ libebackendinclude_HEADERS = \
 	e-offline-listener.h \
 	e-server-side-source.h \
 	e-server-side-source-credentials-provider.h \
-	e-soup-auth-bearer.h \
-	e-soup-ssl-trust.h \
 	e-source-registry-server.h \
 	e-sqlite3-vfs.h \
 	e-subprocess-factory.h \
diff --git a/libebackend/libebackend.h b/libebackend/libebackend.h
index c0fa01c..cd8316e 100644
--- a/libebackend/libebackend.h
+++ b/libebackend/libebackend.h
@@ -34,8 +34,6 @@
 #include <libebackend/e-oauth2-support.h>
 #include <libebackend/e-offline-listener.h>
 #include <libebackend/e-server-side-source.h>
-#include <libebackend/e-soup-auth-bearer.h>
-#include <libebackend/e-soup-ssl-trust.h>
 #include <libebackend/e-source-registry-server.h>
 #include <libebackend/e-sqlite3-vfs.h>
 #include <libebackend/e-subprocess-factory.h>
diff --git a/libedataserver/Makefile.am b/libedataserver/Makefile.am
index b3396dd..e04eee8 100644
--- a/libedataserver/Makefile.am
+++ b/libedataserver/Makefile.am
@@ -66,6 +66,8 @@ libedataserver_1_2_la_SOURCES = \
 	e-operation-pool.c \
 	e-proxy.c \
 	e-sexp.c \
+	e-soup-auth-bearer.c \
+	e-soup-ssl-trust.c \
 	e-source.c \
 	e-source-extension.c \
 	e-source-address-book.c \
@@ -104,6 +106,7 @@ libedataserver_1_2_la_SOURCES = \
 	e-transliterator-private.h \
 	e-uid.c \
 	e-url.c \
+	e-webdav-discover.c \
 	e-data-server-util.c \
 	e-xml-utils.c \
 	e-xml-hash-utils.c \
@@ -150,6 +153,8 @@ libedataserverinclude_HEADERS = \
 	e-operation-pool.h \
 	e-proxy.h \
 	e-sexp.h \
+	e-soup-auth-bearer.h \
+	e-soup-ssl-trust.h \
 	e-source.h \
 	e-source-address-book.h \
 	e-source-alarms.h \
@@ -189,6 +194,7 @@ libedataserverinclude_HEADERS = \
 	e-time-utils.h \
 	e-uid.h \
 	e-url.h \
+	e-webdav-discover.h \
 	e-data-server-util.h \
 	e-xml-utils.h \
 	e-xml-hash-utils.h \
diff --git a/libedataserver/e-soup-auth-bearer.c b/libedataserver/e-soup-auth-bearer.c
new file mode 100644
index 0000000..4cdd029
--- /dev/null
+++ b/libedataserver/e-soup-auth-bearer.c
@@ -0,0 +1,199 @@
+/*
+ * e-soup-auth-bearer.c
+ *
+ * This library 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 library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * SECTION: e-soup-auth-bearer
+ * @include: libedataserver/libedataserver.h
+ * @short_description: OAuth 2.0 support for libsoup
+ *
+ * #ESoupAuthBearer adds libsoup support for the use of bearer tokens in
+ * HTTP requests to access OAuth 2.0 protected resources, as defined in
+ * <ulink url="http://tools.ietf.org/html/rfc6750">RFC 6750</ulink>.
+ *
+ * An #EBackend should integrate #ESoupAuthBearer first by adding it as a
+ * feature to a #SoupSession's #SoupAuthManager, then from a #SoupSession
+ * #SoupSession::authenticate handler call e_source_get_oauth2_access_token()
+ * and pass the results to e_soup_auth_bearer_set_access_token().
+ **/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "e-soup-auth-bearer.h"
+
+#include <time.h>
+
+#define E_SOUP_AUTH_BEARER_GET_PRIVATE(obj) \
+	(G_TYPE_INSTANCE_GET_PRIVATE \
+	((obj), E_TYPE_SOUP_AUTH_BEARER, ESoupAuthBearerPrivate))
+
+#define AUTH_STRENGTH 1
+
+#define EXPIRY_INVALID ((time_t) -1)
+
+struct _ESoupAuthBearerPrivate {
+	gchar *access_token;
+	time_t expiry;
+};
+
+G_DEFINE_TYPE (
+	ESoupAuthBearer,
+	e_soup_auth_bearer,
+	SOUP_TYPE_AUTH)
+
+static gboolean
+e_soup_auth_bearer_is_expired (ESoupAuthBearer *bearer)
+{
+	gboolean expired = FALSE;
+
+	if (bearer->priv->expiry != EXPIRY_INVALID)
+		expired = (bearer->priv->expiry < time (NULL));
+
+	return expired;
+}
+
+static void
+e_soup_auth_bearer_finalize (GObject *object)
+{
+	ESoupAuthBearerPrivate *priv;
+
+	priv = E_SOUP_AUTH_BEARER_GET_PRIVATE (object);
+
+	g_free (priv->access_token);
+
+	/* Chain up to parent's finalize() method. */
+	G_OBJECT_CLASS (e_soup_auth_bearer_parent_class)->finalize (object);
+}
+
+static gboolean
+e_soup_auth_bearer_update (SoupAuth *auth,
+                           SoupMessage *message,
+                           GHashTable *auth_header)
+{
+	/* XXX Not sure what to do here.  Discard the access token? */
+
+	return TRUE;
+}
+
+static GSList *
+e_soup_auth_bearer_get_protection_space (SoupAuth *auth,
+                                         SoupURI *source_uri)
+{
+	/* XXX Not sure what to do here.  Need to return something. */
+
+	return g_slist_prepend (NULL, g_strdup (""));
+}
+
+static gboolean
+e_soup_auth_bearer_is_authenticated (SoupAuth *auth)
+{
+	ESoupAuthBearer *bearer;
+	gboolean authenticated = FALSE;
+
+	bearer = E_SOUP_AUTH_BEARER (auth);
+
+	if (!e_soup_auth_bearer_is_expired (bearer))
+		authenticated = (bearer->priv->access_token != NULL);
+
+	return authenticated;
+}
+
+static gchar *
+e_soup_auth_bearer_get_authorization (SoupAuth *auth,
+                                      SoupMessage *message)
+{
+	ESoupAuthBearer *bearer;
+
+	bearer = E_SOUP_AUTH_BEARER (auth);
+
+	return g_strdup_printf ("Bearer %s", bearer->priv->access_token);
+}
+
+static void
+e_soup_auth_bearer_class_init (ESoupAuthBearerClass *class)
+{
+	GObjectClass *object_class;
+	SoupAuthClass *auth_class;
+
+	g_type_class_add_private (class, sizeof (ESoupAuthBearerPrivate));
+
+	/* Keep the "e" prefix on private methods
+	 * so we don't step on libsoup's namespace. */
+
+	object_class = G_OBJECT_CLASS (class);
+	object_class->finalize = e_soup_auth_bearer_finalize;
+
+	auth_class = SOUP_AUTH_CLASS (class);
+	auth_class->scheme_name = "Bearer";
+	auth_class->strength = AUTH_STRENGTH;
+	auth_class->update = e_soup_auth_bearer_update;
+	auth_class->get_protection_space = e_soup_auth_bearer_get_protection_space;
+	auth_class->is_authenticated = e_soup_auth_bearer_is_authenticated;
+	auth_class->get_authorization = e_soup_auth_bearer_get_authorization;
+}
+
+static void
+e_soup_auth_bearer_init (ESoupAuthBearer *bearer)
+{
+	bearer->priv = E_SOUP_AUTH_BEARER_GET_PRIVATE (bearer);
+	bearer->priv->expiry = EXPIRY_INVALID;
+}
+
+/**
+ * e_soup_auth_bearer_set_access_token:
+ * @bearer: an #ESoupAuthBearer
+ * @access_token: an OAuth 2.0 access token
+ * @expires_in_seconds: expiry for @access_token, or 0 if unknown
+ *
+ * This function is analogous to soup_auth_authenticate() for "Basic" HTTP
+ * authentication, except it takes an OAuth 2.0 access token instead of a
+ * username and password.
+ *
+ * If @expires_in_seconds is greater than zero, soup_auth_is_authenticated()
+ * will return %FALSE after the given number of seconds have elapsed.
+ *
+ * Since: 3.10
+ **/
+void
+e_soup_auth_bearer_set_access_token (ESoupAuthBearer *bearer,
+                                     const gchar *access_token,
+                                     gint expires_in_seconds)
+{
+	gboolean was_authenticated;
+	gboolean now_authenticated;
+
+	g_return_if_fail (E_IS_SOUP_AUTH_BEARER (bearer));
+
+	was_authenticated = soup_auth_is_authenticated (SOUP_AUTH (bearer));
+
+	g_free (bearer->priv->access_token);
+	bearer->priv->access_token = g_strdup (access_token);
+
+	if (expires_in_seconds > 0)
+		bearer->priv->expiry = time (NULL) + expires_in_seconds;
+	else
+		bearer->priv->expiry = EXPIRY_INVALID;
+
+	now_authenticated = soup_auth_is_authenticated (SOUP_AUTH (bearer));
+
+	if (was_authenticated != now_authenticated)
+		g_object_notify (
+			G_OBJECT (bearer),
+			SOUP_AUTH_IS_AUTHENTICATED);
+}
+
diff --git a/libedataserver/e-soup-auth-bearer.h b/libedataserver/e-soup-auth-bearer.h
new file mode 100644
index 0000000..8f08aeb
--- /dev/null
+++ b/libedataserver/e-soup-auth-bearer.h
@@ -0,0 +1,78 @@
+/*
+ * e-soup-auth-bearer.h
+ *
+ * This library 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 library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined (__LIBEDATASERVER_H_INSIDE__) && !defined (LIBEDATASERVER_COMPILATION)
+#error "Only <libedataserver/libedateserver.h> should be included directly."
+#endif
+
+#ifndef E_SOUP_AUTH_BEARER_H
+#define E_SOUP_AUTH_BEARER_H
+
+#include <libsoup/soup.h>
+
+/* Standard GObject macros */
+#define E_TYPE_SOUP_AUTH_BEARER \
+	(e_soup_auth_bearer_get_type ())
+#define E_SOUP_AUTH_BEARER(obj) \
+	(G_TYPE_CHECK_INSTANCE_CAST \
+	((obj), E_TYPE_SOUP_AUTH_BEARER, ESoupAuthBearer))
+#define E_SOUP_AUTH_BEARER_CLASS(cls) \
+	(G_TYPE_CHECK_CLASS_CAST \
+	((cls), E_TYPE_SOUP_AUTH_BEARER, ESoupAuthBearerClass))
+#define E_IS_SOUP_AUTH_BEARER(obj) \
+	(G_TYPE_CHECK_INSTANCE_TYPE \
+	((obj), E_TYPE_SOUP_AUTH_BEARER))
+#define E_IS_SOUP_AUTH_BEARER_CLASS(cls) \
+	(G_TYPE_CHECK_CLASS_TYPE \
+	((cls), E_TYPE_SOUP_AUTH_BEARER))
+#define E_SOUP_AUTH_BEARER_GET_CLASS(obj) \
+	(G_TYPE_INSTANCE_GET_CLASS \
+	((obj), E_TYPE_SOUP_AUTH_BEARER, ESoupAuthBearerClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ESoupAuthBearer ESoupAuthBearer;
+typedef struct _ESoupAuthBearerClass ESoupAuthBearerClass;
+typedef struct _ESoupAuthBearerPrivate ESoupAuthBearerPrivate;
+
+/**
+ * ESoupAuthBearer:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.10
+ **/
+struct _ESoupAuthBearer {
+	SoupAuth parent;
+	ESoupAuthBearerPrivate *priv;
+};
+
+struct _ESoupAuthBearerClass {
+	SoupAuthClass parent_class;
+};
+
+GType		e_soup_auth_bearer_get_type	(void) G_GNUC_CONST;
+void		e_soup_auth_bearer_set_access_token
+						(ESoupAuthBearer *bearer,
+						 const gchar *access_token,
+						 gint expires_in_seconds);
+
+G_END_DECLS
+
+#endif /* E_SOUP_AUTH_BEARER_H */
+
diff --git a/libedataserver/e-soup-ssl-trust.c b/libedataserver/e-soup-ssl-trust.c
new file mode 100644
index 0000000..c12d46a
--- /dev/null
+++ b/libedataserver/e-soup-ssl-trust.c
@@ -0,0 +1,166 @@
+/*
+ * e-soup-ssl-trust.c
+ *
+ * This library 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 library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * SECTION: e-soup-ssl-trust
+ * @include: libedataserver/libedataserver.h
+ * @short_description: SSL certificate trust handling for WebDAV sources
+ *
+ * 
+ **/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "e-source-authentication.h"
+#include "e-source-webdav.h"
+
+#include "e-soup-ssl-trust.h"
+
+typedef struct _ESoupSslTrustData {
+	SoupMessage *soup_message; /* weak */
+	ESource *source;
+
+	GClosure *accept_certificate_closure;
+} ESoupSslTrustData;
+
+static gboolean
+e_soup_ssl_trust_accept_certificate_cb (GTlsConnection *conn,
+					GTlsCertificate *peer_cert,
+					GTlsCertificateFlags errors,
+					gpointer user_data)
+{
+	ESoupSslTrustData *handler = user_data;
+	ETrustPromptResponse response;
+	SoupURI *soup_uri;
+	const gchar *host;
+	gchar *auth_host = NULL;
+
+	soup_uri = soup_message_get_uri (handler->soup_message);
+	if (!soup_uri || !soup_uri_get_host (soup_uri))
+		return FALSE;
+
+	host = soup_uri_get_host (soup_uri);
+
+	if (e_source_has_extension (handler->source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+		ESourceAuthentication *extension_authentication;
+
+		extension_authentication = e_source_get_extension (handler->source, E_SOURCE_EXTENSION_AUTHENTICATION);
+		auth_host = e_source_authentication_dup_host (extension_authentication);
+
+		if (auth_host && *auth_host) {
+			/* Use the 'host' from the Authentication extension, because
+			   it's the one used when storing the trust prompt result.
+			   The SoupMessage can be redirected, thus it would not ever match. */
+			host = auth_host;
+		} else {
+			g_free (auth_host);
+			auth_host = NULL;
+		}
+	}
+
+	response = e_source_webdav_verify_ssl_trust (
+		e_source_get_extension (handler->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND),
+		host, peer_cert, errors);
+
+	g_free (auth_host);
+
+	return (response == E_TRUST_PROMPT_RESPONSE_ACCEPT ||
+	        response == E_TRUST_PROMPT_RESPONSE_ACCEPT_TEMPORARILY);
+}
+
+static void
+e_soup_ssl_trust_network_event_cb (SoupMessage *msg,
+				   GSocketClientEvent event,
+				   GIOStream *connection,
+				   gpointer user_data)
+{
+	ESoupSslTrustData *handler = user_data;
+
+	/* It's either a GTlsConnection or a GTcpConnection */
+	if (event == G_SOCKET_CLIENT_TLS_HANDSHAKING &&
+	    G_IS_TLS_CONNECTION (connection)) {
+		g_signal_connect_closure (
+			G_TLS_CONNECTION (connection), "accept-certificate",
+			handler->accept_certificate_closure, FALSE);
+	}
+}
+
+static void
+e_soup_ssl_trust_message_finalized_cb (gpointer data,
+				       GObject *unused_message)
+{
+	ESoupSslTrustData *handler;
+
+	/* The network event handler will be disconnected from the message just
+	 * before this is called. */
+	handler = data;
+
+	g_clear_object (&handler->source);
+
+	/* Synchronously disconnects the accept certificate handler from all
+	 * GTlsConnections. */
+	g_closure_invalidate (handler->accept_certificate_closure);
+	g_closure_unref (handler->accept_certificate_closure);
+
+	g_free (handler);
+}
+
+/**
+ * e_soup_ssl_trust_connect:
+ * @soup_message: a #SoupMessage about to be sent to the source
+ * @source: an #ESource that uses WebDAV
+ *
+ * Sets up automatic SSL certificate trust handling for @soup_message using the trust
+ * data stored in @source's WebDAV extension. If @soup_message is about to be sent on
+ * an SSL connection with an invalid certificate, the code checks if the WebDAV
+ * extension already has a trust response for that certificate and verifies it
+ * with e_source_webdav_verify_ssl_trust(). If the verification fails, then
+ * the @soup_message send also fails.
+ *
+ * This works by connecting to the "network-event" signal on @soup_message and
+ * connecting to the "accept-certificate" signal on each #GTlsConnection for
+ * which @soup_message reports a #G_SOCKET_CLIENT_TLS_HANDSHAKING event. These
+ * handlers are torn down automatically when @soup_message is disposed. This process
+ * is not thread-safe; it is sufficient for safety if all use of @soup_message's
+ * session and the disposal of @soup_message occur in the same thread.
+ *
+ * Since: 3.16
+ **/
+void
+e_soup_ssl_trust_connect (SoupMessage *soup_message,
+                          ESource *source)
+{
+	ESoupSslTrustData *handler;
+
+	g_return_if_fail (SOUP_IS_MESSAGE (soup_message));
+	g_return_if_fail (E_IS_SOURCE (source));
+
+	handler = g_malloc (sizeof (ESoupSslTrustData));
+	handler->soup_message = soup_message;
+	g_object_weak_ref (G_OBJECT (soup_message), e_soup_ssl_trust_message_finalized_cb, handler);
+	handler->source = g_object_ref (source);
+	handler->accept_certificate_closure = g_cclosure_new (G_CALLBACK (e_soup_ssl_trust_accept_certificate_cb), handler, NULL);
+
+	g_closure_ref (handler->accept_certificate_closure);
+	g_closure_sink (handler->accept_certificate_closure);
+
+	g_signal_connect (
+		soup_message, "network-event",
+		G_CALLBACK (e_soup_ssl_trust_network_event_cb), handler);
+}
diff --git a/libedataserver/e-soup-ssl-trust.h b/libedataserver/e-soup-ssl-trust.h
new file mode 100644
index 0000000..a7e773a
--- /dev/null
+++ b/libedataserver/e-soup-ssl-trust.h
@@ -0,0 +1,36 @@
+/*
+ * e-soup-ssl-trust.h
+ *
+ * This library 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 library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined (__LIBEDATASERVER_H_INSIDE__) && !defined (LIBEDATASERVER_COMPILATION)
+#error "Only <libedataserver/libedateserver.h> should be included directly."
+#endif
+
+#ifndef E_SOUP_SSL_TRUST_H
+#define E_SOUP_SSL_TRUST_H
+
+#include <libedataserver/e-source.h>
+
+#include <libsoup/soup.h>
+
+G_BEGIN_DECLS
+
+void		e_soup_ssl_trust_connect	(SoupMessage *soup_message,
+						 ESource *source);
+
+G_END_DECLS
+
+#endif /* E_SOUP_SSL_TRUST_H */
diff --git a/libedataserver/e-webdav-discover.c b/libedataserver/e-webdav-discover.c
new file mode 100644
index 0000000..8ee1e95
--- /dev/null
+++ b/libedataserver/e-webdav-discover.c
@@ -0,0 +1,1808 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library 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 library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <glib/gi18n-lib.h>
+
+#include <libsoup/soup.h>
+
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+
+#include "e-soup-auth-bearer.h"
+#include "e-soup-ssl-trust.h"
+#include "e-webdav-discover.h"
+
+#define XC(string) ((xmlChar *) string)
+
+/* Standard Namespaces */
+#define NS_WEBDAV  "DAV:"
+#define NS_CALDAV  "urn:ietf:params:xml:ns:caldav"
+#define NS_CARDDAV "urn:ietf:params:xml:ns:carddav"
+
+/* Application-Specific Namespaces */
+#define NS_ICAL    "http://apple.com/ns/ical/"
+
+/* Mainly for readability. */
+enum {
+	DEPTH_0 = 0,
+	DEPTH_1 = 1
+};
+
+typedef struct _EWebDAVDiscoverContext {
+	ESource *source;
+	gchar *url_use_path;
+	ENamedParameters *credentials;
+	gchar *out_certificate_pem;
+	GTlsCertificateFlags out_certificate_errors;
+	GSList *out_discovered_sources;
+	GSList *out_calendar_user_addresses;
+} EWebDAVDiscoverContext;
+
+static EWebDAVDiscoverContext *
+e_webdav_discover_context_new (ESource *source,
+			       const gchar *url_use_path,
+			       const ENamedParameters *credentials)
+{
+	EWebDAVDiscoverContext *context;
+
+	context = g_new0 (EWebDAVDiscoverContext, 1);
+	context->source = g_object_ref (source);
+	context->url_use_path = g_strdup (url_use_path);
+	context->credentials = e_named_parameters_new_clone (credentials);
+	context->out_certificate_pem = NULL;
+	context->out_certificate_errors = 0;
+	context->out_discovered_sources = NULL;
+	context->out_calendar_user_addresses = NULL;
+
+	return context;
+}
+
+static void
+e_webdav_discover_context_free (gpointer ptr)
+{
+	EWebDAVDiscoverContext *context = ptr;
+
+	if (!context)
+		return;
+
+	g_clear_object (&context->source);
+	g_free (context->url_use_path);
+	e_named_parameters_free (context->credentials);
+	g_free (context->out_certificate_pem);
+	e_webdav_discover_free_discovered_sources (context->out_discovered_sources);
+	g_slist_free_full (context->out_calendar_user_addresses, g_free);
+	g_free (context);
+}
+
+static gchar *
+e_webdav_discover_make_href_full_uri (SoupURI *base_uri,
+				      const gchar *href)
+{
+	SoupURI *soup_uri;
+	gchar *full_uri;
+
+	if (!base_uri || !href)
+		return g_strdup (href);
+
+	if (strstr (href, "://"))
+		return g_strdup (href);
+
+	soup_uri = soup_uri_copy (base_uri);
+	soup_uri_set_path (soup_uri, href);
+	soup_uri_set_user (soup_uri, NULL);
+	soup_uri_set_password (soup_uri, NULL);
+
+	full_uri = soup_uri_to_string (soup_uri, FALSE);
+
+	soup_uri_free (soup_uri);
+
+	return full_uri;
+}
+
+static void
+e_webdav_discover_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 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 G_GNUC_NULL_TERMINATED SoupMessage *
+e_webdav_discover_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_CARDDAV), XC ("A"));
+	g_hash_table_insert (namespaces, (gpointer) NS_CARDDAV, 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);
+	xmlFreeDoc (doc);
+
+	soup_message_add_header_handler (
+		message, "got-body", "Location",
+		G_CALLBACK (e_webdav_discover_redirect), session);
+
+	return message;
+}
+
+static xmlXPathObjectPtr
+e_webdav_discover_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 *
+e_webdav_discover_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;
+}
+
+typedef struct _AuthenticateData {
+	ESource *source;
+	const ENamedParameters *credentials;
+} AuthenticateData;
+
+static void
+e_webdav_discover_authenticate_cb (SoupSession *session,
+				   SoupMessage *msg,
+				   SoupAuth *auth,
+				   gboolean retrying,
+				   gpointer user_data)
+{
+	AuthenticateData *auth_data = user_data;
+
+	g_return_if_fail (auth_data != NULL);
+
+	if (retrying)
+		return;
+
+	if (E_IS_SOUP_AUTH_BEARER (auth)) {
+		gchar *access_token = NULL;
+		gint expires_in_seconds = -1;
+		GError *local_error = NULL;
+
+		e_source_get_oauth2_access_token_sync (
+			auth_data->source, NULL, &access_token,
+			&expires_in_seconds, &local_error);
+
+		e_soup_auth_bearer_set_access_token (
+			E_SOUP_AUTH_BEARER (auth),
+			access_token, expires_in_seconds);
+
+		if (local_error != NULL) {
+			soup_message_set_status_full (msg, SOUP_STATUS_FORBIDDEN, local_error->message);
+
+			g_error_free (local_error);
+		}
+
+		g_free (access_token);
+
+	} else {
+		gchar *auth_user = NULL;
+
+		if (e_named_parameters_get (auth_data->credentials, E_SOURCE_CREDENTIAL_USERNAME))
+			auth_user = g_strdup (e_named_parameters_get (auth_data->credentials, E_SOURCE_CREDENTIAL_USERNAME));
+
+		if (auth_user && !*auth_user) {
+			g_free (auth_user);
+			auth_user = NULL;
+		}
+
+		if (!auth_user) {
+			ESourceAuthentication *auth_extension;
+
+			auth_extension = e_source_get_extension (auth_data->source, E_SOURCE_EXTENSION_AUTHENTICATION);
+			auth_user = e_source_authentication_dup_user (auth_extension);
+		}
+
+		if (!auth_user || !*auth_user || !auth_data->credentials || !e_named_parameters_get (auth_data->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+			soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
+		else
+			soup_auth_authenticate (auth, auth_user, e_named_parameters_get (auth_data->credentials, E_SOURCE_CREDENTIAL_PASSWORD));
+
+		g_free (auth_user);
+	}
+}
+
+static gboolean
+e_webdav_discover_check_successful (SoupMessage *message,
+				    gchar **out_certificate_pem,
+				    GTlsCertificateFlags *out_certificate_errors,
+				    GError **error)
+{
+	GIOErrorEnum error_code;
+
+	g_return_val_if_fail (message != NULL, 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:
+			if (out_certificate_pem) {
+				GTlsCertificate *certificate = NULL;
+
+				g_free (*out_certificate_pem);
+				*out_certificate_pem = NULL;
+
+				g_object_get (G_OBJECT (message), "tls-certificate", &certificate, NULL);
+
+				if (certificate) {
+					g_object_get (certificate, "certificate-pem", out_certificate_pem, NULL);
+					g_object_unref (certificate);
+				}
+			}
+
+			if (out_certificate_errors) {
+				*out_certificate_errors = 0;
+				g_object_get (G_OBJECT (message), "tls-errors", out_certificate_errors, NULL);
+			}
+
+			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
+e_webdav_discover_parse_xml (SoupMessage *message,
+			     const gchar *expected_name,
+			     gchar **out_certificate_pem,
+			     GTlsCertificateFlags *out_certificate_errors,
+			     GError **error)
+{
+	xmlDocPtr doc;
+	xmlNodePtr root;
+
+	if (!e_webdav_discover_check_successful (message, out_certificate_pem, out_certificate_errors, 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 void
+e_webdav_discover_process_user_address_set (xmlXPathContextPtr xp_ctx,
+					    GSList **out_calendar_user_addresses)
+{
+	xmlXPathObjectPtr xp_obj;
+	gint ii, length;
+
+	if (!out_calendar_user_addresses)
+		return;
+
+	xp_obj = e_webdav_discover_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++) {
+		GSList *duplicate;
+		const gchar *address;
+		gchar *href;
+
+		href = e_webdav_discover_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_slist_find_custom (
+			*out_calendar_user_addresses,
+			address, (GCompareFunc) g_ascii_strcasecmp);
+
+		if (duplicate != NULL) {
+			g_free (href);
+			continue;
+		}
+
+		*out_calendar_user_addresses = g_slist_prepend (
+			*out_calendar_user_addresses, g_strdup (address));
+
+		g_free (href);
+	}
+
+	xmlXPathFreeObject (xp_obj);
+}
+
+static guint32
+e_webdav_discover_get_supported_component_set (xmlXPathContextPtr xp_ctx,
+					       gint index)
+{
+	xmlXPathObjectPtr xp_obj;
+	guint32 set = 0;
+	gint ii, length;
+
+	xp_obj = e_webdav_discover_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 E_WEBDAV_DISCOVER_SUPPORTS_EVENTS |
+		       E_WEBDAV_DISCOVER_SUPPORTS_MEMOS |
+		       E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
+
+	length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
+
+	for (ii = 0; ii < length; ii++) {
+		gchar *name;
+
+		name = e_webdav_discover_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 |= E_WEBDAV_DISCOVER_SUPPORTS_EVENTS;
+		else if (g_ascii_strcasecmp (name, "VJOURNAL") == 0)
+			set |= E_WEBDAV_DISCOVER_SUPPORTS_MEMOS;
+		else if (g_ascii_strcasecmp (name, "VTODO") == 0)
+			set |= E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
+
+		g_free (name);
+	}
+
+	xmlXPathFreeObject (xp_obj);
+
+	return set;
+}
+
+static void
+e_webdav_discover_process_calendar_response (SoupMessage *message,
+					     xmlXPathContextPtr xp_ctx,
+					     gint index,
+					     GSList **out_discovered_sources)
+{
+	xmlXPathObjectPtr xp_obj;
+	guint32 comp_set;
+	gchar *color_spec;
+	gchar *display_name;
+	gchar *description;
+	gchar *href_encoded;
+	gchar *status_line;
+	guint status;
+	gboolean success;
+	EWebDAVDiscoveredSource *discovered_source;
+
+	if (!out_discovered_sources)
+		return;
+
+	status_line = e_webdav_discover_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;
+
+	comp_set = e_webdav_discover_get_supported_component_set (xp_ctx, index);
+	if (comp_set == E_WEBDAV_DISCOVER_SUPPORTS_NONE)
+		return;
+
+	href_encoded = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:href",
+		index);
+
+	if (href_encoded == NULL)
+		return;
+
+	/* Make sure the resource is a calendar. */
+
+	xp_obj = e_webdav_discover_get_xpath (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/D:resourcetype"
+		"/C:calendar",
+		index);
+
+	if (xp_obj == NULL) {
+		g_free (href_encoded);
+		return;
+	}
+
+	xmlXPathFreeObject (xp_obj);
+
+	/* Get the display name or fall back to the href. */
+
+	display_name = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/D:displayname",
+		index);
+
+	if (display_name == NULL) {
+		gchar *href_decoded = soup_uri_decode (href_encoded);
+
+		if (href_decoded) {
+			gchar *cp;
+
+			/* Use the last non-empty path segment. */
+			while ((cp = strrchr (href_decoded, '/')) != NULL) {
+				if (*(cp + 1) == '\0')
+					*cp = '\0';
+				else {
+					display_name = g_strdup (cp + 1);
+					break;
+				}
+			}
+		}
+
+		g_free (href_decoded);
+	}
+
+	description = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/C:calendar-description",
+		index);
+
+	/* Get the color specification string. */
+
+	color_spec = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/IC:calendar-color",
+		index);
+
+	discovered_source = g_new0 (EWebDAVDiscoveredSource, 1);
+	discovered_source->href = e_webdav_discover_make_href_full_uri (soup_message_get_uri (message), href_encoded);
+	discovered_source->supports = comp_set;
+	discovered_source->display_name = g_strdup (display_name);
+	discovered_source->description = g_strdup (description);
+	discovered_source->color = g_strdup (color_spec);
+
+	*out_discovered_sources = g_slist_prepend (*out_discovered_sources, discovered_source);
+
+	g_free (href_encoded);
+	g_free (display_name);
+	g_free (description);
+	g_free (color_spec);
+}
+
+static gboolean
+e_webdav_discover_get_calendar_collection_details (SoupSession *session,
+						   SoupMessage *message,
+						   const gchar *path_or_uri,
+						   ESource *source,
+						   gchar **out_certificate_pem,
+						   GTlsCertificateFlags *out_certificate_errors,
+						   GSList **out_discovered_sources,
+						   GCancellable *cancellable,
+						   GError **error)
+{
+	xmlDocPtr doc;
+	xmlXPathContextPtr xp_ctx;
+	xmlXPathObjectPtr xp_obj;
+	SoupURI *soup_uri;
+
+	if (g_cancellable_is_cancelled (cancellable))
+		return FALSE;
+
+	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 = e_webdav_discover_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_ICAL,   XC ("calendar-color"),
+		NULL);
+
+	e_soup_ssl_trust_connect (message, source);
+
+	/* This takes ownership of the message. */
+	soup_session_send_message (session, message);
+
+	soup_uri_free (soup_uri);
+
+	doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, out_certificate_errors, error);
+	if (!doc)
+		return FALSE;
+
+	xp_ctx = xmlXPathNewContext (doc);
+	xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
+
+	xp_obj = e_webdav_discover_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++)
+			e_webdav_discover_process_calendar_response (
+				message, xp_ctx, ii + 1, out_discovered_sources);
+
+		xmlXPathFreeObject (xp_obj);
+	}
+
+	xmlXPathFreeContext (xp_ctx);
+	xmlFreeDoc (doc);
+
+	return TRUE;
+}
+
+static gboolean
+e_webdav_discover_process_calendar_home_set (SoupSession *session,
+					     SoupMessage *message,
+					     ESource *source,
+					     gchar **out_certificate_pem,
+					     GTlsCertificateFlags *out_certificate_errors,
+					     GSList **out_discovered_sources,
+					     GSList **out_calendar_user_addresses,
+					     GCancellable *cancellable,
+					     GError **error)
+{
+	SoupURI *soup_uri;
+	xmlDocPtr doc;
+	xmlXPathContextPtr xp_ctx;
+	xmlXPathObjectPtr xp_obj;
+	gchar *calendar_home_set;
+	gboolean success;
+
+	g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
+	g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
+	g_return_val_if_fail (out_discovered_sources != NULL, FALSE);
+	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+	if (g_cancellable_is_cancelled (cancellable))
+		return FALSE;
+
+	doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, out_certificate_errors, error);
+
+	if (!doc)
+		return FALSE;
+
+	xp_ctx = xmlXPathNewContext (doc);
+	xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
+
+	/* Record any "C:calendar-user-address-set" properties. */
+	e_webdav_discover_process_user_address_set (xp_ctx, out_calendar_user_addresses);
+
+	/* 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 = e_webdav_discover_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 = e_webdav_discover_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 = e_webdav_discover_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 = e_webdav_discover_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);
+		xmlXPathFreeContext (xp_ctx);
+		xmlFreeDoc (doc);
+		return TRUE;
+	}
+
+ get_collection_details:
+
+	xmlXPathFreeContext (xp_ctx);
+	xmlFreeDoc (doc);
+
+	if (!e_webdav_discover_get_calendar_collection_details (
+		session, message, calendar_home_set, source,
+		out_certificate_pem, out_certificate_errors, out_discovered_sources,
+		cancellable, error)) {
+		g_free (calendar_home_set);
+		return FALSE;
+	}
+
+	g_free (calendar_home_set);
+
+	return TRUE;
+
+ 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 = e_webdav_discover_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, source);
+
+	/* This takes ownership of the message. */
+	soup_session_send_message (session, message);
+
+	soup_uri_free (soup_uri);
+
+	g_free (calendar_home_set);
+
+	success = e_webdav_discover_process_calendar_home_set (session, message, source,
+		out_certificate_pem, out_certificate_errors, out_discovered_sources, out_calendar_user_addresses,
+		cancellable, error);
+
+	g_object_unref (message);
+
+	return success;
+}
+
+static void
+e_webdav_discover_process_addressbook_response (SoupMessage *message,
+						xmlXPathContextPtr xp_ctx,
+						gint index,
+						GSList **out_discovered_sources)
+{
+	xmlXPathObjectPtr xp_obj;
+	gchar *display_name;
+	gchar *description;
+	gchar *href_encoded;
+	gchar *status_line;
+	guint status;
+	gboolean success;
+	EWebDAVDiscoveredSource *discovered_source;
+
+	if (!out_discovered_sources)
+		return;
+
+	status_line = e_webdav_discover_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 = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:href",
+		index);
+
+	if (href_encoded == NULL)
+		return;
+
+	/* Make sure the resource is an addressbook. */
+
+	xp_obj = e_webdav_discover_get_xpath (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/D:resourcetype"
+		"/A:addressbook",
+		index);
+
+	if (xp_obj == NULL) {
+		g_free (href_encoded);
+		return;
+	}
+
+	xmlXPathFreeObject (xp_obj);
+
+	/* Get the display name or fall back to the href. */
+
+	display_name = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/D:displayname",
+		index);
+
+	if (display_name == NULL) {
+		gchar *href_decoded = soup_uri_decode (href_encoded);
+
+		if (href_decoded) {
+			gchar *cp;
+
+			/* Use the last non-empty path segment. */
+			while ((cp = strrchr (href_decoded, '/')) != NULL) {
+				if (*(cp + 1) == '\0')
+					*cp = '\0';
+				else {
+					display_name = g_strdup (cp + 1);
+					break;
+				}
+			}
+		}
+
+		g_free (href_decoded);
+	}
+
+	description = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response[%d]"
+		"/D:propstat"
+		"/D:prop"
+		"/A:addressbook-description",
+		index);
+
+	discovered_source = g_new0 (EWebDAVDiscoveredSource, 1);
+	discovered_source->href = e_webdav_discover_make_href_full_uri (soup_message_get_uri (message), href_encoded);
+	discovered_source->supports = E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS;
+	discovered_source->display_name = g_strdup (display_name);
+	discovered_source->description = g_strdup (description);
+	discovered_source->color = NULL;
+
+	*out_discovered_sources = g_slist_prepend (*out_discovered_sources, discovered_source);
+
+	g_free (href_encoded);
+	g_free (display_name);
+	g_free (description);
+}
+
+static gboolean
+e_webdav_discover_get_addressbook_collection_details (SoupSession *session,
+						      SoupMessage *message,
+						      const gchar *path_or_uri,
+						      ESource *source,
+						      gchar **out_certificate_pem,
+						      GTlsCertificateFlags *out_certificate_errors,
+						      GSList **out_discovered_sources,
+						      GCancellable *cancellable,
+						      GError **error)
+{
+	xmlDocPtr doc;
+	xmlXPathContextPtr xp_ctx;
+	xmlXPathObjectPtr xp_obj;
+	SoupURI *soup_uri;
+
+	if (g_cancellable_is_cancelled (cancellable))
+		return FALSE;
+
+	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 = e_webdav_discover_new_propfind (
+		session, soup_uri, DEPTH_1,
+		NS_WEBDAV, XC ("displayname"),
+		NS_WEBDAV, XC ("resourcetype"),
+		NS_CARDDAV, XC ("addressbook-description"),
+		NULL);
+
+	e_soup_ssl_trust_connect (message, source);
+
+	/* This takes ownership of the message. */
+	soup_session_send_message (session, message);
+
+	soup_uri_free (soup_uri);
+
+	doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, out_certificate_errors, error);
+	if (!doc)
+		return FALSE;
+
+	xp_ctx = xmlXPathNewContext (doc);
+	xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
+
+	xp_obj = e_webdav_discover_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++)
+			e_webdav_discover_process_addressbook_response (
+				message, xp_ctx, ii + 1, out_discovered_sources);
+
+		xmlXPathFreeObject (xp_obj);
+	}
+
+	xmlXPathFreeContext (xp_ctx);
+	xmlFreeDoc (doc);
+
+	return TRUE;
+}
+
+static gboolean
+e_webdav_discover_process_addressbook_home_set (SoupSession *session,
+						SoupMessage *message,
+						ESource *source,
+						gchar **out_certificate_pem,
+						GTlsCertificateFlags *out_certificate_errors,
+						GSList **out_discovered_sources,
+						GCancellable *cancellable,
+						GError **error)
+{
+	SoupURI *soup_uri;
+	xmlDocPtr doc;
+	xmlXPathContextPtr xp_ctx;
+	xmlXPathObjectPtr xp_obj;
+	gchar *addressbook_home_set;
+	gboolean success;
+
+	g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
+	g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
+	g_return_val_if_fail (out_discovered_sources != NULL, FALSE);
+	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+	if (g_cancellable_is_cancelled (cancellable))
+		return FALSE;
+
+	doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, out_certificate_errors, error);
+
+	if (!doc)
+		return FALSE;
+
+	xp_ctx = xmlXPathNewContext (doc);
+	xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
+	xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
+
+	/* Try to find the addressbook home URL using the
+	 * following properties in order of preference:
+	 *
+	 *   "A:addressbook-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 "A:addressbook-home-set" property.
+	 */
+
+	/* FIXME There can be multiple "D:href" elements for a
+	 *       "A:addressbook-home-set".  We're only processing
+	 *       the first one.  Need to iterate over them. */
+
+	addressbook_home_set = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response"
+		"/D:propstat"
+		"/D:prop"
+		"/A:addressbook-home-set"
+		"/D:href");
+
+	if (addressbook_home_set != NULL)
+		goto get_collection_details;
+
+	g_free (addressbook_home_set);
+
+	addressbook_home_set = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response"
+		"/D:propstat"
+		"/D:prop"
+		"/D:current-user-principal"
+		"/D:href");
+
+	if (addressbook_home_set != NULL)
+		goto retry_propfind;
+
+	g_free (addressbook_home_set);
+
+	addressbook_home_set = e_webdav_discover_get_xpath_string (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response"
+		"/D:propstat"
+		"/D:prop"
+		"/D:principal-URL"
+		"/D:href");
+
+	if (addressbook_home_set != NULL)
+		goto retry_propfind;
+
+	g_free (addressbook_home_set);
+	addressbook_home_set = NULL;
+
+	/* None of the aforementioned properties are present.  If the
+	 * user-supplied CardDAV URL is an addressbook resource, use that. */
+
+	xp_obj = e_webdav_discover_get_xpath (
+		xp_ctx,
+		"/D:multistatus"
+		"/D:response"
+		"/D:propstat"
+		"/D:prop"
+		"/D:resourcetype"
+		"/A:addressbook");
+
+	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';
+					addressbook_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 (addressbook_home_set == NULL || *addressbook_home_set == '\0') {
+		g_free (addressbook_home_set);
+		xmlXPathFreeContext (xp_ctx);
+		xmlFreeDoc (doc);
+		return TRUE;
+	}
+
+ get_collection_details:
+
+	xmlXPathFreeContext (xp_ctx);
+	xmlFreeDoc (doc);
+
+	if (!e_webdav_discover_get_addressbook_collection_details (
+		session, message, addressbook_home_set, source,
+		out_certificate_pem, out_certificate_errors, out_discovered_sources,
+		cancellable, error)) {
+		g_free (addressbook_home_set);
+		return FALSE;
+	}
+
+	g_free (addressbook_home_set);
+
+	return TRUE;
+
+ retry_propfind:
+
+	xmlXPathFreeContext (xp_ctx);
+	xmlFreeDoc (doc);
+
+	soup_uri = soup_uri_copy (soup_message_get_uri (message));
+	soup_uri_set_path (soup_uri, addressbook_home_set);
+
+	/* Note that we omit "D:resourcetype", "D:current-user-principal"
+	 * and "D:principal-URL" in order to short-circuit the recursion. */
+	message = e_webdav_discover_new_propfind (
+		session, soup_uri, DEPTH_1,
+		NS_CARDDAV, XC ("addressbook-home-set"),
+		NULL);
+
+	e_soup_ssl_trust_connect (message, source);
+
+	/* This takes ownership of the message. */
+	soup_session_send_message (session, message);
+
+	soup_uri_free (soup_uri);
+
+	g_free (addressbook_home_set);
+
+	success = e_webdav_discover_process_addressbook_home_set (session, message, source,
+		out_certificate_pem, out_certificate_errors, out_discovered_sources,
+		cancellable, error);
+
+	g_object_unref (message);
+
+	return success;
+}
+
+static void
+e_webdav_discover_source_free (gpointer ptr)
+{
+	EWebDAVDiscoveredSource *discovered_source = ptr;
+
+	if (discovered_source) {
+		g_free (discovered_source->href);
+		g_free (discovered_source->display_name);
+		g_free (discovered_source->description);
+		g_free (discovered_source->color);
+		g_free (discovered_source);
+	}
+}
+
+void
+e_webdav_discover_free_discovered_sources (GSList *discovered_sources)
+{
+	g_slist_free_full (discovered_sources, e_webdav_discover_source_free);
+}
+
+static void
+e_webdav_discover_sources_thread (GTask *task,
+				  gpointer source_object,
+				  gpointer task_data,
+				  GCancellable *cancellable)
+{
+	EWebDAVDiscoverContext *context = task_data;
+	gboolean success;
+	GError *local_error = NULL;
+
+	g_return_if_fail (context != NULL);
+	g_return_if_fail (E_IS_SOURCE (source_object));
+
+	success = e_webdav_discover_sources_sync (E_SOURCE (source_object),
+		context->url_use_path, context->credentials, &context->out_certificate_pem,
+		&context->out_certificate_errors, &context->out_discovered_sources,
+		&context->out_calendar_user_addresses, cancellable, &local_error);
+
+	if (local_error != NULL) {
+		g_task_return_error (task, local_error);
+	} else {
+		g_task_return_boolean (task, success);
+	}
+}
+
+void
+e_webdav_discover_sources (ESource *source,
+			   const gchar *url_use_path,
+			   const ENamedParameters *credentials,
+			   GCancellable *cancellable,
+			   GAsyncReadyCallback callback,
+			   gpointer user_data)
+{
+	EWebDAVDiscoverContext *context;
+	GTask *task;
+
+	g_return_if_fail (E_IS_SOURCE (source));
+
+	context = e_webdav_discover_context_new (source, url_use_path, credentials);
+
+	task = g_task_new (source, cancellable, callback, user_data);
+	g_task_set_source_tag (task, e_webdav_discover_sources);
+	g_task_set_task_data (task, context, e_webdav_discover_context_free);
+
+	g_task_run_in_thread (task, e_webdav_discover_sources_thread);
+
+	g_object_unref (task);
+}
+
+gboolean
+e_webdav_discover_sources_finish (ESource *source,
+				  GAsyncResult *result,
+				  gchar **out_certificate_pem,
+				  GTlsCertificateFlags *out_certificate_errors,
+				  GSList **out_discovered_sources,
+				  GSList **out_calendar_user_addresses,
+				  GError **error)
+{
+	EWebDAVDiscoverContext *context;
+
+	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+	g_return_val_if_fail (g_task_is_valid (result, source), FALSE);
+
+	g_return_val_if_fail (
+		g_async_result_is_tagged (
+		result, e_webdav_discover_sources), FALSE);
+
+	if (!g_task_propagate_boolean (G_TASK (result), error))
+		return FALSE;
+
+	context = g_task_get_task_data (G_TASK (result));
+	g_return_val_if_fail (context != NULL, FALSE);
+
+	if (out_certificate_pem) {
+		*out_certificate_pem = context->out_certificate_pem;
+		context->out_certificate_pem = NULL;
+	}
+
+	if (out_certificate_errors)
+		*out_certificate_errors = context->out_certificate_errors;
+
+	if (out_discovered_sources) {
+		*out_discovered_sources = context->out_discovered_sources;
+		context->out_discovered_sources = NULL;
+	}
+
+	if (out_calendar_user_addresses) {
+		*out_calendar_user_addresses = context->out_calendar_user_addresses;
+		context->out_calendar_user_addresses = NULL;
+	}
+
+	return TRUE;
+}
+
+static void
+e_webdav_discover_cancelled_cb (GCancellable *cancellable,
+				SoupSession *session)
+{
+	soup_session_abort (session);
+}
+
+gboolean
+e_webdav_discover_sources_sync (ESource *source,
+				const gchar *url_use_path,
+				const ENamedParameters *credentials,
+				gchar **out_certificate_pem,
+				GTlsCertificateFlags *out_certificate_errors,
+				GSList **out_discovered_sources,
+				GSList **out_calendar_user_addresses,
+				GCancellable *cancellable,
+				GError **error)
+{
+	ESourceWebdav *webdav_extension;
+	AuthenticateData auth_data;
+	SoupSession *session;
+	SoupMessage *message;
+	SoupURI *soup_uri;
+	gulong cancelled_handler_id = 0, authenticate_handler_id;
+	gboolean success;
+
+	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+	if (url_use_path && (g_ascii_strncasecmp (url_use_path, "http://", 7) == 0 ||
+	    g_ascii_strncasecmp (url_use_path, "https://", 8) == 0)) {
+		soup_uri = soup_uri_new (url_use_path);
+		url_use_path = NULL;
+	} else {
+		g_return_val_if_fail (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND), FALSE);
+
+		webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+		soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+	}
+
+	g_return_val_if_fail (soup_uri != NULL, FALSE);
+
+	if (url_use_path) {
+		GString *new_path;
+
+		/* Absolute path overrides whole path, while relative path is only appended. */
+		if (*url_use_path == '/') {
+			new_path = g_string_new (url_use_path);
+		} else {
+			const gchar *current_path;
+
+			current_path = soup_uri_get_path (soup_uri);
+			new_path = g_string_new (current_path ? current_path : "");
+			if (!new_path->len || new_path->str[new_path->len - 1] != '/')
+				g_string_append_c (new_path, '/');
+			g_string_append (new_path, url_use_path);
+		}
+
+		if (!new_path->len || new_path->str[new_path->len - 1] != '/')
+			g_string_append_c (new_path, '/');
+
+		soup_uri_set_path (soup_uri, new_path->str);
+
+		g_string_free (new_path, TRUE);
+	}
+
+	session = soup_session_new ();
+	message = e_webdav_discover_new_propfind (
+		session, soup_uri, DEPTH_0,
+		NS_WEBDAV, XC ("resourcetype"),
+		NS_WEBDAV, XC ("current-user-principal"),
+		NS_WEBDAV, XC ("principal-URL"),
+		NS_CALDAV, XC ("calendar-home-set"),
+		NS_CALDAV, XC ("calendar-user-address-set"),
+		NS_CARDDAV, XC ("addressbook-home-set"),
+		NS_CARDDAV, XC ("principal-address"),
+		NULL);
+
+	if (!message) {
+		soup_uri_free (soup_uri);
+		g_object_unref (session);
+		return FALSE;
+	}
+
+	if (g_getenv ("WEBDAV_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);
+	}
+
+	if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+		SoupSessionFeature *feature;
+		ESourceAuthentication *auth_extension;
+		gchar *auth_method;
+
+		feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER);
+		soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
+
+		success = TRUE;
+
+		auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+		auth_method = e_source_authentication_dup_method (auth_extension);
+
+		if (g_strcmp0 (auth_method, "OAuth2") == 0) {
+			SoupAuth *soup_auth;
+			gchar *access_token = NULL;
+			gint expires_in_seconds = -1;
+
+			soup_auth = g_object_new (E_TYPE_SOUP_AUTH_BEARER, SOUP_AUTH_HOST, soup_uri->host, NULL);
+
+			success = e_source_get_oauth2_access_token_sync (
+				source, cancellable, &access_token,
+				&expires_in_seconds, error);
+
+			if (success) {
+				e_soup_auth_bearer_set_access_token (
+					E_SOUP_AUTH_BEARER (soup_auth),
+					access_token, expires_in_seconds);
+
+				soup_auth_manager_use_auth (
+					SOUP_AUTH_MANAGER (feature),
+					soup_uri, soup_auth);
+			}
+
+			g_free (access_token);
+			g_object_unref (soup_auth);
+		}
+
+		g_free (auth_method);
+
+		if (!success) {
+			soup_uri_free (soup_uri);
+			g_object_unref (message);
+			g_object_unref (session);
+			return FALSE;
+		}
+	}
+
+	auth_data.source = source;
+	auth_data.credentials = credentials;
+
+	authenticate_handler_id = g_signal_connect (session, "authenticate",
+		G_CALLBACK (e_webdav_discover_authenticate_cb), &auth_data);
+
+	if (cancellable)
+		cancelled_handler_id = g_cancellable_connect (cancellable, G_CALLBACK (e_webdav_discover_cancelled_cb), session, NULL);
+
+	if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
+		GSList *calendars = NULL, *addressbooks = NULL;
+
+		e_soup_ssl_trust_connect (message, source);
+		soup_session_send_message (session, message);
+
+		success = TRUE;
+		success = e_webdav_discover_process_calendar_home_set (session, message, source, out_certificate_pem,
+			out_certificate_errors, &calendars, out_calendar_user_addresses, cancellable, error);
+		if (success)
+			success = e_webdav_discover_process_addressbook_home_set (session, message, source, out_certificate_pem,
+				out_certificate_errors, &addressbooks, cancellable, error);
+
+		if (success && !calendars && !g_cancellable_is_cancelled (cancellable)) {
+			g_clear_object (&message);
+
+			soup_uri_set_path (soup_uri, "/.well-known/caldav");
+
+			message = e_webdav_discover_new_propfind (
+				session, soup_uri, DEPTH_0,
+				NS_WEBDAV, XC ("resourcetype"),
+				NS_WEBDAV, XC ("current-user-principal"),
+				NS_WEBDAV, XC ("principal-URL"),
+				NS_CALDAV, XC ("calendar-home-set"),
+				NS_CALDAV, XC ("calendar-user-address-set"),
+				NULL);
+
+			if (message) {
+				soup_session_send_message (session, message);
+
+				/* Ignore errors here */
+				e_webdav_discover_process_calendar_home_set (session, message, source, out_certificate_pem,
+					out_certificate_errors, &calendars, out_calendar_user_addresses, cancellable, NULL);
+			}
+		}
+
+		if (success && !addressbooks && !g_cancellable_is_cancelled (cancellable)) {
+			g_clear_object (&message);
+
+			soup_uri_set_path (soup_uri, "/.well-known/carddav");
+
+			message = e_webdav_discover_new_propfind (
+				session, soup_uri, DEPTH_0,
+				NS_WEBDAV, XC ("resourcetype"),
+				NS_WEBDAV, XC ("current-user-principal"),
+				NS_WEBDAV, XC ("principal-URL"),
+				NS_CARDDAV, XC ("addressbook-home-set"),
+				NS_CARDDAV, XC ("principal-address"),
+				NULL);
+
+			if (message) {
+				soup_session_send_message (session, message);
+
+				/* Ignore errors here */
+				e_webdav_discover_process_addressbook_home_set (session, message, source, out_certificate_pem,
+					out_certificate_errors, &addressbooks, cancellable, NULL);
+			}
+		}
+
+		if (out_discovered_sources) {
+			if (calendars)
+				*out_discovered_sources = g_slist_concat (*out_discovered_sources, calendars);
+			if (addressbooks)
+				*out_discovered_sources = g_slist_concat (*out_discovered_sources, addressbooks);
+		} else {
+			e_webdav_discover_free_discovered_sources (calendars);
+			e_webdav_discover_free_discovered_sources (addressbooks);
+		}
+
+		if (out_calendar_user_addresses && *out_calendar_user_addresses)
+			*out_calendar_user_addresses = g_slist_reverse (*out_calendar_user_addresses);
+
+		if (out_discovered_sources && *out_discovered_sources)
+			*out_discovered_sources = g_slist_reverse (*out_discovered_sources);
+	} else {
+		success = FALSE;
+	}
+
+	if (cancellable && cancelled_handler_id)
+		g_cancellable_disconnect (cancellable, cancelled_handler_id);
+
+	if (authenticate_handler_id)
+		g_signal_handler_disconnect (session, authenticate_handler_id);
+
+	soup_uri_free (soup_uri);
+	g_clear_object (&message);
+	g_object_unref (session);
+
+	return success;
+}
diff --git a/libedataserver/e-webdav-discover.h b/libedataserver/e-webdav-discover.h
new file mode 100644
index 0000000..a17c947
--- /dev/null
+++ b/libedataserver/e-webdav-discover.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library 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 library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#if !defined (__LIBEDATASERVER_H_INSIDE__) && !defined (LIBEDATASERVER_COMPILATION)
+#error "Only <libedataserver/libedataserver.h> should be included directly."
+#endif
+
+#ifndef E_WEBDAV_DISCOVER_H
+#define E_WEBDAV_DISCOVER_H
+
+#include <glib.h>
+
+#include <libedataserver/libedataserver.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+	E_WEBDAV_DISCOVER_SUPPORTS_NONE		= 0,
+	E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS	= 1 << 0,
+	E_WEBDAV_DISCOVER_SUPPORTS_EVENTS	= 1 << 1,
+	E_WEBDAV_DISCOVER_SUPPORTS_MEMOS	= 1 << 2,
+	E_WEBDAV_DISCOVER_SUPPORTS_TASKS	= 1 << 3
+} EWebDAVDiscoverSupports;
+
+typedef struct _EWebDAVDiscoveredSource {
+	gchar *href;
+	guint32 supports;
+	gchar *display_name;
+	gchar *description;
+	gchar *color;
+} EWebDAVDiscoveredSource;
+
+void		e_webdav_discover_free_discovered_sources
+							(GSList *discovered_sources);
+
+void		e_webdav_discover_sources		(ESource *source,
+							 const gchar *url_use_path,
+							 const ENamedParameters *credentials,
+			 			         GCancellable *cancellable,
+							 GAsyncReadyCallback callback,
+							 gpointer user_data);
+
+gboolean	e_webdav_discover_sources_finish	(ESource *source,
+							 GAsyncResult *result,
+							 gchar **out_certificate_pem,
+							 GTlsCertificateFlags *out_certificate_errors,
+							 GSList **out_discovered_sources,
+							 GSList **out_calendar_user_addresses,
+							 GError **error);
+
+gboolean	e_webdav_discover_sources_sync		(ESource *source,
+							 const gchar *url_use_path,
+							 const ENamedParameters *credentials,
+							 gchar **out_certificate_pem,
+							 GTlsCertificateFlags *out_certificate_errors,
+							 GSList **out_discovered_sources,
+							 GSList **out_calendar_user_addresses,
+							 GCancellable *cancellable,
+							 GError **error);
+
+G_END_DECLS
+
+#endif /* E_WEBDAV_DISCOVER_H */
diff --git a/libedataserver/libedataserver.h b/libedataserver/libedataserver.h
index 989f603..1e6a443 100644
--- a/libedataserver/libedataserver.h
+++ b/libedataserver/libedataserver.h
@@ -40,6 +40,8 @@
 #include <libedataserver/e-operation-pool.h>
 #include <libedataserver/e-proxy.h>
 #include <libedataserver/e-sexp.h>
+#include <libedataserver/e-soup-auth-bearer.h>
+#include <libedataserver/e-soup-ssl-trust.h>
 #include <libedataserver/e-source-address-book.h>
 #include <libedataserver/e-source-alarms.h>
 #include <libedataserver/e-source-authentication.h>
@@ -78,6 +80,7 @@
 #include <libedataserver/e-time-utils.h>
 #include <libedataserver/e-uid.h>
 #include <libedataserver/e-url.h>
+#include <libedataserver/e-webdav-discover.h>
 #include <libedataserver/e-xml-hash-utils.h>
 #include <libedataserver/e-xml-utils.h>
 #include <libedataserver/eds-version.h>
diff --git a/modules/google-backend/module-google-backend.c b/modules/google-backend/module-google-backend.c
index 9b49a5b..227b7d1 100644
--- a/modules/google-backend/module-google-backend.c
+++ b/modules/google-backend/module-google-backend.c
@@ -118,44 +118,22 @@ google_backend_calendar_update_auth_method (ESource *source)
 {
 	EOAuth2Support *oauth2_support;
 	ESourceAuthentication *auth_extension;
-	ESourceWebdav *webdav_extension;
-	const gchar *extension_name;
-	const gchar *host;
 	const gchar *method;
-	const gchar *path_format;
-	gchar *path;
-	gchar *user;
 
-	oauth2_support = e_server_side_source_ref_oauth2_support (
-		E_SERVER_SIDE_SOURCE (source));
+	oauth2_support = e_server_side_source_ref_oauth2_support (E_SERVER_SIDE_SOURCE (source));
 
 	/* The host name and WebDAV resource path depend on the
 	 * authentication method used, so update those here too. */
 
 	if (oauth2_support != NULL) {
 		method = "OAuth2";
-		host = GOOGLE_CALDAV_V2_HOST;
-		path_format = GOOGLE_CALDAV_V2_PATH;
 	} else {
 		method = "plain/password";
-		host = GOOGLE_CALDAV_V1_HOST;
-		path_format = GOOGLE_CALDAV_V1_PATH;
 	}
 
-	extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-	auth_extension = e_source_get_extension (source, extension_name);
-	e_source_authentication_set_host (auth_extension, host);
+	auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
 	e_source_authentication_set_method (auth_extension, method);
 
-	extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-	webdav_extension = e_source_get_extension (source, extension_name);
-
-	user = e_source_authentication_dup_user (auth_extension);
-	path = g_strdup_printf (path_format, (user != NULL) ? user : "");
-	e_source_webdav_set_resource_path (webdav_extension, path);
-	g_free (path);
-	g_free (user);
-
 	g_clear_object (&oauth2_support);
 }
 
@@ -179,80 +157,280 @@ google_backend_contacts_update_auth_method (ESource *source)
 }
 
 static void
-google_backend_add_calendar (ECollectionBackend *backend)
+google_add_uid_to_hashtable (gpointer source,
+			     gpointer known_sources)
+{
+	ESourceResource *resource;
+	gchar *uid, *rid;
+
+	if (!e_source_has_extension (source, E_SOURCE_EXTENSION_RESOURCE))
+		return;
+
+	resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+
+	uid = e_source_dup_uid (source);
+	if (!uid || !*uid) {
+		g_free (uid);
+		return;
+	}
+
+	rid = e_source_resource_dup_identity (resource);
+	if (!rid || !*rid) {
+		g_free (rid);
+		g_free (uid);
+		return;
+	}
+
+	g_hash_table_insert (known_sources, rid, uid);
+}
+
+static void
+google_remove_unknown_sources_cb (gpointer resource_id,
+				  gpointer uid,
+				  gpointer user_data)
 {
+	ESourceRegistryServer *server = user_data;
 	ESource *source;
-	ESource *collection_source;
+
+	source = e_source_registry_server_ref_source (server, uid);
+
+	if (source) {
+		e_source_registry_server_remove_source (server, source);
+		g_object_unref (source);
+	}
+}
+
+static void
+google_add_found_source (ECollectionBackend *collection,
+			   EWebDAVDiscoverSupports source_type,
+			   SoupURI *uri,
+			   const gchar *display_name,
+			   const gchar *color,
+			   GHashTable *known_sources)
+{
 	ESourceRegistryServer *server;
-	ESourceExtension *extension;
-	ESourceCollection *collection_extension;
-	const gchar *backend_name;
-	const gchar *extension_name;
-	const gchar *resource_id;
+	ESourceBackend *backend;
+	ESource *source = NULL;
+	const gchar *backend_name = NULL;
+	const gchar *provider = NULL;
+	const gchar *identity_prefix = NULL;
+	const gchar *source_uid;
+	gboolean is_new;
+	gchar *url;
+	gchar *identity;
+
+	g_return_if_fail (collection != NULL);
+	g_return_if_fail (uri != NULL);
+	g_return_if_fail (display_name != NULL);
+	g_return_if_fail (known_sources != NULL);
+
+	switch (source_type) {
+	case E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS:
+		backend_name = E_SOURCE_EXTENSION_ADDRESS_BOOK;
+		provider = "webdav";
+		identity_prefix = "contacts";
+		break;
+	case E_WEBDAV_DISCOVER_SUPPORTS_EVENTS:
+		backend_name = E_SOURCE_EXTENSION_CALENDAR;
+		provider = "caldav";
+		identity_prefix = "events";
+		break;
+	case E_WEBDAV_DISCOVER_SUPPORTS_MEMOS:
+		backend_name = E_SOURCE_EXTENSION_MEMO_LIST;
+		provider = "caldav";
+		identity_prefix = "memos";
+		break;
+	case E_WEBDAV_DISCOVER_SUPPORTS_TASKS:
+		backend_name = E_SOURCE_EXTENSION_TASK_LIST;
+		provider = "caldav";
+		identity_prefix = "tasks";
+		break;
+	default:
+		g_warn_if_reached ();
+		return;
+	}
 
-	/* FIXME As a future enhancement, we should query Google
-	 *       for a list of user calendars and add them to the
-	 *       collection with matching display names and colors. */
+	g_return_if_fail (backend_name != NULL);
 
-	/* NOTE: Host name and WebDAV resource path are set in
-	 *       google_backend_calendar_update_auth_method(),
-	 *       since they depend on the auth method used. */
+	server = e_collection_backend_ref_server (collection);
 
-	collection_source = e_backend_get_source (E_BACKEND (backend));
+	url = soup_uri_to_string (uri, FALSE);
+	identity = g_strconcat (identity_prefix, "::", url, NULL);
+	source_uid = g_hash_table_lookup (known_sources, identity);
+	is_new = !source_uid;
+	if (is_new) {
+		ESource *master_source;
 
-	resource_id = GOOGLE_CALENDAR_RESOURCE_ID;
-	source = e_collection_backend_new_child (backend, resource_id);
-	e_source_set_display_name (source, _("Calendar"));
+		source = e_collection_backend_new_child (collection, identity);
+		g_warn_if_fail (source != NULL);
 
-	collection_extension = e_source_get_extension (
-		collection_source, E_SOURCE_EXTENSION_COLLECTION);
+		if (source) {
+			ESourceCollection *collection_extension;
+			ESourceAuthentication *child_auth;
+			ESourceResource *resource;
+			ESourceWebdav *master_webdav, *child_webdav;
 
-	/* Configure the calendar source. */
+			master_source = e_backend_get_source (E_BACKEND (collection));
+			master_webdav = e_source_get_extension (master_source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+			collection_extension = e_source_get_extension (master_source, E_SOURCE_EXTENSION_COLLECTION);
+			child_auth = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+			child_webdav = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+			resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
 
-	backend_name = GOOGLE_CALENDAR_BACKEND_NAME;
+			e_source_authentication_set_user (child_auth, e_source_collection_get_identity (collection_extension));
+			e_source_webdav_set_soup_uri (child_webdav, uri);
+			e_source_resource_set_identity (resource, identity);
 
-	extension_name = E_SOURCE_EXTENSION_CALENDAR;
-	extension = e_source_get_extension (source, extension_name);
+			/* inherit ssl trust options */
+			e_source_webdav_set_ssl_trust (child_webdav, e_source_webdav_get_ssl_trust (master_webdav));
+		}
+	} else {
+		source = e_source_registry_server_ref_source (server, source_uid);
+		g_warn_if_fail (source != NULL);
 
-	e_source_backend_set_backend_name (
-		E_SOURCE_BACKEND (extension), backend_name);
+		g_hash_table_remove (known_sources, identity);
+	}
 
-	extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-	extension = e_source_get_extension (source, extension_name);
+	g_free (identity);
+	g_free (url);
 
-	e_binding_bind_property (
-		collection_extension, "identity",
-		extension, "user",
-		G_BINDING_SYNC_CREATE);
+	/* these properties are synchronized always */
+	if (source) {
+		gint rr, gg, bb;
 
-	/* Make sure the WebDAV resource path is up-to-date, since
-	 * it's built from the "user" property that we just set. */
-	google_backend_calendar_update_auth_method (source);
+		backend = e_source_get_extension (source, backend_name);
+		e_source_backend_set_backend_name (backend, provider);
 
-	extension_name = E_SOURCE_EXTENSION_SECURITY;
-	extension = e_source_get_extension (source, extension_name);
+		e_source_set_display_name (source, display_name);
+		/* Also check whether the color format is as expected; it cannot
+		   be used gdk_rgba_parse here, because it required gdk/gtk. */
+		if (source_type != E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS && color &&
+		    sscanf (color, "#%02x%02x%02x", &rr, &gg, &bb) == 3) {
+			gchar *safe_color;
 
-	e_source_security_set_secure (
-		E_SOURCE_SECURITY (extension), TRUE);
+			/* In case an #RRGGBBAA is returned */
+			safe_color = g_strdup_printf ("#%02x%02x%02x", rr, gg, bb);
 
-	extension_name = E_SOURCE_EXTENSION_ALARMS;
-	extension = e_source_get_extension (source, extension_name);
-	if (!e_source_alarms_get_last_notified (E_SOURCE_ALARMS (extension))) {
-		GTimeVal today_tv;
-		gchar *today;
-
-		g_get_current_time (&today_tv);
-		today = g_time_val_to_iso8601 (&today_tv);
-		e_source_alarms_set_last_notified (
-			E_SOURCE_ALARMS (extension), today);
-		g_free (today);
+			e_source_selectable_set_color (E_SOURCE_SELECTABLE (backend), safe_color);
+
+			g_free (safe_color);
+		}
+
+		if (is_new)
+			e_source_registry_server_add_source (server, source);
+
+		g_object_unref (source);
 	}
 
-	server = e_collection_backend_ref_server (backend);
-	e_source_registry_server_add_source (server, source);
 	g_object_unref (server);
+}
 
-	g_object_unref (source);
+static ESourceAuthenticationResult
+google_backend_authenticate_sync (EBackend *backend,
+				  const ENamedParameters *credentials,
+				  gchar **out_certificate_pem,
+				  GTlsCertificateFlags *out_certificate_errors,
+				  GCancellable *cancellable,
+				  GError **error)
+{
+	ECollectionBackend *collection = E_COLLECTION_BACKEND (backend);
+	ESourceCollection *collection_extension;
+	ESourceGoa *goa_extension;
+	ESource *source;
+	ESourceAuthenticationResult result;
+	GHashTable *known_sources;
+	GList *sources;
+	GSList *discovered_sources = NULL;
+	ENamedParameters *credentials_copy = NULL;
+	gboolean any_success = FALSE;
+	GError *local_error = NULL;
+
+	g_return_val_if_fail (collection != NULL, E_SOURCE_AUTHENTICATION_ERROR);
+
+	source = e_backend_get_source (backend);
+	collection_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_COLLECTION);
+	goa_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_GOA);
+
+	g_return_val_if_fail (e_source_collection_get_calendar_enabled (collection_extension) ||
+		e_source_collection_get_contacts_enabled (collection_extension), E_SOURCE_AUTHENTICATION_ERROR);
+
+	if (credentials && !e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME)) {
+		credentials_copy = e_named_parameters_new_clone (credentials);
+		e_named_parameters_set (credentials_copy, E_SOURCE_CREDENTIAL_USERNAME, e_source_collection_get_identity (collection_extension));
+		credentials = credentials_copy;
+	}
+
+	/* resource-id => source's UID */
+	known_sources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+	sources = e_collection_backend_list_calendar_sources (collection);
+	g_list_foreach (sources, google_add_uid_to_hashtable, known_sources);
+	g_list_free_full (sources, g_object_unref);
+
+	if (e_source_collection_get_calendar_enabled (collection_extension) &&
+	    e_webdav_discover_sources_sync (source, e_source_goa_get_calendar_url (goa_extension),
+		credentials, out_certificate_pem, out_certificate_errors,
+		&discovered_sources, NULL, cancellable, &local_error)) {
+		EWebDAVDiscoverSupports source_types[] = {
+			E_WEBDAV_DISCOVER_SUPPORTS_EVENTS,
+			E_WEBDAV_DISCOVER_SUPPORTS_MEMOS,
+			E_WEBDAV_DISCOVER_SUPPORTS_TASKS
+		};
+		GSList *link;
+		gint ii;
+
+		for (link = discovered_sources; link; link = g_slist_next (link)) {
+			EWebDAVDiscoveredSource *discovered_source = link->data;
+			SoupURI *soup_uri;
+
+			if (!discovered_source || !discovered_source->href || !discovered_source->display_name)
+				continue;
+
+			soup_uri = soup_uri_new (discovered_source->href);
+			if (!soup_uri)
+				continue;
+
+			for (ii = 0; ii < G_N_ELEMENTS (source_types); ii++) {
+				if ((discovered_source->supports & source_types[ii]) == source_types[ii])
+					google_add_found_source (collection, source_types[ii], soup_uri,
+						discovered_source->display_name, discovered_source->color, known_sources);
+			}
+
+			soup_uri_free (soup_uri);
+		}
+
+		any_success = TRUE;
+	}
+
+	if (any_success) {
+		ESourceRegistryServer *server;
+
+		server = e_collection_backend_ref_server (collection);
+
+		g_hash_table_foreach (known_sources, google_remove_unknown_sources_cb, server);
+
+		g_object_unref (server);
+	}
+
+	if (local_error == NULL) {
+		result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+		e_collection_backend_authenticate_children (collection, credentials);
+	} else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
+		   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
+		result = E_SOURCE_AUTHENTICATION_REJECTED;
+		g_clear_error (&local_error);
+	} else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
+		result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
+		g_propagate_error (error, local_error);
+	} else {
+		result = E_SOURCE_AUTHENTICATION_ERROR;
+		g_propagate_error (error, local_error);
+	}
+
+	g_hash_table_destroy (known_sources);
+	e_named_parameters_free (credentials_copy);
+
+	return result;
 }
 
 #if GDATA_CHECK_VERSION(0,15,1)
@@ -369,23 +547,18 @@ static void
 google_backend_populate (ECollectionBackend *backend)
 {
 	GList *list, *link;
-	gboolean have_calendar = FALSE, have_tasks = FALSE;
+	gboolean have_tasks = FALSE;
+	ESourceCollection *collection_extension;
+	ESource *source;
 
 	list = e_collection_backend_list_calendar_sources (backend);
-	for (link = list; link; link = g_list_next (link)) {
+	for (link = list; link && !have_tasks; link = g_list_next (link)) {
 		ESource *source = link->data;
 
-		have_calendar = have_calendar || e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR);
 		have_tasks = have_tasks || e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST);
-
-		if (have_calendar && have_tasks)
-			break;
 	}
 	g_list_free_full (list, (GDestroyNotify) g_object_unref);
 
-	if (!have_calendar)
-		google_backend_add_calendar (backend);
-
 #if GDATA_CHECK_VERSION(0,15,1)
 	if (!have_tasks)
 		google_backend_add_tasks (backend);
@@ -397,8 +570,15 @@ google_backend_populate (ECollectionBackend *backend)
 	g_list_free_full (list, (GDestroyNotify) g_object_unref);
 
 	/* Chain up to parent's populate() method. */
-	E_COLLECTION_BACKEND_CLASS (e_google_backend_parent_class)->
-		populate (backend);
+	E_COLLECTION_BACKEND_CLASS (e_google_backend_parent_class)->populate (backend);
+
+	source = e_backend_get_source (E_BACKEND (backend));
+	collection_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_COLLECTION);
+
+	if (e_source_collection_get_calendar_enabled (collection_extension)) {
+		e_backend_schedule_credentials_required (E_BACKEND (backend),
+			E_SOURCE_CREDENTIALS_REASON_REQUIRED, NULL, 0, NULL, NULL, G_STRFUNC);
+	}
 }
 
 static gchar *
@@ -509,12 +689,16 @@ google_backend_child_added (ECollectionBackend *backend,
 static void
 e_google_backend_class_init (EGoogleBackendClass *class)
 {
-	ECollectionBackendClass *backend_class;
+	EBackendClass *backend_class;
+	ECollectionBackendClass *collection_backend_class;
+
+	backend_class = E_BACKEND_CLASS (class);
+	backend_class->authenticate_sync = google_backend_authenticate_sync;
 
-	backend_class = E_COLLECTION_BACKEND_CLASS (class);
-	backend_class->populate = google_backend_populate;
-	backend_class->dup_resource_id = google_backend_dup_resource_id;
-	backend_class->child_added = google_backend_child_added;
+	collection_backend_class = E_COLLECTION_BACKEND_CLASS (class);
+	collection_backend_class->populate = google_backend_populate;
+	collection_backend_class->dup_resource_id = google_backend_dup_resource_id;
+	collection_backend_class->child_added = google_backend_child_added;
 }
 
 static void
diff --git a/modules/owncloud-backend/Makefile.am b/modules/owncloud-backend/Makefile.am
index 14bf232..7069d3f 100644
--- a/modules/owncloud-backend/Makefile.am
+++ b/modules/owncloud-backend/Makefile.am
@@ -12,8 +12,6 @@ module_owncloud_backend_la_CPPFLAGS = \
 
 module_owncloud_backend_la_SOURCES = \
 	module-owncloud-backend.c \
-	owncloud-utils.c \
-	owncloud-utils.h \
 	$(NULL)
 
 module_owncloud_backend_la_LIBADD = \
diff --git a/modules/owncloud-backend/module-owncloud-backend.c b/modules/owncloud-backend/module-owncloud-backend.c
index 0e898cc..9bc71ab 100644
--- a/modules/owncloud-backend/module-owncloud-backend.c
+++ b/modules/owncloud-backend/module-owncloud-backend.c
@@ -20,8 +20,7 @@
 #endif
 
 #include <libebackend/libebackend.h>
-
-#include "owncloud-utils.h"
+#include <libedataserver/libedataserver.h>
 
 /* Standard GObject macros */
 #define E_TYPE_OWNCLOUD_BACKEND \
@@ -74,6 +73,34 @@ G_DEFINE_DYNAMIC_TYPE (
 	E_TYPE_COLLECTION_BACKEND_FACTORY)
 
 static void
+owncloud_add_uid_to_hashtable (gpointer source,
+			       gpointer known_sources)
+{
+	ESourceResource *resource;
+	gchar *uid, *rid;
+
+	if (!e_source_has_extension (source, E_SOURCE_EXTENSION_RESOURCE))
+		return;
+
+	resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+
+	uid = e_source_dup_uid (source);
+	if (!uid || !*uid) {
+		g_free (uid);
+		return;
+	}
+
+	rid = e_source_resource_dup_identity (resource);
+	if (!rid || !*rid) {
+		g_free (rid);
+		g_free (uid);
+		return;
+	}
+
+	g_hash_table_insert (known_sources, rid, uid);
+}
+
+static void
 owncloud_remove_unknown_sources_cb (gpointer resource_id,
                                     gpointer uid,
                                     gpointer user_data)
@@ -90,14 +117,13 @@ owncloud_remove_unknown_sources_cb (gpointer resource_id,
 }
 
 static void
-owncloud_source_found_cb (ECollectionBackend *collection,
-                          OwnCloudSourceType source_type,
-                          SoupURI *uri,
-                          const gchar *display_name,
-                          const gchar *color,
-                          gpointer user_data)
+owncloud_add_found_source (ECollectionBackend *collection,
+			   EWebDAVDiscoverSupports source_type,
+			   SoupURI *uri,
+			   const gchar *display_name,
+			   const gchar *color,
+			   GHashTable *known_sources)
 {
-	GHashTable *known_sources = user_data;
 	ESourceRegistryServer *server;
 	ESourceBackend *backend;
 	ESource *source = NULL;
@@ -115,26 +141,29 @@ owncloud_source_found_cb (ECollectionBackend *collection,
 	g_return_if_fail (known_sources != NULL);
 
 	switch (source_type) {
-	case OwnCloud_Source_Contacts:
+	case E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS:
 		backend_name = E_SOURCE_EXTENSION_ADDRESS_BOOK;
 		provider = "webdav";
 		identity_prefix = "contacts";
 		break;
-	case OwnCloud_Source_Events:
+	case E_WEBDAV_DISCOVER_SUPPORTS_EVENTS:
 		backend_name = E_SOURCE_EXTENSION_CALENDAR;
 		provider = "caldav";
 		identity_prefix = "events";
 		break;
-	case OwnCloud_Source_Memos:
+	case E_WEBDAV_DISCOVER_SUPPORTS_MEMOS:
 		backend_name = E_SOURCE_EXTENSION_MEMO_LIST;
 		provider = "caldav";
 		identity_prefix = "memos";
 		break;
-	case OwnCloud_Source_Tasks:
+	case E_WEBDAV_DISCOVER_SUPPORTS_TASKS:
 		backend_name = E_SOURCE_EXTENSION_TASK_LIST;
 		provider = "caldav";
 		identity_prefix = "tasks";
 		break;
+	default:
+		g_warn_if_reached ();
+		return;
 	}
 
 	g_return_if_fail (backend_name != NULL);
@@ -152,14 +181,19 @@ owncloud_source_found_cb (ECollectionBackend *collection,
 		g_warn_if_fail (source != NULL);
 
 		if (source) {
+			ESourceCollection *collection_extension;
+			ESourceAuthentication *child_auth;
 			ESourceResource *resource;
 			ESourceWebdav *master_webdav, *child_webdav;
 
 			master_source = e_backend_get_source (E_BACKEND (collection));
 			master_webdav = e_source_get_extension (master_source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+			collection_extension = e_source_get_extension (master_source, E_SOURCE_EXTENSION_COLLECTION);
+			child_auth = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
 			child_webdav = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
 			resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
 
+			e_source_authentication_set_user (child_auth, e_source_collection_get_identity (collection_extension));
 			e_source_webdav_set_soup_uri (child_webdav, uri);
 			e_source_resource_set_identity (resource, identity);
 
@@ -186,9 +220,17 @@ owncloud_source_found_cb (ECollectionBackend *collection,
 		e_source_set_display_name (source, display_name);
 		/* Also check whether the color format is as expected; it cannot
 		   be used gdk_rgba_parse here, because it required gdk/gtk. */
-		if (source_type != OwnCloud_Source_Contacts && color &&
-		    sscanf (color, "#%02x%02x%02x", &rr, &gg, &bb) == 3)
-			e_source_selectable_set_color (E_SOURCE_SELECTABLE (backend), color);
+		if (source_type != E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS && color &&
+		    sscanf (color, "#%02x%02x%02x", &rr, &gg, &bb) == 3) {
+			gchar *safe_color;
+
+			/* In case an #RRGGBBAA is returned */
+			safe_color = g_strdup_printf ("#%02x%02x%02x", rr, gg, bb);
+
+			e_source_selectable_set_color (E_SOURCE_SELECTABLE (backend), safe_color);
+
+			g_free (safe_color);
+		}
 
 		if (is_new)
 			e_source_registry_server_add_source (server, source);
@@ -200,31 +242,34 @@ owncloud_source_found_cb (ECollectionBackend *collection,
 }
 
 static void
-owncloud_add_uid_to_hashtable (gpointer source,
-                               gpointer known_sources)
+owncloud_process_discovered_sources (ECollectionBackend *collection,
+				     GSList *discovered_sources,
+				     GHashTable *known_sources,
+				     const EWebDAVDiscoverSupports *source_types,
+				     gint n_source_types)
 {
-	ESourceResource *resource;
-	gchar *uid, *rid;
+	GSList *link;
+	gint ii;
 
-	if (!e_source_has_extension (source, E_SOURCE_EXTENSION_RESOURCE))
-		return;
+	for (link = discovered_sources; link; link = g_slist_next (link)) {
+		EWebDAVDiscoveredSource *discovered_source = link->data;
+		SoupURI *soup_uri;
 
-	resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+		if (!discovered_source || !discovered_source->href || !discovered_source->display_name)
+			continue;
 
-	uid = e_source_dup_uid (source);
-	if (!uid || !*uid) {
-		g_free (uid);
-		return;
-	}
+		soup_uri = soup_uri_new (discovered_source->href);
+		if (!soup_uri)
+			continue;
 
-	rid = e_source_resource_dup_identity (resource);
-	if (!rid || !*rid) {
-		g_free (rid);
-		g_free (uid);
-		return;
-	}
+		for (ii = 0; ii < n_source_types; ii++) {
+			if ((discovered_source->supports & source_types[ii]) == source_types[ii])
+				owncloud_add_found_source (collection, source_types[ii], soup_uri,
+					discovered_source->display_name, discovered_source->color, known_sources);
+		}
 
-	g_hash_table_insert (known_sources, rid, uid);
+		soup_uri_free (soup_uri);
+	}
 }
 
 static ESourceAuthenticationResult
@@ -236,13 +281,33 @@ owncloud_backend_authenticate_sync (EBackend *backend,
 				    GError **error)
 {
 	ECollectionBackend *collection = E_COLLECTION_BACKEND (backend);
+	ESourceCollection *collection_extension;
+	ESourceGoa *goa_extension;
+	ESource *source;
 	ESourceAuthenticationResult result;
 	GHashTable *known_sources;
 	GList *sources;
+	GSList *discovered_sources = NULL;
+	ENamedParameters *credentials_copy = NULL;
+	gboolean any_success = FALSE, contacts_found = FALSE;
 	GError *local_error = NULL;
 
 	g_return_val_if_fail (collection != NULL, E_SOURCE_AUTHENTICATION_ERROR);
 
+	source = e_backend_get_source (backend);
+	collection_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_COLLECTION);
+	goa_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_GOA);
+
+	if (!e_source_collection_get_calendar_enabled (collection_extension) &&
+	    !e_source_collection_get_contacts_enabled (collection_extension))
+		return E_SOURCE_AUTHENTICATION_ACCEPTED;
+
+	if (credentials && !e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME)) {
+		credentials_copy = e_named_parameters_new_clone (credentials);
+		e_named_parameters_set (credentials_copy, E_SOURCE_CREDENTIAL_USERNAME, e_source_collection_get_identity (collection_extension));
+		credentials = credentials_copy;
+	}
+
 	/* resource-id => source's UID */
 	known_sources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
 
@@ -254,8 +319,50 @@ owncloud_backend_authenticate_sync (EBackend *backend,
 	g_list_foreach (sources, owncloud_add_uid_to_hashtable, known_sources);
 	g_list_free_full (sources, g_object_unref);
 
-	if (owncloud_utils_search_server (collection, credentials, out_certificate_pem, out_certificate_errors,
-		owncloud_source_found_cb, known_sources, cancellable, &local_error)) {
+	if (e_source_collection_get_calendar_enabled (collection_extension) &&
+	    e_webdav_discover_sources_sync (source, e_source_goa_get_calendar_url (goa_extension),
+		credentials, out_certificate_pem, out_certificate_errors,
+		&discovered_sources, NULL, cancellable, &local_error)) {
+		GSList *link;
+		EWebDAVDiscoverSupports source_types[] = {
+			E_WEBDAV_DISCOVER_SUPPORTS_EVENTS,
+			E_WEBDAV_DISCOVER_SUPPORTS_MEMOS,
+			E_WEBDAV_DISCOVER_SUPPORTS_TASKS,
+			E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS
+		};
+
+		for (link = discovered_sources; link && !contacts_found; link = g_slist_next (link)) {
+			EWebDAVDiscoveredSource *discovered_source = link->data;
+
+			if (discovered_source)
+				contacts_found = contacts_found || (discovered_source->supports & E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS) != 0;
+		}
+
+		owncloud_process_discovered_sources (collection, discovered_sources, known_sources, source_types,
+			G_N_ELEMENTS (source_types) - (contacts_found ? 0 : 1));
+
+		e_webdav_discover_free_discovered_sources (discovered_sources);
+		discovered_sources = NULL;
+		any_success = TRUE;
+	}
+
+	/* Skip search in this URL, if the previous one returned also contacts - it's quite likely it did */
+	if (!contacts_found && !local_error && e_source_collection_get_contacts_enabled (collection_extension) &&
+	    e_webdav_discover_sources_sync (source, e_source_goa_get_contacts_url (goa_extension),
+		credentials, out_certificate_pem, out_certificate_errors,
+		&discovered_sources, NULL, cancellable, &local_error)) {
+		EWebDAVDiscoverSupports source_types[] = {
+			E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS
+		};
+
+		owncloud_process_discovered_sources (collection, discovered_sources, known_sources, source_types, G_N_ELEMENTS (source_types));
+
+		e_webdav_discover_free_discovered_sources (discovered_sources);
+		discovered_sources = NULL;
+		any_success = TRUE;
+	}
+
+	if (any_success) {
 		ESourceRegistryServer *server;
 
 		server = e_collection_backend_ref_server (collection);
@@ -268,7 +375,8 @@ owncloud_backend_authenticate_sync (EBackend *backend,
 	if (local_error == NULL) {
 		result = E_SOURCE_AUTHENTICATION_ACCEPTED;
 		e_collection_backend_authenticate_children (collection, credentials);
-	} else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+	} else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
+		   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
 		result = E_SOURCE_AUTHENTICATION_REJECTED;
 		g_clear_error (&local_error);
 	} else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
@@ -280,6 +388,7 @@ owncloud_backend_authenticate_sync (EBackend *backend,
 	}
 
 	g_hash_table_destroy (known_sources);
+	e_named_parameters_free (credentials_copy);
 
 	return result;
 }
@@ -289,6 +398,8 @@ owncloud_backend_populate (ECollectionBackend *collection)
 {
 	GList *list, *liter;
 	ESourceRegistryServer *server;
+	ESourceCollection *collection_extension;
+	ESource *source;
 
 	/* Chain up to parent's populate() method. */
 	E_COLLECTION_BACKEND_CLASS (e_owncloud_backend_parent_class)->populate (collection);
@@ -315,8 +426,14 @@ owncloud_backend_populate (ECollectionBackend *collection)
 	g_list_free_full (list, g_object_unref);
 	g_object_unref (server);
 
-	e_backend_schedule_credentials_required (E_BACKEND (collection), E_SOURCE_CREDENTIALS_REASON_REQUIRED,
-		NULL, 0, NULL, NULL, G_STRFUNC);
+	source = e_backend_get_source (E_BACKEND (collection));
+	collection_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_COLLECTION);
+
+	if (e_source_collection_get_calendar_enabled (collection_extension) ||
+	    e_source_collection_get_contacts_enabled (collection_extension)) {
+		e_backend_schedule_credentials_required (E_BACKEND (collection),
+			E_SOURCE_CREDENTIALS_REASON_REQUIRED, NULL, 0, NULL, NULL, G_STRFUNC);
+	}
 }
 
 static void
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8314282..94cd9d5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -191,6 +191,7 @@ libedataserver/e-source-proxy.c
 libedataserver/e-source-registry.c
 libedataserver/e-source-webdav.c
 libedataserver/e-time-utils.c
+libedataserver/e-webdav-discover.c
 libedataserverui/e-credentials-prompter.c
 libedataserverui/e-credentials-prompter-impl-password.c
 libedataserverui/e-trust-prompt.c
