[evolution-data-server/wip/offline-cache] Fill EWebDAVSession with some functions and add more helper bits



commit c0018d42cb9c080631672f18682a9348d18d2c5b
Author: Milan Crha <mcrha redhat com>
Date:   Tue Mar 28 18:41:45 2017 +0200

    Fill EWebDAVSession with some functions and add more helper bits

 po/POTFILES.in                                  |    1 +
 src/calendar/backends/http/e-cal-backend-http.c |    8 +-
 src/libedataserver/CMakeLists.txt               |    6 +-
 src/libedataserver/e-soup-session.c             |  158 ++++-
 src/libedataserver/e-soup-session.h             |   16 +-
 src/libedataserver/e-webdav-session.c           |  960 +++++++++++++++++++++++
 src/libedataserver/e-webdav-session.h           |  211 +++++
 src/libedataserver/e-xml-document.c             |  601 ++++++++++++++
 src/libedataserver/e-xml-document.h             |  115 +++
 src/libedataserver/e-xml-utils.c                |  265 +++++++
 src/libedataserver/e-xml-utils.h                |   24 +-
 src/libedataserver/libedataserver.h             |    1 +
 12 files changed, 2338 insertions(+), 28 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5826587..518ba0f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -198,6 +198,7 @@ src/libedataserver/e-source-registry.c
 src/libedataserver/e-source-webdav.c
 src/libedataserver/e-time-utils.c
 src/libedataserver/e-webdav-discover.c
+src/libedataserver/e-webdav-session.c
 src/libedataserverui/e-credentials-prompter.c
 src/libedataserverui/e-credentials-prompter-impl-google.c
 src/libedataserverui/e-credentials-prompter-impl-password.c
diff --git a/src/calendar/backends/http/e-cal-backend-http.c b/src/calendar/backends/http/e-cal-backend-http.c
index 3e85504..137edfe 100644
--- a/src/calendar/backends/http/e-cal-backend-http.c
+++ b/src/calendar/backends/http/e-cal-backend-http.c
@@ -134,19 +134,15 @@ ecb_http_connect_sync (ECalMetaBackend *meta_backend,
 
        e_soup_session_set_credentials (cbhttp->priv->session, credentials);
 
-       request = soup_session_request_http (SOUP_SESSION (cbhttp->priv->session), SOUP_METHOD_GET, uri, 
&local_error);
+       request = e_soup_session_new_request (cbhttp->priv->session, SOUP_METHOD_GET, uri, &local_error);
        success = request != NULL;
 
        if (success) {
                SoupMessage *message;
 
                message = soup_request_http_get_message (request);
-               if (message) {
-                       soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" 
VERSION);
-                       soup_message_headers_append (message->request_headers, "Connection", "close");
-               }
 
-               input_stream = e_soup_session_send_request_sync (cbhttp->priv->session, SOUP_REQUEST 
(request), cancellable, &local_error);
+               input_stream = e_soup_session_send_request_sync (cbhttp->priv->session, request, cancellable, 
&local_error);
 
                success = input_stream != NULL;
 
diff --git a/src/libedataserver/CMakeLists.txt b/src/libedataserver/CMakeLists.txt
index 33a0eb4..fc523ea 100644
--- a/src/libedataserver/CMakeLists.txt
+++ b/src/libedataserver/CMakeLists.txt
@@ -120,8 +120,9 @@ set(SOURCES
        e-webdav-discover.c
        e-webdav-session.c
        e-data-server-util.c
-       e-xml-utils.c
+       e-xml-document.c
        e-xml-hash-utils.c
+       e-xml-utils.c
        libedataserver-private.h
        eds-version.c
        ${CMAKE_CURRENT_BINARY_DIR}/e-source-enumtypes.c
@@ -201,8 +202,9 @@ set(HEADERS
        e-webdav-discover.h
        e-webdav-session.h
        e-data-server-util.h
-       e-xml-utils.h
+       e-xml-document.h
        e-xml-hash-utils.h
+       e-xml-utils.h
        ${CMAKE_CURRENT_BINARY_DIR}/e-source-enumtypes.h
        ${CMAKE_CURRENT_BINARY_DIR}/eds-version.h
 )
diff --git a/src/libedataserver/e-soup-session.c b/src/libedataserver/e-soup-session.c
index 69edeeb..56cc89f 100644
--- a/src/libedataserver/e-soup-session.c
+++ b/src/libedataserver/e-soup-session.c
@@ -26,6 +26,8 @@
 
 #include "evolution-data-server-config.h"
 
+#include <stdio.h>
+
 #include "e-soup-ssl-trust.h"
 #include "e-source-authentication.h"
 #include "e-source-webdav.h"
@@ -40,6 +42,8 @@ struct _ESoupSessionPrivate {
        gboolean ssl_info_set;
        gchar *ssl_certificate_pem;
        GTlsCertificateFlags ssl_certificate_errors;
+
+       SoupLoggerLogLevel log_level;
 };
 
 enum {
@@ -221,6 +225,7 @@ e_soup_session_init (ESoupSession *session)
 {
        session->priv = G_TYPE_INSTANCE_GET_PRIVATE (session, E_TYPE_SOUP_SESSION, ESoupSessionPrivate);
        session->priv->ssl_info_set = FALSE;
+       session->priv->log_level = SOUP_LOGGER_LOG_NONE;
 
        g_mutex_init (&session->priv->property_lock);
 
@@ -273,6 +278,8 @@ e_soup_session_new (ESource *source)
  * "1" - the same as "all".
  * Any other value, including %NULL, disables logging.
  *
+ * Use e_soup_session_get_log_level() to get current log level.
+ *
  * Since: 3.26
  **/
 void
@@ -280,11 +287,11 @@ e_soup_session_setup_logging (ESoupSession *session,
                              const gchar *logging_level)
 {
        SoupLogger *logger;
-       SoupLoggerLogLevel level;
 
        g_return_if_fail (E_IS_SOUP_SESSION (session));
 
        soup_session_remove_feature_by_type (SOUP_SESSION (session), SOUP_TYPE_LOGGER);
+       session->priv->log_level = SOUP_LOGGER_LOG_NONE;
 
        if (!logging_level)
                return;
@@ -292,20 +299,36 @@ e_soup_session_setup_logging (ESoupSession *session,
        if (g_ascii_strcasecmp (logging_level, "all") == 0 ||
            g_ascii_strcasecmp (logging_level, "body") == 0 ||
            g_ascii_strcasecmp (logging_level, "1") == 0)
-               level = SOUP_LOGGER_LOG_BODY;
+               session->priv->log_level = SOUP_LOGGER_LOG_BODY;
        else if (g_ascii_strcasecmp (logging_level, "headers") == 0)
-               level = SOUP_LOGGER_LOG_HEADERS;
+               session->priv->log_level = SOUP_LOGGER_LOG_HEADERS;
        else if (g_ascii_strcasecmp (logging_level, "min") == 0)
-               level = SOUP_LOGGER_LOG_MINIMAL;
+               session->priv->log_level = SOUP_LOGGER_LOG_MINIMAL;
        else
                return;
 
-       logger = soup_logger_new (level, 10 * 1024 * 1024);
+       logger = soup_logger_new (session->priv->log_level, -1);
        soup_session_add_feature (SOUP_SESSION (session), SOUP_SESSION_FEATURE (logger));
        g_object_unref (logger);
 }
 
 /**
+ * e_soup_session_get_log_level:
+ * @session: an #ESoupSession
+ *
+ * Returns: Current log level, as #SoupLoggerLogLevel
+ *
+ * Since: 3.26
+ **/
+SoupLoggerLogLevel
+e_soup_session_get_log_level (ESoupSession *session)
+{
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), SOUP_LOGGER_LOG_NONE);
+
+       return session->priv->log_level;
+}
+
+/**
  * e_soup_session_get_source:
  * @session: an #ESoupSession
  *
@@ -423,10 +446,103 @@ e_soup_session_get_ssl_error_details (ESoupSession *session,
        return TRUE;
 }
 
+static void
+e_soup_session_preset_request (SoupRequestHTTP *request)
+{
+       SoupMessage *message;
+
+       if (!request)
+               return;
+
+       message = soup_request_http_get_message (request);
+       if (message) {
+               soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
+               soup_message_headers_append (message->request_headers, "Connection", "close");
+
+               /* Disable caching for proxies (RFC 4918, section 10.4.5) */
+               soup_message_headers_append (message->request_headers, "Cache-Control", "no-cache");
+               soup_message_headers_append (message->request_headers, "Pragma", "no-cache");
+
+               g_clear_object (&message);
+       }
+}
+
+/**
+ * e_soup_session_new_request:
+ * @session: an #ESoupSession
+ * @method: an HTTP method
+ * @uri_string: a URI string to use for the request
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #SoupRequestHTTP, similar to soup_session_request_http(),
+ * but also presets request headers with "User-Agent" to be "Evolution/version"
+ * and with "Connection" to be "close".
+ *
+ * See also e_soup_session_new_request_uri().
+ *
+ * Returns: (transfer full): a new #SoupRequestHTTP, or %NULL on error
+ *
+ * Since: 3.26
+ **/
+SoupRequestHTTP *
+e_soup_session_new_request (ESoupSession *session,
+                           const gchar *method,
+                           const gchar *uri_string,
+                           GError **error)
+{
+       SoupRequestHTTP *request;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+       request = soup_session_request_http (SOUP_SESSION (session), method, uri_string, error);
+       if (!request)
+               return NULL;
+
+       e_soup_session_preset_request (request);
+
+       return request;
+}
+
+/**
+ * e_soup_session_new_request:
+ * @session: an #ESoupSession
+ * @method: an HTTP method
+ * @uri: a #SoupURI to use for the request
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #SoupRequestHTTP, similar to soup_session_request_http_uri(),
+ * but also presets request headers with "User-Agent" to be "Evolution/version"
+ * and with "Connection" to be "close".
+ *
+ * See also e_soup_session_new_request().
+ *
+ * Returns: (transfer full): a new #SoupRequestHTTP, or %NULL on error
+ *
+ * Since: 3.26
+ **/
+SoupRequestHTTP *
+e_soup_session_new_request_uri (ESoupSession *session,
+                               const gchar *method,
+                               SoupURI *uri,
+                               GError **error)
+{
+       SoupRequestHTTP *request;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+       request = soup_session_request_http_uri (SOUP_SESSION (session), method, uri, error);
+       if (!request)
+               return NULL;
+
+       e_soup_session_preset_request (request);
+
+       return request;
+}
+
 /**
  * e_soup_session_send_request_sync:
  * @session: an #ESoupSession
- * @request: a #SoupRequest to send
+ * @request: a #SoupRequestHTTP to send
  * @cancellable: optional #GCancellable object, or %NULL
  * @error: return location for a #GError, or %NULL
  *
@@ -442,6 +558,9 @@ e_soup_session_get_ssl_error_details (ESoupSession *session,
  * Use e_soup_session_send_request_simple_sync() to read whole
  * content into a #GByteArray.
  *
+ * Note that SoupSession doesn't log content read from GInputStream,
+ * thus the caller may print the read content on its own when needed.
+ *
  * Returns: (transfer full): A newly allocated #GInputStream,
  *    that can be used to read from the URI pointed to by @request.
  *    Free it with g_object_unref(), when no longer needed.
@@ -450,7 +569,7 @@ e_soup_session_get_ssl_error_details (ESoupSession *session,
  **/
 GInputStream *
 e_soup_session_send_request_sync (ESoupSession *session,
-                                 SoupRequest *request,
+                                 SoupRequestHTTP *request,
                                  GCancellable *cancellable,
                                  GError **error)
 {
@@ -458,7 +577,7 @@ e_soup_session_send_request_sync (ESoupSession *session,
        GError *local_error = NULL;
 
        g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
-       g_return_val_if_fail (SOUP_IS_REQUEST (request), NULL);
+       g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
 
        g_mutex_lock (&session->priv->property_lock);
        g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
@@ -467,18 +586,17 @@ e_soup_session_send_request_sync (ESoupSession *session,
        g_mutex_unlock (&session->priv->property_lock);
 
        if (session->priv->source &&
-           e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND) &&
-           SOUP_IS_REQUEST_HTTP (request)) {
+           e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
                SoupMessage *message;
 
-               message = soup_request_http_get_message (SOUP_REQUEST_HTTP (request));
+               message = soup_request_http_get_message (request);
 
                e_soup_ssl_trust_connect (message, session->priv->source);
 
                g_clear_object (&message);
        }
 
-       input_stream = soup_request_send (request, cancellable, &local_error);
+       input_stream = soup_request_send (SOUP_REQUEST (request), cancellable, &local_error);
        if (input_stream)
                return input_stream;
 
@@ -486,7 +604,7 @@ e_soup_session_send_request_sync (ESoupSession *session,
                GTlsCertificate *certificate = NULL;
                SoupMessage *message;
 
-               message = soup_request_http_get_message (SOUP_REQUEST_HTTP (request));
+               message = soup_request_http_get_message (request);
 
                g_mutex_lock (&session->priv->property_lock);
 
@@ -515,7 +633,7 @@ e_soup_session_send_request_sync (ESoupSession *session,
 /**
  * e_soup_session_send_request_simple_sync:
  * @session: an #ESoupSession
- * @request: a #SoupRequest to send
+ * @request: a #SoupRequestHTTP to send
  * @cancellable: optional #GCancellable object, or %NULL
  * @error: return location for a #GError, or %NULL
  *
@@ -524,6 +642,9 @@ e_soup_session_send_request_sync (ESoupSession *session,
  * Use e_soup_session_send_request_sync() when you want to have
  * more control on the content read.
  *
+ * The function prints read content to stdout when
+ * e_soup_session_get_log_level() returns #SOUP_LOGGER_LOG_BODY.
+ *
  * Returns: (transfer full): A newly allocated #GByteArray,
  *    which contains whole content from the URI pointed to by @request.
  *
@@ -531,7 +652,7 @@ e_soup_session_send_request_sync (ESoupSession *session,
  **/
 GByteArray *
 e_soup_session_send_request_simple_sync (ESoupSession *session,
-                                        SoupRequest *request,
+                                        SoupRequestHTTP *request,
                                         GCancellable *cancellable,
                                         GError **error)
 {
@@ -543,13 +664,13 @@ e_soup_session_send_request_simple_sync (ESoupSession *session,
        gboolean success = FALSE;
 
        g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
-       g_return_val_if_fail (SOUP_IS_REQUEST (request), NULL);
+       g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
 
        input_stream = e_soup_session_send_request_sync (session, request, cancellable, error);
        if (!input_stream)
                return NULL;
 
-       expected_length = soup_request_get_content_length (request);
+       expected_length = soup_request_get_content_length (SOUP_REQUEST (request));
        if (expected_length > 0)
                bytes = g_byte_array_sized_new (expected_length);
        else
@@ -568,6 +689,9 @@ e_soup_session_send_request_simple_sync (ESoupSession *session,
        if (!success) {
                g_byte_array_free (bytes, TRUE);
                bytes = NULL;
+       } else if (e_soup_session_get_log_level (session) == SOUP_LOGGER_LOG_BODY) {
+               fwrite (bytes->data, 1, bytes->len, stdout);
+               fflush (stdout);
        }
 
        return bytes;
diff --git a/src/libedataserver/e-soup-session.h b/src/libedataserver/e-soup-session.h
index 09784e8..d9e4763 100644
--- a/src/libedataserver/e-soup-session.h
+++ b/src/libedataserver/e-soup-session.h
@@ -79,6 +79,8 @@ GType         e_soup_session_get_type                 (void) G_GNUC_CONST;
 ESoupSession * e_soup_session_new                      (ESource *source);
 void           e_soup_session_setup_logging            (ESoupSession *session,
                                                         const gchar *logging_level);
+SoupLoggerLogLevel
+               e_soup_session_get_log_level            (ESoupSession *session);
 ESource *      e_soup_session_get_source               (ESoupSession *session);
 void           e_soup_session_set_credentials          (ESoupSession *session,
                                                         const ENamedParameters *credentials);
@@ -87,12 +89,22 @@ ENamedParameters *
 gboolean       e_soup_session_get_ssl_error_details    (ESoupSession *session,
                                                         gchar **out_certificate_pem,
                                                         GTlsCertificateFlags *out_certificate_errors);
+SoupRequestHTTP *
+               e_soup_session_new_request              (ESoupSession *session,
+                                                        const gchar *method,
+                                                        const gchar *uri_string,
+                                                        GError **error);
+SoupRequestHTTP *
+               e_soup_session_new_request_uri          (ESoupSession *session,
+                                                        const gchar *method,
+                                                        SoupURI *uri,
+                                                        GError **error);
 GInputStream * e_soup_session_send_request_sync        (ESoupSession *session,
-                                                        SoupRequest *request,
+                                                        SoupRequestHTTP *request,
                                                         GCancellable *cancellable,
                                                         GError **error);
 GByteArray *   e_soup_session_send_request_simple_sync (ESoupSession *session,
-                                                        SoupRequest *request,
+                                                        SoupRequestHTTP *request,
                                                         GCancellable *cancellable,
                                                         GError **error);
 
diff --git a/src/libedataserver/e-webdav-session.c b/src/libedataserver/e-webdav-session.c
index 246c36a..221f2b9 100644
--- a/src/libedataserver/e-webdav-session.c
+++ b/src/libedataserver/e-webdav-session.c
@@ -27,7 +27,12 @@
 
 #include "evolution-data-server-config.h"
 
+#include <glib/gi18n-lib.h>
+
+#include "camel/camel.h"
+
 #include "e-source-webdav.h"
+#include "e-xml-utils.h"
 
 #include "e-webdav-session.h"
 
@@ -37,6 +42,114 @@ struct _EWebDAVSessionPrivate {
 
 G_DEFINE_TYPE (EWebDAVSession, e_webdav_session, E_TYPE_SOUP_SESSION)
 
+G_DEFINE_BOXED_TYPE (EWebDAVResource, e_webdav_resource, e_webdav_resource_copy, e_webdav_resource_free)
+
+/**
+ * e_webdav_resource_new:
+ * @kind: an #EWebDAVResourceKind of the resource
+ * @supports: bit-or of #EWebDAVResourceSupports values
+ * @href: href of the resource
+ * @etag: (nullable): optional ETag of the resource, or %NULL
+ * @display_name: (nullable): optional display name of the resource, or %NULL
+ * @description: (nullable): optional description of the resource, or %NULL
+ * @color: (nullable): optional color of the resource, or %NULL
+ *
+ * Some values of the resource are not always valid, depending on the @kind,
+ * but also whether server stores such values and whether it had been asked
+ * for them to be fetched.
+ *
+ * The @etag for %E_WEBDAV_RESOURCE_KIND_COLLECTION can be a change tag instead.
+ *
+ * Returns: (transfer full): A newly created #EWebDAVResource, prefilled with
+ *    given values. Free it with e_webdav_resource_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVResource *
+e_webdav_resource_new (EWebDAVResourceKind kind,
+                      guint32 supports,
+                      const gchar *href,
+                      const gchar *etag,
+                      const gchar *display_name,
+                      const gchar *content_type,
+                      gsize content_length,
+                      glong creation_date,
+                      glong last_modified,
+                      const gchar *description,
+                      const gchar *color)
+{
+       EWebDAVResource *resource;
+
+       resource = g_new0 (EWebDAVResource, 1);
+       resource->kind = kind;
+       resource->supports = supports;
+       resource->href = g_strdup (href);
+       resource->etag = g_strdup (etag);
+       resource->display_name = g_strdup (display_name);
+       resource->content_type = g_strdup (content_type);
+       resource->content_length = content_length;
+       resource->creation_date = creation_date;
+       resource->last_modified = last_modified;
+       resource->description = g_strdup (description);
+       resource->color = g_strdup (color);
+
+       return resource;
+}
+
+/**
+ * e_webdav_resource_copy:
+ * @resource: (nullable): an #EWebDAVResource to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVResource prefilled with
+ *    the same values as @resource, or %NULL, when @resource is %NULL.
+ *    Free it with e_webdav_resource_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVResource *
+e_webdav_resource_copy (const EWebDAVResource *resource)
+{
+       if (!resource)
+               return NULL;
+
+       return e_webdav_resource_new (resource->kind,
+               resource->supports,
+               resource->href,
+               resource->etag,
+               resource->display_name,
+               resource->content_type,
+               resource->content_length,
+               resource->creation_date,
+               resource->last_modified,
+               resource->description,
+               resource->color);
+}
+
+/**
+ * e_webdav_resource_free:
+ * @ptr: (nullable): an #EWebDAVResource
+ *
+ * Frees an #EWebDAVResource previously created with e_webdav_resource_new()
+ * or e_webdav_resource_copy(). The function does nothign if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_resource_free (gpointer ptr)
+{
+       EWebDAVResource *resource = ptr;
+
+       if (resource) {
+               g_free (resource->href);
+               g_free (resource->etag);
+               g_free (resource->display_name);
+               g_free (resource->content_type);
+               g_free (resource->description);
+               g_free (resource->color);
+               g_free (resource);
+       }
+}
+
 static void
 e_webdav_session_class_init (EWebDAVSessionClass *klass)
 {
@@ -72,3 +185,850 @@ e_webdav_session_new (ESource *source)
                "source", source,
                NULL);
 }
+
+static SoupRequestHTTP *
+e_webdav_session_new_request (EWebDAVSession *webdav,
+                             const gchar *method,
+                             const gchar *uri,
+                             GError **error)
+{
+       ESoupSession *session;
+       SoupRequestHTTP *request;
+       SoupURI *soup_uri;
+       ESource *source;
+       ESourceWebdav *webdav_extension;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), NULL);
+
+       session = E_SOUP_SESSION (webdav);
+       if (uri && *uri)
+               return e_soup_session_new_request (session, method, uri, error);
+
+       source = e_soup_session_get_source (session);
+       g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+
+       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, NULL);
+
+       request = e_soup_session_new_request_uri (session, method, soup_uri, error);
+
+       soup_uri_free (soup_uri);
+
+       return request;
+}
+
+static GHashTable *
+e_webdav_session_comma_header_to_hashtable (SoupMessageHeaders *headers,
+                                           const gchar *header_name)
+{
+       GHashTable *soup_params, *result;
+       GHashTableIter iter;
+       const gchar *value;
+       gpointer key;
+
+       g_return_val_if_fail (header_name != NULL, NULL);
+
+       if (!headers)
+               return NULL;
+
+       value = soup_message_headers_get_list (headers, header_name);
+       if (!value)
+               return NULL;
+
+       soup_params = soup_header_parse_param_list (value);
+       if (!soup_params)
+               return NULL;
+
+       result = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL);
+
+       g_hash_table_iter_init (&iter, soup_params);
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               value = key;
+
+               if (value && *value)
+                       g_hash_table_insert (result, g_strdup (value), GINT_TO_POINTER (1));
+       }
+
+       soup_header_free_param_list (soup_params);
+
+       return result;
+}
+
+/**
+ * e_webdav_session_options_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_capabilities: (out) (transfer full): return location for DAV capabilities
+ * @out_allows: (out) (transfer full): return location for allowed operations
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues OPTIONS request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * The @out_capabilities contains a set of returned capabilities. Some known are
+ * defined as E_WEBDAV_CAPABILITY_CLASS_1, and so on. The 'value' of the #GHashTable
+ * doesn't have any particular meaning and the strings are compared case insensitively.
+ * Free the hash table with g_hash_table_destroy(), when no longer needed. The returned
+ * value can be %NULL on success, it's when the server doesn't provide the information.
+ *
+ * The @out_allows contains a set of allowed methods returned by the server. Some known
+ * are defined as E_WEBDAV_ALLOW_OPTIONS, and so on. The 'value' of the #GHashTable
+ * doesn't have any particular meaning and the strings are compared case insensitively.
+ * Free the hash table with g_hash_table_destroy(), when no longer needed. The returned
+ * value can be %NULL on success, it's when the server doesn't provide the information.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_options_sync (EWebDAVSession *webdav,
+                              const gchar *uri,
+                              GHashTable **out_capabilities,
+                              GHashTable **out_allows,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (out_capabilities != NULL, FALSE);
+       g_return_val_if_fail (out_allows != NULL, FALSE);
+
+       *out_capabilities = NULL;
+       *out_allows = NULL;
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_OPTIONS, uri, error);
+       if (!request)
+               return FALSE;
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       if (!bytes) {
+               g_object_unref (request);
+               return FALSE;
+       }
+
+       message = soup_request_http_get_message (request);
+
+       g_byte_array_free (bytes, TRUE);
+       g_object_unref (request);
+
+       g_return_val_if_fail (message != NULL, FALSE);
+
+       *out_capabilities = e_webdav_session_comma_header_to_hashtable (message->response_headers, "DAV");
+       *out_allows = e_webdav_session_comma_header_to_hashtable (message->response_headers, "Allow");
+
+       g_object_unref (message);
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_propfind_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_0, %E_WEBDAV_DEPTH_1 or %E_WEBDAV_DEPTH_INFINITY
+ * @xml: (nullable): the request itself, as an #EXmlDocument, the root element should be DAV:propfind, or 
%NULL
+ * @func: an #EWebDAVPropfindFunc function to call for each DAV:propstat in the multistatus response
+ * @func_user_data: user data passed to @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues PROPFIND request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource. On success, calls @func for each returned
+ * DAV:propstat. The provided XPath context has registered %E_WEBDAV_NS_DAV namespace
+ * with prefix "D". It doesn't have any other namespace registered.
+ *
+ * The @func is called always at least once, with %NULL xpath_prop_prefix, which
+ * is meant to let the caller setup the xpath_ctx, like to register its own namespaces
+ * to it with e_xml_xpath_context_register_namespaces(). All other invocations of @func
+ * will have xpath_prop_prefix non-%NULL.
+ *
+ * The @xml can be %NULL, in which case the server should behave like DAV:allprop request.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_propfind_sync (EWebDAVSession *webdav,
+                               const gchar *uri,
+                               const gchar *depth,
+                               const EXmlDocument *xml,
+                               EWebDAVPropfindFunc func,
+                               gpointer func_user_data,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (depth != NULL, FALSE);
+       if (xml)
+               g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_PROPFIND, uri, error);
+       if (!request)
+               return FALSE;
+
+       message = soup_request_http_get_message (request);
+       if (!message) {
+               g_warn_if_fail (message != NULL);
+               g_object_unref (request);
+
+               return FALSE;
+       }
+
+       soup_message_headers_append (message->request_headers, "Depth", depth);
+
+       if (xml) {
+               gchar *content;
+               gsize content_length;
+
+               content = e_xml_document_get_content (xml, &content_length);
+               if (!content) {
+                       g_object_unref (message);
+                       g_object_unref (request);
+
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get 
input XML content"));
+
+                       return FALSE;
+               }
+
+               soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+                       SOUP_MEMORY_TAKE, content, content_length);
+       }
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       g_object_unref (request);
+
+       success = bytes != NULL;
+
+       if (success && message->status_code != SOUP_STATUS_MULTI_STATUS) {
+               success = FALSE;
+
+               g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+                       _("Expected multistatus response, but %d returned (%s)"), message->status_code,
+                       message->reason_phrase && *message->reason_phrase ? message->reason_phrase :
+                       (soup_status_get_phrase (message->status_code) ? soup_status_get_phrase 
(message->status_code) : _("Unknown error")));
+       }
+
+       if (success) {
+               const gchar *content_type;
+
+               content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+               success = content_type &&
+                       (g_ascii_strcasecmp (content_type, "application/xml") == 0 ||
+                        g_ascii_strcasecmp (content_type, "text/xml") == 0);
+
+               if (!success) {
+                       if (!content_type) {
+                               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                                       _("Expected application/xml response, but none returned"));
+                       } else {
+                               g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                                       _("Expected application/xml response, but %s returned"), 
content_type);
+                       }
+               }
+       }
+
+       if (success) {
+               xmlDocPtr doc = e_xml_parse_data ((const gchar *) bytes->data, bytes->len);
+
+               g_byte_array_free (bytes, TRUE);
+               bytes = NULL;
+
+               if (doc) {
+                       xmlXPathContextPtr xpath_ctx;
+                       SoupURI *request_uri;
+
+                       request_uri = soup_message_get_uri (message);
+
+                       xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+                               "D", E_WEBDAV_NS_DAV,
+                               NULL);
+
+                       if (xpath_ctx &&
+                           func (webdav, xpath_ctx, NULL, request_uri, SOUP_STATUS_NONE, func_user_data)) {
+                               xmlXPathObjectPtr xpath_obj_response;
+
+                               xpath_obj_response = e_xml_xpath_eval (xpath_ctx, 
"/D:multistatus/D:response");
+
+                               if (xpath_obj_response != NULL) {
+                                       gboolean do_stop = FALSE;
+                                       gint response_index, response_length;
+
+                                       response_length = xmlXPathNodeSetGetLength 
(xpath_obj_response->nodesetval);
+
+                                       for (response_index = 0; response_index < response_length && 
!do_stop; response_index++) {
+                                               xmlXPathObjectPtr xpath_obj_propstat;
+
+                                               xpath_obj_propstat = e_xml_xpath_eval (xpath_ctx,
+                                                       "/D:multistatus/D:response[%d]/D:propstat",
+                                                       response_index + 1);
+
+                                               if (xpath_obj_propstat != NULL) {
+                                                       gint propstat_index, propstat_length;
+
+                                                       propstat_length = xmlXPathNodeSetGetLength 
(xpath_obj_propstat->nodesetval);
+
+                                                       for (propstat_index = 0; propstat_index < 
propstat_length && !do_stop; propstat_index++) {
+                                                               gchar *status, *propstat_prefix;
+                                                               guint status_code;
+
+                                                               propstat_prefix = g_strdup_printf 
("/D:multistatus/D:response[%d]/D:propstat[%d]/D:prop",
+                                                                       response_index + 1, propstat_index + 
1);
+
+                                                               status = e_xml_xpath_eval_as_string 
(xpath_ctx, "%s/../D:status", propstat_prefix);
+                                                               if (!status || 
!soup_headers_parse_status_line (status, NULL, &status_code, NULL))
+                                                                       status_code = 0;
+                                                               g_free (status);
+
+                                                               do_stop = !func (webdav, xpath_ctx, 
propstat_prefix, request_uri, status_code, func_user_data);
+
+                                                               g_free (propstat_prefix);
+                                                       }
+
+                                                       xmlXPathFreeObject (xpath_obj_propstat);
+                                               }
+                                       }
+
+                                       xmlXPathFreeObject (xpath_obj_response);
+                               }
+                       }
+
+                       if (xpath_ctx)
+                               xmlXPathFreeContext (xpath_ctx);
+                       xmlFreeDoc (doc);
+               } else {
+                       g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                               _("Failed to parse response as XML"));
+
+                       success = FALSE;
+               }
+       }
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+
+       return success;
+}
+
+/* This assumes ownership of 'text' */
+static gchar *
+e_webdav_session_maybe_dequote (gchar *text)
+{
+       gchar *dequoted;
+       gint len;
+
+       if (!text || *text != '\"')
+               return text;
+
+       len = strlen (text);
+
+       if (len < 2 || text[len - 1] != '\"')
+               return text;
+
+       dequoted = g_strndup (text + 1, len - 2);
+       g_free (text);
+
+       return dequoted;
+}
+
+static gboolean
+e_webdav_session_getctag_cb (EWebDAVSession *webdav,
+                            xmlXPathContextPtr xpath_ctx,
+                            const gchar *xpath_prop_prefix,
+                            const SoupURI *request_uri,
+                            guint status_code,
+                            gpointer user_data)
+{
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx,
+                       "CS", E_WEBDAV_NS_CALENDARSERVER,
+                       NULL);
+
+               return TRUE;
+       }
+
+       if (status_code == SOUP_STATUS_OK) {
+               gchar **out_ctag = user_data;
+               gchar *ctag;
+
+               g_return_val_if_fail (out_ctag != NULL, FALSE);
+
+               ctag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/CS:getctag", xpath_prop_prefix);
+
+               if (ctag && *ctag) {
+                       *out_ctag = e_webdav_session_maybe_dequote (ctag);
+               } else {
+                       g_free (ctag);
+               }
+       }
+
+       return FALSE;
+}
+
+/**
+ * e_webdav_session_getctag_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_ctag: (out) (transfer full): return location for the ctag
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues a getctag property request for a collection identified by @uri, or
+ * by the #ESource resource reference. The ctag is a collection tag, which
+ * changes whenever the collection changes (similar to etag). The getctag is
+ * an extension, thus the function can fail when the server doesn't support it.
+ *
+ * Free the returned @out_ctag with g_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_getctag_sync (EWebDAVSession *webdav,
+                              const gchar *uri,
+                              gchar **out_ctag,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       EXmlDocument *xml;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (out_ctag != NULL, FALSE);
+
+       *out_ctag = NULL;
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       e_xml_document_add_namespaces (xml, "CS", E_WEBDAV_NS_CALENDARSERVER, NULL);
+
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALENDARSERVER, "getctag");
+       e_xml_document_end_element (xml); /* getctag */
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_0, xml,
+               e_webdav_session_getctag_cb, out_ctag, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success && *out_ctag != NULL;
+}
+
+static EWebDAVResourceKind
+e_webdav_session_extract_kind (xmlXPathContextPtr xpath_ctx,
+                              const gchar *xpath_prop_prefix)
+{
+       g_return_val_if_fail (xpath_ctx != NULL, E_WEBDAV_RESOURCE_KIND_UNKNOWN);
+       g_return_val_if_fail (xpath_prop_prefix != NULL, E_WEBDAV_RESOURCE_KIND_UNKNOWN);
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/A:addressbook", xpath_prop_prefix))
+               return E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/C:calendar", xpath_prop_prefix))
+               return E_WEBDAV_RESOURCE_KIND_CALENDAR;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/D:principal", xpath_prop_prefix))
+               return E_WEBDAV_RESOURCE_KIND_PRINCIPAL;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/D:collection", xpath_prop_prefix))
+               return E_WEBDAV_RESOURCE_KIND_COLLECTION;
+
+       return E_WEBDAV_RESOURCE_KIND_RESOURCE;
+}
+
+static guint32
+e_webdav_session_extract_supports (xmlXPathContextPtr xpath_ctx,
+                                  const gchar *xpath_prop_prefix)
+{
+       guint32 supports = E_WEBDAV_RESOURCE_SUPPORTS_NONE;
+
+       g_return_val_if_fail (xpath_ctx != NULL, E_WEBDAV_RESOURCE_SUPPORTS_NONE);
+       g_return_val_if_fail (xpath_prop_prefix != NULL, E_WEBDAV_RESOURCE_SUPPORTS_NONE);
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/A:addressbook", xpath_prop_prefix))
+               supports = supports | E_WEBDAV_RESOURCE_SUPPORTS_CONTACTS;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/C:supported-calendar-component-set", xpath_prop_prefix)) {
+               xmlXPathObjectPtr xpath_obj;
+
+               xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/C:supported-calendar-component-set/C:comp", 
xpath_prop_prefix);
+               if (xpath_obj) {
+                       gint ii, length;
+
+                       length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+                       for (ii = 0; ii < length; ii++) {
+                               gchar *name;
+
+                               name = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/C:supported-calendar-component-set/C:comp[%d]/@name",
+                                       xpath_prop_prefix, ii + 1);
+
+                               if (!name)
+                                       continue;
+
+                               if (g_ascii_strcasecmp (name, "VEVENT") == 0)
+                                       supports |= E_WEBDAV_RESOURCE_SUPPORTS_EVENTS;
+                               else if (g_ascii_strcasecmp (name, "VJOURNAL") == 0)
+                                       supports |= E_WEBDAV_RESOURCE_SUPPORTS_MEMOS;
+                               else if (g_ascii_strcasecmp (name, "VTODO") == 0)
+                                       supports |= E_WEBDAV_RESOURCE_SUPPORTS_TASKS;
+
+                               g_free (name);
+                       }
+
+                       xmlXPathFreeObject (xpath_obj);
+               } else {
+                       /* If the property is not present, assume all component
+                        * types are supported.  (RFC 4791, Section 5.2.3) */
+                       supports = supports |
+                               E_WEBDAV_RESOURCE_SUPPORTS_EVENTS |
+                               E_WEBDAV_RESOURCE_SUPPORTS_MEMOS |
+                               E_WEBDAV_RESOURCE_SUPPORTS_TASKS;
+               }
+       }
+
+       return supports;
+}
+
+static gchar *
+e_webdav_session_extract_nonempty (xmlXPathContextPtr xpath_ctx,
+                                  const gchar *xpath_prop_prefix,
+                                  const gchar *prop,
+                                  const gchar *alternative_prop)
+{
+       gchar *value;
+
+       g_return_val_if_fail (xpath_ctx != NULL, NULL);
+       g_return_val_if_fail (xpath_prop_prefix != NULL, NULL);
+       g_return_val_if_fail (prop != NULL, NULL);
+
+       value = e_xml_xpath_eval_as_string (xpath_ctx, "%s/%s", xpath_prop_prefix, prop);
+       if (!value && alternative_prop)
+               value = e_xml_xpath_eval_as_string (xpath_ctx, "%s/%s", xpath_prop_prefix, alternative_prop);
+       if (!value)
+               return NULL;
+
+       if (!*value) {
+               g_free (value);
+               return NULL;
+       }
+
+       return e_webdav_session_maybe_dequote (value);
+}
+
+static gsize
+e_webdav_session_extract_content_length (xmlXPathContextPtr xpath_ctx,
+                                        const gchar *xpath_prop_prefix)
+{
+       gchar *value;
+       gsize length;
+
+       g_return_val_if_fail (xpath_ctx != NULL, -1);
+       g_return_val_if_fail (xpath_prop_prefix != NULL, -1);
+
+       value = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "D:getcontentlength", NULL);
+       if (!value)
+               return -1;
+
+       length = g_ascii_strtoll (value, NULL, 10);
+
+       g_free (value);
+
+       return length;
+}
+
+static glong
+e_webdav_session_extract_datetime (xmlXPathContextPtr xpath_ctx,
+                                  const gchar *xpath_prop_prefix,
+                                  const gchar *prop,
+                                  gboolean is_iso_property)
+{
+       gchar *value;
+       GTimeVal tv;
+
+       g_return_val_if_fail (xpath_ctx != NULL, -1);
+       g_return_val_if_fail (xpath_prop_prefix != NULL, -1);
+
+       value = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, prop, NULL);
+       if (!value)
+               return -1;
+
+       if (is_iso_property && !g_time_val_from_iso8601 (value, &tv)) {
+               tv.tv_sec = -1;
+       } else if (!is_iso_property) {
+               tv.tv_sec = camel_header_decode_date (value, NULL);
+       }
+
+       g_free (value);
+
+       return tv.tv_sec;
+}
+
+static gboolean
+e_webdav_session_list_cb (EWebDAVSession *webdav,
+                         xmlXPathContextPtr xpath_ctx,
+                         const gchar *xpath_prop_prefix,
+                         const SoupURI *request_uri,
+                         guint status_code,
+                         gpointer user_data)
+{
+       GSList **out_resources = user_data;
+
+       g_return_val_if_fail (out_resources != NULL, FALSE);
+       g_return_val_if_fail (request_uri != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx,
+                       "CS", E_WEBDAV_NS_CALENDARSERVER,
+                       "C", E_WEBDAV_NS_CALDAV,
+                       "A", E_WEBDAV_NS_CARDDAV,
+                       "IC", E_WEBDAV_NS_ICAL,
+                       NULL);
+
+               return TRUE;
+       }
+
+       if (status_code == SOUP_STATUS_OK) {
+               EWebDAVResource *resource;
+               EWebDAVResourceKind kind;
+               guint32 supports;
+               gchar *href;
+               gchar *etag;
+               gchar *display_name;
+               gchar *content_type;
+               gsize content_length;
+               glong creation_date;
+               glong last_modified;
+               gchar *description;
+               gchar *color;
+
+               kind = e_webdav_session_extract_kind (xpath_ctx, xpath_prop_prefix);
+               if (kind == E_WEBDAV_RESOURCE_KIND_UNKNOWN)
+                       return TRUE;
+
+               href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/../../D:href", xpath_prop_prefix);
+               if (!href)
+                       return TRUE;
+
+               if (!strstr (href, "://")) {
+                       SoupURI *soup_uri;
+                       gchar *full_uri;
+
+                       soup_uri = soup_uri_copy ((SoupURI *) request_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);
+                       g_free (href);
+
+                       href = full_uri;
+               }
+
+               supports = e_webdav_session_extract_supports (xpath_ctx, xpath_prop_prefix);
+               etag = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "D:getetag", 
"CS:getctag");
+               display_name = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, 
"D:displayname", NULL);
+               content_type = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, 
"D:getcontenttype", NULL);
+               content_length = e_webdav_session_extract_content_length (xpath_ctx, xpath_prop_prefix);
+               creation_date = e_webdav_session_extract_datetime (xpath_ctx, xpath_prop_prefix, 
"D:creationdate", TRUE);
+               last_modified = e_webdav_session_extract_datetime (xpath_ctx, xpath_prop_prefix, 
"D:getlastmodified", FALSE);
+               description = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, 
"C:calendar-description", "A:addressbook-description");
+               color = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "IC:calendar-color", 
NULL);
+
+               resource = e_webdav_resource_new (kind, supports,
+                       NULL, /* href */
+                       NULL, /* etag */
+                       NULL, /* display_name */
+                       NULL, /* content_type */
+                       content_length,
+                       creation_date,
+                       last_modified,
+                       NULL, /* description */
+                       NULL); /* color */
+               resource->href = href;
+               resource->etag = etag;
+               resource->display_name = display_name;
+               resource->content_type = content_type;
+               resource->description = description;
+               resource->color = color;
+
+               *out_resources = g_slist_prepend (*out_resources, resource);
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_list_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @flags: a bit-or of #EWebDAVListFlags, claiming what properties to read
+ * @out_resources: (out) (transfer full) (element-type EWebDAVResource): return location for the resources
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Lists content of the @uri, or the one stored with the associated #ESource,
+ * which should point to a collection. The @flags influences which properties
+ * are read for the resources.
+ *
+ * The @out_resources is in no particular order.
+ *
+ * Free the returned @out_resources with
+ * g_slist_free_full (resources, e_webdav_resource_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_list_sync (EWebDAVSession *webdav,
+                           const gchar *uri,
+                           guint32 flags,
+                           GSList **out_resources,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       EXmlDocument *xml;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (out_resources != NULL, FALSE);
+
+       *out_resources = NULL;
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       e_xml_document_start_element (xml, NULL, "prop");
+
+       e_xml_document_start_element (xml, NULL, "resourcetype");
+       e_xml_document_end_element (xml);
+
+       if ((flags & E_WEBDAV_LIST_SUPPORTS) != 0 ||
+           (flags & E_WEBDAV_LIST_DESCRIPTION) != 0 ||
+           (flags & E_WEBDAV_LIST_COLOR) != 0) {
+               e_xml_document_add_namespaces (xml, "C", E_WEBDAV_NS_CALDAV, NULL);
+       }
+
+       if ((flags & E_WEBDAV_LIST_SUPPORTS) != 0) {
+               e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "supported-calendar-component-set");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_DISPLAY_NAME) != 0) {
+               e_xml_document_start_element (xml, NULL, "displayname");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_ETAG) != 0) {
+               e_xml_document_start_element (xml, NULL, "getetag");
+               e_xml_document_end_element (xml);
+
+               e_xml_document_add_namespaces (xml, "CS", E_WEBDAV_NS_CALENDARSERVER, NULL);
+
+               e_xml_document_start_element (xml, E_WEBDAV_NS_CALENDARSERVER, "getctag");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_CONTENT_TYPE) != 0) {
+               e_xml_document_start_element (xml, NULL, "getcontenttype");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_CONTENT_LENGTH) != 0) {
+               e_xml_document_start_element (xml, NULL, "getcontentlength");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_CREATION_DATE) != 0) {
+               e_xml_document_start_element (xml, NULL, "creationdate");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_LAST_MODIFIED) != 0) {
+               e_xml_document_start_element (xml, NULL, "getlastmodified");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_DESCRIPTION) != 0) {
+               e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "calendar-description");
+               e_xml_document_end_element (xml);
+
+               e_xml_document_add_namespaces (xml, "A", E_WEBDAV_NS_CARDDAV, NULL);
+
+               e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-description");
+               e_xml_document_end_element (xml);
+       }
+
+       if ((flags & E_WEBDAV_LIST_COLOR) != 0) {
+               e_xml_document_add_namespaces (xml, "IC", E_WEBDAV_NS_ICAL, NULL);
+
+               e_xml_document_start_element (xml, E_WEBDAV_NS_ICAL, "calendar-color");
+               e_xml_document_end_element (xml);
+       }
+
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_1, xml,
+               e_webdav_session_list_cb, out_resources, cancellable, error);
+
+       g_object_unref (xml);
+
+       /* Ensure display name in case the resource doesn't have any */
+       if (success && (flags & E_WEBDAV_LIST_DISPLAY_NAME) != 0) {
+               GSList *link;
+
+               for (link = *out_resources; link; link = g_slist_next (link)) {
+                       EWebDAVResource *resource = link->data;
+
+                       if (resource && !resource->display_name && resource->href) {
+                               gchar *href_decoded = soup_uri_decode (resource->href);
+
+                               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 {
+                                                       resource->display_name = g_strdup (cp + 1);
+                                                       break;
+                                               }
+                                       }
+                               }
+
+                               g_free (href_decoded);
+                       }
+               }
+       }
+
+       if (success) {
+               /* Honour order returned by the server, even it's not significant. */
+               *out_resources = g_slist_reverse (*out_resources);
+       }
+
+       return success;
+}
diff --git a/src/libedataserver/e-webdav-session.h b/src/libedataserver/e-webdav-session.h
index 280ab72..219f93b 100644
--- a/src/libedataserver/e-webdav-session.h
+++ b/src/libedataserver/e-webdav-session.h
@@ -23,10 +23,12 @@
 #define E_WEBDAV_SESSION_H
 
 #include <glib.h>
+#include <libxml/xpath.h>
 
 #include <libedataserver/e-data-server-util.h>
 #include <libedataserver/e-soup-session.h>
 #include <libedataserver/e-source.h>
+#include <libedataserver/e-xml-document.h>
 
 /* Standard GObject macros */
 #define E_TYPE_WEBDAV_SESSION \
@@ -49,11 +51,141 @@
 
 G_BEGIN_DECLS
 
+#define E_WEBDAV_CAPABILITY_CLASS_1                    "1"
+#define E_WEBDAV_CAPABILITY_CLASS_2                    "2"
+#define E_WEBDAV_CAPABILITY_CLASS_3                    "3"
+#define E_WEBDAV_CAPABILITY_ACCESS_CONTROL             "access-control"
+#define E_WEBDAV_CAPABILITY_BIND                       "bind"
+#define E_WEBDAV_CAPABILITY_EXTENDED_MKCOL             "extended-mkcol"
+#define E_WEBDAV_CAPABILITY_ADDRESSBOOK                        "addressbook"
+#define E_WEBDAV_CAPABILITY_CALENDAR_ACCESS            "calendar-access"
+#define E_WEBDAV_CAPABILITY_CALENDAR_SCHEDULE          "calendar-schedule"
+#define E_WEBDAV_CAPABILITY_CALENDAR_AUTO_SCHEDULE     "calendar-auto-schedule"
+#define E_WEBDAV_CAPABILITY_CALENDAR_PROXY             "calendar-proxy"
+
+#define E_WEBDAV_ALLOW_OPTIONS         "OPTIONS"
+#define E_WEBDAV_ALLOW_PROPFIND                "PROPFIND"
+#define E_WEBDAV_ALLOW_REPORT          "REPORT"
+#define E_WEBDAV_ALLOW_DELETE          "DELETE"
+#define E_WEBDAV_ALLOW_GET             "GET"
+#define E_WEBDAV_ALLOW_PUT             "PUT"
+#define E_WEBDAV_ALLOW_HEAD            "HEAD"
+#define E_WEBDAV_ALLOW_ACL             "ACL"
+#define E_WEBDAV_ALLOW_LOCK            "LOCK"
+#define E_WEBDAV_ALLOW_UNLOCK          "UNLOCK"
+#define E_WEBDAV_ALLOW_MOVE            "MOVE"
+#define E_WEBDAV_ALLOW_MKTICKET                "MKTICKET"
+#define E_WEBDAV_ALLOW_DELTICKET       "DELTICKET"
+
+#define E_WEBDAV_DEPTH_0               "0"
+#define E_WEBDAV_DEPTH_1               "1"
+#define E_WEBDAV_DEPTH_INFINITY                "infinity"
+
+#define E_WEBDAV_CONTENT_TYPE_XML      "application/xml; charset=\"utf-8\""
+#define E_WEBDAV_CONTENT_TYPE_CALENDAR "text/calendar; charset=\"utf-8\""
+#define E_WEBDAV_CONTENT_TYPE_VCARD    "text/vcard; charset=\"utf-8\""
+
+#define E_WEBDAV_NS_DAV                        "DAV:"
+#define E_WEBDAV_NS_CALDAV             "urn:ietf:params:xml:ns:caldav"
+#define E_WEBDAV_NS_CARDDAV            "urn:ietf:params:xml:ns:carddav"
+#define E_WEBDAV_NS_CALENDARSERVER     "http://calendarserver.org/ns/";
+#define E_WEBDAV_NS_ICAL               "http://apple.com/ns/ical/";
+
+typedef enum {
+       E_WEBDAV_RESOURCE_KIND_UNKNOWN,
+       E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK,
+       E_WEBDAV_RESOURCE_KIND_CALENDAR,
+       E_WEBDAV_RESOURCE_KIND_PRINCIPAL,
+       E_WEBDAV_RESOURCE_KIND_COLLECTION,
+       E_WEBDAV_RESOURCE_KIND_RESOURCE
+} EWebDAVResourceKind;
+
+typedef enum {
+       E_WEBDAV_RESOURCE_SUPPORTS_NONE         = 0,
+       E_WEBDAV_RESOURCE_SUPPORTS_CONTACTS     = 1 << 0,
+       E_WEBDAV_RESOURCE_SUPPORTS_EVENTS       = 1 << 1,
+       E_WEBDAV_RESOURCE_SUPPORTS_MEMOS        = 1 << 2,
+       E_WEBDAV_RESOURCE_SUPPORTS_TASKS        = 1 << 3
+} EWebDAVResourceSupports;
+
+typedef struct _EWebDAVResource {
+       EWebDAVResourceKind kind;
+       guint32 supports;
+       gchar *href;
+       gchar *etag;
+       gchar *display_name;
+       gchar *content_type;
+       gsize content_length;
+       glong creation_date;
+       glong last_modified;
+       gchar *description;
+       gchar *color;
+} EWebDAVResource;
+
+GType          e_webdav_resource_get_type              (void) G_GNUC_CONST;
+EWebDAVResource *
+               e_webdav_resource_new                   (EWebDAVResourceKind kind,
+                                                        guint32 supports,
+                                                        const gchar *href,
+                                                        const gchar *etag,
+                                                        const gchar *display_name,
+                                                        const gchar *content_type,
+                                                        gsize content_length,
+                                                        glong creation_date,
+                                                        glong last_modified,
+                                                        const gchar *description,
+                                                        const gchar *color);
+EWebDAVResource *
+               e_webdav_resource_copy                  (const EWebDAVResource *resource);
+void           e_webdav_resource_free                  (gpointer ptr /* EWebDAVResource * */);
+
+typedef enum {
+       E_WEBDAV_LIST_ALL               = 0xFFFFFFFF,
+       E_WEBDAV_LIST_NONE              = 0,
+       E_WEBDAV_LIST_SUPPORTS          = 1 << 0,
+       E_WEBDAV_LIST_ETAG              = 1 << 1,
+       E_WEBDAV_LIST_DISPLAY_NAME      = 1 << 2,
+       E_WEBDAV_LIST_CONTENT_TYPE      = 1 << 3,
+       E_WEBDAV_LIST_CONTENT_LENGTH    = 1 << 4,
+       E_WEBDAV_LIST_CREATION_DATE     = 1 << 5,
+       E_WEBDAV_LIST_LAST_MODIFIED     = 1 << 6,
+       E_WEBDAV_LIST_DESCRIPTION       = 1 << 7,
+       E_WEBDAV_LIST_COLOR             = 1 << 8
+} EWebDAVListFlags;
+
 typedef struct _EWebDAVSession EWebDAVSession;
 typedef struct _EWebDAVSessionClass EWebDAVSessionClass;
 typedef struct _EWebDAVSessionPrivate EWebDAVSessionPrivate;
 
 /**
+ * EWebDAVPropfindFunc:
+ * @webdav: an #EWebDAVSession
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @xpath_prop_prefix: (nullable): an XPath prefix for the current prop element, without trailing forward 
slash
+ * @request_uri: a #SoupURI, containing the request URI, maybe redirected by the server
+ * @status_code: an HTTP status code for this property
+ * @user_data: user data, as passed to e_webdav_session_propfind_sync()
+ *
+ * A callback function for e_webdav_session_propfind_sync().
+ *
+ * The @xpath_prop_prefix can be %NULL only once, for the first time,
+ * which is meant to let the caller setup the @xpath_ctx, like to register
+ * its own namespaces to it with e_xml_xpath_context_register_namespaces().
+ * All other invocations of the function will have @xpath_prop_prefix non-%NULL.
+ *
+ * Returns: %TRUE to continue traversal of the returned multistatus response,
+ *    %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* EWebDAVPropfindFunc)(EWebDAVSession *webdav,
+                                        xmlXPathContextPtr xpath_ctx,
+                                        const gchar *xpath_prop_prefix,
+                                        const SoupURI *request_uri,
+                                        guint status_code,
+                                        gpointer user_data);
+
+/**
  * EWebDAVSession:
  *
  * Contains only private data that should be read and manipulated using the
@@ -77,6 +209,85 @@ struct _EWebDAVSessionClass {
 GType          e_webdav_session_get_type               (void) G_GNUC_CONST;
 
 EWebDAVSession *e_webdav_session_new                   (ESource *source);
+gboolean       e_webdav_session_options_sync           (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GHashTable **out_capabilities,
+                                                        GHashTable **out_allows,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_propfind_sync          (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *depth,
+                                                        const EXmlDocument *xml,
+                                                        EWebDAVPropfindFunc func,
+                                                        gpointer func_user_data,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+/*gboolean     e_webdav_session_proppatch_sync         (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const EXmlDocument *xml,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_mkcol_sync             (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_sync               (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_put_sync               (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *content_type,
+                                                        const gchar *bytes,
+                                                        gsize length,
+                                                        gchar **out_redirected_uri,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_delete_sync            (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_copy_sync              (EWebDAVSession *webdav,
+                                                        const gchar *source_uri,
+                                                        const gchar *destination_uri,
+                                                        const gchar *depth,
+                                                        gboolean can_overwrite,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_move_sync              (EWebDAVSession *webdav,
+                                                        const gchar *source_uri,
+                                                        const gchar *destination_uri,
+                                                        gboolean can_overwrite,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_lock_sync              (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *depth,
+                                                        const gchar *lock_token,
+                                                        guint32 lock_timeout,
+                                                        const EXmlDocument *xml,
+                                                        gchar **out_lock_token,
+                                                        EXmlDocument **out_xml_response,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_unlock_sync            (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *lock_token,
+                                                        GCancellable *cancellable,
+                                                        GError **error);*/
+
+gboolean       e_webdav_session_getctag_sync           (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        gchar **out_ctag,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_list_sync              (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        guint32 flags, /* bit-or of EWebDAVListFlags */
+                                                        GSList **out_resources, /* EWebDAVResource * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
 
 G_END_DECLS
 
diff --git a/src/libedataserver/e-xml-document.c b/src/libedataserver/e-xml-document.c
new file mode 100644
index 0000000..9212df4
--- /dev/null
+++ b/src/libedataserver/e-xml-document.c
@@ -0,0 +1,601 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 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/>.
+ */
+
+/**
+ * SECTION: e-xml-document
+ * @include: libedataserver/libedataserver.h
+ * @short_description: An XML document wrapper
+ *
+ * The #EXmlDocument class wraps creation of XML documents.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+
+#include "e-xml-document.h"
+
+struct _EXmlDocumentPrivate {
+       xmlDocPtr doc;
+       xmlNodePtr root;
+       xmlNodePtr current_element;
+
+       GHashTable *namespaces_by_href; /* gchar *ns_href ~> xmlNsPtr */
+};
+
+G_DEFINE_TYPE (EXmlDocument, e_xml_document, G_TYPE_OBJECT)
+
+static void
+e_xml_document_finalize (GObject *object)
+{
+       EXmlDocument *xml = E_XML_DOCUMENT (object);
+
+       if (xml->priv->doc) {
+               xmlFreeDoc (xml->priv->doc);
+               xml->priv->doc = NULL;
+       }
+
+       xml->priv->root = NULL;
+       xml->priv->current_element = NULL;
+
+       if (xml->priv->namespaces_by_href) {
+               g_hash_table_destroy (xml->priv->namespaces_by_href);
+               xml->priv->namespaces_by_href = NULL;
+       }
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_xml_document_parent_class)->finalize (object);
+}
+
+static void
+e_xml_document_class_init (EXmlDocumentClass *klass)
+{
+       GObjectClass *object_class;
+
+       g_type_class_add_private (klass, sizeof (EXmlDocumentPrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = e_xml_document_finalize;
+}
+
+static void
+e_xml_document_init (EXmlDocument *xml)
+{
+       xml->priv = G_TYPE_INSTANCE_GET_PRIVATE (xml, E_TYPE_XML_DOCUMENT, EXmlDocumentPrivate);
+
+       xml->priv->doc = xmlNewDoc ((const xmlChar *) "1.0");
+       g_return_if_fail (xml->priv->doc != NULL);
+
+       xml->priv->doc->encoding = xmlCharStrdup ("UTF-8");
+
+       xml->priv->namespaces_by_href = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+/**
+ * e_xml_document_new:
+ * @ns_href: (nullable): default namespace href to use, or %NULL
+ * @root_element: root element name
+ *
+ * Creates a new #EXmlDocument with root element @root_element and optionally
+ * also with set default namespace @ns_href.
+ *
+ * Returns: (transfer full): a new #EXmlDocument; free it with g_object_unref(),
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EXmlDocument *
+e_xml_document_new (const gchar *ns_href,
+                   const gchar *root_element)
+{
+       EXmlDocument *xml;
+
+       g_return_val_if_fail (root_element != NULL, NULL);
+       g_return_val_if_fail (*root_element, NULL);
+
+       xml = g_object_new (E_TYPE_XML_DOCUMENT, NULL);
+
+       xml->priv->root = xmlNewDocNode (xml->priv->doc, NULL, (const xmlChar *) root_element, NULL);
+       if (ns_href) {
+               xmlNsPtr ns;
+
+               ns = xmlNewNs (xml->priv->root, (const xmlChar *) ns_href, NULL);
+               g_warn_if_fail (ns != NULL);
+
+               xmlSetNs (xml->priv->root, ns);
+
+               if (ns)
+                       g_hash_table_insert (xml->priv->namespaces_by_href, g_strdup (ns_href), ns);
+       }
+
+       xmlDocSetRootElement (xml->priv->doc, xml->priv->root);
+
+       xml->priv->current_element = xml->priv->root;
+
+       return xml;
+}
+
+/**
+ * e_xml_document_get_xmldoc:
+ * @xml: an #EXmlDocument
+ *
+ * Returns: (transfer none): Underlying #xmlDocPtr.
+ *
+ * Since: 3.26
+ **/
+xmlDocPtr
+e_xml_document_get_xmldoc (EXmlDocument *xml)
+{
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+
+       return xml->priv->doc;
+}
+
+/**
+ * e_xml_document_get_content:
+ * @xml: an #EXmlDocument
+ * @out_length: (out) (nullable): optional return location for length of the content, or %NULL
+ *
+ * Gets content of the @xml as string. The string is nul-terminated, but
+ * if @out_length is also provided, then it doesn't contain this additional
+ * nul character.
+ *
+ * Returns: (transfer full): Content of the @xml as newly allocated string.
+ *    Free it with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_xml_document_get_content (const EXmlDocument *xml,
+                           gsize *out_length)
+{
+       xmlOutputBufferPtr xmlbuffer;
+       gsize length;
+       gchar *text;
+
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+
+       xmlbuffer = xmlAllocOutputBuffer (NULL);
+       xmlNodeDumpOutput (xmlbuffer, xml->priv->doc, xml->priv->root, 0, 1, NULL);
+       xmlOutputBufferFlush (xmlbuffer);
+
+#ifdef LIBXML2_NEW_BUFFER
+       length = xmlOutputBufferGetSize (xmlbuffer);
+       text = g_strndup ((const gchar *) xmlOutputBufferGetContent (xmlbuffer), length);
+#else
+       length = xmlbuffer->buffer->use;
+       text = g_strndup ((const gchar *) xmlbuffer->buffer->content, length);
+#endif
+
+       xmlOutputBufferClose (xmlbuffer);
+
+       if (out_length)
+               *out_length = length;
+
+       return text;
+}
+
+/**
+ * e_xml_document_add_namespaces:
+ * @xml: an #EXmlDocument
+ * @ns_prefix: namespace prefix to use for this namespace
+ * @ns_href: namespace href
+ * @...: %NULL-terminated pairs of (ns_prefix, ns_href)
+ *
+ * Adds one or more namespaces to @xml, which can be referenced
+ * later by @ns_href. The caller should take care that neither
+ * used @ns_prefix, nor @ns_href, is already used by @xml.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_namespaces (EXmlDocument *xml,
+                              const gchar *ns_prefix,
+                              const gchar *ns_href,
+                              ...)
+{
+       xmlNsPtr ns;
+       va_list va;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (ns_prefix != NULL);
+       g_return_if_fail (xml->priv->root != NULL);
+
+       if (!ns_href)
+               ns_href = "";
+
+       if (!g_hash_table_contains (xml->priv->namespaces_by_href, ns_href)) {
+               ns = xmlNewNs (xml->priv->root, (const xmlChar *) ns_href, (const xmlChar *) ns_prefix);
+               g_return_if_fail (ns != NULL);
+
+               g_hash_table_insert (xml->priv->namespaces_by_href, g_strdup (ns_href), ns);
+       }
+
+       va_start (va, ns_href);
+
+       while (ns_prefix = va_arg (va, const gchar *), ns_prefix) {
+               ns_href = va_arg (va, const gchar *);
+               if (!ns_href)
+                       ns_href = "";
+
+               if (!g_hash_table_contains (xml->priv->namespaces_by_href, ns_href)) {
+                       ns = xmlNewNs (xml->priv->root, (const xmlChar *) ns_href, (const xmlChar *) 
ns_prefix);
+                       g_return_if_fail (ns != NULL);
+
+                       g_hash_table_insert (xml->priv->namespaces_by_href, g_strdup (ns_href), ns);
+               }
+       }
+
+       va_end (va);
+}
+
+static gchar *
+e_xml_document_number_to_alpha (gint number)
+{
+       GString *alpha;
+
+       g_return_val_if_fail (number >= 0, NULL);
+
+       alpha = g_string_new ("");
+       g_string_append_c (alpha, 'A' + (number % 26));
+
+       while (number = number / 26, number > 0) {
+               g_string_prepend_c (alpha, 'A' + (number % 26));
+       }
+
+       return g_string_free (alpha, FALSE);
+}
+
+static gchar *
+e_xml_document_gen_ns_prefix (EXmlDocument *xml,
+                             const gchar *ns_href)
+{
+       GHashTable *prefixes;
+       GHashTableIter iter;
+       gpointer value;
+       gchar *new_prefix = NULL;
+       const gchar *ptr;
+       gint counter = 0, n_prefixes;
+
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+       g_return_val_if_fail (ns_href && *ns_href, NULL);
+
+       if (!ns_href)
+               return NULL;
+
+       prefixes = g_hash_table_new (g_str_hash, g_str_equal);
+
+       g_hash_table_iter_init (&iter, xml->priv->namespaces_by_href);
+       while (g_hash_table_iter_next (&iter, NULL, &value)) {
+               xmlNsPtr ns = value;
+
+               if (ns && ns->prefix)
+                       g_hash_table_insert (prefixes, (gpointer) ns->prefix, NULL);
+       }
+
+       ptr = strrchr (ns_href, ':');
+
+       /* the ns_href ends with ':' */
+       if (ptr && !ptr[1] && g_ascii_isalpha (ns_href[0])) {
+               new_prefix = g_strndup (ns_href, 1);
+       } else if (ptr && strchr (ns_href, ':') < ptr && g_ascii_isalpha (ptr[1])) {
+               new_prefix = g_strndup (ptr + 1, 1);
+       } else if (g_str_has_prefix (ns_href, "http://";) &&
+                  g_ascii_isalpha (ns_href[7])) {
+               new_prefix = g_strndup (ns_href + 7, 1);
+       }
+
+       n_prefixes = g_hash_table_size (prefixes);
+
+       while (!new_prefix || g_hash_table_contains (prefixes, new_prefix)) {
+               g_free (new_prefix);
+
+               if (counter > n_prefixes + 2) {
+                       new_prefix = NULL;
+                       break;
+               }
+
+               new_prefix = e_xml_document_number_to_alpha (counter);
+               counter++;
+       }
+
+       g_hash_table_destroy (prefixes);
+
+       return new_prefix;
+}
+
+static xmlNsPtr
+e_xml_document_ensure_namespace (EXmlDocument *xml,
+                                const gchar *ns_href)
+{
+       xmlNsPtr ns;
+       gchar *ns_prefix;
+
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+
+       if (!ns_href)
+               return NULL;
+
+       ns = g_hash_table_lookup (xml->priv->namespaces_by_href, ns_href);
+       if (ns || !*ns_href)
+               return ns;
+
+       ns_prefix = e_xml_document_gen_ns_prefix (xml, ns_href);
+
+       e_xml_document_add_namespaces (xml, ns_prefix, ns_href, NULL);
+
+       g_free (ns_prefix);
+
+       return g_hash_table_lookup (xml->priv->namespaces_by_href, ns_href);
+}
+
+/**
+ * e_xml_document_start_element:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new element, or %NULL
+ * @name: name of the new element
+ *
+ * Starts a new non-text element as a child of the current element.
+ * Each such call should be ended with corresponding e_xml_document_end_element().
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * To start a text node use e_xml_document_start_text_element().
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_start_element (EXmlDocument *xml,
+                             const gchar *ns_href,
+                             const gchar *name)
+{
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (name != NULL);
+       g_return_if_fail (*name);
+       g_return_if_fail (xml->priv->current_element != NULL);
+
+       xml->priv->current_element = xmlNewChild (xml->priv->current_element,
+               e_xml_document_ensure_namespace (xml, ns_href), (const xmlChar *) name, NULL);
+}
+
+/**
+ * e_xml_document_start_text_element:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new element, or %NULL
+ * @name: name of the new element
+ *
+ * Starts a new text element as a child of the current element.
+ * Each such call should be ended with corresponding e_xml_document_end_element().
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * To start a non-text node use e_xml_document_start_element().
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_start_text_element (EXmlDocument *xml,
+                                  const gchar *ns_href,
+                                  const gchar *name)
+{
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (name != NULL);
+       g_return_if_fail (*name);
+       g_return_if_fail (xml->priv->current_element != NULL);
+
+       xml->priv->current_element = xmlNewTextChild (xml->priv->current_element,
+               e_xml_document_ensure_namespace (xml, ns_href), (const xmlChar *) name, NULL);
+}
+
+/**
+ * e_xml_document_end_element:
+ * @xml: an #EXmlDocument
+ *
+ * This is a pair function for e_xml_document_start_element() and
+ * e_xml_document_start_text_element(), which changes current
+ * element to the parent of that element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_end_element (EXmlDocument *xml)
+{
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (xml->priv->current_element != xml->priv->root);
+
+       xml->priv->current_element = xml->priv->current_element->parent;
+}
+
+/**
+ * e_xml_document_add_attribute:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: value of the attribute
+ *
+ * Adds a new attribute to the current element.
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_attribute (EXmlDocument *xml,
+                             const gchar *ns_href,
+                             const gchar *name,
+                             const gchar *value)
+{
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (name != NULL);
+       g_return_if_fail (value != NULL);
+
+       xmlNewNsProp (
+               xml->priv->current_element,
+               e_xml_document_ensure_namespace (xml, ns_href),
+               (const xmlChar *) name,
+               (const xmlChar *) value);
+}
+
+/**
+ * e_xml_document_write_int:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_int (EXmlDocument *xml,
+                         gint64 value)
+{
+       gchar *strvalue;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+
+       strvalue = g_strdup_printf ("%" G_GINT64_FORMAT, value);
+       e_xml_document_write_string (xml, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_double:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_double (EXmlDocument *xml,
+                            gdouble value)
+{
+       gchar *strvalue;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+
+       strvalue = g_strdup_printf ("%f", value);
+       e_xml_document_write_string (xml, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_base64:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ * @len: length of @value
+ *
+ * Writes @value of length @len, encoded to base64, as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_base64 (EXmlDocument *xml,
+                            const gchar *value,
+                            gint len)
+{
+       gchar *strvalue;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (value != NULL);
+
+       strvalue = g_base64_encode ((const guchar *) value, len);
+       e_xml_document_write_string (xml, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_time:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value in ISO 8601 format as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_time (EXmlDocument *xml,
+                          time_t value)
+{
+       GTimeVal tv;
+       gchar *strvalue;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+
+       tv.tv_usec = 0;
+       tv.tv_sec = value;
+
+       strvalue = g_time_val_to_iso8601 (&tv);
+       e_xml_document_write_string (xml, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_string:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_string (EXmlDocument *xml,
+                            const gchar *value)
+{
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (value != NULL);
+
+       xmlNodeAddContent (
+               xml->priv->current_element,
+               (const xmlChar *) value);
+}
+
+/**
+ * e_xml_document_write_buffer:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ * @len: length of @value
+ *
+ * Writes @value of length @len as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_buffer (EXmlDocument *xml,
+                            const gchar *value,
+                            gint len)
+{
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (value != NULL);
+
+       xmlNodeAddContentLen (
+               xml->priv->current_element,
+               (const xmlChar *) value, len);
+}
diff --git a/src/libedataserver/e-xml-document.h b/src/libedataserver/e-xml-document.h
new file mode 100644
index 0000000..afadf2e
--- /dev/null
+++ b/src/libedataserver/e-xml-document.h
@@ -0,0 +1,115 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 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_XML_DOCUMENT_H
+#define E_XML_DOCUMENT_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <libxml/parser.h>
+
+/* Standard GObject macros */
+#define E_TYPE_XML_DOCUMENT \
+       (e_xml_document_get_type ())
+#define E_XML_DOCUMENT(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_XML_DOCUMENT, EXmlDocument))
+#define E_XML_DOCUMENT_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_XML_DOCUMENT, EXmlDocumentClass))
+#define E_IS_XML_DOCUMENT(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_XML_DOCUMENT))
+#define E_IS_XML_DOCUMENT_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_XML_DOCUMENT))
+#define E_XML_DOCUMENT_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_XML_DOCUMENT, EXmlDocumentClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EXmlDocument EXmlDocument;
+typedef struct _EXmlDocumentClass EXmlDocumentClass;
+typedef struct _EXmlDocumentPrivate EXmlDocumentPrivate;
+
+/**
+ * EXmlDocument:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EXmlDocument {
+       /*< private >*/
+       GObject parent;
+       EXmlDocumentPrivate *priv;
+};
+
+struct _EXmlDocumentClass {
+       GObjectClass parent_class;
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_xml_document_get_type         (void) G_GNUC_CONST;
+
+EXmlDocument * e_xml_document_new              (const gchar *ns_href,
+                                                const gchar *root_element);
+xmlDocPtr      e_xml_document_get_xmldoc       (EXmlDocument *xml);
+gchar *                e_xml_document_get_content      (const EXmlDocument *xml,
+                                                gsize *out_length);
+void           e_xml_document_add_namespaces   (EXmlDocument *xml,
+                                                const gchar *ns_prefix,
+                                                const gchar *ns_href,
+                                                ...) G_GNUC_NULL_TERMINATED;
+void           e_xml_document_start_element    (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name);
+void           e_xml_document_start_text_element
+                                               (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name);
+void           e_xml_document_end_element      (EXmlDocument *xml);
+void           e_xml_document_add_attribute    (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name,
+                                                const gchar *value);
+void           e_xml_document_write_int        (EXmlDocument *xml,
+                                                gint64 value);
+void           e_xml_document_write_double     (EXmlDocument *xml,
+                                                gdouble value);
+void           e_xml_document_write_base64     (EXmlDocument *xml,
+                                                const gchar *value,
+                                                gint len);
+void           e_xml_document_write_time       (EXmlDocument *xml,
+                                                time_t value);
+void           e_xml_document_write_string     (EXmlDocument *xml,
+                                                const gchar *value);
+void           e_xml_document_write_buffer     (EXmlDocument *xml,
+                                                const gchar *value,
+                                                gint len);
+
+G_END_DECLS
+
+#endif /* E_XML_DOCUMENT_H */
diff --git a/src/libedataserver/e-xml-utils.c b/src/libedataserver/e-xml-utils.c
index 56f0f66..a02c327 100644
--- a/src/libedataserver/e-xml-utils.c
+++ b/src/libedataserver/e-xml-utils.c
@@ -28,6 +28,7 @@
 
 #include <libxml/parser.h>
 #include <libxml/tree.h>
+#include <libxml/xpathInternals.h>
 
 #include <glib/gstdio.h>
 
@@ -177,3 +178,267 @@ e_xml_get_child_by_name (const xmlNode *parent,
        return NULL;
 }
 
+/**
+ * e_xml_parse_data:
+ * @data: an XML data
+ * @length: (length-of data): length of data, should be greated than zero
+ *
+ * Parses XML data into an #xmlDocPtr. Free returned pointer
+ * with xmlFreeDoc(), when no longer needed.
+ *
+ * Returns: (nullable) (transfer full): a new #xmlDocPtr with parsed @data,
+ *    or %NULL on error.
+ *
+ * Since: 3.26
+ **/
+xmlDocPtr
+e_xml_parse_data (const gchar *data,
+                 gsize length)
+{
+       g_return_val_if_fail (data != NULL, NULL);
+       g_return_val_if_fail (length > 0, NULL);
+
+       return xmlReadMemory (data, length, "data.xml", NULL, 0);
+}
+
+/**
+ * e_xml_new_xpath_context_with_namespaces:
+ * @doc: an #xmlDocPtr
+ * @...: %NULL-terminated list of pairs (prefix, href) with namespaces
+ *
+ * Creates a new #xmlXPathContextPtr on @doc with preregistered
+ * namespaces. The namepsaces are pair of (prefix, href), terminated
+ * by %NULL.
+ *
+ * Returns: (transfer full): a new #xmlXPathContextPtr. Free the returned
+ *    pointer with xmlXPathFreeContext() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+xmlXPathContextPtr
+e_xml_new_xpath_context_with_namespaces (xmlDocPtr doc,
+                                        ...)
+{
+       xmlXPathContextPtr xpath_ctx;
+       va_list va;
+       const gchar *prefix;
+
+       g_return_val_if_fail (doc != NULL, NULL);
+
+       xpath_ctx = xmlXPathNewContext (doc);
+       g_return_val_if_fail (xpath_ctx != NULL, NULL);
+
+       va_start (va, doc);
+
+       while (prefix = va_arg (va, const gchar *), prefix) {
+               const gchar *href = va_arg (va, const gchar *);
+
+               if (!href) {
+                       g_warn_if_fail (href != NULL);
+                       break;
+               }
+
+               xmlXPathRegisterNs (xpath_ctx, (const xmlChar *) prefix, (const xmlChar *) href);
+       }
+
+       va_end (va);
+
+       return xpath_ctx;
+}
+
+/**
+ * e_xml_xpath_context_register_namespaces:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @prefix: namespace prefix
+ * @href: namespace href
+ * @...: %NULL-terminated list of pairs (prefix, href) with additional namespaces
+ *
+ * Registers one or more additional namespaces. It's a caller's error
+ * to try to register a namespace with the same prefix again, unless
+ * the prefix uses the same namespace href.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_xpath_context_register_namespaces (xmlXPathContextPtr xpath_ctx,
+                                        const gchar *prefix,
+                                        const gchar *href,
+                                        ...)
+{
+       va_list va;
+       const gchar *used_href;
+
+       g_return_if_fail (xpath_ctx != NULL);
+       g_return_if_fail (prefix != NULL);
+       g_return_if_fail (href != NULL);
+
+       used_href = (const gchar *) xmlXPathNsLookup (xpath_ctx, (const xmlChar *) prefix);
+       if (used_href && g_strcmp0 (used_href, href) != 0) {
+               g_warning ("%s: Trying to register prefix '%s' with href '%s', but it already points to '%s'",
+                       G_STRFUNC, prefix, href, used_href);
+       } else if (!used_href) {
+               xmlXPathRegisterNs (xpath_ctx, (const xmlChar *) prefix, (const xmlChar *) href);
+       }
+
+       va_start (va, href);
+
+       while (prefix = va_arg (va, const gchar *), prefix) {
+               href = va_arg (va, const gchar *);
+
+               if (!href) {
+                       g_warn_if_fail (href != NULL);
+                       break;
+               }
+
+               used_href = (const gchar *) xmlXPathNsLookup (xpath_ctx, (const xmlChar *) prefix);
+               if (used_href && g_strcmp0 (used_href, href) != 0) {
+                       g_warning ("%s: Trying to register prefix '%s' with href '%s', but it already points 
to '%s'",
+                               G_STRFUNC, prefix, href, used_href);
+               } else if (!used_href) {
+                       xmlXPathRegisterNs (xpath_ctx, (const xmlChar *) prefix, (const xmlChar *) href);
+               }
+       }
+
+       va_end (va);
+}
+
+/**
+ * e_xml_xpath_eval:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @format: printf-like format specifier of path to evaluate
+ * @...: arguments for the @format
+ *
+ * Evaluates path specified by @format and returns its #xmlXPathObjectPtr,
+ * in case the path evaluates to a non-empty node set. See also
+ * e_xml_xpath_eval_as_string() which evaluates the path to string.
+ *
+ * Returns: (nullable) (transfer full): a new #xmlXPathObjectPtr which
+ *    references given path, or %NULL if path cannot be found or when
+ *    it evaluates to an empty nodeset. Free returned pointer with
+ *    xmlXPathFreeObject(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+xmlXPathObjectPtr
+e_xml_xpath_eval (xmlXPathContextPtr xpath_ctx,
+                 const gchar *format,
+                 ...)
+{
+       xmlXPathObjectPtr object;
+       va_list va;
+       gchar *expr;
+
+       g_return_val_if_fail (xpath_ctx != NULL, NULL);
+       g_return_val_if_fail (format != NULL, NULL);
+
+       va_start (va, format);
+       expr = g_strdup_vprintf (format, va);
+       va_end (va);
+
+       object = xmlXPathEvalExpression ((const xmlChar *) expr, xpath_ctx);
+       g_free (expr);
+
+       if (!object)
+               return NULL;
+
+       if (object->type == XPATH_NODESET &&
+           xmlXPathNodeSetIsEmpty (object->nodesetval)) {
+               xmlXPathFreeObject (object);
+               return NULL;
+       }
+
+       return object;
+}
+
+/**
+ * e_xml_xpath_eval_as_string:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @format: printf-like format specifier of path to evaluate
+ * @...: arguments for the @format
+ *
+ * Evaluates path specified by @format and returns its result as string,
+ * in case the path evaluates to a non-empty node set. See also
+ * e_xml_xpath_eval() which evaluates the path to an #xmlXPathObjectPtr.
+ *
+ * Returns: (nullable) (transfer full): a new string which contains value
+ *    of the given path, or %NULL if path cannot be found or when
+ *    it evaluates to an empty nodeset. Free returned pointer with
+ *    g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_xml_xpath_eval_as_string (xmlXPathContextPtr xpath_ctx,
+                           const gchar *format,
+                           ...)
+{
+       xmlXPathObjectPtr object;
+       va_list va;
+       gchar *expr, *value;
+
+       g_return_val_if_fail (xpath_ctx != NULL, NULL);
+       g_return_val_if_fail (format != NULL, NULL);
+
+       va_start (va, format);
+       expr = g_strdup_vprintf (format, va);
+       va_end (va);
+
+       if (!g_str_has_prefix (format, "string(")) {
+               gchar *tmp = expr;
+
+               expr = g_strconcat ("string(", expr, ")", NULL);
+
+               g_free (tmp);
+       }
+
+       object = e_xml_xpath_eval (xpath_ctx, "%s", expr);
+       if (!object)
+               return NULL;
+
+       if (object->type == XPATH_STRING &&
+           *object->stringval)
+               value = g_strdup ((const gchar *) object->stringval);
+       else
+               value = NULL;
+
+       xmlXPathFreeObject (object);
+
+       return value;
+}
+
+/**
+ * e_xml_xpath_eval_exists:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @format: printf-like format specifier of path to evaluate
+ * @...: arguments for the @format
+ *
+ * Evaluates path specified by @format and returns whether it exists.
+ *
+ * Returns: %TRUE, when the given XPath exists, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_xml_xpath_eval_exists (xmlXPathContextPtr xpath_ctx,
+                        const gchar *format,
+                        ...)
+{
+       xmlXPathObjectPtr object;
+       va_list va;
+       gchar *expr;
+
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (format != NULL, FALSE);
+
+       va_start (va, format);
+       expr = g_strdup_vprintf (format, va);
+       va_end (va);
+
+       object = e_xml_xpath_eval (xpath_ctx, "%s", expr);
+       if (!object)
+               return FALSE;
+
+       xmlXPathFreeObject (object);
+
+       return TRUE;
+}
diff --git a/src/libedataserver/e-xml-utils.h b/src/libedataserver/e-xml-utils.h
index de3cebf..2c98ecb 100644
--- a/src/libedataserver/e-xml-utils.h
+++ b/src/libedataserver/e-xml-utils.h
@@ -25,6 +25,7 @@
 
 #include <glib.h>
 #include <libxml/parser.h>
+#include <libxml/xpath.h>
 
 G_BEGIN_DECLS
 
@@ -34,7 +35,28 @@ gint         e_xml_save_file                 (const gchar *filename,
 xmlNode *      e_xml_get_child_by_name         (const xmlNode *parent,
                                                 const xmlChar *child_name);
 
+xmlDocPtr      e_xml_parse_data                (const gchar *data,
+                                                gsize length);
+xmlXPathContextPtr
+               e_xml_new_xpath_context_with_namespaces
+                                               (xmlDocPtr doc,
+                                                ...) G_GNUC_NULL_TERMINATED;
+void           e_xml_xpath_context_register_namespaces
+                                               (xmlXPathContextPtr xpath_ctx,
+                                                const gchar *prefix,
+                                                const gchar *href,
+                                                ...) G_GNUC_NULL_TERMINATED;
+xmlXPathObjectPtr
+               e_xml_xpath_eval                (xmlXPathContextPtr xpath_ctx,
+                                                const gchar *format,
+                                                ...) G_GNUC_PRINTF (2, 3);
+gchar *                e_xml_xpath_eval_as_string      (xmlXPathContextPtr xpath_ctx,
+                                                const gchar *format,
+                                                ...) G_GNUC_PRINTF (2, 3);
+gboolean       e_xml_xpath_eval_exists         (xmlXPathContextPtr xpath_ctx,
+                                                const gchar *format,
+                                                ...) G_GNUC_PRINTF (2, 3);
+
 G_END_DECLS
 
 #endif /* E_XML_UTILS_H */
-
diff --git a/src/libedataserver/libedataserver.h b/src/libedataserver/libedataserver.h
index 437cb02..b11f8f8 100644
--- a/src/libedataserver/libedataserver.h
+++ b/src/libedataserver/libedataserver.h
@@ -93,6 +93,7 @@
 #include <libedataserver/e-url.h>
 #include <libedataserver/e-webdav-discover.h>
 #include <libedataserver/e-webdav-session.h>
+#include <libedataserver/e-xml-document.h>
 #include <libedataserver/e-xml-hash-utils.h>
 #include <libedataserver/e-xml-utils.h>
 #include <libedataserver/eds-version.h>


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