[evolution-data-server] Merge offline-cache changes into master



commit 3abbcce2eadfef5a9de866b0aef43b8945b419b1
Author: Milan Crha <mcrha redhat com>
Date:   Wed May 17 15:32:39 2017 +0200

    Merge offline-cache changes into master

 .../evolution-data-server-docs.sgml.in             |   13 +
 po/POTFILES.in                                     |    8 +
 .../backends/google/e-book-backend-google.c        | 2398 ++------
 .../backends/google/e-book-backend-google.h        |    4 +-
 .../backends/google/e-book-google-utils.c          |   52 +-
 .../backends/google/e-book-google-utils.h          |   35 +-
 .../backends/google/tests/phone-numbers.c          |    9 +-
 .../backends/webdav/e-book-backend-webdav.c        | 2409 +++------
 .../backends/webdav/e-book-backend-webdav.h        |   23 +-
 src/addressbook/libebook-contacts/e-vcard.c        |   74 +
 src/addressbook/libebook-contacts/e-vcard.h        |    6 +
 src/addressbook/libedata-book/CMakeLists.txt       |    8 +-
 src/addressbook/libedata-book/e-book-backend.c     |   99 +-
 src/addressbook/libedata-book/e-book-backend.h     |   26 +-
 src/addressbook/libedata-book/e-book-cache.c       | 6113 +++++++++++++++++++
 src/addressbook/libedata-book/e-book-cache.h       |  323 +
 .../libedata-book/e-book-meta-backend.c            | 3767 ++++++++++++
 .../libedata-book/e-book-meta-backend.h            |  276 +
 .../libedata-book/e-data-book-cursor-cache.c       |  438 ++
 .../libedata-book/e-data-book-cursor-cache.h       |   81 +
 src/addressbook/libedata-book/libedata-book.h      |    3 +
 .../backends/caldav/e-cal-backend-caldav.c         | 6198 ++++----------------
 .../backends/caldav/e-cal-backend-caldav.h         |    4 +-
 .../backends/gtasks/e-cal-backend-gtasks.c         | 1655 ++----
 .../backends/gtasks/e-cal-backend-gtasks.h         |    4 +-
 src/calendar/backends/http/e-cal-backend-http.c    | 1715 ++-----
 src/calendar/backends/http/e-cal-backend-http.h    |    4 +-
 src/calendar/libecal/e-cal-util.c                  |  204 +-
 src/calendar/libecal/e-cal-util.h                  |   12 +
 src/calendar/libedata-cal/CMakeLists.txt           |    6 +-
 src/calendar/libedata-cal/e-cal-backend-sexp.c     |   85 +-
 src/calendar/libedata-cal/e-cal-backend.c          |   98 +-
 src/calendar/libedata-cal/e-cal-backend.h          |   26 +-
 src/calendar/libedata-cal/e-cal-cache.c            | 3651 ++++++++++++
 src/calendar/libedata-cal/e-cal-cache.h            |  335 ++
 src/calendar/libedata-cal/e-cal-meta-backend.c     | 4570 +++++++++++++++
 src/calendar/libedata-cal/e-cal-meta-backend.h     |  282 +
 src/calendar/libedata-cal/libedata-cal.h           |    2 +
 src/libebackend/CMakeLists.txt                     |    2 +
 src/libebackend/e-backend-enums.h                  |   43 +
 src/libebackend/e-backend.c                        |   14 +-
 src/libebackend/e-cache.c                          | 3068 ++++++++++
 src/libebackend/e-cache.h                          |  524 ++
 src/libebackend/libebackend.h                      |    1 +
 src/libedataserver/CMakeLists.txt                  |   10 +-
 src/libedataserver/e-data-server-util.c            |   90 +
 src/libedataserver/e-data-server-util.h            |    4 +
 src/libedataserver/e-soup-session.c                | 1019 ++++
 src/libedataserver/e-soup-session.h                |  120 +
 .../e-source-credentials-provider-impl-google.c    |    8 +-
 src/libedataserver/e-source-enums.h                |    3 +
 src/libedataserver/e-webdav-discover.c             | 1927 +------
 src/libedataserver/e-webdav-session.c              | 4983 ++++++++++++++++
 src/libedataserver/e-webdav-session.h              |  582 ++
 src/libedataserver/e-xml-document.c                |  727 +++
 src/libedataserver/e-xml-document.h                |  134 +
 src/libedataserver/e-xml-utils.c                   |  265 +
 src/libedataserver/e-xml-utils.h                   |   24 +-
 src/libedataserver/libedataserver.h                |    3 +
 .../client/test-book-client-view-operations.c      |   27 +-
 .../libebook/client/test-book-client-write-write.c |   16 +-
 tests/libebook/data/vcards/.gitattributes          |    1 +
 tests/libebook/data/vcards/custom-1.vcf            |    1 +
 tests/libebook/data/vcards/custom-2.vcf            |    1 +
 tests/libebook/data/vcards/custom-3.vcf            |    1 +
 tests/libebook/data/vcards/custom-4.vcf            |    1 +
 tests/libebook/data/vcards/custom-5.vcf            |    1 +
 tests/libebook/data/vcards/custom-6.vcf            |    1 +
 tests/libebook/data/vcards/custom-7.vcf            |    1 +
 tests/libebook/data/vcards/custom-8.vcf            |    1 +
 tests/libebook/data/vcards/custom-9.vcf            |    1 +
 tests/libebook/data/vcards/logo-1.vcf              |   21 +
 tests/libebook/data/vcards/photo-1.vcf             |   59 +
 tests/libebook/vcard/.gitattributes                |    1 +
 tests/libebook/vcard/11.vcf                        |   16 +-
 tests/libebook/vcard/12.vcf                        |   46 +-
 tests/libebook/vcard/3.vcf                         |   26 +-
 tests/libebook/vcard/4.vcf                         |   22 +-
 tests/libebook/vcard/5.vcf                         |   64 +-
 tests/libedata-book/CMakeLists.txt                 |   30 +-
 tests/libedata-book/data-test-utils.h              |    4 +
 .../libedata-book/test-book-cache-create-cursor.c  |  125 +
 .../test-book-cache-cursor-calculate.c             |  695 +++
 .../test-book-cache-cursor-change-locale.c         |  102 +
 .../test-book-cache-cursor-move-by-de-DE.c         |   82 +
 .../test-book-cache-cursor-move-by-en-US.c         |  100 +
 .../test-book-cache-cursor-move-by-fr-CA.c         |   82 +
 .../test-book-cache-cursor-move-by-posix.c         |   82 +
 .../test-book-cache-cursor-set-sexp.c              |  155 +
 .../test-book-cache-cursor-set-target.c            |  225 +
 tests/libedata-book/test-book-cache-get-contact.c  |   78 +
 tests/libedata-book/test-book-cache-offline.c      | 1138 ++++
 tests/libedata-book/test-book-cache-utils.c        |  695 +++
 tests/libedata-book/test-book-cache-utils.h        |  178 +
 tests/libedata-book/test-book-meta-backend.c       | 1718 ++++++
 tests/libedata-book/test-sqlite-create-cursor.c    |    8 +-
 tests/libedata-cal/CMakeLists.txt                  |   71 +-
 tests/libedata-cal/components/.gitattributes       |    1 +
 tests/libedata-cal/components/event-1.ics          |   18 +
 tests/libedata-cal/components/event-2.ics          |   14 +
 tests/libedata-cal/components/event-3.ics          |   12 +
 tests/libedata-cal/components/event-4.ics          |   12 +
 tests/libedata-cal/components/event-5.ics          |   18 +
 tests/libedata-cal/components/event-6-a.ics        |   13 +
 tests/libedata-cal/components/event-6.ics          |   12 +
 tests/libedata-cal/components/event-7.ics          |   14 +
 tests/libedata-cal/components/event-8.ics          |   16 +
 tests/libedata-cal/components/event-9.ics          |   17 +
 tests/libedata-cal/components/invite-1.ics         |   19 +
 tests/libedata-cal/components/invite-2.ics         |   19 +
 tests/libedata-cal/components/invite-3.ics         |   21 +
 tests/libedata-cal/components/invite-4.ics         |   21 +
 tests/libedata-cal/components/task-1.ics           |    9 +
 tests/libedata-cal/components/task-2.ics           |   11 +
 tests/libedata-cal/components/task-3.ics           |   13 +
 tests/libedata-cal/components/task-4.ics           |   13 +
 tests/libedata-cal/components/task-5.ics           |   13 +
 tests/libedata-cal/components/task-6.ics           |   14 +
 tests/libedata-cal/components/task-7.ics           |   17 +
 tests/libedata-cal/components/task-8.ics           |   11 +
 tests/libedata-cal/components/task-9.ics           |   11 +
 tests/libedata-cal/test-cal-cache-getters.c        |  247 +
 tests/libedata-cal/test-cal-cache-intervals.c      |  344 ++
 tests/libedata-cal/test-cal-cache-offline.c        | 1043 ++++
 tests/libedata-cal/test-cal-cache-search.c         |  473 ++
 tests/libedata-cal/test-cal-cache-utils.c          |  180 +
 tests/libedata-cal/test-cal-cache-utils.h          |   52 +
 tests/libedata-cal/test-cal-meta-backend.c         | 2723 +++++++++
 128 files changed, 47015 insertions(+), 12829 deletions(-)
---
diff --git a/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in 
b/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
index 3dfff75..0b981ba 100644
--- a/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
+++ b/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
@@ -157,6 +157,7 @@
       <xi:include href="xml/e-backend.xml"/>
       <xi:include href="xml/e-backend-enums.xml"/>
       <xi:include href="xml/e-backend-factory.xml"/>
+      <xi:include href="xml/e-cache.xml"/>
       <xi:include href="xml/e-data-factory.xml"/>
       <xi:include href="xml/e-dbus-server.xml"/>
       <xi:include href="xml/e-extensible.xml"/>
@@ -180,10 +181,13 @@
       <xi:include href="xml/e-book-backend.xml"/>
       <xi:include href="xml/e-book-backend-factory.xml"/>
       <xi:include href="xml/e-book-backend-sexp.xml"/>
+      <xi:include href="xml/e-book-cache.xml"/>
+      <xi:include href="xml/e-book-meta-backend.xml"/>
       <xi:include href="xml/e-book-sqlite.xml"/>
       <xi:include href="xml/e-data-book.xml"/>
       <xi:include href="xml/e-data-book-direct.xml"/>
       <xi:include href="xml/e-data-book-cursor.xml"/>
+      <xi:include href="xml/e-data-book-cursor-cache.xml"/>
       <xi:include href="xml/e-data-book-cursor-sqlite.xml"/>
       <xi:include href="xml/e-data-book-factory.xml"/>
       <xi:include href="xml/e-data-book-view.xml"/>
@@ -199,6 +203,8 @@
       <xi:include href="xml/e-cal-backend-store.xml"/>
       <xi:include href="xml/e-cal-backend-sync.xml"/>
       <xi:include href="xml/e-cal-backend-intervaltree.xml"/>
+      <xi:include href="xml/e-cal-cache.xml"/>
+      <xi:include href="xml/e-cal-meta-backend.xml"/>
       <xi:include href="xml/e-data-cal.xml"/>
       <xi:include href="xml/e-data-cal-factory.xml"/>
       <xi:include href="xml/e-data-cal-view.xml"/>
@@ -226,10 +232,13 @@
       <xi:include href="xml/e-operation-pool.xml"/>
       <xi:include href="xml/e-secret-store.xml"/>
       <xi:include href="xml/e-sexp.xml"/>
+      <xi:include href="xml/e-soup-session.xml"/>
       <xi:include href="xml/e-soup-ssl-trust.xml"/>
       <xi:include href="xml/e-time-utils.xml"/>
       <xi:include href="xml/e-uid.xml"/>
       <xi:include href="xml/e-webdav-discover.xml"/>
+      <xi:include href="xml/e-webdav-session.xml"/>
+      <xi:include href="xml/e-xml-document.xml"/>
       <xi:include href="xml/e-xml-hash-utils.xml"/>
       <xi:include href="xml/e-xml-utils.xml"/>
       <xi:include href="xml/eds-version.xml"/>
@@ -324,6 +333,10 @@
     <title>Index of deprecated symbols</title>
     <xi:include href="xml/api-index-deprecated.xml"><xi:fallback /></xi:include>
   </index>
+  <index id="api-index-3.26" role="3.26">
+    <title>Index of new symbols in 3.26</title>
+    <xi:include href="xml/api-index-3.26.xml"><xi:fallback /></xi:include>
+  </index>
   <index id="api-index-3.24" role="3.24">
     <title>Index of new symbols in 3.24</title>
     <xi:include href="xml/api-index-3.24.xml"><xi:fallback /></xi:include>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4cdf92c..2a2d003 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -18,9 +18,12 @@ src/addressbook/libebook/e-destination.c
 src/addressbook/libedata-book/e-book-backend.c
 src/addressbook/libedata-book/e-book-backend-sexp.c
 src/addressbook/libedata-book/e-book-backend-sqlitedb.c
+src/addressbook/libedata-book/e-book-cache.c
+src/addressbook/libedata-book/e-book-meta-backend.c
 src/addressbook/libedata-book/e-book-sqlite.c
 src/addressbook/libedata-book/e-data-book.c
 src/addressbook/libedata-book/e-data-book-cursor.c
+src/addressbook/libedata-book/e-data-book-cursor-cache.c
 src/addressbook/libedata-book/e-data-book-cursor-sqlite.c
 src/addressbook/libedata-book/e-data-book-factory.c
 src/calendar/backends/caldav/e-cal-backend-caldav.c
@@ -38,6 +41,8 @@ src/calendar/libedata-cal/e-cal-backend.c
 src/calendar/libedata-cal/e-cal-backend-sexp.c
 src/calendar/libedata-cal/e-cal-backend-sync.c
 src/calendar/libedata-cal/e-cal-backend-util.c
+src/calendar/libedata-cal/e-cal-cache.c
+src/calendar/libedata-cal/e-cal-meta-backend.c
 src/calendar/libedata-cal/e-data-cal.c
 src/calendar/libedata-cal/e-data-cal-factory.c
 src/camel/camel-address.c
@@ -176,6 +181,7 @@ data/org.gnome.evolution-data-server.calendar.gschema.xml.in
 data/org.gnome.evolution-data-server.gschema.xml.in
 data/org.gnome.evolution.shell.network-config.gschema.xml.in
 src/libebackend/e-backend.c
+src/libebackend/e-cache.c
 src/libebackend/e-collection-backend.c
 src/libebackend/e-data-factory.c
 src/libebackend/e-server-side-source.c
@@ -184,6 +190,7 @@ src/libebackend/e-subprocess-factory.c
 src/libebackend/e-user-prompter-server.c
 src/libedataserver/e-categories.c
 src/libedataserver/e-client.c
+src/libedataserver/e-soup-session.c
 src/libedataserver/e-source.c
 src/libedataserver/e-source-credentials-provider-impl.c
 src/libedataserver/e-source-credentials-provider-impl-google.c
@@ -194,6 +201,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/addressbook/backends/google/e-book-backend-google.c 
b/src/addressbook/backends/google/e-book-backend-google.c
index 1284c0e..92e9ca6 100644
--- a/src/addressbook/backends/google/e-book-backend-google.c
+++ b/src/addressbook/backends/google/e-book-backend-google.c
@@ -2,6 +2,7 @@
  *
  * Copyright (C) 2008 Joergen Scheibengruber
  * Copyright (C) 2010, 2011 Philip Withnall
+ * 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
@@ -31,26 +32,11 @@
 #include "e-book-google-utils.h"
 #include "e-gdata-oauth2-authorizer.h"
 
-#define E_BOOK_BACKEND_GOOGLE_GET_PRIVATE(obj) \
-       (G_TYPE_INSTANCE_GET_PRIVATE \
-       ((obj), E_TYPE_BOOK_BACKEND_GOOGLE, EBookBackendGooglePrivate))
-
-#define CLIENT_ID "evolution-client-0.1.0"
-
 #define URI_GET_CONTACTS "https://www.google.com/m8/feeds/contacts/default/full";
 
-/* This macro was introduced in libgdata 0.11,
- * but we currently only require libgdata 0.10. */
-#ifndef GDATA_CHECK_VERSION
-#define GDATA_CHECK_VERSION(major,minor,micro) 0
-#endif
-
-G_DEFINE_TYPE (EBookBackendGoogle, e_book_backend_google, E_TYPE_BOOK_BACKEND)
+G_DEFINE_TYPE (EBookBackendGoogle, e_book_backend_google, E_TYPE_BOOK_META_BACKEND)
 
 struct _EBookBackendGooglePrivate {
-       EBookBackendCache *cache;
-       GMutex cache_lock;
-
        /* For all the group-related members */
        GRecMutex groups_lock;
        /* Mapping from group ID to (human readable) group name */
@@ -63,27 +49,25 @@ struct _EBookBackendGooglePrivate {
        GHashTable *system_groups_by_entry_id;
        /* Time when the groups were last queried */
        GTimeVal groups_last_update;
+       /* Did the server-side groups change? If so, re-download the book */
+       gboolean groups_changed;
 
        GDataAuthorizer *authorizer;
        GDataService *service;
-
-       guint refresh_id;
-
-       /* Map of active opids to GCancellables */
-       GHashTable *cancellables;
-
-       /* Did the server-side groups change? If so, re-download the book */
-       gboolean groups_changed;
+       GHashTable *preloaded; /* gchar *uid ~> EContact * */
 };
 
 static void
-data_book_error_from_gdata_error (GError **error,
-                                  const GError *gdata_error)
+ebb_google_data_book_error_from_gdata_error (GError **error,
+                                            const GError *gdata_error)
 {
        gboolean use_fallback = FALSE;
 
        g_return_if_fail (gdata_error != NULL);
 
+       if (!error)
+               return;
+
        /* Authentication errors */
        if (gdata_error->domain == GDATA_SERVICE_ERROR) {
                switch (gdata_error->code) {
@@ -158,214 +142,62 @@ data_book_error_from_gdata_error (GError **error,
                        gdata_error->message);
 }
 
-static void
-migrate_cache (EBookBackendCache *cache)
-{
-       const gchar *version;
-       const gchar *version_key = "book-cache-version";
-
-       g_return_if_fail (cache != NULL);
-
-       version = e_file_cache_get_object (E_FILE_CACHE (cache), version_key);
-       if (!version || atoi (version) < 2) {
-               /* not versioned yet or too old, dump the cache and reload it from the server */
-               e_file_cache_clean (E_FILE_CACHE (cache));
-               e_file_cache_add_object (E_FILE_CACHE (cache), version_key, "2");
-       }
-}
-
-static void
-cache_init (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       const gchar *cache_dir;
-       gchar *filename;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_mutex_lock (&priv->cache_lock);
-
-       cache_dir = e_book_backend_get_cache_dir (backend);
-       filename = g_build_filename (cache_dir, "cache.xml", NULL);
-       priv->cache = e_book_backend_cache_new (filename);
-       g_free (filename);
-
-       migrate_cache (priv->cache);
-
-       g_mutex_unlock (&priv->cache_lock);
-}
-
-static EContact *
-cache_add_contact (EBookBackend *backend,
-                   GDataEntry *entry)
-{
-       EBookBackendGooglePrivate *priv;
-       EContact *contact;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_rec_mutex_lock (&priv->groups_lock);
-       contact = e_contact_new_from_gdata_entry (entry, priv->groups_by_id, priv->system_groups_by_entry_id);
-       g_rec_mutex_unlock (&priv->groups_lock);
-
-       if (!contact)
-               return NULL;
-
-       e_contact_add_gdata_entry_xml (contact, entry);
-       g_mutex_lock (&priv->cache_lock);
-       e_book_backend_cache_add_contact (priv->cache, contact);
-       g_mutex_unlock (&priv->cache_lock);
-       e_contact_remove_gdata_entry_xml (contact);
-
-       return contact;
-}
-
 static gboolean
-cache_remove_contact (EBookBackend *backend,
-                      const gchar *uid)
+ebb_google_is_authorized (EBookBackendGoogle *bbgoogle)
 {
-       EBookBackendGooglePrivate *priv;
-       gboolean removed;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle), FALSE);
 
-       g_mutex_lock (&priv->cache_lock);
-       removed = e_book_backend_cache_remove_contact (priv->cache, uid);
-       g_mutex_unlock (&priv->cache_lock);
+       if (!bbgoogle->priv->service)
+               return FALSE;
 
-       return removed;
+       return gdata_service_is_authorized (GDATA_SERVICE (bbgoogle->priv->service));
 }
 
 static gboolean
-cache_has_contact (EBookBackend *backend,
-                   const gchar *uid)
-{
-       EBookBackendGooglePrivate *priv;
-       gboolean has_contact;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_mutex_lock (&priv->cache_lock);
-       has_contact = e_book_backend_cache_check_contact (priv->cache, uid);
-       g_mutex_unlock (&priv->cache_lock);
-
-       return has_contact;
-}
-
-static EContact *
-cache_get_contact (EBookBackend *backend,
-                   const gchar *uid,
-                   GDataEntry **entry)
+ebb_google_request_authorization (EBookBackendGoogle *bbgoogle,
+                                 const ENamedParameters *credentials,
+                                 GCancellable *cancellable,
+                                 GError **error)
 {
-       EBookBackendGooglePrivate *priv;
-       EContact *contact;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_mutex_lock (&priv->cache_lock);
-       contact = e_book_backend_cache_get_contact (priv->cache, uid);
-       g_mutex_unlock (&priv->cache_lock);
-
-       if (contact) {
-               if (entry) {
-                       const gchar *entry_xml, *edit_uri = NULL;
+       /* Make sure we have the GDataService configured
+        * before requesting authorization. */
 
-                       entry_xml = e_contact_get_gdata_entry_xml (contact, &edit_uri);
-                       *entry = GDATA_ENTRY (gdata_parsable_new_from_xml (GDATA_TYPE_CONTACTS_CONTACT, 
entry_xml, -1, NULL));
+       if (!bbgoogle->priv->authorizer) {
+               ESource *source;
+               EGDataOAuth2Authorizer *authorizer;
 
-                       if (*entry) {
-                               GDataLink *edit_link = gdata_link_new (edit_uri, GDATA_LINK_EDIT);
-                               gdata_entry_add_link (*entry, edit_link);
-                               g_object_unref (edit_link);
-                       }
-               }
+               source = e_backend_get_source (E_BACKEND (bbgoogle));
 
-               e_contact_remove_gdata_entry_xml (contact);
+               /* Only OAuth2 is supported with Google Tasks */
+               authorizer = e_gdata_oauth2_authorizer_new (source);
+               bbgoogle->priv->authorizer = GDATA_AUTHORIZER (authorizer);
        }
 
-       return contact;
-}
-
-static void
-cache_get_contacts (EBookBackend *backend,
-                    GQueue *out_contacts)
-{
-       EBookBackendGooglePrivate *priv;
-       GList *list, *link;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_mutex_lock (&priv->cache_lock);
-       list = e_book_backend_cache_get_contacts (
-               priv->cache, "(contains \"x-evolution-any-field\" \"\")");
-       g_mutex_unlock (&priv->cache_lock);
-
-       for (link = list; link != NULL; link = g_list_next (link)) {
-               EContact *contact = E_CONTACT (link->data);
-
-               e_contact_remove_gdata_entry_xml (contact);
-               g_queue_push_tail (out_contacts, g_object_ref (contact));
+       if (E_IS_GDATA_OAUTH2_AUTHORIZER (bbgoogle->priv->authorizer)) {
+               e_gdata_oauth2_authorizer_set_credentials (E_GDATA_OAUTH2_AUTHORIZER 
(bbgoogle->priv->authorizer), credentials);
        }
 
-       g_list_free_full (list, (GDestroyNotify) g_object_unref);
-}
-
-static void
-cache_freeze (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       e_file_cache_freeze_changes (E_FILE_CACHE (priv->cache));
-}
-
-static void
-cache_thaw (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       e_file_cache_thaw_changes (E_FILE_CACHE (priv->cache));
-}
-
-static gchar *
-cache_get_last_update (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       gchar *last_update;
+       if (!bbgoogle->priv->service) {
+               GDataContactsService *contacts_service;
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+               contacts_service = gdata_contacts_service_new (bbgoogle->priv->authorizer);
+               bbgoogle->priv->service = GDATA_SERVICE (contacts_service);
 
-       g_mutex_lock (&priv->cache_lock);
-       last_update = e_book_backend_cache_get_time (priv->cache);
-       g_mutex_unlock (&priv->cache_lock);
+               e_binding_bind_property (
+                       bbgoogle, "proxy-resolver",
+                       bbgoogle->priv->service, "proxy-resolver",
+                       G_BINDING_SYNC_CREATE);
+       }
 
-       return last_update;
-}
+       /* If we're using OAuth tokens, then as far as the backend
+        * is concerned it's always authorized.  The GDataAuthorizer
+        * will take care of everything in the background. */
+       if (!GDATA_IS_CLIENT_LOGIN_AUTHORIZER (bbgoogle->priv->authorizer))
+               return TRUE;
 
-static void
-cache_set_last_update (EBookBackend *backend,
-                       GTimeVal *tv)
-{
-       EBookBackendGooglePrivate *priv;
-       gchar *_time;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       if (tv)
-               _time = g_time_val_to_iso8601 (tv);
-       else
-               _time = NULL;
-
-       g_mutex_lock (&priv->cache_lock);
-       if (tv)
-               e_book_backend_cache_set_time (priv->cache, _time);
-       else
-               e_file_cache_remove_object (E_FILE_CACHE (priv->cache), "last_update_time");
-       g_mutex_unlock (&priv->cache_lock);
-       g_free (_time);
+       /* Otherwise it's up to us to obtain a login secret, but
+          there is currently no way to do it, thus simply fail. */
+       return FALSE;
 }
 
 /* returns whether group changed from the one stored in the cache;
@@ -374,661 +206,476 @@ cache_set_last_update (EBookBackend *backend,
  * use group_name = NULL to remove it from the cache.
  */
 static gboolean
-cache_update_group (EBookBackend *backend,
-                    const gchar *group_id,
-                    const gchar *group_name)
+ebb_google_cache_update_group (EBookBackendGoogle *bbgoogle,
+                              const gchar *group_id,
+                              const gchar *group_name)
 {
-       EBookBackendGooglePrivate *priv;
-       EFileCache *file_cache;
+       EBookCache *book_cache;
        gboolean changed;
-       gchar *key;
+       gchar *key, *old_value;
 
-       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (backend), FALSE);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle), FALSE);
        g_return_val_if_fail (group_id != NULL, FALSE);
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-       key = g_strconcat ("google-group", ":", group_id, NULL);
+       book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (bbgoogle));
+       g_return_val_if_fail (book_cache != NULL, FALSE);
 
-       g_mutex_lock (&priv->cache_lock);
-
-       file_cache = E_FILE_CACHE (priv->cache);
+       key = g_strconcat ("google-group", ":", group_id, NULL);
+       old_value = e_cache_dup_key (E_CACHE (book_cache), key, NULL);
 
        if (group_name) {
-               const gchar *old_value;
-
-               old_value = e_file_cache_get_object (file_cache, key);
                changed = old_value && g_strcmp0 (old_value, group_name) != 0;
 
-               if (!e_file_cache_replace_object (file_cache, key, group_name))
-                       e_file_cache_add_object (file_cache, key, group_name);
+               e_cache_set_key (E_CACHE (book_cache), key, group_name, NULL);
 
                /* Add the category to Evolution’s category list. */
                e_categories_add (group_name, NULL, NULL, TRUE);
        } else {
-               const gchar *old_value;
+               changed = old_value != NULL;
 
-               old_value = e_file_cache_get_object (file_cache, key);
-               changed = e_file_cache_remove_object (file_cache, key);
+               e_cache_set_key (E_CACHE (book_cache), key, NULL, NULL);
 
                /* Remove the category from Evolution’s category list. */
-               if (old_value != NULL) {
+               if (changed)
                        e_categories_remove (old_value);
-               }
        }
 
-       g_mutex_unlock (&priv->cache_lock);
-
+       g_object_unref (book_cache);
+       g_free (old_value);
        g_free (key);
 
        return changed;
 }
 
-static gboolean
-backend_is_authorized (EBookBackend *backend)
+static void
+ebb_google_process_group (GDataEntry *entry,
+                         guint entry_key,
+                         guint entry_count,
+                         gpointer user_data)
 {
-       EBookBackendGooglePrivate *priv;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       if (priv->service == NULL)
-               return FALSE;
-
-       return gdata_service_is_authorized (priv->service);
-}
+       EBookBackendGoogle *bbgoogle = user_data;
+       const gchar *uid, *system_group_id;
+       gchar *name;
+       gboolean is_deleted;
 
-static GCancellable *
-start_operation (EBookBackend *backend,
-                 guint32 opid,
-                 GCancellable *cancellable,
-                 const gchar *message)
-{
-       EBookBackendGooglePrivate *priv;
-       GList *list, *link;
+       g_return_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle));
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+       uid = gdata_entry_get_id (entry);
+       name = e_contact_sanitise_google_group_name (entry);
 
-       /* Insert the operation into the set of active cancellable operations */
-       if (cancellable)
-               g_object_ref (cancellable);
-       else
-               cancellable = g_cancellable_new ();
-       g_hash_table_insert (priv->cancellables, GUINT_TO_POINTER (opid), g_object_ref (cancellable));
+       system_group_id = gdata_contacts_group_get_system_group_id (GDATA_CONTACTS_GROUP (entry));
+       is_deleted = gdata_contacts_group_is_deleted (GDATA_CONTACTS_GROUP (entry));
 
-       /* Send out a status message to each view */
-       list = e_book_backend_list_views (backend);
+       g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
 
-       for (link = list; link != NULL; link = g_list_next (link)) {
-               EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
-               e_data_book_view_notify_progress (view, -1, message);
-       }
+       if (system_group_id) {
+               if (is_deleted) {
+                       gchar *entry_id = g_hash_table_lookup (bbgoogle->priv->system_groups_by_id, 
system_group_id);
+                       g_hash_table_remove (bbgoogle->priv->system_groups_by_entry_id, entry_id);
+                       g_hash_table_remove (bbgoogle->priv->system_groups_by_id, system_group_id);
+               } else {
+                       gchar *entry_id, *system_group_id_dup;
 
-       g_list_free_full (list, (GDestroyNotify) g_object_unref);
+                       entry_id = e_contact_sanitise_google_group_id (uid);
+                       system_group_id_dup = g_strdup (system_group_id);
 
-       return cancellable;
-}
+                       g_hash_table_replace (bbgoogle->priv->system_groups_by_entry_id, entry_id, 
system_group_id_dup);
+                       g_hash_table_replace (bbgoogle->priv->system_groups_by_id, system_group_id_dup, 
entry_id);
+               }
 
-static void
-finish_operation (EBookBackend *backend,
-                  guint32 opid,
-                  const GError *gdata_error)
-{
-       EBookBackendGooglePrivate *priv;
-       GError *book_error = NULL;
+               g_free (name);
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+               /* use evolution's names for google's system groups */
+               name = g_strdup (e_contact_map_google_with_evo_group (system_group_id, TRUE));
 
-       if (gdata_error != NULL) {
-               data_book_error_from_gdata_error (&book_error, gdata_error);
-               g_debug ("Book view query failed: %s", book_error->message);
+               g_warn_if_fail (name != NULL);
+               if (!name)
+                       name = g_strdup (system_group_id);
        }
 
-       if (g_hash_table_remove (priv->cancellables, GUINT_TO_POINTER (opid))) {
-               GList *list, *link;
-
-               list = e_book_backend_list_views (backend);
+       if (is_deleted) {
+               g_hash_table_remove (bbgoogle->priv->groups_by_id, uid);
+               g_hash_table_remove (bbgoogle->priv->groups_by_name, name);
 
-               for (link = list; link != NULL; link = g_list_next (link)) {
-                       EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
-                       e_data_book_view_notify_complete (view, book_error);
-               }
+               bbgoogle->priv->groups_changed = ebb_google_cache_update_group (bbgoogle, uid, NULL) || 
bbgoogle->priv->groups_changed;
+       } else {
+               g_hash_table_replace (bbgoogle->priv->groups_by_id, e_contact_sanitise_google_group_id (uid), 
g_strdup (name));
+               g_hash_table_replace (bbgoogle->priv->groups_by_name, g_strdup (name), 
e_contact_sanitise_google_group_id (uid));
 
-               g_list_free_full (list, (GDestroyNotify) g_object_unref);
+               bbgoogle->priv->groups_changed = ebb_google_cache_update_group (bbgoogle, uid, name) || 
bbgoogle->priv->groups_changed;
        }
 
-       g_clear_error (&book_error);
-}
-
-static void
-process_contact_finish (EBookBackend *backend,
-                        GDataEntry *entry)
-{
-       EContact *new_contact;
-
-       g_debug (G_STRFUNC);
-
-       new_contact = cache_add_contact (backend, entry);
+       g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
-       if (!new_contact)
-               return;
-
-       e_book_backend_notify_update (backend, new_contact);
-
-       g_object_unref (new_contact);
+       g_free (name);
 }
 
-typedef struct {
-       EBookBackend *backend;
-       GCancellable *cancellable;
-       GError *gdata_error;
+static gboolean
+ebb_google_get_groups_sync (EBookBackendGoogle *bbgoogle,
+                           gboolean with_time_constraint,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       GDataQuery *query;
+       GDataFeed *feed;
+       gboolean success;
 
-       /* These two don't need locking; they're only accessed from the main thread. */
-       gboolean update_complete;
-       guint num_contacts_pending_photos;
-} GetContactsData;
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle), FALSE);
+       g_return_val_if_fail (ebb_google_is_authorized (bbgoogle), FALSE);
 
-static void
-check_get_new_contacts_finished (GetContactsData *data)
-{
-       g_debug (G_STRFUNC);
+       g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
 
-       /* Are we finished yet? */
-       if (data->update_complete == FALSE || data->num_contacts_pending_photos > 0) {
-               g_debug (
-                       "Bailing from check_get_new_contacts_finished(): update_complete: %u, 
num_contacts_pending_photos: %u, data: %p",
-                       data->update_complete, data->num_contacts_pending_photos, data);
-               return;
+       /* Build our query, always fetch all of them */
+       query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
+       if (with_time_constraint && bbgoogle->priv->groups_last_update.tv_sec != 0) {
+               gdata_query_set_updated_min (query, bbgoogle->priv->groups_last_update.tv_sec);
+               gdata_contacts_query_set_show_deleted (GDATA_CONTACTS_QUERY (query), TRUE);
        }
 
-       g_debug ("Proceeding with check_get_new_contacts_finished() for data: %p.", data);
-
-       finish_operation (data->backend, -1, data->gdata_error);
+       bbgoogle->priv->groups_changed = FALSE;
 
-       /* Tidy up */
-       g_object_unref (data->cancellable);
-       g_object_unref (data->backend);
-       g_clear_error (&data->gdata_error);
+       /* Run the query synchronously */
+       feed = gdata_contacts_service_query_groups (
+               GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
+               query, cancellable, ebb_google_process_group, bbgoogle, error);
 
-       g_slice_free (GetContactsData, data);
-}
+       success = feed != NULL;
 
-typedef struct {
-       GetContactsData *parent_data;
+       if (success)
+               g_get_current_time (&bbgoogle->priv->groups_last_update);
 
-       GCancellable *cancellable;
-       gulong cancelled_handle;
-} PhotoData;
+       g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
-static void
-process_contact_photo_cancelled_cb (GCancellable *parent_cancellable,
-                                    GCancellable *photo_cancellable)
-{
-       g_debug (G_STRFUNC);
+       g_clear_object (&feed);
+       g_object_unref (query);
 
-       g_cancellable_cancel (photo_cancellable);
+       return success;
 }
 
-static void
-process_contact_photo_cb (GDataContactsContact *gdata_contact,
-                          GAsyncResult *async_result,
-                          PhotoData *data)
+static gboolean
+ebb_google_connect_sync (EBookMetaBackend *meta_backend,
+                        const ENamedParameters *credentials,
+                        ESourceAuthenticationResult *out_auth_result,
+                        gchar **out_certificate_pem,
+                        GTlsCertificateFlags *out_certificate_errors,
+                        GCancellable *cancellable,
+                        GError **error)
 {
-       EBookBackend *backend = data->parent_data->backend;
-       guint8 *photo_data = NULL;
-       gsize photo_length;
-       gchar *photo_content_type = NULL;
-       GError *error = NULL;
-
-       g_debug (G_STRFUNC);
-
-       /* Finish downloading the photo */
-       photo_data = gdata_contacts_contact_get_photo_finish (gdata_contact, async_result, &photo_length, 
&photo_content_type, &error);
+       EBookBackendGoogle *bbgoogle;
+       gboolean success;
+       GError *local_error = NULL;
 
-       if (error == NULL) {
-               EContactPhoto *photo;
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
 
-               /* Success! Create an EContactPhoto and store it on the final GDataContactsContact object so 
it makes it into the cache. */
-               photo = e_contact_photo_new ();
-               photo->type = E_CONTACT_PHOTO_TYPE_INLINED;
-               photo->data.inlined.data = (guchar *) photo_data;
-               photo->data.inlined.length = photo_length;
-               photo->data.inlined.mime_type = photo_content_type;
+       bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
 
-               g_object_set_data_full (G_OBJECT (gdata_contact), "photo", photo, (GDestroyNotify) 
e_contact_photo_free);
+       *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
 
-               photo_data = NULL;
-               photo_content_type = NULL;
-       } else {
-               /* Error. */
-               g_debug ("Downloading contact photo for '%s' failed: %s", gdata_entry_get_id (GDATA_ENTRY 
(gdata_contact)), error->message);
-               g_error_free (error);
-       }
+       if (ebb_google_is_authorized (bbgoogle))
+               return TRUE;
 
-       process_contact_finish (backend, GDATA_ENTRY (gdata_contact));
+       success = ebb_google_request_authorization (bbgoogle, credentials, cancellable, &local_error);
+       if (success)
+               success = gdata_authorizer_refresh_authorization (bbgoogle->priv->authorizer, cancellable, 
&local_error);
 
-       g_free (photo_data);
-       g_free (photo_content_type);
+       if (success)
+               success = ebb_google_get_groups_sync (bbgoogle, FALSE, cancellable, &local_error);
 
-       /* Disconnect from the cancellable. */
-       g_cancellable_disconnect (data->parent_data->cancellable, data->cancelled_handle);
-       g_object_unref (data->cancellable);
+       if (!success) {
+               if (g_error_matches (local_error, GDATA_SERVICE_ERROR, 
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
+                       if (!e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       else
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+               } else {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+                       ebb_google_data_book_error_from_gdata_error (error, local_error);
+               }
 
-       data->parent_data->num_contacts_pending_photos--;
-       check_get_new_contacts_finished (data->parent_data);
+               g_clear_error (&local_error);
+       }
 
-       g_slice_free (PhotoData, data);
+       return success;
 }
 
-static void
-process_contact_cb (GDataEntry *entry,
-                    guint entry_key,
-                    guint entry_count,
-                    GetContactsData *data)
+static gboolean
+ebb_google_disconnect_sync (EBookMetaBackend *meta_backend,
+                           GCancellable *cancellable,
+                           GError **error)
 {
-       EBookBackendGooglePrivate *priv;
-       EBookBackend *backend = data->backend;
-       gboolean is_deleted, is_cached;
-       const gchar *uid;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_debug (G_STRFUNC);
-       uid = gdata_entry_get_id (entry);
-       is_deleted = gdata_contacts_contact_is_deleted (GDATA_CONTACTS_CONTACT (entry));
-
-       is_cached = cache_has_contact (backend, uid);
-       if (is_deleted) {
-               /* Do we have this item in our cache? */
-               if (is_cached) {
-                       cache_remove_contact (backend, uid);
-                       e_book_backend_notify_remove (backend, uid);
-               }
-       } else {
-               gchar *old_photo_etag = NULL;
-               const gchar *new_photo_etag;
-
-               /* Download the contact's photo first, if the contact's uncached or if the photo's been 
updated. */
-               if (is_cached == TRUE) {
-                       EContact *old_contact;
-                       EContactPhoto *photo;
-                       EVCardAttribute *old_attr;
-
-                       old_contact = cache_get_contact (backend, uid, NULL);
-
-                       /* Get the old ETag. */
-                       old_attr = e_vcard_get_attribute (E_VCARD (old_contact), GDATA_PHOTO_ETAG_ATTR);
-                       old_photo_etag = (old_attr != NULL) ? e_vcard_attribute_get_value (old_attr) : NULL;
-
-                       /* Attach the old photo to the new contact. */
-                       photo = e_contact_get (old_contact, E_CONTACT_PHOTO);
-
-                       if (photo != NULL && photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
-                               g_object_set_data_full (G_OBJECT (entry), "photo", photo, (GDestroyNotify) 
e_contact_photo_free);
-                       } else if (photo != NULL) {
-                               e_contact_photo_free (photo);
-                       }
-
-                       g_object_unref (old_contact);
-               }
+       EBookBackendGoogle *bbgoogle;
 
-               new_photo_etag = gdata_contacts_contact_get_photo_etag (GDATA_CONTACTS_CONTACT (entry));
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
 
-               if ((old_photo_etag == NULL && new_photo_etag != NULL) ||
-                   (old_photo_etag != NULL && new_photo_etag != NULL && strcmp (old_photo_etag, 
new_photo_etag) != 0)) {
-                       GCancellable *cancellable;
-                       PhotoData *photo_data;
+       bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
 
-                       photo_data = g_slice_new (PhotoData);
-                       photo_data->parent_data = data;
+       g_clear_object (&bbgoogle->priv->service);
+       g_clear_object (&bbgoogle->priv->authorizer);
 
-                       /* Increment the count of contacts whose photos we're waiting for. */
-                       data->num_contacts_pending_photos++;
+       return TRUE;
+}
 
-                       /* Cancel downloading if the get_new_contacts() operation is cancelled. */
-                       cancellable = g_cancellable_new ();
+static gboolean
+ebb_google_get_changes_sync (EBookMetaBackend *meta_backend,
+                            const gchar *last_sync_tag,
+                            gboolean is_repeat,
+                            gchar **out_new_sync_tag,
+                            gboolean *out_repeat,
+                            GSList **out_created_objects, /* EBookMetaBackendInfo * */
+                            GSList **out_modified_objects, /* EBookMetaBackendInfo * */
+                            GSList **out_removed_objects, /* EBookMetaBackendInfo * */
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       EBookBackendGoogle *bbgoogle;
+       EBookCache *book_cache;
+       gint64 updated_time = 0;
+       GTimeVal last_updated;
+       GDataFeed *feed;
+       GDataContactsQuery *contacts_query;
+       GError *local_error = NULL;
 
-                       photo_data->cancellable = g_object_ref (cancellable);
-                       photo_data->cancelled_handle = g_cancellable_connect (
-                               data->cancellable, (GCallback) process_contact_photo_cancelled_cb,
-                               g_object_ref (cancellable), (GDestroyNotify) g_object_unref);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
 
-                       /* Download the photo. */
-                       gdata_contacts_contact_get_photo_async (
-                               GDATA_CONTACTS_CONTACT (entry),
-                               GDATA_CONTACTS_SERVICE (priv->service), cancellable,
-                               (GAsyncReadyCallback) process_contact_photo_cb, photo_data);
+       bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
 
-                       g_object_unref (cancellable);
-                       g_free (old_photo_etag);
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
 
-                       return;
-               }
+       if (!ebb_google_get_groups_sync (bbgoogle, TRUE, cancellable, error))
+               return FALSE;
 
-               g_free (old_photo_etag);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
 
-               /* Since we're not downloading a photo, add the contact to the cache now. */
-               process_contact_finish (backend, entry);
+       if (!last_sync_tag ||
+           !g_time_val_from_iso8601 (last_sync_tag, &last_updated)) {
+               last_updated.tv_sec = 0;
        }
-}
-
-static void
-get_new_contacts_cb (GDataService *service,
-                     GAsyncResult *result,
-                     GetContactsData *data)
-{
-       EBookBackend *backend = data->backend;
-       GDataFeed *feed;
-       GError *gdata_error = NULL;
 
-       g_debug (G_STRFUNC);
-       feed = gdata_service_query_finish (service, result, &gdata_error);
-       if (feed != NULL) {
-               GList *entries = gdata_feed_get_entries (feed);
-               g_debug ("Feed has %d entries", g_list_length (entries));
+       contacts_query = gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT);
+       if (last_updated.tv_sec > 0 && !bbgoogle->priv->groups_changed) {
+               gdata_query_set_updated_min (GDATA_QUERY (contacts_query), last_updated.tv_sec);
+               gdata_contacts_query_set_show_deleted (contacts_query, TRUE);
        }
 
-       if (feed != NULL)
-               g_object_unref (feed);
+       feed = gdata_contacts_service_query_contacts (GDATA_CONTACTS_SERVICE (bbgoogle->priv->service), 
GDATA_QUERY (contacts_query), cancellable, NULL, NULL, &local_error);
 
-       if (!gdata_error) {
-               /* Finish updating the cache */
-               GTimeVal current_time;
-               g_get_current_time (&current_time);
-               cache_set_last_update (backend, &current_time);
+       if (feed && !g_cancellable_is_cancelled (cancellable) && !local_error) {
+               GList *link;
 
-               e_backend_ensure_source_status_connected (E_BACKEND (backend));
-       }
+               if (gdata_feed_get_updated (feed) > updated_time)
+                       updated_time = gdata_feed_get_updated (feed);
 
-       /* Thaw the cache again */
-       cache_thaw (backend);
+               for (link = gdata_feed_get_entries (feed); link && !g_cancellable_is_cancelled (cancellable); 
link = g_list_next (link)) {
+                       GDataContactsContact *gdata_contact = link->data;
+                       EContact *cached_contact = NULL;
+                       gchar *uid;
 
-       /* Note: The operation's only marked as finished when all the
-        * process_contact_photo_cb() callbacks have been called as well. */
-       data->update_complete = TRUE;
-       data->gdata_error = gdata_error;
-       check_get_new_contacts_finished (data);
-}
-
-static void
-get_new_contacts (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       gchar *last_updated;
-       GTimeVal updated;
-       GDataQuery *query;
-       GCancellable *cancellable;
-       GetContactsData *data;
+                       if (!GDATA_IS_CONTACTS_CONTACT (gdata_contact))
+                               continue;
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+                       uid = g_strdup (gdata_entry_get_id (GDATA_ENTRY (gdata_contact)));
+                       if (!uid || !*uid) {
+                               g_free (uid);
+                               continue;
+                       }
 
-       g_debug (G_STRFUNC);
-       g_return_if_fail (backend_is_authorized (backend));
+                       if (!e_book_cache_get_contact (book_cache, uid, FALSE, &cached_contact, cancellable, 
NULL))
+                               cached_contact = NULL;
 
-       /* Sort out update times */
-       last_updated = cache_get_last_update (backend);
-       g_return_if_fail (last_updated == NULL || g_time_val_from_iso8601 (last_updated, &updated) == TRUE);
-       g_free (last_updated);
+                       if (gdata_contacts_contact_is_deleted (gdata_contact)) {
+                               *out_removed_objects = g_slist_prepend (*out_removed_objects,
+                                       e_book_meta_backend_info_new (uid, NULL, NULL, NULL));
+                       } else {
+                               EContact *new_contact;
 
-       /* Prevent the cache writing each change to disk individually (thawed in get_new_contacts_cb()) */
-       cache_freeze (backend);
+                               if (cached_contact) {
+                                       gchar *old_etag;
 
-       /* Build our query */
-       query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
-       if (last_updated) {
-               gdata_query_set_updated_min (query, updated.tv_sec);
-               gdata_contacts_query_set_show_deleted (GDATA_CONTACTS_QUERY (query), TRUE);
-       }
+                                       old_etag = e_contact_get (cached_contact, E_CONTACT_REV);
 
-       /* Query for new contacts asynchronously */
-       cancellable = start_operation (backend, -1, NULL, _("Querying for updated contacts…"));
-
-       data = g_slice_new (GetContactsData);
-       data->backend = g_object_ref (backend);
-       data->cancellable = g_object_ref (cancellable);
-       data->gdata_error = NULL;
-       data->num_contacts_pending_photos = 0;
-       data->update_complete = FALSE;
-
-       gdata_contacts_service_query_contacts_async (
-               GDATA_CONTACTS_SERVICE (priv->service),
-               query,
-               cancellable,
-               (GDataQueryProgressCallback) process_contact_cb,
-               data,
-               (GDestroyNotify) NULL,
-               (GAsyncReadyCallback) get_new_contacts_cb,
-               data);
-
-       g_object_unref (cancellable);
-       g_object_unref (query);
-}
+                                       if (g_strcmp0 (gdata_entry_get_etag (GDATA_ENTRY (gdata_contact)), 
old_etag) == 0) {
+                                               g_object_unref (cached_contact);
+                                               g_free (old_etag);
+                                               g_free (uid);
+                                               continue;
+                                       }
 
-static void
-process_group (GDataEntry *entry,
-               guint entry_key,
-               guint entry_count,
-               EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       const gchar *uid, *system_group_id;
-       gchar *name;
-       gboolean is_deleted;
+                                       g_free (old_etag);
+                               }
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+                               g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+                               new_contact = e_contact_new_from_gdata_entry (GDATA_ENTRY (gdata_contact),
+                                       bbgoogle->priv->groups_by_id, 
bbgoogle->priv->system_groups_by_entry_id);
+                               g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
-       g_debug (G_STRFUNC);
-       uid = gdata_entry_get_id (entry);
-       name = e_contact_sanitise_google_group_name (entry);
+                               if (new_contact) {
+                                       const gchar *revision, *photo_etag;
+                                       gchar *object, *extra;
 
-       system_group_id = gdata_contacts_group_get_system_group_id (GDATA_CONTACTS_GROUP (entry));
-       is_deleted = gdata_contacts_group_is_deleted (GDATA_CONTACTS_GROUP (entry));
+                                       photo_etag = gdata_contacts_contact_get_photo_etag (gdata_contact);
+                                       if (photo_etag && cached_contact) {
+                                               gchar *old_photo_etag;
 
-       g_rec_mutex_lock (&priv->groups_lock);
+                                               old_photo_etag = e_vcard_util_dup_x_attribute (E_VCARD 
(cached_contact), E_GOOGLE_X_PHOTO_ETAG);
+                                               if (g_strcmp0 (photo_etag, old_photo_etag) == 0) {
+                                                       EContactPhoto *photo;
 
-       if (system_group_id) {
-               g_debug ("Processing %ssystem group %s, %s", is_deleted ? "(deleted) " : "", system_group_id, 
uid);
+                                                       /* To not download it again, when it's already 
available locally */
+                                                       photo_etag = NULL;
 
-               if (is_deleted) {
-                       gchar *entry_id = g_hash_table_lookup (priv->system_groups_by_id, system_group_id);
-                       g_hash_table_remove (priv->system_groups_by_entry_id, entry_id);
-                       g_hash_table_remove (priv->system_groups_by_id, system_group_id);
-               } else {
-                       gchar *entry_id, *system_group_id_dup;
+                                                       /* Copy the photo attribute to the changed contact */
+                                                       photo = e_contact_get (cached_contact, 
E_CONTACT_PHOTO);
+                                                       e_contact_set (new_contact, E_CONTACT_PHOTO, photo);
 
-                       entry_id = e_contact_sanitise_google_group_id (uid);
-                       system_group_id_dup = g_strdup (system_group_id);
+                                                       e_contact_photo_free (photo);
+                                               }
 
-                       g_hash_table_replace (priv->system_groups_by_entry_id, entry_id, system_group_id_dup);
-                       g_hash_table_replace (priv->system_groups_by_id, system_group_id_dup, entry_id);
-               }
+                                               g_free (old_photo_etag);
+                                       }
 
-               g_free (name);
+                                       if (photo_etag) {
+                                               guint8 *photo_data;
+                                               gsize photo_length = 0;
+                                               gchar *photo_content_type = NULL;
+                                               GError *local_error2 = NULL;
 
-               /* use evolution's names for google's system groups */
-               name = g_strdup (e_contact_map_google_with_evo_group (system_group_id, TRUE));
+                                               photo_data = gdata_contacts_contact_get_photo (gdata_contact, 
GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
+                                                       &photo_length, &photo_content_type, cancellable, 
&local_error2);
 
-               g_warn_if_fail (name != NULL);
-               if (!name)
-                       name = g_strdup (system_group_id);
-       }
+                                               if (!local_error2) {
+                                                       EContactPhoto *photo;
 
-       if (is_deleted) {
-               g_debug ("Processing (deleting) group %s, %s", uid, name);
-               g_hash_table_remove (priv->groups_by_id, uid);
-               g_hash_table_remove (priv->groups_by_name, name);
+                                                       photo = e_contact_photo_new ();
+                                                       photo->type = E_CONTACT_PHOTO_TYPE_INLINED;
+                                                       photo->data.inlined.data = (guchar *) photo_data;
+                                                       photo->data.inlined.length = photo_length;
+                                                       photo->data.inlined.mime_type = photo_content_type;
 
-               priv->groups_changed = cache_update_group (backend, uid, NULL) || priv->groups_changed;
-       } else {
-               g_debug ("Processing group %s, %s", uid, name);
-               g_hash_table_replace (priv->groups_by_id, e_contact_sanitise_google_group_id (uid), g_strdup 
(name));
-               g_hash_table_replace (priv->groups_by_name, g_strdup (name), 
e_contact_sanitise_google_group_id (uid));
+                                                       e_contact_set (E_CONTACT (new_contact), 
E_CONTACT_PHOTO, photo);
 
-               priv->groups_changed = cache_update_group (backend, uid, name) || priv->groups_changed;
-       }
+                                                       e_contact_photo_free (photo);
 
-       g_rec_mutex_unlock (&priv->groups_lock);
+                                                       /* Read of the photo frees previously obtained 
photo_etag */
+                                                       photo_etag = gdata_contacts_contact_get_photo_etag 
(gdata_contact);
 
-       g_free (name);
-}
+                                                       e_vcard_util_set_x_attribute (E_VCARD (new_contact), 
E_GOOGLE_X_PHOTO_ETAG, photo_etag);
+                                               } else {
+                                                       g_debug ("%s: Downloading contact photo for '%s' 
failed: %s", G_STRFUNC,
+                                                               gdata_entry_get_id (GDATA_ENTRY 
(gdata_contact)), local_error2->message);
 
-static void
-get_groups_cb (GDataService *service,
-               GAsyncResult *result,
-               EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       GDataFeed *feed;
-       GError *gdata_error = NULL;
+                                                       g_clear_error (&local_error2);
+                                               }
+                                       }
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+                                       revision = gdata_entry_get_etag (GDATA_ENTRY (gdata_contact));
+                                       e_contact_set (new_contact, E_CONTACT_REV, revision);
+                                       object = e_vcard_to_string (E_VCARD (new_contact), 
EVC_FORMAT_VCARD_30);
+                                       extra = gdata_parsable_get_xml (GDATA_PARSABLE (gdata_contact));
 
-       g_debug (G_STRFUNC);
-       feed = gdata_service_query_finish (service, result, &gdata_error);
-       if (feed != NULL) {
-               GList *entries = gdata_feed_get_entries (feed);
-               g_debug ("Group feed has %d entries", g_list_length (entries));
-       }
+                                       if (cached_contact) {
+                                               *out_modified_objects = g_slist_prepend 
(*out_modified_objects,
+                                                       e_book_meta_backend_info_new (uid, revision, object, 
extra));
+                                       } else {
+                                               *out_created_objects = g_slist_prepend (*out_created_objects,
+                                                       e_book_meta_backend_info_new (uid, revision, object, 
extra));
+                                       }
 
-       if (feed != NULL)
-               g_object_unref (feed);
+                                       g_free (object);
+                                       g_free (extra);
+                               }
 
-       if (!gdata_error) {
-               /* Update the update time */
-               g_rec_mutex_lock (&priv->groups_lock);
-               g_get_current_time (&(priv->groups_last_update));
-               g_rec_mutex_unlock (&priv->groups_lock);
+                               g_clear_object (&new_contact);
+                       }
 
-               e_backend_ensure_source_status_connected (E_BACKEND (backend));
+                       g_clear_object (&cached_contact);
+                       g_free (uid);
+               }
        }
 
-       finish_operation (backend, -2, gdata_error);
+       g_clear_object (&contacts_query);
+       g_clear_object (&feed);
 
-       g_rec_mutex_lock (&priv->groups_lock);
+       if (!g_cancellable_is_cancelled (cancellable) && !local_error) {
+               last_updated.tv_sec = updated_time;
+               last_updated.tv_usec = 0;
 
-       if (priv->groups_changed) {
-               priv->groups_changed = FALSE;
+               *out_new_sync_tag = g_time_val_to_iso8601 (&last_updated);
+       }
 
-               g_rec_mutex_unlock (&priv->groups_lock);
+       g_clear_object (&book_cache);
 
-               /* do the update for all contacts, like with an empty cache */
-               cache_set_last_update (backend, NULL);
-               get_new_contacts (backend);
-       } else {
-               g_rec_mutex_unlock (&priv->groups_lock);
+       if (local_error) {
+               g_propagate_error (error, local_error);
+               return FALSE;
        }
 
-       g_object_unref (backend);
-
-       g_clear_error (&gdata_error);
+       return TRUE;
 }
 
-static void
-get_groups_and_update_cache_cb (GDataService *service,
-                               GAsyncResult *result,
-                               EBookBackend *backend)
+static gboolean
+ebb_google_load_contact_sync (EBookMetaBackend *meta_backend,
+                             const gchar *uid,
+                             const gchar *extra,
+                             EContact **out_contact,
+                             gchar **out_extra,
+                             GCancellable *cancellable,
+                             GError **error)
 {
-       g_object_ref (backend);
+       EBookBackendGoogle *bbgoogle;
 
-       get_groups_cb (service, result, backend);
-       get_new_contacts (backend);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_contact != NULL, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
 
-       g_object_unref (backend);
-}
+       bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
 
-static void
-get_groups (EBookBackend *backend,
-           gboolean and_update_cache)
-{
-       EBookBackendGooglePrivate *priv;
-       GDataQuery *query;
-       GCancellable *cancellable;
+       /* Only "load" preloaded during save, otherwise fail with an error,
+          because the backend provides objects within get_changes_sync() */
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+       if (bbgoogle->priv->preloaded) {
+               EContact *contact;
 
-       g_debug (G_STRFUNC);
-       g_return_if_fail (backend_is_authorized (backend));
+               contact = g_hash_table_lookup (bbgoogle->priv->preloaded, uid);
+               if (contact) {
+                       *out_contact = e_contact_duplicate (contact);
 
-       g_rec_mutex_lock (&priv->groups_lock);
+                       g_hash_table_remove (bbgoogle->priv->preloaded, uid);
 
-       /* Build our query */
-       query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
-       if (priv->groups_last_update.tv_sec != 0 || priv->groups_last_update.tv_usec != 0) {
-               gdata_query_set_updated_min (query, priv->groups_last_update.tv_sec);
-               gdata_contacts_query_set_show_deleted (GDATA_CONTACTS_QUERY (query), TRUE);
+                       return TRUE;
+               }
        }
 
-       priv->groups_changed = FALSE;
-
-       g_rec_mutex_unlock (&priv->groups_lock);
-
-       g_object_ref (backend);
-
-       /* Run the query asynchronously */
-       cancellable = start_operation (backend, -2, NULL, _("Querying for updated groups…"));
-       gdata_contacts_service_query_groups_async (
-               GDATA_CONTACTS_SERVICE (priv->service),
-               query,
-               cancellable,
-               (GDataQueryProgressCallback) process_group,
-               backend,
-               (GDestroyNotify) NULL,
-               (GAsyncReadyCallback) (and_update_cache ? get_groups_and_update_cache_cb : get_groups_cb),
-               backend);
-
-       g_object_unref (cancellable);
-       g_object_unref (query);
-}
+       g_set_error_literal (error, E_BOOK_CLIENT_ERROR, E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
+               e_book_client_error_to_string (E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
 
-static void
-get_groups_sync (EBookBackend *backend,
-                 GCancellable *cancellable,
-                GError **error)
-{
-       EBookBackendGooglePrivate *priv;
-       GDataQuery *query;
-       GDataFeed *feed;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_debug (G_STRFUNC);
-       g_return_if_fail (backend_is_authorized (backend));
-
-       /* Build our query, always fetch all of them */
-       query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
-
-       /* Run the query synchronously */
-       feed = gdata_contacts_service_query_groups (
-               GDATA_CONTACTS_SERVICE (priv->service),
-               query,
-               cancellable,
-               (GDataQueryProgressCallback) process_group,
-               backend,
-               error);
-
-       if (feed)
-               g_object_unref (feed);
-
-       g_object_unref (query);
+       return FALSE;
 }
 
 static gchar *
-create_group (EBookBackend *backend,
-              const gchar *category_name,
-              GError **error)
+ebb_google_create_group_sync (EBookBackendGoogle *bbgoogle,
+                             const gchar *category_name,
+                             GCancellable *cancellable,
+                             GError **error)
 {
-       EBookBackendGooglePrivate *priv;
        GDataEntry *group, *new_group;
-       gchar *uid;
        const gchar *system_group_id;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+       gchar *uid;
 
        system_group_id = e_contact_map_google_with_evo_group (category_name, FALSE);
        if (system_group_id) {
                gchar *group_entry_id;
 
-               g_rec_mutex_lock (&priv->groups_lock);
-               group_entry_id = g_strdup (g_hash_table_lookup (priv->system_groups_by_id, system_group_id));
-               g_rec_mutex_unlock (&priv->groups_lock);
+               g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+               group_entry_id = g_strdup (g_hash_table_lookup (bbgoogle->priv->system_groups_by_id, 
system_group_id));
+               g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
                g_return_val_if_fail (group_entry_id != NULL, NULL);
 
@@ -1038,14 +685,12 @@ create_group (EBookBackend *backend,
        group = GDATA_ENTRY (gdata_contacts_group_new (NULL));
 
        gdata_entry_set_title (group, category_name);
-       g_debug ("Creating group %s", category_name);
 
        /* Insert the new group */
-       new_group = GDATA_ENTRY (
-               gdata_contacts_service_insert_group (
-                       GDATA_CONTACTS_SERVICE (priv->service),
+       new_group = GDATA_ENTRY (gdata_contacts_service_insert_group (
+                       GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
                        GDATA_CONTACTS_GROUP (group),
-                       NULL, error));
+                       cancellable, error));
        g_object_unref (group);
 
        if (new_group == NULL)
@@ -1054,217 +699,60 @@ create_group (EBookBackend *backend,
        /* Add the new group to the group mappings */
        uid = g_strdup (gdata_entry_get_id (new_group));
 
-       g_rec_mutex_lock (&priv->groups_lock);
-       g_hash_table_replace (priv->groups_by_id, e_contact_sanitise_google_group_id (uid), g_strdup 
(category_name));
-       g_hash_table_replace (priv->groups_by_name, g_strdup (category_name), 
e_contact_sanitise_google_group_id (uid));
-       g_rec_mutex_unlock (&priv->groups_lock);
+       g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+       g_hash_table_replace (bbgoogle->priv->groups_by_id, e_contact_sanitise_google_group_id (uid), 
g_strdup (category_name));
+       g_hash_table_replace (bbgoogle->priv->groups_by_name, g_strdup (category_name), 
e_contact_sanitise_google_group_id (uid));
+       g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
        g_object_unref (new_group);
 
        /* Update the cache. */
-       cache_update_group (backend, uid, category_name);
-
-       g_debug ("...got UID %s", uid);
+       ebb_google_cache_update_group (bbgoogle, uid, category_name);
 
        return uid;
 }
 
-static gchar *
-_create_group (const gchar *category_name,
-               gpointer user_data,
-               GError **error)
-{
-       return create_group (E_BOOK_BACKEND (user_data), category_name, error);
-}
-
-static void
-refresh_local_cache_cb (ESource *source,
-                        gpointer user_data)
-{
-       EBookBackend *backend = user_data;
-
-       g_debug ("Invoking cache refresh");
-
-       /* The TRUE means the cache update will be run immediately
-          after groups are updated */
-       get_groups (backend, TRUE);
-}
-
-static void
-cache_refresh_if_needed (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       gboolean is_online;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_debug (G_STRFUNC);
-
-       is_online = e_backend_get_online (E_BACKEND (backend));
-
-       if (!is_online || !backend_is_authorized (backend)) {
-               g_debug ("We are not connected to Google%s.", (!is_online) ? " (offline mode)" : "");
-               return;
-       }
-
-       if (!priv->refresh_id) {
-               /* Update the cache asynchronously */
-               refresh_local_cache_cb (NULL, backend);
-
-               priv->refresh_id = e_source_refresh_add_timeout (
-                       e_backend_get_source (E_BACKEND (backend)),
-                       NULL,
-                       refresh_local_cache_cb,
-                       backend,
-                       NULL);
-       } else {
-               g_rec_mutex_lock (&priv->groups_lock);
-               if (g_hash_table_size (priv->system_groups_by_id) == 0) {
-                       g_rec_mutex_unlock (&priv->groups_lock);
-                       get_groups (backend, FALSE);
-               } else {
-                       g_rec_mutex_unlock (&priv->groups_lock);
-               }
-       }
-
-       return;
-}
-
-#if !GDATA_CHECK_VERSION(0,15,0)
-static void
-fallback_set_proxy_uri (EBookBackend *backend)
-{
-       EBookBackendGooglePrivate *priv;
-       GProxyResolver *proxy_resolver;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       proxy_resolver = e_book_backend_ref_proxy_resolver (backend);
-
-       if (proxy_resolver != NULL) {
-               SoupURI *proxy_uri = NULL;
-               gchar **proxies;
-
-               /* Don't worry about errors since this is a
-                * fallback function.  It works if it works. */
-               proxies = g_proxy_resolver_lookup (
-                       proxy_resolver, URI_GET_CONTACTS, NULL, NULL);
-
-               if (proxies != NULL && strcmp (proxies[0], "direct://") != 0) {
-                       proxy_uri = soup_uri_new (proxies[0]);
-                       g_strfreev (proxies);
-               }
-
-               if (proxy_uri != NULL) {
-                       gdata_service_set_proxy_uri (priv->service, proxy_uri);
-                       soup_uri_free (proxy_uri);
-               }
-
-               g_object_unref (proxy_resolver);
-       }
-}
-#endif
-
 static gboolean
-connect_without_password (EBookBackend *backend,
-                         GCancellable *cancellable,
-                         GError **error)
-{
-       ESource *source;
-       ESourceAuthentication *extension;
-       EGDataOAuth2Authorizer *authorizer;
-       gchar *method;
-       gboolean is_oauth2_method;
-       EBookBackendGooglePrivate *priv;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       source = e_backend_get_source (E_BACKEND (backend));
-       extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
-       method = e_source_authentication_dup_method (extension);
-       is_oauth2_method = g_strcmp0 (method, "OAuth2") == 0;
-       g_free (method);
-
-       /* Make sure we have the GDataService configured
-        * before requesting authorization. */
-
-       if (priv->authorizer == NULL) {
-               authorizer = e_gdata_oauth2_authorizer_new (source);
-               priv->authorizer = GDATA_AUTHORIZER (authorizer);
-       }
-
-       if (priv->service == NULL) {
-               GDataContactsService *contacts_service;
-
-               contacts_service =
-                       gdata_contacts_service_new (priv->authorizer);
-               priv->service = GDATA_SERVICE (contacts_service);
-
-#if GDATA_CHECK_VERSION(0,15,0)
-               /* proxy-resolver was added in 0.15.0.
-                * (https://bugzilla.gnome.org/709758) */
-               e_binding_bind_property (
-                       backend, "proxy-resolver",
-                       priv->service, "proxy-resolver",
-                       G_BINDING_SYNC_CREATE);
-#else
-               /* XXX The fallback approach doesn't listen for proxy
-                *     setting changes, but really how often do proxy
-                *     settings change? */
-               fallback_set_proxy_uri (backend);
-#endif
-       }
-
-       /* If we're using OAuth tokens, then as far as the backend
-        * is concerned it's always authorized.  The GDataAuthorizer
-        * will take care of everything in the background. */
-       if (is_oauth2_method)
-               return TRUE;
-
-       /* Otherwise it's up to us to obtain an OAuth 2 token. */
-       return FALSE;
-}
-
-typedef enum {
-       LEAVE_PHOTO,
-       ADD_PHOTO,
-       REMOVE_PHOTO,
-       UPDATE_PHOTO,
-} PhotoOperation;
-
-static PhotoOperation
-pick_photo_operation (EContact *old_contact,
-                      EContact *new_contact)
+ebb_google_photo_changed (EBookMetaBackend *meta_backend,
+                         EContact *old_contact,
+                         EContact *new_contact,
+                         GCancellable *cancellable)
 {
+       EContact *old_contact_copy = NULL;
        EContactPhoto *old_photo;
        EContactPhoto *new_photo;
-       gboolean have_old_photo;
-       gboolean have_new_photo;
-       PhotoOperation photo_operation = LEAVE_PHOTO;
+       gboolean changed = FALSE;
 
        old_photo = e_contact_get (old_contact, E_CONTACT_PHOTO);
        new_photo = e_contact_get (new_contact, E_CONTACT_PHOTO);
 
-       have_old_photo =
-               (old_photo != NULL) &&
-               (old_photo->type == E_CONTACT_PHOTO_TYPE_INLINED);
+       if (!old_photo && new_photo)
+               changed = TRUE;
+
+       if (old_photo && !new_photo)
+               changed = TRUE;
 
-       have_new_photo =
-               (new_photo != NULL) &&
-               (new_photo->type == E_CONTACT_PHOTO_TYPE_INLINED);
+       /* old_photo comes from cache, thus it's always URI (to local file or elsewhere),
+          while the new_photo is to be saved, which is always inlined. */
+       if (!changed && old_photo && new_photo &&
+           old_photo->type == E_CONTACT_PHOTO_TYPE_URI &&
+           new_photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
+               e_contact_photo_free (old_photo);
+               old_photo = NULL;
 
-       if (!have_old_photo && have_new_photo)
-               photo_operation = ADD_PHOTO;
+               old_contact_copy = e_contact_duplicate (old_contact);
 
-       if (have_old_photo && !have_new_photo)
-               photo_operation = REMOVE_PHOTO;
+               if (e_book_meta_backend_inline_local_photos_sync (meta_backend, old_contact_copy, 
cancellable, NULL))
+                       old_photo = e_contact_get (old_contact_copy, E_CONTACT_PHOTO);
+       }
 
-       if (have_old_photo && have_new_photo) {
+       if (old_photo && new_photo &&
+           old_photo->type == E_CONTACT_PHOTO_TYPE_INLINED &&
+           new_photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
                guchar *old_data;
                guchar *new_data;
                gsize old_length;
                gsize new_length;
-               gboolean changed;
 
                old_data = old_photo->data.inlined.data;
                new_data = new_photo->data.inlined.data;
@@ -1275,36 +763,30 @@ pick_photo_operation (EContact *old_contact,
                changed =
                        (old_length != new_length) ||
                        (memcmp (old_data, new_data, old_length) != 0);
-
-               if (changed)
-                       photo_operation = UPDATE_PHOTO;
        }
 
-       if (old_photo != NULL)
-               e_contact_photo_free (old_photo);
-
-       if (new_photo != NULL)
-               e_contact_photo_free (new_photo);
+       e_contact_photo_free (old_photo);
+       e_contact_photo_free (new_photo);
+       g_clear_object (&old_contact_copy);
 
-       return photo_operation;
+       return changed;
 }
 
 static GDataEntry *
-update_contact_photo (GDataContactsContact *contact,
-                      GDataContactsService *service,
-                      EContactPhoto *photo,
-                      GCancellable *cancellable,
-                      GError **error)
+ebb_google_update_contact_photo_sync (GDataContactsContact *contact,
+                                     GDataContactsService *service,
+                                     EContactPhoto *photo,
+                                     GCancellable *cancellable,
+                                     GError **error)
 {
        GDataAuthorizationDomain *authorization_domain;
-       GDataEntry *new_contact = NULL;
+       GDataEntry *gdata_contact = NULL;
        const gchar *content_type;
        const guint8 *photo_data;
        gsize photo_length;
        gboolean success;
 
-       authorization_domain =
-               gdata_contacts_service_get_primary_authorization_domain ();
+       authorization_domain = gdata_contacts_service_get_primary_authorization_domain ();
 
        if (photo != NULL) {
                photo_data = (guint8 *) photo->data.inlined.data;
@@ -1325,7 +807,7 @@ update_contact_photo (GDataContactsContact *contact,
        if (success) {
                /* Setting the photo changes the contact's ETag,
                 * so query for the contact to obtain its new ETag. */
-               new_contact = gdata_service_query_single_entry (
+               gdata_contact = gdata_service_query_single_entry (
                        GDATA_SERVICE (service),
                        authorization_domain,
                        gdata_entry_get_id (GDATA_ENTRY (contact)),
@@ -1333,146 +815,231 @@ update_contact_photo (GDataContactsContact *contact,
                        cancellable, error);
        }
 
-       return new_contact;
+       return gdata_contact;
 }
 
-static void
-google_cancel_all_operations (EBookBackend *backend)
+static gboolean
+ebb_google_save_contact_sync (EBookMetaBackend *meta_backend,
+                             gboolean overwrite_existing,
+                             EConflictResolution conflict_resolution,
+                             /* const */ EContact *contact,
+                             const gchar *extra,
+                             gchar **out_new_uid,
+                             gchar **out_new_extra,
+                             GCancellable *cancellable,
+                             GError **error)
 {
-       EBookBackendGooglePrivate *priv;
-       GHashTableIter iter;
-       gpointer opid_ptr;
-       GCancellable *cancellable;
+       EBookBackendGoogle *bbgoogle;
+       EBookCache *book_cache;
+       GDataEntry *entry = NULL;
+       GDataContactsContact *gdata_contact;
+       EContact *cached_contact = NULL;
+       EContact *new_contact;
+       const gchar *uid;
+       EContactPhoto *photo;
+       gboolean photo_changed;
+       GError *local_error = NULL;
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
+       g_return_val_if_fail (out_new_extra != NULL, FALSE);
 
-       g_debug (G_STRFUNC);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
 
-       if (!priv->cancellables)
-               return;
+       bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
 
-       /* Cancel all active operations */
-       g_hash_table_iter_init (&iter, priv->cancellables);
-       while (g_hash_table_iter_next (&iter, &opid_ptr, (gpointer *) &cancellable)) {
-               g_cancellable_cancel (cancellable);
+       if (!overwrite_existing || !e_book_cache_get_contact (book_cache, e_contact_get_const (contact, 
E_CONTACT_UID),
+               FALSE, &cached_contact, cancellable, NULL)) {
+               cached_contact = NULL;
        }
-}
 
-static void
-e_book_backend_google_notify_online_cb (EBookBackend *backend,
-                                        GParamSpec *pspec)
-{
-       EBookBackendGooglePrivate *priv;
-       ESource *source;
-       gboolean is_online;
+       if (extra && *extra)
+               entry = GDATA_ENTRY (gdata_parsable_new_from_xml (GDATA_TYPE_CONTACTS_CONTACT, extra, -1, 
NULL));
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+       g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
 
-       g_debug (G_STRFUNC);
+       /* Ensure the system groups have been fetched. */
+       if (g_hash_table_size (bbgoogle->priv->system_groups_by_id) == 0)
+               ebb_google_get_groups_sync (bbgoogle, FALSE, cancellable, NULL);
+
+       if (overwrite_existing || entry) {
+               if (gdata_entry_update_from_e_contact (entry, contact, FALSE,
+                       bbgoogle->priv->groups_by_name,
+                       bbgoogle->priv->system_groups_by_id,
+                       ebb_google_create_group_sync,
+                       bbgoogle,
+                       cancellable)) {
+                       overwrite_existing = TRUE;
+               } else {
+                       g_clear_object (&entry);
+               }
+       } else {
+               /* Build the GDataEntry from the vCard */
+               entry = gdata_entry_new_from_e_contact (
+                       contact,
+                       bbgoogle->priv->groups_by_name,
+                       bbgoogle->priv->system_groups_by_id,
+                       ebb_google_create_group_sync,
+                       bbgoogle,
+                       cancellable);
+       }
 
-       is_online = e_backend_get_online (E_BACKEND (backend));
-       source = e_backend_get_source (E_BACKEND (backend));
+       g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
-       if (is_online && e_book_backend_is_opened (backend)) {
-               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+       photo_changed = cached_contact && ebb_google_photo_changed (meta_backend, cached_contact, contact, 
cancellable);
 
-               if (connect_without_password (backend, NULL, NULL)) {
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
+       g_clear_object (&cached_contact);
+       g_clear_object (&book_cache);
 
-                       e_book_backend_set_writable (backend, TRUE);
-                       cache_refresh_if_needed (backend);
-               } else {
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+       if (!entry) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Object 
to save is not a valid vCard")));
+               return FALSE;
+       }
 
-                       e_backend_schedule_credentials_required (E_BACKEND (backend), 
E_SOURCE_CREDENTIALS_REASON_REQUIRED,
-                               NULL, 0, NULL, NULL, G_STRFUNC);
-               }
+       if (overwrite_existing) {
+               gdata_contact = GDATA_CONTACTS_CONTACT (gdata_service_update_entry (
+                       bbgoogle->priv->service,
+                       gdata_contacts_service_get_primary_authorization_domain (),
+                       entry, cancellable, &local_error));
        } else {
-               /* Going offline, so cancel all running operations */
-               google_cancel_all_operations (backend);
+               gdata_contact = gdata_contacts_service_insert_contact (
+                       GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
+                       GDATA_CONTACTS_CONTACT (entry),
+                       cancellable, &local_error);
+       }
+
+       photo = g_object_steal_data (G_OBJECT (entry), "photo");
 
-               /* Mark the book as unwriteable if we're going offline,
-                * but don't do the inverse when we go online;
-                * e_book_backend_google_authenticate_user() will mark us
-                * as writeable again once the user's authenticated again. */
-               e_book_backend_set_writable (backend, FALSE);
+       g_object_unref (entry);
 
-               if (e_source_get_connection_status (source) != E_SOURCE_CONNECTION_STATUS_DISCONNECTED)
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+       if (!gdata_contact) {
+               ebb_google_data_book_error_from_gdata_error (error, local_error);
+               g_clear_error (&local_error);
+               e_contact_photo_free (photo);
 
-               /* We can free our service. */
-               g_clear_object (&priv->service);
+               return FALSE;
        }
-}
 
-static void
-book_backend_google_dispose (GObject *object)
-{
-       EBookBackendGooglePrivate *priv;
+       if (photo_changed) {
+               entry = ebb_google_update_contact_photo_sync (gdata_contact, GDATA_CONTACTS_SERVICE 
(bbgoogle->priv->service), photo, cancellable, &local_error);
+               if (!entry) {
+                       ebb_google_data_book_error_from_gdata_error (error, local_error);
+                       g_clear_error (&local_error);
+                       e_contact_photo_free (photo);
+                       g_clear_object (&gdata_contact);
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (object);
+                       return FALSE;
+               }
 
-       g_debug (G_STRFUNC);
+               g_object_unref (gdata_contact);
+               gdata_contact = GDATA_CONTACTS_CONTACT (entry);
+       }
 
-       /* Cancel all outstanding operations */
-       google_cancel_all_operations (E_BOOK_BACKEND (object));
+       g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+       new_contact = e_contact_new_from_gdata_entry (GDATA_ENTRY (gdata_contact),
+               bbgoogle->priv->groups_by_id,
+               bbgoogle->priv->system_groups_by_entry_id);
+       g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
 
-       if (priv->refresh_id > 0) {
-               e_source_refresh_remove_timeout (
-                       e_backend_get_source (E_BACKEND (object)),
-                       priv->refresh_id);
-               priv->refresh_id = 0;
+       if (!new_contact) {
+               g_object_unref (gdata_contact);
+               e_contact_photo_free (photo);
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Failed 
to create contact from returned server data")));
+               return FALSE;
        }
 
-       g_clear_object (&priv->service);
-       g_clear_object (&priv->authorizer);
-       g_clear_object (&priv->cache);
+       e_contact_set (new_contact, E_CONTACT_PHOTO, photo);
+       e_vcard_util_set_x_attribute (E_VCARD (new_contact), E_GOOGLE_X_PHOTO_ETAG, 
gdata_contacts_contact_get_photo_etag (gdata_contact));
 
-       /* Chain up to parent's dispose() method. */
-       G_OBJECT_CLASS (e_book_backend_google_parent_class)->dispose (object);
+       *out_new_extra = gdata_parsable_get_xml (GDATA_PARSABLE (gdata_contact));
+
+       g_object_unref (gdata_contact);
+
+       e_contact_photo_free (photo);
+
+       uid = e_contact_get_const (new_contact, E_CONTACT_UID);
+
+       if (!uid) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Server 
returned contact without UID")));
+
+               g_object_unref (new_contact);
+               g_free (*out_new_extra);
+               *out_new_extra = NULL;
+
+               return FALSE;
+       }
+
+       if (bbgoogle->priv->preloaded) {
+               *out_new_uid = g_strdup (uid);
+               g_hash_table_insert (bbgoogle->priv->preloaded, g_strdup (uid), new_contact);
+       } else {
+               g_object_unref (new_contact);
+       }
+
+       return TRUE;
 }
 
-static void
-book_backend_google_finalize (GObject *object)
+static gboolean
+ebb_google_remove_contact_sync (EBookMetaBackend *meta_backend,
+                               EConflictResolution conflict_resolution,
+                               const gchar *uid,
+                               const gchar *extra,
+                               const gchar *object,
+                               GCancellable *cancellable,
+                               GError **error)
 {
-       EBookBackendGooglePrivate *priv;
+       EBookBackendGoogle *bbgoogle;
+       GDataEntry *entry;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (extra != NULL, FALSE);
+
+       entry = GDATA_ENTRY (gdata_parsable_new_from_xml (GDATA_TYPE_CONTACTS_CONTACT, extra, -1, NULL));
+       if (!entry) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, NULL));
+               return FALSE;
+       }
 
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (object);
+       bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
 
-       g_debug (G_STRFUNC);
+       if (!gdata_service_delete_entry (bbgoogle->priv->service,
+               gdata_contacts_service_get_primary_authorization_domain (), entry,
+               cancellable, &local_error)) {
+               ebb_google_data_book_error_from_gdata_error (error, local_error);
+               g_error_free (local_error);
+               g_object_unref (entry);
 
-       if (priv->cancellables) {
-               g_hash_table_destroy (priv->groups_by_id);
-               g_hash_table_destroy (priv->groups_by_name);
-               g_hash_table_destroy (priv->system_groups_by_entry_id);
-               g_hash_table_destroy (priv->system_groups_by_id);
-               g_hash_table_destroy (priv->cancellables);
+               return FALSE;
        }
 
-       g_mutex_clear (&priv->cache_lock);
-       g_rec_mutex_clear (&priv->groups_lock);
+       g_object_unref (entry);
 
-       /* Chain up to parent's finalize() method. */
-       G_OBJECT_CLASS (e_book_backend_google_parent_class)->finalize (object);
+       return TRUE;
 }
 
 static gchar *
-book_backend_google_get_backend_property (EBookBackend *backend,
-                                            const gchar *prop_name)
+ebb_google_get_backend_property (EBookBackend *book_backend,
+                                const gchar *prop_name)
 {
-       g_debug (G_STRFUNC);
-
        g_return_val_if_fail (prop_name != NULL, NULL);
 
        if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
-               return g_strdup ("net,do-initial-query,contact-lists,refresh-supported");
+               return g_strjoin (",",
+                       "net",
+                       "do-initial-query",
+                       "contact-lists",
+                       e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+                       NULL);
 
        } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
                return g_strdup ("");
 
        } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
-               return g_strjoin (
-                       ",",
+               return g_strjoin (",",
                        e_contact_field_name (E_CONTACT_UID),
                        e_contact_field_name (E_CONTACT_REV),
                        e_contact_field_name (E_CONTACT_FULL_NAME),
@@ -1600,774 +1167,99 @@ book_backend_google_get_backend_property (EBookBackend *backend,
                        e_contact_field_name (E_CONTACT_NOTE),
                        e_contact_field_name (E_CONTACT_PHOTO),
                        e_contact_field_name (E_CONTACT_CATEGORIES),
-#if defined(GDATA_CHECK_VERSION)
-#if GDATA_CHECK_VERSION(0, 11, 0)
                        e_contact_field_name (E_CONTACT_CATEGORY_LIST),
                        e_contact_field_name (E_CONTACT_FILE_AS),
-#else
-                       e_contact_field_name (E_CONTACT_CATEGORY_LIST),
-#endif
-#else
-                       e_contact_field_name (E_CONTACT_CATEGORY_LIST),
-#endif
                        e_contact_field_name (E_CONTACT_NICKNAME),
                        NULL);
        }
 
-       /* Chain up to parent's get_backend_property() method. */
-       return E_BOOK_BACKEND_CLASS (e_book_backend_google_parent_class)->
-               get_backend_property (backend, prop_name);
+       /* Chain up to parent's method. */
+       return E_BOOK_BACKEND_CLASS (e_book_backend_google_parent_class)->get_backend_property (book_backend, 
prop_name);
 }
 
-static gboolean
-book_backend_google_open_sync (EBookBackend *backend,
-                               GCancellable *cancellable,
-                               GError **error)
-{
-       EBookBackendGooglePrivate *priv;
-       gboolean is_online;
-       gboolean success = TRUE;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_debug (G_STRFUNC);
-
-       if (priv->cancellables && backend_is_authorized (backend))
-               return TRUE;
-
-       /* Set up our object */
-       if (priv->cancellables == NULL) {
-               priv->groups_by_id = g_hash_table_new_full (
-                       (GHashFunc) g_str_hash,
-                       (GEqualFunc) g_str_equal,
-                       (GDestroyNotify) g_free,
-                       (GDestroyNotify) g_free);
-               priv->groups_by_name = g_hash_table_new_full (
-                       (GHashFunc) g_str_hash,
-                       (GEqualFunc) g_str_equal,
-                       (GDestroyNotify) g_free,
-                       (GDestroyNotify) g_free);
-               priv->system_groups_by_id = g_hash_table_new_full (
-                       (GHashFunc) g_str_hash,
-                       (GEqualFunc) g_str_equal,
-                       (GDestroyNotify) g_free,
-                       (GDestroyNotify) g_free);
-               /* shares keys and values with system_groups_by_id */
-               priv->system_groups_by_entry_id = g_hash_table_new (
-                       (GHashFunc) g_str_hash,
-                       (GEqualFunc) g_str_equal);
-               priv->cancellables = g_hash_table_new_full (
-                       (GHashFunc) g_direct_hash,
-                       (GEqualFunc) g_direct_equal,
-                       (GDestroyNotify) NULL,
-                       (GDestroyNotify) g_object_unref);
-       }
-
-       cache_init (backend);
-
-       /* Set up ready to be interacted with */
-       is_online = e_backend_get_online (E_BACKEND (backend));
-       e_book_backend_set_writable (backend, FALSE);
-
-       if (is_online) {
-               ESource *source = e_backend_get_source (E_BACKEND (backend));
-
-               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
-
-               success = connect_without_password (backend, cancellable, error);
-               if (success) {
-                       GError *local_error = NULL;
-
-                       /* Refresh the authorizer.  This may block. */
-                       success = gdata_authorizer_refresh_authorization (
-                               priv->authorizer, cancellable, &local_error);
-
-                       if (success) {
-                               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
-                       } else {
-                               GError *local_error2 = NULL;
-
-                               e_source_set_connection_status (source, 
E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-
-                               if (local_error && !e_backend_credentials_required_sync (E_BACKEND (backend), 
E_SOURCE_CREDENTIALS_REASON_ERROR,
-                                       NULL, 0, local_error, cancellable, &local_error2)) {
-                                       g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC, 
local_error2 ? local_error2->message : "Unknown error");
-                               }
-
-                               g_clear_error (&local_error2);
-
-                               if (local_error)
-                                       g_propagate_error (error, local_error);
-                       }
-               } else {
-                       GError *local_error = NULL;
-
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-
-                       if (!e_backend_credentials_required_sync (E_BACKEND (backend), 
E_SOURCE_CREDENTIALS_REASON_REQUIRED,
-                               NULL, 0, NULL, cancellable, &local_error)) {
-                               g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC, 
local_error ? local_error->message : "Unknown error");
-                       }
-
-                       g_clear_error (&local_error);
-               }
-       }
-
-       if (is_online && backend_is_authorized (backend)) {
-               e_book_backend_set_writable (backend, TRUE);
-               cache_refresh_if_needed (backend);
-       }
-
-       return success;
-}
-
-static gboolean
-book_backend_google_create_contacts_sync (EBookBackend *backend,
-                                          const gchar * const *vcards,
-                                          GQueue *out_contacts,
-                                          GCancellable *cancellable,
-                                          GError **error)
-{
-       EBookBackendGooglePrivate *priv;
-       EContactPhoto *photo = NULL;
-       EContact *contact;
-       GDataEntry *entry;
-       GDataContactsContact *new_contact;
-       gchar *xml;
-       gboolean success = TRUE;
-       GError *gdata_error = NULL;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       /* We make the assumption that the vCard list we're passed is always
-        * exactly one element long, since we haven't specified "bulk-adds"
-        * in our static capability list. This simplifies the logic. */
-       if (g_strv_length ((gchar **) vcards) > 1) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_NOT_SUPPORTED,
-                       _("The backend does not support bulk additions"));
-               return FALSE;
-       }
-
-       g_debug (G_STRFUNC);
-
-       g_debug ("Creating: %s", vcards[0]);
-
-       if (!e_backend_get_online (E_BACKEND (backend))) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
-               return FALSE;
-       }
-
-       g_warn_if_fail (backend_is_authorized (backend));
-
-       g_rec_mutex_lock (&priv->groups_lock);
-
-       /* Ensure the system groups have been fetched. */
-       if (g_hash_table_size (priv->system_groups_by_id) == 0)
-               get_groups_sync (backend, cancellable, NULL);
-
-       /* Build the GDataEntry from the vCard */
-       contact = e_contact_new_from_vcard (vcards[0]);
-       entry = gdata_entry_new_from_e_contact (
-               contact,
-               priv->groups_by_name,
-               priv->system_groups_by_id,
-               _create_group, backend);
-       g_object_unref (contact);
-
-       g_rec_mutex_unlock (&priv->groups_lock);
-
-       /* Debug XML output */
-       xml = gdata_parsable_get_xml (GDATA_PARSABLE (entry));
-       g_debug ("new entry with xml: %s", xml);
-       g_free (xml);
-
-       new_contact = gdata_contacts_service_insert_contact (
-               GDATA_CONTACTS_SERVICE (priv->service),
-               GDATA_CONTACTS_CONTACT (entry),
-               cancellable, &gdata_error);
-
-       if (new_contact == NULL) {
-               success = FALSE;
-               goto exit;
-       }
-
-       /* Add a photo for the new contact, if appropriate.  This has to
-        * be done before we finish the contact creation operation so we
-        * can update the EContact with the photo data and ETag. */
-       photo = g_object_steal_data (G_OBJECT (entry), "photo");
-       if (photo != NULL) {
-               GDataEntry *updated_entry;
-               gchar *xml;
-
-               updated_entry = update_contact_photo (
-                       new_contact,
-                       GDATA_CONTACTS_SERVICE (priv->service),
-                       photo, cancellable, &gdata_error);
-
-               /* Sanity check. */
-               g_return_val_if_fail (
-                       ((updated_entry != NULL) && (gdata_error == NULL)) ||
-                       ((updated_entry == NULL) && (gdata_error != NULL)),
-                       FALSE);
-
-               if (gdata_error != NULL) {
-                       g_debug (
-                               "Uploading contact photo "
-                               "for '%s' failed: %s",
-                               gdata_entry_get_id (GDATA_ENTRY (new_contact)),
-                               gdata_error->message);
-                       success = FALSE;
-                       goto exit;
-               }
-
-               /* Output debug XML */
-               xml = gdata_parsable_get_xml (
-                       GDATA_PARSABLE (updated_entry));
-               g_debug ("After re-querying:\n%s", xml);
-               g_free (xml);
-
-               g_object_unref (new_contact);
-               new_contact = GDATA_CONTACTS_CONTACT (updated_entry);
-
-               /* Store the photo on the final GDataContactsContact
-                * object so it makes it into the cache. */
-               g_object_set_data_full (
-                       G_OBJECT (new_contact), "photo", photo,
-                       (GDestroyNotify) e_contact_photo_free);
-               photo = NULL;
-       }
-
-       contact = cache_add_contact (backend, GDATA_ENTRY (new_contact));
-       if (contact) {
-               g_queue_push_tail (out_contacts, g_object_ref (contact));
-               g_object_unref (contact);
-       }
-
-exit:
-       g_clear_object (&entry);
-       g_clear_object (&new_contact);
-
-       if (photo != NULL)
-               e_contact_photo_free (photo);
-
-       if (gdata_error != NULL) {
-               g_warn_if_fail (success == FALSE);
-               data_book_error_from_gdata_error (error, gdata_error);
-               g_error_free (gdata_error);
-       } else {
-               e_backend_ensure_source_status_connected (E_BACKEND (backend));
-       }
-
-       return success;
-}
-
-static gboolean
-book_backend_google_modify_contacts_sync (EBookBackend *backend,
-                                          const gchar * const *vcards,
-                                          GQueue *out_contacts,
-                                          GCancellable *cancellable,
-                                          GError **error)
-{
-       EBookBackendGooglePrivate *priv;
-       GDataAuthorizationDomain *authorization_domain;
-       EContact *contact, *cached_contact;
-       PhotoOperation photo_operation;
-       EContactPhoto *photo;
-       GDataEntry *entry = NULL;
-       GDataEntry *new_contact;
-       const gchar *uid;
-       gboolean success = TRUE;
-       gchar *xml;
-       GError *gdata_error = NULL;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       authorization_domain =
-               gdata_contacts_service_get_primary_authorization_domain ();
-
-       g_debug (G_STRFUNC);
-
-       g_debug ("Updating: %s", vcards[0]);
-
-       /* We make the assumption that the vCard list we're passed is
-        * always exactly one element long, since we haven't specified
-        * "bulk-modifies" in our static capability list.  This is because
-        * there is no clean way to roll back changes in case of an error. */
-       if (g_strv_length ((gchar **) vcards) > 1) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_NOT_SUPPORTED,
-                       _("The backend does not support bulk modifications"));
-               return FALSE;
-       }
-
-       if (!e_backend_get_online (E_BACKEND (backend))) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
-               return FALSE;
-       }
-
-       g_warn_if_fail (backend_is_authorized (backend));
-
-       /* Get the new contact and its UID. */
-       contact = e_contact_new_from_vcard (vcards[0]);
-       uid = e_contact_get (contact, E_CONTACT_UID);
-
-       /* Get the old cached contact with the same UID,
-        * and its associated GDataEntry. */
-       cached_contact = cache_get_contact (backend, uid, &entry);
-
-       if (cached_contact == NULL) {
-               g_debug (
-                       "Modifying contact failed: "
-                       "Contact with uid %s not found in cache.", uid);
-               g_object_unref (contact);
-
-               g_set_error_literal (
-                       error, E_BOOK_CLIENT_ERROR,
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
-                       e_book_client_error_to_string (
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
-               return FALSE;
-       }
-
-       g_rec_mutex_lock (&priv->groups_lock);
-
-       /* Ensure the system groups have been fetched. */
-       if (g_hash_table_size (priv->system_groups_by_id) == 0)
-               get_groups_sync (backend, cancellable, NULL);
-
-       /* Update the old GDataEntry from the new contact. */
-       gdata_entry_update_from_e_contact (
-               entry, contact, FALSE,
-               priv->groups_by_name,
-               priv->system_groups_by_id,
-               _create_group, backend);
-
-       g_rec_mutex_unlock (&priv->groups_lock);
-
-       /* Output debug XML */
-       xml = gdata_parsable_get_xml (GDATA_PARSABLE (entry));
-       g_debug ("Before:\n%s", xml);
-       g_free (xml);
-
-       photo = g_object_steal_data (G_OBJECT (entry), "photo");
-
-       /* Update the contact's photo. We can't rely on the ETags at this
-        * point, as the ETag in @contact may be out of sync with the photo
-        * in the EContact (since the photo may have been updated).
-        * Consequently, after updating @entry its ETag may also be out of
-        * sync with its attached photo data.  This means that we have to
-        * detect whether the photo has changed by comparing the photo data
-        * itself, which is guaranteed to be in sync between @contact and
-        * @entry. */
-       photo_operation = pick_photo_operation (cached_contact, contact);
-
-       /* Sanity check the photo operation. */
-       switch (photo_operation) {
-               case LEAVE_PHOTO:
-                       break;
-
-               case ADD_PHOTO:
-               case UPDATE_PHOTO:
-                       g_return_val_if_fail (photo != NULL, FALSE);
-                       break;
-
-               case REMOVE_PHOTO:
-                       g_return_val_if_fail (photo == NULL, FALSE);
-                       break;
-
-               default:
-                       g_return_val_if_reached (FALSE);
-       }
-
-       g_clear_object (&cached_contact);
-       g_clear_object (&contact);
-
-       new_contact = gdata_service_update_entry (
-               priv->service,
-               authorization_domain,
-               entry,
-               cancellable, &gdata_error);
-
-       if (new_contact == NULL) {
-               g_debug (
-                       "Modifying contact failed: %s",
-                       gdata_error->message);
-               success = FALSE;
-               goto exit;
-       }
-
-       /* Output debug XML */
-       xml = gdata_parsable_get_xml (GDATA_PARSABLE (new_contact));
-       g_debug ("After:\n%s", xml);
-       g_free (xml);
-
-       /* Add a photo for the new contact, if appropriate. This has to be
-        * done before we respond to the contact creation operation so that
-        * we can update the EContact with the photo data and ETag. */
-       if (photo_operation != LEAVE_PHOTO) {
-               GDataEntry *updated_entry;
-
-               updated_entry = update_contact_photo (
-                       GDATA_CONTACTS_CONTACT (new_contact),
-                       GDATA_CONTACTS_SERVICE (priv->service),
-                       photo, cancellable, &gdata_error);
-
-               /* Sanity check. */
-               g_return_val_if_fail (
-                       ((updated_entry != NULL) && (gdata_error == NULL)) ||
-                       ((updated_entry == NULL) && (gdata_error != NULL)),
-                       FALSE);
-
-               if (gdata_error != NULL) {
-                       g_debug (
-                               "Uploading contact photo "
-                               "for '%s' failed: %s",
-                               gdata_entry_get_id (new_contact),
-                               gdata_error->message);
-                       success = FALSE;
-                       goto exit;
-               }
-
-               /* Output debug XML */
-               xml = gdata_parsable_get_xml (
-                       GDATA_PARSABLE (updated_entry));
-               g_debug ("After re-querying:\n%s", xml);
-               g_free (xml);
-
-               g_object_unref (new_contact);
-               new_contact = updated_entry;
-       }
-
-       /* Store the photo on the final GDataEntry
-        * object so it makes it to the cache. */
-       if (photo != NULL) {
-               g_object_set_data_full (
-                       G_OBJECT (new_contact), "photo", photo,
-                       (GDestroyNotify) e_contact_photo_free);
-               photo = NULL;
-       } else {
-               g_object_set_data (
-                       G_OBJECT (new_contact), "photo", NULL);
-       }
-
-       contact = cache_add_contact (backend, new_contact);
-       if (contact) {
-               g_queue_push_tail (out_contacts, g_object_ref (contact));
-               g_object_unref (contact);
-       }
-
-exit:
-       g_clear_object (&entry);
-       g_clear_object (&new_contact);
-
-       if (photo != NULL)
-               e_contact_photo_free (photo);
-
-       if (gdata_error != NULL) {
-               g_warn_if_fail (success == FALSE);
-               data_book_error_from_gdata_error (error, gdata_error);
-               g_error_free (gdata_error);
-       } else {
-               e_backend_ensure_source_status_connected (E_BACKEND (backend));
-       }
-
-       return success;
-}
-
-static gboolean
-book_backend_google_remove_contacts_sync (EBookBackend *backend,
-                                          const gchar *const *uids,
-                                          GCancellable *cancellable,
-                                          GError **error)
-{
-       EBookBackendGooglePrivate *priv;
-       GDataAuthorizationDomain *authorization_domain;
-       GDataEntry *entry = NULL;
-       EContact *cached_contact;
-       gboolean success;
-       GError *gdata_error = NULL;
-
-       priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       authorization_domain =
-               gdata_contacts_service_get_primary_authorization_domain ();
-
-       g_debug (G_STRFUNC);
-
-       /* We make the assumption that the ID list we're passed is always
-        * exactly one element long, since we haven't specified "bulk-removes"
-        * in our static capability list.  This simplifies the logic. */
-       if (g_strv_length ((gchar **) uids) > 1) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_NOT_SUPPORTED,
-                       _("The backend does not support bulk removals"));
-               return FALSE;
-       }
-
-       if (!e_backend_get_online (E_BACKEND (backend))) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
-               return FALSE;
-       }
-
-       g_warn_if_fail (backend_is_authorized (backend));
-
-       /* Get the contact and associated GDataEntry from the cache */
-       cached_contact = cache_get_contact (backend, uids[0], &entry);
-
-       if (cached_contact == NULL) {
-               g_set_error_literal (
-                       error, E_BOOK_CLIENT_ERROR,
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
-                       e_book_client_error_to_string (
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
-               return FALSE;
-       }
-
-       g_object_unref (cached_contact);
-
-       /* Remove the contact from the cache */
-       cache_remove_contact (backend, uids[0]);
-
-       success = gdata_service_delete_entry (
-               priv->service,
-               authorization_domain, entry,
-               cancellable, &gdata_error);
-
-       g_object_unref (entry);
-
-       if (gdata_error != NULL) {
-               g_warn_if_fail (success == FALSE);
-               data_book_error_from_gdata_error (error, gdata_error);
-               g_error_free (gdata_error);
-       } else {
-               e_backend_ensure_source_status_connected (E_BACKEND (backend));
-       }
-
-       return success;
-}
-
-static EContact *
-book_backend_google_get_contact_sync (EBookBackend *backend,
-                                      const gchar *uid,
-                                      GCancellable *cancellable,
-                                      GError **error)
-{
-       EContact *contact;
-
-       g_debug (G_STRFUNC);
-
-       /* Get the contact */
-       contact = cache_get_contact (backend, uid, NULL);
-       if (contact == NULL) {
-               g_set_error_literal (
-                       error, E_BOOK_CLIENT_ERROR,
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
-                       e_book_client_error_to_string (
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
-       }
-
-       return contact;
-}
-
-static gboolean
-book_backend_google_get_contact_list_sync (EBookBackend *backend,
-                                           const gchar *query,
-                                           GQueue *out_contacts,
-                                           GCancellable *cancellable,
-                                           GError **error)
+static void
+ebb_google_constructed (GObject *object)
 {
-       EBookBackendSExp *sexp;
-       GQueue queue = G_QUEUE_INIT;
-
-       g_debug (G_STRFUNC);
-
-       sexp = e_book_backend_sexp_new (query);
-
-       /* Get all contacts */
-       cache_get_contacts (backend, &queue);
-
-       while (!g_queue_is_empty (&queue)) {
-               EContact *contact;
-
-               contact = g_queue_pop_head (&queue);
+       EBookBackendGoogle *bbgoogle = E_BOOK_BACKEND_GOOGLE (object);
 
-               /* If the search expression matches the contact,
-                * include it in the search results. */
-               if (e_book_backend_sexp_match_contact (sexp, contact)) {
-                       g_object_ref (contact);
-                       g_queue_push_tail (out_contacts, contact);
-               }
-
-               g_object_unref (contact);
-       }
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_google_parent_class)->constructed (object);
 
-       g_object_unref (sexp);
-
-       return TRUE;
+       /* Set it as always writable, regardless online/offline state */
+       e_book_backend_set_writable (E_BOOK_BACKEND (bbgoogle), TRUE);
 }
 
 static void
-book_backend_google_start_view (EBookBackend *backend,
-                                EDataBookView *bookview)
+ebb_google_dispose (GObject *object)
 {
-       GQueue queue = G_QUEUE_INIT;
-       GError *error = NULL;
-
-       g_return_if_fail (E_IS_BOOK_BACKEND_GOOGLE (backend));
-       g_return_if_fail (E_IS_DATA_BOOK_VIEW (bookview));
+       EBookBackendGoogle *bbgoogle = E_BOOK_BACKEND_GOOGLE (object);
 
-       g_debug (G_STRFUNC);
-
-       e_data_book_view_notify_progress (bookview, -1, _("Loading…"));
-
-       /* Ensure that we're ready to support a view */
-       cache_refresh_if_needed (backend);
-
-       /* Get the contacts */
-       cache_get_contacts (backend, &queue);
-       g_debug (
-               "%d contacts found in cache",
-               g_queue_get_length (&queue));
-
-       /* Notify the view that all the contacts have changed (i.e. been added) */
-       while (!g_queue_is_empty (&queue)) {
-               EContact *contact;
+       g_clear_object (&bbgoogle->priv->service);
+       g_clear_object (&bbgoogle->priv->authorizer);
 
-               contact = g_queue_pop_head (&queue);
-               e_data_book_view_notify_update (bookview, contact);
-               g_object_unref (contact);
-       }
+       g_hash_table_destroy (bbgoogle->priv->preloaded);
+       bbgoogle->priv->preloaded = NULL;
 
-       /* This function frees the GError passed to it. */
-       e_data_book_view_notify_complete (bookview, error);
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_google_parent_class)->dispose (object);
 }
 
 static void
-book_backend_google_stop_view (EBookBackend *backend,
-                               EDataBookView *bookview)
+ebb_google_finalize (GObject *object)
 {
-       g_debug (G_STRFUNC);
-}
+       EBookBackendGoogle *bbgoogle = E_BOOK_BACKEND_GOOGLE (object);
 
-static gboolean
-book_backend_google_refresh_sync (EBookBackend *backend,
-                                 GCancellable *cancellable,
-                                 GError **error)
-{
-       g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (backend), FALSE);
+       g_clear_pointer (&bbgoogle->priv->groups_by_id, (GDestroyNotify) g_hash_table_destroy);
+       g_clear_pointer (&bbgoogle->priv->groups_by_id, (GDestroyNotify) g_hash_table_destroy);
+       g_clear_pointer (&bbgoogle->priv->groups_by_name, (GDestroyNotify) g_hash_table_destroy);
+       g_clear_pointer (&bbgoogle->priv->system_groups_by_entry_id, (GDestroyNotify) g_hash_table_destroy);
+       g_clear_pointer (&bbgoogle->priv->system_groups_by_id, (GDestroyNotify) g_hash_table_destroy);
 
-       /* get only changes, it's not needed to redownload whole cache */
-       get_new_contacts (backend);
+       g_rec_mutex_clear (&bbgoogle->priv->groups_lock);
 
-       return TRUE;
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_google_parent_class)->finalize (object);
 }
 
-static ESourceAuthenticationResult
-book_backend_google_authenticate_sync (EBackend *backend,
-                                      const ENamedParameters *credentials,
-                                      gchar **out_certificate_pem,
-                                      GTlsCertificateFlags *out_certificate_errors,
-                                      GCancellable *cancellable,
-                                      GError **error)
+static void
+e_book_backend_google_init (EBookBackendGoogle *bbgoogle)
 {
-       EBookBackend *book_backend = E_BOOK_BACKEND (backend);
-       EBookBackendGooglePrivate *priv;
-       ESourceAuthenticationResult result;
-       EGDataOAuth2Authorizer *authorizer;
-       GError *local_error = NULL;
+       bbgoogle->priv = G_TYPE_INSTANCE_GET_PRIVATE (bbgoogle, E_TYPE_BOOK_BACKEND_GOOGLE, 
EBookBackendGooglePrivate);
+       bbgoogle->priv->preloaded = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
 
-       g_debug (G_STRFUNC);
+       g_rec_mutex_init (&bbgoogle->priv->groups_lock);
 
-       /* We should not have gotten here if we're offline. */
-       g_return_val_if_fail (e_backend_get_online (backend), E_SOURCE_AUTHENTICATION_ERROR);
-
-       priv = E_BOOK_BACKEND_GOOGLE (backend)->priv;
-
-       g_return_val_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (priv->authorizer), E_SOURCE_AUTHENTICATION_ERROR);
-
-       authorizer = E_GDATA_OAUTH2_AUTHORIZER (priv->authorizer);
-       e_gdata_oauth2_authorizer_set_credentials (authorizer, credentials);
-
-       get_groups_sync (E_BOOK_BACKEND (backend), cancellable, &local_error);
-
-       if (local_error == NULL) {
-               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
-
-               if (backend_is_authorized (book_backend)) {
-                       e_book_backend_set_writable (book_backend, TRUE);
-                       cache_refresh_if_needed (book_backend);
-               }
-       } else if (g_error_matches (local_error, GDATA_SERVICE_ERROR, 
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
-               if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
-                       result = E_SOURCE_AUTHENTICATION_REQUIRED;
-               else
-                       result = E_SOURCE_AUTHENTICATION_REJECTED;
-               g_clear_error (&local_error);
-       } else {
-               g_propagate_error (error, local_error);
-               result = E_SOURCE_AUTHENTICATION_ERROR;
-       }
-
-       return result;
+       bbgoogle->priv->groups_by_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       bbgoogle->priv->groups_by_name = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       bbgoogle->priv->system_groups_by_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       /* shares keys and values with system_groups_by_id */
+       bbgoogle->priv->system_groups_by_entry_id = g_hash_table_new (g_str_hash, g_str_equal);
 }
 
 static void
-e_book_backend_google_class_init (EBookBackendGoogleClass *class)
+e_book_backend_google_class_init (EBookBackendGoogleClass *klass)
 {
        GObjectClass *object_class;
-       EBackendClass *backend_class;
        EBookBackendClass *book_backend_class;
-
-       g_type_class_add_private (class, sizeof (EBookBackendGooglePrivate));
-
-       object_class = G_OBJECT_CLASS (class);
-       object_class->dispose = book_backend_google_dispose;
-       object_class->finalize = book_backend_google_finalize;
-
-       backend_class = E_BACKEND_CLASS (class);
-       backend_class->authenticate_sync = book_backend_google_authenticate_sync;
-
-       book_backend_class = E_BOOK_BACKEND_CLASS (class);
-       book_backend_class->get_backend_property = book_backend_google_get_backend_property;
-       book_backend_class->open_sync = book_backend_google_open_sync;
-       book_backend_class->create_contacts_sync = book_backend_google_create_contacts_sync;
-       book_backend_class->modify_contacts_sync = book_backend_google_modify_contacts_sync;
-       book_backend_class->remove_contacts_sync = book_backend_google_remove_contacts_sync;
-       book_backend_class->get_contact_sync = book_backend_google_get_contact_sync;
-       book_backend_class->get_contact_list_sync = book_backend_google_get_contact_list_sync;
-       book_backend_class->start_view = book_backend_google_start_view;
-       book_backend_class->stop_view = book_backend_google_stop_view;
-       book_backend_class->refresh_sync = book_backend_google_refresh_sync;
-}
-
-static void
-e_book_backend_google_init (EBookBackendGoogle *backend)
-{
-       g_debug (G_STRFUNC);
-
-       backend->priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
-       g_mutex_init (&backend->priv->cache_lock);
-       g_rec_mutex_init (&backend->priv->groups_lock);
-
-       g_signal_connect (
-               backend, "notify::online",
-               G_CALLBACK (e_book_backend_google_notify_online_cb), NULL);
+       EBookMetaBackendClass *book_meta_backend_class;
+
+       g_type_class_add_private (klass, sizeof (EBookBackendGooglePrivate));
+
+       book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+       book_meta_backend_class->backend_module_filename = "libebookbackendgoogle.so";
+       book_meta_backend_class->backend_factory_type_name = "EBookBackendGoogleFactory";
+       book_meta_backend_class->connect_sync = ebb_google_connect_sync;
+       book_meta_backend_class->disconnect_sync = ebb_google_disconnect_sync;
+       book_meta_backend_class->get_changes_sync = ebb_google_get_changes_sync;
+       book_meta_backend_class->load_contact_sync = ebb_google_load_contact_sync;
+       book_meta_backend_class->save_contact_sync = ebb_google_save_contact_sync;
+       book_meta_backend_class->remove_contact_sync = ebb_google_remove_contact_sync;
+
+       book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+       book_backend_class->get_backend_property = ebb_google_get_backend_property;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = ebb_google_constructed;
+       object_class->dispose = ebb_google_dispose;
+       object_class->finalize = ebb_google_finalize;
 }
-
diff --git a/src/addressbook/backends/google/e-book-backend-google.h 
b/src/addressbook/backends/google/e-book-backend-google.h
index c910efd..fcbf1de 100644
--- a/src/addressbook/backends/google/e-book-backend-google.h
+++ b/src/addressbook/backends/google/e-book-backend-google.h
@@ -48,12 +48,12 @@ typedef struct _EBookBackendGoogleClass EBookBackendGoogleClass;
 typedef struct _EBookBackendGooglePrivate EBookBackendGooglePrivate;
 
 struct _EBookBackendGoogle {
-       EBookBackend parent_object;
+       EBookMetaBackend parent_object;
        EBookBackendGooglePrivate *priv;
 };
 
 struct _EBookBackendGoogleClass {
-       EBookBackendClass parent_class;
+       EBookMetaBackendClass parent_class;
 };
 
 GType          e_book_backend_google_get_type  (void);
diff --git a/src/addressbook/backends/google/e-book-google-utils.c 
b/src/addressbook/backends/google/e-book-google-utils.c
index 4157894..027eb25 100644
--- a/src/addressbook/backends/google/e-book-google-utils.c
+++ b/src/addressbook/backends/google/e-book-google-utils.c
@@ -68,10 +68,11 @@ static gboolean is_known_google_im_protocol (const gchar *protocol);
 
 GDataEntry *
 gdata_entry_new_from_e_contact (EContact *contact,
-                                GHashTable *groups_by_name,
-                                GHashTable *system_groups_by_id,
-                                EContactGoogleCreateGroupFunc create_group,
-                                gpointer create_group_user_data)
+                               GHashTable *groups_by_name,
+                               GHashTable *system_groups_by_id,
+                               EContactGoogleCreateGroupFunc create_group,
+                               EBookBackendGoogle *bbgoogle,
+                               GCancellable *cancellable)
 {
        GDataEntry *entry;
 
@@ -83,7 +84,7 @@ gdata_entry_new_from_e_contact (EContact *contact,
 
        entry = GDATA_ENTRY (gdata_contacts_contact_new (NULL));
 
-       if (gdata_entry_update_from_e_contact (entry, contact, TRUE, groups_by_name, system_groups_by_id, 
create_group, create_group_user_data))
+       if (gdata_entry_update_from_e_contact (entry, contact, TRUE, groups_by_name, system_groups_by_id, 
create_group, bbgoogle, cancellable))
                return entry;
 
        g_object_unref (entry);
@@ -117,12 +118,13 @@ remove_anniversary (GDataContactsContact *contact)
 
 gboolean
 gdata_entry_update_from_e_contact (GDataEntry *entry,
-                                   EContact *contact,
-                                   gboolean ensure_personal_group,
-                                   GHashTable *groups_by_name,
-                                   GHashTable *system_groups_by_id,
-                                   EContactGoogleCreateGroupFunc create_group,
-                                   gpointer create_group_user_data)
+                                  EContact *contact,
+                                  gboolean ensure_personal_group,
+                                  GHashTable *groups_by_name,
+                                  GHashTable *system_groups_by_id,
+                                  EContactGoogleCreateGroupFunc create_group,
+                                  EBookBackendGoogle *bbgoogle,
+                                  GCancellable *cancellable)
 {
        GList *attributes, *iter, *category_names, *extended_property_names;
        EContactName *name_struct = NULL;
@@ -228,6 +230,7 @@ gdata_entry_update_from_e_contact (GDataEntry *entry,
                name = e_vcard_attribute_get_name (attr);
 
                if (0 == g_ascii_strcasecmp (name, EVC_UID) ||
+                   0 == g_ascii_strcasecmp (name, EVC_REV) ||
                    0 == g_ascii_strcasecmp (name, EVC_N) ||
                    0 == g_ascii_strcasecmp (name, EVC_FN) ||
                    0 == g_ascii_strcasecmp (name, EVC_LABEL) ||
@@ -239,7 +242,8 @@ gdata_entry_update_from_e_contact (GDataEntry *entry,
                    0 == g_ascii_strcasecmp (name, EVC_CATEGORIES) ||
                    0 == g_ascii_strcasecmp (name, EVC_PHOTO) ||
                    0 == g_ascii_strcasecmp (name, GOOGLE_SYSTEM_GROUP_ATTR) ||
-                   0 == g_ascii_strcasecmp (name, e_contact_field_name (E_CONTACT_NICKNAME))) {
+                   0 == g_ascii_strcasecmp (name, e_contact_field_name (E_CONTACT_NICKNAME)) ||
+                   0 == g_ascii_strcasecmp (name, E_GOOGLE_X_PHOTO_ETAG)) {
                        /* Ignore attributes which are treated separately */
                } else if (0 == g_ascii_strcasecmp (name, EVC_EMAIL)) {
                        /* EMAIL */
@@ -450,12 +454,12 @@ gdata_entry_update_from_e_contact (GDataEntry *entry,
                if (category_id == NULL)
                        category_id = g_strdup (g_hash_table_lookup (groups_by_name, category_name));
                if (category_id == NULL) {
-                       GError *error = NULL;
+                       GError *local_error = NULL;
 
-                       category_id = create_group (category_name, create_group_user_data, &error);
+                       category_id = create_group (bbgoogle, category_name, cancellable, &local_error);
                        if (category_id == NULL) {
-                               g_warning ("Error creating group '%s': %s", category_name, error->message);
-                               g_error_free (error);
+                               g_warning ("Error creating group '%s': %s", category_name, local_error ? 
local_error->message : "Unknown error");
+                               g_clear_error (&local_error);
                                continue;
                        }
                }
@@ -565,8 +569,6 @@ e_contact_new_from_gdata_entry (GDataEntry *entry,
 {
        EVCard *vcard;
        EVCardAttribute *attr, *system_group_ids_attr;
-       EContactPhoto *photo;
-       const gchar *photo_etag;
        GList *email_addresses, *im_addresses, *phone_numbers, *postal_addresses, *orgs, *category_names, 
*category_ids;
        const gchar *uid, *note, *nickname;
        GList *itr;
@@ -858,20 +860,6 @@ e_contact_new_from_gdata_entry (GDataEntry *entry,
                break;
        }
 
-       /* PHOTO */
-       photo = g_object_get_data (G_OBJECT (entry), "photo");
-       photo_etag = gdata_contacts_contact_get_photo_etag (GDATA_CONTACTS_CONTACT (entry));
-
-       if (photo != NULL) {
-               /* Photo */
-               e_contact_set (E_CONTACT (vcard), E_CONTACT_PHOTO, photo);
-
-               /* ETag */
-               attr = e_vcard_attribute_new ("", GDATA_PHOTO_ETAG_ATTR);
-               e_vcard_attribute_add_value (attr, photo_etag);
-               e_vcard_add_attribute (vcard, attr);
-       }
-
        return E_CONTACT (vcard);
 }
 
diff --git a/src/addressbook/backends/google/e-book-google-utils.h 
b/src/addressbook/backends/google/e-book-google-utils.h
index fdeb24d..6d7e133 100644
--- a/src/addressbook/backends/google/e-book-google-utils.h
+++ b/src/addressbook/backends/google/e-book-google-utils.h
@@ -20,19 +20,34 @@
 #ifndef E_BOOK_GOOGLE_UTILS_H
 #define E_BOOK_GOOGLE_UTILS_H
 
-G_BEGIN_DECLS
+#include <gdata/gdata.h>
+
+#include "e-book-backend-google.h"
 
-/* Custom attribute names. */
-#define GDATA_PHOTO_ETAG_ATTR "X-GDATA-PHOTO-ETAG"
+#define E_GOOGLE_X_PHOTO_ETAG "X-EVOLUTION-GOOGLE-PHOTO-ETAG"
+
+G_BEGIN_DECLS
 
-typedef gchar *(*EContactGoogleCreateGroupFunc) (const gchar *category_name, gpointer user_data, GError 
**error);
+typedef gchar *(*EContactGoogleCreateGroupFunc) (EBookBackendGoogle *bbgoogle,
+                                                const gchar *category_name,
+                                                GCancellable *cancellable,
+                                                GError **error);
 
-GDataEntry *gdata_entry_new_from_e_contact (EContact *contact, GHashTable *groups_by_name, GHashTable 
*system_groups_by_id,
-                                            EContactGoogleCreateGroupFunc create_group,
-                                            gpointer create_group_user_data) G_GNUC_MALLOC 
G_GNUC_WARN_UNUSED_RESULT;
-gboolean gdata_entry_update_from_e_contact (GDataEntry *entry, EContact *contact, gboolean 
ensure_personal_group, GHashTable *groups_by_name,
-                                            GHashTable *system_groups_by_id,
-                                            EContactGoogleCreateGroupFunc create_group, gpointer 
create_group_user_data);
+GDataEntry *   gdata_entry_new_from_e_contact  (EContact *contact,
+                                                GHashTable *groups_by_name,
+                                                GHashTable *system_groups_by_id,
+                                                EContactGoogleCreateGroupFunc create_group,
+                                                EBookBackendGoogle *bbgoogle,
+                                                GCancellable *cancellable) G_GNUC_MALLOC 
G_GNUC_WARN_UNUSED_RESULT;
+gboolean       gdata_entry_update_from_e_contact
+                                               (GDataEntry *entry,
+                                                EContact *contact,
+                                                gboolean ensure_personal_group,
+                                                GHashTable *groups_by_name,
+                                                GHashTable *system_groups_by_id,
+                                                EContactGoogleCreateGroupFunc create_group,
+                                                EBookBackendGoogle *bbgoogle,
+                                                GCancellable *cancellable);
 
 EContact *e_contact_new_from_gdata_entry (GDataEntry *entry, GHashTable *groups_by_id,
                                           GHashTable *system_groups_by_entry_id) G_GNUC_MALLOC 
G_GNUC_WARN_UNUSED_RESULT;
diff --git a/src/addressbook/backends/google/tests/phone-numbers.c 
b/src/addressbook/backends/google/tests/phone-numbers.c
index 67fd609..f2ca12f 100644
--- a/src/addressbook/backends/google/tests/phone-numbers.c
+++ b/src/addressbook/backends/google/tests/phone-numbers.c
@@ -37,9 +37,10 @@ build_system_groups_by_id (void)
 }
 
 static gchar *
-create_group_null (const gchar *category_name,
-                   gpointer user_data,
-                   GError **error)
+create_group_null (EBookBackendGoogle *bbgoogle,
+                  const gchar *category_name,
+                  GCancellable *cancellable,
+                  GError **error)
 {
        /* Must never be reached. */
        g_assert_not_reached ();
@@ -61,7 +62,7 @@ create_group_null (const gchar *category_name,
                "END:VCARD" \
        ); \
 \
-       entry = gdata_entry_new_from_e_contact (contact, groups_by_name, system_groups_by_id, 
create_group_null, NULL); \
+       entry = gdata_entry_new_from_e_contact (contact, groups_by_name, system_groups_by_id, 
create_group_null, NULL, NULL); \
        g_assert (entry != NULL); \
 \
        g_hash_table_unref (system_groups_by_id); \
diff --git a/src/addressbook/backends/webdav/e-book-backend-webdav.c 
b/src/addressbook/backends/webdav/e-book-backend-webdav.c
index 7deda66..12d4989 100644
--- a/src/addressbook/backends/webdav/e-book-backend-webdav.c
+++ b/src/addressbook/backends/webdav/e-book-backend-webdav.c
@@ -1,6 +1,6 @@
-/* e-book-backend-webdav.c - Webdav contact backend.
- *
+/*
  * Copyright (C) 2008 Matthias Braun <matze braunis de>
+ * 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
@@ -17,12 +17,6 @@
  * Authors: Matthias Braun <matze braunis de>
  */
 
-/*
- * Implementation notes:
- *   We use the DavResource URIs as UID in the evolution contact
- *   ETags are saved in the WEBDAV_CONTACT_ETAG field so we know which cached contacts
- *   are outdated.
- */
 #include "evolution-data-server-config.h"
 
 #include <stdio.h>
@@ -30,1960 +24,1073 @@
 #include <string.h>
 #include <glib/gi18n-lib.h>
 
+#include "libedataserver/libedataserver.h"
+
 #include "e-book-backend-webdav.h"
 
-#include <libsoup/soup.h>
-
-#include <libxml/parser.h>
-#include <libxml/xmlreader.h>
-#include <libxml/xpath.h>
-#include <libxml/xpathInternals.h>
-
-#define E_BOOK_BACKEND_WEBDAV_GET_PRIVATE(obj) \
-       (G_TYPE_INSTANCE_GET_PRIVATE \
-       ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdavPrivate))
-
-#define USERAGENT             "Evolution/" VERSION
-#define WEBDAV_CLOSURE_NAME   "EBookBackendWebdav.BookView::closure"
-#define WEBDAV_CTAG_KEY "WEBDAV_CTAG"
-#define WEBDAV_CACHE_VERSION_KEY "WEBDAV_CACHE_VERSION"
-#define WEBDAV_CACHE_VERSION "2"
-#define WEBDAV_CONTACT_ETAG "X-EVOLUTION-WEBDAV-ETAG"
-#define WEBDAV_CONTACT_HREF "X-EVOLUTION-WEBDAV-HREF"
-
-G_DEFINE_TYPE (EBookBackendWebdav, e_book_backend_webdav, E_TYPE_BOOK_BACKEND)
-
-struct _EBookBackendWebdavPrivate {
-       gboolean           marked_for_offline;
-       SoupSession       *session;
-       gchar             *uri;
-       gchar              *username;
-       gchar              *password;
-       gboolean supports_getctag;
-       gint64 last_server_test_us; /* real-time, in microseconds, when the last server test
-                                       for changes had been made, when the server doesn't support ctag */
-
-       GMutex cache_lock;
-       GMutex update_lock;
-       EBookBackendCache *cache;
-};
+#define E_WEBDAV_MAX_MULTIGET_AMOUNT 100 /* what's the maximum count of items to fetch within a multiget 
request */
 
-typedef struct {
-       EBookBackendWebdav *webdav;
-       GThread            *thread;
-       EFlag              *running;
-} WebdavBackendSearchClosure;
+#define E_WEBDAV_X_ETAG "X-EVOLUTION-WEBDAV-ETAG"
 
-static void
-webdav_debug_setup (SoupSession *session)
-{
-       const gchar *debug_str;
-       SoupLogger *logger;
-       SoupLoggerLogLevel level;
-
-       g_return_if_fail (session != NULL);
-
-       debug_str = g_getenv ("WEBDAV_DEBUG");
-       if (!debug_str || !*debug_str)
-               return;
-
-       if (g_ascii_strcasecmp (debug_str, "all") == 0)
-               level = SOUP_LOGGER_LOG_BODY;
-       else if (g_ascii_strcasecmp (debug_str, "headers") == 0)
-               level = SOUP_LOGGER_LOG_HEADERS;
-       else
-               level = SOUP_LOGGER_LOG_MINIMAL;
-
-       logger = soup_logger_new (level, 100 * 1024 * 1024);
-       soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
-       g_object_unref (logger);
-}
+#define EDB_ERROR(_code) e_data_book_create_error (_code, NULL)
+#define EDB_ERROR_EX(_code, _msg) e_data_book_create_error (_code, _msg)
 
-static void
-webdav_contact_set_etag (EContact *contact,
-                         const gchar *etag)
-{
-       EVCardAttribute *attr;
+struct _EBookBackendWebDAVPrivate {
+       /* The main WebDAV session  */
+       EWebDAVSession *webdav;
 
-       g_return_if_fail (E_IS_CONTACT (contact));
+       /* support for 'getctag' extension */
+       gboolean ctag_supported;
+};
 
-       attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_ETAG);
+G_DEFINE_TYPE (EBookBackendWebDAV, e_book_backend_webdav, E_TYPE_BOOK_META_BACKEND)
 
-       if (attr) {
-               e_vcard_attribute_remove_values (attr);
-               if (etag) {
-                       e_vcard_attribute_add_value (attr, etag);
-               } else {
-                       e_vcard_remove_attribute (E_VCARD (contact), attr);
-               }
-       } else if (etag) {
-               e_vcard_append_attribute_with_value (
-                       E_VCARD (contact),
-                       e_vcard_attribute_new (NULL, WEBDAV_CONTACT_ETAG),
-                       etag);
-       }
-}
-
-static gchar *
-webdav_contact_get_etag (EContact *contact)
+static gboolean
+ebb_webdav_connect_sync (EBookMetaBackend *meta_backend,
+                        const ENamedParameters *credentials,
+                        ESourceAuthenticationResult *out_auth_result,
+                        gchar **out_certificate_pem,
+                        GTlsCertificateFlags *out_certificate_errors,
+                        GCancellable *cancellable,
+                        GError **error)
 {
-       EVCardAttribute *attr;
-       GList *v = NULL;
-
-       g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
-
-       attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_ETAG);
+       EBookBackendWebDAV *bbdav;
+       GHashTable *capabilities = NULL, *allows = NULL;
+       ESource *source;
+       gboolean success;
+       GError *local_error = NULL;
 
-       if (attr)
-               v = e_vcard_attribute_get_values (attr);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
 
-       return ((v && v->data) ? g_strstrip (g_strdup (v->data)) : NULL);
-}
-
-static void
-webdav_contact_set_href (EContact *contact,
-                         const gchar *href)
-{
-       EVCardAttribute *attr;
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
 
-       g_return_if_fail (E_IS_CONTACT (contact));
+       if (bbdav->priv->webdav)
+               return TRUE;
 
-       attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_HREF);
+       source = e_backend_get_source (E_BACKEND (meta_backend));
 
-       if (attr) {
-               e_vcard_attribute_remove_values (attr);
-               if (href) {
-                       e_vcard_attribute_add_value (attr, href);
-               } else {
-                       e_vcard_remove_attribute (E_VCARD (contact), attr);
-               }
-       } else if (href) {
-               e_vcard_append_attribute_with_value (
-                       E_VCARD (contact),
-                       e_vcard_attribute_new (NULL, WEBDAV_CONTACT_HREF),
-                       href);
-       }
-}
+       bbdav->priv->webdav = e_webdav_session_new (source);
 
-static gchar *
-webdav_contact_get_href (EContact *contact)
-{
-       EVCardAttribute *attr;
-       GList *v = NULL;
+       e_soup_session_setup_logging (E_SOUP_SESSION (bbdav->priv->webdav), g_getenv ("WEBDAV_DEBUG"));
 
-       g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+       e_binding_bind_property (
+               bbdav, "proxy-resolver",
+               bbdav->priv->webdav, "proxy-resolver",
+               G_BINDING_SYNC_CREATE);
 
-       attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_HREF);
+       /* Thinks the 'getctag' extension is available the first time, but unset it when realizes it isn't. */
+       bbdav->priv->ctag_supported = TRUE;
 
-       if (attr)
-               v = e_vcard_attribute_get_values (attr);
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
 
-       return ((v && v->data) ? g_strstrip (g_strdup (v->data)) : NULL);
-}
+       e_soup_session_set_credentials (E_SOUP_SESSION (bbdav->priv->webdav), credentials);
 
-static void
-closure_destroy (WebdavBackendSearchClosure *closure)
-{
-       e_flag_free (closure->running);
-       if (closure->thread)
-               g_thread_unref (closure->thread);
-       g_free (closure);
-}
+       success = e_webdav_session_options_sync (bbdav->priv->webdav, NULL,
+               &capabilities, &allows, cancellable, &local_error);
 
-static WebdavBackendSearchClosure *
-init_closure (EDataBookView *book_view,
-              EBookBackendWebdav *webdav)
-{
-       WebdavBackendSearchClosure *closure = g_new (WebdavBackendSearchClosure, 1);
+       /* iCloud and Google servers can return "404 Not Found" when issued OPTIONS on the addressbook 
collection */
+       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+               ESourceWebdav *webdav_extension;
+               SoupURI *soup_uri;
 
-       closure->webdav = webdav;
-       closure->thread = NULL;
-       closure->running = e_flag_new ();
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+               if (soup_uri) {
+                       if (soup_uri->host && soup_uri->path && *soup_uri->path &&
+                           e_util_utf8_strstrcase (soup_uri->host, ".icloud.com")) {
+                               /* Try parent directory */
+                               gchar *path;
+                               gint len = strlen (soup_uri->path);
 
-       g_object_set_data_full (
-               G_OBJECT (book_view), WEBDAV_CLOSURE_NAME,
-               closure, (GDestroyNotify) closure_destroy);
+                               if (soup_uri->path[len - 1] == '/')
+                                       soup_uri->path[len - 1] = '\0';
 
-       return closure;
-}
+                               path = g_path_get_dirname (soup_uri->path);
+                               if (path && g_str_has_prefix (soup_uri->path, path)) {
+                                       gchar *uri;
 
-static WebdavBackendSearchClosure *
-get_closure (EDataBookView *book_view)
-{
-       return g_object_get_data (G_OBJECT (book_view), WEBDAV_CLOSURE_NAME);
-}
+                                       soup_uri_set_path (soup_uri, path);
 
-static guint
-send_and_handle_ssl (EBookBackendWebdav *webdav,
-                     SoupMessage *message,
-                     GCancellable *cancellable)
-{
-       guint status_code;
+                                       uri = soup_uri_to_string (soup_uri, FALSE);
+                                       if (uri) {
+                                               g_clear_error (&local_error);
 
-       e_soup_ssl_trust_connect (message, e_backend_get_source (E_BACKEND (webdav)));
+                                               success = e_webdav_session_options_sync (bbdav->priv->webdav, 
uri,
+                                                       &capabilities, &allows, cancellable, &local_error);
+                                       }
 
-       status_code = soup_session_send_message (webdav->priv->session, message);
+                                       g_free (uri);
+                               }
 
-       if (SOUP_STATUS_IS_SUCCESSFUL (status_code))
-               e_backend_ensure_source_status_connected (E_BACKEND (webdav));
+                               g_free (path);
+                       } else if (soup_uri->host && e_util_utf8_strstrcase (soup_uri->host, 
".googleusercontent.com")) {
+                               g_clear_error (&local_error);
+                               success = TRUE;
 
-       return status_code;
-}
+                               /* Google's WebDAV doesn't like OPTIONS, hard-code it */
+                               capabilities = g_hash_table_new_full (camel_strcase_hash, 
camel_strcase_equal, g_free, NULL);
+                               g_hash_table_insert (capabilities, g_strdup 
(E_WEBDAV_CAPABILITY_ADDRESSBOOK), GINT_TO_POINTER (1));
 
-static EContact *
-download_contact (EBookBackendWebdav *webdav,
-                  const gchar *uri,
-                  GCancellable *cancellable)
-{
-       SoupMessage *message;
-       const gchar  *etag;
-       EContact    *contact;
-       guint        status;
-
-       message = soup_message_new (SOUP_METHOD_GET, uri);
-       soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
-       soup_message_headers_append (message->request_headers, "Connection", "close");
-
-       status = send_and_handle_ssl (webdav, message, cancellable);
-       if (status != 200) {
-               g_warning ("Couldn't load '%s' (http status %d)", uri, status);
-               g_object_unref (message);
-               return NULL;
-       }
+                               allows = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, 
g_free, NULL);
+                               g_hash_table_insert (allows, g_strdup (SOUP_METHOD_PUT), GINT_TO_POINTER (1));
+                       }
 
-       if (message->response_body == NULL) {
-               g_message ("no response body after requesting '%s'", uri);
-               g_object_unref (message);
-               return NULL;
+                       soup_uri_free (soup_uri);
+               }
        }
 
-       if (message->response_body->length <= 11 || 0 != g_ascii_strncasecmp ((const gchar *) 
message->response_body->data, "BEGIN:VCARD", 11)) {
-               g_object_unref (message);
-               return NULL;
-       }
+       if (success) {
+               ESourceWebdav *webdav_extension;
+               EBookCache *book_cache;
+               SoupURI *soup_uri;
+               gboolean is_writable;
+               gboolean addressbook;
 
-       etag = soup_message_headers_get_list (message->response_headers, "ETag");
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+               book_cache = e_book_meta_backend_ref_cache (meta_backend);
 
-       /* we use our URI as UID */
-       contact = e_contact_new_from_vcard (message->response_body->data);
-       if (contact == NULL) {
-               g_warning ("Invalid vcard at '%s'", uri);
-               g_object_unref (message);
-               return NULL;
-       }
+               /* The POST added for FastMail servers, which doesn't advertise PUT on collections. */
+               is_writable = allows && (
+                       g_hash_table_contains (allows, SOUP_METHOD_PUT) ||
+                       g_hash_table_contains (allows, SOUP_METHOD_POST) ||
+                       g_hash_table_contains (allows, SOUP_METHOD_DELETE));
 
-       webdav_contact_set_href (contact, uri);
-       /* the etag is remembered in the WEBDAV_CONTACT_ETAG field */
-       if (etag != NULL) {
-               webdav_contact_set_etag (contact, etag);
-       }
+               addressbook = capabilities && g_hash_table_contains (capabilities, 
E_WEBDAV_CAPABILITY_ADDRESSBOOK);
 
-       g_object_unref (message);
-       return contact;
-}
+               if (addressbook) {
+                       e_book_backend_set_writable (E_BOOK_BACKEND (bbdav), is_writable);
 
-static guint
-upload_contact (EBookBackendWebdav *webdav,
-               const gchar *uri,
-                EContact *contact,
-                gchar **reason,
-                GCancellable *cancellable)
-{
-       ESource     *source;
-       ESourceWebdav *webdav_extension;
-       SoupMessage *message;
-       gchar       *etag;
-       const gchar  *new_etag, *redir_uri;
-       gchar        *request;
-       guint        status;
-       gboolean     avoid_ifmatch;
-       const gchar *extension_name;
-
-       g_return_val_if_fail (uri != NULL, SOUP_STATUS_BAD_REQUEST);
-
-       source = e_backend_get_source (E_BACKEND (webdav));
-
-       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-       webdav_extension = e_source_get_extension (source, extension_name);
-
-       message = soup_message_new (SOUP_METHOD_PUT, uri);
-       soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
-       soup_message_headers_append (message->request_headers, "Connection", "close");
-
-       avoid_ifmatch = e_source_webdav_get_avoid_ifmatch (webdav_extension);
-
-       /* some servers (like apache < 2.2.8) don't handle If-Match, correctly so
-        * we can leave it out */
-       if (!avoid_ifmatch) {
-               /* only override if etag is still the same on the server */
-               etag = webdav_contact_get_etag (contact);
-               if (etag == NULL) {
-                       soup_message_headers_append (
-                               message->request_headers,
-                               "If-None-Match", "*");
-               } else if (etag[0] == 'W' && etag[1] == '/') {
-                       g_warning ("we only have a weak ETag, don't use If-Match synchronisation");
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
                } else {
-                       soup_message_headers_append (
-                               message->request_headers,
-                               "If-Match", etag);
-               }
+                       gchar *uri;
 
-               g_free (etag);
-       }
+                       uri = soup_uri_to_string (soup_uri, FALSE);
 
-       /* Remove the stored ETag, before saving to the server */
-       webdav_contact_set_etag (contact, NULL);
+                       success = FALSE;
+                       g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                               _("Given URL “%s” doesn't reference WebDAV address book"), uri);
 
-       request = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
-       soup_message_set_request (
-               message, "text/vcard", SOUP_MEMORY_TEMPORARY,
-               request, strlen (request));
+                       g_free (uri);
 
-       status = send_and_handle_ssl (webdav, message, cancellable);
-       new_etag = soup_message_headers_get_list (message->response_headers, "ETag");
-
-       redir_uri = soup_message_headers_get_list (message->response_headers, "Location");
-
-       /* set UID and WEBDAV_CONTACT_ETAG fields */
-       webdav_contact_set_etag (contact, new_etag);
-       if (redir_uri && *redir_uri) {
-               if (!strstr (redir_uri, "://")) {
-                       /* it's a relative URI */
-                       SoupURI *suri = soup_uri_new (uri);
-                       gchar *full_uri;
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+               }
 
-                       if (*redir_uri != '/' && *redir_uri != '\\') {
-                               gchar *slashed_path = g_strconcat ("/", redir_uri, NULL);
+               g_clear_object (&book_cache);
+               soup_uri_free (soup_uri);
+       }
 
-                               soup_uri_set_path (suri, slashed_path);
-                               g_free (slashed_path);
-                       } else {
-                               soup_uri_set_path (suri, redir_uri);
-                       }
-                       full_uri = soup_uri_to_string (suri, FALSE);
+       if (success) {
+               gchar *ctag = NULL;
 
-                       webdav_contact_set_href (contact, full_uri);
+               /* Some servers, notably Google, allow OPTIONS when not
+                  authorized (aka without credentials), thus try something
+                  more aggressive, just in case.
 
-                       g_free (full_uri);
-                       soup_uri_free (suri);
+                  The 'getctag' extension is not required, thuch check
+                  for unauthorized error only. */
+               if (!e_webdav_session_getctag_sync (bbdav->priv->webdav, NULL, &ctag, cancellable, 
&local_error) &&
+                   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       success = FALSE;
                } else {
-                       webdav_contact_set_href (contact, redir_uri);
+                       g_clear_error (&local_error);
                }
-       } else {
-               webdav_contact_set_href (contact, uri);
-       }
-
-       if (reason != NULL) {
-               const gchar *phrase;
 
-               phrase = message->reason_phrase;
-               if (phrase == NULL)
-                       phrase = soup_status_get_phrase (message->status_code);
-               if (phrase == NULL)
-                       phrase = _("Unknown error");
-
-               *reason = g_strdup (phrase);
+               g_free (ctag);
        }
 
-       g_object_unref (message);
-       g_free (request);
-
-       return status;
-}
-
-static gboolean
-webdav_handle_auth_request (EBookBackendWebdav *webdav,
-                            GError **error)
-{
-       EBookBackendWebdavPrivate *priv = webdav->priv;
-
-       if (priv->username != NULL) {
-               g_free (priv->username);
-               priv->username = NULL;
-               g_free (priv->password);
-               priv->password = NULL;
-
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_AUTHENTICATION_FAILED,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_AUTHENTICATION_FAILED));
+       if (success) {
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
        } else {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_AUTHENTICATION_REQUIRED,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_AUTHENTICATION_REQUIRED));
-       }
-
-       return FALSE;
-}
-
-static guint
-delete_contact (EBookBackendWebdav *webdav,
-                const gchar *uri,
-                GCancellable *cancellable)
-{
-       SoupMessage *message;
-       guint        status;
-
-       message = soup_message_new (SOUP_METHOD_DELETE, uri);
-       soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
-       soup_message_headers_append (message->request_headers, "Connection", "close");
-
-       status = send_and_handle_ssl (webdav, message, cancellable);
-       g_object_unref (message);
-
-       return status;
-}
-
-typedef struct parser_strings_t {
-       const xmlChar *multistatus;
-       const xmlChar *dav;
-       const xmlChar *href;
-       const xmlChar *response;
-       const xmlChar *propstat;
-       const xmlChar *prop;
-       const xmlChar *getetag;
-} parser_strings_t;
-
-typedef struct response_element_t response_element_t;
-struct response_element_t {
-       xmlChar            *href;
-       xmlChar            *etag;
-       response_element_t *next;
-};
-
-static response_element_t *
-parse_response_tag (const parser_strings_t *strings,
-                    xmlTextReaderPtr reader)
-{
-       xmlChar            *href = NULL;
-       xmlChar            *etag = NULL;
-       gint                 depth = xmlTextReaderDepth (reader);
-       response_element_t *element;
-
-       while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > depth) {
-               const xmlChar *tag_name;
-               if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
-                       continue;
+               gboolean credentials_empty;
+               gboolean is_ssl_error;
+
+               credentials_empty = !credentials || !e_named_parameters_count (credentials);
+               is_ssl_error = g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED);
+
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+
+               /* because evolution knows only G_IO_ERROR_CANCELLED */
+               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_CANCELLED)) {
+                       local_error->domain = G_IO_ERROR;
+                       local_error->code = G_IO_ERROR_CANCELLED;
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) && 
credentials_empty) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       if (credentials_empty)
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       else
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+               } else if (local_error) {
+                       g_propagate_error (error, local_error);
+                       local_error = NULL;
+               } else {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               _("Unknown error"));
+               }
 
-               if (xmlTextReaderConstNamespaceUri (reader) != strings->dav)
-                       continue;
+               if (is_ssl_error) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
 
-               tag_name = xmlTextReaderConstLocalName (reader);
-               if (tag_name == strings->href) {
-                       if (href != NULL) {
-                               /* multiple href elements?!? */
-                               xmlFree (href);
-                       }
-                       href = xmlTextReaderReadString (reader);
-               } else if (tag_name == strings->propstat) {
-                       /* find <propstat><prop><getetag> hierarchy */
-                       gint depth2 = xmlTextReaderDepth (reader);
-                       while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > depth2) {
-                               gint depth3;
-                               if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
-                                       continue;
-
-                               if (xmlTextReaderConstNamespaceUri (reader) != strings->dav
-                                               || xmlTextReaderConstLocalName (reader) != strings->prop)
-                                       continue;
-
-                               depth3 = xmlTextReaderDepth (reader);
-                               while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > 
depth3) {
-                                       if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
-                                               continue;
-
-                                       if (xmlTextReaderConstNamespaceUri (reader) != strings->dav
-                                                       || xmlTextReaderConstLocalName (reader)
-                                                       != strings->getetag)
-                                               continue;
-
-                                       if (etag != NULL) {
-                                               /* multiple etag elements?!? */
-                                               xmlFree (etag);
-                                       }
-                                       etag = xmlTextReaderReadString (reader);
-                               }
-                       }
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+                       e_soup_session_get_ssl_error_details (E_SOUP_SESSION (bbdav->priv->webdav), 
out_certificate_pem, out_certificate_errors);
+               } else {
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
                }
        }
 
-       if (href == NULL) {
-               g_warning ("webdav returned response element without href");
-               return NULL;
-       }
+       if (capabilities)
+               g_hash_table_destroy (capabilities);
+       if (allows)
+               g_hash_table_destroy (allows);
 
-       /* append element to list */
-       element = g_malloc (sizeof (element[0]));
-       element->href = href;
-       element->etag = etag;
-       return element;
+       if (!success)
+               g_clear_object (&bbdav->priv->webdav);
+
+       return success;
 }
 
-static response_element_t *
-parse_propfind_response (xmlTextReaderPtr reader)
+static gboolean
+ebb_webdav_disconnect_sync (EBookMetaBackend *meta_backend,
+                           GCancellable *cancellable,
+                           GError **error)
 {
-       parser_strings_t    strings;
-       response_element_t *elements;
-
-       /* get internalized versions of some strings to avoid strcmp while
-        * parsing */
-       strings.multistatus = xmlTextReaderConstString (reader, BAD_CAST "multistatus");
-       strings.dav = xmlTextReaderConstString (reader, BAD_CAST "DAV:");
-       strings.href = xmlTextReaderConstString (reader, BAD_CAST "href");
-       strings.response = xmlTextReaderConstString (reader, BAD_CAST "response");
-       strings.propstat = xmlTextReaderConstString (reader, BAD_CAST "propstat");
-       strings.prop = xmlTextReaderConstString (reader, BAD_CAST "prop");
-       strings.getetag = xmlTextReaderConstString (reader, BAD_CAST "getetag");
-
-       while (xmlTextReaderRead (reader) == 1 && xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT) {
-       }
-
-       if (xmlTextReaderConstLocalName (reader) != strings.multistatus
-                       || xmlTextReaderConstNamespaceUri (reader) != strings.dav) {
-               g_warning ("webdav PROPFIND result is not <DAV:multistatus>");
-               return NULL;
-       }
-
-       elements = NULL;
+       EBookBackendWebDAV *bbdav;
+       ESource *source;
 
-       /* parse all DAV:response tags */
-       while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > 0) {
-               response_element_t *element;
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
 
-               if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
-                       continue;
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
 
-               if (xmlTextReaderConstLocalName (reader) != strings.response
-                               || xmlTextReaderConstNamespaceUri (reader) != strings.dav)
-                       continue;
+       if (bbdav->priv->webdav)
+               soup_session_abort (SOUP_SESSION (bbdav->priv->webdav));
 
-               element = parse_response_tag (&strings, reader);
-               if (element == NULL)
-                       continue;
+       g_clear_object (&bbdav->priv->webdav);
 
-               element->next = elements;
-               elements = element;
-       }
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
 
-       return elements;
+       return TRUE;
 }
 
-static SoupMessage *
-send_propfind (EBookBackendWebdav *webdav,
-              GCancellable *cancellable,
-              GError **error)
+static void
+ebb_webdav_update_nfo_with_contact (EBookMetaBackendInfo *nfo,
+                                   EContact *contact,
+                                   const gchar *etag)
 {
-       SoupMessage               *message;
-       EBookBackendWebdavPrivate *priv = webdav->priv;
-       const gchar               *request =
-               "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
-               "<propfind xmlns=\"DAV:\"><prop><getetag/></prop></propfind>";
-
-       message = soup_message_new (SOUP_METHOD_PROPFIND, priv->uri);
-       if (!message) {
-               g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Malformed URI: %s"), 
priv->uri);
-               return NULL;
-       }
-
-       soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
-       soup_message_headers_append (message->request_headers, "Connection", "close");
-       soup_message_headers_append (message->request_headers, "Depth", "1");
-       soup_message_set_request (
-               message, "text/xml", SOUP_MEMORY_TEMPORARY,
-               (gchar *) request, strlen (request));
-
-       send_and_handle_ssl (webdav, message, cancellable);
-
-       return message;
-}
+       const gchar *uid;
 
-static xmlXPathObjectPtr
-xpath_eval (xmlXPathContextPtr ctx,
-            const gchar *format,
-            ...)
-{
-       xmlXPathObjectPtr  result;
-       va_list            args;
-       gchar              *expr;
+       g_return_if_fail (nfo != NULL);
+       g_return_if_fail (E_IS_CONTACT (contact));
 
-       if (ctx == NULL) {
-               return NULL;
-       }
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
 
-       va_start (args, format);
-       expr = g_strdup_vprintf (format, args);
-       va_end (args);
+       if (!etag || !*etag)
+               etag = nfo->revision;
 
-       result = xmlXPathEvalExpression ((xmlChar *) expr, ctx);
-       g_free (expr);
+       e_vcard_util_set_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG, etag);
 
-       if (result == NULL) {
-               return NULL;
-       }
+       g_warn_if_fail (nfo->object == NULL);
+       nfo->object = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
 
-       if (result->type == XPATH_NODESET &&
-           xmlXPathNodeSetIsEmpty (result->nodesetval)) {
-               xmlXPathFreeObject (result);
-               return NULL;
+       if (!nfo->uid || !*(nfo->uid)) {
+               g_free (nfo->uid);
+               nfo->uid = g_strdup (uid);
        }
 
-       return result;
-}
-
-static gchar *
-xp_object_get_string (xmlXPathObjectPtr result)
-{
-       gchar *ret = NULL;
+       if (g_strcmp0 (etag, nfo->revision) != 0) {
+               gchar *copy = g_strdup (etag);
 
-       if (result == NULL)
-               return ret;
-
-       if (result->type == XPATH_STRING) {
-               ret = g_strdup ((gchar *) result->stringval);
+               g_free (nfo->revision);
+               nfo->revision = copy;
        }
-
-       xmlXPathFreeObject (result);
-       return ret;
-}
-
-static guint
-xp_object_get_status (xmlXPathObjectPtr result)
-{
-       gboolean res;
-       guint    ret = 0;
-
-       if (result == NULL)
-               return ret;
-
-       if (result->type == XPATH_STRING) {
-               res = soup_headers_parse_status_line ((gchar *) result->stringval, NULL, &ret, NULL);
-               if (!res) {
-                       ret = 0;
-               }
-       }
-
-       xmlXPathFreeObject (result);
-       return ret;
 }
 
 static gboolean
-check_addressbook_changed (EBookBackendWebdav *webdav,
-                           gchar **new_ctag,
-                           GCancellable *cancellable)
+ebb_webdav_multiget_response_cb (EWebDAVSession *webdav,
+                                xmlXPathContextPtr xpath_ctx,
+                                const gchar *xpath_prop_prefix,
+                                const SoupURI *request_uri,
+                                const gchar *href,
+                                guint status_code,
+                                gpointer user_data)
 {
-       gboolean res = TRUE;
-       const gchar *request = "<?xml version=\"1.0\" encoding=\"utf-8\"?><propfind 
xmlns=\"DAV:\"><prop><getctag/></prop></propfind>";
-       EBookBackendWebdavPrivate *priv;
-       SoupMessage *message;
+       GSList **from_link = user_data;
 
-       g_return_val_if_fail (webdav != NULL, TRUE);
-       g_return_val_if_fail (new_ctag != NULL, TRUE);
+       g_return_val_if_fail (from_link != NULL, FALSE);
 
-       *new_ctag = NULL;
-       priv = webdav->priv;
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CARDDAV, NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               gchar *address_data, *etag;
 
-       if (!priv->supports_getctag) {
-               gint64 real_time_us = g_get_real_time ();
+               g_return_val_if_fail (href != NULL, FALSE);
 
-               /* Fifteen minutes in microseconds */
-               if (real_time_us - priv->last_server_test_us < 15 * 60 * 1000 * 1000)
-                       return FALSE;
+               address_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:address-data", xpath_prop_prefix);
+               etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:getetag", xpath_prop_prefix));
 
-               priv->last_server_test_us = real_time_us;
+               if (address_data) {
+                       EContact *contact;
 
-               return TRUE;
-       }
+                       contact = e_contact_new_from_vcard (address_data);
+                       if (contact) {
+                               const gchar *uid;
 
-       priv->supports_getctag = FALSE;
+                               uid = e_contact_get_const (contact, E_CONTACT_UID);
+                               if (uid) {
+                                       GSList *link;
 
-       message = soup_message_new (SOUP_METHOD_PROPFIND, priv->uri);
-       if (!message)
-               return TRUE;
+                                       for (link = *from_link; link; link = g_slist_next (link)) {
+                                               EBookMetaBackendInfo *nfo = link->data;
 
-       soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
-       soup_message_headers_append (message->request_headers, "Connection", "close");
-       soup_message_headers_append (message->request_headers, "Depth", "0");
-       soup_message_set_request (message, "text/xml", SOUP_MEMORY_TEMPORARY, (gchar *) request, strlen 
(request));
-       send_and_handle_ssl (webdav, message, cancellable);
-
-       if (message->status_code == 207 && message->response_body) {
-               xmlDocPtr xml;
-
-               xml = xmlReadMemory (message->response_body->data, message->response_body->length, NULL, 
NULL, XML_PARSE_NOWARNING);
-               if (xml) {
-                       const gchar *GETCTAG_XPATH_STATUS = 
"string(/D:multistatus/D:response/D:propstat/D:prop/D:getctag/../../D:status)";
-                       const gchar *GETCTAG_XPATH_VALUE = 
"string(/D:multistatus/D:response/D:propstat/D:prop/D:getctag)";
-                       xmlXPathContextPtr xpctx;
-
-                       xpctx = xmlXPathNewContext (xml);
-                       xmlXPathRegisterNs (xpctx, (xmlChar *) "D", (xmlChar *) "DAV:");
-
-                       if (xp_object_get_status (xpath_eval (xpctx, GETCTAG_XPATH_STATUS)) == 200) {
-                               gchar *txt = xp_object_get_string (xpath_eval (xpctx, GETCTAG_XPATH_VALUE));
-                               const gchar *stored_version;
-                               gboolean old_version;
-
-                               g_mutex_lock (&priv->cache_lock);
-                               stored_version = e_file_cache_get_object (E_FILE_CACHE (priv->cache), 
WEBDAV_CACHE_VERSION_KEY);
-
-                               /* The ETag was moved from REV to its own attribute, thus
-                                * if the cache version is too low, update it. */
-                               old_version = !stored_version || atoi (stored_version) < atoi 
(WEBDAV_CACHE_VERSION);
-                               g_mutex_unlock (&priv->cache_lock);
-
-                               if (txt && *txt) {
-                                       gint len = strlen (txt);
-
-                                       if (*txt == '\"' && len > 2 && txt[len - 1] == '\"') {
-                                               /* dequote */
-                                               *new_ctag = g_strndup (txt + 1, len - 2);
-                                       } else {
-                                               *new_ctag = txt;
-                                               txt = NULL;
-                                       }
+                                               if (!nfo)
+                                                       continue;
 
-                                       if (*new_ctag) {
-                                               const gchar *my_ctag;
+                                               if (g_strcmp0 (nfo->extra, href) == 0) {
+                                                       /* If the server returns data in the same order as it 
had been requested,
+                                                          then this speeds up lookup for the matching 
object. */
+                                                       if (link == *from_link)
+                                                               *from_link = g_slist_next (*from_link);
 
-                                               g_mutex_lock (&priv->cache_lock);
-                                               my_ctag = e_file_cache_get_object (E_FILE_CACHE 
(priv->cache), WEBDAV_CTAG_KEY);
-                                               res = old_version || !my_ctag || !g_str_equal (my_ctag, 
*new_ctag);
+                                                       ebb_webdav_update_nfo_with_contact (nfo, contact, 
etag);
 
-                                               priv->supports_getctag = TRUE;
-                                               g_mutex_unlock (&priv->cache_lock);
+                                                       break;
+                                               }
                                        }
                                }
 
-                               g_free (txt);
-
-                               if (old_version) {
-                                       g_mutex_lock (&priv->cache_lock);
-
-                                       if (!e_file_cache_replace_object (E_FILE_CACHE (priv->cache),
-                                               WEBDAV_CACHE_VERSION_KEY,
-                                               WEBDAV_CACHE_VERSION))
-                                               e_file_cache_add_object (
-                                                       E_FILE_CACHE (priv->cache),
-                                                       WEBDAV_CACHE_VERSION_KEY,
-                                                       WEBDAV_CACHE_VERSION);
-
-                                       g_mutex_unlock (&priv->cache_lock);
-                               }
+                               g_object_unref (contact);
                        }
-
-                       xmlXPathFreeContext (xpctx);
-                       xmlFreeDoc (xml);
                }
-       }
-
-       g_object_unref (message);
-
-       return res;
-}
 
-static void
-remove_unknown_contacts_cb (gpointer href,
-                           gpointer pcontact,
-                           gpointer pwebdav)
-{
-       EContact *contact = pcontact;
-       EBookBackendWebdav *webdav = pwebdav;
-       const gchar *uid;
+               g_free (address_data);
+               g_free (etag);
+       }
 
-       uid = e_contact_get_const (contact, E_CONTACT_UID);
-       if (uid && e_book_backend_cache_remove_contact (webdav->priv->cache, uid))
-               e_book_backend_notify_remove ((EBookBackend *) webdav, uid);
+       return TRUE;
 }
 
 static gboolean
-download_contacts (EBookBackendWebdav *webdav,
-                   EFlag *running,
-                   EDataBookView *book_view,
-                  gboolean force,
-                   GCancellable *cancellable,
-                   GError **error)
+ebb_webdav_multiget_from_sets_sync (EBookBackendWebDAV *bbdav,
+                                   GSList **in_link,
+                                   GSList **set2,
+                                   GCancellable *cancellable,
+                                   GError **error)
 {
-       EBookBackendWebdavPrivate *priv = webdav->priv;
-       EBookBackend              *book_backend;
-       SoupMessage               *message;
-       guint                      status;
-       xmlTextReaderPtr           reader;
-       response_element_t        *elements;
-       response_element_t        *element;
-       response_element_t        *next;
-       gint                        count;
-       gint                        i;
-       gchar                     *new_ctag = NULL;
-       GHashTable                *href_to_contact;
-       GList                     *cached_contacts, *iter;
-
-       g_mutex_lock (&priv->update_lock);
-
-       if (!force && !check_addressbook_changed (webdav, &new_ctag, cancellable)) {
-               g_free (new_ctag);
-               g_mutex_unlock (&priv->update_lock);
-               return TRUE;
-       }
+       EXmlDocument *xml;
+       gint left_to_go = E_WEBDAV_MAX_MULTIGET_AMOUNT;
+       GSList *link;
+       gboolean success = TRUE;
 
-       book_backend = E_BOOK_BACKEND (webdav);
+       g_return_val_if_fail (in_link != NULL, FALSE);
+       g_return_val_if_fail (*in_link != NULL, FALSE);
+       g_return_val_if_fail (set2 != NULL, FALSE);
 
-       if (book_view != NULL) {
-               e_data_book_view_notify_progress (book_view, -1,
-                               _("Loading Addressbook summary..."));
-       }
+       xml = e_xml_document_new (E_WEBDAV_NS_CARDDAV, "addressbook-multiget");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       message = send_propfind (webdav, cancellable, error);
-       if (!message) {
-               g_free (new_ctag);
-               if (book_view)
-                       e_data_book_view_notify_progress (book_view, -1, NULL);
-               g_mutex_unlock (&priv->update_lock);
-               return FALSE;
-       }
+       e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
 
-       status = message->status_code;
-
-       if (status == SOUP_STATUS_UNAUTHORIZED ||
-           status == SOUP_STATUS_PROXY_UNAUTHORIZED ||
-           status == SOUP_STATUS_FORBIDDEN) {
-               g_object_unref (message);
-               g_free (new_ctag);
-               if (book_view)
-                       e_data_book_view_notify_progress (book_view, -1, NULL);
-               g_mutex_unlock (&priv->update_lock);
-               return webdav_handle_auth_request (webdav, error);
-       }
-       if (status != 207) {
-               g_set_error (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OTHER_ERROR,
-                       _("PROPFIND on webdav failed with HTTP status %d (%s)"),
-                       status,
-                       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")));
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "address-data");
+       e_xml_document_end_element (xml); /* prop */
 
-               g_object_unref (message);
-               g_free (new_ctag);
+       link = *in_link;
 
-               if (book_view)
-                       e_data_book_view_notify_progress (book_view, -1, NULL);
+       while (link && left_to_go > 0) {
+               EBookMetaBackendInfo *nfo = link->data;
+               SoupURI *suri;
+               gchar *path = NULL;
 
-               g_mutex_unlock (&priv->update_lock);
+               link = g_slist_next (link);
+               if (!link) {
+                       link = *set2;
+                       *set2 = NULL;
+               }
 
-               return FALSE;
-       }
-       if (message->response_body == NULL) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OTHER_ERROR,
-                       _("No response body in webdav PROPFIND result"));
+               if (!nfo)
+                       continue;
 
-               g_object_unref (message);
-               g_free (new_ctag);
+               left_to_go--;
 
-               if (book_view)
-                       e_data_book_view_notify_progress (book_view, -1, NULL);
+               suri = soup_uri_new (nfo->extra);
+               if (suri) {
+                       path = soup_uri_to_string (suri, TRUE);
+                       soup_uri_free (suri);
+               }
 
-               g_mutex_unlock (&priv->update_lock);
+               e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "href");
+               e_xml_document_write_string (xml, path ? path : nfo->extra);
+               e_xml_document_end_element (xml); /* href */
 
-               return FALSE;
+               g_free (path);
        }
 
-       /* parse response */
-       reader = xmlReaderForMemory (
-               message->response_body->data,
-               message->response_body->length, NULL, NULL,
-               XML_PARSE_NOWARNING);
+       if (left_to_go != E_WEBDAV_MAX_MULTIGET_AMOUNT && success) {
+               GSList *from_link = *in_link;
 
-       elements = parse_propfind_response (reader);
-
-       /* count contacts */
-       count = 0;
-       for (element = elements; element != NULL; element = element->next) {
-               ++count;
+               success = e_webdav_session_report_sync (bbdav->priv->webdav, NULL, NULL, xml,
+                       ebb_webdav_multiget_response_cb, &from_link, NULL, NULL, cancellable, error);
        }
 
-       href_to_contact = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
-       g_mutex_lock (&priv->cache_lock);
-       e_file_cache_freeze_changes (E_FILE_CACHE (priv->cache));
-       cached_contacts = e_book_backend_cache_get_contacts (priv->cache, NULL);
-       for (iter = cached_contacts; iter; iter = g_list_next (iter)) {
-               EContact *contact = iter->data;
-               gchar *href;
-
-               if (!contact)
-                       continue;
+       g_object_unref (xml);
 
-               href = webdav_contact_get_href (contact);
+       *in_link = link;
 
-               if (href)
-                       g_hash_table_insert (href_to_contact, href, g_object_ref (contact));
-       }
-       g_list_free_full (cached_contacts, g_object_unref);
-       g_mutex_unlock (&priv->cache_lock);
-
-       /* download contacts */
-       i = 0;
-       for (element = elements; element != NULL; element = element->next, ++i) {
-               const gchar  *uri;
-               const gchar *etag;
-               EContact    *contact;
-               gchar *complete_uri, *stored_etag;
-
-               /* stop downloading if search was aborted */
-               if (running != NULL && !e_flag_is_set (running))
-                       break;
-
-               if (book_view != NULL) {
-                       gfloat percent = 100.0 / count * i;
-                       gchar buf[100];
-                       snprintf (buf, sizeof (buf), _("Loading Contacts (%d%%)"), (gint) percent);
-                       e_data_book_view_notify_progress (book_view, -1, buf);
-               }
+       return success;
+}
 
-               /* skip collections */
-               uri = (const gchar *) element->href;
-               if (uri[strlen (uri) - 1] == '/')
-                       continue;
+static gboolean
+ebb_webdav_get_contact_items_cb (EWebDAVSession *webdav,
+                                xmlXPathContextPtr xpath_ctx,
+                                const gchar *xpath_prop_prefix,
+                                const SoupURI *request_uri,
+                                const gchar *href,
+                                guint status_code,
+                                gpointer user_data)
+{
+       GHashTable *known_items = user_data; /* gchar *href ~> EBookMetaBackendInfo * */
 
-               /* uri might be relative, construct complete one */
-               if (uri[0] == '/') {
-                       SoupURI *soup_uri = soup_uri_new (priv->uri);
-                       g_free (soup_uri->path);
-                       soup_uri->path = g_strdup (uri);
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (known_items != NULL, FALSE);
 
-                       complete_uri = soup_uri_to_string (soup_uri, FALSE);
-                       soup_uri_free (soup_uri);
-               } else {
-                       complete_uri = g_strdup (uri);
-               }
+       if (xpath_prop_prefix &&
+           status_code == SOUP_STATUS_OK) {
+               EBookMetaBackendInfo *nfo;
+               gchar *etag;
 
-               etag = (const gchar *) element->etag;
+               g_return_val_if_fail (href != NULL, FALSE);
 
-               contact = g_hash_table_lookup (href_to_contact, complete_uri);
-               if (contact) {
-                       g_object_ref (contact);
-                       g_hash_table_remove (href_to_contact, complete_uri);
-                       stored_etag = webdav_contact_get_etag (contact);
-               } else {
-                       stored_etag = NULL;
+               /* Skip collection resource, if returned by the server (like iCloud.com does) */
+               if (g_str_has_suffix (href, "/") ||
+                   (request_uri && request_uri->path && g_str_has_suffix (href, request_uri->path))) {
+                       return TRUE;
                }
 
+               etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:getetag", xpath_prop_prefix));
+               /* Return 'TRUE' to not stop on faulty data from the server */
+               g_return_val_if_fail (etag != NULL, TRUE);
 
-               /* download contact if it is not cached or its ETag changed */
-               if (contact == NULL || etag == NULL || !stored_etag ||
-                   strcmp (stored_etag, etag) != 0) {
-                       if (contact != NULL)
-                               g_object_unref (contact);
-                       contact = download_contact (webdav, complete_uri, cancellable);
-                       if (contact != NULL) {
-                               const gchar *uid;
+               /* UID is unknown at this moment */
+               nfo = e_book_meta_backend_info_new ("", etag, NULL, href);
 
-                               uid = e_contact_get_const (contact, E_CONTACT_UID);
-
-                               g_mutex_lock (&priv->cache_lock);
-                               if (e_book_backend_cache_remove_contact (priv->cache, uid))
-                                       e_book_backend_notify_remove (book_backend, uid);
-                               e_book_backend_cache_add_contact (priv->cache, contact);
-                               g_mutex_unlock (&priv->cache_lock);
-                               e_book_backend_notify_update (book_backend, contact);
-                       }
-               }
+               g_free (etag);
+               g_return_val_if_fail (nfo != NULL, FALSE);
 
-               if (contact != NULL)
-                       g_object_unref (contact);
-               g_free (complete_uri);
-               g_free (stored_etag);
+               g_hash_table_insert (known_items, g_strdup (href), nfo);
        }
 
-       /* free element list */
-       for (element = elements; element != NULL; element = next) {
-               next = element->next;
+       return TRUE;
+}
 
-               xmlFree (element->href);
-               xmlFree (element->etag);
-               g_free (element);
-       }
+typedef struct _WebDAVChangesData {
+       GSList **out_modified_objects;
+       GSList **out_removed_objects;
+       GHashTable *known_items; /* gchar *href ~> EBookMetaBackendInfo * */
+} WebDAVChangesData;
 
-       xmlFreeTextReader (reader);
-       g_object_unref (message);
+static gboolean
+ebb_webdav_search_changes_cb (EBookCache *book_cache,
+                             const gchar *uid,
+                             const gchar *revision,
+                             const gchar *object,
+                             const gchar *extra,
+                             EOfflineState offline_state,
+                             gpointer user_data)
+{
+       WebDAVChangesData *ccd = user_data;
 
-       if (new_ctag) {
-               g_mutex_lock (&priv->cache_lock);
-               if (!e_file_cache_replace_object (E_FILE_CACHE (priv->cache), WEBDAV_CTAG_KEY, new_ctag))
-                       e_file_cache_add_object (E_FILE_CACHE (priv->cache), WEBDAV_CTAG_KEY, new_ctag);
-               g_mutex_unlock (&priv->cache_lock);
-       }
-       g_free (new_ctag);
+       g_return_val_if_fail (ccd != NULL, FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
 
-       if (book_view)
-               e_data_book_view_notify_progress (book_view, -1, NULL);
+       /* Can be NULL for added components in offline mode */
+       if (extra && *extra) {
+               EBookMetaBackendInfo *nfo;
 
-       g_mutex_lock (&priv->cache_lock);
+               nfo = g_hash_table_lookup (ccd->known_items, extra);
+               if (nfo) {
+                       if (g_strcmp0 (revision, nfo->revision) == 0) {
+                               g_hash_table_remove (ccd->known_items, extra);
+                       } else {
+                               if (!nfo->uid || !*(nfo->uid)) {
+                                       g_free (nfo->uid);
+                                       nfo->uid = g_strdup (uid);
+                               }
 
-       if (!g_cancellable_is_cancelled (cancellable) &&
-           (!running || e_flag_is_set (running))) {
-               /* clean-up the cache only if it wasn't cancelled during the work */
-               g_hash_table_foreach (href_to_contact, remove_unknown_contacts_cb, webdav);
-       }
-       e_file_cache_thaw_changes (E_FILE_CACHE (priv->cache));
-       g_mutex_unlock (&priv->cache_lock);
-       g_mutex_unlock (&priv->update_lock);
+                               *(ccd->out_modified_objects) = g_slist_prepend (*(ccd->out_modified_objects),
+                                       e_book_meta_backend_info_copy (nfo));
 
-       g_hash_table_destroy (href_to_contact);
+                               g_hash_table_remove (ccd->known_items, extra);
+                       }
+               } else {
+                       *(ccd->out_removed_objects) = g_slist_prepend (*(ccd->out_removed_objects),
+                               e_book_meta_backend_info_new (uid, revision, object, extra));
+               }
+       }
 
        return TRUE;
 }
 
-static gpointer
-book_view_thread (gpointer data)
+static gboolean
+ebb_webdav_get_changes_sync (EBookMetaBackend *meta_backend,
+                            const gchar *last_sync_tag,
+                            gboolean is_repeat,
+                            gchar **out_new_sync_tag,
+                            gboolean *out_repeat,
+                            GSList **out_created_objects,
+                            GSList **out_modified_objects,
+                            GSList **out_removed_objects,
+                            GCancellable *cancellable,
+                            GError **error)
 {
-       EDataBookView *book_view = data;
-       WebdavBackendSearchClosure *closure = get_closure (book_view);
-       EBookBackendWebdav *webdav = closure->webdav;
-
-       e_flag_set (closure->running);
-
-       /* ref the book view because it'll be removed and unrefed when/if
-        * it's stopped */
-       g_object_ref (book_view);
-
-       download_contacts (webdav, closure->running, book_view, FALSE, NULL, NULL);
-
-       g_object_unref (book_view);
-
-       return NULL;
-}
+       EBookBackendWebDAV *bbdav;
+       EXmlDocument *xml;
+       GHashTable *known_items; /* gchar *href ~> EBookMetaBackendInfo * */
+       GHashTableIter iter;
+       gpointer key = NULL, value = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag, FALSE);
+       g_return_val_if_fail (out_created_objects, FALSE);
+       g_return_val_if_fail (out_modified_objects, FALSE);
+       g_return_val_if_fail (out_removed_objects, FALSE);
+
+       *out_new_sync_tag = NULL;
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
+
+       if (bbdav->priv->ctag_supported) {
+               gchar *new_sync_tag = NULL;
+
+               success = e_webdav_session_getctag_sync (bbdav->priv->webdav, NULL, &new_sync_tag, 
cancellable, NULL);
+               if (!success) {
+                       bbdav->priv->ctag_supported = g_cancellable_set_error_if_cancelled (cancellable, 
error);
+                       if (bbdav->priv->ctag_supported || !bbdav->priv->webdav)
+                               return FALSE;
+               } else if (new_sync_tag && last_sync_tag && g_strcmp0 (last_sync_tag, new_sync_tag) == 0) {
+                       *out_new_sync_tag = new_sync_tag;
+                       return TRUE;
+               }
 
-static void
-e_book_backend_webdav_start_view (EBookBackend *backend,
-                                  EDataBookView *book_view)
-{
-       EBookBackendWebdav        *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       EBookBackendWebdavPrivate *priv = webdav->priv;
-       EBookBackendSExp *sexp;
-       const gchar *query;
-       GList *contacts;
-       GList *l;
-
-       sexp = e_data_book_view_get_sexp (book_view);
-       query = e_book_backend_sexp_text (sexp);
-
-       g_mutex_lock (&priv->cache_lock);
-       contacts = e_book_backend_cache_get_contacts (priv->cache, query);
-       g_mutex_unlock (&priv->cache_lock);
-
-       for (l = contacts; l != NULL; l = g_list_next (l)) {
-               EContact *contact = l->data;
-               e_data_book_view_notify_update (book_view, contact);
-               g_object_unref (contact);
+               *out_new_sync_tag = new_sync_tag;
        }
-       g_list_free (contacts);
 
-       /* this way the UI is notified about cached contacts immediately,
-        * and the update thread notifies about possible changes only */
-       e_data_book_view_notify_complete (book_view, NULL /* Success */);
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       if (e_backend_get_online (E_BACKEND (backend))) {
-               WebdavBackendSearchClosure *closure;
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_add_empty_element (xml, NULL, "getetag");
+       e_xml_document_end_element (xml); /* prop */
 
-               closure = init_closure (
-                       book_view, E_BOOK_BACKEND_WEBDAV (backend));
+       known_items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_book_meta_backend_info_free);
 
-               closure->thread = g_thread_new (
-                       NULL, book_view_thread, book_view);
+       success = e_webdav_session_propfind_sync (bbdav->priv->webdav, NULL, 
E_WEBDAV_DEPTH_THIS_AND_CHILDREN, xml,
+               ebb_webdav_get_contact_items_cb, known_items, cancellable, error);
 
-               e_flag_wait (closure->running);
-       }
-}
+       g_object_unref (xml);
 
-static void
-e_book_backend_webdav_stop_view (EBookBackend *backend,
-                                 EDataBookView *book_view)
-{
-       WebdavBackendSearchClosure *closure;
-       gboolean                    need_join;
+       if (success) {
+               EBookCache *book_cache;
+               WebDAVChangesData ccd;
 
-       if (!e_backend_get_online (E_BACKEND (backend)))
-               return;
+               ccd.out_modified_objects = out_modified_objects;
+               ccd.out_removed_objects = out_removed_objects;
+               ccd.known_items = known_items;
 
-       closure = get_closure (book_view);
-       if (closure == NULL)
-               return;
+               book_cache = e_book_meta_backend_ref_cache (meta_backend);
 
-       need_join = e_flag_is_set (closure->running);
-       e_flag_clear (closure->running);
+               success = e_book_cache_search_with_callback (book_cache, NULL, ebb_webdav_search_changes_cb, 
&ccd, cancellable, error);
 
-       if (need_join) {
-               g_thread_join (closure->thread);
-               closure->thread = NULL;
+               g_clear_object (&book_cache);
        }
-}
 
-/** authentication callback for libsoup */
-static void
-soup_authenticate (SoupSession *session,
-                   SoupMessage *message,
-                   SoupAuth *auth,
-                   gboolean retrying,
-                   gpointer data)
-{
-       EBookBackendWebdav        *webdav = data;
-       EBookBackendWebdavPrivate *priv = webdav->priv;
-
-       if (retrying)
-               return;
-
-       if (!priv->username || !*priv->username || !priv->password)
-               soup_message_set_status (message, SOUP_STATUS_FORBIDDEN);
-       else
-               soup_auth_authenticate (auth, priv->username, priv->password);
-}
-
-static void
-e_book_backend_webdav_notify_online_cb (EBookBackend *backend,
-                                        GParamSpec *pspec)
-{
-       gboolean online;
+       if (!success) {
+               g_hash_table_destroy (known_items);
+               return FALSE;
+       }
 
-       /* set_mode is called before the backend is loaded */
-       if (!e_book_backend_is_opened (backend))
-               return;
+       g_hash_table_iter_init (&iter, known_items);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               *out_created_objects = g_slist_prepend (*out_created_objects, e_book_meta_backend_info_copy 
(value));
+       }
 
-       /* XXX Could just use a property binding for this.
-        *     EBackend:online --> EBookBackend:writable */
-       online = e_backend_get_online (E_BACKEND (backend));
-       e_book_backend_set_writable (backend, online);
-}
+       g_hash_table_destroy (known_items);
 
-static void
-book_backend_webdav_dispose (GObject *object)
-{
-       EBookBackendWebdavPrivate *priv;
+       if (*out_created_objects || *out_modified_objects) {
+               GSList *link, *set2 = *out_modified_objects;
 
-       priv = E_BOOK_BACKEND_WEBDAV_GET_PRIVATE (object);
+               if (*out_created_objects) {
+                       link = *out_created_objects;
+               } else {
+                       link = set2;
+                       set2 = NULL;
+               }
 
-       g_clear_object (&priv->session);
-       g_clear_object (&priv->cache);
+               do {
+                       success = ebb_webdav_multiget_from_sets_sync (bbdav, &link, &set2, cancellable, 
error);
+               } while (success && link);
+       }
 
-       /* Chain up to parent's dispose() method. */
-       G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->dispose (object);
+       return success;
 }
 
-static void
-book_backend_webdav_finalize (GObject *object)
+static gboolean
+ebb_webdav_extract_existing_cb (EWebDAVSession *webdav,
+                               xmlXPathContextPtr xpath_ctx,
+                               const gchar *xpath_prop_prefix,
+                               const SoupURI *request_uri,
+                               const gchar *href,
+                               guint status_code,
+                               gpointer user_data)
 {
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (object);
-       EBookBackendWebdavPrivate *priv = webdav->priv;
+       GSList **out_existing_objects = user_data;
 
-       g_free (priv->uri);
-       g_free (priv->username);
-       g_free (priv->password);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
 
-       g_mutex_clear (&priv->cache_lock);
-       g_mutex_clear (&priv->update_lock);
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CARDDAV, NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               gchar *etag;
+               gchar *address_data;
 
-       /* Chain up to parent's finalize() method. */
-       G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->finalize (object);
-}
+               g_return_val_if_fail (href != NULL, FALSE);
 
-static gchar *
-book_backend_webdav_get_backend_property (EBookBackend *backend,
-                                          const gchar *prop_name)
-{
-       g_return_val_if_fail (prop_name != NULL, NULL);
+               etag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:getetag", xpath_prop_prefix);
+               address_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:address-data", xpath_prop_prefix);
 
-       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
-               return g_strdup ("net,do-initial-query,contact-lists,refresh-supported");
+               if (address_data) {
+                       EContact *contact;
 
-       } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
-               return g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));
+                       contact = e_contact_new_from_vcard (address_data);
+                       if (contact) {
+                               const gchar *uid;
 
-       } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
-               GString *fields;
-               gint ii;
+                               uid = e_contact_get_const (contact, E_CONTACT_UID);
 
-               fields = g_string_sized_new (1024);
+                               if (uid) {
+                                       etag = e_webdav_session_util_maybe_dequote (etag);
+                                       *out_existing_objects = g_slist_prepend (*out_existing_objects,
+                                               e_book_meta_backend_info_new (uid, etag, NULL, href));
+                               }
 
-               /* we support everything */
-               for (ii = 1; ii < E_CONTACT_FIELD_LAST; ii++) {
-                       if (fields->len > 0)
-                               g_string_append_c (fields, ',');
-                       g_string_append (fields, e_contact_field_name (ii));
+                               g_object_unref (contact);
+                       }
                }
 
-               return g_string_free (fields, FALSE);
+               g_free (address_data);
+               g_free (etag);
        }
 
-       /* Chain up to parent's get_backend_property() method. */
-       return E_BOOK_BACKEND_CLASS (e_book_backend_webdav_parent_class)->
-               get_backend_property (backend, prop_name);
+       return TRUE;
 }
 
 static gboolean
-book_backend_webdav_test_can_connect (EBookBackendWebdav *webdav,
-                                     gchar **out_certificate_pem,
-                                     GTlsCertificateFlags *out_certificate_errors,
-                                     GCancellable *cancellable,
-                                     GError **error)
+ebb_webdav_list_existing_sync (EBookMetaBackend *meta_backend,
+                              gchar **out_new_sync_tag,
+                              GSList **out_existing_objects,
+                              GCancellable *cancellable,
+                              GError **error)
 {
-       SoupMessage *message;
-       gboolean res = FALSE;
+       EBookBackendWebDAV *bbdav;
+       EXmlDocument *xml;
+       gboolean success;
 
-       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (webdav), FALSE);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
 
-       /* Send a PROPFIND to test whether user/password is correct. */
-       message = send_propfind (webdav, cancellable, error);
-       if (!message)
-               return FALSE;
+       *out_existing_objects = NULL;
 
-       switch (message->status_code) {
-               case SOUP_STATUS_OK:
-               case SOUP_STATUS_MULTI_STATUS:
-                       res = TRUE;
-                       break;
-
-               case SOUP_STATUS_UNAUTHORIZED:
-               case SOUP_STATUS_PROXY_UNAUTHORIZED:
-                       g_free (webdav->priv->username);
-                       webdav->priv->username = NULL;
-                       g_free (webdav->priv->password);
-                       webdav->priv->password = NULL;
-                       g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_FAILED,
-                               e_client_error_to_string (E_CLIENT_ERROR_AUTHENTICATION_FAILED));
-                       break;
-
-               case SOUP_STATUS_FORBIDDEN:
-                       g_free (webdav->priv->username);
-                       webdav->priv->username = NULL;
-                       g_free (webdav->priv->password);
-                       webdav->priv->password = NULL;
-                       g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_REQUIRED,
-                               e_client_error_to_string (E_CLIENT_ERROR_AUTHENTICATION_REQUIRED));
-                       break;
-
-               case SOUP_STATUS_SSL_FAILED:
-                       if (out_certificate_pem && out_certificate_errors) {
-                               GTlsCertificate *certificate = NULL;
-
-                               g_object_get (G_OBJECT (message),
-                                       "tls-certificate", &certificate,
-                                       "tls-errors", out_certificate_errors,
-                                       NULL);
-
-                               if (certificate) {
-                                       g_object_get (certificate, "certificate-pem", out_certificate_pem, 
NULL);
-                                       g_object_unref (certificate);
-                               }
-                       }
-
-                       g_set_error_literal (
-                               error, SOUP_HTTP_ERROR,
-                               message->status_code,
-                               message->reason_phrase);
-                       break;
-
-               default:
-                       g_set_error_literal (
-                               error, SOUP_HTTP_ERROR,
-                               message->status_code,
-                               message->reason_phrase);
-                       break;
-       }
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
 
-       g_object_unref (message);
+       xml = e_xml_document_new (E_WEBDAV_NS_CARDDAV, "addressbook-query");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       return res;
-}
-
-static gboolean
-book_backend_webdav_open_sync (EBookBackend *backend,
-                               GCancellable *cancellable,
-                               GError **error)
-{
-       EBookBackendWebdav        *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       ESourceAuthentication     *auth_extension;
-       ESourceOffline            *offline_extension;
-       ESourceWebdav             *webdav_extension;
-       ESource                   *source;
-       const gchar               *extension_name;
-       const gchar               *cache_dir;
-       gchar                     *filename;
-       SoupSession               *session;
-       SoupURI                   *suri;
-       gboolean                   success = TRUE;
-
-       /* will try fetch ctag for the first time, if it fails then sets this to FALSE */
-       webdav->priv->supports_getctag = TRUE;
-
-       source = e_backend_get_source (E_BACKEND (backend));
-       cache_dir = e_book_backend_get_cache_dir (backend);
-
-       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-       auth_extension = e_source_get_extension (source, extension_name);
-
-       extension_name = E_SOURCE_EXTENSION_OFFLINE;
-       offline_extension = e_source_get_extension (source, extension_name);
-
-       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-       webdav_extension = e_source_get_extension (source, extension_name);
-
-       webdav->priv->marked_for_offline =
-               e_source_offline_get_stay_synchronized (offline_extension);
-
-       if (!e_backend_get_online (E_BACKEND (backend)) &&
-           !webdav->priv->marked_for_offline ) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
-               return FALSE;
-       }
+       e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
 
-       suri = e_source_webdav_dup_soup_uri (webdav_extension);
-
-       webdav->priv->uri = soup_uri_to_string (suri, FALSE);
-       if (!webdav->priv->uri || !*webdav->priv->uri) {
-               g_free (webdav->priv->uri);
-               webdav->priv->uri = NULL;
-               soup_uri_free (suri);
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OTHER_ERROR,
-                       _("Cannot transform SoupURI to string"));
-               return FALSE;
-       }
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "address-data");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "prop");
+       e_xml_document_add_attribute (xml, NULL, "name", "VERSION");
+       e_xml_document_end_element (xml); /* prop / VERSION */
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "prop");
+       e_xml_document_add_attribute (xml, NULL, "name", "UID");
+       e_xml_document_end_element (xml); /* prop / UID */
+       e_xml_document_end_element (xml); /* address-data */
+       e_xml_document_end_element (xml); /* prop */
 
-       g_mutex_lock (&webdav->priv->cache_lock);
+       success = e_webdav_session_report_sync (bbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS, xml,
+               ebb_webdav_extract_existing_cb, out_existing_objects, NULL, NULL, cancellable, error);
 
-       /* make sure the uri ends with a forward slash */
-       if (webdav->priv->uri[strlen (webdav->priv->uri) - 1] != '/') {
-               gchar *tmp = webdav->priv->uri;
-               webdav->priv->uri = g_strconcat (tmp, "/", NULL);
-               g_free (tmp);
-       }
+       g_object_unref (xml);
 
-       if (!webdav->priv->cache) {
-               filename = g_build_filename (cache_dir, "cache.xml", NULL);
-               webdav->priv->cache = e_book_backend_cache_new (filename);
-               g_free (filename);
-       }
-       g_mutex_unlock (&webdav->priv->cache_lock);
+       if (success)
+               *out_existing_objects = g_slist_reverse (*out_existing_objects);
 
-       session = soup_session_sync_new ();
-       g_object_set (
-               session,
-               SOUP_SESSION_TIMEOUT, 90,
-               SOUP_SESSION_SSL_STRICT, TRUE,
-               SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
-               SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
-               NULL);
-
-       e_binding_bind_property (
-               backend, "proxy-resolver",
-               session, "proxy-resolver",
-               G_BINDING_SYNC_CREATE);
-
-       e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
-
-       g_signal_connect (
-               session, "authenticate",
-               G_CALLBACK (soup_authenticate), webdav);
-
-       webdav->priv->session = session;
-       webdav_debug_setup (webdav->priv->session);
+       return success;
+}
 
-       e_backend_set_online (E_BACKEND (backend), TRUE);
-       e_book_backend_set_writable (backend, TRUE);
+static gchar *
+ebb_webdav_uid_to_uri (EBookBackendWebDAV *bbdav,
+                      const gchar *uid,
+                      const gchar *extension)
+{
+       ESourceWebdav *webdav_extension;
+       SoupURI *soup_uri;
+       gchar *uri, *tmp, *filename;
 
-       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (bbdav), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
 
-       if (e_source_authentication_required (auth_extension)) {
-               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+       webdav_extension = e_source_get_extension (e_backend_get_source (E_BACKEND (bbdav)), 
E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+       g_return_val_if_fail (soup_uri != NULL, NULL);
 
-               success = e_backend_credentials_required_sync (E_BACKEND (backend),
-                       E_SOURCE_CREDENTIALS_REASON_REQUIRED, NULL, 0, NULL,
-                       cancellable, error);
+       if (extension) {
+               tmp = g_strconcat (uid, extension, NULL);
+               filename = soup_uri_encode (tmp, NULL);
+               g_free (tmp);
        } else {
-               gchar *certificate_pem = NULL;
-               GTlsCertificateFlags certificate_errors = 0;
-               GError *local_error = NULL;
-
-               success = book_backend_webdav_test_can_connect (webdav, &certificate_pem, 
&certificate_errors, cancellable, &local_error);
-               if (!success && !g_cancellable_is_cancelled (cancellable)) {
-                       ESourceCredentialsReason reason;
-                       GError *local_error2 = NULL;
-
-                       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-                               reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
-                               e_source_set_connection_status (source, 
E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
-                       } else if (g_error_matches (local_error, E_CLIENT_ERROR, 
E_CLIENT_ERROR_AUTHENTICATION_FAILED) ||
-                                  g_error_matches (local_error, E_CLIENT_ERROR, 
E_CLIENT_ERROR_AUTHENTICATION_REQUIRED)) {
-                               reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
-                       } else {
-                               reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
-                       }
-
-                       if (!e_backend_credentials_required_sync (E_BACKEND (backend), reason, 
certificate_pem, certificate_errors,
-                               local_error, cancellable, &local_error2)) {
-                               g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC, 
local_error2 ? local_error2->message : "Unknown error");
-                       }
-
-                       if (!local_error2 && g_error_matches (local_error, SOUP_HTTP_ERROR, 
SOUP_STATUS_SSL_FAILED)) {
-                               /* These cerificate errors are treated through the authentication */
-                               g_clear_error (&local_error);
-                       } else {
-                               g_propagate_error (error, local_error);
-                               local_error = NULL;
-                       }
-
-                       g_clear_error (&local_error2);
-               } else {
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
-               }
-
-               g_free (certificate_pem);
-
-               if (local_error)
-                       g_propagate_error (error, local_error);
+               filename = soup_uri_encode (uid, NULL);
        }
 
-       soup_uri_free (suri);
+       if (soup_uri->path) {
+               gchar *slash = strrchr (soup_uri->path, '/');
 
-       return success;
-}
+               if (slash && !slash[1])
+                       *slash = '\0';
+       }
 
-static gboolean
-webdav_can_use_uid (const gchar *uid)
-{
-       const gchar *ptr;
+       soup_uri_set_user (soup_uri, NULL);
+       soup_uri_set_password (soup_uri, NULL);
 
-       if (!uid || !*uid)
-               return FALSE;
+       tmp = g_strconcat (soup_uri->path && *soup_uri->path ? soup_uri->path : "", "/", filename, NULL);
+       soup_uri_set_path (soup_uri, tmp);
+       g_free (tmp);
 
-       for (ptr = uid; *ptr; ptr++) {
-               if ((*ptr >= 'a' && *ptr <= 'z') ||
-                   (*ptr >= 'A' && *ptr <= 'Z') ||
-                   (*ptr >= '0' && *ptr <= '9') ||
-                   strchr (".-@", *ptr) != NULL)
-                       continue;
+       uri = soup_uri_to_string (soup_uri, FALSE);
 
-               return FALSE;
-       }
+       soup_uri_free (soup_uri);
+       g_free (filename);
 
-       return TRUE;
+       return uri;
 }
 
 static gboolean
-book_backend_webdav_create_contacts_sync (EBookBackend *backend,
-                                          const gchar * const *vcards,
-                                          GQueue *out_contacts,
-                                          GCancellable *cancellable,
-                                          GError **error)
+ebb_webdav_load_contact_sync (EBookMetaBackend *meta_backend,
+                             const gchar *uid,
+                             const gchar *extra,
+                             EContact **out_contact,
+                             gchar **out_extra,
+                             GCancellable *cancellable,
+                             GError **error)
 {
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       EContact *contact;
-       gchar *uid, *href;
-       const gchar *orig_uid;
-       guint status;
-       gchar *status_reason = NULL, *stored_etag;
-
-       /* We make the assumption that the vCard list we're passed is
-        * always exactly one element long, since we haven't specified
-        * "bulk-adds" in our static capability list.  This is because
-        * there is no way to roll back changes in case of an error. */
-       if (g_strv_length ((gchar **) vcards) > 1) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_NOT_SUPPORTED,
-                       _("The backend does not support bulk additions"));
-               return FALSE;
-       }
-
-       if (!e_backend_get_online (E_BACKEND (backend))) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_REPOSITORY_OFFLINE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_REPOSITORY_OFFLINE));
-               return FALSE;
-       }
+       EBookBackendWebDAV *bbdav;
+       gchar *uri = NULL, *href = NULL, *etag = NULL, *bytes = NULL;
+       gsize length = -1;
+       gboolean success = FALSE;
+       GError *local_error = NULL;
 
-       contact = e_contact_new_from_vcard (vcards[0]);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_contact != NULL, FALSE);
 
-       orig_uid = e_contact_get_const (contact, E_CONTACT_UID);
-       if (orig_uid && *orig_uid && webdav_can_use_uid (orig_uid) && !e_book_backend_cache_check_contact 
(webdav->priv->cache, orig_uid)) {
-               uid = g_strdup (orig_uid);
-       } else {
-               uid = NULL;
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
 
-               do {
-                       g_free (uid);
+       if (extra && *extra) {
+               uri = g_strdup (extra);
 
-                       /* do 3 random() calls to construct a unique ID... poor way but should be
-                        * good enough for us */
-                       uid = g_strdup_printf ("%08X-%08X-%08X", g_random_int (), g_random_int (), 
g_random_int ());
+               success = e_webdav_session_get_data_sync (bbdav->priv->webdav, uri, &href, &etag, &bytes, 
&length, cancellable, &local_error);
 
-               } while (e_book_backend_cache_check_contact (webdav->priv->cache, uid) &&
-                        !g_cancellable_is_cancelled (cancellable));
-
-               e_contact_set (contact, E_CONTACT_UID, uid);
+               if (!success) {
+                       g_free (uri);
+                       uri = NULL;
+               }
        }
 
-       href = g_strconcat (webdav->priv->uri, uid, ".vcf", NULL);
+       if (!success) {
+               uri = ebb_webdav_uid_to_uri (bbdav, uid, ".vcf");
+               g_return_val_if_fail (uri != NULL, FALSE);
+
+               g_clear_error (&local_error);
 
-       /* kill WEBDAV_CONTACT_ETAG field (might have been set by some other backend) */
-       webdav_contact_set_href (contact, NULL);
-       webdav_contact_set_etag (contact, NULL);
+               success = e_webdav_session_get_data_sync (bbdav->priv->webdav, uri, &href, &etag, &bytes, 
&length, cancellable, &local_error);
+               if (!success && !g_cancellable_is_cancelled (cancellable) &&
+                   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+                       g_free (uri);
+                       uri = ebb_webdav_uid_to_uri (bbdav, uid, NULL);
 
-       status = upload_contact (webdav, href, contact, &status_reason, cancellable);
-       g_free (href);
+                       if (uri) {
+                               g_clear_error (&local_error);
 
-       if (status != 201 && status != 204) {
-               g_object_unref (contact);
-               if (status == 401 || status == 407) {
-                       webdav_handle_auth_request (webdav, error);
-               } else {
-                       g_set_error (
-                               error, E_CLIENT_ERROR,
-                               E_CLIENT_ERROR_OTHER_ERROR,
-                               _("Create resource “%s” failed with HTTP status %d (%s)"),
-                               uid, status, status_reason);
+                               success = e_webdav_session_get_data_sync (bbdav->priv->webdav, uri, &href, 
&etag, &bytes, &length, cancellable, &local_error);
+                       }
                }
-               g_free (uid);
-               g_free (status_reason);
-               return FALSE;
        }
 
-       g_free (status_reason);
-       g_free (uid);
+       if (success) {
+               *out_contact = NULL;
 
-       /* PUT request didn't return an etag? try downloading to get one */
-       stored_etag = webdav_contact_get_etag (contact);
-       if (!stored_etag) {
-               gchar *href;
-               EContact *new_contact = NULL;
+               if (href && etag && bytes && length != ((gsize) -1)) {
+                       EContact *contact;
 
-               href = webdav_contact_get_href (contact);
-               if (href) {
-                       new_contact = download_contact (webdav, href, cancellable);
-                       g_free (href);
+                       contact = e_contact_new_from_vcard (bytes);
+                       if (contact) {
+                               e_vcard_util_set_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG, etag);
+                               *out_contact = contact;
+                       }
                }
 
-               g_object_unref (contact);
-
-               if (new_contact == NULL) {
-                       g_set_error_literal (
-                               error, E_CLIENT_ERROR,
-                               E_CLIENT_ERROR_OTHER_ERROR,
-                               e_client_error_to_string (
-                               E_CLIENT_ERROR_OTHER_ERROR));
-                       return FALSE;
+               if (!*out_contact) {
+                       success = FALSE;
+                       g_propagate_error (&local_error, EDB_ERROR_EX (E_DATA_BOOK_STATUS_OTHER_ERROR, 
_("Received object is not a valid vCard")));
                }
-               contact = new_contact;
-       } else {
-               g_free (stored_etag);
        }
 
-       g_mutex_lock (&webdav->priv->cache_lock);
-       e_book_backend_cache_add_contact (webdav->priv->cache, contact);
-       g_mutex_unlock (&webdav->priv->cache_lock);
-
-       g_queue_push_tail (out_contacts, g_object_ref (contact));
+       g_free (uri);
+       g_free (href);
+       g_free (etag);
+       g_free (bytes);
 
-       g_object_unref (contact);
+       if (local_error)
+               g_propagate_error (error, local_error);
 
-       return TRUE;
+       return success;
 }
 
 static gboolean
-book_backend_webdav_modify_contacts_sync (EBookBackend *backend,
-                                          const gchar * const *vcards,
-                                          GQueue *out_contacts,
-                                          GCancellable *cancellable,
-                                          GError **error)
+ebb_webdav_save_contact_sync (EBookMetaBackend *meta_backend,
+                             gboolean overwrite_existing,
+                             EConflictResolution conflict_resolution,
+                             /* const */ EContact *contact,
+                             const gchar *extra,
+                             gchar **out_new_uid,
+                             gchar **out_new_extra,
+                             GCancellable *cancellable,
+                             GError **error)
 {
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       EContact *contact;
-       const gchar *uid;
-       gchar *href, *etag;
-       guint status;
-       gchar *status_reason = NULL;
-
-       /* We make the assumption that the vCard list we're passed is
-        * always exactly one element long, since we haven't specified
-        * "bulk-modifies" in our static capability list.  This is because
-        * there is no clean way to roll back changes in case of an error. */
-       if (g_strv_length ((gchar **) vcards) > 1) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_NOT_SUPPORTED,
-                       _("The backend does not support bulk modifications"));
-               return FALSE;
-       }
+       EBookBackendWebDAV *bbdav;
+       gchar *href = NULL, *etag = NULL, *uid = NULL;
+       gchar *vcard_string = NULL;
+       gboolean success;
 
-       if (!e_backend_get_online (E_BACKEND (backend))) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_REPOSITORY_OFFLINE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_REPOSITORY_OFFLINE));
-               return FALSE;
-       }
-
-       /* modify contact */
-       contact = e_contact_new_from_vcard (vcards[0]);
-       href = webdav_contact_get_href (contact);
-       status = upload_contact (webdav, href, contact, &status_reason, cancellable);
-       g_free (href);
-       if (status != 200 && status != 201 && status != 204) {
-               g_object_unref (contact);
-               if (status == 401 || status == 407) {
-                       webdav_handle_auth_request (webdav, error);
-                       g_free (status_reason);
-                       return FALSE;
-               }
-               /* data changed on server while we were editing */
-               if (status == 412) {
-                       /* too bad no special error code in evolution for this... */
-                       g_set_error_literal (
-                               error, E_CLIENT_ERROR,
-                               E_CLIENT_ERROR_OTHER_ERROR,
-                               _("Contact on server changed -> not modifying"));
-                       g_free (status_reason);
-                       return FALSE;
-               }
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+       g_return_val_if_fail (out_new_uid, FALSE);
+       g_return_val_if_fail (out_new_extra, FALSE);
 
-               g_set_error (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OTHER_ERROR,
-                       _("Modify contact failed with HTTP status %d (%s)"),
-                       status, status_reason);
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
 
-               g_free (status_reason);
-               return FALSE;
-       }
+       uid = e_contact_get (contact, E_CONTACT_UID);
+       etag = e_vcard_util_dup_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG);
 
-       g_free (status_reason);
+       e_vcard_util_set_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG, NULL);
 
-       uid = e_contact_get_const (contact, E_CONTACT_UID);
-       g_mutex_lock (&webdav->priv->cache_lock);
-       e_book_backend_cache_remove_contact (webdav->priv->cache, uid);
+       vcard_string = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
 
-       etag = webdav_contact_get_etag (contact);
+       if (uid && vcard_string && (!overwrite_existing || (extra && *extra))) {
+               gboolean force_write = FALSE;
 
-       /* PUT request didn't return an etag? try downloading to get one */
-       if (etag == NULL || (etag[0] == 'W' && etag[1] == '/')) {
-               EContact *new_contact = NULL;
+               if (!extra || !*extra)
+                       href = ebb_webdav_uid_to_uri (bbdav, uid, ".vcf");
 
-               href = webdav_contact_get_href (contact);
-               if (href) {
-                       new_contact = download_contact (webdav, href, cancellable);
-                       g_free (href);
+               if (overwrite_existing) {
+                       switch (conflict_resolution) {
+                       case E_CONFLICT_RESOLUTION_FAIL:
+                       case E_CONFLICT_RESOLUTION_USE_NEWER:
+                       case E_CONFLICT_RESOLUTION_KEEP_SERVER:
+                       case E_CONFLICT_RESOLUTION_WRITE_COPY:
+                               break;
+                       case E_CONFLICT_RESOLUTION_KEEP_LOCAL:
+                               force_write = TRUE;
+                               break;
+                       }
                }
 
-               if (new_contact != NULL) {
-                       g_object_unref (contact);
-                       contact = new_contact;
-               }
+               success = e_webdav_session_put_data_sync (bbdav->priv->webdav, (extra && *extra) ? extra : 
href,
+                       force_write ? "" : overwrite_existing ? etag : NULL, E_WEBDAV_CONTENT_TYPE_VCARD,
+                       vcard_string, -1, out_new_extra, NULL, cancellable, error);
+
+               /* To read the component back, because server can change it */
+               if (success)
+                       *out_new_uid = g_strdup (uid);
+       } else {
+               success = FALSE;
+               g_propagate_error (error, EDB_ERROR_EX (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Object to save is 
not a valid vCard")));
        }
 
+       g_free (vcard_string);
+       g_free (href);
        g_free (etag);
+       g_free (uid);
 
-       e_book_backend_cache_add_contact (webdav->priv->cache, contact);
-       g_mutex_unlock (&webdav->priv->cache_lock);
-
-       g_queue_push_tail (out_contacts, g_object_ref (contact));
-
-       g_object_unref (contact);
-
-       return TRUE;
+       return success;
 }
 
 static gboolean
-book_backend_webdav_remove_contacts_sync (EBookBackend *backend,
-                                          const gchar * const *uids,
-                                          GCancellable *cancellable,
-                                          GError **error)
+ebb_webdav_remove_contact_sync (EBookMetaBackend *meta_backend,
+                               EConflictResolution conflict_resolution,
+                               const gchar *uid,
+                               const gchar *extra,
+                               const gchar *object,
+                               GCancellable *cancellable,
+                               GError **error)
 {
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
+       EBookBackendWebDAV *bbdav;
        EContact *contact;
-       gchar *href;
-       guint status;
-
-       /* We make the assumption that the ID list we're passed is
-        * always exactly one element long, since we haven't specified
-        * "bulk-removes" in our static capability list. */
-       if (g_strv_length ((gchar **) uids) > 1) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_NOT_SUPPORTED,
-                       _("The backend does not support bulk removals"));
-               return FALSE;
-       }
-
-       if (!e_backend_get_online (E_BACKEND (backend))) {
-               g_set_error_literal (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_REPOSITORY_OFFLINE,
-                       e_client_error_to_string (
-                       E_CLIENT_ERROR_REPOSITORY_OFFLINE));
-               return FALSE;
-       }
+       gchar *etag = NULL;
+       gboolean success;
+       GError *local_error = NULL;
 
-       g_mutex_lock (&webdav->priv->cache_lock);
-       contact = e_book_backend_cache_get_contact (webdav->priv->cache, uids[0]);
-       g_mutex_unlock (&webdav->priv->cache_lock);
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
 
-       if (!contact) {
-               g_set_error_literal (
-                       error, E_BOOK_CLIENT_ERROR,
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
-                       e_book_client_error_to_string (
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
-               return FALSE;
-       }
+       bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
 
-       href = webdav_contact_get_href (contact);
-       if (!href) {
-               g_object_unref (contact);
-               g_set_error (
-                       error, E_CLIENT_ERROR,
-                       E_CLIENT_ERROR_OTHER_ERROR,
-                       _("DELETE failed with HTTP status %d"), SOUP_STATUS_MALFORMED);
+       if (!extra || !*extra) {
+               g_propagate_error (error, EDB_ERROR (E_DATA_BOOK_STATUS_INVALID_ARG));
                return FALSE;
        }
 
-       status = delete_contact (webdav, href, cancellable);
-
-       g_object_unref (contact);
-       g_free (href);
-
-       if (!SOUP_STATUS_IS_SUCCESSFUL (status)) {
-               if (status == 401 || status == 407) {
-                       webdav_handle_auth_request (webdav, error);
-               } else {
-                       g_set_error (
-                               error, E_CLIENT_ERROR,
-                               E_CLIENT_ERROR_OTHER_ERROR,
-                               _("DELETE failed with HTTP status %d"), status);
-               }
+       contact = e_contact_new_from_vcard (object);
+       if (!contact) {
+               g_propagate_error (error, EDB_ERROR (E_DATA_BOOK_STATUS_INVALID_ARG));
                return FALSE;
        }
 
-       g_mutex_lock (&webdav->priv->cache_lock);
-       e_book_backend_cache_remove_contact (webdav->priv->cache, uids[0]);
-       g_mutex_unlock (&webdav->priv->cache_lock);
-
-       return TRUE;
-}
-
-static EContact *
-book_backend_webdav_get_contact_sync (EBookBackend *backend,
-                                      const gchar *uid,
-                                      GCancellable *cancellable,
-                                      GError **error)
-{
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       EContact *contact;
+       if (conflict_resolution == E_CONFLICT_RESOLUTION_FAIL)
+               etag = e_vcard_util_dup_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG);
 
-       g_mutex_lock (&webdav->priv->cache_lock);
-       contact = e_book_backend_cache_get_contact (
-               webdav->priv->cache, uid);
-       g_mutex_unlock (&webdav->priv->cache_lock);
+       success = e_webdav_session_delete_sync (bbdav->priv->webdav, extra,
+               NULL, etag, cancellable, &local_error);
 
-       if (contact && e_backend_get_online (E_BACKEND (backend))) {
+       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
                gchar *href;
 
-               href = webdav_contact_get_href (contact);
-               g_object_unref (contact);
-
+               href = ebb_webdav_uid_to_uri (bbdav, uid, ".vcf");
                if (href) {
-                       contact = download_contact (webdav, href, cancellable);
+                       g_clear_error (&local_error);
+                       success = e_webdav_session_delete_sync (bbdav->priv->webdav, href,
+                               NULL, etag, cancellable, &local_error);
+
                        g_free (href);
-               } else {
-                       contact = NULL;
                }
 
-               /* update cache as we possibly have changes */
-               if (contact != NULL) {
-                       g_mutex_lock (&webdav->priv->cache_lock);
-                       e_book_backend_cache_remove_contact (
-                               webdav->priv->cache, uid);
-                       e_book_backend_cache_add_contact (
-                               webdav->priv->cache, contact);
-                       g_mutex_unlock (&webdav->priv->cache_lock);
+               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+                       href = ebb_webdav_uid_to_uri (bbdav, uid, NULL);
+                       if (href) {
+                               g_clear_error (&local_error);
+                               success = e_webdav_session_delete_sync (bbdav->priv->webdav, href,
+                                       NULL, etag, cancellable, &local_error);
+
+                               g_free (href);
+                       }
                }
        }
 
-       if (contact == NULL) {
-               g_set_error_literal (
-                       error, E_BOOK_CLIENT_ERROR,
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
-                       e_book_client_error_to_string (
-                       E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
-               return FALSE;
-       }
+       g_object_unref (contact);
+       g_free (etag);
 
-       return contact;
+       if (local_error)
+               g_propagate_error (error, local_error);
+
+       return success;
 }
 
-static gboolean
-book_backend_webdav_get_contact_list_sync (EBookBackend *backend,
-                                           const gchar *query,
-                                           GQueue *out_contacts,
-                                           GCancellable *cancellable,
-                                           GError **error)
+static gchar *
+ebb_webdav_get_backend_property (EBookBackend *book_backend,
+                                const gchar *prop_name)
 {
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       GList *contact_list;
-
-       if (e_backend_get_online (E_BACKEND (backend)) &&
-           e_source_get_connection_status (e_backend_get_source (E_BACKEND (backend))) == 
E_SOURCE_CONNECTION_STATUS_CONNECTED) {
-               /* make sure the cache is up to date */
-               if (!download_contacts (webdav, NULL, NULL, FALSE, cancellable, error))
-                       return FALSE;
-       }
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (book_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
 
-       /* answer query from cache */
-       g_mutex_lock (&webdav->priv->cache_lock);
-       contact_list = e_book_backend_cache_get_contacts (
-               webdav->priv->cache, query);
-       g_mutex_unlock (&webdav->priv->cache_lock);
-
-       /* This appends contact_list to out_contacts, one element at a
-        * time, since GLib lacks something like g_queue_append_list().
-        *
-        * XXX Would be better if e_book_backend_cache_get_contacts()
-        *     took an output GQueue instead of returning a GList. */
-       while (contact_list != NULL) {
-               GList *link = contact_list;
-               contact_list = g_list_remove_link (contact_list, link);
-               g_queue_push_tail_link (out_contacts, link);
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strjoin (",",
+                       "net",
+                       "do-initial-query",
+                       "contact-lists",
+                       e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+                       NULL);
        }
 
-       return TRUE;
+       /* Chain up to parent's method. */
+       return E_BOOK_BACKEND_CLASS (e_book_backend_webdav_parent_class)->get_backend_property (book_backend, 
prop_name);
 }
 
-static ESourceAuthenticationResult
-book_backend_webdav_authenticate_sync (EBackend *backend,
-                                      const ENamedParameters *credentials,
-                                      gchar **out_certificate_pem,
-                                      GTlsCertificateFlags *out_certificate_errors,
-                                      GCancellable *cancellable,
-                                      GError **error)
+static gchar *
+ebb_webdav_dup_contact_revision_cb (EBookCache *book_cache,
+                                   EContact *contact)
 {
-       EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
-       ESourceAuthentication *auth_extension;
-       ESourceAuthenticationResult result;
-       ESource *source;
-       const gchar *username;
-       GError *local_error = NULL;
+       g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
 
-       source = e_backend_get_source (backend);
-       auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+       return e_vcard_util_dup_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG);
+}
 
-       g_free (webdav->priv->username);
-       webdav->priv->username = NULL;
+static void
+e_book_backend_webdav_constructed (GObject *object)
+{
+       EBookBackendWebDAV *bbdav = E_BOOK_BACKEND_WEBDAV (object);
+       EBookCache *book_cache;
 
-       g_free (webdav->priv->password);
-       webdav->priv->password = g_strdup (e_named_parameters_get (credentials, 
E_SOURCE_CREDENTIAL_PASSWORD));
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->constructed (object);
 
-       username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
-       if (username && *username) {
-               webdav->priv->username = g_strdup (username);
-       } else {
-               webdav->priv->username = e_source_authentication_dup_user (auth_extension);
-       }
+       book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (bbdav));
 
-       if (book_backend_webdav_test_can_connect (webdav, out_certificate_pem, out_certificate_errors, 
cancellable, &local_error)) {
-               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
-       } else if (g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_FAILED) ||
-                  g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_REQUIRED)) {
-               if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD) ||
-                   g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_REQUIRED))
-                       result = E_SOURCE_AUTHENTICATION_REQUIRED;
-               else
-                       result = E_SOURCE_AUTHENTICATION_REJECTED;
-               g_clear_error (&local_error);
-       } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-               result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
-               g_propagate_error (error, local_error);
-       } else {
-               result = E_SOURCE_AUTHENTICATION_ERROR;
-               g_propagate_error (error, local_error);
-       }
+       g_signal_connect (book_cache, "dup-contact-revision",
+               G_CALLBACK (ebb_webdav_dup_contact_revision_cb), NULL);
 
-       return result;
+       g_clear_object (&book_cache);
 }
 
-static gboolean
-e_book_backend_webdav_refresh_sync (EBookBackend *book_backend,
-                                   GCancellable *cancellable,
-                                   GError **error)
+static void
+e_book_backend_webdav_dispose (GObject *object)
 {
-       EBackend *backend;
-
-       g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (book_backend), FALSE);
+       EBookBackendWebDAV *bbdav = E_BOOK_BACKEND_WEBDAV (object);
 
-       backend = E_BACKEND (book_backend);
+       g_clear_object (&bbdav->priv->webdav);
 
-       if (!e_backend_get_online (backend) &&
-           e_backend_is_destination_reachable (backend, cancellable, NULL)) {
-               e_backend_set_online (backend, TRUE);
-       }
-
-       if (e_backend_get_online (backend) && !g_cancellable_is_cancelled (cancellable)) {
-               return download_contacts (E_BOOK_BACKEND_WEBDAV (book_backend), NULL, NULL, TRUE, 
cancellable, error);
-       }
-
-       return TRUE;
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->dispose (object);
 }
 
 static void
-e_book_backend_webdav_class_init (EBookBackendWebdavClass *class)
+e_book_backend_webdav_init (EBookBackendWebDAV *bbdav)
 {
-       GObjectClass *object_class;
-       EBackendClass *backend_class;
-       EBookBackendClass *book_backend_class;
-
-       g_type_class_add_private (class, sizeof (EBookBackendWebdavPrivate));
-
-       object_class = G_OBJECT_CLASS (class);
-       object_class->dispose = book_backend_webdav_dispose;
-       object_class->finalize = book_backend_webdav_finalize;
-
-       backend_class = E_BACKEND_CLASS (class);
-       backend_class->authenticate_sync = book_backend_webdav_authenticate_sync;
-
-       book_backend_class = E_BOOK_BACKEND_CLASS (class);
-       book_backend_class->get_backend_property = book_backend_webdav_get_backend_property;
-       book_backend_class->open_sync = book_backend_webdav_open_sync;
-       book_backend_class->create_contacts_sync = book_backend_webdav_create_contacts_sync;
-       book_backend_class->modify_contacts_sync = book_backend_webdav_modify_contacts_sync;
-       book_backend_class->remove_contacts_sync = book_backend_webdav_remove_contacts_sync;
-       book_backend_class->get_contact_sync = book_backend_webdav_get_contact_sync;
-       book_backend_class->get_contact_list_sync = book_backend_webdav_get_contact_list_sync;
-       book_backend_class->start_view = e_book_backend_webdav_start_view;
-       book_backend_class->stop_view = e_book_backend_webdav_stop_view;
-       book_backend_class->refresh_sync = e_book_backend_webdav_refresh_sync;
+       bbdav->priv = G_TYPE_INSTANCE_GET_PRIVATE (bbdav, E_TYPE_BOOK_BACKEND_WEBDAV, 
EBookBackendWebDAVPrivate);
 }
 
 static void
-e_book_backend_webdav_init (EBookBackendWebdav *backend)
+e_book_backend_webdav_class_init (EBookBackendWebDAVClass *klass)
 {
-       backend->priv = E_BOOK_BACKEND_WEBDAV_GET_PRIVATE (backend);
-
-       g_mutex_init (&backend->priv->cache_lock);
-       g_mutex_init (&backend->priv->update_lock);
-
-       g_signal_connect (
-               backend, "notify::online",
-               G_CALLBACK (e_book_backend_webdav_notify_online_cb), NULL);
+       GObjectClass *object_class;
+       EBookBackendClass *book_backend_class;
+       EBookMetaBackendClass *book_meta_backend_class;
+
+       g_type_class_add_private (klass, sizeof (EBookBackendWebDAVPrivate));
+
+       book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+       book_meta_backend_class->backend_module_filename = "libebookbackendwebdav.so";
+       book_meta_backend_class->backend_factory_type_name = "EBookBackendWebdavFactory";
+       book_meta_backend_class->connect_sync = ebb_webdav_connect_sync;
+       book_meta_backend_class->disconnect_sync = ebb_webdav_disconnect_sync;
+       book_meta_backend_class->get_changes_sync = ebb_webdav_get_changes_sync;
+       book_meta_backend_class->list_existing_sync = ebb_webdav_list_existing_sync;
+       book_meta_backend_class->load_contact_sync = ebb_webdav_load_contact_sync;
+       book_meta_backend_class->save_contact_sync = ebb_webdav_save_contact_sync;
+       book_meta_backend_class->remove_contact_sync = ebb_webdav_remove_contact_sync;
+
+       book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+       book_backend_class->get_backend_property = ebb_webdav_get_backend_property;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_book_backend_webdav_constructed;
+       object_class->dispose = e_book_backend_webdav_dispose;
 }
-
diff --git a/src/addressbook/backends/webdav/e-book-backend-webdav.h 
b/src/addressbook/backends/webdav/e-book-backend-webdav.h
index e7b1ed0..93162fb 100644
--- a/src/addressbook/backends/webdav/e-book-backend-webdav.h
+++ b/src/addressbook/backends/webdav/e-book-backend-webdav.h
@@ -27,10 +27,10 @@
        (e_book_backend_webdav_get_type ())
 #define E_BOOK_BACKEND_WEBDAV(obj) \
        (G_TYPE_CHECK_INSTANCE_CAST \
-       ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdav))
+       ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebDAV))
 #define E_BOOK_BACKEND_WEBDAV_CLASS(cls) \
        (G_TYPE_CHECK_CLASS_CAST \
-       ((cls), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdavClass))
+       ((cls), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebDAVClass))
 #define E_IS_BOOK_BACKEND_WEBDAV(obj) \
        (G_TYPE_CHECK_INSTANCE_TYPE \
        ((obj), E_TYPE_BOOK_BACKEND_WEBDAV))
@@ -39,21 +39,21 @@
        ((cls), E_TYPE_BOOK_BACKEND_WEBDAV))
 #define E_BOOK_BACKEND_WEBDAV_GET_CLASS(cls) \
        (G_TYPE_INSTANCE_GET_CLASS \
-       ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdavClass))
+       ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebDAVClass))
 
 G_BEGIN_DECLS
 
-typedef struct _EBookBackendWebdav EBookBackendWebdav;
-typedef struct _EBookBackendWebdavClass EBookBackendWebdavClass;
-typedef struct _EBookBackendWebdavPrivate EBookBackendWebdavPrivate;
+typedef struct _EBookBackendWebDAV EBookBackendWebDAV;
+typedef struct _EBookBackendWebDAVClass EBookBackendWebDAVClass;
+typedef struct _EBookBackendWebDAVPrivate EBookBackendWebDAVPrivate;
 
-struct _EBookBackendWebdav {
-       EBookBackend parent;
-       EBookBackendWebdavPrivate *priv;
+struct _EBookBackendWebDAV {
+       EBookMetaBackend parent;
+       EBookBackendWebDAVPrivate *priv;
 };
 
-struct _EBookBackendWebdavClass {
-       EBookBackendClass parent_class;
+struct _EBookBackendWebDAVClass {
+       EBookMetaBackendClass parent_class;
 };
 
 GType          e_book_backend_webdav_get_type  (void);
@@ -61,4 +61,3 @@ GType         e_book_backend_webdav_get_type  (void);
 G_END_DECLS
 
 #endif /* E_BOOK_BACKEND_WEBDAV_H */
-
diff --git a/src/addressbook/libebook-contacts/e-vcard.c b/src/addressbook/libebook-contacts/e-vcard.c
index b969e41..f9d897c 100644
--- a/src/addressbook/libebook-contacts/e-vcard.c
+++ b/src/addressbook/libebook-contacts/e-vcard.c
@@ -2034,6 +2034,11 @@ e_vcard_attribute_remove_param (EVCardAttribute *attr,
                param = l->data;
                if (g_ascii_strcasecmp (e_vcard_attribute_param_get_name (param),
                                        param_name) == 0) {
+                       if (g_ascii_strcasecmp (param_name, EVC_ENCODING) == 0) {
+                               attr->encoding_set = FALSE;
+                               attr->encoding = EVC_ENCODING_RAW;
+                       }
+
                        attr->params = g_list_delete_link (attr->params, l);
                        e_vcard_attribute_param_free (param);
                        break;
@@ -2870,3 +2875,72 @@ e_vcard_attribute_param_get_values (EVCardAttributeParam *param)
 
        return param->values;
 }
+
+/**
+ * e_vcard_util_set_x_attribute:
+ * @vcard: an #EVCard
+ * @x_name: the attribute name, which starts with "X-"
+ * @value: (nullable): the value to set, or %NULL to unset
+ *
+ * Sets an "X-" attribute @x_name to value @value in @vcard, or
+ * removes it from @vcard, when @value is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_vcard_util_set_x_attribute (EVCard *vcard,
+                             const gchar *x_name,
+                             const gchar *value)
+{
+       EVCardAttribute *attr;
+
+       g_return_if_fail (E_IS_VCARD (vcard));
+       g_return_if_fail (x_name != NULL);
+       g_return_if_fail (g_str_has_prefix (x_name, "X-"));
+
+       attr = e_vcard_get_attribute (vcard, x_name);
+
+       if (attr) {
+               e_vcard_attribute_remove_values (attr);
+               if (value) {
+                       e_vcard_attribute_add_value (attr, value);
+               } else {
+                       e_vcard_remove_attribute (vcard, attr);
+               }
+       } else if (value) {
+               e_vcard_append_attribute_with_value (
+                       vcard,
+                       e_vcard_attribute_new (NULL, x_name),
+                       value);
+       }
+}
+
+/**
+ * e_vcard_util_dup_x_attribute:
+ * @vcard: an #EVCard
+ * @x_name: the attribute name, which starts with "X-"
+ *
+ * Returns: (nullable) (transfer-full): Value of attribute @x_name, or %NULL,
+ *    when there is no such attribute. Free the returned pointer with g_free(),
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_vcard_util_dup_x_attribute (EVCard *vcard,
+                             const gchar *x_name)
+{
+       EVCardAttribute *attr;
+       GList *v = NULL;
+
+       g_return_val_if_fail (E_IS_VCARD (vcard), NULL);
+       g_return_val_if_fail (x_name != NULL, NULL);
+       g_return_val_if_fail (g_str_has_prefix (x_name, "X-"), NULL);
+
+       attr = e_vcard_get_attribute (vcard, x_name);
+
+       if (attr)
+               v = e_vcard_attribute_get_values (attr);
+
+       return ((v && v->data) ? g_strstrip (g_strdup (v->data)) : NULL);
+}
diff --git a/src/addressbook/libebook-contacts/e-vcard.h b/src/addressbook/libebook-contacts/e-vcard.h
index e3f9ed0..9efcb3d 100644
--- a/src/addressbook/libebook-contacts/e-vcard.h
+++ b/src/addressbook/libebook-contacts/e-vcard.h
@@ -316,6 +316,12 @@ gboolean         e_vcard_attribute_has_type         (EVCardAttribute *attr, cons
 gchar *            e_vcard_escape_string (const gchar *s);
 gchar *            e_vcard_unescape_string (const gchar *s);
 
+void           e_vcard_util_set_x_attribute    (EVCard *vcard,
+                                                const gchar *x_name,
+                                                const gchar *value);
+gchar *                e_vcard_util_dup_x_attribute    (EVCard *vcard,
+                                                const gchar *x_name);
+
 G_END_DECLS
 
 #endif /* _EVCARD_H */
diff --git a/src/addressbook/libedata-book/CMakeLists.txt b/src/addressbook/libedata-book/CMakeLists.txt
index a247c7b..2bac3a9 100644
--- a/src/addressbook/libedata-book/CMakeLists.txt
+++ b/src/addressbook/libedata-book/CMakeLists.txt
@@ -16,9 +16,12 @@ set(SOURCES
        e-book-backend-cache.c
        e-book-backend-sqlitedb.c
        e-book-backend.c
+       e-book-cache.c
+       e-book-meta-backend.c
        e-book-sqlite.c
        e-data-book.c
        e-data-book-cursor.c
+       e-data-book-cursor-cache.c
        e-data-book-cursor-sqlite.c
        e-data-book-direct.c
        e-data-book-factory.c
@@ -33,15 +36,18 @@ set(HEADERS
        e-book-backend-sexp.h
        e-book-backend-summary.h
        e-book-backend.h
+       e-book-cache.h
+       e-book-meta-backend.h
+       e-book-sqlite.h
        e-data-book-factory.h
        e-data-book-view.h
        e-data-book.h
        e-data-book-cursor.h
+       e-data-book-cursor-cache.h
        e-data-book-cursor-sqlite.h
        e-data-book-direct.h
        e-book-backend-cache.h
        e-book-backend-sqlitedb.h
-       e-book-sqlite.h
        e-subprocess-book-factory.h
 )
 
diff --git a/src/addressbook/libedata-book/e-book-backend.c b/src/addressbook/libedata-book/e-book-backend.c
index 9efac67..45aa092 100644
--- a/src/addressbook/libedata-book/e-book-backend.c
+++ b/src/addressbook/libedata-book/e-book-backend.c
@@ -67,6 +67,7 @@ struct _EBookBackendPrivate {
        GQueue pending_operations;
        guint32 next_operation_id;
        GSimpleAsyncResult *blocked;
+       gboolean blocked_by_custom_op;
 };
 
 struct _AsyncContext {
@@ -98,6 +99,11 @@ struct _DispatchNode {
 
        GSimpleAsyncResult *simple;
        GCancellable *cancellable;
+
+       GWeakRef *book_backend_weak_ref;
+       EBookBackendCustomOpFunc custom_func;
+       gpointer custom_func_user_data;
+       GDestroyNotify custom_func_user_data_free;
 };
 
 enum {
@@ -146,6 +152,12 @@ dispatch_node_free (DispatchNode *dispatch_node)
        g_clear_object (&dispatch_node->simple);
        g_clear_object (&dispatch_node->cancellable);
 
+       if (dispatch_node->custom_func_user_data_free)
+               dispatch_node->custom_func_user_data_free (dispatch_node->custom_func_user_data);
+
+       if (dispatch_node->book_backend_weak_ref)
+               e_weak_ref_free (dispatch_node->book_backend_weak_ref);
+
        g_slice_free (DispatchNode, dispatch_node);
 }
 
@@ -176,13 +188,35 @@ book_backend_push_operation (EBookBackend *backend,
        g_mutex_unlock (&backend->priv->operation_lock);
 }
 
+static void book_backend_unblock_operations (EBookBackend *backend, GSimpleAsyncResult *simple);
+
 static void
 book_backend_dispatch_thread (DispatchNode *node)
 {
        GCancellable *cancellable = node->cancellable;
        GError *local_error = NULL;
 
-       if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
+       if (node->custom_func) {
+               EBookBackend *book_backend;
+
+               book_backend = g_weak_ref_get (node->book_backend_weak_ref);
+               if (book_backend &&
+                   !g_cancellable_is_cancelled (cancellable)) {
+                       node->custom_func (book_backend, node->custom_func_user_data, cancellable, 
&local_error);
+
+                       if (local_error) {
+                               if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                                       e_book_backend_notify_error (book_backend, local_error->message);
+
+                               g_clear_error (&local_error);
+                       }
+               }
+
+               if (book_backend) {
+                       book_backend_unblock_operations (book_backend, NULL);
+                       e_util_unref_in_thread (book_backend);
+               }
+       } else if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
                g_simple_async_result_take_error (node->simple, local_error);
                g_simple_async_result_complete_in_idle (node->simple);
        } else {
@@ -207,7 +241,8 @@ book_backend_dispatch_next_operation (EBookBackend *backend)
 
        /* We can't dispatch additional operations
         * while a blocking operation is in progress. */
-       if (backend->priv->blocked != NULL) {
+       if (backend->priv->blocked != NULL ||
+           backend->priv->blocked_by_custom_op) {
                g_mutex_unlock (&backend->priv->operation_lock);
                return FALSE;
        }
@@ -221,8 +256,12 @@ book_backend_dispatch_next_operation (EBookBackend *backend)
 
        /* If this a blocking operation, block any
         * further dispatching until this finishes. */
-       if (node->blocking_operation)
-               backend->priv->blocked = g_object_ref (node->simple);
+       if (node->blocking_operation) {
+               if (node->simple)
+                       backend->priv->blocked = g_object_ref (node->simple);
+               else
+                       backend->priv->blocked_by_custom_op = TRUE;
+       }
 
        g_mutex_unlock (&backend->priv->operation_lock);
 
@@ -244,6 +283,7 @@ book_backend_unblock_operations (EBookBackend *backend,
        g_mutex_lock (&backend->priv->operation_lock);
        if (backend->priv->blocked == simple)
                g_clear_object (&backend->priv->blocked);
+       backend->priv->blocked_by_custom_op = FALSE;
        g_mutex_unlock (&backend->priv->operation_lock);
 
        while (book_backend_dispatch_next_operation (backend))
@@ -705,6 +745,7 @@ e_book_backend_class_init (EBookBackendClass *class)
        backend_class = E_BACKEND_CLASS (class);
        backend_class->prepare_shutdown = book_backend_prepare_shutdown;
 
+       class->use_serial_dispatch_queue = TRUE;
        class->get_backend_property = book_backend_get_backend_property;
        class->get_contact_list_uids_sync = book_backend_get_contact_list_uids_sync;
        class->notify_update = book_backend_notify_update;
@@ -3640,3 +3681,53 @@ e_book_backend_delete_cursor (EBookBackend *backend,
 
        return success;
 }
+
+/**
+ * e_book_backend_schedule_custom_operation:
+ * @book_backend: an #EBookBackend
+ * @use_cancellable: (nullable): an optional #GCancellable to use for @func
+ * @func: a function to call in a dedicated thread
+ * @user_data: user data being passed to @func
+ * @user_data_free: (nullable): optional destroy call back for @user_data
+ *
+ * Schedules user function @func to be run in a dedicated thread as
+ * a blocking operation.
+ *
+ * The function adds its own reference to @use_cancellable, if not %NULL.
+ *
+ * The error returned from @func is propagated to client using
+ * e_book_backend_notify_error() function. If it's not desired,
+ * then left the error unchanged and notify about errors manually.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_backend_schedule_custom_operation (EBookBackend *book_backend,
+                                         GCancellable *use_cancellable,
+                                         EBookBackendCustomOpFunc func,
+                                         gpointer user_data,
+                                         GDestroyNotify user_data_free)
+{
+       DispatchNode *node;
+
+       g_return_if_fail (E_IS_BOOK_BACKEND (book_backend));
+       g_return_if_fail (func != NULL);
+
+       g_mutex_lock (&book_backend->priv->operation_lock);
+
+       node = g_slice_new0 (DispatchNode);
+       node->blocking_operation = TRUE;
+       node->book_backend_weak_ref = e_weak_ref_new (book_backend);
+       node->custom_func = func;
+       node->custom_func_user_data = user_data;
+       node->custom_func_user_data_free = user_data_free;
+
+       if (G_IS_CANCELLABLE (use_cancellable))
+               node->cancellable = g_object_ref (use_cancellable);
+
+       g_queue_push_tail (&book_backend->priv->pending_operations, node);
+
+       g_mutex_unlock (&book_backend->priv->operation_lock);
+
+       book_backend_dispatch_next_operation (book_backend);
+}
diff --git a/src/addressbook/libedata-book/e-book-backend.h b/src/addressbook/libedata-book/e-book-backend.h
index a9b7bd5..cffaa40 100644
--- a/src/addressbook/libedata-book/e-book-backend.h
+++ b/src/addressbook/libedata-book/e-book-backend.h
@@ -113,7 +113,7 @@ struct _EBookBackend {
 /**
  * EBookBackendClass:
  * @use_serial_dispatch_queue: Whether a serial dispatch queue should
- *                             be used for this backend or not.
+ *                             be used for this backend or not. The default is %TRUE.
  * @get_backend_property: Fetch a property value by name from the backend
  * @open_sync: Open the backend
  * @refresh_sync: Refresh the backend
@@ -475,6 +475,30 @@ GSimpleAsyncResult *
                                                (EBookBackend *backend,
                                                 guint32 opid,
                                                 GQueue **result_queue);
+/**
+ * EBookBackendCustomOpFunc:
+ * @book_backend: an #EBookBackend
+ * @user_data: a function user data, as provided to e_book_backend_schedule_custom_operation()
+ * @cancellable: an optional #GCancellable, as provided to e_book_backend_schedule_custom_operation()
+ * @error: return location for a #GError, or %NULL
+ *
+ * A callback prototype being called in a dedicated thread, scheduled
+ * by e_book_backend_schedule_custom_operation().
+ *
+ * Since: 3.26
+ **/
+typedef void   (* EBookBackendCustomOpFunc)    (EBookBackend *book_backend,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+void           e_book_backend_schedule_custom_operation
+                                               (EBookBackend *book_backend,
+                                                GCancellable *use_cancellable,
+                                                EBookBackendCustomOpFunc func,
+                                                gpointer user_data,
+                                                GDestroyNotify user_data_free);
+
 
 G_END_DECLS
 
diff --git a/src/addressbook/libedata-book/e-book-cache.c b/src/addressbook/libedata-book/e-book-cache.c
new file mode 100644
index 0000000..cb1fda2
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-cache.c
@@ -0,0 +1,6113 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+/**
+ * SECTION: e-book-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An #ECache descendant for addressbooks
+ *
+ * The #EBookCache is an API for storing and looking up #EContacts
+ * in an #ECache. It also supports cursors.
+ *
+ * The API is thread safe, in the similar way as the #ECache is.
+ *
+ * Any operations which can take a lot of time to complete (depending
+ * on the size of your addressbook) can be cancelled using a #GCancellable.
+ *
+ * Depending on your summary configuration, your mileage will vary. Refer
+ * to the #ESourceBackendSummarySetup for configuring your addressbook
+ * for the type of usage you mean to make of it.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <string.h>
+#include <errno.h>
+#include <sqlite3.h>
+
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include "e-book-backend-sexp.h"
+
+#include "e-book-cache.h"
+
+#define E_BOOK_CACHE_VERSION           1
+#define INSERT_MULTI_STMT_BYTES                128
+#define COLUMN_DEFINITION_BYTES                32
+#define GENERATED_QUERY_BYTES          1024
+
+/* We use a 64 bitmask to track which auxiliary tables
+ * are needed to satisfy a query, it's doubtful that
+ * anyone will need an addressbook with 64 fields configured
+ * in the summary.
+ */
+#define EBC_MAX_SUMMARY_FIELDS      64
+
+/* The number of SQLite virtual machine instructions that are
+ * evaluated at a time, the user passed GCancellable is
+ * checked between each batch of evaluated instructions.
+ */
+#define EBC_CANCEL_BATCH_SIZE       200
+
+#define EBC_ESCAPE_SEQUENCE        "ESCAPE '^'"
+
+/* Names for custom functions */
+#define EBC_FUNC_COMPARE_VCARD     "compare_vcard"
+#define EBC_FUNC_EQPHONE_EXACT     "eqphone_exact"
+#define EBC_FUNC_EQPHONE_NATIONAL  "eqphone_national"
+#define EBC_FUNC_EQPHONE_SHORT     "eqphone_short"
+
+/* Fallback collations are generated as with a prefix and an EContactField name */
+#define EBC_COLLATE_PREFIX         "book_cache_"
+
+/* A special vcard attribute that we use only for private vcards */
+#define EBC_VCARD_SORT_KEY         "X-EVOLUTION-SORT-KEY"
+
+/* Key names for e_cache_dup/set_key{_int} functions */
+#define EBC_KEY_MULTIVALUES    "multivalues"
+#define EBC_KEY_LC_COLLATE     "lc_collate"
+#define EBC_KEY_COUNTRYCODE    "countrycode"
+
+/* Suffixes for column names used to store specialized data */
+#define EBC_SUFFIX_REVERSE         "reverse"
+#define EBC_SUFFIX_SORT_KEY        "localized"
+#define EBC_SUFFIX_PHONE           "phone"
+#define EBC_SUFFIX_COUNTRY         "country"
+
+/* Track EBookIndexType's in a bit mask  */
+#define INDEX_FLAG(type)  (1 << E_BOOK_INDEX_##type)
+
+#define EBC_COLUMN_EXTRA       "bdata"
+
+typedef struct {
+       EContactField field_id;         /* The EContact field */
+       GType type;                     /* The GType (only support string or gboolean) */
+       const gchar *dbname;            /* The key for this field in the sqlite3 table */
+       gint index;                     /* Types of searches this field should support (see EBookIndexType) */
+       gchar *dbname_idx_suffix;       /* dbnames for various indexes; can be NULL */
+       gchar *dbname_idx_phone;
+       gchar *dbname_idx_country;
+       gchar *dbname_idx_sort_key;
+       gchar *aux_table;               /* Name of auxiliary table for this field, for multivalued fields 
only */
+       gchar *aux_table_symbolic;      /* Symbolic name of auxiliary table used in queries */
+} SummaryField;
+
+struct _EBookCachePrivate {
+       ESource *source;                /* Optional, can be %NULL */
+
+       /* Parameters and settings */
+       gchar *locale;                  /* The current locale */
+       gchar *region_code;             /* Region code (for phone number parsing) */
+
+       /* Summary configuration */
+       SummaryField *summary_fields;
+       gint n_summary_fields;
+
+       ECollator *collator;            /* The ECollator to create sort keys for any sortable fields */
+};
+
+enum {
+       PROP_0,
+       PROP_LOCALE
+};
+
+enum {
+       E164_CHANGED,
+       DUP_CONTACT_REVISION,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE (EBookCache, e_book_cache, E_TYPE_CACHE,
+                        G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))
+
+G_DEFINE_BOXED_TYPE (EBookCacheSearchData, e_book_cache_search_data, e_book_cache_search_data_copy, 
e_book_cache_search_data_free)
+
+/**
+ * e_book_cache_search_data_new:
+ * @uid: a contact UID; cannot be %NULL
+ * @vcard: the contact as a vCard string; cannot be %NULL
+ * @extra: (nullable): any extra data stored with the contact, or %NULL
+ *
+ * Creates a new EBookCacheSearchData prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #EBookCacheSearchData. Free it with
+ *    e_book_cache_search_data_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookCacheSearchData *
+e_book_cache_search_data_new (const gchar *uid,
+                             const gchar *vcard,
+                             const gchar *extra)
+{
+       EBookCacheSearchData *data;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+       g_return_val_if_fail (vcard != NULL, NULL);
+
+       data = g_new0 (EBookCacheSearchData, 1);
+       data->uid = g_strdup (uid);
+       data->vcard = g_strdup (vcard);
+       data->extra = g_strdup (extra);
+
+       return data;
+}
+
+/**
+ * e_book_cache_search_data_copy:
+ * @data: (nullable): a source #EBookCacheSearchData to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @data. Free it with
+ *    e_book_cache_search_data_free() when no longer needed.
+ *    If the @data is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+EBookCacheSearchData *
+e_book_cache_search_data_copy (const EBookCacheSearchData *data)
+{
+       if (!data)
+               return NULL;
+
+       return e_book_cache_search_data_new (data->uid, data->vcard, data->extra);
+}
+
+/**
+ * e_book_cache_search_data_free:
+ * @data: (nullable): an #EBookCacheSearchData
+ *
+ * Frees the @data structure, previously allocated with e_book_cache_search_data_new()
+ * or e_book_cache_search_data_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_search_data_free (gpointer ptr)
+{
+       EBookCacheSearchData *data = ptr;
+
+       if (data) {
+               g_free (data->uid);
+               g_free (data->vcard);
+               g_free (data->extra);
+               g_free (data);
+       }
+}
+
+/* Default summary configuration */
+static EContactField default_summary_fields[] = {
+       E_CONTACT_UID,
+       E_CONTACT_REV,
+       E_CONTACT_FILE_AS,
+       E_CONTACT_NICKNAME,
+       E_CONTACT_FULL_NAME,
+       E_CONTACT_GIVEN_NAME,
+       E_CONTACT_FAMILY_NAME,
+       E_CONTACT_EMAIL,
+       E_CONTACT_TEL,
+       E_CONTACT_IS_LIST,
+       E_CONTACT_LIST_SHOW_ADDRESSES,
+       E_CONTACT_WANTS_HTML,
+       E_CONTACT_X509_CERT
+};
+
+/* Create indexes on full_name and email fields as autocompletion
+ * queries would mainly rely on this.
+ *
+ * Add sort keys for name fields as those are likely targets for
+ * cursor usage.
+ */
+static EContactField default_indexed_fields[] = {
+       E_CONTACT_FULL_NAME,
+       E_CONTACT_NICKNAME,
+       E_CONTACT_FILE_AS,
+       E_CONTACT_GIVEN_NAME,
+       E_CONTACT_FAMILY_NAME,
+       E_CONTACT_EMAIL,
+       E_CONTACT_FILE_AS,
+       E_CONTACT_FAMILY_NAME,
+       E_CONTACT_GIVEN_NAME
+};
+
+static EBookIndexType default_index_types[] = {
+       E_BOOK_INDEX_PREFIX,
+       E_BOOK_INDEX_PREFIX,
+       E_BOOK_INDEX_PREFIX,
+       E_BOOK_INDEX_PREFIX,
+       E_BOOK_INDEX_PREFIX,
+       E_BOOK_INDEX_PREFIX,
+       E_BOOK_INDEX_SORT_KEY,
+       E_BOOK_INDEX_SORT_KEY,
+       E_BOOK_INDEX_SORT_KEY
+};
+
+/******************************************************
+ *                  Summary Fields                    *
+ ******************************************************/
+
+static ECacheColumnInfo *
+column_info_new (SummaryField *field,
+                 const gchar *column_name,
+                 const gchar *column_type,
+                 const gchar *idx_prefix)
+{
+       ECacheColumnInfo *info;
+       gchar *index = NULL;
+
+       g_return_val_if_fail (column_name != NULL, NULL);
+
+       if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+               column_name = "value";
+
+       if (!column_type) {
+               if (field->type == G_TYPE_STRING)
+                       column_type = "TEXT";
+               else if (field->type == G_TYPE_BOOLEAN || field->type == E_TYPE_CONTACT_CERT)
+                       column_type = "INTEGER";
+               else if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+                       column_type = "TEXT";
+               else
+                       g_warn_if_reached ();
+       }
+
+       if (idx_prefix)
+               index = g_strconcat (idx_prefix, "_", field->dbname, NULL);
+
+       info = e_cache_column_info_new (column_name, column_type, index);
+
+       g_free (index);
+
+       return info;
+}
+
+static gint
+summary_field_array_index (GArray *array,
+                           EContactField field)
+{
+       gint ii;
+
+       for (ii = 0; ii < array->len; ii++) {
+               SummaryField *iter = &g_array_index (array, SummaryField, ii);
+               if (field == iter->field_id)
+                       return ii;
+       }
+
+       return -1;
+}
+
+static SummaryField *
+summary_field_append (GArray *array,
+                     EContactField field_id,
+                     GError **error)
+{
+       const gchar *dbname = NULL;
+       GType type = G_TYPE_INVALID;
+       gint idx;
+       SummaryField new_field = { 0, };
+
+       if (field_id < 1 || field_id >= E_CONTACT_FIELD_LAST) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_FIELD,
+                       _("Unsupported contact field “%d” specified in summary"),
+                       field_id);
+
+               return NULL;
+       }
+
+       /* Avoid including the same field twice in the summary */
+       idx = summary_field_array_index (array, field_id);
+       if (idx >= 0)
+               return &g_array_index (array, SummaryField, idx);
+
+       /* Resolve some exceptions, we store these
+        * specific contact fields with different names
+        * than those found in the EContactField table
+        */
+       switch (field_id) {
+       case E_CONTACT_UID:
+       case E_CONTACT_REV:
+               /* Skip these, it's already in the ECache */
+               return NULL;
+       case E_CONTACT_IS_LIST:
+               dbname = "is_list";
+               break;
+       default:
+               dbname = e_contact_field_name (field_id);
+               break;
+       }
+
+       type = e_contact_field_type (field_id);
+
+       if (type != G_TYPE_STRING &&
+           type != G_TYPE_BOOLEAN &&
+           type != E_TYPE_CONTACT_CERT &&
+           type != E_TYPE_CONTACT_ATTR_LIST) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_FIELD,
+                       _("Contact field “%s” of type “%s” specified in summary, "
+                       "but only boolean, string and string list field types are supported"),
+                       e_contact_pretty_name (field_id), g_type_name (type));
+
+               return NULL;
+       }
+
+       if (type == E_TYPE_CONTACT_ATTR_LIST) {
+               new_field.aux_table = g_strconcat ("attrlist", "_", dbname, "_list", NULL);
+               new_field.aux_table_symbolic = g_strconcat (dbname, "_list", NULL);
+       }
+
+       new_field.field_id = field_id;
+       new_field.dbname = dbname;
+       new_field.type = type;
+       new_field.index = 0;
+
+       g_array_append_val (array, new_field);
+
+       return &g_array_index (array, SummaryField, array->len - 1);
+}
+
+static void
+summary_fields_add_indexes (GArray *array,
+                            EContactField *indexes,
+                            EBookIndexType *index_types,
+                            gint n_indexes)
+{
+       gint ii, jj;
+
+       for (ii = 0; ii < array->len; ii++) {
+               SummaryField *sfield = &g_array_index (array, SummaryField, ii);
+
+               for (jj = 0; jj < n_indexes; jj++) {
+                       if (sfield->field_id == indexes[jj])
+                               sfield->index |= (1 << index_types[jj]);
+
+               }
+       }
+}
+
+static inline gint
+summary_field_get_index (EBookCache *book_cache,
+                         EContactField field_id)
+{
+       gint ii;
+
+       for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+               if (book_cache->priv->summary_fields[ii].field_id == field_id)
+                       return ii;
+       }
+
+       return -1;
+}
+
+static inline SummaryField *
+summary_field_get (EBookCache *book_cache,
+                   EContactField field_id)
+{
+       gint index;
+
+       index = summary_field_get_index (book_cache, field_id);
+       if (index >= 0)
+               return &(book_cache->priv->summary_fields[index]);
+
+       return NULL;
+}
+
+static void
+summary_field_init_dbnames (SummaryField *field)
+{
+       if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+               field->dbname_idx_sort_key = g_strconcat (field->dbname, "_", EBC_SUFFIX_SORT_KEY, NULL);
+       }
+
+       if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+           (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+               field->dbname_idx_suffix = g_strconcat (field->dbname, "_", EBC_SUFFIX_REVERSE, NULL);
+       }
+
+       if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+           (field->index & INDEX_FLAG (PHONE)) != 0) {
+               field->dbname_idx_phone = g_strconcat (field->dbname, "_", EBC_SUFFIX_PHONE, NULL);
+               field->dbname_idx_country = g_strconcat (field->dbname, "_", EBC_SUFFIX_COUNTRY, NULL);
+       }
+}
+
+static void
+summary_field_prepend_columns (SummaryField *field,
+                              GSList **out_columns)
+{
+       ECacheColumnInfo *info;
+
+       /* Doesn't hurt to verify a bit more here, this shouldn't happen though */
+       g_return_if_fail (
+               field->type == G_TYPE_STRING ||
+               field->type == G_TYPE_BOOLEAN ||
+               field->type == E_TYPE_CONTACT_CERT ||
+               field->type == E_TYPE_CONTACT_ATTR_LIST);
+
+       /* Normal / default column */
+       info = column_info_new (field, field->dbname, NULL,
+               (field->index & INDEX_FLAG (PREFIX)) != 0 ? "INDEX" : NULL);
+       *out_columns = g_slist_prepend (*out_columns, info);
+
+       /* Localized column, for storing sort keys */
+       if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+               info = column_info_new (field, field->dbname_idx_sort_key, "TEXT", "SINDEX");
+               *out_columns = g_slist_prepend (*out_columns, info);
+       }
+
+       /* Suffix match column */
+       if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+           (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+               info = column_info_new (field, field->dbname_idx_suffix, "TEXT", "RINDEX");
+               *out_columns = g_slist_prepend (*out_columns, info);
+       }
+
+       /* Phone match columns */
+       if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+           (field->index & INDEX_FLAG (PHONE)) != 0) {
+
+               /* One indexed column for storing the national number */
+               info = column_info_new (field, field->dbname_idx_phone, "TEXT", "PINDEX");
+               *out_columns = g_slist_prepend (*out_columns, info);
+
+               /* One integer column for storing the country code */
+               info = column_info_new (field, field->dbname_idx_country, "INTEGER DEFAULT 0", NULL);
+               *out_columns = g_slist_prepend (*out_columns, info);
+       }
+}
+
+static void
+summary_fields_array_free (SummaryField *fields,
+                           gint n_fields)
+{
+       gint ii;
+
+       for (ii = 0; ii < n_fields; ii++) {
+               g_free (fields[ii].dbname_idx_suffix);
+               g_free (fields[ii].dbname_idx_phone);
+               g_free (fields[ii].dbname_idx_country);
+               g_free (fields[ii].dbname_idx_sort_key);
+               g_free (fields[ii].aux_table);
+               g_free (fields[ii].aux_table_symbolic);
+       }
+
+       g_free (fields);
+}
+
+/******************************************************
+ *       Functions installed into the SQLite          *
+ ******************************************************/
+
+/* Implementation for REGEXP keyword */
+static void
+ebc_regexp (sqlite3_context *context,
+           gint argc,
+           sqlite3_value **argv)
+{
+       GRegex *regex;
+       const gchar *expression;
+       const gchar *text;
+
+       /* Reuse the same GRegex for all REGEXP queries with the same expression */
+       regex = sqlite3_get_auxdata (context, 0);
+       if (!regex) {
+               GError *error = NULL;
+
+               expression = (const gchar *) sqlite3_value_text (argv[0]);
+
+               regex = g_regex_new (expression, 0, 0, &error);
+
+               if (!regex) {
+                       sqlite3_result_error (
+                               context,
+                               error ? error->message :
+                               _("Error parsing regular expression"),
+                               -1);
+                       g_clear_error (&error);
+                       return;
+               }
+
+               /* SQLite will take care of freeing the GRegex when we're done with the query */
+               sqlite3_set_auxdata (context, 0, regex, (GDestroyNotify) g_regex_unref);
+       }
+
+       /* Now perform the comparison */
+       text = (const gchar *) sqlite3_value_text (argv[1]);
+       if (text != NULL) {
+               gboolean match;
+
+               match = g_regex_match (regex, text, 0, NULL);
+               sqlite3_result_int (context, match ? 1 : 0);
+       }
+}
+
+/* Implementation of EBC_FUNC_COMPARE_VCARD (fallback for non-summary queries) */
+static void
+ebc_compare_vcard (sqlite3_context *context,
+                  gint argc,
+                  sqlite3_value **argv)
+{
+       EBookBackendSExp *sexp = NULL;
+       const gchar *text;
+       const gchar *vcard;
+
+       /* Reuse the same sexp for all queries with the same search expression */
+       sexp = sqlite3_get_auxdata (context, 0);
+       if (!sexp) {
+
+               /* The first argument will be reused for many rows */
+               text = (const gchar *) sqlite3_value_text (argv[0]);
+               if (text) {
+                       sexp = e_book_backend_sexp_new (text);
+                       sqlite3_set_auxdata (
+                               context, 0,
+                               sexp,
+                               g_object_unref);
+               }
+
+               /* This shouldn't happen, catch invalid sexp in preflight */
+               if (!sexp) {
+                       sqlite3_result_int (context, 0);
+                       return;
+               }
+
+       }
+
+       /* Reuse the same vcard as much as possible (it can be referred to more than
+        * once in the query, so it can be reused for multiple comparisons on the same row)
+        */
+       vcard = sqlite3_get_auxdata (context, 1);
+       if (!vcard) {
+               vcard = (const gchar *) sqlite3_value_text (argv[1]);
+
+               if (vcard)
+                       sqlite3_set_auxdata (context, 1, g_strdup (vcard), g_free);
+       }
+
+       /* A NULL vcard can never match */
+       if (!vcard || !*vcard) {
+               sqlite3_result_int (context, 0);
+               return;
+       }
+
+       /* Compare this vcard */
+       if (e_book_backend_sexp_match_vcard (sexp, vcard))
+               sqlite3_result_int (context, 1);
+       else
+               sqlite3_result_int (context, 0);
+}
+
+static void
+ebc_eqphone (sqlite3_context *context,
+            gint argc,
+            sqlite3_value **argv,
+            EPhoneNumberMatch requested_match)
+{
+       EBookCache *ebc = sqlite3_user_data (context);
+       EPhoneNumber *input_phone = NULL, *row_phone = NULL;
+       EPhoneNumberMatch match = E_PHONE_NUMBER_MATCH_NONE;
+       const gchar *text;
+
+       /* Reuse the same phone number for all queries with the same phone number argument */
+       input_phone = sqlite3_get_auxdata (context, 0);
+       if (!input_phone) {
+
+               /* The first argument will be reused for many rows */
+               text = (const gchar *) sqlite3_value_text (argv[0]);
+               if (text) {
+
+                       /* Ignore errors, they are fine for phone numbers */
+                       input_phone = e_phone_number_from_string (text, ebc->priv->region_code, NULL);
+
+                       /* SQLite will take care of freeing the EPhoneNumber when we're done with the 
expression */
+                       if (input_phone)
+                               sqlite3_set_auxdata (
+                                       context, 0,
+                                       input_phone,
+                                       (GDestroyNotify) e_phone_number_free);
+               }
+       }
+
+       /* This shouldn't happen, as we catch invalid phone number queries in preflight
+        */
+       if (!input_phone) {
+               sqlite3_result_int (context, 0);
+               return;
+       }
+
+       /* Parse the phone number for this row */
+       text = (const gchar *) sqlite3_value_text (argv[1]);
+       if (text != NULL) {
+               row_phone = e_phone_number_from_string (text, ebc->priv->region_code, NULL);
+
+               /* And perform the comparison */
+               if (row_phone) {
+                       match = e_phone_number_compare (input_phone, row_phone);
+
+                       e_phone_number_free (row_phone);
+               }
+       }
+
+       /* Now report the result */
+       if (match != E_PHONE_NUMBER_MATCH_NONE &&
+           match <= requested_match)
+               sqlite3_result_int (context, 1);
+       else
+               sqlite3_result_int (context, 0);
+}
+
+/* Exact phone number match function: EBC_FUNC_EQPHONE_EXACT */
+static void
+ebc_eqphone_exact (sqlite3_context *context,
+                  gint argc,
+                  sqlite3_value **argv)
+{
+       ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_EXACT);
+}
+
+/* National phone number match function: EBC_FUNC_EQPHONE_NATIONAL */
+static void
+ebc_eqphone_national (sqlite3_context *context,
+                     gint argc,
+                     sqlite3_value **argv)
+{
+       ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_NATIONAL);
+}
+
+/* Short phone number match function: EBC_FUNC_EQPHONE_SHORT */
+static void
+ebc_eqphone_short (sqlite3_context *context,
+                  gint argc,
+                  sqlite3_value **argv)
+{
+       ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_SHORT);
+}
+
+typedef void   (*EBCCustomFunc)        (sqlite3_context *context,
+                                        gint argc,
+                                        sqlite3_value **argv);
+
+typedef struct {
+       const gchar *name;
+       EBCCustomFunc func;
+       gint arguments;
+} EBCCustomFuncTab;
+
+static EBCCustomFuncTab ebc_custom_functions[] = {
+       { "regexp",                  ebc_regexp,           2 }, /* regexp (expression, column_data) */
+       { EBC_FUNC_COMPARE_VCARD,    ebc_compare_vcard,    2 }, /* compare_vcard (sexp, vcard) */
+       { EBC_FUNC_EQPHONE_EXACT,    ebc_eqphone_exact,    2 }, /* eqphone_exact (search_input, column_data) 
*/
+       { EBC_FUNC_EQPHONE_NATIONAL, ebc_eqphone_national, 2 }, /* eqphone_national (search_input, 
column_data) */
+       { EBC_FUNC_EQPHONE_SHORT,    ebc_eqphone_short,    2 }, /* eqphone_national (search_input, 
column_data) */
+};
+
+/******************************************************
+ *            Fallback Collation Sequences            *
+ ******************************************************
+ *
+ * The fallback simply compares vcards, vcards which have been
+ * stored on the cursor will have a preencoded key (these
+ * utilities encode & decode that key).
+ */
+static gchar *
+ebc_encode_vcard_sort_key (const gchar *sort_key)
+{
+       EVCard *vcard = e_vcard_new ();
+       gchar *base64;
+       gchar *encoded;
+
+       /* Encode this otherwise e-vcard messes it up */
+       base64 = g_base64_encode ((const guchar *) sort_key, strlen (sort_key));
+       e_vcard_append_attribute_with_value (
+               vcard,
+               e_vcard_attribute_new (NULL, EBC_VCARD_SORT_KEY),
+               base64);
+       encoded = e_vcard_to_string (vcard, EVC_FORMAT_VCARD_30);
+
+       g_free (base64);
+       g_object_unref (vcard);
+
+       return encoded;
+}
+
+static gchar *
+ebc_decode_vcard_sort_key_from_vcard (EVCard *vcard)
+{
+       EVCardAttribute *attr;
+       GList *values = NULL;
+       gchar *sort_key = NULL;
+       gchar *base64 = NULL;
+
+       attr = e_vcard_get_attribute (vcard, EBC_VCARD_SORT_KEY);
+       if (attr)
+               values = e_vcard_attribute_get_values (attr);
+
+       if (values && values->data) {
+               gsize len;
+
+               base64 = g_strdup (values->data);
+
+               sort_key = (gchar *) g_base64_decode (base64, &len);
+               g_free (base64);
+       }
+
+       return sort_key;
+}
+
+static gchar *
+ebc_decode_vcard_sort_key (const gchar *encoded)
+{
+       EVCard *vcard;
+       gchar *sort_key;
+
+       vcard = e_vcard_new_from_string (encoded);
+       sort_key = ebc_decode_vcard_sort_key_from_vcard (vcard);
+       g_object_unref (vcard);
+
+       return sort_key;
+}
+
+static gchar *
+convert_phone (const gchar *normal,
+               const gchar *region_code,
+               gint *out_country_code)
+{
+       EPhoneNumber *number = NULL;
+       gchar *national_number = NULL;
+       gint country_code = 0;
+
+       /* Don't warn about erronous phone number strings, it's a perfectly normal
+        * use case for users to enter notes instead of phone numbers in the phone
+        * number contact fields, such as "Ask Jenny for Lisa's phone number"
+        */
+       if (normal && e_phone_number_is_supported ())
+               number = e_phone_number_from_string (normal, region_code, NULL);
+
+       if (number) {
+               EPhoneNumberCountrySource source;
+
+               national_number = e_phone_number_get_national_number (number);
+               country_code = e_phone_number_get_country_code (number, &source);
+               e_phone_number_free (number);
+
+               if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+                       country_code = 0;
+       }
+
+       if (out_country_code)
+               *out_country_code = country_code;
+
+       return national_number;
+}
+
+static gchar *
+remove_leading_zeros (gchar *number)
+{
+       gchar *trimmed = NULL;
+       gchar *tmp = number;
+
+       g_return_val_if_fail (NULL != number, NULL);
+
+       while ('0' == *tmp)
+               tmp++;
+       trimmed = g_strdup (tmp);
+       g_free (number);
+
+       return trimmed;
+}
+
+static void
+ebc_fill_other_columns (EBookCache *book_cache,
+                       EContact *contact,
+                       ECacheColumnValues *other_columns)
+{
+       gint ii;
+
+       g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+       g_return_if_fail (E_IS_CONTACT (contact));
+       g_return_if_fail (other_columns != NULL);
+
+       for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+               if (field->field_id == E_CONTACT_UID ||
+                   field->field_id == E_CONTACT_REV) {
+                       continue;
+               }
+
+               if (field->type == G_TYPE_STRING) {
+                       gchar *val;
+                       gchar *normal;
+                       gchar *str;
+
+                       val = e_contact_get (contact, field->field_id);
+                       normal = e_util_utf8_normalize (val);
+
+                       e_cache_column_values_take_value (other_columns, field->dbname, normal);
+
+                       if ((field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+                               if (val)
+                                       str = e_collator_generate_key (book_cache->priv->collator, val, NULL);
+                               else
+                                       str = g_strdup ("");
+
+                               e_cache_column_values_take_value (other_columns, field->dbname_idx_sort_key, 
str);
+                       }
+
+                       if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+                               if (normal)
+                                       str = g_utf8_strreverse (normal, -1);
+                               else
+                                       str = NULL;
+
+                               e_cache_column_values_take_value (other_columns, field->dbname_idx_suffix, 
str);
+                       }
+
+                       if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+                               gint country_code;
+
+                               str = convert_phone (normal, book_cache->priv->region_code, &country_code);
+                               str = remove_leading_zeros (str);
+
+                               e_cache_column_values_take_value (other_columns, field->dbname_idx_phone, 
str);
+
+                               str = g_strdup_printf ("%d", country_code);
+
+                               e_cache_column_values_take_value (other_columns, field->dbname_idx_country, 
str);
+                       }
+
+                       g_free (val);
+               } else if (field->type == G_TYPE_BOOLEAN) {
+                       gboolean val;
+
+                       val = e_contact_get (contact, field->field_id) ? TRUE : FALSE;
+
+                       e_cache_column_values_take_value (other_columns, field->dbname, g_strdup_printf 
("%d", val ? 1 : 0));
+               } else if (field->type == E_TYPE_CONTACT_CERT) {
+                       EContactCert *cert = NULL;
+
+                       cert = e_contact_get (contact, field->field_id);
+
+                       /* We don't actually store the cert; only a boolean to indicate
+                        * that is *has* a cert. */
+                       e_cache_column_values_take_value (other_columns, field->dbname, g_strdup_printf 
("%d", cert ? 1 : 0));
+                       e_contact_cert_free (cert);
+               } else if (field->type != E_TYPE_CONTACT_ATTR_LIST) {
+                       g_warn_if_reached ();
+               }
+       }
+}
+
+static inline void
+format_column_declaration (GString *string,
+                          ECacheColumnInfo *info)
+{
+       g_string_append (string, info->name);
+       g_string_append_c (string, ' ');
+
+       g_string_append (string, info->type);
+
+}
+
+static gboolean
+ebc_init_aux_tables (EBookCache *book_cache,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       GString *string;
+       gboolean success = TRUE;
+       gchar *tmp;
+       gint ii;
+
+       for (ii = 0; success && ii < book_cache->priv->n_summary_fields; ii++) {
+               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+               GSList *aux_columns = NULL, *link;
+
+               if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               summary_field_prepend_columns (field, &aux_columns);
+               if (!aux_columns)
+                       continue;
+
+               /* Create the auxiliary table for this multi valued field */
+               string = g_string_sized_new (
+                       COLUMN_DEFINITION_BYTES * 3 +
+                       COLUMN_DEFINITION_BYTES * g_slist_length (aux_columns));
+
+               e_cache_sqlite_stmt_append_printf (string, "CREATE TABLE IF NOT EXISTS %Q (uid TEXT NOT NULL 
REFERENCES " E_CACHE_TABLE_OBJECTS
+                                                 " (" E_CACHE_COLUMN_UID ")",
+                                                 field->aux_table);
+               for (link = aux_columns; link; link = g_slist_next (link)) {
+                       ECacheColumnInfo *info = link->data;
+
+                       g_string_append (string, ", ");
+                       format_column_declaration (string, info);
+               }
+               g_string_append_c (string, ')');
+
+               success = e_cache_sqlite_exec (E_CACHE (book_cache), string->str, cancellable, error);
+               g_string_free (string, TRUE);
+
+               if (success) {
+                       /* Create an index on the implied 'uid' column, this is important
+                        * when replacing (modifying) contacts, since we need to remove
+                        * all rows in an auxiliary table which matches a given UID.
+                        *
+                        * This index speeds up the constraint in a statement such as:
+                        *
+                        *   DELETE from email_list WHERE email_list.uid = 'contact uid'
+                        */
+                       tmp = e_cache_sqlite_stmt_printf ("CREATE INDEX IF NOT EXISTS UID_INDEX_%s_%s ON %Q 
(uid)",
+                               field->dbname, field->aux_table, field->aux_table);
+                       success = e_cache_sqlite_exec (E_CACHE (book_cache), tmp, cancellable, error);
+                       e_cache_sqlite_stmt_free (tmp);
+               }
+
+               /* Add indexes to columns in this auxiliary table
+                */
+               for (link = aux_columns; success && link; link = g_slist_next (link)) {
+                       ECacheColumnInfo *info = link->data;
+
+                       if (info->index_name) {
+                               tmp = e_cache_sqlite_stmt_printf ("CREATE INDEX IF NOT EXISTS %Q ON %Q (%s)",
+                                       info->index_name, field->aux_table, info->name);
+                               success = e_cache_sqlite_exec (E_CACHE (book_cache), tmp, cancellable, error);
+                               e_cache_sqlite_stmt_free (tmp);
+                       }
+               }
+
+               g_slist_free_full (aux_columns, e_cache_column_info_free);
+       }
+
+       return success;
+}
+
+static gboolean
+ebc_run_multi_insert_one (ECache *cache,
+                          SummaryField *field,
+                          const gchar *uid,
+                          const gchar *value,
+                         GCancellable *cancellable,
+                          GError **error)
+{
+       GString *stmt, *values;
+       gchar *normal;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (field != NULL, FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       stmt = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+       values = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+
+       normal = e_util_utf8_normalize (value);
+
+       e_cache_sqlite_stmt_append_printf (stmt, "INSERT INTO %Q (uid, value", field->aux_table);
+
+       if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+               g_string_append (stmt, ", value_" EBC_SUFFIX_REVERSE);
+
+               if (normal) {
+                       gchar *str;
+
+                       str = g_utf8_strreverse (normal, -1);
+
+                       e_cache_sqlite_stmt_append_printf (values, ", %Q", str);
+
+                       g_free (str);
+               } else {
+                       g_string_append (values, ", NULL");
+               }
+       }
+
+       if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+               EBookCache *book_cache;
+               gint country_code = 0;
+               gchar *str;
+
+               g_string_append (stmt, ", value_" EBC_SUFFIX_PHONE);
+               g_string_append (stmt, ", value_" EBC_SUFFIX_COUNTRY);
+
+               book_cache = E_BOOK_CACHE (cache);
+               str = convert_phone (normal, book_cache->priv->region_code, &country_code);
+               str = remove_leading_zeros (str);
+
+               if (str) {
+                       e_cache_sqlite_stmt_append_printf (values, ", %Q", str);
+               } else {
+                       g_string_append (values, ",NULL");
+               }
+
+               g_string_append_printf (values, ",%d", country_code);
+       }
+
+       e_cache_sqlite_stmt_append_printf (stmt, ") VALUES (%Q, %Q", uid, normal);
+       g_free (normal);
+
+       g_string_append (stmt, values->str);
+       g_string_append_c (stmt, ')');
+
+       success = e_cache_sqlite_exec (cache, stmt->str, cancellable, error);
+
+       g_string_free (stmt, TRUE);
+       g_string_free (values, TRUE);
+
+       return success;
+}
+
+static gboolean
+ebc_run_multi_insert (ECache *cache,
+                     SummaryField *field,
+                     const gchar *uid,
+                     EContact *contact,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       GList *values, *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (field != NULL, FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+       values = e_contact_get (contact, field->field_id);
+
+       for (link = values; success && link; link = g_list_next (link)) {
+               const gchar *value = link->data;
+
+               success = ebc_run_multi_insert_one (cache, field, uid, value, cancellable, error);
+       }
+
+       /* Free the list of allocated strings */
+       e_contact_attr_list_free (values);
+
+       return success;
+}
+
+static gboolean
+ebc_run_multi_delete (ECache *cache,
+                     SummaryField *field,
+                     const gchar *uid,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (field != NULL, FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q WHERE uid=%Q", field->aux_table, uid);
+       success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+       e_cache_sqlite_stmt_free (stmt);
+
+       return success;
+}
+
+static gboolean
+ebc_update_aux_tables (ECache *cache,
+                      const gchar *uid,
+                      const gchar *revision,
+                      const gchar *object,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       EBookCache *book_cache;
+       EContact *contact = NULL;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+       book_cache = E_BOOK_CACHE (cache);
+
+       for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+               if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               if (!contact) {
+                       contact = e_contact_new_from_vcard_with_uid (object, uid);
+                       success = contact != NULL;
+               }
+
+               success = success && ebc_run_multi_delete (cache, field, uid, cancellable, error);
+               success = success && ebc_run_multi_insert (cache, field, uid, contact, cancellable, error);
+       }
+
+       g_clear_object (&contact);
+
+       return success;
+}
+
+static gboolean
+ebc_delete_from_aux_tables (ECache *cache,
+                           const gchar *uid,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       EBookCache *book_cache;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       book_cache = E_BOOK_CACHE (cache);
+
+       for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+               if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               success = success && ebc_run_multi_delete (cache, field, uid, cancellable, error);
+       }
+
+       return success;
+}
+
+static gboolean
+ebc_delete_from_aux_tables_offline_deleted (ECache *cache,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       EBookCache *book_cache;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+       book_cache = E_BOOK_CACHE (cache);
+
+       for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+               gchar *stmt;
+
+               if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q WHERE uid IN ("
+                       "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_STATE "=%d)",
+                       field->aux_table, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+               success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+
+               e_cache_sqlite_stmt_free (stmt);
+       }
+
+       return success;
+}
+
+static gboolean
+ebc_empty_aux_tables (ECache *cache,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       EBookCache *book_cache;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+       book_cache = E_BOOK_CACHE (cache);
+
+       for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+               gchar *stmt;
+
+               if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q", field->aux_table);
+               success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+               e_cache_sqlite_stmt_free (stmt);
+       }
+
+       return success;
+}
+
+static gboolean
+ebc_upgrade_cb (ECache *cache,
+               const gchar *uid,
+               const gchar *revision,
+               const gchar *object,
+               EOfflineState offline_state,
+               gint ncols,
+               const gchar *column_names[],
+               const gchar *column_values[],
+               gchar **out_revision,
+               gchar **out_object,
+               EOfflineState *out_offline_state,
+               ECacheColumnValues **out_other_columns,
+               gpointer user_data)
+{
+       EContact *contact;
+       ECacheColumnValues *other_columns;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+       contact = e_contact_new_from_vcard_with_uid (object, uid);
+
+       /* Ignore broken rows? */
+       if (!contact)
+               return TRUE;
+
+       other_columns = e_cache_column_values_new ();
+
+       ebc_fill_other_columns (E_BOOK_CACHE (cache), contact, other_columns);
+
+       g_clear_object (&contact);
+
+       /* This will cause rewrite even when no values changed, but it's
+          necessary, because the locale changed, which can influence
+          other tables, not only the other columns. */
+       *out_other_columns = other_columns;
+
+       return TRUE;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebc_upgrade (EBookCache *book_cache,
+            GCancellable *cancellable,
+            GError **error)
+{
+       gboolean success;
+
+       success = e_cache_foreach_update (E_CACHE (book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
+               ebc_upgrade_cb, NULL, cancellable, error);
+
+       /* Store the new locale & country code */
+       success = success && e_cache_set_key (E_CACHE (book_cache), EBC_KEY_LC_COLLATE, 
book_cache->priv->locale, error);
+       success = success && e_cache_set_key (E_CACHE (book_cache), EBC_KEY_COUNTRYCODE, 
book_cache->priv->region_code, error);
+
+       return success;
+}
+
+static gboolean
+ebc_set_locale_internal (EBookCache *book_cache,
+                        const gchar *locale,
+                        GError **error)
+{
+       ECollator *collator;
+
+       g_return_val_if_fail (locale && locale[0], FALSE);
+
+       if (g_strcmp0 (book_cache->priv->locale, locale) != 0) {
+               gchar *country_code = NULL;
+
+               collator = e_collator_new_interpret_country (locale, &country_code, error);
+               if (collator == NULL)
+                       return FALSE;
+
+               /* Assign region code parsed from the locale by ICU */
+               g_free (book_cache->priv->region_code);
+               book_cache->priv->region_code = country_code;
+
+               /* Assign locale */
+               g_free (book_cache->priv->locale);
+               book_cache->priv->locale = g_strdup (locale);
+
+               /* Assign collator */
+               if (book_cache->priv->collator)
+                       e_collator_unref (book_cache->priv->collator);
+               book_cache->priv->collator = collator;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebc_init_locale (EBookCache *book_cache,
+                GCancellable *cancellable,
+                GError **error)
+{
+       gchar *stored_lc_collate;
+       gchar *stored_region_code;
+       const gchar *lc_collate;
+       gboolean success = TRUE;
+       gboolean relocalize_needed = FALSE;
+
+       /* Get the locale setting for this addressbook */
+       stored_lc_collate = e_cache_dup_key (E_CACHE (book_cache), EBC_KEY_LC_COLLATE, NULL);
+       stored_region_code = e_cache_dup_key (E_CACHE (book_cache), EBC_KEY_COUNTRYCODE, NULL);
+
+       lc_collate = stored_lc_collate;
+
+       /* When creating a new addressbook, or upgrading from a version
+        * where we did not have any locale setting; default to system locale,
+        * we must absolutely always have a locale set.
+        */
+       if (!lc_collate || !lc_collate[0])
+               lc_collate = setlocale (LC_COLLATE, NULL);
+       if (!lc_collate || !lc_collate[0])
+               lc_collate = setlocale (LC_ALL, NULL);
+       if (!lc_collate || !lc_collate[0])
+               lc_collate = "en_US.utf8";
+
+       /* Before touching any data, make sure we have a valid ECollator,
+        * this will also resolve our region code
+        */
+       if (success)
+               success = ebc_set_locale_internal (book_cache, lc_collate, error);
+
+       /* Check if we need to relocalize */
+       if (success) {
+               /* We may need to relocalize for a country code change */
+               if (g_strcmp0 (book_cache->priv->region_code, stored_region_code) != 0)
+                       relocalize_needed = TRUE;
+       }
+
+       /* Reinsert all contacts with new locale & country code */
+       if (success && relocalize_needed)
+               success = ebc_upgrade (book_cache, cancellable, error);
+
+       g_free (stored_region_code);
+       g_free (stored_lc_collate);
+
+       return success;
+}
+
+typedef struct {
+       EBookCache *book_cache;
+       EContactField field;
+} EBCCollData;
+
+static gint
+ebc_fallback_collator (gpointer ref,
+                      gint len1,
+                      gconstpointer data1,
+                      gint len2,
+                      gconstpointer data2)
+{
+       EBCCollData *data = ref;
+       EBookCache *book_cache;
+       EContact *contact1, *contact2;
+       const gchar *str1, *str2;
+       gchar *key1, *key2;
+       gchar *tmp;
+       gint result = 0;
+
+       book_cache = data->book_cache;
+
+       str1 = (const gchar *) data1;
+       str2 = (const gchar *) data2;
+
+       /* Construct 2 contacts (we're comparing vcards) */
+       contact1 = e_contact_new ();
+       contact2 = e_contact_new ();
+       e_vcard_construct_full (E_VCARD (contact1), str1, len1, NULL);
+       e_vcard_construct_full (E_VCARD (contact2), str2, len2, NULL);
+
+       /* Extract first key */
+       key1 = ebc_decode_vcard_sort_key_from_vcard (E_VCARD (contact1));
+       if (!key1) {
+               tmp = e_contact_get (contact1, data->field);
+               if (tmp)
+                       key1 = e_collator_generate_key (book_cache->priv->collator, tmp, NULL);
+               g_free (tmp);
+       }
+       if (!key1)
+               key1 = g_strdup ("");
+
+       /* Extract second key */
+       key2 = ebc_decode_vcard_sort_key_from_vcard (E_VCARD (contact2));
+       if (!key2) {
+               tmp = e_contact_get (contact2, data->field);
+               if (tmp)
+                       key2 = e_collator_generate_key (book_cache->priv->collator, tmp, NULL);
+               g_free (tmp);
+       }
+       if (!key2)
+               key2 = g_strdup ("");
+
+       result = strcmp (key1, key2);
+
+       g_free (key1);
+       g_free (key2);
+       g_object_unref (contact1);
+       g_object_unref (contact2);
+
+       return result;
+}
+
+static EBCCollData *
+ebc_coll_data_new (EBookCache *book_cache,
+                  EContactField field)
+{
+       EBCCollData *data = g_slice_new (EBCCollData);
+
+       data->book_cache = book_cache;
+       data->field = field;
+
+       return data;
+}
+
+static void
+ebc_coll_data_free (EBCCollData *data)
+{
+       if (data)
+               g_slice_free (EBCCollData, data);
+}
+
+/* COLLATE functions are generated on demand only */
+static void
+ebc_generate_collator (gpointer ref,
+                      sqlite3 *db,
+                      gint eTextRep,
+                      const gchar *coll_name)
+{
+       EBookCache *book_cache = ref;
+       EBCCollData *data;
+       EContactField field;
+       const gchar *field_name;
+
+       field_name = coll_name + strlen (EBC_COLLATE_PREFIX);
+       field = e_contact_field_id (field_name);
+
+       /* This should be caught before reaching here, just an extra check */
+       if (field == 0 || field >= E_CONTACT_FIELD_LAST ||
+           e_contact_field_type (field) != G_TYPE_STRING) {
+               g_warning ("Specified collation on invalid contact field");
+               return;
+       }
+
+       data = ebc_coll_data_new (book_cache, field);
+       sqlite3_create_collation_v2 (
+               db, coll_name, SQLITE_UTF8,
+               data, ebc_fallback_collator,
+               (GDestroyNotify) ebc_coll_data_free);
+}
+
+/***************************************************************
+ * Structures and utilities for preflight and query generation *
+ ***************************************************************/
+
+/* This enumeration is ordered by severity, higher values
+ * of PreflightStatus take precedence in error reporting.
+ */
+typedef enum {
+       PREFLIGHT_OK = 0,
+       PREFLIGHT_LIST_ALL,
+       PREFLIGHT_NOT_SUMMARIZED,
+       PREFLIGHT_INVALID,
+       PREFLIGHT_UNSUPPORTED,
+} PreflightStatus;
+
+/* Whether we can satisfy the constraints or whether we
+ * need to do a fallback, we still need to call
+ * ebc_generate_constraints()
+ */
+#define EBC_STATUS_GEN_CONSTRAINTS(status) \
+       ((status) == PREFLIGHT_OK || \
+        (status) == PREFLIGHT_NOT_SUMMARIZED)
+
+/* Internal extension of the EBookQueryTest enumeration */
+enum {
+       /* 'exists' is a supported query on a field, but not part of EBookQueryTest */
+       BOOK_QUERY_EXISTS = E_BOOK_QUERY_LAST,
+       BOOK_QUERY_EXISTS_VCARD,
+
+       /* From here the compound types start */
+       BOOK_QUERY_SUB_AND,
+       BOOK_QUERY_SUB_OR,
+       BOOK_QUERY_SUB_NOT,
+       BOOK_QUERY_SUB_END,
+
+       BOOK_QUERY_SUB_FIRST = BOOK_QUERY_SUB_AND,
+};
+
+#define EBC_QUERY_TYPE_STR(query) \
+       ((query) == BOOK_QUERY_EXISTS ? "exists" : \
+        (query) == BOOK_QUERY_EXISTS_VCARD ? "exists_vcard" : \
+        (query) == BOOK_QUERY_SUB_AND ? "AND" : \
+        (query) == BOOK_QUERY_SUB_OR ? "OR" : \
+        (query) == BOOK_QUERY_SUB_NOT ? "NOT" : \
+        (query) == BOOK_QUERY_SUB_END ? "END" : \
+        (query) == E_BOOK_QUERY_IS ? "is" : \
+        (query) == E_BOOK_QUERY_CONTAINS ? "contains" : \
+        (query) == E_BOOK_QUERY_BEGINS_WITH ? "begins-with" : \
+        (query) == E_BOOK_QUERY_ENDS_WITH ? "ends-with" : \
+        (query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER ? "eqphone" : \
+        (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER ? "eqphone-national" : \
+        (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER ? "eqphone-short" : \
+        (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-normal" : \
+        (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-raw" : "(unknown)")
+
+#define EBC_FIELD_ID_STR(field_id) \
+       ((field_id) == E_CONTACT_FIELD_LAST ? "x-evolution-any-field" : \
+        (field_id) == 0 ? "(not an EContactField)" : \
+        e_contact_field_name (field_id))
+
+#define IS_QUERY_PHONE(query) \
+       ((query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER || \
+        (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER || \
+        (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER)
+
+typedef struct {
+       guint          query; /* EBookQueryTest (extended) */
+} QueryElement;
+
+typedef struct {
+       guint          query; /* EBookQueryTest (extended) */
+} QueryDelimiter;
+
+typedef struct {
+       guint          query;          /* EBookQueryTest (extended) */
+
+       EContactField  field_id;       /* The EContactField to compare */
+       SummaryField  *field;          /* The summary field for 'field' */
+       gchar         *value;          /* The value to compare with */
+
+} QueryFieldTest;
+
+typedef struct {
+       guint          query;          /* EBookQueryTest (extended) */
+
+       /* Common fields from QueryFieldTest */
+       EContactField  field_id;       /* The EContactField to compare */
+       SummaryField  *field;          /* The summary field for 'field' */
+       gchar         *value;          /* The value to compare with */
+
+       /* Extension */
+       gchar         *region;   /* Region code from the query input */
+       gchar         *national; /* Parsed national number */
+       gint           country;  /* Parsed country code */
+} QueryPhoneTest;
+
+/* Stack initializer for the PreflightContext struct below */
+#define PREFLIGHT_CONTEXT_INIT { PREFLIGHT_OK, NULL, 0, FALSE }
+
+typedef struct {
+       PreflightStatus  status;         /* result status */
+       GPtrArray       *constraints;    /* main query; may be NULL */
+       guint64          aux_mask;       /* Bitmask of which auxiliary tables are needed in the query */
+       guint64          left_join_mask; /* Do we need to use a LEFT JOIN */
+} PreflightContext;
+
+static QueryElement *
+query_delimiter_new (guint query)
+{
+       QueryDelimiter *delim;
+
+       g_return_val_if_fail (query >= BOOK_QUERY_SUB_FIRST, NULL);
+
+       delim = g_slice_new (QueryDelimiter);
+       delim->query = query;
+
+       return (QueryElement *) delim;
+}
+
+static QueryFieldTest *
+query_field_test_new (guint query,
+                     EContactField field)
+{
+       QueryFieldTest *test;
+
+       g_return_val_if_fail (query < BOOK_QUERY_SUB_FIRST, NULL);
+       g_return_val_if_fail (IS_QUERY_PHONE (query) == FALSE, NULL);
+
+       test = g_slice_new (QueryFieldTest);
+       test->query = query;
+       test->field_id = field;
+
+       /* Instead of g_slice_new0, NULL them out manually */
+       test->field = NULL;
+       test->value = NULL;
+
+       return test;
+}
+
+static QueryPhoneTest *
+query_phone_test_new (guint query,
+                     EContactField field)
+{
+       QueryPhoneTest *test;
+
+       g_return_val_if_fail (IS_QUERY_PHONE (query), NULL);
+
+       test = g_slice_new (QueryPhoneTest);
+       test->query = query;
+       test->field_id = field;
+
+       /* Instead of g_slice_new0, NULL them out manually */
+       test->field = NULL;
+       test->value = NULL;
+
+       /* Extra QueryPhoneTest fields */
+       test->region = NULL;
+       test->national = NULL;
+       test->country = 0;
+
+       return test;
+}
+
+static void
+query_element_free (QueryElement *element)
+{
+       if (element) {
+
+               if (element->query >= BOOK_QUERY_SUB_FIRST) {
+                       QueryDelimiter *delim = (QueryDelimiter *) element;
+
+                       g_slice_free (QueryDelimiter, delim);
+               } else if (IS_QUERY_PHONE (element->query)) {
+                       QueryPhoneTest *test = (QueryPhoneTest *) element;
+
+                       g_free (test->value);
+                       g_free (test->region);
+                       g_free (test->national);
+                       g_slice_free (QueryPhoneTest, test);
+               } else {
+                       QueryFieldTest *test = (QueryFieldTest *) element;
+
+                       g_free (test->value);
+                       g_slice_free (QueryFieldTest, test);
+               }
+       }
+}
+
+/* We use ptr arrays for the QueryElement vectors */
+static inline void
+constraints_insert (GPtrArray *array,
+                   gint idx,
+                   gpointer data)
+{
+       g_return_if_fail ((idx >= -1) && (idx < (gint) array->len + 1));
+
+       if (idx < 0)
+               idx = array->len;
+
+       g_ptr_array_add (array, NULL);
+
+       if (idx != (array->len - 1))
+               memmove (
+                       &(array->pdata[idx + 1]),
+                       &(array->pdata[idx]),
+                       ((array->len - 1) - idx) * sizeof (gpointer));
+
+       array->pdata[idx] = data;
+}
+
+static inline void
+constraints_insert_delimiter (GPtrArray *array,
+                             gint idx,
+                             guint query)
+{
+       QueryElement *delim;
+
+       delim = query_delimiter_new (query);
+       constraints_insert (array, idx, delim);
+}
+
+static inline void
+constraints_insert_field_test (GPtrArray *array,
+                              gint idx,
+                              SummaryField *field,
+                              guint query,
+                              const gchar *value)
+{
+       QueryFieldTest *test;
+
+       test = query_field_test_new (query, field->field_id);
+       test->field = field;
+       test->value = g_strdup (value);
+
+       constraints_insert (array, idx, test);
+}
+
+static void
+preflight_context_clear (PreflightContext *context)
+{
+       if (context) {
+               /* Free any allocated data, but leave the context values in place */
+               if (context->constraints)
+                       g_ptr_array_free (context->constraints, TRUE);
+               context->constraints = NULL;
+       }
+}
+
+/* A small API to track the current sub-query context.
+ *
+ * I.e. sub contexts can be OR, AND, or NOT, in which
+ * field tests or other sub contexts are nested.
+ *
+ * The 'count' field is a simple counter of how deep the contexts are nested.
+ *
+ * The 'cond_count' field is to be used by the caller for its own purposes;
+ * it is incremented in sub_query_context_push() only if the inc_cond_count
+ * parameter is TRUE. This is used by query_preflight_check() in a complex
+ * fashion which is described there.
+ */
+typedef GQueue SubQueryContext;
+
+typedef struct {
+       guint sub_type; /* The type of this sub context */
+       guint count;    /* The number of field tests so far in this context */
+       guint cond_count; /* User-specific conditional counter */
+} SubQueryData;
+
+#define sub_query_context_new g_queue_new
+#define sub_query_context_free(ctx) g_queue_free (ctx)
+
+static inline void
+sub_query_context_push (SubQueryContext *ctx,
+                       guint sub_type,
+                       gboolean inc_cond_count)
+{
+       SubQueryData *data, *prev;
+
+       prev = g_queue_peek_tail (ctx);
+
+       data = g_slice_new (SubQueryData);
+       data->sub_type = sub_type;
+       data->count = 0;
+       data->cond_count = prev ? prev->cond_count : 0;
+       if (inc_cond_count)
+               data->cond_count++;
+
+       g_queue_push_tail (ctx, data);
+}
+
+static inline void
+sub_query_context_pop (SubQueryContext *ctx)
+{
+       SubQueryData *data;
+
+       data = g_queue_pop_tail (ctx);
+       g_slice_free (SubQueryData, data);
+}
+
+static inline guint
+sub_query_context_peek_type (SubQueryContext *ctx)
+{
+       SubQueryData *data;
+
+       data = g_queue_peek_tail (ctx);
+
+       return data->sub_type;
+}
+
+static inline guint
+sub_query_context_peek_cond_counter (SubQueryContext *ctx)
+{
+       SubQueryData *data;
+
+       data = g_queue_peek_tail (ctx);
+
+       if (data)
+               return data->cond_count;
+       else
+               return 0;
+}
+
+/* Returns the context field test count before incrementing */
+static inline guint
+sub_query_context_increment (SubQueryContext *ctx)
+{
+       SubQueryData *data;
+
+       data = g_queue_peek_tail (ctx);
+
+       if (data) {
+               data->count++;
+
+               return (data->count - 1);
+       }
+
+       /* If we're not in a sub context, just return 0 */
+       return 0;
+}
+
+/**********************************************************
+ *                  Querying preflighting                 *
+ **********************************************************
+ *
+ * The preflight checks are performed before a query might
+ * take place in order to evaluate whether the given query
+ * can be performed with the current summary configuration.
+ *
+ * After preflighting, all relevant data has been extracted
+ * from the search expression and the search expression need
+ * not be parsed again.
+ */
+
+/* The PreflightSubCallback is expected to return TRUE
+ * to keep iterating and FALSE to abort iteration.
+ *
+ * The sub_level is the counter of how deep the 'element'
+ * is nested in sub elements, the offset is the real offset
+ * of 'element' in the array passed to query_preflight_foreach_sub().
+ */
+typedef gboolean (* PreflightSubCallback) (QueryElement *element,
+                                          gint          sub_level,
+                                          gint          offset,
+                                          gpointer      user_data);
+
+static void
+query_preflight_foreach_sub (QueryElement **elements,
+                            gint n_elements,
+                            gint offset,
+                            gboolean include_delim,
+                            PreflightSubCallback callback,
+                            gpointer user_data)
+{
+       gint sub_counter = 1, ii;
+
+       g_return_if_fail (offset >= 0 && offset < n_elements);
+       g_return_if_fail (elements[offset]->query >= BOOK_QUERY_SUB_FIRST);
+       g_return_if_fail (callback != NULL);
+
+       if (include_delim && !callback (elements[offset], 0, offset, user_data))
+               return;
+
+       for (ii = (offset + 1); sub_counter > 0 && ii < n_elements; ii++) {
+
+               if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+
+                       if (elements[ii]->query == BOOK_QUERY_SUB_END)
+                               sub_counter--;
+                       else
+                               sub_counter++;
+
+                       if (include_delim &&
+                           !callback (elements[ii], sub_counter, ii, user_data))
+                               break;
+               } else {
+
+                       if (!callback (elements[ii], sub_counter, ii, user_data))
+                               break;
+               }
+       }
+}
+
+/* Table used in ESExp parsing below */
+static const struct {
+       const gchar *name;    /* Name of the symbol to match for this parse phase */
+       gboolean     subset;  /* TRUE for the subset ESExpIFunc, otherwise the field check ESExpFunc */
+       guint        test;    /* Extended EBookQueryTest value */
+} check_symbols[] = {
+       { "and",              TRUE, BOOK_QUERY_SUB_AND },
+       { "or",               TRUE, BOOK_QUERY_SUB_OR },
+       { "not",              TRUE, BOOK_QUERY_SUB_NOT },
+
+       { "contains",         FALSE, E_BOOK_QUERY_CONTAINS },
+       { "is",               FALSE, E_BOOK_QUERY_IS },
+       { "beginswith",       FALSE, E_BOOK_QUERY_BEGINS_WITH },
+       { "endswith",         FALSE, E_BOOK_QUERY_ENDS_WITH },
+       { "eqphone",          FALSE, E_BOOK_QUERY_EQUALS_PHONE_NUMBER },
+       { "eqphone_national", FALSE, E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER },
+       { "eqphone_short",    FALSE, E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER },
+       { "regex_normal",     FALSE, E_BOOK_QUERY_REGEX_NORMAL },
+       { "regex_raw",        FALSE, E_BOOK_QUERY_REGEX_RAW },
+       { "exists",           FALSE, BOOK_QUERY_EXISTS },
+       { "exists_vcard",     FALSE, BOOK_QUERY_EXISTS_VCARD }
+};
+
+/* Cheat our way into passing mode data to these funcs */
+static ESExpResult *
+func_check_subset (ESExp *f,
+                  gint argc,
+                  struct _ESExpTerm **argv,
+                  gpointer data)
+{
+       ESExpResult *result, *sub_result;
+       GPtrArray *result_array;
+       QueryElement *element, **sub_elements;
+       gint ii, jj, len;
+       guint query_type;
+
+       query_type = GPOINTER_TO_UINT (data);
+
+       /* The compound query delimiter is the first element in this return array */
+       result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+       element = query_delimiter_new (query_type);
+       g_ptr_array_add (result_array, element);
+
+       for (ii = 0; ii < argc; ii++) {
+               sub_result = e_sexp_term_eval (f, argv[ii]);
+
+               if (sub_result->type == ESEXP_RES_ARRAY_PTR) {
+                       /* Steal the elements directly from the sub result */
+                       sub_elements = (QueryElement **) sub_result->value.ptrarray->pdata;
+                       len = sub_result->value.ptrarray->len;
+
+                       for (jj = 0; jj < len; jj++) {
+                               element = sub_elements[jj];
+                               sub_elements[jj] = NULL;
+
+                               g_ptr_array_add (result_array, element);
+                       }
+               }
+               e_sexp_result_free (f, sub_result);
+       }
+
+       /* The last element in this return array is the sub end delimiter */
+       element = query_delimiter_new (BOOK_QUERY_SUB_END);
+       g_ptr_array_add (result_array, element);
+
+       result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+       result->value.ptrarray = result_array;
+
+       return result;
+}
+
+static ESExpResult *
+func_check (struct _ESExp *f,
+           gint argc,
+           struct _ESExpResult **argv,
+           gpointer data)
+{
+       ESExpResult *result;
+       GPtrArray *result_array;
+       QueryElement *element = NULL;
+       EContactField field_id = 0;
+       const gchar *query_name = NULL;
+       const gchar *query_value = NULL;
+       const gchar *query_extra = NULL;
+       guint query_type;
+
+       query_type = GPOINTER_TO_UINT (data);
+
+       if (argc == 1 && query_type == BOOK_QUERY_EXISTS &&
+           argv[0]->type == ESEXP_RES_STRING) {
+               query_name = argv[0]->value.string;
+
+               field_id = e_contact_field_id (query_name);
+       } else if (argc == 2 &&
+           argv[0]->type == ESEXP_RES_STRING &&
+           argv[1]->type == ESEXP_RES_STRING) {
+               query_name = argv[0]->value.string;
+               query_value = argv[1]->value.string;
+
+               /* We use E_CONTACT_FIELD_LAST to hold the special case of "x-evolution-any-field" */
+               if (g_strcmp0 (query_name, "x-evolution-any-field") == 0)
+                       field_id = E_CONTACT_FIELD_LAST;
+               else
+                       field_id = e_contact_field_id (query_name);
+
+       } else if (argc == 3 &&
+                  argv[0]->type == ESEXP_RES_STRING &&
+                  argv[1]->type == ESEXP_RES_STRING &&
+                  argv[2]->type == ESEXP_RES_STRING) {
+               query_name = argv[0]->value.string;
+               query_value = argv[1]->value.string;
+               query_extra = argv[2]->value.string;
+
+               field_id = e_contact_field_id (query_name);
+       }
+
+       if (IS_QUERY_PHONE (query_type)) {
+               QueryPhoneTest *test;
+
+               /* Collect data from this field test */
+               test = query_phone_test_new (query_type, field_id);
+               test->value = g_strdup (query_value);
+               test->region = g_strdup (query_extra);
+
+               element = (QueryElement *) test;
+       } else {
+               QueryFieldTest *test;
+
+               /* Collect data from this field test */
+               test = query_field_test_new (query_type, field_id);
+               test->value = g_strdup (query_value);
+
+               element = (QueryElement *) test;
+       }
+
+       /* Return an array with only one element, for lack of a pointer type ESExpResult */
+       result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+       g_ptr_array_add (result_array, element);
+
+       result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+       result->value.ptrarray = result_array;
+
+       return result;
+}
+
+/* Initial stage of preflighting:
+ *
+ *  o Parse the search expression and generate our array of QueryElements
+ *  o Collect lengths of query terms
+ */
+static void
+query_preflight_initialize (PreflightContext *context,
+                           const gchar *sexp)
+{
+       ESExp *sexp_parser;
+       ESExpResult *result;
+       gint esexp_error, ii;
+
+       if (!sexp || !*sexp) {
+               context->status = PREFLIGHT_LIST_ALL;
+               return;
+       }
+
+       sexp_parser = e_sexp_new ();
+
+       for (ii = 0; ii < G_N_ELEMENTS (check_symbols); ii++) {
+               if (check_symbols[ii].subset) {
+                       e_sexp_add_ifunction (
+                               sexp_parser, 0, check_symbols[ii].name,
+                               func_check_subset,
+                               GUINT_TO_POINTER (check_symbols[ii].test));
+               } else {
+                       e_sexp_add_function (
+                               sexp_parser, 0, check_symbols[ii].name,
+                               func_check,
+                               GUINT_TO_POINTER (check_symbols[ii].test));
+               }
+       }
+
+       e_sexp_input_text (sexp_parser, sexp, strlen (sexp));
+       esexp_error = e_sexp_parse (sexp_parser);
+
+       if (esexp_error == -1) {
+               context->status = PREFLIGHT_INVALID;
+       } else {
+               result = e_sexp_eval (sexp_parser);
+               if (result) {
+                       if (result->type == ESEXP_RES_ARRAY_PTR) {
+                               /* Just steal the array away from the ESexpResult */
+                               context->constraints = result->value.ptrarray;
+                               result->value.ptrarray = NULL;
+                       } else {
+                               context->status = PREFLIGHT_INVALID;
+                       }
+               }
+
+               e_sexp_result_free (sexp_parser, result);
+       }
+
+       g_object_unref (sexp_parser);
+}
+
+typedef struct {
+       EBookCache *book_cache;
+       SummaryField *field;
+       gboolean condition;
+} AttrListCheckData;
+
+static gboolean
+check_has_attr_list_cb (QueryElement *element,
+                       gint sub_level,
+                       gint offset,
+                       gpointer user_data)
+{
+       QueryFieldTest *test = (QueryFieldTest *) element;
+       AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+       /* We havent resolved all the fields at this stage yet */
+       if (!test->field)
+               test->field = summary_field_get (data->book_cache, test->field_id);
+
+       if (test->field && test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+               data->condition = TRUE;
+
+       /* Keep looping until we find one */
+       return !data->condition;
+}
+
+static gboolean
+check_different_fields_cb (QueryElement *element,
+                          gint sub_level,
+                          gint offset,
+                          gpointer user_data)
+{
+       QueryFieldTest *test = (QueryFieldTest *) element;
+       AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+       /* We havent resolved all the fields at this stage yet */
+       if (!test->field)
+               test->field = summary_field_get (data->book_cache, test->field_id);
+
+       if (test->field && data->field && test->field != data->field)
+               data->condition = TRUE;
+       else
+               data->field = test->field;
+
+       /* Keep looping until we find one */
+       return !data->condition;
+}
+
+/* What is done in this pass:
+ *  o Viability of the query is analyzed, i.e. can it be done with the summary columns.
+ *  o Phone numbers are parsed and loaded onto QueryPhoneTests
+ *  o Bitmask of auxiliary tables is collected
+ */
+static void
+query_preflight_check (PreflightContext *context,
+                      EBookCache *book_cache)
+{
+       gint ii, n_elements;
+       QueryElement **elements;
+       SubQueryContext *ctx;
+
+       context->status = PREFLIGHT_OK;
+
+       if (context->constraints != NULL) {
+               elements = (QueryElement **) context->constraints->pdata;
+               n_elements = context->constraints->len;
+       } else {
+               elements = NULL;
+               n_elements = 0;
+       }
+
+       ctx = sub_query_context_new ();
+
+       for (ii = 0; ii < n_elements; ii++) {
+               QueryFieldTest *test;
+               guint field_test;
+
+               if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+                       AttrListCheckData data = { book_cache, NULL, FALSE };
+
+                       switch (elements[ii]->query) {
+                       case BOOK_QUERY_SUB_OR:
+                               /* An OR doesn't have to force us to use a LEFT JOIN, as long
+                                  as all its sub-conditions are on the same field. */
+                               query_preflight_foreach_sub (elements,
+                                                            n_elements,
+                                                            ii, FALSE,
+                                                            check_different_fields_cb,
+                                                            &data);
+                       case BOOK_QUERY_SUB_AND:
+                               sub_query_context_push (ctx, elements[ii]->query, data.condition);
+                               break;
+                       case BOOK_QUERY_SUB_END:
+                               sub_query_context_pop (ctx);
+                               break;
+
+                       /* It's too complicated to properly perform
+                        * the unary NOT operator on a constraint which
+                        * accesses attribute lists.
+                        *
+                        * Hint, if the contact has a "%.com" email address
+                        * and a "%.org" email address, what do we return
+                        * for (not (endswith "email" ".com") ?
+                        *
+                        * Currently we rely on DISTINCT to sort out
+                        * muliple results from the attribute list tables,
+                        * this breaks down with NOT.
+                        */
+                       case BOOK_QUERY_SUB_NOT:
+                               query_preflight_foreach_sub (elements,
+                                                            n_elements,
+                                                            ii, FALSE,
+                                                            check_has_attr_list_cb,
+                                                            &data);
+
+                               if (data.condition) {
+                                       context->status = MAX (
+                                               context->status,
+                                               PREFLIGHT_NOT_SUMMARIZED);
+                               }
+                               break;
+
+                       default:
+                               g_warn_if_reached ();
+                       }
+
+                       continue;
+               }
+
+               test = (QueryFieldTest *) elements[ii];
+               field_test = (EBookQueryTest) test->query;
+
+               if (!test->field)
+                       test->field = summary_field_get (book_cache, test->field_id);
+
+               /* Even if the field is not in the summary, we need to
+                * retport unsupported errors if phone number queries are
+                * issued while libphonenumber is unavailable
+                */
+               if (!test->field) {
+                       /* Special case for e_book_query_any_field_contains().
+                        *
+                        * We interpret 'x-evolution-any-field' as E_CONTACT_FIELD_LAST
+                        */
+                       if (test->field_id == E_CONTACT_FIELD_LAST) {
+                               /* If we search for a NULL or zero length string, it
+                                * means 'get all contacts', that is considered a summary
+                                * query but is handled differently (i.e. we just drop the
+                                * field tests and run a regular query).
+                                *
+                                * This is only true if the 'any field contains' query is
+                                * the only test in the constraints, however.
+                                */
+                               if (n_elements == 1 && (!test->value || !test->value[0])) {
+
+                                       context->status = MAX (context->status, PREFLIGHT_LIST_ALL);
+                               } else {
+
+                                       /* Searching for a value with 'x-evolution-any-field' is
+                                        * not a summary query.
+                                        */
+                                       context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+                               }
+                       } else {
+                               /* Couldnt resolve the field, it's not a summary query */
+                               context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+                       }
+               }
+
+               if (test->field && test->field->type == E_TYPE_CONTACT_CERT) {
+                       /* For certificates, and later potentially other fields,
+                        * the only information in the summary is the fact that
+                        * they exist, or not. So the only check we can do from
+                        * the summary is BOOK_QUERY_EXISTS. */
+                       if (field_test != BOOK_QUERY_EXISTS) {
+                               context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+                       }
+                       /* Bypass the other checks below which are not appropriate. */
+                       continue;
+               }
+
+               switch (field_test) {
+               case E_BOOK_QUERY_IS:
+                       break;
+
+               case BOOK_QUERY_EXISTS:
+               case E_BOOK_QUERY_CONTAINS:
+               case E_BOOK_QUERY_BEGINS_WITH:
+               case E_BOOK_QUERY_ENDS_WITH:
+               case E_BOOK_QUERY_REGEX_NORMAL:
+                       /* All of these queries can only apply to string fields,
+                        * or fields which hold multiple strings
+                        */
+                       if (test->field) {
+                               if (test->field->type != G_TYPE_STRING &&
+                                   test->field->type != E_TYPE_CONTACT_ATTR_LIST) {
+                                       context->status = MAX (context->status, PREFLIGHT_INVALID);
+                               }
+                       }
+
+                       break;
+
+               case BOOK_QUERY_EXISTS_VCARD:
+                       /* Exists vCard queries only supported in the fallback */
+                       context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+                       break;
+
+               case E_BOOK_QUERY_REGEX_RAW:
+                       /* Raw regex queries only supported in the fallback */
+                       context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+                       break;
+
+               case E_BOOK_QUERY_EQUALS_PHONE_NUMBER:
+               case E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER:
+               case E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER:
+                       /* Phone number queries are supported so long as they are in the summary,
+                        * libphonenumber is available, and the phone number string is a valid one
+                        */
+                       if (!e_phone_number_is_supported ()) {
+                               context->status = MAX (context->status, PREFLIGHT_UNSUPPORTED);
+                       } else {
+                               QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+                               EPhoneNumberCountrySource source;
+                               EPhoneNumber *number;
+                               const gchar *region_code;
+
+                               if (phone_test->region)
+                                       region_code = phone_test->region;
+                               else
+                                       region_code = book_cache->priv->region_code;
+
+                               number = e_phone_number_from_string (
+                                       phone_test->value,
+                                       region_code, NULL);
+
+                               if (number == NULL) {
+                                       context->status = MAX (context->status, PREFLIGHT_INVALID);
+                               } else {
+                                       /* Collect values we'll need later while generating field
+                                        * tests, no need to parse the phone number more than once
+                                        */
+                                       phone_test->national = e_phone_number_get_national_number (number);
+                                       phone_test->country = e_phone_number_get_country_code (number, 
&source);
+                                       phone_test->national = remove_leading_zeros (phone_test->national);
+
+                                       if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+                                               phone_test->country = 0;
+
+                                       e_phone_number_free (number);
+                               }
+                       }
+                       break;
+               }
+
+               if (test->field &&
+                   test->field->type == E_TYPE_CONTACT_ATTR_LIST) {
+                       gint aux_index = summary_field_get_index (book_cache, test->field_id);
+
+                       /* It's really improbable that we ever get 64 fields in the summary
+                        * In any case we warn about this in e_book_sqlite_new_full().
+                        */
+                       g_warn_if_fail (aux_index >= 0 && aux_index < EBC_MAX_SUMMARY_FIELDS);
+                       context->aux_mask |= (1 << aux_index);
+
+                       /* If this condition is a *requirement* for the overall query to
+                          match a given record (i.e. there's no surrounding 'OR' but
+                          only 'AND'), then we can use an inner join for the query and
+                          it will be a lot more efficient. If records without this
+                          condition can also match the overall condition, then we must
+                          use LEFT JOIN. */
+                       if (sub_query_context_peek_cond_counter (ctx)) {
+                               context->left_join_mask |= (1 << aux_index);
+                       }
+               }
+       }
+
+       sub_query_context_free (ctx);
+}
+
+/* Handle special case of E_CONTACT_FULL_NAME
+ *
+ * For any query which accesses the full name field,
+ * we need to also OR it with any of the related name
+ * fields, IF those are found in the summary as well.
+ */
+static void
+query_preflight_substitute_full_name (PreflightContext *context,
+                                     EBookCache *book_cache)
+{
+       gint ii, jj;
+
+       for (ii = 0; context->constraints != NULL && ii < context->constraints->len; ii++) {
+               SummaryField *family_name, *given_name, *nickname;
+               QueryElement *element;
+               QueryFieldTest *test;
+
+               element = g_ptr_array_index (context->constraints, ii);
+
+               if (element->query >= BOOK_QUERY_SUB_FIRST)
+                       continue;
+
+               test = (QueryFieldTest *) element;
+               if (test->field_id != E_CONTACT_FULL_NAME)
+                       continue;
+
+               family_name = summary_field_get (book_cache, E_CONTACT_FAMILY_NAME);
+               given_name = summary_field_get (book_cache, E_CONTACT_GIVEN_NAME);
+               nickname = summary_field_get (book_cache, E_CONTACT_NICKNAME);
+
+               /* If any of these are in the summary, then we'll construct
+                * a grouped OR statment for this E_CONTACT_FULL_NAME test */
+               if (family_name || given_name || nickname) {
+                       /* Add the OR directly before the E_CONTACT_FULL_NAME test */
+                       constraints_insert_delimiter (context->constraints, ii, BOOK_QUERY_SUB_OR);
+
+                       jj = ii + 2;
+
+                       if (family_name)
+                               constraints_insert_field_test (
+                                       context->constraints, jj++,
+                                       family_name, test->query,
+                                       test->value);
+
+                       if (given_name)
+                               constraints_insert_field_test (
+                                       context->constraints, jj++,
+                                       given_name, test->query,
+                                       test->value);
+
+                       if (nickname)
+                               constraints_insert_field_test (
+                                       context->constraints, jj++,
+                                       nickname, test->query,
+                                       test->value);
+
+                       constraints_insert_delimiter (context->constraints, jj, BOOK_QUERY_SUB_END);
+
+                       ii = jj;
+               }
+       }
+}
+
+static void
+query_preflight (PreflightContext *context,
+                EBookCache *book_cache,
+                const gchar *sexp)
+{
+       query_preflight_initialize (context, sexp);
+
+       if (context->status == PREFLIGHT_OK) {
+               query_preflight_check (context, book_cache);
+
+               /* No need to change the constraints if we're not
+                * going to generate statements with it
+                */
+               if (context->status == PREFLIGHT_OK) {
+                       /* Handle E_CONTACT_FULL_NAME substitutions */
+                       query_preflight_substitute_full_name (context, book_cache);
+               } else {
+                       /* We might use this context to perform a fallback query,
+                        * so let's clear out all the constraints now
+                        */
+                       preflight_context_clear (context);
+               }
+       }
+}
+
+/**********************************************************
+ *                 Field Test Generators                  *
+ **********************************************************
+ *
+ * This section contains the field test generators for
+ * various EBookQueryTest types. When implementing new
+ * query types, a new GenerateFieldTest needs to be created
+ * and added to the table below.
+ */
+
+typedef void (* GenerateFieldTest) (EBookCache *book_cache,
+                                   GString *string,
+                                   QueryFieldTest *test);
+
+/* Appends an identifier suitable to identify the
+ * column to test in the context of a query.
+ *
+ * The suffix is for special indexed columns (such as
+ * reverse values, sort keys, phone numbers, etc).
+ */
+static void
+ebc_string_append_column (GString *string,
+                         SummaryField *field,
+                         const gchar *suffix)
+{
+       if (field->aux_table) {
+               g_string_append (string, field->aux_table_symbolic);
+               g_string_append (string, ".value");
+       } else {
+               g_string_append (string, "summary.");
+               g_string_append (string, field->dbname);
+       }
+
+       if (suffix) {
+               g_string_append_c (string, '_');
+               g_string_append (string, suffix);
+       }
+}
+
+/* This function escapes characters which need escaping
+ * for LIKE statements as well as the single quotes.
+ *
+ * The return value is not suitable to be formatted
+ * with %Q or %q
+ */
+static gchar *
+ebc_normalize_for_like (QueryFieldTest *test,
+                       gboolean reverse_string,
+                       gboolean *escape_needed)
+{
+       GString *str;
+       size_t len;
+       gchar cc;
+       gboolean escape_modifier_needed = FALSE;
+       const gchar *normal = NULL;
+       const gchar *ptr;
+       const gchar *str_to_escape;
+       gchar *reverse = NULL;
+       gchar *freeme = NULL;
+
+       if (test->field_id == E_CONTACT_UID ||
+           test->field_id == E_CONTACT_REV) {
+               normal = test->value;
+       } else {
+               freeme = e_util_utf8_normalize (test->value);
+               normal = freeme;
+       }
+
+       if (reverse_string) {
+               reverse = g_utf8_strreverse (normal, -1);
+               str_to_escape = reverse;
+       } else
+               str_to_escape = normal;
+
+       /* Just assume each character must be escaped. The result of this function
+        * is discarded shortly after calling this function. Therefore it's
+        * acceptable to possibly allocate twice the memory needed.
+        */
+       len = strlen (str_to_escape);
+       str = g_string_sized_new (2 * len + 4 + strlen (EBC_ESCAPE_SEQUENCE) - 1);
+
+       ptr = str_to_escape;
+       while ((cc = *ptr++)) {
+               if (cc == '\'') {
+                       g_string_append_c (str, '\'');
+               } else if (cc == '%' || cc == '_' || cc == '^') {
+                       g_string_append_c (str, '^');
+                       escape_modifier_needed = TRUE;
+               }
+
+               g_string_append_c (str, cc);
+       }
+
+       if (escape_needed)
+               *escape_needed = escape_modifier_needed;
+
+       g_free (freeme);
+       g_free (reverse);
+
+       return g_string_free (str, FALSE);
+}
+
+static void
+field_test_query_is (EBookCache *book_cache,
+                    GString *string,
+                    QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+       gchar *normal;
+
+       ebc_string_append_column (string, field, NULL);
+
+       if (test->field_id == E_CONTACT_UID ||
+           test->field_id == E_CONTACT_REV) {
+               /* UID & REV fields are not normalized in the summary */
+               e_cache_sqlite_stmt_append_printf (string, " = %Q", test->value);
+       } else {
+               normal = e_util_utf8_normalize (test->value);
+               e_cache_sqlite_stmt_append_printf (string, " = %Q", normal);
+               g_free (normal);
+       }
+}
+
+static void
+field_test_query_contains (EBookCache *book_cache,
+                          GString *string,
+                          QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+       gboolean need_escape;
+       gchar *escaped;
+
+       escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+
+       g_string_append_c (string, '(');
+
+       ebc_string_append_column (string, field, NULL);
+       g_string_append (string, " IS NOT NULL AND ");
+       ebc_string_append_column (string, field, NULL);
+       g_string_append (string, " LIKE '%");
+       g_string_append (string, escaped);
+       g_string_append (string, "%'");
+
+       if (need_escape)
+               g_string_append (string, EBC_ESCAPE_SEQUENCE);
+
+       g_string_append_c (string, ')');
+
+       g_free (escaped);
+}
+
+static void
+field_test_query_begins_with (EBookCache *book_cache,
+                             GString *string,
+                             QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+       gboolean need_escape;
+       gchar *escaped;
+
+       escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+
+       g_string_append_c (string, '(');
+       ebc_string_append_column (string, field, NULL);
+       g_string_append (string, " IS NOT NULL AND ");
+
+       ebc_string_append_column (string, field, NULL);
+       g_string_append (string, " LIKE \'");
+       g_string_append (string, escaped);
+       g_string_append (string, "%\'");
+
+       if (need_escape)
+               g_string_append (string, EBC_ESCAPE_SEQUENCE);
+       g_string_append_c (string, ')');
+
+       g_free (escaped);
+}
+
+static void
+field_test_query_ends_with (EBookCache *book_cache,
+                           GString *string,
+                           QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+       gboolean need_escape;
+       gchar *escaped;
+
+       if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+               escaped = ebc_normalize_for_like (test, TRUE, &need_escape);
+
+               g_string_append_c (string, '(');
+               ebc_string_append_column (string, field, EBC_SUFFIX_REVERSE);
+               g_string_append (string, " IS NOT NULL AND ");
+
+               ebc_string_append_column (string, field, EBC_SUFFIX_REVERSE);
+               g_string_append (string, " LIKE \'");
+               g_string_append (string, escaped);
+               g_string_append (string, "%\'");
+       } else {
+               escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+               g_string_append_c (string, '(');
+
+               ebc_string_append_column (string, field, NULL);
+               g_string_append (string, " IS NOT NULL AND ");
+
+               ebc_string_append_column (string, field, NULL);
+               g_string_append (string, " LIKE \'%");
+               g_string_append (string, escaped);
+               g_string_append (string, "\'");
+       }
+
+       if (need_escape)
+               g_string_append (string, EBC_ESCAPE_SEQUENCE);
+
+       g_string_append_c (string, ')');
+       g_free (escaped);
+}
+
+static void
+field_test_query_eqphone (EBookCache *book_cache,
+                         GString *string,
+                         QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+       QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+       if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+               g_string_append_c (string, '(');
+               ebc_string_append_column (string, field, EBC_SUFFIX_PHONE);
+               e_cache_sqlite_stmt_append_printf (string, " = %Q AND ", phone_test->national);
+
+               /* For exact matches, a country code qualifier is required by both
+                * query input and row input
+                */
+               ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+               g_string_append (string, " != 0 AND ");
+
+               ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+               e_cache_sqlite_stmt_append_printf (string, " = %d", phone_test->country);
+               g_string_append_c (string, ')');
+       } else {
+               /* No indexed columns available, perform the fallback */
+               g_string_append (string, EBC_FUNC_EQPHONE_EXACT " (");
+               ebc_string_append_column (string, field, NULL);
+               e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+       }
+}
+
+static void
+field_test_query_eqphone_national (EBookCache *book_cache,
+                                  GString *string,
+                                  QueryFieldTest *test)
+{
+
+       SummaryField *field = test->field;
+       QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+       if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+               /* Only a compound expression if there is a country code */
+               if (phone_test->country)
+                       g_string_append_c (string, '(');
+
+               /* Generate: phone = %Q */
+               ebc_string_append_column (string, field, EBC_SUFFIX_PHONE);
+               e_cache_sqlite_stmt_append_printf (string, " = %Q", phone_test->national);
+
+               /* When doing a national search, no need to check country
+                * code unless the query number also has a country code
+                */
+               if (phone_test->country) {
+                       /* Generate: (phone = %Q AND (country = 0 OR country = %d)) */
+                       g_string_append (string, " AND (");
+                       ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+                       g_string_append (string, " = 0 OR ");
+                       ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+                       e_cache_sqlite_stmt_append_printf (string, " = %d))", phone_test->country);
+               }
+       } else {
+               /* No indexed columns available, perform the fallback */
+               g_string_append (string, EBC_FUNC_EQPHONE_NATIONAL " (");
+               ebc_string_append_column (string, field, NULL);
+               e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+       }
+}
+
+static void
+field_test_query_eqphone_short (EBookCache *book_cache,
+                               GString *string,
+                               QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+
+       /* No quick way to do the short match */
+       g_string_append (string, EBC_FUNC_EQPHONE_SHORT " (");
+       ebc_string_append_column (string, field, NULL);
+       e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+}
+
+static void
+field_test_query_regex_normal (EBookCache *book_cache,
+                              GString *string,
+                              QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+       gchar *normal;
+
+       normal = e_util_utf8_normalize (test->value);
+
+       if (field->aux_table) {
+               e_cache_sqlite_stmt_append_printf (
+                       string, "%s.value REGEXP %Q",
+                       field->aux_table_symbolic,
+                       normal);
+       } else {
+               e_cache_sqlite_stmt_append_printf (
+                       string, "summary.%s REGEXP %Q",
+                       field->dbname,
+                       normal);
+       }
+
+       g_free (normal);
+}
+
+static void
+field_test_query_exists (EBookCache *book_cache,
+                        GString *string,
+                        QueryFieldTest *test)
+{
+       SummaryField *field = test->field;
+
+       ebc_string_append_column (string, field, NULL);
+
+       if (test->field->type == E_TYPE_CONTACT_CERT)
+               e_cache_sqlite_stmt_append_printf (string, " IS NOT '0'");
+       else
+               e_cache_sqlite_stmt_append_printf (string, " IS NOT NULL");
+}
+
+/* Lookup table for field test generators per EBookQueryTest,
+ *
+ * WARNING: This must stay in line with the EBookQueryTest definition.
+ */
+static const GenerateFieldTest field_test_func_table[] = {
+       field_test_query_is,               /* E_BOOK_QUERY_IS */
+       field_test_query_contains,         /* E_BOOK_QUERY_CONTAINS */
+       field_test_query_begins_with,      /* E_BOOK_QUERY_BEGINS_WITH */
+       field_test_query_ends_with,        /* E_BOOK_QUERY_ENDS_WITH */
+       field_test_query_eqphone,          /* E_BOOK_QUERY_EQUALS_PHONE_NUMBER */
+       field_test_query_eqphone_national, /* E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER */
+       field_test_query_eqphone_short,    /* E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER */
+       field_test_query_regex_normal,     /* E_BOOK_QUERY_REGEX_NORMAL */
+       NULL /* Requires fallback */,      /* E_BOOK_QUERY_REGEX_RAW  */
+       field_test_query_exists,           /* BOOK_QUERY_EXISTS */
+       NULL /* Requires fallback */       /* BOOK_QUERY_EXISTS_VCARD */
+};
+
+/**********************************************************
+ *                   Querying Contacts                    *
+ **********************************************************/
+
+/* The various search types indicate what should be fetched
+ */
+typedef enum {
+       SEARCH_FULL,          /* Get a list of EBookCacheSearchData*/
+       SEARCH_UID_AND_REV,   /* Get a list of EBookCacheSearchData, with shallow vcards only containing UID 
& REV */
+       SEARCH_UID,           /* Get a list of UID strings */
+       SEARCH_COUNT,         /* Get the number of matching rows */
+} SearchType;
+
+static void
+ebc_generate_constraints (EBookCache *book_cache,
+                         GString *string,
+                         GPtrArray *constraints,
+                         const gchar *sexp)
+{
+       SubQueryContext *ctx;
+       QueryDelimiter *delim;
+       QueryFieldTest *test;
+       QueryElement **elements;
+       gint n_elements, ii;
+
+       /* If there are no constraints, we generate the fallback constraint for 'sexp' */
+       if (constraints == NULL) {
+               e_cache_sqlite_stmt_append_printf (
+                       string,
+                       EBC_FUNC_COMPARE_VCARD " (%Q,summary." E_CACHE_COLUMN_OBJECT ")",
+                       sexp);
+               return;
+       }
+
+       elements = (QueryElement **) constraints->pdata;
+       n_elements = constraints->len;
+
+       ctx = sub_query_context_new ();
+
+       for (ii = 0; ii < n_elements; ii++) {
+               GenerateFieldTest generate_test_func = NULL;
+
+               /* Seperate field tests with the appropriate grouping */
+               if (elements[ii]->query != BOOK_QUERY_SUB_END &&
+                   sub_query_context_increment (ctx) > 0) {
+                       guint delim_type = sub_query_context_peek_type (ctx);
+
+                       switch (delim_type) {
+                       case BOOK_QUERY_SUB_AND:
+                               g_string_append (string, " AND ");
+                               break;
+
+                       case BOOK_QUERY_SUB_OR:
+                               g_string_append (string, " OR ");
+                               break;
+
+                       case BOOK_QUERY_SUB_NOT:
+                               /* Nothing to do between children of NOT,
+                                * there should only ever be one child of NOT anyway
+                                */
+                               break;
+
+                       case BOOK_QUERY_SUB_END:
+                       default:
+                               g_warn_if_reached ();
+                       }
+               }
+
+               if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+                       delim = (QueryDelimiter *) elements[ii];
+
+                       switch (delim->query) {
+                       case BOOK_QUERY_SUB_NOT:
+                               /* NOT is a unary operator and as such
+                                * comes before the opening parenthesis
+                                */
+                               g_string_append (string, "NOT ");
+
+                               /* Fall through */
+
+                       case BOOK_QUERY_SUB_AND:
+                       case BOOK_QUERY_SUB_OR:
+                               /* Open a grouped statement and push the context */
+                               sub_query_context_push (ctx, delim->query, FALSE);
+                               g_string_append_c (string, '(');
+                               break;
+
+                       case BOOK_QUERY_SUB_END:
+                               /* Close a grouped statement and pop the context */
+                               g_string_append_c (string, ')');
+                               sub_query_context_pop (ctx);
+                               break;
+                       default:
+                               g_warn_if_reached ();
+                       }
+
+                       continue;
+               }
+
+               /* Find the appropriate field test generator */
+               test = (QueryFieldTest *) elements[ii];
+               if (test->query < G_N_ELEMENTS (field_test_func_table))
+                       generate_test_func = field_test_func_table[test->query];
+
+               /* These should never happen, if it does it should be
+                * fixed in the preflight checks
+                */
+               g_warn_if_fail (generate_test_func != NULL);
+               g_warn_if_fail (test->field != NULL);
+
+               /* Generate the field test */
+               /* coverity[var_deref_op] */
+               generate_test_func (book_cache, string, test);
+       }
+
+       sub_query_context_free (ctx);
+}
+
+static void
+ebc_search_meta_contacts_cb (ECache *cache,
+                            const gchar *uid,
+                            const gchar *revision,
+                            const gchar *object,
+                            const gchar *extra,
+                            gpointer out_value)
+{
+       GSList **out_list = out_value;
+       EBookCacheSearchData *sd;
+       EContact *contact;
+       gchar *vcard;
+
+       g_return_if_fail (out_list != NULL);
+
+       contact = e_contact_new ();
+
+       e_contact_set (contact, E_CONTACT_UID, uid);
+       if (revision)
+               e_contact_set (contact, E_CONTACT_REV, revision);
+
+       vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+       g_object_unref (contact);
+
+       sd = e_book_cache_search_data_new (uid, vcard, extra);
+
+       *out_list = g_slist_prepend (*out_list, sd);
+
+       g_free (vcard);
+}
+
+static void
+ebc_search_full_contacts_cb (ECache *cache,
+                            const gchar *uid,
+                            const gchar *revision,
+                            const gchar *object,
+                            const gchar *extra,
+                            gpointer out_value)
+{
+       GSList **out_list = out_value;
+       EBookCacheSearchData *sd;
+
+       g_return_if_fail (out_list != NULL);
+
+       sd = e_book_cache_search_data_new (uid, object, extra);
+
+       *out_list = g_slist_prepend (*out_list, sd);
+}
+
+static void
+ebc_search_uids_cb (ECache *cache,
+                   const gchar *uid,
+                   const gchar *revision,
+                   const gchar *object,
+                   const gchar *extra,
+                   gpointer out_value)
+{
+       GSList **out_list = out_value;
+
+       g_return_if_fail (out_list != NULL);
+
+       *out_list = g_slist_prepend (*out_list, g_strdup (uid));
+}
+
+typedef void (* EBookCacheInternalSearchFunc)  (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const gchar *extra,
+                                                gpointer out_value);
+
+/* Generates the SELECT portion of the query, this will take care of
+ * preparing the context of the query, and add the needed JOIN statements
+ * based on which fields are referenced in the query expression.
+ *
+ * This also handles getting the correct callback and asking for the
+ * right data depending on the 'search_type'
+ */
+static EBookCacheInternalSearchFunc
+ebc_generate_select (EBookCache *book_cache,
+                    GString *string,
+                    SearchType search_type,
+                    PreflightContext *context,
+                    GError **error)
+{
+       EBookCacheInternalSearchFunc callback = NULL;
+       gboolean add_auxiliary_tables = FALSE;
+       gint ii;
+
+       if (context->status == PREFLIGHT_OK &&
+           context->aux_mask != 0)
+               add_auxiliary_tables = TRUE;
+
+       g_string_append (string, "SELECT ");
+       if (add_auxiliary_tables)
+               g_string_append (string, "DISTINCT ");
+
+       switch (search_type) {
+       case SEARCH_FULL:
+               callback = ebc_search_full_contacts_cb;
+               g_string_append (string, "summary." E_CACHE_COLUMN_UID ",");
+               g_string_append (string, "summary." E_CACHE_COLUMN_REVISION ",");
+               g_string_append (string, "summary." E_CACHE_COLUMN_OBJECT ",");
+               g_string_append (string, "summary." E_CACHE_COLUMN_STATE ",");
+               g_string_append (string, "summary." EBC_COLUMN_EXTRA " ");
+               break;
+       case SEARCH_UID_AND_REV:
+               callback = ebc_search_meta_contacts_cb;
+               g_string_append (string, "summary." E_CACHE_COLUMN_UID ", summary." E_CACHE_COLUMN_REVISION 
", summary." EBC_COLUMN_EXTRA " ");
+               break;
+       case SEARCH_UID:
+               callback = ebc_search_uids_cb;
+               g_string_append (string, "summary." E_CACHE_COLUMN_UID ",");
+               g_string_append (string, "summary." E_CACHE_COLUMN_REVISION " ");
+               break;
+       case SEARCH_COUNT:
+               if (context->aux_mask != 0)
+                       g_string_append (string, "count (DISTINCT summary." E_CACHE_COLUMN_UID ") ");
+               else
+                       g_string_append (string, "count (*) ");
+               break;
+       }
+
+       e_cache_sqlite_stmt_append_printf (string, "FROM %Q AS summary", E_CACHE_TABLE_OBJECTS);
+
+       /* Add any required auxiliary tables into the query context */
+       if (add_auxiliary_tables) {
+               for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+
+                       /* We cap this at EBC_MAX_SUMMARY_FIELDS (64 bits) at creation time */
+                       if ((context->aux_mask & (1 << ii)) != 0) {
+                               SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+                               gboolean left_join = (context->left_join_mask >> ii) & 1;
+
+                               /* Note the '+' in the JOIN statement.
+                                *
+                                * This plus makes the uid's index ineligable to participate
+                                * in any indexing.
+                                *
+                                * Without this, the indexes which we prefer for prefix or
+                                * suffix matching in the auxiliary tables are ignored and
+                                * only considered on exact matches.
+                                *
+                                * This is crucial to ensure that the uid index does not
+                                * compete with the value index in constraints such as:
+                                *
+                                *     WHERE email_list.value LIKE "boogieman%"
+                                */
+                               e_cache_sqlite_stmt_append_printf (
+                                       string, " %sJOIN %Q AS %s ON %s%s.uid = summary." E_CACHE_COLUMN_UID,
+                                       left_join ? "LEFT " : "",
+                                       field->aux_table,
+                                       field->aux_table_symbolic,
+                                       left_join ? "" : "+",
+                                       field->aux_table_symbolic);
+                       }
+               }
+       }
+
+       return callback;
+}
+
+static gboolean
+ebc_is_autocomplete_query (PreflightContext *context)
+{
+       QueryFieldTest *test;
+       QueryElement **elements;
+       gint n_elements, ii;
+       int non_aux_fields = 0;
+
+       if (context->status != PREFLIGHT_OK || context->aux_mask == 0)
+               return FALSE;
+
+       elements = (QueryElement **) context->constraints->pdata;
+       n_elements = context->constraints->len;
+
+       for (ii = 0; ii < n_elements; ii++) {
+               test = (QueryFieldTest *) elements[ii];
+
+               /* For these, check if the field being operated on is
+                  an auxiliary field or not. */
+               if (elements[ii]->query == E_BOOK_QUERY_BEGINS_WITH ||
+                   elements[ii]->query == E_BOOK_QUERY_ENDS_WITH ||
+                   elements[ii]->query == E_BOOK_QUERY_IS ||
+                   elements[ii]->query == BOOK_QUERY_EXISTS ||
+                   elements[ii]->query == E_BOOK_QUERY_CONTAINS) {
+                       if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+                               non_aux_fields++;
+                       continue;
+               }
+
+               /* Nothing else is allowed other than "(or" ... ")" */
+               if (elements[ii]->query != BOOK_QUERY_SUB_OR &&
+                   elements[ii]->query != BOOK_QUERY_SUB_END)
+                       return FALSE;
+       }
+
+       /* If there were no non-aux fields being queried, don't bother */
+       return non_aux_fields != 0;
+}
+
+static EBookCacheInternalSearchFunc
+ebc_generate_autocomplete_query (EBookCache *book_cache,
+                                GString *string,
+                                SearchType search_type,
+                                PreflightContext *context,
+                                GError **error)
+{
+       QueryElement **elements;
+       gint n_elements, ii;
+       guint64 aux_mask = context->aux_mask;
+       guint64 left_join_mask = context->left_join_mask;
+       EBookCacheInternalSearchFunc callback;
+       gboolean first = TRUE;
+
+       elements = (QueryElement **) context->constraints->pdata;
+       n_elements = context->constraints->len;
+
+       /* First the queries which use aux tables. */
+       for (ii = 0; ii < n_elements; ii++) {
+               GenerateFieldTest generate_test_func = NULL;
+               QueryFieldTest *test;
+               gint aux_index;
+
+               if (elements[ii]->query == BOOK_QUERY_SUB_OR ||
+                   elements[ii]->query == BOOK_QUERY_SUB_END)
+                       continue;
+
+               test = (QueryFieldTest *) elements[ii];
+               if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               aux_index = summary_field_get_index (book_cache, test->field_id);
+               g_warn_if_fail (aux_index >= 0 && aux_index < EBC_MAX_SUMMARY_FIELDS);
+               context->aux_mask = (1 << aux_index);
+               context->left_join_mask = 0;
+
+               callback = ebc_generate_select (book_cache, string, search_type, context, error);
+               e_cache_sqlite_stmt_append_printf (string, " WHERE summary." E_CACHE_COLUMN_STATE "!=%d AND 
(", E_OFFLINE_STATE_LOCALLY_DELETED);
+               context->aux_mask = aux_mask;
+               context->left_join_mask = left_join_mask;
+               if (!callback)
+                       return NULL;
+
+               generate_test_func = field_test_func_table[test->query];
+               generate_test_func (book_cache, string, test);
+
+               g_string_append (string, ") UNION ");
+       }
+
+       /* Finally, generate the SELECT for the primary fields. */
+       context->aux_mask = 0;
+       callback = ebc_generate_select (book_cache, string, search_type, context, error);
+       context->aux_mask = aux_mask;
+       if (!callback)
+               return NULL;
+
+       e_cache_sqlite_stmt_append_printf (string, " WHERE summary." E_CACHE_COLUMN_STATE "!=%d AND (", 
E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       for (ii = 0; ii < n_elements; ii++) {
+               GenerateFieldTest generate_test_func = NULL;
+               QueryFieldTest *test;
+
+               if (elements[ii]->query == BOOK_QUERY_SUB_OR ||
+                   elements[ii]->query == BOOK_QUERY_SUB_END)
+                       continue;
+
+               test = (QueryFieldTest *) elements[ii];
+               if (test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+                       continue;
+
+               if (!first)
+                       g_string_append (string, " OR ");
+               else
+                       first = FALSE;
+
+               generate_test_func = field_test_func_table[test->query];
+               generate_test_func (book_cache, string, test);
+       }
+
+       g_string_append (string, ")");
+
+       return callback;
+}
+
+struct EBCSearchData {
+       gint uid_index;
+       gint revision_index;
+       gint object_index;
+       gint extra_index;
+       gint state_index;
+
+       EBookCacheInternalSearchFunc func;
+       gpointer out_value;
+
+       EBookCacheSearchFunc user_func;
+       gpointer user_func_user_data;
+};
+
+static gboolean
+ebc_search_select_cb (ECache *cache,
+                     gint ncols,
+                     const gchar *column_names[],
+                     const gchar *column_values[],
+                     gpointer user_data)
+{
+       struct EBCSearchData *sd = user_data;
+       const gchar *object = NULL, *extra = NULL;
+       EOfflineState offline_state = E_OFFLINE_STATE_UNKNOWN;
+
+       g_return_val_if_fail (sd != NULL, FALSE);
+       g_return_val_if_fail (sd->func != NULL || sd->user_func != NULL, FALSE);
+       g_return_val_if_fail (sd->out_value != NULL || sd->user_func != NULL, FALSE);
+
+       if (sd->uid_index == -1 ||
+           sd->revision_index == -1 ||
+           sd->object_index == -1 ||
+           sd->extra_index == -1 ||
+           sd->state_index == -1) {
+               gint ii;
+
+               for (ii = 0; ii < ncols && (sd->uid_index == -1 ||
+                    sd->revision_index == -1 ||
+                    sd->object_index == -1 ||
+                    sd->extra_index == -1 ||
+                    sd->state_index == -1); ii++) {
+                       const gchar *cname = column_names[ii];
+
+                       if (!cname)
+                               continue;
+
+                       if (g_str_has_prefix (cname, "summary."))
+                               cname += 8;
+
+                       if (sd->uid_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_UID) == 0) {
+                               sd->uid_index = ii;
+                       } else if (sd->revision_index == -1 && g_ascii_strcasecmp (cname, 
E_CACHE_COLUMN_REVISION) == 0) {
+                               sd->revision_index = ii;
+                       } else if (sd->object_index == -1 && g_ascii_strcasecmp (cname, 
E_CACHE_COLUMN_OBJECT) == 0) {
+                               sd->object_index = ii;
+                       } else if (sd->extra_index == -1 && g_ascii_strcasecmp (cname, EBC_COLUMN_EXTRA) == 
0) {
+                               sd->extra_index = ii;
+                       } else if (sd->state_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_STATE) 
== 0) {
+                               sd->state_index = ii;
+                       }
+               }
+       }
+
+       g_return_val_if_fail (sd->uid_index >= 0 && sd->uid_index < ncols, FALSE);
+       g_return_val_if_fail (sd->revision_index >= 0 && sd->revision_index < ncols, FALSE);
+
+       if (sd->object_index != -2) {
+               g_return_val_if_fail (sd->object_index >= 0 && sd->object_index < ncols, FALSE);
+               object = column_values[sd->object_index];
+       }
+
+       if (sd->extra_index != -2) {
+               g_return_val_if_fail (sd->extra_index >= 0 && sd->extra_index < ncols, FALSE);
+               extra = column_values[sd->extra_index];
+       }
+
+       if (sd->state_index != -2) {
+               g_return_val_if_fail (sd->extra_index >= 0 && sd->extra_index < ncols, FALSE);
+
+               if (!column_values[sd->state_index])
+                       offline_state = E_OFFLINE_STATE_UNKNOWN;
+               else
+                       offline_state = g_ascii_strtoull (column_values[sd->state_index], NULL, 10);
+       }
+
+       if (sd->user_func) {
+               return sd->user_func (E_BOOK_CACHE (cache), column_values[sd->uid_index], 
column_values[sd->revision_index],
+                       object, extra, offline_state, sd->user_func_user_data);
+       }
+
+       sd->func (cache, column_values[sd->uid_index], column_values[sd->revision_index], object, extra, 
sd->out_value);
+
+       return TRUE;
+}
+
+static gboolean
+ebc_do_search_query (EBookCache *book_cache,
+                    PreflightContext *context,
+                    const gchar *sexp,
+                    SearchType search_type,
+                    gpointer out_value,
+                    EBookCacheSearchFunc func,
+                    gpointer func_user_data,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       struct EBCSearchData sd;
+       GString *stmt;
+       gboolean success = FALSE;
+
+       /* We might calculate a reasonable estimation of bytes
+        * during the preflight checks */
+       stmt = g_string_sized_new (GENERATED_QUERY_BYTES);
+
+       /* Extra special case. For the common case of the email composer's
+          addressbook autocompletion, we really want the most optimal query.
+          So check for it and use a basically hand-crafted one. */
+        if (ebc_is_autocomplete_query (context)) {
+               sd.func = ebc_generate_autocomplete_query (book_cache, stmt, search_type, context, error);
+       } else {
+               /* Generate the leading SELECT statement */
+               sd.func = ebc_generate_select (book_cache, stmt, search_type, context, error);
+
+               if (sd.func) {
+                       e_cache_sqlite_stmt_append_printf (stmt,
+                               " WHERE summary." E_CACHE_COLUMN_STATE "!=%d",
+                               E_OFFLINE_STATE_LOCALLY_DELETED);
+
+                       if (EBC_STATUS_GEN_CONSTRAINTS (context->status)) {
+                               GString *where_clause = g_string_new ("");
+
+                               /*
+                                * Now generate the search expression on the main contacts table
+                                */
+                               ebc_generate_constraints (book_cache, where_clause, context->constraints, 
sexp);
+                               if (where_clause->len)
+                                       e_cache_sqlite_stmt_append_printf (stmt, " AND (%s)", 
where_clause->str);
+                               g_string_free (where_clause, TRUE);
+                       }
+               }
+       }
+
+       if (sd.func) {
+               sd.uid_index = -1;
+               sd.revision_index = -1;
+               sd.object_index = search_type == SEARCH_FULL ? -1 : -2;
+               sd.extra_index = search_type == SEARCH_UID ? -2 : -1;
+               sd.state_index = search_type == SEARCH_FULL ? -1 : -2;
+               sd.out_value = out_value;
+               sd.user_func = func;
+               sd.user_func_user_data = func_user_data;
+
+               success = e_cache_sqlite_select (E_CACHE (book_cache), stmt->str,
+                       ebc_search_select_cb, &sd, cancellable, error);
+       }
+
+       g_string_free (stmt, TRUE);
+
+       return success;
+}
+
+static gboolean
+ebc_search_internal (EBookCache *book_cache,
+                    const gchar *sexp,
+                    SearchType search_type,
+                    gpointer out_value,
+                    EBookCacheSearchFunc func,
+                    gpointer func_user_data,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+       gboolean success = FALSE;
+
+       /* Now start with the query preflighting */
+       query_preflight (&context, book_cache, sexp);
+
+       switch (context.status) {
+       case PREFLIGHT_OK:
+       case PREFLIGHT_LIST_ALL:
+       case PREFLIGHT_NOT_SUMMARIZED:
+               /* No errors, let's really search */
+               success = ebc_do_search_query (
+                       book_cache, &context, sexp,
+                       search_type, out_value, func, func_user_data,
+                       cancellable, error);
+               break;
+
+       case PREFLIGHT_INVALID:
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+                       _("Invalid query: %s"), sexp);
+               break;
+
+       case PREFLIGHT_UNSUPPORTED:
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_QUERY,
+                       _("Query contained unsupported elements"));
+               break;
+       }
+
+       preflight_context_clear (&context);
+
+       return success;
+}
+
+/******************************************************************
+ *                    EBookCacheCursor Implementation                  *
+ ******************************************************************/
+typedef struct _CursorState CursorState;
+
+struct _CursorState {
+       gchar **values;                 /* The current cursor position, results will be returned after this 
position */
+       gchar *last_uid;                /* The current cursor contact UID position, used as a tie breaker */
+       EBookCacheCursorOrigin position;/* The position is updated with the cursor state and is used to 
distinguish
+                                        * between the beginning and the ending of the cursor's contact list.
+                                        * While the cursor is in a non-null state, the position will be
+                                        * E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT.
+                                        */
+};
+
+struct _EBookCacheCursor {
+       EBookBackendSExp *sexp;       /* An EBookBackendSExp based on the query, used by 
e_book_sqlite_cursor_compare () */
+       gchar         *select_vcards; /* The first fragment when querying results */
+       gchar         *select_count;  /* The first fragment when querying contact counts */
+       gchar         *query;         /* The SQL query expression derived from the passed search expression */
+       gchar         *order;         /* The normal order SQL query fragment to append at the end, containing 
ORDER BY etc */
+       gchar         *reverse_order; /* The reverse order SQL query fragment to append at the end, 
containing ORDER BY etc */
+
+       EContactField       *sort_fields;   /* The fields to sort in a query in the order or sort priority */
+       EBookCursorSortType *sort_types;    /* The sort method to use for each field */
+       gint                 n_sort_fields; /* The amound of sort fields */
+
+       CursorState          state;
+};
+
+static CursorState *cursor_state_copy             (EBookCacheCursor     *cursor,
+                                                  CursorState          *state);
+static void         cursor_state_free             (EBookCacheCursor     *cursor,
+                                                  CursorState          *state);
+static void         cursor_state_clear            (EBookCacheCursor     *cursor,
+                                                  CursorState          *state,
+                                                  EBookCacheCursorOrigin position);
+static void         cursor_state_set_from_contact (EBookCache           *book_cache,
+                                                  EBookCacheCursor     *cursor,
+                                                  CursorState          *state,
+                                                  EContact             *contact);
+static void         cursor_state_set_from_vcard   (EBookCache           *book_cache,
+                                                  EBookCacheCursor     *cursor,
+                                                  CursorState          *state,
+                                                  const gchar          *vcard);
+
+static CursorState *
+cursor_state_copy (EBookCacheCursor *cursor,
+                  CursorState *state)
+{
+       CursorState *copy;
+       gint ii;
+
+       copy = g_slice_new0 (CursorState);
+       copy->values = g_new0 (gchar *, cursor->n_sort_fields);
+
+       for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+               copy->values[ii] = g_strdup (state->values[ii]);
+       }
+
+       copy->last_uid = g_strdup (state->last_uid);
+       copy->position = state->position;
+
+       return copy;
+}
+
+static void
+cursor_state_free (EBookCacheCursor *cursor,
+                  CursorState *state)
+{
+       if (state) {
+               cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+               g_free (state->values);
+               g_slice_free (CursorState, state);
+       }
+}
+
+static void
+cursor_state_clear (EBookCacheCursor *cursor,
+                   CursorState *state,
+                   EBookCacheCursorOrigin position)
+{
+       gint ii;
+
+       for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+               g_free (state->values[ii]);
+               state->values[ii] = NULL;
+       }
+
+       g_free (state->last_uid);
+       state->last_uid = NULL;
+       state->position = position;
+}
+
+static void
+cursor_state_set_from_contact (EBookCache *book_cache,
+                              EBookCacheCursor *cursor,
+                              CursorState *state,
+                              EContact *contact)
+{
+       gint ii;
+
+       cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+
+       for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+               const gchar *string = e_contact_get_const (contact, cursor->sort_fields[ii]);
+               SummaryField *field;
+               gchar *sort_key;
+
+               if (string)
+                       sort_key = e_collator_generate_key (book_cache->priv->collator, string, NULL);
+               else
+                       sort_key = g_strdup ("");
+
+               field = summary_field_get (book_cache, cursor->sort_fields[ii]);
+
+               if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+                       state->values[ii] = sort_key;
+               } else {
+                       state->values[ii] = ebc_encode_vcard_sort_key (sort_key);
+                       g_free (sort_key);
+               }
+       }
+
+       state->last_uid = e_contact_get (contact, E_CONTACT_UID);
+       state->position = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+}
+
+static void
+cursor_state_set_from_vcard (EBookCache *book_cache,
+                            EBookCacheCursor *cursor,
+                            CursorState *state,
+                            const gchar *vcard)
+{
+       EContact *contact;
+
+       contact = e_contact_new_from_vcard (vcard);
+       cursor_state_set_from_contact (book_cache, cursor, state, contact);
+       g_object_unref (contact);
+}
+
+static gboolean
+ebc_cursor_setup_query (EBookCache *book_cache,
+                       EBookCacheCursor *cursor,
+                       const gchar *sexp,
+                       GError **error)
+{
+       PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+       GString *string, *where_clause;
+
+       /* Preflighting and error checking */
+       if (sexp) {
+               query_preflight (&context, book_cache, sexp);
+
+               if (context.status > PREFLIGHT_NOT_SUMMARIZED) {
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+                               _("Invalid query for a book cursor"));
+
+                       preflight_context_clear (&context);
+                       return FALSE;
+               }
+       }
+
+       /* Now we caught the errors, let's generate our queries and get out of here ... */
+       g_free (cursor->select_vcards);
+       g_free (cursor->select_count);
+       g_free (cursor->query);
+       g_clear_object (&(cursor->sexp));
+
+       /* Generate the leading SELECT portions that we need */
+       string = g_string_new ("");
+       ebc_generate_select (book_cache, string, SEARCH_FULL, &context, NULL);
+       cursor->select_vcards = g_string_free (string, FALSE);
+
+       string = g_string_new ("");
+       ebc_generate_select (book_cache, string, SEARCH_COUNT, &context, NULL);
+       cursor->select_count = g_string_free (string, FALSE);
+
+       where_clause = g_string_new ("");
+
+       e_cache_sqlite_stmt_append_printf (where_clause, "summary." E_CACHE_COLUMN_STATE "!=%d",
+               E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       if (!sexp || context.status == PREFLIGHT_LIST_ALL) {
+               cursor->sexp = NULL;
+       } else {
+               cursor->sexp = e_book_backend_sexp_new (sexp);
+
+               string = g_string_new (NULL);
+               ebc_generate_constraints (book_cache, string, context.constraints, sexp);
+               if (string->len)
+                       e_cache_sqlite_stmt_append_printf (where_clause, " AND (%s)", string->str);
+               g_string_free (string, TRUE);
+       }
+
+       cursor->query = g_string_free (where_clause, FALSE);
+
+       preflight_context_clear (&context);
+
+       return TRUE;
+}
+
+static gchar *
+ebc_cursor_order_by_fragment (EBookCache *book_cache,
+                             const EContactField *sort_fields,
+                             const EBookCursorSortType *sort_types,
+                             guint n_sort_fields,
+                             gboolean reverse)
+{
+       GString *string;
+       gint ii;
+
+       string = g_string_new ("ORDER BY ");
+
+       for (ii = 0; ii < n_sort_fields; ii++) {
+               SummaryField *field = summary_field_get (book_cache, sort_fields[ii]);
+
+               if (ii > 0)
+                       g_string_append (string, ", ");
+
+               if (field &&
+                   (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+                       g_string_append (string, "summary.");
+                       g_string_append (string, field->dbname);
+                       g_string_append (string, "_" EBC_SUFFIX_SORT_KEY " ");
+               } else {
+                       g_string_append (string, "summary." E_CACHE_COLUMN_OBJECT);
+                       g_string_append (string, " COLLATE ");
+                       g_string_append (string, EBC_COLLATE_PREFIX);
+                       g_string_append (string, e_contact_field_name (sort_fields[ii]));
+                       g_string_append_c (string, ' ');
+               }
+
+               if (reverse)
+                       g_string_append (string, (sort_types[ii] == E_BOOK_CURSOR_SORT_ASCENDING ? "DESC" : 
"ASC"));
+               else
+                       g_string_append (string, (sort_types[ii] == E_BOOK_CURSOR_SORT_ASCENDING ? "ASC" : 
"DESC"));
+       }
+
+       /* Also order the UID, since it's our tie breaker */
+       if (n_sort_fields > 0)
+               g_string_append (string, ", ");
+
+       g_string_append (string, "summary." E_CACHE_COLUMN_UID " ");
+       g_string_append (string, reverse ? "DESC" : "ASC");
+
+       return g_string_free (string, FALSE);
+}
+
+static EBookCacheCursor *
+ebc_cursor_new (EBookCache *book_cache,
+               const gchar *sexp,
+               const EContactField *sort_fields,
+               const EBookCursorSortType *sort_types,
+               guint n_sort_fields)
+{
+       EBookCacheCursor *cursor = g_slice_new0 (EBookCacheCursor);
+
+       cursor->order = ebc_cursor_order_by_fragment (book_cache, sort_fields, sort_types, n_sort_fields, 
FALSE);
+       cursor->reverse_order = ebc_cursor_order_by_fragment (book_cache, sort_fields, sort_types, 
n_sort_fields, TRUE);
+
+       /* Sort parameters */
+       cursor->n_sort_fields = n_sort_fields;
+       cursor->sort_fields = g_memdup (sort_fields, sizeof (EContactField) * n_sort_fields);
+       cursor->sort_types = g_memdup (sort_types,  sizeof (EBookCursorSortType) * n_sort_fields);
+
+       /* Cursor state */
+       cursor->state.values = g_new0 (gchar *, n_sort_fields);
+       cursor->state.last_uid = NULL;
+       cursor->state.position = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+
+       return cursor;
+}
+
+static void
+ebc_cursor_free (EBookCacheCursor *cursor)
+{
+       if (cursor) {
+               cursor_state_clear (cursor, &(cursor->state), E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+               g_free (cursor->state.values);
+
+               g_clear_object (&(cursor->sexp));
+               g_free (cursor->select_vcards);
+               g_free (cursor->select_count);
+               g_free (cursor->query);
+               g_free (cursor->order);
+               g_free (cursor->reverse_order);
+               g_free (cursor->sort_fields);
+               g_free (cursor->sort_types);
+
+               g_slice_free (EBookCacheCursor, cursor);
+       }
+}
+
+#define GREATER_OR_LESS(cursor, idx, reverse) \
+       (reverse ? \
+        (((EBookCacheCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '<' : '>') : \
+        (((EBookCacheCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '>' : '<'))
+
+static inline void
+ebc_cursor_format_equality (EBookCache *book_cache,
+                           GString *string,
+                           EContactField field_id,
+                           const gchar *value,
+                           gchar equality)
+{
+       SummaryField *field = summary_field_get (book_cache, field_id);
+
+       if (field &&
+           (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+               g_string_append (string, "summary.");
+               g_string_append (string, field->dbname);
+               g_string_append (string, "_" EBC_SUFFIX_SORT_KEY " ");
+
+               e_cache_sqlite_stmt_append_printf (string, "%c %Q", equality, value);
+       } else {
+               e_cache_sqlite_stmt_append_printf (string, "(summary." E_CACHE_COLUMN_OBJECT " %c %Q ", 
equality, value);
+
+               g_string_append (string, "COLLATE " EBC_COLLATE_PREFIX);
+               g_string_append (string, e_contact_field_name (field_id));
+               g_string_append_c (string, ')');
+       }
+}
+
+static gchar *
+ebc_cursor_constraints (EBookCache *book_cache,
+                       EBookCacheCursor *cursor,
+                       CursorState *state,
+                       gboolean reverse,
+                       gboolean include_current_uid)
+{
+       GString *string;
+       gint ii, jj;
+
+       /* Example for:
+        *    ORDER BY family_name ASC, given_name DESC
+        *
+        * Where current cursor values are:
+        *    family_name = Jackson
+        *    given_name  = Micheal
+        *
+        * With reverse = FALSE
+        *
+        *    (summary.family_name > 'Jackson') OR
+        *    (summary.family_name = 'Jackson' AND summary.given_name < 'Micheal') OR
+        *    (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid > 
'last-uid')
+        *
+        * With reverse = TRUE (needed for moving the cursor backwards through results)
+        *
+        *    (summary.family_name < 'Jackson') OR
+        *    (summary.family_name = 'Jackson' AND summary.given_name > 'Micheal') OR
+        *    (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid < 
'last-uid')
+        *
+        */
+       string = g_string_new (NULL);
+
+       for (ii = 0; ii <= cursor->n_sort_fields; ii++) {
+               /* Break once we hit a NULL value */
+               if ((ii < cursor->n_sort_fields && state->values[ii] == NULL) ||
+                   (ii == cursor->n_sort_fields && state->last_uid == NULL))
+                       break;
+
+               /* Between each qualifier, add an 'OR' */
+               if (ii > 0)
+                       g_string_append (string, " OR ");
+
+               /* Begin qualifier */
+               g_string_append_c (string, '(');
+
+               /* Create the '=' statements leading up to the current tie breaker */
+               for (jj = 0; jj < ii; jj++) {
+                       ebc_cursor_format_equality (book_cache, string,
+                                                   cursor->sort_fields[jj],
+                                                   state->values[jj], '=');
+                       g_string_append (string, " AND ");
+               }
+
+               if (ii == cursor->n_sort_fields) {
+                       /* The 'include_current_uid' clause is used for calculating
+                        * the current position of the cursor, inclusive of the
+                        * current position.
+                        */
+                       if (include_current_uid)
+                               g_string_append_c (string, '(');
+
+                       /* Append the UID tie breaker */
+                       e_cache_sqlite_stmt_append_printf (
+                               string,
+                               "summary." E_CACHE_COLUMN_UID " %c %Q",
+                               reverse ? '<' : '>',
+                               state->last_uid);
+
+                       if (include_current_uid)
+                               e_cache_sqlite_stmt_append_printf (
+                                       string,
+                                       " OR summary." E_CACHE_COLUMN_UID " = %Q)",
+                                       state->last_uid);
+               } else {
+                       /* SPECIAL CASE: If we have a parially set cursor state, then we must
+                        * report next results that are inclusive of the final qualifier.
+                        *
+                        * This allows one to set the cursor with the family name set to 'J'
+                        * and include the results for contact's Mr & Miss 'J'.
+                        */
+                       gboolean include_exact_match =
+                               (reverse == FALSE &&
+                                ((ii + 1 < cursor->n_sort_fields && state->values[ii + 1] == NULL) ||
+                                 (ii + 1 == cursor->n_sort_fields && state->last_uid == NULL)));
+
+                       if (include_exact_match)
+                               g_string_append_c (string, '(');
+
+                       /* Append the final qualifier for this field */
+                       ebc_cursor_format_equality (book_cache, string,
+                                                   cursor->sort_fields[ii],
+                                                   state->values[ii],
+                                                   GREATER_OR_LESS (cursor, ii, reverse));
+
+                       if (include_exact_match) {
+                               g_string_append (string, " OR ");
+                               ebc_cursor_format_equality (book_cache, string,
+                                                           cursor->sort_fields[ii],
+                                                           state->values[ii], '=');
+                               g_string_append_c (string, ')');
+                       }
+               }
+
+               /* End qualifier */
+               g_string_append_c (string, ')');
+       }
+
+       return g_string_free (string, FALSE);
+}
+
+static gboolean
+ebc_get_int_cb (ECache *cache,
+               gint ncols,
+               const gchar **column_names,
+               const gchar **column_values,
+               gpointer user_data)
+{
+       gint *pint = user_data;
+
+       g_return_val_if_fail (pint != NULL, FALSE);
+
+       if (ncols == 1) {
+               *pint = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0;
+       } else {
+               *pint = 0;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+cursor_count_total_locked (EBookCache *book_cache,
+                          EBookCacheCursor *cursor,
+                          gint *out_total,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       GString *query;
+       gboolean success;
+
+       query = g_string_new (cursor->select_count);
+
+       /* Add the filter constraints (if any) */
+       if (cursor->query) {
+               g_string_append (query, " WHERE ");
+
+               g_string_append_c (query, '(');
+               g_string_append (query, cursor->query);
+               g_string_append_c (query, ')');
+       }
+
+       /* Execute the query */
+       success = e_cache_sqlite_select (E_CACHE (book_cache), query->str, ebc_get_int_cb, out_total, 
cancellable, error);
+
+       g_string_free (query, TRUE);
+
+       return success;
+}
+
+static gboolean
+cursor_count_position_locked (EBookCache *book_cache,
+                             EBookCacheCursor *cursor,
+                             gint *out_position,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       GString *query;
+       gboolean success;
+
+       query = g_string_new (cursor->select_count);
+
+       /* Add the filter constraints (if any) */
+       if (cursor->query) {
+               g_string_append (query, " WHERE ");
+
+               g_string_append_c (query, '(');
+               g_string_append (query, cursor->query);
+               g_string_append_c (query, ')');
+       }
+
+       /* Add the cursor constraints (if any) */
+       if (cursor->state.values[0] != NULL) {
+               gchar *constraints = NULL;
+
+               if (!cursor->query)
+                       g_string_append (query, " WHERE ");
+               else
+                       g_string_append (query, " AND ");
+
+               /* Here we do a reverse query, we're looking for all the
+                * results leading up to the current cursor value, including
+                * the cursor value
+                */
+               constraints = ebc_cursor_constraints (book_cache, cursor, &(cursor->state), TRUE, TRUE);
+
+               g_string_append_c (query, '(');
+               g_string_append (query, constraints);
+               g_string_append_c (query, ')');
+
+               g_free (constraints);
+       }
+
+       /* Execute the query */
+       success = e_cache_sqlite_select (E_CACHE (book_cache), query->str, ebc_get_int_cb, out_position, 
cancellable, error);
+
+       g_string_free (query, TRUE);
+
+       return success;
+}
+
+typedef struct {
+       gint country_code;
+       gchar *national;
+} E164Number;
+
+static E164Number *
+ebc_e164_number_new (gint country_code,
+                    const gchar *national)
+{
+       E164Number *number = g_slice_new (E164Number);
+
+       number->country_code = country_code;
+       number->national = g_strdup (national);
+
+       return number;
+}
+
+static void
+ebc_e164_number_free (E164Number *number)
+{
+       if (number) {
+               g_free (number->national);
+               g_slice_free (E164Number, number);
+       }
+}
+
+static gint
+ebc_e164_number_find (E164Number *number_a,
+                     E164Number *number_b)
+{
+       gint ret;
+
+       ret = number_a->country_code - number_b->country_code;
+
+       if (ret == 0) {
+               ret = g_strcmp0 (
+                       number_a->national,
+                       number_b->national);
+       }
+
+       return ret;
+}
+
+static GList *
+extract_e164_attribute_params (EContact *contact)
+{
+       EVCard *vcard = E_VCARD (contact);
+       GList *extracted = NULL;
+       GList *attr_list;
+
+       for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+               EVCardAttribute *const attr = attr_list->data;
+               EVCardAttributeParam *param = NULL;
+               GList *param_list, *values, *l;
+               gchar *this_national = NULL;
+               gint this_country = 0;
+
+               /* We only attach E164 parameters to TEL attributes. */
+               if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+                       continue;
+
+               /* Find already exisiting parameter, so that we can reuse it. */
+               for (param_list = e_vcard_attribute_get_params (attr); param_list; param_list = 
param_list->next) {
+                       if (strcmp (e_vcard_attribute_param_get_name (param_list->data), EVC_X_E164) == 0) {
+                               param = param_list->data;
+                               break;
+                       }
+               }
+
+               if (!param)
+                       continue;
+
+               values = e_vcard_attribute_param_get_values (param);
+               for (l = values; l; l = l->next) {
+                       const gchar *value = l->data;
+
+                       if (value[0] == '+')
+                               this_country = g_ascii_strtoll (&value[1], NULL, 10);
+                       else if (this_national == NULL)
+                               this_national = g_strdup (value);
+               }
+
+               if (this_national) {
+                       E164Number *number;
+
+                       number = ebc_e164_number_new (this_country, this_national);
+                       extracted = g_list_prepend (extracted, number);
+               }
+
+               g_free (this_national);
+
+               /* Clear the values, we'll insert new ones */
+               e_vcard_attribute_param_remove_values (param);
+               e_vcard_attribute_remove_param (attr, EVC_X_E164);
+       }
+
+       return extracted;
+}
+
+static gboolean
+update_e164_attribute_params (EBookCache *book_cache,
+                             EContact *contact,
+                             const gchar *default_region)
+{
+       GList *original_numbers = NULL;
+       GList *attr_list;
+       gboolean changed = FALSE;
+       gint n_numbers = 0;
+       EVCard *vcard = E_VCARD (contact);
+
+       original_numbers = extract_e164_attribute_params (contact);
+
+       for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+               EVCardAttribute *const attr = attr_list->data;
+               EVCardAttributeParam *param = NULL;
+               const gchar *original_number = NULL;
+               gchar *country_string;
+               GList *values;
+               E164Number number = { 0, NULL };
+
+               /* We only attach E164 parameters to TEL attributes. */
+               if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+                       continue;
+
+               /* Fetch the TEL value */
+               values = e_vcard_attribute_get_values (attr);
+
+               /* Compute E164 number based on the TEL value */
+               if (values && values->data) {
+                       original_number = (const gchar *) values->data;
+                       number.national = convert_phone (original_number, book_cache->priv->region_code, 
&(number.country_code));
+               }
+
+               if (number.national == NULL)
+                       continue;
+
+               /* Count how many we successfully parsed in this region code */
+               n_numbers++;
+
+               /* Check if we have a differing e164 number, if there is no match
+                * in the old existing values then the vcard changed
+                */
+               if (!g_list_find_custom (original_numbers, &number, (GCompareFunc) ebc_e164_number_find))
+                       changed = TRUE;
+
+               if (number.country_code != 0)
+                       country_string = g_strdup_printf ("+%d", number.country_code);
+               else
+                       country_string = g_strdup ("");
+
+               param = e_vcard_attribute_param_new (EVC_X_E164);
+               e_vcard_attribute_add_param (attr, param);
+
+               /* Assign the parameter values. It seems odd that we revert
+                * the order of NN and CC, but at least EVCard's parser doesn't
+                * permit an empty first param value. Which of course could be
+                * fixed - in order to create a nice potential IOP problem with
+                ** other vCard parsers. */
+               e_vcard_attribute_param_add_values (param, number.national, country_string, NULL);
+
+               g_free (number.national);
+               g_free (country_string);
+       }
+
+       if (!changed && n_numbers != g_list_length (original_numbers))
+               changed = TRUE;
+
+       g_list_free_full (original_numbers, (GDestroyNotify) ebc_e164_number_free);
+
+       return changed;
+}
+
+static gboolean
+e_book_cache_get_string (ECache *cache,
+                        gint ncols,
+                        const gchar **column_names,
+                        const gchar **column_values,
+                        gpointer user_data)
+{
+       gchar **pvalue = user_data;
+
+       g_return_val_if_fail (ncols == 1, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+       g_return_val_if_fail (pvalue != NULL, FALSE);
+
+       if (!*pvalue)
+               *pvalue = g_strdup (column_values[0]);
+
+       return TRUE;
+}
+
+static gboolean
+e_book_cache_get_old_contacts_cb (ECache *cache,
+                                 gint ncols,
+                                 const gchar *column_names[],
+                                 const gchar *column_values[],
+                                 gpointer user_data)
+{
+       GSList **pold_contacts = user_data;
+
+       g_return_val_if_fail (pold_contacts != NULL, FALSE);
+       g_return_val_if_fail (ncols == 3, FALSE);
+
+       if (column_values[0] && column_values[1]) {
+               *pold_contacts = g_slist_prepend (*pold_contacts,
+                       e_book_cache_search_data_new (column_values[0], column_values[1], column_values[2]));
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_book_cache_gather_table_names_cb (ECache *cache,
+                                   gint ncols,
+                                   const gchar *column_names[],
+                                   const gchar *column_values[],
+                                   gpointer user_data)
+{
+       GSList **ptables = user_data;
+
+       g_return_val_if_fail (ptables != NULL, FALSE);
+       g_return_val_if_fail (ncols == 1, FALSE);
+
+       *ptables = g_slist_prepend (*ptables, g_strdup (column_values[0]));
+
+       return TRUE;
+}
+
+static gboolean
+e_book_cache_migrate (ECache *cache,
+                     gint from_version,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       EBookCache *book_cache = E_BOOK_CACHE (cache);
+       gboolean success = TRUE;
+
+       /* Migration from EBookSqlite database */
+       if (from_version <= 0) {
+               GSList *tables = NULL, *old_contacts = NULL, *link;
+
+               if (e_cache_sqlite_select (cache, "SELECT uid,vcard,bdata FROM folder_id ORDER BY uid",
+                       e_book_cache_get_old_contacts_cb, &old_contacts, cancellable, NULL)) {
+
+                       old_contacts = g_slist_reverse (old_contacts);
+
+                       for (link = old_contacts; link && success; link = g_slist_next (link)) {
+                               EBookCacheSearchData *data = link->data;
+                               EContact *contact;
+
+                               if (!data)
+                                       continue;
+
+                               contact = e_contact_new_from_vcard_with_uid (data->vcard, data->uid);
+                               if (!contact)
+                                       continue;
+
+                               success = e_book_cache_put_contact (book_cache, contact, data->extra, 
E_CACHE_IS_ONLINE, cancellable, error);
+                       }
+               }
+
+               /* Delete obsolete tables */
+               success = success && e_cache_sqlite_select (cache,
+                       "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'folder_id%'",
+                       e_book_cache_gather_table_names_cb, &tables, cancellable, error);
+
+               for (link = tables; link && success; link = g_slist_next (link)) {
+                       const gchar *name = link->data;
+                       gchar *stmt;
+
+                       if (!link)
+                               continue;
+
+                       stmt = e_cache_sqlite_stmt_printf ("DROP TABLE IF EXISTS %Q", name);
+                       success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+                       e_cache_sqlite_stmt_free (stmt);
+               }
+
+               g_slist_free_full (tables, g_free);
+
+               success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS keys", cancellable, 
error);
+               success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS folders", cancellable, 
error);
+               success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS folder_id", 
cancellable, error);
+
+               if (success) {
+                       /* Save the changes by finishing the transaction */
+                       e_cache_unlock (cache, E_CACHE_UNLOCK_COMMIT);
+                       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+                       /* Try to vacuum, but do not claim any error if failed */
+                       e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
+               }
+
+               g_slist_free_full (old_contacts, e_book_cache_search_data_free);
+       }
+
+       /* Add any version-related changes here */
+       /*if (from_version < E_BOOK_CACHE_VERSION) {
+       }*/
+
+       return success;
+}
+
+static gboolean
+e_book_cache_populate_other_columns (EBookCache *book_cache,
+                                    ESourceBackendSummarySetup *setup,
+                                    GSList **out_columns, /* ECacheColumnInfo * */
+                                    GError **error)
+{
+       GSList *columns = NULL;
+       gboolean use_default;
+       gboolean success = TRUE;
+       gint ii;
+
+       g_return_val_if_fail (out_columns != NULL, FALSE);
+
+       #define add_column(_name, _type, _index_name) G_STMT_START { \
+               columns = g_slist_prepend (columns, e_cache_column_info_new (_name, _type, _index_name)); \
+               } G_STMT_END
+
+       add_column (EBC_COLUMN_EXTRA, "TEXT", NULL);
+
+       use_default = !setup;
+
+       if (setup) {
+               EContactField *fields;
+               EContactField *indexed_fields;
+               EBookIndexType *index_types = NULL;
+               gint n_fields = 0, n_indexed_fields = 0, ii;
+
+               fields = e_source_backend_summary_setup_get_summary_fields (setup, &n_fields);
+               indexed_fields = e_source_backend_summary_setup_get_indexed_fields (setup, &index_types, 
&n_indexed_fields);
+
+               if (n_fields <= 0 || n_fields >= EBC_MAX_SUMMARY_FIELDS) {
+                       if (n_fields)
+                               g_warning ("EBookCache refused to create cache with more than %d summary 
fields", EBC_MAX_SUMMARY_FIELDS);
+                       use_default = TRUE;
+               } else {
+                       GArray *summary_fields;
+
+                       summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+                       /* Ensure the non-optional fields first */
+                       summary_field_append (summary_fields, E_CONTACT_UID, error);
+                       summary_field_append (summary_fields, E_CONTACT_REV, error);
+
+                       for (ii = 0; ii < n_fields; ii++) {
+                               if (!summary_field_append (summary_fields, fields[ii], error)) {
+                                       success = FALSE;
+                                       break;
+                               }
+                       }
+
+                       if (!success) {
+                               gint n_sfields;
+                               SummaryField *sfields;
+
+                               /* Properly free the array */
+                               n_sfields = summary_fields->len;
+                               sfields = (SummaryField *) g_array_free (summary_fields, FALSE);
+                               summary_fields_array_free (sfields, n_sfields);
+
+                               g_free (fields);
+                               g_free (index_types);
+                               g_free (indexed_fields);
+
+                               g_slist_free_full (columns, e_cache_column_info_free);
+
+                               return FALSE;
+                       }
+
+                       /* Add the 'indexed' flag to the SummaryField structs */
+                       summary_fields_add_indexes (summary_fields, indexed_fields, index_types, 
n_indexed_fields);
+
+                       book_cache->priv->n_summary_fields = summary_fields->len;
+                       book_cache->priv->summary_fields = (SummaryField *) g_array_free (summary_fields, 
FALSE);
+               }
+
+               g_free (fields);
+               g_free (index_types);
+               g_free (indexed_fields);
+       }
+
+       if (use_default) {
+               GArray *summary_fields;
+
+               g_warn_if_fail (book_cache->priv->n_summary_fields == 0);
+
+               /* Create the default summary structs */
+               summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+               for (ii = 0; ii < G_N_ELEMENTS (default_summary_fields); ii++) {
+                       summary_field_append (summary_fields, default_summary_fields[ii], NULL);
+               }
+
+               /* Add the default index flags */
+               summary_fields_add_indexes (
+                       summary_fields,
+                       default_indexed_fields,
+                       default_index_types,
+                       G_N_ELEMENTS (default_indexed_fields));
+
+               book_cache->priv->n_summary_fields = summary_fields->len;
+               book_cache->priv->summary_fields = (SummaryField *) g_array_free (summary_fields, FALSE);
+       }
+
+       #undef add_column
+
+       if (success) {
+               for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+                       SummaryField *fld = &(book_cache->priv->summary_fields[ii]);
+
+                       summary_field_init_dbnames (fld);
+
+                       if (fld->type != E_TYPE_CONTACT_ATTR_LIST)
+                               summary_field_prepend_columns (fld, &columns);
+               }
+       }
+
+       *out_columns = columns;
+
+       return success;
+}
+
+static gboolean
+e_book_cache_initialize (EBookCache *book_cache,
+                        const gchar *filename,
+                        ESource *source,
+                        ESourceBackendSummarySetup *setup,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       ECache *cache;
+       GSList *other_columns = NULL;
+       sqlite3 *db;
+       gint ii, sqret;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (filename != NULL, FALSE);
+       if (source)
+               g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+       if (setup)
+               g_return_val_if_fail (E_IS_SOURCE_BACKEND_SUMMARY_SETUP (setup), FALSE);
+
+       if (source)
+               book_cache->priv->source = g_object_ref (source);
+
+       cache = E_CACHE (book_cache);
+
+       success = e_book_cache_populate_other_columns (book_cache, setup, &other_columns, error);
+       if (!success)
+               goto exit;
+
+       success = e_cache_initialize_sync (cache, filename, other_columns, cancellable, error);
+       if (!success)
+               goto exit;
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       db = e_cache_get_sqlitedb (cache);
+       sqret = SQLITE_OK;
+
+       /* Install our custom functions */
+       for (ii = 0; sqret == SQLITE_OK && ii < G_N_ELEMENTS (ebc_custom_functions); ii++) {
+               sqret = sqlite3_create_function (
+                       db,
+                       ebc_custom_functions[ii].name,
+                       ebc_custom_functions[ii].arguments,
+                       SQLITE_UTF8, book_cache,
+                       ebc_custom_functions[ii].func,
+                       NULL, NULL);
+       }
+
+       /* Fallback COLLATE implementations generated on demand */
+       if (sqret == SQLITE_OK)
+               sqret = sqlite3_collation_needed (db, book_cache, ebc_generate_collator);
+
+       if (sqret != SQLITE_OK) {
+               if (!db) {
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Insufficient 
memory"));
+               } else {
+                       const gchar *errmsg = sqlite3_errmsg (db);
+
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, _("Can't open database %s: 
%s"), filename, errmsg);
+               }
+
+               success = FALSE;
+       }
+
+       success = success && ebc_init_locale (book_cache, cancellable, error);
+
+       success = success && ebc_init_aux_tables (book_cache, cancellable, error);
+
+       /* Check for data migration */
+       success = success && e_book_cache_migrate (cache, e_cache_get_version (cache), cancellable, error);
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       if (!success)
+               goto exit;
+
+       if (e_cache_get_version (cache) != E_BOOK_CACHE_VERSION)
+               e_cache_set_version (cache, E_BOOK_CACHE_VERSION);
+
+ exit:
+       g_slist_free_full (other_columns, e_cache_column_info_free);
+
+       return success;
+}
+
+/**
+ * e_book_cache_new:
+ * @filename: file name to load or create the new cache
+ * @source: (nullable): an optional #ESource, associated with the #EBookCache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCache with the default summary configuration.
+ *
+ * Aside from the mandatory fields %E_CONTACT_UID, %E_CONTACT_REV,
+ * the default configuration stores the following fields for quick
+ * performance of searches: %E_CONTACT_FILE_AS, %E_CONTACT_NICKNAME,
+ * %E_CONTACT_FULL_NAME, %E_CONTACT_GIVEN_NAME, %E_CONTACT_FAMILY_NAME,
+ * %E_CONTACT_EMAIL, %E_CONTACT_TEL, %E_CONTACT_IS_LIST, %E_CONTACT_LIST_SHOW_ADDRESSES,
+ * and %E_CONTACT_WANTS_HTML.
+ *
+ * The fields %E_CONTACT_FULL_NAME and %E_CONTACT_EMAIL are configured
+ * to respond extra quickly with the %E_BOOK_INDEX_PREFIX index flag.
+ *
+ * The fields %E_CONTACT_FILE_AS, %E_CONTACT_FAMILY_NAME and
+ * %E_CONTACT_GIVEN_NAME are configured to perform well with
+ * the #EBookCacheCursor, using the %E_BOOK_INDEX_SORT_KEY
+ * index flag.
+ *
+ * Returns: (transfer full) (nullable): A new #EBookCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_cache_new (const gchar *filename,
+                 ESource *source,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       g_return_val_if_fail (filename != NULL, NULL);
+
+       return e_book_cache_new_full (filename, source, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_new_full:
+ * @filename: file name to load or create the new cache
+ * @source: (nullable): an optional #ESource, associated with the #EBookCache, or %NULL
+ * @setup: (nullable): an #ESourceBackendSummarySetup describing how the summary should be setup, or %NULL 
to use the default
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCache with the given or the default summary configuration.
+ *
+ * Like e_book_sqlite_new(), but allows configuration of which contact fields
+ * will be stored for quick reference in the summary. The configuration indicated by
+ * @setup will only be taken into account when initially creating the underlying table,
+ * further configurations will be ignored.
+ *
+ * The fields %E_CONTACT_UID and %E_CONTACT_REV are not optional,
+ * they will be stored in the summary regardless of this function's parameters.
+ * Only #EContactFields with the type %G_TYPE_STRING, %G_TYPE_BOOLEAN or
+ * %E_TYPE_CONTACT_ATTR_LIST are currently supported.
+ *
+ * Returns: (transfer full) (nullable): A new #EBookCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_cache_new_full (const gchar *filename,
+                      ESource *source,
+                      ESourceBackendSummarySetup *setup,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       EBookCache *book_cache;
+
+       g_return_val_if_fail (filename != NULL, NULL);
+
+       book_cache = g_object_new (E_TYPE_BOOK_CACHE, NULL);
+
+       if (!e_book_cache_initialize (book_cache, filename, source, setup, cancellable, error)) {
+               g_object_unref (book_cache);
+               book_cache = NULL;
+       }
+
+       return book_cache;
+}
+
+/**
+ * e_book_cache_ref_source:
+ * @book_cache: An #EBookCache
+ *
+ * References the #ESource to which @book_cache is paired,
+ * use g_object_unref() when no longer needed.
+ * It can be %NULL in some cases, like when running tests.
+ *
+ * Returns: (transfer full): A reference to the #ESource to which @book_cache
+ *    is paired, or %NULL.
+ *
+ * Since: 3.26
+ **/
+ESource *
+e_book_cache_ref_source (EBookCache *book_cache)
+{
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+       if (book_cache->priv->source)
+               return g_object_ref (book_cache->priv->source);
+
+       return NULL;
+}
+
+/**
+ * e_book_cache_dup_contact_revision:
+ * @book_cache: an #EBookCache
+ * @contact: an #EContact
+ *
+ * Returns the @contact revision, used to detect changes.
+ * The returned string should be freed with g_free(), when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly allocated string containing
+ *    revision of the @contact.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_book_cache_dup_contact_revision (EBookCache *book_cache,
+                                  EContact *contact)
+{
+       gchar *revision = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+       g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+
+       g_signal_emit (book_cache, signals[DUP_CONTACT_REVISION], 0, contact, &revision);
+
+       return revision;
+}
+
+/**
+ * e_book_cache_set_locale:
+ * @book_cache: An #EBookCache
+ * @lc_collate: The new locale for the cache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Relocalizes any locale specific data in the specified
+ * new @lc_collate locale.
+ *
+ * The @lc_collate locale setting is stored and remembered on
+ * subsequent accesses of the cache, changing the locale will
+ * store the new locale and will modify sort keys and any
+ * locale specific data in the cache.
+ *
+ * As a side effect, it's possible that changing the locale
+ * will cause stored vCard-s to change.
+ *
+ * Returns: Whether the new locale was successfully set.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_set_locale (EBookCache *book_cache,
+                        const gchar *lc_collate,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       ECache *cache;
+       gboolean success, changed = FALSE;
+       gchar *stored_lc_collate = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+
+       cache = E_CACHE (book_cache);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       success = ebc_set_locale_internal (book_cache, lc_collate, error);
+
+       if (success)
+               stored_lc_collate = e_cache_dup_key (cache, EBC_KEY_LC_COLLATE, NULL);
+
+       if (success && g_strcmp0 (stored_lc_collate, lc_collate) != 0)
+               success = ebc_upgrade (book_cache, cancellable, error);
+
+       /* If for some reason we failed, then reset the collator to use the old locale */
+       if (!success && stored_lc_collate && stored_lc_collate[0]) {
+               ebc_set_locale_internal (book_cache, stored_lc_collate, NULL);
+               changed = TRUE;
+       }
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       g_free (stored_lc_collate);
+
+       if (success && changed)
+               g_object_notify (G_OBJECT (book_cache), "locale");
+
+       return success;
+}
+
+/**
+ * e_book_cache_dup_locale:
+ * @book_cache: An #EBookCache
+ *
+ * Returns: (transfer full): A new string containing the current local
+ *    being used by the @book_cache. Free it with g_free(), when no
+ *    longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_book_cache_dup_locale (EBookCache *book_cache)
+{
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+       return g_strdup (book_cache->priv->locale);
+}
+
+/**
+ * e_book_cache_ref_collator:
+ * @book_cache: An #EBookCache
+ *
+ * References the currently active #ECollator for @book_cache,
+ * use e_collator_unref() when finished using the returned collator.
+ *
+ * Note that the active collator will change with the active locale setting.
+ *
+ * Returns: (transfer full): A reference to the active collator.
+ *
+ * Since: 3.26
+ **/
+ECollator *
+e_book_cache_ref_collator (EBookCache *book_cache)
+{
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+       return e_collator_ref (book_cache->priv->collator);
+}
+
+/**
+ * e_book_cache_put_contact:
+ * @book_cache: An #EBookCache
+ * @contact: an #EContact to be added
+ * @extra: extra data to store in association with this contact
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is a convenience wrapper for e_book_cache_put_contacts(),
+ * which is the preferred way to add or modify multiple contacts when possible.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_put_contact (EBookCache *book_cache,
+                         EContact *contact,
+                         const gchar *extra,
+                         ECacheOfflineFlag offline_flag,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       GSList *contacts, *extras;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+       contacts = g_slist_append (NULL, contact);
+       extras = g_slist_append (NULL, (gpointer) extra);
+
+       success = e_book_cache_put_contacts (book_cache, contacts, extras, offline_flag, cancellable, error);
+
+       g_slist_free (contacts);
+       g_slist_free (extras);
+
+       return success;
+}
+
+/**
+ * e_book_cache_put_contacts:
+ * @book_cache: An #EBookCache
+ * @contacts: (element-type EContact): A list of contacts to add to @book_cache
+ * @extras: (nullable) (element-type utf8): A list of extra data to store in association with the @contacts
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting these contacts in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds or replaces contacts in @book_cache.
+ *
+ * If @extras is specified, it must have an equal length as the @contacts list. Each element
+ * from the @extras list will be stored in association with its corresponding contact
+ * in the @contacts list.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_put_contacts (EBookCache *book_cache,
+                          const GSList *contacts,
+                          const GSList *extras,
+                          ECacheOfflineFlag offline_flag,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       const GSList *clink, *elink;
+       ECache *cache;
+       ECacheColumnValues *other_columns;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (contacts != NULL, FALSE);
+       g_return_val_if_fail (extras == NULL || g_slist_length ((GSList *) extras) == g_slist_length ((GSList 
*) contacts), FALSE);
+
+       cache = E_CACHE (book_cache);
+       other_columns = e_cache_column_values_new ();
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+       e_cache_freeze_revision_change (cache);
+
+       for (clink = contacts, elink = extras; clink; clink = g_slist_next (clink), elink = g_slist_next 
(elink)) {
+               EContact *contact = clink->data;
+               const gchar *extra = elink ? elink->data : NULL;
+               gchar *uid, *rev, *vcard;
+
+               g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+               vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+               g_return_val_if_fail (vcard != NULL, FALSE);
+
+               e_cache_column_values_remove_all (other_columns);
+
+               if (extra)
+                       e_cache_column_values_take_value (other_columns, EBC_COLUMN_EXTRA, g_strdup (extra));
+
+               uid = e_contact_get (contact, E_CONTACT_UID);
+               rev = e_book_cache_dup_contact_revision (book_cache, contact);
+
+               ebc_fill_other_columns (book_cache, contact, other_columns);
+
+               success = e_cache_put (cache, uid, rev, vcard, other_columns, offline_flag, cancellable, 
error);
+
+               g_free (vcard);
+               g_free (rev);
+               g_free (uid);
+
+               if (!success)
+                       break;
+       }
+
+       e_cache_thaw_revision_change (cache);
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       e_cache_column_values_free (other_columns);
+
+       return success;
+}
+
+/**
+ * e_book_cache_remove_contact:
+ * @book_cache: An #EBookCache
+ * @uid: the uid of the contact to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the contact identified by @uid from @book_cache.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_remove_contact (EBookCache *book_cache,
+                            const gchar *uid,
+                            ECacheOfflineFlag offline_flag,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       GSList *uids;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       uids = g_slist_append (NULL, (gpointer) uid);
+
+       success = e_book_cache_remove_contacts (book_cache, uids, offline_flag, cancellable, error);
+
+       g_slist_free (uids);
+
+       return success;
+}
+
+/**
+ * e_book_cache_remove_contacts:
+ * @book_cache: An #EBookCache
+ * @uids: (element-type utf8): a #GSList of uids indicating which contacts to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing these contacts in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the contacts indicated by @uids from @book_cache.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_remove_contacts (EBookCache *book_cache,
+                             const GSList *uids,
+                             ECacheOfflineFlag offline_flag,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       ECache *cache;
+       const GSList *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (uids != NULL, FALSE);
+
+       cache = E_CACHE (book_cache);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+       e_cache_freeze_revision_change (cache);
+
+       for (link = uids; success && link; link = g_slist_next (link)) {
+               const gchar *uid = link->data;
+
+               success = e_cache_remove (cache, uid, offline_flag, cancellable, error);
+       }
+
+       e_cache_thaw_revision_change (cache);
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+/**
+ * e_book_cache_get_contact:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @out_contact: (out) (transfer full): Return location to store the fetched contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetch the #EContact specified by @uid in @book_cache.
+ *
+ * If @meta_contact is specified, then a shallow #EContact will be created
+ * holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_contact (EBookCache *book_cache,
+                         const gchar *uid,
+                         gboolean meta_contact,
+                         EContact **out_contact,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       gchar *vcard = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_contact != NULL, FALSE);
+
+       *out_contact = NULL;
+
+       if (!e_book_cache_get_vcard (book_cache, uid, meta_contact, &vcard, cancellable, error) ||
+           !vcard) {
+               return FALSE;
+       }
+
+       *out_contact = e_contact_new_from_vcard_with_uid (vcard, uid);
+
+       g_free (vcard);
+
+       return TRUE;
+}
+
+/**
+ * e_book_cache_get_vcard:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @out_vcard: (out) (transfer full): Return location to store the fetched vCard string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetch a vCard string for @uid in @book_cache.
+ *
+ * If @meta_contact is specified, then a shallow vCard representation will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_vcard (EBookCache *book_cache,
+                       const gchar *uid,
+                       gboolean meta_contact,
+                       gchar **out_vcard,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       gchar *full_vcard, *revision = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_vcard != NULL, FALSE);
+
+       *out_vcard = NULL;
+
+       full_vcard = e_cache_get (E_CACHE (book_cache), uid,
+               meta_contact ? &revision : NULL,
+               NULL, cancellable, error);
+
+       if (!full_vcard) {
+               g_warn_if_fail (revision == NULL);
+               return FALSE;
+       }
+
+       if (meta_contact) {
+               EContact *contact = e_contact_new ();
+
+               e_contact_set (contact, E_CONTACT_UID, uid);
+               if (revision)
+                       e_contact_set (contact, E_CONTACT_REV, revision);
+
+               *out_vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+               g_object_unref (contact);
+               g_free (full_vcard);
+       } else {
+               *out_vcard = full_vcard;
+       }
+
+       g_free (revision);
+
+       return TRUE;
+}
+
+/**
+ * e_book_cache_set_contact_extra:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to set the extra data for
+ * @extra: (nullable): The extra data to set
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets or replaces the extra data associated with @uid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_set_contact_extra (EBookCache *book_cache,
+                               const gchar *uid,
+                               const gchar *extra,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (!e_cache_contains (E_CACHE (book_cache), uid, E_CACHE_INCLUDE_DELETED)) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+               return FALSE;
+       }
+
+       if (extra) {
+               stmt = e_cache_sqlite_stmt_printf (
+                       "UPDATE " E_CACHE_TABLE_OBJECTS " SET " EBC_COLUMN_EXTRA "=%Q"
+                       " WHERE " E_CACHE_COLUMN_UID "=%Q",
+                       extra, uid);
+       } else {
+               stmt = e_cache_sqlite_stmt_printf (
+                       "UPDATE " E_CACHE_TABLE_OBJECTS " SET " EBC_COLUMN_EXTRA "=NULL"
+                       " WHERE " E_CACHE_COLUMN_UID "=%Q",
+                       uid);
+       }
+
+       success = e_cache_sqlite_exec (E_CACHE (book_cache), stmt, cancellable, error);
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       return success;
+}
+
+/**
+ * e_book_cache_get_contact_extra:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch the extra data for
+ * @out_extra: (out) (transfer full): Return location to store the extra data
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetches the extra data previously set for @uid, either with
+ * e_book_cache_set_contact_extra() or when adding contacts.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_contact_extra (EBookCache *book_cache,
+                               const gchar *uid,
+                               gchar **out_extra,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (!e_cache_contains (E_CACHE (book_cache), uid, E_CACHE_INCLUDE_DELETED)) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+               return FALSE;
+       }
+
+       stmt = e_cache_sqlite_stmt_printf (
+               "SELECT " EBC_COLUMN_EXTRA " FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID "=%Q",
+               uid);
+
+       success = e_cache_sqlite_select (E_CACHE (book_cache), stmt, e_book_cache_get_string, out_extra, 
cancellable, error);
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       return success;
+}
+
+/**
+ * e_book_cache_search:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored contacts
+ * @meta_contacts: Whether entire contacts are desired, or only the metadata
+ * @out_list: (out) (transfer full) (element-type EBookCacheSearchData): Return location
+ *    to store a #GSList of #EBookCacheSearchData structures
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @book_cache for contacts matching the search expression @sexp.
+ *
+ * When @sexp refers only to #EContactFields configured in the summary of @book_cache,
+ * the search should always be quick, when searching for other #EContactFields
+ * a fallback will be used.
+ *
+ * The returned @out_list list should be freed with g_slist_free_full (list, e_book_cache_search_data_free)
+ * when no longer needed.
+ *
+ * If @meta_contact is specified, then shallow vCard representations will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search (EBookCache *book_cache,
+                    const gchar *sexp,
+                    gboolean meta_contacts,
+                    GSList **out_list,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (out_list != NULL, FALSE);
+
+       *out_list = NULL;
+
+       return ebc_search_internal (book_cache, sexp,
+               meta_contacts ? SEARCH_UID_AND_REV : SEARCH_FULL,
+               out_list, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_search_uids:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to get all stored contacts
+ * @out_list: (out) (transfer full) (element-type utf8): Return location to store a #GSList of contact uids
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_book_cache_search(), but fetches only a list of contact UIDs.
+ *
+ * The returned @out_list list should be freed with g_slist_free_full(list, g_free)
+ * when no longer needed.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search_uids (EBookCache *book_cache,
+                         const gchar *sexp,
+                         GSList **out_list,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (out_list != NULL, FALSE);
+
+       *out_list = NULL;
+
+       return ebc_search_internal (book_cache, sexp, SEARCH_UID, out_list, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_search_with_callback:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to get all stored contacts
+ * @func: an #EBookCacheSearchFunc callback to call for each found row
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_book_cache_search(), but calls the @func for each found contact.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search_with_callback (EBookCache *book_cache,
+                                  const gchar *sexp,
+                                  EBookCacheSearchFunc func,
+                                  gpointer user_data,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       return ebc_search_internal (book_cache, sexp, SEARCH_FULL, NULL, func, user_data, cancellable, error);
+}
+
+/**
+ * e_book_cache_cursor_new:
+ * @book_cache: An #EBookCache
+ * @sexp: search expression; use %NULL or an empty string to get all stored contacts
+ * @sort_fields: (array length=n_sort_fields): An array of #EContactField-s as sort keys in order of priority
+ * @sort_types: (array length=n_sort_fields): An array of #EBookCursorSortTypes, one for each field in 
@sort_fields
+ * @n_sort_fields: The number of fields to sort results by
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCacheCursor.
+ *
+ * The cursor should be freed with e_book_cache_cursor_free() when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly created #EBookCacheCursor
+ *
+ * Since: 3.26
+ **/
+EBookCacheCursor *
+e_book_cache_cursor_new (EBookCache *book_cache,
+                        const gchar *sexp,
+                        const EContactField *sort_fields,
+                        const EBookCursorSortType *sort_types,
+                        guint n_sort_fields,
+                        GError **error)
+{
+       EBookCacheCursor *cursor;
+       gint ii;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+       /* We don't like '\0' sexps, prefer NULL */
+       if (sexp && !*sexp)
+               sexp = NULL;
+
+       e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+       /* Need one sort key ... */
+       if (n_sort_fields == 0) {
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+                       _("At least one sort field must be specified to use a cursor"));
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return NULL;
+       }
+
+       /* We only support string fields to sort the cursor */
+       for (ii = 0; ii < n_sort_fields; ii++) {
+               if (e_contact_field_type (sort_fields[ii]) != G_TYPE_STRING) {
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+                               _("Cannot sort by a field that is not a string type"));
+
+                       e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+                       return NULL;
+               }
+       }
+
+       /* Now we need to create the cursor instance before setting up the query
+        * (not really true, but more convenient that way).
+        */
+       cursor = ebc_cursor_new (book_cache, sexp, sort_fields, sort_types, n_sort_fields);
+
+       /* Setup the cursor's query expression which might fail */
+       if (!ebc_cursor_setup_query (book_cache, cursor, sexp, error)) {
+               ebc_cursor_free (cursor);
+               cursor = NULL;
+       }
+
+       e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+       return cursor;
+}
+
+/**
+ * e_book_cache_cursor_free:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to free
+ *
+ * Frees the @cursor, previously allocated with e_book_cache_cursor_new().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_cursor_free (EBookCache *book_cache,
+                         EBookCacheCursor *cursor)
+{
+       g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+       g_return_if_fail (cursor != NULL);
+
+       ebc_cursor_free (cursor);
+}
+
+typedef struct {
+       gint uid_index;
+       gint object_index;
+       gint extra_index;
+
+       GSList *results;
+       gchar *alloc_vcard;
+       const gchar *last_vcard;
+
+       gboolean collect_results;
+       gint n_results;
+} CursorCollectData;
+
+static gboolean
+ebc_collect_results_for_cursor_cb (ECache *cache,
+                                  gint ncols,
+                                  const gchar *column_names[],
+                                  const gchar *column_values[],
+                                  gpointer user_data)
+{
+       CursorCollectData *data = user_data;
+       const gchar *object = NULL, *extra = NULL;
+
+       if (data->uid_index == -1 ||
+           data->object_index == -1 ||
+           data->extra_index == -1) {
+               gint ii;
+
+               for (ii = 0; ii < ncols && (data->uid_index == -1 ||
+                    data->object_index == -1 ||
+                    data->extra_index == -1); ii++) {
+                       const gchar *cname = column_names[ii];
+
+                       if (!cname)
+                               continue;
+
+                       if (g_str_has_prefix (cname, "summary."))
+                               cname += 8;
+
+                       if (data->uid_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_UID) == 0) {
+                               data->uid_index = ii;
+                       } else if (data->object_index == -1 && g_ascii_strcasecmp (cname, 
E_CACHE_COLUMN_OBJECT) == 0) {
+                               data->object_index = ii;
+                       } else if (data->extra_index == -1 && g_ascii_strcasecmp (cname, EBC_COLUMN_EXTRA) == 
0) {
+                               data->extra_index = ii;
+                       }
+               }
+
+               if (data->object_index == -1)
+                       data->object_index = -2;
+
+               if (data->extra_index == -1)
+                       data->extra_index = -2;
+       }
+
+       g_return_val_if_fail (data->uid_index >= 0 && data->uid_index < ncols, FALSE);
+
+       if (data->object_index != -2) {
+               g_return_val_if_fail (data->object_index >= 0 && data->object_index < ncols, FALSE);
+               object = column_values[data->object_index];
+       }
+
+       if (data->extra_index != -2) {
+               g_return_val_if_fail (data->extra_index >= 0 && data->extra_index < ncols, FALSE);
+               extra = column_values[data->extra_index];
+       }
+
+       if (data->collect_results) {
+               EBookCacheSearchData *search_data;
+
+               search_data = e_book_cache_search_data_new (column_values[data->uid_index], object, extra);
+
+               data->results = g_slist_prepend (data->results, search_data);
+
+               data->last_vcard = search_data->vcard;
+       } else {
+               g_free (data->alloc_vcard);
+               data->alloc_vcard = g_strdup (object);
+
+               data->last_vcard = data->alloc_vcard;
+       }
+
+       data->n_results++;
+
+       return TRUE;
+}
+
+/**
+ * e_book_cache_cursor_step:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to use
+ * @flags: The #EBookCacheCursorStepFlags for this step
+ * @origin: The #EBookCacheCursorOrigin from whence to step
+ * @count: A positive or negative amount of contacts to try and fetch
+ * @out_results: (out) (nullable) (element-type EBookCacheSearchData) (transfer full):
+ *   A return location to store the results, or %NULL if %E_BOOK_CACHE_CURSOR_STEP_FETCH is not specified in 
@flags.
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Steps @cursor through its sorted query by a maximum of @count contacts
+ * starting from @origin.
+ *
+ * If @count is negative, then the cursor will move through the list in reverse.
+ *
+ * If @cursor reaches the beginning or end of the query results, then the
+ * returned list might not contain the amount of desired contacts, or might
+ * return no results if the cursor currently points to the last contact.
+ * Reaching the end of the list is not considered an error condition. Attempts
+ * to step beyond the end of the list after having reached the end of the list
+ * will however trigger an %E_CACHE_ERROR_END_OF_LIST error.
+ *
+ * If %E_BOOK_CACHE_CURSOR_STEP_FETCH is specified in @flags, a pointer to
+ * a %NULL #GSList pointer should be provided for the @out_results parameter.
+ *
+ * The result list will be stored to @out_results and should be freed
+ * with g_slist_free_full (results, e_book_cache_search_data_free);
+ * when no longer needed.
+ *
+ * Returns: The number of contacts traversed if successful, otherwise -1 is
+ *    returned and the @error is set.
+ *
+ * Since: 3.26
+ **/
+gint
+e_book_cache_cursor_step (EBookCache *book_cache,
+                         EBookCacheCursor *cursor,
+                         EBookCacheCursorStepFlags flags,
+                         EBookCacheCursorOrigin origin,
+                         gint count,
+                         GSList **out_results,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       CursorCollectData data = { -1, -1, -1, NULL, NULL, NULL, FALSE, 0 };
+       CursorState *state;
+       GString *query;
+       gboolean success;
+       EBookCacheCursorOrigin try_position;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), -1);
+       g_return_val_if_fail (cursor != NULL, -1);
+       g_return_val_if_fail ((flags & E_BOOK_CACHE_CURSOR_STEP_FETCH) == 0 ||
+                             (out_results != NULL), -1);
+
+       if (out_results)
+               *out_results = NULL;
+
+       e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return -1;
+       }
+
+       /* Check if this step should result in an end of list error first */
+       try_position = cursor->state.position;
+       if (origin != E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT)
+               try_position = origin;
+
+       /* Report errors for requests to run off the end of the list */
+       if (try_position == E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN && count < 0) {
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST,
+                       _("Tried to step a cursor in reverse, "
+                       "but cursor is already at the beginning of the contact list"));
+
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return -1;
+       } else if (try_position == E_BOOK_CACHE_CURSOR_ORIGIN_END && count > 0) {
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST,
+                       _("Tried to step a cursor forwards, "
+                       "but cursor is already at the end of the contact list"));
+
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return -1;
+       }
+
+       /* Nothing to do, silently return */
+       if (count == 0 && try_position == E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT) {
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return 0;
+       }
+
+       /* If we're not going to modify the position, just use
+        * a copy of the current cursor state.
+        */
+       if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) != 0)
+               state = &(cursor->state);
+       else
+               state = cursor_state_copy (cursor, &(cursor->state));
+
+       /* Every query starts with the STATE_CURRENT position, first
+        * fix up the cursor state according to 'origin'
+        */
+       switch (origin) {
+       case E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT:
+               /* Do nothing, normal operation */
+               break;
+
+       case E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN:
+       case E_BOOK_CACHE_CURSOR_ORIGIN_END:
+
+               /* Prepare the state before executing the query */
+               cursor_state_clear (cursor, state, origin);
+               break;
+       }
+
+       /* If count is 0 then there is no need to run any
+        * query, however it can be useful if you just want
+        * to move the cursor to the beginning or ending of
+        * the list.
+        */
+       if (count == 0) {
+               /* Free the state copy if need be */
+               if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) == 0)
+                       cursor_state_free (cursor, state);
+
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return 0;
+       }
+
+       query = g_string_new (cursor->select_vcards);
+
+       /* Add the filter constraints (if any) */
+       if (cursor->query) {
+               g_string_append (query, " WHERE ");
+
+               g_string_append_c (query, '(');
+               g_string_append (query, cursor->query);
+               g_string_append_c (query, ')');
+       }
+
+       /* Add the cursor constraints (if any) */
+       if (state->values[0] != NULL) {
+               gchar *constraints = NULL;
+
+               if (!cursor->query)
+                       g_string_append (query, " WHERE ");
+               else
+                       g_string_append (query, " AND ");
+
+               constraints = ebc_cursor_constraints (book_cache, cursor, state, count < 0, FALSE);
+
+               g_string_append_c (query, '(');
+               g_string_append (query, constraints);
+               g_string_append_c (query, ')');
+
+               g_free (constraints);
+       }
+
+       /* Add the sort order */
+       g_string_append_c (query, ' ');
+       if (count > 0)
+               g_string_append (query, cursor->order);
+       else
+               g_string_append (query, cursor->reverse_order);
+
+       /* Add the limit */
+       g_string_append_printf (query, " LIMIT %d", ABS (count));
+
+       /* Specify whether we really want results or not */
+       data.collect_results = (flags & E_BOOK_CACHE_CURSOR_STEP_FETCH) != 0;
+
+       /* Execute the query */
+       success = e_cache_sqlite_select (E_CACHE (book_cache), query->str,
+               ebc_collect_results_for_cursor_cb, &data,
+               cancellable, error);
+
+       /* Lock was obtained above */
+       e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+       g_string_free (query, TRUE);
+
+       /* If there was no error, update the internal cursor state */
+       if (success) {
+               if (data.n_results < ABS (count)) {
+                       /* We've reached the end, clear the current state */
+                       if (count < 0)
+                               cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+                       else
+                               cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_END);
+
+               } else if (data.last_vcard) {
+                       /* Set the cursor state to the last result */
+                       cursor_state_set_from_vcard (book_cache, cursor, state, data.last_vcard);
+               } else {
+                       /* Should never get here */
+                       g_warn_if_reached ();
+               }
+
+               /* Assign the results to return (if any) */
+               if (out_results) {
+                       /* Correct the order of results at the last minute */
+                       *out_results = g_slist_reverse (data.results);
+                       data.results = NULL;
+               }
+       }
+
+       /* Cleanup what was allocated by collect_results_for_cursor_cb() */
+       if (data.results)
+               g_slist_free_full (data.results, e_book_cache_search_data_free);
+       g_free (data.alloc_vcard);
+
+       /* Free the copy state if we were working with a copy */
+       if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) == 0)
+               cursor_state_free (cursor, state);
+
+       if (success)
+               return data.n_results;
+
+       return -1;
+}
+
+/**
+ * e_book_cache_cursor_set_target_alphabetic_index:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to modify
+ * @idx: The alphabetic index
+ *
+ * Sets the @cursor position to an
+ * <link linkend="cursor-alphabet">Alphabetic Index</link>
+ * into the alphabet active in @book_cache's locale.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_book_cache_cursor_step()
+ * will return results starting with the letter 'E' (or results starting
+ * with the last result in 'D', if moving in a negative direction).
+ *
+ * The passed index must be a valid index in the active locale, knowledge
+ * on the currently active alphabet index must be obtained using #ECollator
+ * APIs.
+ *
+ * Use e_book_cahce_ref_collator() to obtain the active collator for @book_cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_cursor_set_target_alphabetic_index (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor,
+                                                gint idx)
+{
+       gint n_labels = 0;
+
+       g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+       g_return_if_fail (cursor != NULL);
+       g_return_if_fail (idx >= 0);
+
+       e_collator_get_index_labels (book_cache->priv->collator, &n_labels, NULL, NULL, NULL);
+       g_return_if_fail (idx < n_labels);
+
+       cursor_state_clear (cursor, &(cursor->state), E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT);
+       if (cursor->n_sort_fields > 0) {
+               SummaryField *field;
+               gchar *index_key;
+
+               index_key = e_collator_generate_key_for_index (book_cache->priv->collator, idx);
+               field = summary_field_get (book_cache, cursor->sort_fields[0]);
+
+               if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+                       cursor->state.values[0] = index_key;
+               } else {
+                       cursor->state.values[0] = ebc_encode_vcard_sort_key (index_key);
+                       g_free (index_key);
+               }
+       }
+}
+
+/**
+ * e_book_cache_cursor_set_sexp:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to modify
+ * @sexp: The new query expression for @cursor
+ * @error: return location for a #GError, or %NULL
+ *
+ * Modifies the current query expression for @cursor. This will not
+ * modify @cursor's state, but will change the outcome of any further
+ * calls to e_book_cache_cursor_step() or e_book_cache_cursor_calculate().
+ *
+ * Returns: %TRUE if the expression was valid and accepted by @cursor
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_cursor_set_sexp (EBookCache *book_cache,
+                             EBookCacheCursor *cursor,
+                             const gchar *sexp,
+                             GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (cursor != NULL, FALSE);
+
+       /* We don't like '\0' sexps, prefer NULL */
+       if (sexp && !*sexp)
+               sexp = NULL;
+
+       e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+       success = ebc_cursor_setup_query (book_cache, cursor, sexp, error);
+
+       e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+       return success;
+}
+
+/**
+ * e_book_cache_cursor_calculate:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor
+ * @out_total: (out) (nullable): A return location to store the total result set for this cursor
+ * @out_position: (out) (nullable): A return location to store the cursor position
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calculates the @out_total amount of results for the @cursor's query expression,
+ * as well as the current @out_position of @cursor in the results. The @out_position is
+ * represented as the amount of results which lead up to the current value
+ * of @cursor, if @cursor currently points to an exact contact, the position
+ * also includes the cursor contact.
+ *
+ * Returns: Whether @out_total and @out_position were successfully calculated.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_cursor_calculate (EBookCache *book_cache,
+                              EBookCacheCursor *cursor,
+                              gint *out_total,
+                              gint *out_position,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       gboolean success = TRUE;
+       gint local_total = 0;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+       g_return_val_if_fail (cursor != NULL, FALSE);
+
+       /* If we're in a clear cursor state, then the position is 0 */
+       if (out_position && cursor->state.values[0] == NULL) {
+               if (cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN) {
+                       /* Mark the local pointer NULL, no need to calculate this anymore */
+                       *out_position = 0;
+                       out_position = NULL;
+               } else if (cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_END) {
+                       /* Make sure that we look up the total so we can
+                        * set the position to 'total + 1'
+                        */
+                       if (!out_total)
+                               out_total = &local_total;
+               }
+       }
+
+       /* Early return if there is nothing to do */
+       if (!out_total && !out_position)
+               return TRUE;
+
+       e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+               return FALSE;
+       }
+
+       if (out_total)
+               success = cursor_count_total_locked (book_cache, cursor, out_total, cancellable, error);
+
+       if (success && out_position)
+               success = cursor_count_position_locked (book_cache, cursor, out_position, cancellable, error);
+
+       e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+       /* In the case we're at the end, we just set the position
+        * to be the total + 1
+        */
+       if (success && out_position && out_total &&
+           cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_END)
+               *out_position = *out_total + 1;
+
+       return success;
+}
+
+/**
+ * e_book_cache_cursor_compare_contact:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor
+ * @contact: The #EContact to compare
+ * @out_matches_sexp: (out) (nullable): Whether the contact matches the cursor's search expression
+ *
+ * Compares @contact with @cursor and returns whether @contact is less than, equal to, or greater
+ * than @cursor.
+ *
+ * Returns: A value that is less than, equal to, or greater than zero if @contact is found,
+ *    respectively, to be less than, to match, or be greater than the current value of @cursor.
+ *
+ * Since: 3.26
+ **/
+gint
+e_book_cache_cursor_compare_contact (EBookCache *book_cache,
+                                    EBookCacheCursor *cursor,
+                                    EContact *contact,
+                                    gboolean *out_matches_sexp)
+{
+       gint ii;
+       gint comparison = 0;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), -1);
+       g_return_val_if_fail (cursor != NULL, -1);
+       g_return_val_if_fail (E_IS_CONTACT (contact), -1);
+
+       if (out_matches_sexp) {
+               if (!cursor->sexp)
+                       *out_matches_sexp = TRUE;
+               else
+                       *out_matches_sexp = e_book_backend_sexp_match_contact (cursor->sexp, contact);
+       }
+
+       for (ii = 0; ii < cursor->n_sort_fields && comparison == 0; ii++) {
+               SummaryField *field;
+               gchar *contact_key = NULL;
+               const gchar *cursor_key = NULL;
+               const gchar *field_value;
+               gchar *freeme = NULL;
+
+               field_value = e_contact_get_const (contact, cursor->sort_fields[ii]);
+               if (field_value)
+                       contact_key = e_collator_generate_key (book_cache->priv->collator, field_value, NULL);
+
+               field = summary_field_get (book_cache, cursor->sort_fields[ii]);
+
+               if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+                       cursor_key = cursor->state.values[ii];
+               } else {
+
+                       if (cursor->state.values[ii])
+                               freeme = ebc_decode_vcard_sort_key (cursor->state.values[ii]);
+
+                       cursor_key = freeme;
+               }
+
+               /* Empty state sorts below any contact value, which means the contact sorts above cursor */
+               if (cursor_key == NULL)
+                       comparison = 1;
+               else
+                       /* Check if contact sorts below, equal to, or above the cursor */
+                       comparison = g_strcmp0 (contact_key, cursor_key);
+
+               g_free (contact_key);
+               g_free (freeme);
+       }
+
+       /* UID tie-breaker */
+       if (comparison == 0) {
+               const gchar *uid;
+
+               uid = e_contact_get_const (contact, E_CONTACT_UID);
+
+               if (cursor->state.last_uid == NULL)
+                       comparison = 1;
+               else if (uid == NULL)
+                       comparison = -1;
+               else
+                       comparison = strcmp (uid, cursor->state.last_uid);
+       }
+
+       return comparison;
+}
+
+static gchar *
+ebc_dup_contact_revision (EBookCache *book_cache,
+                         EContact *contact)
+{
+       g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+
+       return e_contact_get (contact, E_CONTACT_REV);
+}
+
+static gboolean
+e_book_cache_put_locked (ECache *cache,
+                        const gchar *uid,
+                        const gchar *revision,
+                        const gchar *object,
+                        ECacheColumnValues *other_columns,
+                        EOfflineState offline_state,
+                        gboolean is_replace,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       EBookCache *book_cache;
+       EContact *contact;
+       gchar *updated_vcard = NULL;
+       gboolean e164_changed;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->put_locked != NULL, FALSE);
+
+       book_cache = E_BOOK_CACHE (cache);
+
+       contact = e_contact_new_from_vcard_with_uid (object, uid);
+
+       /* Update E.164 parameters in vcard if needed */
+       e164_changed = update_e164_attribute_params (book_cache, contact, book_cache->priv->region_code);
+
+       if (e164_changed) {
+               updated_vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+               object = updated_vcard;
+       }
+
+       success = E_CACHE_CLASS (e_book_cache_parent_class)->put_locked (cache, uid, revision, object, 
other_columns, offline_state,
+               is_replace, cancellable, error);
+
+       success = success && ebc_update_aux_tables (cache, uid, revision, object, cancellable, error);
+
+       if (success && e164_changed)
+               g_signal_emit (book_cache, signals[E164_CHANGED], 0, contact, is_replace);
+
+       g_clear_object (&contact);
+       g_free (updated_vcard);
+
+       return success;
+}
+
+static gboolean
+e_book_cache_remove_locked (ECache *cache,
+                           const gchar *uid,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->remove_locked != NULL, FALSE);
+
+       success = ebc_delete_from_aux_tables (cache, uid, cancellable, error);
+
+       success = success && E_CACHE_CLASS (e_book_cache_parent_class)->remove_locked (cache, uid, 
cancellable, error);
+
+       return success;
+}
+
+static gboolean
+e_book_cache_remove_all_locked (ECache *cache,
+                               const GSList *uids,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->remove_all_locked != NULL, FALSE);
+
+       success = ebc_empty_aux_tables (cache, cancellable, error);
+
+       success = success && E_CACHE_CLASS (e_book_cache_parent_class)->remove_all_locked (cache, uids, 
cancellable, error);
+
+       return success;
+}
+
+static gboolean
+e_book_cache_clear_offline_changes_locked (ECache *cache,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+       g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->clear_offline_changes_locked != 
NULL, FALSE);
+
+       /* First check whether there are any locally deleted objects at all */
+       if (e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, cancellable, error) >
+           e_cache_get_count (cache, E_CACHE_EXCLUDE_DELETED, cancellable, error))
+               success = ebc_delete_from_aux_tables_offline_deleted (cache, cancellable, error);
+       else
+               success = TRUE;
+
+       success = success && E_CACHE_CLASS (e_book_cache_parent_class)->clear_offline_changes_locked (cache, 
cancellable, error);
+
+       return success;
+}
+
+static void
+e_book_cache_get_property (GObject *object,
+                          guint property_id,
+                          GValue *value,
+                          GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_LOCALE:
+                       g_value_take_string (
+                               value,
+                               e_book_cache_dup_locale (E_BOOK_CACHE (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_cache_finalize (GObject *object)
+{
+       EBookCache *book_cache = E_BOOK_CACHE (object);
+
+       g_clear_object (&book_cache->priv->source);
+
+       if (book_cache->priv->collator) {
+               e_collator_unref (book_cache->priv->collator);
+               book_cache->priv->collator = NULL;
+       }
+
+       g_free (book_cache->priv->locale);
+       g_free (book_cache->priv->region_code);
+
+       if (book_cache->priv->summary_fields) {
+               summary_fields_array_free (book_cache->priv->summary_fields, 
book_cache->priv->n_summary_fields);
+               book_cache->priv->summary_fields = NULL;
+       }
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_cache_parent_class)->finalize (object);
+}
+
+static void
+e_book_cache_class_init (EBookCacheClass *klass)
+{
+       GObjectClass *object_class;
+       ECacheClass *cache_class;
+
+       g_type_class_add_private (klass, sizeof (EBookCachePrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->get_property = e_book_cache_get_property;
+       object_class->finalize = e_book_cache_finalize;
+
+       cache_class = E_CACHE_CLASS (klass);
+       cache_class->put_locked = e_book_cache_put_locked;
+       cache_class->remove_locked = e_book_cache_remove_locked;
+       cache_class->remove_all_locked = e_book_cache_remove_all_locked;
+       cache_class->clear_offline_changes_locked = e_book_cache_clear_offline_changes_locked;
+
+       klass->dup_contact_revision = ebc_dup_contact_revision;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_LOCALE,
+               g_param_spec_string (
+                       "locale",
+                       "Locate",
+                       "The locale currently being used",
+                       NULL,
+                       G_PARAM_READABLE |
+                       G_PARAM_STATIC_STRINGS));
+
+       signals[E164_CHANGED] = g_signal_new (
+               "e164-changed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (EBookCacheClass, e164_changed),
+               NULL,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_NONE, 2,
+               E_TYPE_CONTACT,
+               G_TYPE_BOOLEAN);
+
+       /**
+        * @EBookCache:dup-contact-revision:
+        * A signal being called to get revision of an EContact.
+        * The default implementation returns E_CONTACT_REV field value.
+        **/
+       signals[DUP_CONTACT_REVISION] = g_signal_new (
+               "dup-contact-revision",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+               G_STRUCT_OFFSET (EBookCacheClass, dup_contact_revision),
+               g_signal_accumulator_first_wins,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_STRING, 1,
+               E_TYPE_CONTACT);
+}
+
+static void
+e_book_cache_init (EBookCache *book_cache)
+{
+       book_cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (book_cache, E_TYPE_BOOK_CACHE, EBookCachePrivate);
+}
diff --git a/src/addressbook/libedata-book/e-book-cache.h b/src/addressbook/libedata-book/e-book-cache.h
new file mode 100644
index 0000000..5e13799
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-cache.h
@@ -0,0 +1,323 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_CACHE_H
+#define E_BOOK_CACHE_H
+
+#include <libebackend/libebackend.h>
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_CACHE \
+       (e_book_cache_get_type ())
+#define E_BOOK_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_BOOK_CACHE, EBookCache))
+#define E_BOOK_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_BOOK_CACHE, EBookCacheClass))
+#define E_IS_BOOK_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_BOOK_CACHE))
+#define E_IS_BOOK_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_BOOK_CACHE))
+#define E_BOOK_CACHE_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_BOOK_CACHE, EBookCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookCache EBookCache;
+typedef struct _EBookCacheClass EBookCacheClass;
+typedef struct _EBookCachePrivate EBookCachePrivate;
+
+/**
+ * EBookCacheSearchData:
+ * @uid: The %E_CONTACT_UID field of this contact
+ * @vcard: The vcard string
+ * @extra: Any extra data associated with the vcard
+ *
+ * This structure is used to represent contacts returned
+ * by the #EBookCache from various functions
+ * such as e_book_cache_search().
+ *
+ * The @extra parameter will contain any data which was
+ * previously passed for this contact in e_book_cache_put_contact()
+ * or set with e_book_cache_set_contact_extra().
+ *
+ * These should be freed with e_book_cache_search_data_free().
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+       gchar *uid;
+       gchar *vcard;
+       gchar *extra;
+} EBookCacheSearchData;
+
+#define E_TYPE_BOOK_CACHE_SEARCH_DATA (e_book_cache_search_data_get_type ())
+
+GType          e_book_cache_search_data_get_type
+                                               (void) G_GNUC_CONST;
+EBookCacheSearchData *
+               e_book_cache_search_data_new    (const gchar *uid,
+                                                const gchar *vcard,
+                                                const gchar *extra);
+EBookCacheSearchData *
+               e_book_cache_search_data_copy   (const EBookCacheSearchData *data);
+void           e_book_cache_search_data_free   (/* EBookCacheSearchData * */ gpointer data);
+
+/**
+ * EBookCacheSearchFunc:
+ * @book_cache: an #EBookCache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @extra: extra data stored with the object
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @user_data: user data, as used in e_book_cache_search_with_callback()
+ *
+ * A callback called for each object row when using
+ * e_book_cache_search_with_callback() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* EBookCacheSearchFunc)      (EBookCache *book_cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const gchar *extra,
+                                                EOfflineState offline_state,
+                                                gpointer user_data);
+
+/**
+ * EBookCache:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EBookCache {
+       /*< private >*/
+       ECache parent;
+       EBookCachePrivate *priv;
+};
+
+/**
+ * EBookCacheClass:
+ *
+ * Class structure for the #EBookCache class.
+ *
+ * Since: 3.26
+ */
+struct _EBookCacheClass {
+       /*< private >*/
+       ECacheClass parent_class;
+
+       /* Signals */
+       void            (* e164_changed)        (EBookCache *book_cache,
+                                                EContact *contact,
+                                                gboolean is_replace);
+
+       gchar *         (* dup_contact_revision)
+                                               (EBookCache *book_cache,
+                                                EContact *contact);
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+/**
+ * EBookCacheCursor:
+ *
+ * An opaque cursor pointer
+ *
+ * Since: 3.26
+ */
+typedef struct _EBookCacheCursor EBookCacheCursor;
+
+/**
+ * EBookCacheCursorOrigin:
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT: The current cursor position.
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN: The beginning of the cursor results.
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_END: The end of the cursor results.
+ *
+ * Specifies the start position to in the list of traversed contacts
+ * in calls to e_book_cache_cursor_step().
+ *
+ * When an #EBookCacheCursor is created, the current position implied by %E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT
+ * is the same as %E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN.
+ *
+ * Since: 3.26
+ */
+typedef enum {
+       E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT = 0,
+       E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+       E_BOOK_CACHE_CURSOR_ORIGIN_END
+} EBookCacheCursorOrigin;
+
+/**
+ * EBookCacheCursorStepFlags:
+ * @E_BOOK_CACHE_CURSOR_STEP_MOVE: The cursor position should be modified while stepping.
+ * @E_BOOK_CACHE_CURSOR_STEP_FETCH: Traversed contacts should be listed and returned while stepping.
+ *
+ * Defines the behaviour of e_book_cache_cursor_step().
+ *
+ * Since: 3.26
+ */
+typedef enum {
+       E_BOOK_CACHE_CURSOR_STEP_MOVE = (1 << 0),
+       E_BOOK_CACHE_CURSOR_STEP_FETCH = (1 << 1)
+} EBookCacheCursorStepFlags;
+
+GType          e_book_cache_get_type           (void) G_GNUC_CONST;
+
+EBookCache *   e_book_cache_new                (const gchar *filename,
+                                                ESource *source,
+                                                GCancellable *cancellable,
+                                                GError **error);
+EBookCache *   e_book_cache_new_full           (const gchar *filename,
+                                                ESource *source,
+                                                ESourceBackendSummarySetup *setup,
+                                                GCancellable *cancellable,
+                                                GError **error);
+ESource *      e_book_cache_ref_source         (EBookCache *book_cache);
+gchar *                e_book_cache_dup_contact_revision
+                                               (EBookCache *book_cache,
+                                                EContact *contact);
+gboolean       e_book_cache_set_locale         (EBookCache *book_cache,
+                                                const gchar *lc_collate,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gchar *                e_book_cache_dup_locale         (EBookCache *book_cache);
+
+ECollator *    e_book_cache_ref_collator       (EBookCache *book_cache);
+
+/* Adding / Removing / Searching contacts */
+gboolean       e_book_cache_put_contact        (EBookCache *book_cache,
+                                                EContact *contact,
+                                                const gchar *extra,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_put_contacts       (EBookCache *book_cache,
+                                                const GSList *contacts,
+                                                const GSList *extras,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_remove_contact     (EBookCache *book_cache,
+                                                const gchar *uid,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_remove_contacts    (EBookCache *book_cache,
+                                                const GSList *uids,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_get_contact        (EBookCache *book_cache,
+                                                const gchar *uid,
+                                                gboolean meta_contact,
+                                                EContact **out_contact,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_get_vcard          (EBookCache *book_cache,
+                                                const gchar *uid,
+                                                gboolean meta_contact,
+                                                gchar **out_vcard,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_set_contact_extra  (EBookCache *book_cache,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_get_contact_extra  (EBookCache *book_cache,
+                                                const gchar *uid,
+                                                gchar **out_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_search             (EBookCache *book_cache,
+                                                const gchar *sexp,
+                                                gboolean meta_contacts,
+                                                GSList **out_list, /* EBookCacheSearchData * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_search_uids        (EBookCache *book_cache,
+                                                const gchar *sexp,
+                                                GSList **out_list, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_cache_search_with_callback
+                                               (EBookCache *book_cache,
+                                                const gchar *sexp,
+                                                EBookCacheSearchFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+/* Cursor API */
+EBookCacheCursor *
+               e_book_cache_cursor_new         (EBookCache *book_cache,
+                                                const gchar *sexp,
+                                                const EContactField *sort_fields,
+                                                const EBookCursorSortType *sort_types,
+                                                guint n_sort_fields,
+                                                GError **error);
+void           e_book_cache_cursor_free        (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor);
+gint           e_book_cache_cursor_step        (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor,
+                                                EBookCacheCursorStepFlags flags,
+                                                EBookCacheCursorOrigin origin,
+                                                gint count,
+                                                GSList **out_results,
+                                                GCancellable *cancellable,
+                                                GError **error);
+void           e_book_cache_cursor_set_target_alphabetic_index
+                                               (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor,
+                                                gint idx);
+gboolean       e_book_cache_cursor_set_sexp    (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor,
+                                                const gchar *sexp,
+                                                GError **error);
+gboolean       e_book_cache_cursor_calculate   (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor,
+                                                gint *out_total,
+                                                gint *out_position,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gint           e_book_cache_cursor_compare_contact
+                                               (EBookCache *book_cache,
+                                                EBookCacheCursor *cursor,
+                                                EContact *contact,
+                                                gboolean *out_matches_sexp);
+
+G_END_DECLS
+
+#endif /* E_BOOK_CACHE_H */
diff --git a/src/addressbook/libedata-book/e-book-meta-backend.c 
b/src/addressbook/libedata-book/e-book-meta-backend.c
new file mode 100644
index 0000000..bde296b
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-meta-backend.c
@@ -0,0 +1,3767 @@
+/* -*- 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-book-meta-backend
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An #EBookBackend descendant for book backends
+ *
+ * The #EBookMetaBackend is an abstract #EBookBackend descendant which
+ * aims to implement all evolution-data-server internals for the backend
+ * itself and lefts the backend do as minimum work as possible, like
+ * loading and saving contacts, listing available contacts and so on,
+ * thus the backend implementation can focus on things like converting
+ * (possibly) remote data into vCard objects and back.
+ *
+ * As the #EBookMetaBackend uses an #EBookCache, the offline support
+ * is provided by default.
+ *
+ * The structure is thread safe.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include "e-book-backend-sexp.h"
+#include "e-book-backend.h"
+#include "e-data-book-cursor-cache.h"
+#include "e-data-book-factory.h"
+
+#include "e-book-meta-backend.h"
+
+#define EBMB_KEY_SYNC_TAG              "ebmb::sync-tag"
+#define EBMB_KEY_EVER_CONNECTED                "ebmb::ever-connected"
+#define EBMB_KEY_CONNECTED_WRITABLE    "ebmb::connected-writable"
+
+#define LOCAL_PREFIX "file://"
+
+struct _EBookMetaBackendPrivate {
+       GMutex connect_lock;
+       GMutex property_lock;
+       GError *create_cache_error;
+       EBookCache *cache;
+       ENamedParameters *last_credentials;
+       GHashTable *view_cancellables;
+       GCancellable *refresh_cancellable;      /* Set when refreshing the content */
+       GCancellable *source_changed_cancellable; /* Set when processing source changed signal */
+       GCancellable *go_offline_cancellable;   /* Set when going offline */
+       gboolean current_online_state;          /* The only state of the internal structures;
+                                                  used to detect false notifications on EBackend::online */
+       gulong source_changed_id;
+       gulong notify_online_id;
+       gulong revision_changed_id;
+       guint refresh_timeout_id;
+
+       gboolean refresh_after_authenticate;
+       gint ever_connected;
+       gint connected_writable;
+
+       /* Last successful connect data, for some extensions */
+       guint16 authentication_port;
+       gchar *authentication_host;
+       gchar *authentication_user;
+       gchar *authentication_method;
+       gchar *authentication_proxy_uid;
+       gchar *authentication_credential_name;
+       SoupURI *webdav_soup_uri;
+
+       GSList *cursors;
+};
+
+enum {
+       PROP_0,
+       PROP_CACHE
+};
+
+enum {
+       REFRESH_COMPLETED,
+       SOURCE_CHANGED,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_ABSTRACT_TYPE (EBookMetaBackend, e_book_meta_backend, E_TYPE_BOOK_BACKEND)
+
+G_DEFINE_BOXED_TYPE (EBookMetaBackendInfo, e_book_meta_backend_info, e_book_meta_backend_info_copy, 
e_book_meta_backend_info_free)
+
+static void ebmb_schedule_refresh (EBookMetaBackend *meta_backend);
+static void ebmb_schedule_source_changed (EBookMetaBackend *meta_backend);
+static void ebmb_schedule_go_offline (EBookMetaBackend *meta_backend);
+static gboolean ebmb_load_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+                                               EBookCache *book_cache,
+                                               const gchar *uid,
+                                               const gchar *preloaded_object,
+                                               const gchar *preloaded_extra,
+                                               gchar **out_new_uid,
+                                               GCancellable *cancellable,
+                                               GError **error);
+static gboolean ebmb_save_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+                                               EBookCache *book_cache,
+                                               gboolean overwrite_existing,
+                                               EConflictResolution conflict_resolution,
+                                               /* const */ EContact *in_contact,
+                                               const gchar *extra,
+                                               const gchar *orig_uid,
+                                               gboolean *out_requires_put,
+                                               gchar **out_new_uid,
+                                               gchar **out_new_extra,
+                                               GCancellable *cancellable,
+                                               GError **error);
+
+/**
+ * e_book_meta_backend_info_new:
+ * @uid: a contact UID; cannot be %NULL
+ * @revision: (nullable): the contact revision; can be %NULL
+ * @object: (nullable): the contact object as a vCard string; can be %NULL
+ * @extra: (nullable): extra backend-specific data; can be %NULL
+ *
+ * Creates a new #EBookMetaBackendInfo prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #EBookMetaBackendInfo. Free it with
+ *    e_book_meta_backend_info_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookMetaBackendInfo *
+e_book_meta_backend_info_new (const gchar *uid,
+                             const gchar *revision,
+                             const gchar *object,
+                             const gchar *extra)
+{
+       EBookMetaBackendInfo *info;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       info = g_new0 (EBookMetaBackendInfo, 1);
+       info->uid = g_strdup (uid);
+       info->revision = g_strdup (revision);
+       info->object = g_strdup (object);
+       info->extra = g_strdup (extra);
+
+       return info;
+}
+
+/**
+ * e_book_meta_backend_info_copy:
+ * @src: (nullable): a source EBookMetaBackendInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @src. Free it with
+ *    e_book_meta_backend_info_free() when no longer needed.
+ *    If the @src is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+EBookMetaBackendInfo *
+e_book_meta_backend_info_copy (const EBookMetaBackendInfo *src)
+{
+       if (!src)
+               return NULL;
+
+       return e_book_meta_backend_info_new (src->uid, src->revision, src->object, src->extra);
+}
+
+/**
+ * e_book_meta_backend_info_free:
+ * @ptr: (nullable): an #EBookMetaBackendInfo
+ *
+ * Frees the @ptr structure, previously allocated with e_book_meta_backend_info_new()
+ * or e_book_meta_backend_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_info_free (gpointer ptr)
+{
+       EBookMetaBackendInfo *info = ptr;
+
+       if (info) {
+               g_free (info->uid);
+               g_free (info->revision);
+               g_free (info->object);
+               g_free (info->extra);
+               g_free (info);
+       }
+}
+
+/* Unref returned cancellable with g_object_unref(), when done with it */
+static GCancellable *
+ebmb_create_view_cancellable (EBookMetaBackend *meta_backend,
+                             EDataBookView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       cancellable = g_cancellable_new ();
+       g_hash_table_insert (meta_backend->priv->view_cancellables, view, g_object_ref (cancellable));
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return cancellable;
+}
+
+static GCancellable *
+ebmb_steal_view_cancellable (EBookMetaBackend *meta_backend,
+                            EDataBookView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       cancellable = g_hash_table_lookup (meta_backend->priv->view_cancellables, view);
+       if (cancellable) {
+               g_object_ref (cancellable);
+               g_hash_table_remove (meta_backend->priv->view_cancellables, view);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return cancellable;
+}
+
+static void
+ebmb_update_connection_values (EBookMetaBackend *meta_backend)
+{
+       ESource *source;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       meta_backend->priv->authentication_port = 0;
+       g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+       g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+       if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *auth_extension;
+
+               auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+               meta_backend->priv->authentication_port = e_source_authentication_get_port (auth_extension);
+               meta_backend->priv->authentication_host = e_source_authentication_dup_host (auth_extension);
+               meta_backend->priv->authentication_user = e_source_authentication_dup_user (auth_extension);
+               meta_backend->priv->authentication_method = e_source_authentication_dup_method 
(auth_extension);
+               meta_backend->priv->authentication_proxy_uid = e_source_authentication_dup_proxy_uid 
(auth_extension);
+               meta_backend->priv->authentication_credential_name = 
e_source_authentication_dup_credential_name (auth_extension);
+       }
+
+       if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               ESourceWebdav *webdav_extension;
+
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+
+               meta_backend->priv->webdav_soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_book_meta_backend_set_ever_connected (meta_backend, TRUE);
+       e_book_meta_backend_set_connected_writable (meta_backend, e_book_backend_get_writable (E_BOOK_BACKEND 
(meta_backend)));
+}
+
+static gboolean
+ebmb_connect_wrapper_sync (EBookMetaBackend *meta_backend,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       ENamedParameters *credentials;
+       ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+       ESourceCredentialsReason creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+       gchar *certificate_pem = NULL;
+       GTlsCertificateFlags certificate_errors = 0;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+                       e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+               return FALSE;
+       }
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+       credentials = e_named_parameters_new_clone (meta_backend->priv->last_credentials);
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       g_mutex_lock (&meta_backend->priv->connect_lock);
+       if (e_book_meta_backend_connect_sync (meta_backend, credentials, &auth_result, &certificate_pem, 
&certificate_errors,
+               cancellable, &local_error)) {
+               ebmb_update_connection_values (meta_backend);
+               g_mutex_unlock (&meta_backend->priv->connect_lock);
+               e_named_parameters_free (credentials);
+
+               return TRUE;
+       }
+
+       g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+       e_named_parameters_free (credentials);
+
+       g_warn_if_fail (auth_result != E_SOURCE_AUTHENTICATION_ACCEPTED);
+
+       switch (auth_result) {
+       case E_SOURCE_AUTHENTICATION_UNKNOWN:
+               if (local_error)
+                       g_propagate_error (error, local_error);
+               g_free (certificate_pem);
+               return FALSE;
+       case E_SOURCE_AUTHENTICATION_ERROR:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+               break;
+       case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+               break;
+       case E_SOURCE_AUTHENTICATION_ACCEPTED:
+               g_warn_if_reached ();
+               break;
+       case E_SOURCE_AUTHENTICATION_REJECTED:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_REJECTED;
+               break;
+       case E_SOURCE_AUTHENTICATION_REQUIRED:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+               break;
+       }
+
+       e_backend_schedule_credentials_required (E_BACKEND (meta_backend), creds_reason, certificate_pem, 
certificate_errors,
+               local_error, cancellable, G_STRFUNC);
+
+       g_clear_error (&local_error);
+       g_free (certificate_pem);
+
+       return FALSE;
+}
+
+static gboolean
+ebmb_gather_locally_cached_objects_cb (EBookCache *book_cache,
+                                      const gchar *uid,
+                                      const gchar *revision,
+                                      const gchar *object,
+                                      const gchar *extra,
+                                      EOfflineState offline_state,
+                                      gpointer user_data)
+{
+       GHashTable *locally_cached = user_data;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (locally_cached != NULL, FALSE);
+
+       if (offline_state == E_OFFLINE_STATE_SYNCED) {
+               g_hash_table_insert (locally_cached,
+                       g_strdup (uid),
+                       g_strdup (revision));
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebmb_get_changes_sync (EBookMetaBackend *meta_backend,
+                      const gchar *last_sync_tag,
+                      gboolean is_repeat,
+                      gchar **out_new_sync_tag,
+                      gboolean *out_repeat,
+                      GSList **out_created_objects,
+                      GSList **out_modified_objects,
+                      GSList **out_removed_objects,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       GHashTable *locally_cached; /* EContactId * ~> gchar *revision */
+       GHashTableIter iter;
+       GSList *existing_objects = NULL, *link;
+       EBookCache *book_cache;
+       gpointer key, value;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_created_objects, FALSE);
+       g_return_val_if_fail (out_modified_objects, FALSE);
+       g_return_val_if_fail (out_removed_objects, FALSE);
+
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend)))
+               return TRUE;
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       if (!ebmb_connect_wrapper_sync (meta_backend, cancellable, error) ||
+           !e_book_meta_backend_list_existing_sync (meta_backend, out_new_sync_tag, &existing_objects, 
cancellable, error)) {
+               g_object_unref (book_cache);
+               return FALSE;
+       }
+
+       locally_cached = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+       g_warn_if_fail (e_book_cache_search_with_callback (book_cache, NULL,
+               ebmb_gather_locally_cached_objects_cb, locally_cached, cancellable, error));
+
+       for (link = existing_objects; link; link = g_slist_next (link)) {
+               EBookMetaBackendInfo *nfo = link->data;
+
+               if (!nfo)
+                       continue;
+
+               if (!g_hash_table_contains (locally_cached, nfo->uid)) {
+                       link->data = NULL;
+
+                       *out_created_objects = g_slist_prepend (*out_created_objects, nfo);
+               } else {
+                       const gchar *local_revision = g_hash_table_lookup (locally_cached, nfo->uid);
+
+                       if (g_strcmp0 (local_revision, nfo->revision) != 0) {
+                               link->data = NULL;
+
+                               *out_modified_objects = g_slist_prepend (*out_modified_objects, nfo);
+                       }
+
+                       g_hash_table_remove (locally_cached, nfo->uid);
+               }
+       }
+
+       /* What left in the hash table is removed from the remote side */
+       g_hash_table_iter_init (&iter, locally_cached);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               const gchar *uid = key;
+               const gchar *revision = value;
+               EBookMetaBackendInfo *nfo;
+
+               if (!uid) {
+                       g_warn_if_reached ();
+                       continue;
+               }
+
+               nfo = e_book_meta_backend_info_new (uid, revision, NULL, NULL);
+               *out_removed_objects = g_slist_prepend (*out_removed_objects, nfo);
+       }
+
+       g_slist_free_full (existing_objects, e_book_meta_backend_info_free);
+       g_hash_table_destroy (locally_cached);
+       g_object_unref (book_cache);
+
+       *out_created_objects = g_slist_reverse (*out_created_objects);
+       *out_modified_objects = g_slist_reverse (*out_modified_objects);
+       *out_removed_objects = g_slist_reverse (*out_removed_objects);
+
+       return TRUE;
+}
+
+static gboolean
+ebmb_search_sync (EBookMetaBackend *meta_backend,
+                 const gchar *expr,
+                 gboolean meta_contact,
+                 GSList **out_contacts,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       EBookCache *book_cache;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+       *out_contacts = NULL;
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       success = e_book_cache_search (book_cache, expr, meta_contact, out_contacts, cancellable, error);
+
+       if (success) {
+               GSList *link;
+
+               for (link = *out_contacts; link; link = g_slist_next (link)) {
+                       EBookCacheSearchData *search_data = link->data;
+                       EContact *contact = NULL;
+
+                       if (search_data) {
+                               contact = e_contact_new_from_vcard_with_uid (search_data->vcard, 
search_data->uid);
+                               e_book_cache_search_data_free (search_data);
+                       }
+
+                       link->data = contact;
+               }
+       }
+
+       g_object_unref (book_cache);
+
+       return success;
+}
+
+static gboolean
+ebmb_search_uids_sync (EBookMetaBackend *meta_backend,
+                      const gchar *expr,
+                      GSList **out_uids,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       EBookCache *book_cache;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_uids != NULL, FALSE);
+
+       *out_uids = NULL;
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       success = e_book_cache_search_uids (book_cache, expr, out_uids, cancellable, error);
+
+       g_object_unref (book_cache);
+
+       return success;
+}
+
+static gboolean
+ebmb_requires_reconnect (EBookMetaBackend *meta_backend)
+{
+       ESource *source;
+       gboolean requires = FALSE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+       g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *auth_extension;
+
+               auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+               e_source_extension_property_lock (E_SOURCE_EXTENSION (auth_extension));
+
+               requires = meta_backend->priv->authentication_port != e_source_authentication_get_port 
(auth_extension) ||
+                       g_strcmp0 (meta_backend->priv->authentication_host, e_source_authentication_get_host 
(auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_user, e_source_authentication_get_user 
(auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_method, 
e_source_authentication_get_method (auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_proxy_uid, 
e_source_authentication_get_proxy_uid (auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_credential_name, 
e_source_authentication_get_credential_name (auth_extension)) != 0;
+
+               e_source_extension_property_unlock (E_SOURCE_EXTENSION (auth_extension));
+       }
+
+       if (!requires && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               ESourceWebdav *webdav_extension;
+               SoupURI *soup_uri;
+
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+
+               requires = (!meta_backend->priv->webdav_soup_uri && soup_uri) ||
+                       (soup_uri && meta_backend->priv->webdav_soup_uri &&
+                       !soup_uri_equal (meta_backend->priv->webdav_soup_uri, soup_uri));
+
+               if (soup_uri)
+                       soup_uri_free (soup_uri);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return requires;
+}
+
+static GSList * /* gchar * */
+ebmb_gather_photos_local_filenames (EBookMetaBackend *meta_backend,
+                                   EContact *contact)
+{
+       EBookCache *book_cache;
+       GList *attributes, *link;
+       GSList *filenames = NULL;
+       gchar *cache_path;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, NULL);
+
+       cache_path = g_path_get_dirname (e_cache_get_filename (E_CACHE (book_cache)));
+
+       g_object_unref (book_cache);
+
+       attributes = e_vcard_get_attributes (E_VCARD (contact));
+
+       for (link = attributes; link; link = g_list_next (link)) {
+               EVCardAttribute *attr = link->data;
+               const gchar *attr_name;
+               GList *values;
+
+               attr_name = e_vcard_attribute_get_name (attr);
+               if (!attr_name || (
+                   g_ascii_strcasecmp (attr_name, EVC_PHOTO) != 0 &&
+                   g_ascii_strcasecmp (attr_name, EVC_LOGO) != 0)) {
+                       continue;
+               }
+
+               values = e_vcard_attribute_get_param (attr, EVC_VALUE);
+               if (values && g_ascii_strcasecmp (values->data, "uri") == 0) {
+                       const gchar *url;
+
+                       url = e_vcard_attribute_get_value (attr);
+                       if (url && g_str_has_prefix (url, LOCAL_PREFIX)) {
+                               gchar *filename;
+
+                               filename = g_filename_from_uri (url, NULL, NULL);
+                               if (filename && g_str_has_prefix (filename, cache_path))
+                                       filenames = g_slist_prepend (filenames, filename);
+                               else
+                                       g_free (filename);
+                       }
+               }
+       }
+
+       g_free (cache_path);
+
+       return filenames;
+}
+
+static void
+ebmb_start_view_thread_func (EBookBackend *book_backend,
+                            gpointer user_data,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       EDataBookView *view = user_data;
+       EBookBackendSExp *sexp;
+       GSList *contacts = NULL;
+       const gchar *expr = NULL;
+       gboolean meta_contact = FALSE;
+       GHashTable *fields_of_interest;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+       g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       /* Fill the view with known (locally stored) contacts satisfying the expression */
+       sexp = e_data_book_view_get_sexp (view);
+       if (sexp)
+               expr = e_book_backend_sexp_text (sexp);
+
+       fields_of_interest = e_data_book_view_get_fields_of_interest (view);
+       if (fields_of_interest && g_hash_table_size (fields_of_interest) == 2) {
+               GHashTableIter iter;
+               gpointer key, value;
+
+               meta_contact = TRUE;
+
+               g_hash_table_iter_init (&iter, fields_of_interest);
+               while (g_hash_table_iter_next (&iter, &key, &value)) {
+                       const gchar *field_name = key;
+                       EContactField field = e_contact_field_id (field_name);
+
+                       if (field != E_CONTACT_UID &&
+                           field != E_CONTACT_REV) {
+                               meta_contact = FALSE;
+                               break;
+                       }
+               }
+       }
+
+       if (e_book_meta_backend_search_sync (E_BOOK_META_BACKEND (book_backend), expr, meta_contact, 
&contacts, cancellable, &local_error) && contacts) {
+               if (!g_cancellable_is_cancelled (cancellable)) {
+                       GSList *link;
+
+                       for (link = contacts; link; link = g_slist_next (link)) {
+                               EContact *contact = link->data;
+                               gchar *vcard;
+
+                               if (!contact)
+                                       continue;
+
+                               vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+                               e_data_book_view_notify_update_prefiltered_vcard (view,
+                                       e_contact_get_const (contact, E_CONTACT_UID),
+                                       vcard);
+                       }
+               }
+
+               g_slist_free_full (contacts, g_object_unref);
+       }
+
+       e_data_book_view_notify_complete (view, local_error);
+
+       g_clear_error (&local_error);
+}
+
+static gboolean
+ebmb_upload_local_changes_sync (EBookMetaBackend *meta_backend,
+                               EBookCache *book_cache,
+                               EConflictResolution conflict_resolution,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       GSList *offline_changes, *link;
+       GHashTable *covered_uids;
+       ECache *cache;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+
+       cache = E_CACHE (book_cache);
+       covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+       offline_changes = e_cache_get_offline_changes (cache, cancellable, error);
+       for (link = offline_changes; link && success; link = g_slist_next (link)) {
+               ECacheOfflineChange *change = link->data;
+               gchar *extra = NULL;
+
+               success = !g_cancellable_set_error_if_cancelled (cancellable, error);
+               if (!success)
+                       break;
+
+               if (!change || g_hash_table_contains (covered_uids, change->uid))
+                       continue;
+
+               g_hash_table_insert (covered_uids, change->uid, NULL);
+
+               if (!e_book_cache_get_contact_extra (book_cache, change->uid, &extra, cancellable, NULL))
+                       extra = NULL;
+
+               if (change->state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+                   change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED) {
+                       EContact *contact = NULL;
+
+                       success = e_book_cache_get_contact (book_cache, change->uid, FALSE, &contact, 
cancellable, error);
+                       if (success) {
+                               success = ebmb_save_contact_wrapper_sync (meta_backend, book_cache,
+                                       change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED,
+                                       conflict_resolution, contact, extra, change->uid, NULL, NULL, NULL, 
cancellable, error);
+                       }
+
+                       g_clear_object (&contact);
+               } else if (change->state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+                       GError *local_error = NULL;
+
+                       success = e_book_meta_backend_remove_contact_sync (meta_backend, conflict_resolution,
+                               change->uid, extra, change->object, cancellable, &local_error);
+
+                       if (!success) {
+                               if (g_error_matches (local_error, E_DATA_BOOK_ERROR, 
E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND)) {
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+               } else {
+                       g_warn_if_reached ();
+               }
+
+               g_free (extra);
+       }
+
+       g_slist_free_full (offline_changes, e_cache_offline_change_free);
+       g_hash_table_destroy (covered_uids);
+
+       if (success)
+               success = e_cache_clear_offline_changes (cache, cancellable, error);
+
+       return success;
+}
+
+static void
+ebmb_foreach_cursor (EBookMetaBackend *meta_backend,
+                    EContact *contact,
+                    void (* func) (EDataBookCursor *cursor, EContact *contact))
+{
+       GSList *link;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+       g_return_if_fail (func != NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       for (link = meta_backend->priv->cursors; link; link = g_slist_next (link)) {
+               EDataBookCursor *cursor = link->data;
+
+               func (cursor, contact);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static gboolean
+ebmb_maybe_remove_from_cache (EBookMetaBackend *meta_backend,
+                             EBookCache *book_cache,
+                             ECacheOfflineFlag offline_flag,
+                             const gchar *uid,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       EBookBackend *book_backend;
+       EContact *contact = NULL;
+       GSList *local_photos, *link;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (!e_book_cache_get_contact (book_cache, uid, FALSE, &contact, cancellable, &local_error)) {
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       return TRUE;
+               }
+
+               g_propagate_error (error, local_error);
+               return FALSE;
+       }
+
+       book_backend = E_BOOK_BACKEND (meta_backend);
+
+       if (!e_book_cache_remove_contact (book_cache, uid, offline_flag, cancellable, error)) {
+               g_object_unref (contact);
+               return FALSE;
+       }
+
+       local_photos = ebmb_gather_photos_local_filenames (meta_backend, contact);
+       for (link = local_photos; link; link = g_slist_next (link)) {
+               const gchar *filename = link->data;
+
+               if (filename && g_unlink (filename) == -1) {
+                       /* Ignore these errors */
+               }
+       }
+
+       g_slist_free_full (local_photos, g_free);
+
+       e_book_backend_notify_remove (book_backend, uid);
+
+       ebmb_foreach_cursor (meta_backend, contact, e_data_book_cursor_contact_removed);
+
+       g_object_unref (contact);
+
+       return TRUE;
+}
+
+static void
+ebmb_refresh_thread_func (EBookBackend *book_backend,
+                         gpointer user_data,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       gboolean success, repeat = TRUE, is_repeat = FALSE;
+       GString *invalid_objects = NULL;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               goto done;
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend)) ||
+           !ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+               /* Ignore connection errors here */
+               g_mutex_lock (&meta_backend->priv->property_lock);
+               meta_backend->priv->refresh_after_authenticate = TRUE;
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               goto done;
+       }
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+       meta_backend->priv->refresh_after_authenticate = FALSE;
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       if (!book_cache) {
+               g_warn_if_reached ();
+               goto done;
+       }
+
+       success = ebmb_upload_local_changes_sync (meta_backend, book_cache, E_CONFLICT_RESOLUTION_FAIL, 
cancellable, error);
+
+       while (repeat && success &&
+              !g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               GSList *created_objects = NULL, *modified_objects = NULL, *removed_objects = NULL, *link;
+               gchar *last_sync_tag, *new_sync_tag = NULL;
+
+               repeat = FALSE;
+
+               last_sync_tag = e_cache_dup_key (E_CACHE (book_cache), EBMB_KEY_SYNC_TAG, NULL);
+               if (last_sync_tag && !*last_sync_tag) {
+                       g_free (last_sync_tag);
+                       last_sync_tag = NULL;
+               }
+
+               success = e_book_meta_backend_get_changes_sync (meta_backend, last_sync_tag, is_repeat, 
&new_sync_tag, &repeat,
+                       &created_objects, &modified_objects, &removed_objects, cancellable, error);
+
+               if (success) {
+                       GHashTable *covered_uids;
+
+                       covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+                       /* Removed objects first */
+                       for (link = removed_objects; link && success; link = g_slist_next (link)) {
+                               EBookMetaBackendInfo *nfo = link->data;
+
+                               if (!nfo) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               success = ebmb_maybe_remove_from_cache (meta_backend, book_cache, 
E_CACHE_IS_ONLINE, nfo->uid, cancellable, error);
+                       }
+
+                       /* Then modified objects */
+                       for (link = modified_objects; link && success; link = g_slist_next (link)) {
+                               EBookMetaBackendInfo *nfo = link->data;
+                               GError *local_error = NULL;
+
+                               if (!nfo || !nfo->uid) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               if (!*nfo->uid ||
+                                   g_hash_table_contains (covered_uids, nfo->uid))
+                                       continue;
+
+                               g_hash_table_insert (covered_uids, nfo->uid, NULL);
+
+                               success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, nfo->uid, 
nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+                               /* Do not stop on invalid objects, just notify about them later, and load as 
many as possible */
+                               if (!success && g_error_matches (local_error, E_DATA_BOOK_ERROR, 
E_DATA_BOOK_STATUS_INVALID_ARG)) {
+                                       if (!invalid_objects) {
+                                               invalid_objects = g_string_new (local_error->message);
+                                       } else {
+                                               g_string_append_c (invalid_objects, '\n');
+                                               g_string_append (invalid_objects, local_error->message);
+                                       }
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+
+                       g_hash_table_remove_all (covered_uids);
+
+                       /* Finally created objects */
+                       for (link = created_objects; link && success; link = g_slist_next (link)) {
+                               EBookMetaBackendInfo *nfo = link->data;
+                               GError *local_error = NULL;
+
+                               if (!nfo || !nfo->uid) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               if (!*nfo->uid)
+                                       continue;
+
+                               success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, nfo->uid, 
nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+                               /* Do not stop on invalid objects, just notify about them later, and load as 
many as possible */
+                               if (!success && g_error_matches (local_error, E_DATA_BOOK_ERROR, 
E_DATA_BOOK_STATUS_INVALID_ARG)) {
+                                       if (!invalid_objects) {
+                                               invalid_objects = g_string_new (local_error->message);
+                                       } else {
+                                               g_string_append_c (invalid_objects, '\n');
+                                               g_string_append (invalid_objects, local_error->message);
+                                       }
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+
+                       g_hash_table_destroy (covered_uids);
+               }
+
+               if (success && new_sync_tag)
+                       e_cache_set_key (E_CACHE (book_cache), EBMB_KEY_SYNC_TAG, new_sync_tag, NULL);
+
+               g_slist_free_full (created_objects, e_book_meta_backend_info_free);
+               g_slist_free_full (modified_objects, e_book_meta_backend_info_free);
+               g_slist_free_full (removed_objects, e_book_meta_backend_info_free);
+               g_free (last_sync_tag);
+               g_free (new_sync_tag);
+
+               is_repeat = TRUE;
+       }
+
+       g_object_unref (book_cache);
+
+ done:
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->refresh_cancellable == cancellable)
+               g_clear_object (&meta_backend->priv->refresh_cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       if (invalid_objects) {
+               e_book_backend_notify_error (E_BOOK_BACKEND (meta_backend), invalid_objects->str);
+
+               g_string_free (invalid_objects, TRUE);
+       }
+
+       g_signal_emit (meta_backend, signals[REFRESH_COMPLETED], 0, NULL);
+}
+
+static void
+ebmb_source_refresh_timeout_cb (ESource *source,
+                               gpointer user_data)
+{
+       GWeakRef *weak_ref = user_data;
+       EBookMetaBackend *meta_backend;
+
+       g_return_if_fail (weak_ref != NULL);
+
+       meta_backend = g_weak_ref_get (weak_ref);
+       if (meta_backend) {
+               ebmb_schedule_refresh (meta_backend);
+               g_object_unref (meta_backend);
+       }
+}
+
+static void
+ebmb_source_changed_thread_func (EBookBackend *book_backend,
+                                gpointer user_data,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       EBookMetaBackend *meta_backend;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+       if (!meta_backend->priv->refresh_timeout_id) {
+               ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+               if (e_source_has_extension (source, E_SOURCE_EXTENSION_REFRESH)) {
+                       meta_backend->priv->refresh_timeout_id = e_source_refresh_add_timeout (source, NULL,
+                               ebmb_source_refresh_timeout_cb, e_weak_ref_new (meta_backend), 
(GDestroyNotify) e_weak_ref_free);
+               }
+       }
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       g_signal_emit (meta_backend, signals[SOURCE_CHANGED], 0, NULL);
+
+       if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+           e_book_meta_backend_requires_reconnect (meta_backend)) {
+               gboolean can_refresh;
+
+               g_mutex_lock (&meta_backend->priv->connect_lock);
+               can_refresh = e_book_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+               g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+               if (can_refresh)
+                       ebmb_schedule_refresh (meta_backend);
+       }
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->source_changed_cancellable == cancellable)
+               g_clear_object (&meta_backend->priv->source_changed_cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static void
+ebmb_go_offline_thread_func (EBookBackend *book_backend,
+                            gpointer user_data,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       EBookMetaBackend *meta_backend;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       g_mutex_lock (&meta_backend->priv->connect_lock);
+       e_book_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+       g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->go_offline_cancellable == cancellable)
+               g_clear_object (&meta_backend->priv->go_offline_cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static gboolean
+ebmb_put_contact (EBookMetaBackend *meta_backend,
+                 EBookCache *book_cache,
+                 ECacheOfflineFlag offline_flag,
+                 EContact *contact,
+                 const gchar *extra,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       EContact *existing_contact = NULL;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+       success = e_book_meta_backend_store_inline_photos_sync (meta_backend, contact, cancellable, error);
+
+       if (success && e_book_cache_get_contact (book_cache,
+               e_contact_get_const (contact, E_CONTACT_UID), FALSE, &existing_contact, cancellable, NULL)) {
+               GSList *old_photos, *new_photos, *link;
+
+               old_photos = ebmb_gather_photos_local_filenames (meta_backend, existing_contact);
+               if (old_photos) {
+                       GHashTable *photos_hash;
+
+                       photos_hash = g_hash_table_new (g_str_hash, g_str_equal);
+
+                       new_photos = ebmb_gather_photos_local_filenames (meta_backend, contact);
+
+                       for (link = new_photos; link; link = g_slist_next (link)) {
+                               const gchar *filename = link->data;
+
+                               if (filename)
+                                       g_hash_table_insert (photos_hash, (gpointer) filename, NULL);
+                       }
+
+                       for (link = old_photos; link; link = g_slist_next (link)) {
+                               const gchar *filename = link->data;
+
+                               if (filename && !g_hash_table_contains (photos_hash, filename)) {
+                                       if (g_unlink (filename) == -1) {
+                                               /* Ignore these errors */
+                                       }
+                               }
+                       }
+
+                       g_slist_free_full (old_photos, g_free);
+                       g_slist_free_full (new_photos, g_free);
+                       g_hash_table_destroy (photos_hash);
+               }
+       }
+
+       success = success && e_book_cache_put_contact (book_cache, contact, extra, offline_flag, cancellable, 
error);
+
+       if (success)
+               e_book_backend_notify_update (E_BOOK_BACKEND (meta_backend), contact);
+
+       g_clear_object (&existing_contact);
+
+       return success;
+}
+
+static gboolean
+ebmb_load_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+                               EBookCache *book_cache,
+                               const gchar *uid,
+                               const gchar *preloaded_object,
+                               const gchar *preloaded_extra,
+                               gchar **out_new_uid,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       ECacheOfflineFlag offline_flag = E_CACHE_IS_ONLINE;
+       EContact *contact = NULL;
+       gchar *extra = NULL;
+       gboolean success = TRUE;
+
+       if (preloaded_object && *preloaded_object) {
+               contact = e_contact_new_from_vcard_with_uid (preloaded_object, uid);
+               if (!contact) {
+                       g_propagate_error (error, e_data_book_create_error_fmt 
(E_DATA_BOOK_STATUS_INVALID_ARG, _("Preloaded object for UID “%s” is invalid"), uid));
+                       return FALSE;
+               }
+       } else if (!e_book_meta_backend_load_contact_sync (meta_backend, uid, preloaded_extra, &contact, 
&extra, cancellable, error)) {
+               g_free (extra);
+               return FALSE;
+       } else if (!contact) {
+               g_propagate_error (error, e_data_book_create_error_fmt (E_DATA_BOOK_STATUS_INVALID_ARG, 
_("Received object for UID “%s” is invalid"), uid));
+               g_free (extra);
+               return FALSE;
+       }
+
+       success = ebmb_put_contact (meta_backend, book_cache, offline_flag,
+               contact, extra ? extra : preloaded_extra, cancellable, error);
+
+       if (success && out_new_uid)
+               *out_new_uid = e_contact_get (contact, E_CONTACT_UID);
+
+       g_object_unref (contact);
+       g_free (extra);
+
+       return success;
+}
+
+static gboolean
+ebmb_save_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+                               EBookCache *book_cache,
+                               gboolean overwrite_existing,
+                               EConflictResolution conflict_resolution,
+                               /* const */ EContact *in_contact,
+                               const gchar *extra,
+                               const gchar *orig_uid,
+                               gboolean *out_requires_put,
+                               gchar **out_new_uid,
+                               gchar **out_new_extra,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       EContact *contact;
+       gchar *new_uid = NULL, *new_extra = NULL;
+       gboolean success = TRUE;
+
+       if (out_requires_put)
+               *out_requires_put = TRUE;
+
+       if (out_new_uid)
+               *out_new_uid = NULL;
+
+       contact = e_contact_duplicate (in_contact);
+
+       success = e_book_meta_backend_inline_local_photos_sync (meta_backend, contact, cancellable, error);
+
+       success = success && e_book_meta_backend_save_contact_sync (meta_backend, overwrite_existing, 
conflict_resolution,
+               contact, extra, &new_uid, &new_extra, cancellable, error);
+
+       if (success && new_uid && *new_uid) {
+               gchar *loaded_uid = NULL;
+
+               success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, new_uid, NULL,
+                       new_extra ? new_extra : extra, &loaded_uid, cancellable, error);
+
+               if (success && g_strcmp0 (loaded_uid, orig_uid) != 0)
+                       success = ebmb_maybe_remove_from_cache (meta_backend, book_cache, E_CACHE_IS_ONLINE, 
orig_uid, cancellable, error);
+
+               if (success && out_new_uid)
+                       *out_new_uid = loaded_uid;
+               else
+                       g_free (loaded_uid);
+
+               if (out_requires_put)
+                       *out_requires_put = FALSE;
+       }
+
+       g_free (new_uid);
+
+       if (success && out_new_extra)
+               *out_new_extra = new_extra;
+       else
+               g_free (new_extra);
+       g_object_unref (contact);
+
+       return success;
+}
+
+static gchar *
+ebmb_get_backend_property (EBookBackend *book_backend,
+                          const gchar *prop_name)
+{
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REVISION)) {
+               EBookCache *book_cache;
+               gchar *revision = NULL;
+
+               book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (book_backend));
+               if (book_cache) {
+                       revision = e_cache_dup_revision (E_CACHE (book_cache));
+                       g_object_unref (book_cache);
+               } else {
+                       g_warn_if_reached ();
+               }
+
+               return revision;
+       } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strdup (e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)));
+       } else  if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
+               return g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));
+       } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
+               GString *fields;
+               gint ii;
+
+               fields = g_string_sized_new (1024);
+
+               /* Claim to support everything by default */
+               for (ii = 1; ii < E_CONTACT_FIELD_LAST; ii++) {
+                       if (fields->len > 0)
+                               g_string_append_c (fields, ',');
+                       g_string_append (fields, e_contact_field_name (ii));
+               }
+
+               return g_string_free (fields, FALSE);
+       }
+
+       /* Chain up to parent's method. */
+       return E_BOOK_BACKEND_CLASS (e_book_meta_backend_parent_class)->get_backend_property (book_backend, 
prop_name);
+}
+
+static gboolean
+ebmb_open_sync (EBookBackend *book_backend,
+               GCancellable *cancellable,
+               GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       ESource *source;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+       if (e_book_backend_is_opened (book_backend))
+               return TRUE;
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+       if (meta_backend->priv->create_cache_error) {
+               g_propagate_error (error, meta_backend->priv->create_cache_error);
+               meta_backend->priv->create_cache_error = NULL;
+               return FALSE;
+       }
+
+       source = e_backend_get_source (E_BACKEND (book_backend));
+
+       if (!meta_backend->priv->source_changed_id) {
+               meta_backend->priv->source_changed_id = g_signal_connect_swapped (source, "changed",
+                       G_CALLBACK (ebmb_schedule_source_changed), meta_backend);
+       }
+
+       if (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               ESourceWebdav *webdav_extension;
+
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
+       }
+
+       if (e_book_meta_backend_get_ever_connected (meta_backend)) {
+               e_book_backend_set_writable (E_BOOK_BACKEND (meta_backend),
+                       e_book_meta_backend_get_connected_writable (meta_backend));
+       } else {
+               if (!ebmb_connect_wrapper_sync (meta_backend, cancellable, error)) {
+                       g_mutex_lock (&meta_backend->priv->property_lock);
+                       meta_backend->priv->refresh_after_authenticate = TRUE;
+                       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+                       return FALSE;
+               }
+       }
+
+       ebmb_schedule_refresh (E_BOOK_META_BACKEND (book_backend));
+
+       return TRUE;
+}
+
+static gboolean
+ebmb_refresh_sync (EBookBackend *book_backend,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       if (!e_backend_get_online (E_BACKEND (book_backend)))
+               return TRUE;
+
+       success = ebmb_connect_wrapper_sync (meta_backend, cancellable, error);
+
+       if (success)
+               ebmb_schedule_refresh (meta_backend);
+
+       return success;
+}
+
+/* Copied from e_cal_component_gen_uid() */
+static gchar *
+e_book_meta_backend_gen_uid (void)
+{
+       gchar *iso, *ret;
+       static const gchar *hostname;
+       time_t t = time (NULL);
+       struct tm stm;
+       static gint serial;
+
+       if (!hostname) {
+#ifndef G_OS_WIN32
+               static gchar buffer[512];
+
+               if ((gethostname (buffer, sizeof (buffer) - 1) == 0) &&
+                   (buffer[0] != 0))
+                       hostname = buffer;
+               else
+                       hostname = "localhost";
+#else
+               hostname = g_get_host_name ();
+#endif
+       }
+
+#ifdef G_OS_WIN32
+#ifdef gmtime_r
+#undef gmtime_r
+#endif
+
+/* The gmtime() in Microsoft's C library is MT-safe */
+#define gmtime_r(tp,tmp) (gmtime(tp)?(*(tmp)=*gmtime(tp),(tmp)):0)
+#endif
+
+       gmtime_r (&t, &stm);
+       iso = g_strdup_printf ("%04d%02d%02dT%02d%02d%02dZ",
+               (stm.tm_year + 1900),
+               (stm.tm_mon + 1),
+               stm.tm_mday,
+               stm.tm_hour,
+               stm.tm_min,
+               stm.tm_sec);
+
+       ret = g_strdup_printf (
+               "%s-%d-%d-%d-%d@%s",
+               iso,
+               getpid (),
+               getgid (),
+               getppid (),
+               serial++,
+               hostname);
+       g_free (iso);
+
+       return ret;
+}
+
+static gboolean
+ebmb_create_contact_sync (EBookMetaBackend *meta_backend,
+                         EBookCache *book_cache,
+                         ECacheOfflineFlag *offline_flag,
+                         EConflictResolution conflict_resolution,
+                         EContact *contact,
+                         gchar **out_new_uid,
+                         EContact **out_new_contact,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       const gchar *uid;
+       gchar *new_uid = NULL, *new_extra = NULL;
+       gboolean success, requires_put = TRUE;
+
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       if (!uid) {
+               gchar *new_uid;
+
+               new_uid = e_book_meta_backend_gen_uid ();
+               if (!new_uid) {
+                       g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, 
NULL));
+                       return FALSE;
+               }
+
+               e_contact_set (contact, E_CONTACT_UID, new_uid);
+               uid = e_contact_get_const (contact, E_CONTACT_UID);
+
+               g_free (new_uid);
+       }
+
+       if (e_cache_contains (E_CACHE (book_cache), uid, E_CACHE_EXCLUDE_DELETED)) {
+               g_propagate_error (error, e_data_book_create_error 
(E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS, NULL));
+               return FALSE;
+       }
+
+       if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+                       *offline_flag = E_CACHE_IS_ONLINE;
+               } else {
+                       *offline_flag = E_CACHE_IS_OFFLINE;
+               }
+       }
+
+       if (*offline_flag == E_CACHE_IS_ONLINE) {
+               if (!ebmb_save_contact_wrapper_sync (meta_backend, book_cache, FALSE, conflict_resolution, 
contact, NULL, uid,
+                       &requires_put, &new_uid, &new_extra, cancellable, error)) {
+                       return FALSE;
+               }
+       }
+
+       if (requires_put) {
+               success = e_book_cache_put_contact (book_cache, contact, new_extra, *offline_flag, 
cancellable, error);
+               if (success)
+                       e_book_backend_notify_update (E_BOOK_BACKEND (meta_backend), contact);
+       } else {
+               success = TRUE;
+       }
+
+       if (success) {
+               if (out_new_uid)
+                       *out_new_uid = g_strdup (new_uid ? new_uid : uid);
+               if (out_new_contact) {
+                       if (new_uid) {
+                               if (!e_book_cache_get_contact (book_cache, new_uid, FALSE, out_new_contact, 
cancellable, NULL))
+                                       *out_new_contact = g_object_ref (contact);
+                       } else {
+                               *out_new_contact = g_object_ref (contact);
+                       }
+               }
+       }
+
+       g_free (new_uid);
+       g_free (new_extra);
+
+       return success;
+}
+
+static gboolean
+ebmb_create_contacts_sync (EBookBackend *book_backend,
+                          const gchar * const *vcards,
+                          GQueue *out_contacts,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+       g_return_val_if_fail (vcards != NULL, FALSE);
+       g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+       if (!e_book_backend_get_writable (book_backend)) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_PERMISSION_DENIED, 
NULL));
+               return FALSE;
+       }
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       for (ii = 0; vcards[ii] && success; ii++) {
+               EContact *contact, *new_contact = NULL;
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+                       success = FALSE;
+                       break;
+               }
+
+               contact = e_contact_new_from_vcard (vcards[ii]);
+               if (!contact) {
+                       g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, 
NULL));
+                       success = FALSE;
+                       break;
+               }
+
+               success = ebmb_create_contact_sync (meta_backend, book_cache, &offline_flag, 
conflict_resolution,
+                       contact, NULL, &new_contact, cancellable, error);
+
+               if (success) {
+                       ebmb_foreach_cursor (meta_backend, new_contact, e_data_book_cursor_contact_added);
+
+                       g_queue_push_tail (out_contacts, new_contact);
+               }
+
+               g_object_unref (contact);
+       }
+
+       g_object_unref (book_cache);
+
+       if (!success) {
+               g_queue_foreach (out_contacts, (GFunc) g_object_unref, NULL);
+               g_queue_clear (out_contacts);
+       }
+
+       return success;
+}
+
+static gboolean
+ebmb_modify_contact_sync (EBookMetaBackend *meta_backend,
+                         EBookCache *book_cache,
+                         ECacheOfflineFlag *offline_flag,
+                         EConflictResolution conflict_resolution,
+                         EContact *contact,
+                         EContact **out_new_contact,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       const gchar *uid;
+       EContact *existing_contact = NULL;
+       gchar *extra = NULL, *new_uid = NULL, *new_extra = NULL;
+       gboolean success = TRUE, requires_put = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (contact != NULL, FALSE);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       if (!uid) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, NULL));
+               return FALSE;
+       }
+
+       if (!e_book_cache_get_contact (book_cache, uid, FALSE, &existing_contact, cancellable, &local_error)) 
{
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       local_error = e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL);
+               }
+
+               g_propagate_error (error, local_error);
+
+               return FALSE;
+       }
+
+       if (!e_book_cache_get_contact_extra (book_cache, uid, &extra, cancellable, NULL))
+               extra = NULL;
+
+       if (success && *offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+                       *offline_flag = E_CACHE_IS_ONLINE;
+               } else {
+                       *offline_flag = E_CACHE_IS_OFFLINE;
+               }
+       }
+
+       if (success && *offline_flag == E_CACHE_IS_ONLINE) {
+               success = ebmb_save_contact_wrapper_sync (meta_backend, book_cache, TRUE, conflict_resolution,
+                       contact, extra, uid, &requires_put, &new_uid, &new_extra, cancellable, error);
+       }
+
+       if (success && requires_put)
+               success = ebmb_put_contact (meta_backend, book_cache, *offline_flag, contact, new_extra ? 
new_extra : extra, cancellable, error);
+
+       if (success && out_new_contact) {
+               if (new_uid) {
+                       if (!e_book_cache_get_contact (book_cache, new_uid, FALSE, out_new_contact, 
cancellable, NULL))
+                               *out_new_contact = NULL;
+               } else {
+                       *out_new_contact = g_object_ref (contact);
+               }
+       }
+
+       g_clear_object (&existing_contact);
+       g_free (new_extra);
+       g_free (new_uid);
+       g_free (extra);
+
+       return success;
+}
+
+static gboolean
+ebmb_modify_contacts_sync (EBookBackend *book_backend,
+                          const gchar * const *vcards,
+                          GQueue *out_contacts,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+       g_return_val_if_fail (vcards != NULL, FALSE);
+       g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+       if (!e_book_backend_get_writable (book_backend)) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_PERMISSION_DENIED, 
NULL));
+               return FALSE;
+       }
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       for (ii = 0; vcards[ii] && success; ii++) {
+               EContact *contact, *new_contact = NULL;
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+                       success = FALSE;
+                       break;
+               }
+
+               contact = e_contact_new_from_vcard (vcards[ii]);
+               if (!contact) {
+                       g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, 
NULL));
+                       success = FALSE;
+                       break;
+               }
+
+               success = ebmb_modify_contact_sync (meta_backend, book_cache, &offline_flag, 
conflict_resolution,
+                       contact, &new_contact, cancellable, error);
+
+               if (success && new_contact) {
+                       ebmb_foreach_cursor (meta_backend, contact, e_data_book_cursor_contact_removed);
+                       ebmb_foreach_cursor (meta_backend, new_contact, e_data_book_cursor_contact_added);
+
+                       g_queue_push_tail (out_contacts, g_object_ref (new_contact));
+               }
+
+               g_clear_object (&new_contact);
+               g_object_unref (contact);
+       }
+
+       g_object_unref (book_cache);
+
+       if (!success) {
+               g_queue_foreach (out_contacts, (GFunc) g_object_unref, NULL);
+               g_queue_clear (out_contacts);
+       }
+
+       return success;
+}
+
+static gboolean
+ebmb_remove_contact_sync (EBookMetaBackend *meta_backend,
+                         EBookCache *book_cache,
+                         ECacheOfflineFlag *offline_flag,
+                         EConflictResolution conflict_resolution,
+                         const gchar *uid,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       EContact *existing_contact = NULL;
+       gchar *extra = NULL;
+       gboolean success = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (!e_book_cache_get_contact (book_cache, uid, FALSE, &existing_contact, cancellable, &local_error)) 
{
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       local_error = e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL);
+               }
+
+               g_propagate_error (error, local_error);
+
+               return FALSE;
+       }
+
+       if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+                       *offline_flag = E_CACHE_IS_ONLINE;
+               } else {
+                       *offline_flag = E_CACHE_IS_OFFLINE;
+               }
+       }
+
+       if (!e_book_cache_get_contact_extra (book_cache, uid, &extra, cancellable, NULL))
+               extra = NULL;
+
+       if (*offline_flag == E_CACHE_IS_ONLINE) {
+               gchar *vcard_string = NULL;
+
+               g_warn_if_fail (e_book_cache_get_vcard (book_cache, uid, FALSE, &vcard_string, cancellable, 
NULL));
+
+               success = e_book_meta_backend_remove_contact_sync (meta_backend, conflict_resolution, uid, 
extra, vcard_string, cancellable, error);
+
+               g_free (vcard_string);
+       }
+
+       success = success && ebmb_maybe_remove_from_cache (meta_backend, book_cache, *offline_flag, uid, 
cancellable, error);
+
+       g_clear_object (&existing_contact);
+       g_free (extra);
+
+       return success;
+}
+
+static gboolean
+ebmb_remove_contacts_sync (EBookBackend *book_backend,
+                          const gchar * const *uids,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+       g_return_val_if_fail (uids != NULL, FALSE);
+
+       if (!e_book_backend_get_writable (book_backend)) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_PERMISSION_DENIED, 
NULL));
+               return FALSE;
+       }
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       for (ii = 0; uids[ii] && success; ii++) {
+               const gchar *uid = uids[ii];
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+                       success = FALSE;
+                       break;
+               }
+
+               if (!uid) {
+                       g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, 
NULL));
+                       success = FALSE;
+                       break;
+               }
+
+               success = ebmb_remove_contact_sync (meta_backend, book_cache, &offline_flag, 
conflict_resolution, uid, cancellable, error);
+       }
+
+       g_object_unref (book_cache);
+
+       return success;
+}
+
+static EContact *
+ebmb_get_contact_sync (EBookBackend *book_backend,
+                      const gchar *uid,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       EContact *contact = NULL;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+       g_return_val_if_fail (uid && *uid, NULL);
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+
+       g_return_val_if_fail (book_cache != NULL, NULL);
+
+       if (!e_book_cache_get_contact (book_cache, uid, FALSE, &contact, cancellable, &local_error) &&
+           g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+               gchar *loaded_uid = NULL;
+               gboolean found = FALSE;
+
+               g_clear_error (&local_error);
+
+               /* Ignore errors here, just try whether it's on the remote side, but not in the local cache */
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL) &&
+                   ebmb_load_contact_wrapper_sync (meta_backend, book_cache, uid, NULL, NULL, &loaded_uid, 
cancellable, NULL)) {
+                       found = e_book_cache_get_contact (book_cache, loaded_uid, FALSE, &contact, 
cancellable, NULL);
+               }
+
+               if (!found)
+                       g_propagate_error (error, e_data_book_create_error 
(E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL));
+
+               g_free (loaded_uid);
+       } else if (local_error) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, 
local_error->message));
+               g_clear_error (&local_error);
+       }
+
+       g_object_unref (book_cache);
+
+       return contact;
+}
+
+static gboolean
+ebmb_get_contact_list_sync (EBookBackend *book_backend,
+                           const gchar *query,
+                           GQueue *out_contacts,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       GSList *contacts = NULL, *link;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+       g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+       success = e_book_meta_backend_search_sync (E_BOOK_META_BACKEND (book_backend), query, FALSE, 
&contacts, cancellable, error);
+       if (success) {
+               for (link = contacts; link; link = g_slist_next (link)) {
+                       EContact *contact = link->data;
+
+                       g_queue_push_tail (out_contacts, g_object_ref (contact));
+               }
+
+               g_slist_free_full (contacts, g_object_unref);
+       }
+
+       return success;
+}
+
+static gboolean
+ebmb_get_contact_list_uids_sync (EBookBackend *book_backend,
+                                const gchar *query,
+                                GQueue *out_uids,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       GSList *uids = NULL, *link;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+       g_return_val_if_fail (out_uids != NULL, FALSE);
+
+       success = e_book_meta_backend_search_uids_sync (E_BOOK_META_BACKEND (book_backend), query, &uids, 
cancellable, error);
+       if (success) {
+               for (link = uids; link; link = g_slist_next (link)) {
+                       gchar *uid = link->data;
+
+                       g_queue_push_tail (out_uids, uid);
+                       link->data = NULL;
+               }
+
+               g_slist_free_full (uids, g_free);
+       }
+
+       return success;
+}
+
+static void
+ebmb_start_view (EBookBackend *book_backend,
+                EDataBookView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+       cancellable = ebmb_create_view_cancellable (E_BOOK_META_BACKEND (book_backend), view);
+
+       e_book_backend_schedule_custom_operation (book_backend, cancellable,
+               ebmb_start_view_thread_func, g_object_ref (view), g_object_unref);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ebmb_stop_view (EBookBackend *book_backend,
+               EDataBookView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+       cancellable = ebmb_steal_view_cancellable (E_BOOK_META_BACKEND (book_backend), view);
+       if (cancellable) {
+               g_cancellable_cancel (cancellable);
+               g_object_unref (cancellable);
+       }
+}
+
+static EDataBookDirect *
+ebmb_get_direct_book (EBookBackend *book_backend)
+{
+       EBookMetaBackendClass *klass;
+       EBookCache *book_cache;
+       EDataBookDirect *direct_book;
+       const gchar *cache_filename;
+       gchar *backend_path;
+       gchar *dirname;
+       const gchar *modules_env;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (book_backend);
+       g_return_val_if_fail (klass != NULL, NULL);
+
+       if (!klass->backend_module_filename ||
+           !klass->backend_factory_type_name)
+               return NULL;
+
+       book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (book_backend));
+       g_return_val_if_fail (book_cache != NULL, NULL);
+
+       cache_filename = e_cache_get_filename (E_CACHE (book_cache));
+       dirname = g_path_get_dirname (cache_filename);
+
+       modules_env = g_getenv (EDS_ADDRESS_BOOK_MODULES);
+
+       /* Support in-tree testing / relocated modules */
+       if (modules_env) {
+               backend_path = g_build_filename (modules_env, klass->backend_module_filename, NULL);
+       } else {
+               backend_path = g_build_filename (BACKENDDIR, klass->backend_module_filename, NULL);
+       }
+
+       direct_book = e_data_book_direct_new (backend_path, klass->backend_factory_type_name, dirname);
+
+       g_object_unref (book_cache);
+       g_free (backend_path);
+       g_free (dirname);
+
+       return direct_book;
+}
+
+static void
+ebmb_configure_direct (EBookBackend *book_backend,
+                      const gchar *base_directory)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       const gchar *cache_filename;
+       gchar *dirname;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+       if (!base_directory)
+               return;
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_if_fail (book_cache != NULL);
+
+       cache_filename = e_cache_get_filename (E_CACHE (book_cache));
+       dirname = g_path_get_dirname (cache_filename);
+
+       /* Did path for the cache change? Change the cache as well */
+       if (dirname && !g_str_equal (base_directory, dirname) &&
+           !g_str_has_prefix (dirname, base_directory)) {
+               gchar *filename = g_path_get_basename (cache_filename);
+               gchar *new_cache_filename;
+               EBookCache *new_cache;
+               ESource *source;
+
+               new_cache_filename = g_build_filename (base_directory, filename, NULL);
+               source = e_backend_get_source (E_BACKEND (book_backend));
+
+               g_clear_error (&meta_backend->priv->create_cache_error);
+
+               new_cache = e_book_cache_new (new_cache_filename, source, NULL, 
&meta_backend->priv->create_cache_error);
+               g_prefix_error (&meta_backend->priv->create_cache_error, _("Failed to create cache ”%s”:"), 
new_cache_filename);
+
+               if (new_cache) {
+                       e_book_meta_backend_set_cache (meta_backend, new_cache);
+                       g_clear_object (&new_cache);
+               }
+
+               g_free (new_cache_filename);
+               g_free (filename);
+       }
+
+       g_free (dirname);
+       g_object_unref (book_cache);
+}
+
+static gboolean
+ebmb_set_locale (EBookBackend *book_backend,
+                const gchar *locale,
+                GCancellable *cancellable,
+                GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       success = e_book_cache_set_locale (book_cache, locale, cancellable, error);
+       if (success) {
+               GSList *link;
+
+               g_mutex_lock (&meta_backend->priv->property_lock);
+
+               for (link = meta_backend->priv->cursors; success && link; link = g_slist_next (link)) {
+                       EDataBookCursor *cursor = link->data;
+
+                       success = e_data_book_cursor_load_locale (cursor, NULL, cancellable, error);
+               }
+
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+       }
+
+       g_object_unref (book_cache);
+
+       return success;
+}
+
+static gchar *
+ebmb_dup_locale (EBookBackend *book_backend)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       gchar *locale;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, NULL);
+
+       locale = e_book_cache_dup_locale (book_cache);
+
+       g_object_unref (book_cache);
+
+       return locale;
+}
+
+static EDataBookCursor *
+ebmb_create_cursor (EBookBackend *book_backend,
+                   EContactField *sort_fields,
+                   EBookCursorSortType *sort_types,
+                   guint n_fields,
+                   GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       EBookCache *book_cache;
+       EDataBookCursor *cursor;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, NULL);
+
+       cursor = e_data_book_cursor_cache_new (book_backend, book_cache, sort_fields, sort_types, n_fields, 
error);
+
+       if (cursor) {
+               g_mutex_lock (&meta_backend->priv->property_lock);
+
+               meta_backend->priv->cursors = g_slist_prepend (meta_backend->priv->cursors, cursor);
+
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+       }
+
+       g_object_unref (book_cache);
+
+       return cursor;
+}
+
+static gboolean
+ebmb_delete_cursor (EBookBackend *book_backend,
+                   EDataBookCursor *cursor,
+                   GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       GSList *link;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+       meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       link = g_slist_find (meta_backend->priv->cursors, cursor);
+
+       if (link) {
+               meta_backend->priv->cursors = g_slist_remove (meta_backend->priv->cursors, cursor);
+               g_object_unref (cursor);
+       } else {
+               g_set_error_literal (
+                       error,
+                       E_CLIENT_ERROR,
+                       E_CLIENT_ERROR_INVALID_ARG,
+                       _("Requested to delete an unrelated cursor"));
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return link != NULL;
+}
+
+static ESourceAuthenticationResult
+ebmb_authenticate_sync (EBackend *backend,
+                       const ENamedParameters *credentials,
+                       gchar **out_certificate_pem,
+                       GTlsCertificateFlags *out_certificate_errors,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       EBookMetaBackend *meta_backend;
+       ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+       gboolean success, refresh_after_authenticate = FALSE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (backend), E_SOURCE_AUTHENTICATION_ERROR);
+
+       meta_backend = E_BOOK_META_BACKEND (backend);
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+                       e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+               return E_SOURCE_AUTHENTICATION_ERROR;
+       }
+
+       g_mutex_lock (&meta_backend->priv->connect_lock);
+       success = e_book_meta_backend_connect_sync (meta_backend, credentials, &auth_result,
+               out_certificate_pem, out_certificate_errors, cancellable, error);
+
+       if (success) {
+               ebmb_update_connection_values (meta_backend);
+               auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+       } else {
+               if (auth_result == E_SOURCE_AUTHENTICATION_UNKNOWN)
+                       auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+       }
+       g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       e_named_parameters_free (meta_backend->priv->last_credentials);
+       if (success) {
+               meta_backend->priv->last_credentials = e_named_parameters_new_clone (credentials);
+
+               refresh_after_authenticate = meta_backend->priv->refresh_after_authenticate;
+               meta_backend->priv->refresh_after_authenticate = FALSE;
+       } else {
+               meta_backend->priv->last_credentials = NULL;
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       if (refresh_after_authenticate)
+               ebmb_schedule_refresh (meta_backend);
+
+       return auth_result;
+}
+
+static void
+ebmb_schedule_refresh (EBookMetaBackend *meta_backend)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->refresh_cancellable) {
+               /* Already refreshing the content */
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       cancellable = g_cancellable_new ();
+       meta_backend->priv->refresh_cancellable = g_object_ref (cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_book_backend_schedule_custom_operation (E_BOOK_BACKEND (meta_backend), cancellable,
+               ebmb_refresh_thread_func, NULL, NULL);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ebmb_schedule_source_changed (EBookMetaBackend *meta_backend)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->source_changed_cancellable) {
+               /* Already updating */
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       cancellable = g_cancellable_new ();
+       meta_backend->priv->source_changed_cancellable = g_object_ref (cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_book_backend_schedule_custom_operation (E_BOOK_BACKEND (meta_backend), cancellable,
+               ebmb_source_changed_thread_func, NULL, NULL);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ebmb_schedule_go_offline (EBookMetaBackend *meta_backend)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       /* Cancel anything ongoing now, but disconnect in a dedicated thread */
+       if (meta_backend->priv->refresh_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+               g_clear_object (&meta_backend->priv->refresh_cancellable);
+       }
+
+       if (meta_backend->priv->source_changed_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+               g_clear_object (&meta_backend->priv->source_changed_cancellable);
+       }
+
+       if (meta_backend->priv->go_offline_cancellable) {
+               /* Already going offline */
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       cancellable = g_cancellable_new ();
+       meta_backend->priv->go_offline_cancellable = g_object_ref (cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_book_backend_schedule_custom_operation (E_BOOK_BACKEND (meta_backend), cancellable,
+               ebmb_go_offline_thread_func, NULL, NULL);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ebmb_notify_online_cb (GObject *object,
+                      GParamSpec *param,
+                      gpointer user_data)
+{
+       EBookMetaBackend *meta_backend = user_data;
+       gboolean new_value;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       new_value = e_backend_get_online (E_BACKEND (meta_backend));
+       if (!new_value == !meta_backend->priv->current_online_state)
+               return;
+
+       meta_backend->priv->current_online_state = new_value;
+
+       if (new_value)
+               ebmb_schedule_refresh (meta_backend);
+       else
+               ebmb_schedule_go_offline (meta_backend);
+}
+
+static void
+ebmb_cancel_view_cb (gpointer key,
+                    gpointer value,
+                    gpointer user_data)
+{
+       GCancellable *cancellable = value;
+
+       g_return_if_fail (G_IS_CANCELLABLE (cancellable));
+
+       g_cancellable_cancel (cancellable);
+}
+
+static void
+e_book_meta_backend_set_property (GObject *object,
+                                 guint property_id,
+                                 const GValue *value,
+                                 GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CACHE:
+                       e_book_meta_backend_set_cache (
+                               E_BOOK_META_BACKEND (object),
+                               g_value_get_object (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_meta_backend_get_property (GObject *object,
+                                 guint property_id,
+                                 GValue *value,
+                                 GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CACHE:
+                       g_value_take_object (
+                               value,
+                               e_book_meta_backend_ref_cache (
+                               E_BOOK_META_BACKEND (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_meta_backend_constructed (GObject *object)
+{
+       EBookMetaBackend *meta_backend = E_BOOK_META_BACKEND (object);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_meta_backend_parent_class)->constructed (object);
+
+       meta_backend->priv->current_online_state = e_backend_get_online (E_BACKEND (meta_backend));
+
+       meta_backend->priv->notify_online_id = g_signal_connect (meta_backend, "notify::online",
+               G_CALLBACK (ebmb_notify_online_cb), meta_backend);
+
+       if (!meta_backend->priv->cache) {
+               EBookCache *cache;
+               ESource *source;
+               gchar *filename;
+
+               source = e_backend_get_source (E_BACKEND (meta_backend));
+               filename = g_build_filename (e_book_backend_get_cache_dir (E_BOOK_BACKEND (meta_backend)), 
"cache.db", NULL);
+               cache = e_book_cache_new (filename, source, NULL, &meta_backend->priv->create_cache_error);
+               g_prefix_error (&meta_backend->priv->create_cache_error, _("Failed to create cache ”%s”:"), 
filename);
+
+               g_free (filename);
+
+               if (cache) {
+                       e_book_meta_backend_set_cache (meta_backend, cache);
+                       g_clear_object (&cache);
+               }
+       }
+}
+
+static void
+e_book_meta_backend_dispose (GObject *object)
+{
+       EBookMetaBackend *meta_backend = E_BOOK_META_BACKEND (object);
+       ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->cursors) {
+               g_slist_free_full (meta_backend->priv->cursors, g_object_unref);
+               meta_backend->priv->cursors = NULL;
+       }
+
+       if (meta_backend->priv->refresh_timeout_id) {
+               if (source)
+                       e_source_refresh_remove_timeout (source, meta_backend->priv->refresh_timeout_id);
+               meta_backend->priv->refresh_timeout_id = 0;
+       }
+
+       if (meta_backend->priv->source_changed_id) {
+               if (source)
+                       g_signal_handler_disconnect (source, meta_backend->priv->source_changed_id);
+               meta_backend->priv->source_changed_id = 0;
+       }
+
+       if (meta_backend->priv->notify_online_id) {
+               g_signal_handler_disconnect (meta_backend, meta_backend->priv->notify_online_id);
+               meta_backend->priv->notify_online_id = 0;
+       }
+
+       if (meta_backend->priv->revision_changed_id) {
+               if (meta_backend->priv->cache)
+                       g_signal_handler_disconnect (meta_backend->priv->cache, 
meta_backend->priv->revision_changed_id);
+               meta_backend->priv->revision_changed_id = 0;
+       }
+
+       g_hash_table_foreach (meta_backend->priv->view_cancellables, ebmb_cancel_view_cb, NULL);
+
+       if (meta_backend->priv->refresh_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+               g_clear_object (&meta_backend->priv->refresh_cancellable);
+       }
+
+       if (meta_backend->priv->source_changed_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+               g_clear_object (&meta_backend->priv->source_changed_cancellable);
+       }
+
+       if (meta_backend->priv->go_offline_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->go_offline_cancellable);
+               g_clear_object (&meta_backend->priv->go_offline_cancellable);
+       }
+
+       e_named_parameters_free (meta_backend->priv->last_credentials);
+       meta_backend->priv->last_credentials = NULL;
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_meta_backend_parent_class)->dispose (object);
+}
+
+static void
+e_book_meta_backend_finalize (GObject *object)
+{
+       EBookMetaBackend *meta_backend = E_BOOK_META_BACKEND (object);
+
+       g_clear_object (&meta_backend->priv->cache);
+       g_clear_object (&meta_backend->priv->refresh_cancellable);
+       g_clear_object (&meta_backend->priv->source_changed_cancellable);
+       g_clear_object (&meta_backend->priv->go_offline_cancellable);
+       g_clear_error (&meta_backend->priv->create_cache_error);
+       g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+       g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+       g_mutex_clear (&meta_backend->priv->connect_lock);
+       g_mutex_clear (&meta_backend->priv->property_lock);
+       g_hash_table_destroy (meta_backend->priv->view_cancellables);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_meta_backend_parent_class)->finalize (object);
+}
+
+static void
+e_book_meta_backend_class_init (EBookMetaBackendClass *klass)
+{
+       GObjectClass *object_class;
+       EBackendClass *backend_class;
+       EBookBackendClass *book_backend_class;
+
+       g_type_class_add_private (klass, sizeof (EBookMetaBackendPrivate));
+
+       klass->backend_factory_type_name = NULL;
+       klass->backend_module_filename = NULL;
+       klass->get_changes_sync = ebmb_get_changes_sync;
+       klass->search_sync = ebmb_search_sync;
+       klass->search_uids_sync = ebmb_search_uids_sync;
+       klass->requires_reconnect = ebmb_requires_reconnect;
+
+       book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+       book_backend_class->get_backend_property = ebmb_get_backend_property;
+       book_backend_class->open_sync = ebmb_open_sync;
+       book_backend_class->refresh_sync = ebmb_refresh_sync;
+       book_backend_class->create_contacts_sync = ebmb_create_contacts_sync;
+       book_backend_class->modify_contacts_sync = ebmb_modify_contacts_sync;
+       book_backend_class->remove_contacts_sync = ebmb_remove_contacts_sync;
+       book_backend_class->get_contact_sync = ebmb_get_contact_sync;
+       book_backend_class->get_contact_list_sync = ebmb_get_contact_list_sync;
+       book_backend_class->get_contact_list_uids_sync = ebmb_get_contact_list_uids_sync;
+       book_backend_class->start_view = ebmb_start_view;
+       book_backend_class->stop_view = ebmb_stop_view;
+       book_backend_class->get_direct_book = ebmb_get_direct_book;
+       book_backend_class->configure_direct = ebmb_configure_direct;
+       book_backend_class->set_locale = ebmb_set_locale;
+       book_backend_class->dup_locale = ebmb_dup_locale;
+       book_backend_class->create_cursor = ebmb_create_cursor;
+       book_backend_class->delete_cursor = ebmb_delete_cursor;
+
+       backend_class = E_BACKEND_CLASS (klass);
+       backend_class->authenticate_sync = ebmb_authenticate_sync;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->set_property = e_book_meta_backend_set_property;
+       object_class->get_property = e_book_meta_backend_get_property;
+       object_class->constructed = e_book_meta_backend_constructed;
+       object_class->dispose = e_book_meta_backend_dispose;
+       object_class->finalize = e_book_meta_backend_finalize;
+
+       /**
+        * EBookMetaBackend:cache:
+        *
+        * The #EBookCache being used for this meta backend.
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_CACHE,
+               g_param_spec_object (
+                       "cache",
+                       "Cache",
+                       "Book Cache",
+                       E_TYPE_BOOK_CACHE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_STATIC_STRINGS));
+
+       /* This signal is meant for testing purposes mainly */
+       signals[REFRESH_COMPLETED] = g_signal_new (
+               "refresh-completed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               0,
+               NULL, NULL, NULL,
+               G_TYPE_NONE, 0, G_TYPE_NONE);
+
+       /**
+        * EBookMetaBackend::source-changed
+        *
+        * This signal is emitted whenever the underlying backend #ESource
+        * changes. Unlike the #ESource's 'changed' signal this one is
+        * tight to the #EBookMetaBackend itself and is emitted from
+        * a dedicated thread, thus it doesn't block the main thread.
+        *
+        * Since: 3.26
+        **/
+       signals[SOURCE_CHANGED] = g_signal_new (
+               "source-changed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (EBookMetaBackendClass, source_changed),
+               NULL, NULL, NULL,
+               G_TYPE_NONE, 0, G_TYPE_NONE);
+}
+
+static void
+e_book_meta_backend_init (EBookMetaBackend *meta_backend)
+{
+       meta_backend->priv = G_TYPE_INSTANCE_GET_PRIVATE (meta_backend, E_TYPE_BOOK_META_BACKEND, 
EBookMetaBackendPrivate);
+
+       g_mutex_init (&meta_backend->priv->connect_lock);
+       g_mutex_init (&meta_backend->priv->property_lock);
+
+       meta_backend->priv->view_cancellables = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
g_object_unref);
+       meta_backend->priv->current_online_state = FALSE;
+       meta_backend->priv->refresh_after_authenticate = FALSE;
+       meta_backend->priv->ever_connected = -1;
+       meta_backend->priv->connected_writable = -1;
+}
+
+/**
+ * e_book_meta_backend_get_capabilities:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Returns: an #EBookBackend::capabilities property to be used by
+ *    the descendant in conjunction to the descendant's capabilities
+ *    in the result of e_book_backend_get_backend_property() with
+ *    #CLIENT_BACKEND_PROPERTY_CAPABILITIES.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_book_meta_backend_get_capabilities (EBookMetaBackend *meta_backend)
+{
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+
+       return "refresh-supported" ","
+               "bulk-adds" ","
+               "bulk-modifies" ","
+               "bulk-removes";
+}
+
+/**
+ * e_book_meta_backend_set_ever_connected:
+ * @meta_backend: an #EBookMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend ever made a successful connection
+ * to its destination.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * when it had not been connected yet, then it does so immediately, to
+ * eventually report settings error easily.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_set_ever_connected (EBookMetaBackend *meta_backend,
+                                       gboolean value)
+{
+       EBookCache *book_cache;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       if ((value ? 1 : 0) == meta_backend->priv->ever_connected)
+               return;
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       meta_backend->priv->ever_connected = value ? 1 : 0;
+       e_cache_set_key_int (E_CACHE (book_cache), EBMB_KEY_EVER_CONNECTED, 
meta_backend->priv->ever_connected, NULL);
+       g_clear_object (&book_cache);
+}
+
+/**
+ * e_book_meta_backend_get_ever_connected:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Returns: Whether the @meta_backend ever made a successful connection
+ *    to its destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_get_ever_connected (EBookMetaBackend *meta_backend)
+{
+       gboolean result;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       if (meta_backend->priv->ever_connected == -1) {
+               EBookCache *book_cache;
+
+               book_cache = e_book_meta_backend_ref_cache (meta_backend);
+               result = e_cache_get_key_int (E_CACHE (book_cache), EBMB_KEY_EVER_CONNECTED, NULL) == 1;
+               g_clear_object (&book_cache);
+
+               meta_backend->priv->ever_connected = result ? 1 : 0;
+       } else {
+               result = meta_backend->priv->ever_connected == 1;
+       }
+
+       return result;
+}
+
+/**
+ * e_book_meta_backend_set_connected_writable:
+ * @meta_backend: an #EBookMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend connected to a writable destination.
+ * This value has meaning only if e_book_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * to set the backend writable or not also in the offline mode.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_set_connected_writable (EBookMetaBackend *meta_backend,
+                                           gboolean value)
+{
+       EBookCache *book_cache;
+
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       if ((value ? 1 : 0) == meta_backend->priv->connected_writable)
+               return;
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       meta_backend->priv->connected_writable = value ? 1 : 0;
+       e_cache_set_key_int (E_CACHE (book_cache), EBMB_KEY_CONNECTED_WRITABLE, 
meta_backend->priv->connected_writable, NULL);
+       g_clear_object (&book_cache);
+}
+
+/**
+ * e_book_meta_backend_get_connected_writable:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * This value has meaning only if e_book_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * Returns: Whether the @meta_backend connected to a writable destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_get_connected_writable (EBookMetaBackend *meta_backend)
+{
+       gboolean result;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       if (meta_backend->priv->connected_writable == -1) {
+               EBookCache *book_cache;
+
+               book_cache = e_book_meta_backend_ref_cache (meta_backend);
+               result = e_cache_get_key_int (E_CACHE (book_cache), EBMB_KEY_CONNECTED_WRITABLE, NULL) == 1;
+               g_clear_object (&book_cache);
+
+               meta_backend->priv->connected_writable = result ? 1 : 0;
+       } else {
+               result = meta_backend->priv->connected_writable == 1;
+       }
+
+       return result;
+}
+
+static void
+ebmb_cache_revision_changed_cb (ECache *cache,
+                               gpointer user_data)
+{
+       EBookMetaBackend *meta_backend = user_data;
+       gchar *revision;
+
+       g_return_if_fail (E_IS_CACHE (cache));
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+       revision = e_cache_dup_revision (cache);
+       if (revision) {
+               e_book_backend_notify_property_changed (E_BOOK_BACKEND (meta_backend),
+                       BOOK_BACKEND_PROPERTY_REVISION, revision);
+               g_free (revision);
+       }
+}
+
+/**
+ * e_book_meta_backend_set_cache:
+ * @meta_backend: an #EBookMetaBackend
+ * @cache: an #EBookCache to use
+ *
+ * Sets the @cache as the cache to be used by the @meta_backend.
+ * By default, a cache.db in EBookBackend::cache-dir is created
+ * in the constructed method. This function can be used to override
+ * the default.
+ *
+ * Note the @meta_backend adds its own reference to the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_set_cache (EBookMetaBackend *meta_backend,
+                              EBookCache *cache)
+{
+       g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+       g_return_if_fail (E_IS_BOOK_CACHE (cache));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->cache == cache) {
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       g_clear_error (&meta_backend->priv->create_cache_error);
+
+       if (meta_backend->priv->cache) {
+               g_signal_handler_disconnect (meta_backend->priv->cache,
+                       meta_backend->priv->revision_changed_id);
+       }
+
+       g_clear_object (&meta_backend->priv->cache);
+       meta_backend->priv->cache = g_object_ref (cache);
+
+       meta_backend->priv->revision_changed_id = g_signal_connect_object (meta_backend->priv->cache,
+               "revision-changed", G_CALLBACK (ebmb_cache_revision_changed_cb), meta_backend, 0);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       g_object_notify (G_OBJECT (meta_backend), "cache");
+}
+
+/**
+ * e_book_meta_backend_ref_cache:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Returns: (transfer full): Referenced #EBookCache, which is used by @meta_backend.
+ *    Unref it with g_object_unref(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_meta_backend_ref_cache (EBookMetaBackend *meta_backend)
+{
+       EBookCache *cache;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->cache)
+               cache = g_object_ref (meta_backend->priv->cache);
+       else
+               cache = NULL;
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return cache;
+}
+
+static gchar *
+ebmb_get_mime_type (const gchar *url,
+                   const gchar *content,
+                   gsize content_len)
+{
+       gchar *content_type, *filename = NULL, *mime_type = NULL;
+
+       if (url) {
+               filename = g_filename_from_uri (url, NULL, NULL);
+               if (filename) {
+                       gchar *extension;
+
+                       /* When storing inline attachments to the local file,
+                          the file extension is the mime type as stored in the attribute */
+                       extension = strrchr (filename, '.');
+                       if (extension)
+                               extension++;
+
+                       if (extension) {
+                               mime_type = g_uri_unescape_string (extension, NULL);
+                               if (mime_type && !strchr (mime_type, '/')) {
+                                       gchar *tmp;
+
+                                       tmp = g_strconcat ("image/", mime_type, NULL);
+
+                                       g_free (mime_type);
+                                       mime_type = tmp;
+                               }
+
+                               content_type = g_content_type_from_mime_type (mime_type);
+
+                               if (!content_type) {
+                                       g_free (mime_type);
+                                       mime_type = NULL;
+                               }
+
+                               g_free (content_type);
+                       }
+               }
+       }
+
+       if (!mime_type) {
+               content_type = g_content_type_guess (filename, (const guchar *) content, content_len, NULL);
+
+               if (content_type)
+                       mime_type = g_content_type_get_mime_type (content_type);
+
+               g_free (content_type);
+       }
+
+       g_free (filename);
+
+       return mime_type;
+}
+
+/**
+ * e_book_meta_backend_inline_local_photos_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @contact: an #EContact to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all URL photos and logos which point to a local file in @contact
+ * to inline type, aka adds the file content into the @contact.
+ * This is called automatically before e_book_meta_backend_save_contact_sync().
+ *
+ * The reverse operation is e_book_meta_backend_store_inline_photos_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_inline_local_photos_sync (EBookMetaBackend *meta_backend,
+                                             EContact *contact,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       GList *attributes, *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+       attributes = e_vcard_get_attributes (E_VCARD (contact));
+
+       for (link = attributes; link; link = g_list_next (link)) {
+               EVCardAttribute *attr = link->data;
+               const gchar *attr_name;
+               GList *values;
+
+               attr_name = e_vcard_attribute_get_name (attr);
+               if (!attr_name || (
+                   g_ascii_strcasecmp (attr_name, EVC_PHOTO) != 0 &&
+                   g_ascii_strcasecmp (attr_name, EVC_LOGO) != 0)) {
+                       continue;
+               }
+
+               values = e_vcard_attribute_get_param (attr, EVC_VALUE);
+               if (values && g_ascii_strcasecmp (values->data, "uri") == 0) {
+                       gchar *url;
+
+                       url = e_vcard_attribute_get_value (attr);
+                       if (url && g_str_has_prefix (url, LOCAL_PREFIX)) {
+                               GFile *file;
+                               gchar *basename;
+                               gchar *content;
+                               gsize len;
+
+                               file = g_file_new_for_uri (url);
+                               basename = g_file_get_basename (file);
+                               if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) {
+                                       gchar *mime_type;
+                                       const gchar *image_type, *pp;
+
+                                       mime_type = ebmb_get_mime_type (url, content, len);
+                                       if (mime_type && (pp = strchr (mime_type, '/'))) {
+                                               image_type = pp + 1;
+                                       } else {
+                                               image_type = "X-EVOLUTION-UNKNOWN";
+                                       }
+
+                                       e_vcard_attribute_remove_param (attr, EVC_TYPE);
+                                       e_vcard_attribute_remove_param (attr, EVC_ENCODING);
+                                       e_vcard_attribute_remove_param (attr, EVC_VALUE);
+                                       e_vcard_attribute_remove_values (attr);
+
+                                       e_vcard_attribute_add_param_with_value (attr, 
e_vcard_attribute_param_new (EVC_TYPE), image_type);
+                                       e_vcard_attribute_add_param_with_value (attr, 
e_vcard_attribute_param_new (EVC_ENCODING), "b");
+                                       e_vcard_attribute_add_value_decoded (attr, content, len);
+
+                                       g_free (mime_type);
+                                       g_free (content);
+                               } else {
+                                       success = FALSE;
+                               }
+
+                               g_object_unref (file);
+                               g_free (basename);
+                       }
+
+                       g_free (url);
+               }
+       }
+
+       return success;
+}
+
+static gchar *
+ebmb_create_photo_local_filename (EBookMetaBackend *meta_backend,
+                                 const gchar *uid,
+                                 const gchar *attr_name,
+                                 gint fileindex,
+                                 const gchar *type)
+{
+       EBookCache *book_cache;
+       gchar *local_filename, *cache_path, *checksum, *prefix, *extension, *filename;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+       g_return_val_if_fail (attr_name != NULL, NULL);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, NULL);
+
+       cache_path = g_path_get_dirname (e_cache_get_filename (E_CACHE (book_cache)));
+       checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, uid, -1);
+       prefix = g_strdup_printf ("%s-%s-%d", attr_name, checksum, fileindex);
+
+       if (type && *type)
+               extension = g_uri_escape_string (type, NULL, TRUE);
+       else
+               extension = NULL;
+
+       filename = g_strconcat (prefix, extension ? "." : NULL, extension, NULL);
+
+       local_filename = g_build_filename (cache_path, filename, NULL);
+
+       g_object_unref (book_cache);
+       g_free (cache_path);
+       g_free (checksum);
+       g_free (prefix);
+       g_free (extension);
+       g_free (filename);
+
+       return local_filename;
+}
+
+/**
+ * e_book_meta_backend_store_inline_photos_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @contact: an #EContact to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all inline photos and logos to URL type in @contact, which
+ * will point to a local file instead, beside the cache file.
+ * This is called automatically after e_book_meta_backend_load_contact_sync().
+ *
+ * The reverse operation is e_book_meta_backend_inline_local_photos_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_store_inline_photos_sync (EBookMetaBackend *meta_backend,
+                                             EContact *contact,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       gint fileindex;
+       GList *attributes, *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+       attributes = e_vcard_get_attributes (E_VCARD (contact));
+
+       for (link = attributes, fileindex = 0; link; link = g_list_next (link), fileindex++) {
+               EVCardAttribute *attr = link->data;
+               const gchar *attr_name;
+               GList *values;
+
+               attr_name = e_vcard_attribute_get_name (attr);
+               if (!attr_name || (
+                   g_ascii_strcasecmp (attr_name, EVC_PHOTO) != 0 &&
+                   g_ascii_strcasecmp (attr_name, EVC_LOGO) != 0)) {
+                       continue;
+               }
+
+               values = e_vcard_attribute_get_param (attr, EVC_ENCODING);
+               if (values && (g_ascii_strcasecmp (values->data, "b") == 0 || g_ascii_strcasecmp 
(values->data, "base64") == 0)) {
+                       values = e_vcard_attribute_get_values_decoded (attr);
+                       if (values && values->data) {
+                               const GString *decoded = values->data;
+                               gchar *local_filename;
+
+                               if (!decoded->len)
+                                       continue;
+
+                               values = e_vcard_attribute_get_param (attr, EVC_TYPE);
+
+                               local_filename = ebmb_create_photo_local_filename (meta_backend, 
e_contact_get_const (contact, E_CONTACT_UID),
+                                       attr_name, fileindex, values ? values->data : NULL);
+                               if (local_filename &&
+                                   g_file_set_contents (local_filename, decoded->str, decoded->len, error)) {
+                                       gchar *url;
+
+                                       e_vcard_attribute_remove_param (attr, EVC_TYPE);
+                                       e_vcard_attribute_remove_param (attr, EVC_ENCODING);
+                                       e_vcard_attribute_remove_param (attr, EVC_VALUE);
+                                       e_vcard_attribute_remove_values (attr);
+
+                                       url = g_filename_to_uri (local_filename, NULL, NULL);
+
+                                       e_vcard_attribute_add_param_with_value (attr, 
e_vcard_attribute_param_new (EVC_VALUE), "uri");
+                                       e_vcard_attribute_add_value (attr, url);
+
+                                       g_free (url);
+                               } else {
+                                       success = FALSE;
+                               }
+
+                               g_free (local_filename);
+                       }
+               }
+       }
+
+       return success;
+}
+
+/**
+ * e_book_meta_backend_empty_cache_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Empties the local cache by removing all known contacts from it
+ * and notifies about such removal any opened views.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_empty_cache_sync (EBookMetaBackend *meta_backend,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       EBookBackend *book_backend;
+       EBookCache *book_cache;
+       GSList *uids = NULL, *link;
+       gchar *cache_path, *cache_filename;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (book_cache != NULL, FALSE);
+
+       e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_WRITE);
+
+       book_backend = E_BOOK_BACKEND (meta_backend);
+
+       success = e_book_cache_search_uids (book_cache, NULL, &uids, cancellable, error);
+       if (success)
+               success = e_cache_remove_all (E_CACHE (book_cache), cancellable, error);
+
+       e_cache_unlock (E_CACHE (book_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       cache_path = g_path_get_dirname (e_cache_get_filename (E_CACHE (book_cache)));
+       cache_filename = g_path_get_basename (e_cache_get_filename (E_CACHE (book_cache)));
+
+       g_object_unref (book_cache);
+
+       if (success) {
+               GDir *dir;
+
+               for (link = uids; link; link = g_slist_next (link)) {
+                       const gchar *uid = link->data;
+
+                       if (!uid)
+                               continue;
+
+                       e_book_backend_notify_remove (book_backend, uid);
+               }
+
+               g_mutex_lock (&meta_backend->priv->property_lock);
+
+               for (link = meta_backend->priv->cursors; link; link = g_slist_next (link)) {
+                       EDataBookCursor *cursor = link->data;
+
+                       e_data_book_cursor_recalculate (cursor, cancellable, NULL);
+               }
+
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+
+               /* Remove also all photos and logos stored beside the cache */
+               dir = g_dir_open (cache_path, 0, NULL);
+               if (dir) {
+                       const gchar *filename;
+
+                       while (filename = g_dir_read_name (dir), filename) {
+                               if ((g_str_has_prefix (filename, EVC_PHOTO) ||
+                                   g_str_has_prefix (filename, EVC_LOGO)) &&
+                                   g_strcmp0 (cache_filename, filename) != 0) {
+                                       if (g_unlink (filename) == -1) {
+                                               /* Something failed, ignore the error */
+                                       }
+                               }
+                       }
+
+                       g_dir_close (dir);
+               }
+       }
+
+       g_slist_free_full (uids, g_free);
+       g_free (cache_filename);
+       g_free (cache_path);
+
+       return success;
+}
+
+/**
+ * e_book_meta_backend_connect_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @credentials: (nullable): an #ENamedParameters with previously used credentials, or %NULL
+ * @out_auth_result: (out): an #ESourceAuthenticationResult with an authentication result
+ * @out_certificate_pem: (out) (transfer full): a PEM encoded certificate on failure, or %NULL
+ * @out_certificate_errors: (out): a #GTlsCertificateFlags on failure, or 0
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called always before any operation which requires a connection
+ * to the remote side. It can fail with an #E_CLIENT_ERROR_REPOSITORY_OFFLINE
+ * error to indicate that the remote side cannot be currently reached. Other
+ * errors are propagated to the caller/client side. This method is not called
+ * when the backend is offline.
+ *
+ * The descendant should also call e_book_backend_set_writable() after successful
+ * connect to the remote side. This value is stored for later use, when being
+ * opened offline.
+ *
+ * The @credentials parameter consists of the previously used credentials.
+ * It's always %NULL with the first connection attempt. To get the credentials,
+ * just set the @out_auth_result to %E_SOURCE_AUTHENTICATION_REQUIRED for
+ * the first time and the function will be called again once the credentials
+ * are available. See the documentation of #ESourceAuthenticationResult for
+ * other available results.
+ *
+ * The out parameters are passed to e_backend_schedule_credentials_required()
+ * and are ignored when the descendant returns %TRUE, aka they are used
+ * only if the connection fails. The @out_certificate_pem and @out_certificate_errors
+ * should be used together and they can be left untouched if the failure reason was
+ * not related to certificate. Use @out_auth_result %E_SOURCE_AUTHENTICATION_UNKNOWN
+ * to indicate other error than @credentials error, otherwise the @error is used
+ * according to @out_auth_result value.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_connect_sync (EBookMetaBackend *meta_backend,
+                                 const ENamedParameters *credentials,
+                                 ESourceAuthenticationResult *out_auth_result,
+                                 gchar **out_certificate_pem,
+                                 GTlsCertificateFlags *out_certificate_errors,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->connect_sync != NULL, FALSE);
+
+       return klass->connect_sync (meta_backend, credentials, out_auth_result, out_certificate_pem, 
out_certificate_errors, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_disconnect_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called when the backend goes into offline mode or
+ * when the disconnect is required. The implementation should
+ * not report any error when it is called and the @meta_backend
+ * is not connected.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_disconnect_sync (EBookMetaBackend *meta_backend,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->disconnect_sync != NULL, FALSE);
+
+       return klass->disconnect_sync (meta_backend, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_get_changes_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @last_sync_tag: (nullable): optional sync tag from the last check
+ * @is_repeat: set to %TRUE when this is the repeated call
+ * @out_new_sync_tag: (out) (transfer full): new sync tag to store on success
+ * @out_repeat: (out): whether to repeat this call again; default is %FALSE
+ * @out_created_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ *    a #GSList of #EBookMetaBackendInfo object infos which had been created since
+ *    the last check
+ * @out_modified_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ *    a #GSList of #EBookMetaBackendInfo object infos which had been modified since
+ *    the last check
+ * @out_removed_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ *    a #GSList of #EBookMetaBackendInfo object infos which had been removed since
+ *    the last check
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the changes since the last check which had been done
+ * on the remote side.
+ *
+ * The @last_sync_tag can be used as a tag of the last check. This can be %NULL,
+ * when there was no previous call or when the descendant doesn't store any
+ * such tags. The @out_new_sync_tag can be populated with a value to be stored
+ * and used the next time.
+ *
+ * The @out_repeat can be set to %TRUE when the descendant didn't finish
+ * read of all the changes. In that case the @meta_backend calls this
+ * function again with the @out_new_sync_tag as the @last_sync_tag, but also
+ * notifies about the found changes immediately. The @is_repeat is set
+ * to %TRUE as well in this case, otherwise it's %FALSE.
+ *
+ * The descendant can populate also EBookMetaBackendInfo::object of
+ * the @out_created_objects and @out_modified_objects, if known, in which
+ * case this will be used instead of loading it with e_book_meta_backend_load_contact_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation calls e_book_meta_backend_list_existing_sync()
+ * and then compares the list with the current content of the local cache
+ * and populates the respective lists appropriately.
+ *
+ * Each output #GSList should be freed with
+ * g_slist_free_full (objects, e_book_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_get_changes_sync (EBookMetaBackend *meta_backend,
+                                     const gchar *last_sync_tag,
+                                     gboolean is_repeat,
+                                     gchar **out_new_sync_tag,
+                                     gboolean *out_repeat,
+                                     GSList **out_created_objects,
+                                     GSList **out_modified_objects,
+                                     GSList **out_removed_objects,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_repeat != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->get_changes_sync != NULL, FALSE);
+
+       return klass->get_changes_sync (meta_backend,
+               last_sync_tag,
+               is_repeat,
+               out_new_sync_tag,
+               out_repeat,
+               out_created_objects,
+               out_modified_objects,
+               out_removed_objects,
+               cancellable,
+               error);
+}
+
+/**
+ * e_book_meta_backend_list_existing_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @out_new_sync_tag: (out) (transfer full): optional return location for a new sync tag
+ * @out_existing_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ *    a #GSList of #EBookMetaBackendInfo object infos which are stored on the remote side
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Used to get list of all existing objects on the remote side. The descendant
+ * can optionally provide @out_new_sync_tag, which will be stored on success, if
+ * not %NULL. The descendant can populate also EBookMetaBackendInfo::object of
+ * the @out_existing_objects, if known, in which case this will be used instead
+ * of loading it with e_book_meta_backend_load_contact_sync().
+ *
+ * It is mandatory to implement this virtual method by the descendant, unless
+ * it implements its own get_changes_sync().
+ *
+ * The @out_existing_objects #GSList should be freed with
+ * g_slist_free_full (objects, e_book_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_list_existing_sync (EBookMetaBackend *meta_backend,
+                                       gchar **out_new_sync_tag,
+                                       GSList **out_existing_objects,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->list_existing_sync != NULL, FALSE);
+
+       return klass->list_existing_sync (meta_backend, out_new_sync_tag, out_existing_objects, cancellable, 
error);
+}
+
+/**
+ * e_book_meta_backend_load_contact_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @uid: a contact UID
+ * @extra: (nullable): optional extra data stored with the contact, or %NULL
+ * @out_contact: (out) (transfer full): a loaded contact, as an #EContact
+ * @out_extra: (out) (transfer full): an extra data to store to #EBookCache with this contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Loads a contact from the remote side.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * The returned @out_contact should be freed with g_object_unref(),
+ * when no longer needed.
+ *
+ * The returned @out_extra should be freed with g_free(), when no longer
+ * needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_load_contact_sync (EBookMetaBackend *meta_backend,
+                                      const gchar *uid,
+                                      const gchar *extra,
+                                      EContact **out_contact,
+                                      gchar **out_extra,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_contact != NULL, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->load_contact_sync != NULL, FALSE);
+
+       return klass->load_contact_sync (meta_backend, uid, extra, out_contact, out_extra, cancellable, 
error);
+}
+
+/**
+ * e_book_meta_backend_save_contact_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @overwrite_existing: %TRUE when can overwrite existing contacts, %FALSE otherwise
+ * @conflict_resolution: one of #EConflictResolution, what to do on conflicts
+ * @contact: an #EContact to save
+ * @extra: (nullable): extra data saved with the contacts in an #EBookCache
+ * @out_new_uid: (out) (transfer full): return location for the UID of the saved contact
+ * @out_new_extra: (out) (transfer full): return location for the extra data to store with the contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Saves one contact into the remote side.  When the @overwrite_existing is %TRUE, then
+ * the descendant can overwrite an object with the same UID on the remote side
+ * (usually used for modify). The @conflict_resolution defines what to do when
+ * the remote side had made any changes to the object since the last update.
+ *
+ * The @contact has already converted locally stored photos and logos
+ * into inline variants, thus it's not needed to call
+ * e_book_meta_backend_inline_local_photos_sync() by the descendant.
+ *
+ * The @out_new_uid can be populated with a UID of the saved contact as the server
+ * assigned it to it. This UID, if set, is loaded from the remote side afterwards,
+ * also to see whether any changes had been made to the contact by the remote side.
+ *
+ * The @out_new_extra can be populated with a new extra data to save with the contact.
+ * Left it %NULL, to keep the same value as the @extra.
+ *
+ * The descendant can use an #E_CLIENT_ERROR_OUT_OF_SYNC error to indicate that
+ * the save failed due to made changes on the remote side, and let the @meta_backend
+ * resolve this conflict based on the @conflict_resolution on its own.
+ * The #E_CLIENT_ERROR_OUT_OF_SYNC error should not be used when the descendant
+ * is able to resolve the conflicts itself.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_save_contact_sync (EBookMetaBackend *meta_backend,
+                                      gboolean overwrite_existing,
+                                      EConflictResolution conflict_resolution,
+                                      /* const */ EContact *contact,
+                                      const gchar *extra,
+                                      gchar **out_new_uid,
+                                      gchar **out_new_extra,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
+       g_return_val_if_fail (out_new_extra != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+
+       if (!klass->save_contact_sync) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_NOT_SUPPORTED, NULL));
+               return FALSE;
+       }
+
+       return klass->save_contact_sync (meta_backend,
+               overwrite_existing,
+               conflict_resolution,
+               contact,
+               extra,
+               out_new_uid,
+               out_new_extra,
+               cancellable,
+               error);
+}
+
+/**
+ * e_book_meta_backend_remove_contact_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @conflict_resolution: an #EConflictResolution to use
+ * @uid: a contact UID
+ * @extra: (nullable): extra data being saved with the contact in the local cache, or %NULL
+ * @object: (nullable): corresponding vCard object, as stored in the local cache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes a contact from the remote side. The @object is not %NULL when
+ * it's removing locally deleted object in offline mode. Being it %NULL,
+ * the descendant can obtain the object from the #EBookCache.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_remove_contact_sync (EBookMetaBackend *meta_backend,
+                                        EConflictResolution conflict_resolution,
+                                        const gchar *uid,
+                                        const gchar *extra,
+                                        const gchar *object,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+
+       if (!klass->remove_contact_sync) {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_NOT_SUPPORTED, NULL));
+               return FALSE;
+       }
+
+       return klass->remove_contact_sync (meta_backend, conflict_resolution, uid, extra, object, 
cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_search_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @meta_contact: %TRUE, when return #EContact filled with UID and REV only, %FALSE to return full contacts
+ * @out_contacts: (out) (transfer full) (element-type EContact): return location for the found contacts as 
#EContact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found contacts as a #GSList of #EContact @out_contacts.
+ * Free the returned @out_contacts with g_slist_free_full (contacts, g_object_unref);
+ * when no longer needed.
+ * When the @expr is %NULL, all objects are returned. To get
+ * UID-s instead, call e_book_meta_backend_search_uids_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_search_sync (EBookMetaBackend *meta_backend,
+                                const gchar *expr,
+                                gboolean meta_contact,
+                                GSList **out_contacts,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->search_sync != NULL, FALSE);
+
+       return klass->search_sync (meta_backend, expr, meta_contact, out_contacts, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_search_uids_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @out_uids: (out) (transfer full) (element-type utf8): return location for the found contact UID-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found contact UID-s as a #GSList @out_contacts.
+ * Free the returned @out_uids with g_slist_free_full (uids, g_free);
+ * when no longer needed.
+ * When the @expr is %NULL, all UID-s are returned. To get #EContact-s
+ * instead, call e_book_meta_backend_search_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_search_uids_sync (EBookMetaBackend *meta_backend,
+                                     const gchar *expr,
+                                     GSList **out_uids,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_uids != NULL, FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->search_uids_sync != NULL, FALSE);
+
+       return klass->search_uids_sync (meta_backend, expr, out_uids, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_requires_reconnect:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Determines, whether current source content requires reconnect of the backend.
+ *
+ * It is optional to implement this virtual method by the descendant. The default
+ * implementation compares %E_SOURCE_EXTENSION_AUTHENTICATION and
+ * %E_SOURCE_EXTENSION_WEBDAV_BACKEND, if existing in the source,
+ * with the values after the last successful connect and returns
+ * %TRUE when they changed. It always return %TRUE when there was
+ * no successful connect done yet.
+ *
+ * Returns: %TRUE, when reconnect is required, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_requires_reconnect (EBookMetaBackend *meta_backend)
+{
+       EBookMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+       klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->requires_reconnect != NULL, FALSE);
+
+       return klass->requires_reconnect (meta_backend);
+}
diff --git a/src/addressbook/libedata-book/e-book-meta-backend.h 
b/src/addressbook/libedata-book/e-book-meta-backend.h
new file mode 100644
index 0000000..740f321
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-meta-backend.h
@@ -0,0 +1,276 @@
+/* -*- 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 (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_META_BACKEND_H
+#define E_BOOK_META_BACKEND_H
+
+#include <libebackend/libebackend.h>
+#include <libedata-book/e-book-backend.h>
+#include <libedata-book/e-book-cache.h>
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_META_BACKEND \
+       (e_book_meta_backend_get_type ())
+#define E_BOOK_META_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_BOOK_META_BACKEND, EBookMetaBackend))
+#define E_BOOK_META_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_BOOK_META_BACKEND, EBookMetaBackendClass))
+#define E_IS_BOOK_META_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_BOOK_META_BACKEND))
+#define E_IS_BOOK_META_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_BOOK_META_BACKEND))
+#define E_BOOK_META_BACKEND_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_BOOK_META_BACKEND, EBookMetaBackendClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookMetaBackendInfo {
+       gchar *uid;
+       gchar *revision;
+       gchar *object;
+       gchar *extra;
+} EBookMetaBackendInfo;
+
+#define E_TYPE_BOOK_META_BACKEND_INFO (e_book_meta_backend_info_get_type ())
+
+GType          e_book_meta_backend_info_get_type
+                                               (void) G_GNUC_CONST;
+EBookMetaBackendInfo *
+               e_book_meta_backend_info_new    (const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const gchar *extra);
+EBookMetaBackendInfo *
+               e_book_meta_backend_info_copy   (const EBookMetaBackendInfo *src);
+void           e_book_meta_backend_info_free   (gpointer ptr /* EBookMetaBackendInfo * */);
+
+typedef struct _EBookMetaBackend EBookMetaBackend;
+typedef struct _EBookMetaBackendClass EBookMetaBackendClass;
+typedef struct _EBookMetaBackendPrivate EBookMetaBackendPrivate;
+
+/**
+ * EBookMetaBackend:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EBookMetaBackend {
+       /*< private >*/
+       EBookBackend parent;
+       EBookMetaBackendPrivate *priv;
+};
+
+/**
+ * EBookMetaBackendClass:
+ *
+ * Class structure for the #EBookMetaBackend class.
+ *
+ * Since: 3.26
+ */
+struct _EBookMetaBackendClass {
+       /*< private >*/
+       EBookBackendClass parent_class;
+
+       /* For Direct Read Access */
+       const gchar *backend_module_filename;
+       const gchar *backend_factory_type_name;
+
+       /* Virtual methods */
+       gboolean        (* connect_sync)        (EBookMetaBackend *meta_backend,
+                                                const ENamedParameters *credentials,
+                                                ESourceAuthenticationResult *out_auth_result,
+                                                gchar **out_certificate_pem,
+                                                GTlsCertificateFlags *out_certificate_errors,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* disconnect_sync)     (EBookMetaBackend *meta_backend,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+       gboolean        (* get_changes_sync)    (EBookMetaBackend *meta_backend,
+                                                const gchar *last_sync_tag,
+                                                gboolean is_repeat,
+                                                gchar **out_new_sync_tag,
+                                                gboolean *out_repeat,
+                                                GSList **out_created_objects, /* EBookMetaBackendInfo * */
+                                                GSList **out_modified_objects, /* EBookMetaBackendInfo * */
+                                                GSList **out_removed_objects, /* EBookMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* list_existing_sync)  (EBookMetaBackend *meta_backend,
+                                                gchar **out_new_sync_tag,
+                                                GSList **out_existing_objects, /* EBookMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* load_contact_sync)   (EBookMetaBackend *meta_backend,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                EContact **out_contact,
+                                                gchar **out_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* save_contact_sync)   (EBookMetaBackend *meta_backend,
+                                                gboolean overwrite_existing,
+                                                EConflictResolution conflict_resolution,
+                                                /* const */ EContact *contact,
+                                                const gchar *extra,
+                                                gchar **out_new_uid,
+                                                gchar **out_new_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* remove_contact_sync) (EBookMetaBackend *meta_backend,
+                                                EConflictResolution conflict_resolution,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                const gchar *object,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* search_sync)         (EBookMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                gboolean meta_contact,
+                                                GSList **out_contacts, /* EContact * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* search_uids_sync)    (EBookMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                GSList **out_uids, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* requires_reconnect)  (EBookMetaBackend *meta_backend);
+
+       /* Signals */
+       void            (* source_changed)      (EBookMetaBackend *meta_backend);
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_book_meta_backend_get_type    (void) G_GNUC_CONST;
+
+const gchar *  e_book_meta_backend_get_capabilities
+                                               (EBookMetaBackend *meta_backend);
+void           e_book_meta_backend_set_ever_connected
+                                               (EBookMetaBackend *meta_backend,
+                                                gboolean value);
+gboolean       e_book_meta_backend_get_ever_connected
+                                               (EBookMetaBackend *meta_backend);
+void           e_book_meta_backend_set_connected_writable
+                                               (EBookMetaBackend *meta_backend,
+                                                gboolean value);
+gboolean       e_book_meta_backend_get_connected_writable
+                                               (EBookMetaBackend *meta_backend);
+void           e_book_meta_backend_set_cache   (EBookMetaBackend *meta_backend,
+                                                EBookCache *cache);
+EBookCache *   e_book_meta_backend_ref_cache   (EBookMetaBackend *meta_backend);
+gboolean       e_book_meta_backend_inline_local_photos_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                EContact *contact,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_store_inline_photos_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                EContact *contact,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_empty_cache_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_connect_sync(EBookMetaBackend *meta_backend,
+                                                const ENamedParameters *credentials,
+                                                ESourceAuthenticationResult *out_auth_result,
+                                                gchar **out_certificate_pem,
+                                                GTlsCertificateFlags *out_certificate_errors,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_disconnect_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_get_changes_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                const gchar *last_sync_tag,
+                                                gboolean is_repeat,
+                                                gchar **out_new_sync_tag,
+                                                gboolean *out_repeat,
+                                                GSList **out_created_objects, /* EBookMetaBackendInfo * */
+                                                GSList **out_modified_objects, /* EBookMetaBackendInfo * */
+                                                GSList **out_removed_objects, /* EBookMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_list_existing_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                gchar **out_new_sync_tag,
+                                                GSList **out_existing_objects, /* EBookMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_load_contact_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                EContact **out_contact,
+                                                gchar **out_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_save_contact_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                gboolean overwrite_existing,
+                                                EConflictResolution conflict_resolution,
+                                                /* const */ EContact *contact,
+                                                const gchar *extra,
+                                                gchar **out_new_uid,
+                                                gchar **out_new_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_remove_contact_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                EConflictResolution conflict_resolution,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                const gchar *object,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_search_sync (EBookMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                gboolean meta_contact,
+                                                GSList **out_contacts, /* EContact * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_search_uids_sync
+                                               (EBookMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                GSList **out_uids, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_book_meta_backend_requires_reconnect
+                                               (EBookMetaBackend *meta_backend);
+
+G_END_DECLS
+
+#endif /* E_BOOK_META_BACKEND_H */
diff --git a/src/addressbook/libedata-book/e-data-book-cursor-cache.c 
b/src/addressbook/libedata-book/e-data-book-cursor-cache.c
new file mode 100644
index 0000000..e6c22ce
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor-cache.c
@@ -0,0 +1,438 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+/**
+ * SECTION: e-data-book-cursor-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: The SQLite cursor implementation
+ *
+ * This cursor implementation can be used with any backend which
+ * stores contacts using #EBookCache.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n.h>
+
+#include "e-data-book-cursor-cache.h"
+
+struct _EDataBookCursorCachePrivate {
+       EBookCache *book_cache;
+       EBookCacheCursor *cursor;
+};
+
+enum {
+       PROP_0,
+       PROP_BOOK_CACHE,
+       PROP_CURSOR,
+};
+
+G_DEFINE_TYPE (EDataBookCursorCache, e_data_book_cursor_cache, E_TYPE_DATA_BOOK_CURSOR);
+
+static gboolean
+edbcc_set_sexp (EDataBookCursor *cursor,
+               const gchar *sexp,
+               GError **error)
+{
+       EDataBookCursorCache *cache_cursor;
+       gboolean success;
+       GError *local_error = NULL;
+
+       cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+       success = e_book_cache_cursor_set_sexp (cache_cursor->priv->book_cache, cache_cursor->priv->cursor, 
sexp, &local_error);
+
+       if (!success) {
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY)) {
+                       g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_QUERY, 
local_error->message);
+                       g_clear_error (&local_error);
+               } else {
+                       g_propagate_error (error, local_error);
+               }
+       }
+
+       return success;
+}
+
+static gboolean
+convert_origin (EBookCursorOrigin src_origin,
+               EBookCacheCursorOrigin *dest_origin,
+               GError **error)
+{
+       gboolean success = TRUE;
+
+       switch (src_origin) {
+       case E_BOOK_CURSOR_ORIGIN_CURRENT:
+               *dest_origin = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+               break;
+       case E_BOOK_CURSOR_ORIGIN_BEGIN:
+               *dest_origin = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+               break;
+       case E_BOOK_CURSOR_ORIGIN_END:
+               *dest_origin = E_BOOK_CACHE_CURSOR_ORIGIN_END;
+               break;
+       default:
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_ARG, _("Unrecognized 
cursor origin"));
+               success = FALSE;
+               break;
+       }
+
+       return success;
+}
+
+static void
+convert_flags (EBookCursorStepFlags src_flags,
+              EBookCacheCursorStepFlags *dest_flags)
+{
+       if (src_flags & E_BOOK_CURSOR_STEP_MOVE)
+               *dest_flags |= E_BOOK_CACHE_CURSOR_STEP_MOVE;
+
+       if (src_flags & E_BOOK_CURSOR_STEP_FETCH)
+               *dest_flags |= E_BOOK_CACHE_CURSOR_STEP_FETCH;
+}
+
+static gint
+edbcc_step (EDataBookCursor *cursor,
+           const gchar *revision_guard,
+           EBookCursorStepFlags flags,
+           EBookCursorOrigin origin,
+           gint count,
+           GSList **results,
+           GCancellable *cancellable,
+           GError **error)
+{
+       EDataBookCursorCache *cache_cursor;
+       GSList *local_results = NULL, *local_converted_results = NULL, *link;
+       EBookCacheCursorOrigin cache_origin = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+       EBookCacheCursorStepFlags cache_flags = 0;
+       gchar *revision = NULL;
+       gboolean success = TRUE;
+       gint n_results = -1;
+
+       cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+       if (!convert_origin (origin, &cache_origin, error))
+               return FALSE;
+
+       convert_flags (flags, &cache_flags);
+
+       /* Here we check the EBookCache revision
+        * against the revision_guard with an atomic transaction
+        * with the cache.
+        *
+        * The addressbook modifications and revision changes
+        * are also atomically committed to the SQLite.
+        */
+       e_cache_lock (E_CACHE (cache_cursor->priv->book_cache), E_CACHE_LOCK_READ);
+
+       if (revision_guard)
+               revision = e_cache_dup_revision (E_CACHE (cache_cursor->priv->book_cache));
+
+       if (revision_guard &&
+           g_strcmp0 (revision, revision_guard) != 0) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC,
+                       _("Out of sync revision while moving cursor"));
+               success = FALSE;
+       }
+
+       if (success) {
+               GError *local_error = NULL;
+
+               n_results = e_book_cache_cursor_step (
+                       cache_cursor->priv->book_cache,
+                       cache_cursor->priv->cursor,
+                       cache_flags,
+                       cache_origin,
+                       count,
+                       &local_results,
+                       cancellable,
+                       &local_error);
+
+               if (n_results < 0) {
+
+                       /* Convert the SQLite backend error to an EClient error */
+                       if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST)) {
+                               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_QUERY_REFUSED, 
local_error->message);
+                               g_clear_error (&local_error);
+                       } else {
+                               g_propagate_error (error, local_error);
+                       }
+
+                       success = FALSE;
+               }
+       }
+
+       e_cache_unlock (E_CACHE (cache_cursor->priv->book_cache), E_CACHE_UNLOCK_NONE);
+
+       for (link = local_results; link; link = link->next) {
+               EBookCacheSearchData *data = link->data;
+
+               local_converted_results = g_slist_prepend (local_converted_results, data->vcard);
+               data->vcard = NULL;
+       }
+
+       g_slist_free_full (local_results, e_book_cache_search_data_free);
+
+       if (results)
+               *results = g_slist_reverse (local_converted_results);
+       else
+               g_slist_free_full (local_converted_results, g_free);
+
+       g_free (revision);
+
+       if (success)
+               return n_results;
+
+       return -1;
+}
+
+static gboolean
+edbcc_set_alphabetic_index (EDataBookCursor *cursor,
+                           gint index,
+                           const gchar *locale,
+                           GError **error)
+{
+       EDataBookCursorCache *cache_cursor;
+       gchar *current_locale;
+
+       cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+       current_locale = e_book_cache_dup_locale (cache_cursor->priv->book_cache);
+
+       /* Locale mismatch, need to report error */
+       if (g_strcmp0 (current_locale, locale) != 0) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC,
+                       _("Alphabetic index was set for incorrect locale"));
+               g_free (current_locale);
+
+               return FALSE;
+       }
+
+       e_book_cache_cursor_set_target_alphabetic_index (
+               cache_cursor->priv->book_cache,
+               cache_cursor->priv->cursor,
+               index);
+
+       g_free (current_locale);
+
+       return TRUE;
+}
+
+static gboolean
+edbcc_get_position (EDataBookCursor *cursor,
+                   gint *total,
+                   gint *position,
+                   GCancellable *cancellable,
+                   GError **error)
+{
+       EDataBookCursorCache *cache_cursor;
+
+       cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+       return e_book_cache_cursor_calculate (
+               cache_cursor->priv->book_cache,
+               cache_cursor->priv->cursor,
+               total,
+               position,
+               cancellable,
+               error);
+}
+
+static gint
+edbcc_compare_contact (EDataBookCursor *cursor,
+                      EContact *contact,
+                      gboolean *matches_sexp)
+{
+       EDataBookCursorCache *cache_cursor;
+
+       cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+       return e_book_cache_cursor_compare_contact (
+               cache_cursor->priv->book_cache,
+               cache_cursor->priv->cursor,
+               contact,
+               matches_sexp);
+}
+
+static gboolean
+edbcc_load_locale (EDataBookCursor *cursor,
+                  gchar **out_locale,
+                  GError **error)
+{
+       EDataBookCursorCache *cache_cursor;
+
+       g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR_CACHE (cursor), FALSE);
+       g_return_val_if_fail (out_locale != NULL, FALSE);
+
+       cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+       *out_locale = e_book_cache_dup_locale (cache_cursor->priv->book_cache);
+
+       return TRUE;
+}
+
+static void
+e_data_book_cursor_cache_set_property (GObject *object,
+                                      guint property_id,
+                                      const GValue *value,
+                                      GParamSpec *pspec)
+{
+       EDataBookCursorCache *cache_cursor = E_DATA_BOOK_CURSOR_CACHE (object);
+
+       switch (property_id) {
+
+       case PROP_BOOK_CACHE:
+               /* Construct-only, can only be set once */
+               cache_cursor->priv->book_cache = g_value_dup_object (value);
+               return;
+
+       case PROP_CURSOR:
+               /* Construct-only, can only be set once */
+               cache_cursor->priv->cursor = g_value_get_pointer (value);
+               return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_data_book_cursor_cache_dispose (GObject *object)
+{
+       EDataBookCursorCache *cache_cursor = E_DATA_BOOK_CURSOR_CACHE (object);
+
+       if (cache_cursor->priv->book_cache) {
+               if (cache_cursor->priv->cursor) {
+                       e_book_cache_cursor_free (cache_cursor->priv->book_cache, cache_cursor->priv->cursor);
+                       cache_cursor->priv->cursor = NULL;
+               }
+
+               g_clear_object (&cache_cursor->priv->book_cache);
+       }
+
+       /* Chain up to parent's method */
+       G_OBJECT_CLASS (e_data_book_cursor_cache_parent_class)->dispose (object);
+}
+
+static void
+e_data_book_cursor_cache_class_init (EDataBookCursorCacheClass *class)
+{
+       GObjectClass *object_class;
+       EDataBookCursorClass *cursor_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = e_data_book_cursor_cache_set_property;
+       object_class->dispose = e_data_book_cursor_cache_dispose;
+
+       cursor_class = E_DATA_BOOK_CURSOR_CLASS (class);
+       cursor_class->set_sexp = edbcc_set_sexp;
+       cursor_class->step = edbcc_step;
+       cursor_class->set_alphabetic_index = edbcc_set_alphabetic_index;
+       cursor_class->get_position = edbcc_get_position;
+       cursor_class->compare_contact = edbcc_compare_contact;
+       cursor_class->load_locale = edbcc_load_locale;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_BOOK_CACHE,
+               g_param_spec_object (
+                       "book-cache",
+                       "Book Cache",
+                       "The EBookCache to use for queries",
+                       E_TYPE_BOOK_CACHE,
+                       G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_CURSOR,
+               g_param_spec_pointer (
+                       "cursor",
+                       "Cursor",
+                       "The EBookCacheCursor pointer",
+                       G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_type_class_add_private (class, sizeof (EDataBookCursorCachePrivate));
+}
+
+static void
+e_data_book_cursor_cache_init (EDataBookCursorCache *cache_cursor)
+{
+       cache_cursor->priv = G_TYPE_INSTANCE_GET_PRIVATE (cache_cursor, E_TYPE_DATA_BOOK_CURSOR_CACHE, 
EDataBookCursorCachePrivate);
+}
+
+/**
+ * e_data_book_cursor_cache_new:
+ * @book_backend: the #EBookBackend creating this cursor
+ * @book_cache: the #EBookCache object to base this cursor on
+ * @sort_fields: (array length=n_fields): an array of #EContactFields as sort keys in order of priority
+ * @sort_types: (array length=n_fields): an array of #EBookCursorSortTypes, one for each field in 
@sort_fields
+ * @n_fields: the number of fields to sort results by.
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates an #EDataBookCursor and implements all of the cursor methods
+ * using the delegate @book_cache object.
+ *
+ * This is suitable cursor type for any backend which stores its contacts
+ * using the #EBookCache object. The #EBookMetaBackend does that transparently.
+ *
+ * Returns: (transfer full): A newly created #EDataBookCursor, or %NULL if cursor creation failed.
+ *
+ * Since: 3.26
+ */
+EDataBookCursor *
+e_data_book_cursor_cache_new (EBookBackend *book_backend,
+                             EBookCache *book_cache,
+                             const EContactField *sort_fields,
+                             const EBookCursorSortType *sort_types,
+                             guint n_fields,
+                             GError **error)
+{
+       EDataBookCursor *cursor = NULL;
+       EBookCacheCursor *cache_cursor;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND (book_backend), NULL);
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+       cache_cursor = e_book_cache_cursor_new (
+               book_cache, NULL,
+               sort_fields,
+               sort_types,
+               n_fields,
+               &local_error);
+
+       if (cache_cursor) {
+               cursor = g_object_new (E_TYPE_DATA_BOOK_CURSOR_CACHE,
+                       "book-cache", book_cache,
+                       "cursor", cache_cursor,
+                       NULL);
+
+               /* Initially created cursors should have a position & total */
+               if (!e_data_book_cursor_load_locale (E_DATA_BOOK_CURSOR (cursor), NULL, NULL, error))
+                       g_clear_object (&cursor);
+
+       } else if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY)) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_QUERY, 
local_error->message);
+               g_clear_error (&local_error);
+       } else {
+               g_propagate_error (error, local_error);
+       }
+
+       return cursor;
+}
diff --git a/src/addressbook/libedata-book/e-data-book-cursor-cache.h 
b/src/addressbook/libedata-book/e-data-book-cursor-cache.h
new file mode 100644
index 0000000..a443d86
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor-cache.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_CURSOR_CACHE_H
+#define E_DATA_BOOK_CURSOR_CACHE_H
+
+#include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-book-cache.h>
+#include <libedata-book/e-book-backend.h>
+
+#define E_TYPE_DATA_BOOK_CURSOR_CACHE        (e_data_book_cursor_cache_get_type ())
+#define E_DATA_BOOK_CURSOR_CACHE(o)          (G_TYPE_CHECK_INSTANCE_CAST ((o), 
E_TYPE_DATA_BOOK_CURSOR_CACHE, EDataBookCursorCache))
+#define E_DATA_BOOK_CURSOR_CACHE_CLASS(k)    (G_TYPE_CHECK_CLASS_CAST((k), E_TYPE_DATA_BOOK_CURSOR_CACHE, 
EDataBookCursorCacheClass))
+#define E_IS_DATA_BOOK_CURSOR_CACHE(o)       (G_TYPE_CHECK_INSTANCE_TYPE ((o), 
E_TYPE_DATA_BOOK_CURSOR_CACHE))
+#define E_IS_DATA_BOOK_CURSOR_CACHE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_TYPE_DATA_BOOK_CURSOR_CACHE))
+#define E_DATA_BOOK_CURSOR_CACHE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), 
E_TYPE_DATA_BOOK_CURSOR_CACHE, EDataBookCursorCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EDataBookCursorCache EDataBookCursorCache;
+typedef struct _EDataBookCursorCacheClass EDataBookCursorCacheClass;
+typedef struct _EDataBookCursorCachePrivate EDataBookCursorCachePrivate;
+
+/**
+ * EDataBookCursorCache:
+ *
+ * An opaque handle for the #EBookCache cursor instance.
+ *
+ * Since: 3.26
+ */
+struct _EDataBookCursorCache {
+       /*< private >*/
+       EDataBookCursor parent;
+       EDataBookCursorCachePrivate *priv;
+};
+
+/**
+ * EDataBookCursorCacheClass:
+ *
+ * The #EBookCache cursor class structure.
+ *
+ * Since: 3.26
+ */
+struct _EDataBookCursorCacheClass {
+       /*< private >*/
+       EDataBookCursorClass parent;
+};
+
+GType          e_data_book_cursor_cache_get_type       (void);
+EDataBookCursor *
+               e_data_book_cursor_cache_new            (EBookBackend *book_backend,
+                                                        EBookCache *book_cache,
+                                                        const EContactField *sort_fields,
+                                                        const EBookCursorSortType *sort_types,
+                                                        guint n_fields,
+                                                        GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_CURSOR_CACHE_H */
diff --git a/src/addressbook/libedata-book/libedata-book.h b/src/addressbook/libedata-book/libedata-book.h
index ff070ec..50ee041 100644
--- a/src/addressbook/libedata-book/libedata-book.h
+++ b/src/addressbook/libedata-book/libedata-book.h
@@ -29,8 +29,11 @@
 #include <libedata-book/e-book-backend-sqlitedb.h>
 #include <libedata-book/e-book-backend-summary.h>
 #include <libedata-book/e-book-backend.h>
+#include <libedata-book/e-book-cache.h>
+#include <libedata-book/e-book-meta-backend.h>
 #include <libedata-book/e-book-sqlite.h>
 #include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-data-book-cursor-cache.h>
 #include <libedata-book/e-data-book-cursor-sqlite.h>
 #include <libedata-book/e-data-book-direct.h>
 #include <libedata-book/e-data-book-factory.h>
diff --git a/src/calendar/backends/caldav/e-cal-backend-caldav.c 
b/src/calendar/backends/caldav/e-cal-backend-caldav.c
index 8f0f601..d2ae19b 100644
--- a/src/calendar/backends/caldav/e-cal-backend-caldav.c
+++ b/src/calendar/backends/caldav/e-cal-backend-caldav.c
@@ -1,7 +1,6 @@
 /*
- * Evolution calendar - caldav backend
- *
  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * 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
@@ -21,89 +20,25 @@
 #include "evolution-data-server-config.h"
 
 #include <string.h>
-#include <unistd.h>
-#include <glib/gstdio.h>
 #include <glib/gi18n-lib.h>
 
-/* LibXML2 includes */
-#include <libxml/parser.h>
-#include <libxml/tree.h>
-#include <libxml/xpath.h>
-#include <libxml/xpathInternals.h>
-
-/* LibSoup includes */
-#include <libsoup/soup.h>
-
 #include <libedataserver/libedataserver.h>
 
 #include "e-cal-backend-caldav.h"
 
-#define d(x)
-
-#define E_CAL_BACKEND_CALDAV_GET_PRIVATE(obj) \
-       (G_TYPE_INSTANCE_GET_PRIVATE \
-       ((obj), E_TYPE_CAL_BACKEND_CALDAV, ECalBackendCalDAVPrivate))
+#define E_CALDAV_MAX_MULTIGET_AMOUNT 100 /* what's the maximum count of items to fetch within a multiget 
request */
 
-#define CALDAV_CTAG_KEY "CALDAV_CTAG"
-#define CALDAV_MAX_MULTIGET_AMOUNT 100 /* what's the maximum count of items to fetch within a multiget 
request */
-#define LOCAL_PREFIX "file://"
-
-/* in seconds */
-#define DEFAULT_REFRESH_TIME 60
+#define E_CALDAV_X_ETAG "X-EVOLUTION-CALDAV-ETAG"
 
 #define EDC_ERROR(_code) e_data_cal_create_error (_code, NULL)
 #define EDC_ERROR_EX(_code, _msg) e_data_cal_create_error (_code, _msg)
 
-typedef enum {
-
-       SLAVE_SHOULD_SLEEP,
-       SLAVE_SHOULD_WORK,
-       SLAVE_SHOULD_WORK_NO_CTAG_CHECK,
-       SLAVE_SHOULD_DIE
-
-} SlaveCommand;
-
-/* Private part of the ECalBackendHttp structure */
 struct _ECalBackendCalDAVPrivate {
-
-       /* The local disk cache */
-       ECalBackendStore *store;
-
-       /* should we sync for offline mode? */
-       gboolean do_offline;
-
-       /* TRUE after caldav_open */
-       gboolean loaded;
-       /* TRUE when server reachable */
-       gboolean opened;
-
-       /* lock to indicate a busy state */
-       GMutex busy_lock;
-
-       /* cond to synch threads */
-       GCond cond;
-
-       /* cond to know the slave gone */
-       GCond slave_gone_cond;
-
-       /* BG synch thread */
-       const GThread *synch_slave; /* just for a reference, whether thread exists */
-       SlaveCommand slave_cmd;
-       gboolean slave_busy; /* whether is slave working */
-
-       /* The main soup session  */
-       SoupSession *session;
-
-       /* clandar uri */
-       gchar *uri;
-
-       /* Authentication info */
-       ENamedParameters *credentials;
-       gboolean auth_required;
+       /* The main WebDAV session  */
+       EWebDAVSession *webdav;
 
        /* support for 'getctag' extension */
        gboolean ctag_supported;
-       gchar *ctag_to_store;
 
        /* TRUE when 'calendar-schedule' supported on the server */
        gboolean calendar_schedule;
@@ -119,4515 +54,1238 @@ struct _ECalBackendCalDAVPrivate {
 
        /* The iCloud.com requires timezone IDs as locations */
        gboolean is_icloud;
-
-       /* set to true if thread for ESource::changed is invoked */
-       gboolean updating_source;
-
-       guint refresh_id;
-
-       /* If we fail to obtain an OAuth2 access token,
-        * soup_authenticate_bearer() stashes an error
-        * here to be claimed in caldav_credentials_required_sync().
-        * This lets us propagate a more useful error
-        * message than a generic SOUP_STATUS_UNAUTHORIZED description. */
-       GError *bearer_auth_error;
-       GMutex bearer_auth_error_lock;
-       ESoupAuthBearer *using_bearer_auth;
 };
 
-/* Forward Declarations */
-static void    e_caldav_backend_initable_init
-                               (GInitableIface *interface);
-
-G_DEFINE_TYPE_WITH_CODE (
-       ECalBackendCalDAV,
-       e_cal_backend_caldav,
-       E_TYPE_CAL_BACKEND_SYNC,
-       G_IMPLEMENT_INTERFACE (
-               G_TYPE_INITABLE,
-               e_caldav_backend_initable_init))
-
-/* ************************************************************************* */
-/* Debugging */
-
-#define DEBUG_MESSAGE "message"
-#define DEBUG_MESSAGE_HEADER "message:header"
-#define DEBUG_MESSAGE_BODY "message:body"
-#define DEBUG_SERVER_ITEMS "items"
-#define DEBUG_ATTACHMENTS "attachments"
-
-static gboolean open_calendar_wrapper (ECalBackendCalDAV *cbdav,
-                                      GCancellable *cancellable,
-                                      GError **error,
-                                      gboolean first_attempt,
-                                      gboolean *know_unreachable,
-                                      gchar **out_certificate_pem,
-                                      GTlsCertificateFlags *out_certificate_errors);
-
-static void convert_to_inline_attachment (ECalBackendCalDAV *cbdav, icalcomponent *icalcomp);
-static void convert_to_url_attachment (ECalBackendCalDAV *cbdav, icalcomponent *icalcomp);
-static void remove_cached_attachment (ECalBackendCalDAV *cbdav, const gchar *uid);
-
-static gboolean caldav_debug_all = FALSE;
-static GHashTable *caldav_debug_table = NULL;
-
-static void
-add_debug_key (const gchar *start,
-               const gchar *end)
-{
-       gchar *debug_key;
-       gchar *debug_value;
-
-       if (start == end) {
-               return;
-       }
-
-       debug_key = debug_value = g_strndup (start, end - start);
-
-       debug_key = g_strchug (debug_key);
-       debug_key = g_strchomp (debug_key);
-
-       if (strlen (debug_key) == 0) {
-               g_free (debug_value);
-               return;
-       }
-
-       g_hash_table_insert (
-               caldav_debug_table,
-               debug_key,
-               debug_value);
-
-       d (g_debug ("Adding %s to enabled debugging keys", debug_key));
-}
-
-static gpointer
-caldav_debug_init_once (gpointer data)
-{
-       const gchar *dbg;
-
-       dbg = g_getenv ("CALDAV_DEBUG");
-
-       if (dbg) {
-               const gchar *ptr;
-
-               d (g_debug ("Got debug env variable: [%s]", dbg));
-
-               caldav_debug_table = g_hash_table_new (
-                       g_str_hash,
-                       g_str_equal);
-
-               ptr = dbg;
-
-               while (*ptr != '\0') {
-                       if (*ptr == ',' || *ptr == ':') {
-
-                               add_debug_key (dbg, ptr);
-
-                               if (*ptr == ',') {
-                                       dbg = ptr + 1;
-                               }
-                       }
-
-                       ptr++;
-               }
-
-               if (ptr - dbg > 0) {
-                       add_debug_key (dbg, ptr);
-               }
-
-               if (g_hash_table_lookup (caldav_debug_table, "all")) {
-                       caldav_debug_all = TRUE;
-                       g_hash_table_destroy (caldav_debug_table);
-                       caldav_debug_table = NULL;
-               }
-       }
-
-       return NULL;
-}
-
-static void
-caldav_debug_init (void)
-{
-       static GOnce debug_once = G_ONCE_INIT;
-
-       g_once (
-               &debug_once,
-               caldav_debug_init_once,
-               NULL);
-}
-
-static gboolean
-caldav_debug_show (const gchar *component)
-{
-       if (G_UNLIKELY (caldav_debug_all)) {
-               return TRUE;
-       } else if (G_UNLIKELY (caldav_debug_table != NULL) &&
-                  g_hash_table_lookup (caldav_debug_table, component)) {
-               return TRUE;
-       }
-
-       return FALSE;
-}
-
-#define DEBUG_MAX_BODY_SIZE (100 * 1024 * 1024)
-
-static void
-caldav_debug_setup (SoupSession *session)
-{
-       SoupLogger *logger;
-       SoupLoggerLogLevel level;
-
-       if (caldav_debug_show (DEBUG_MESSAGE_BODY))
-               level = SOUP_LOGGER_LOG_BODY;
-       else if (caldav_debug_show (DEBUG_MESSAGE_HEADER))
-               level = SOUP_LOGGER_LOG_HEADERS;
-       else
-               level = SOUP_LOGGER_LOG_MINIMAL;
-
-       logger = soup_logger_new (level, DEBUG_MAX_BODY_SIZE);
-       soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
-       g_object_unref (logger);
-}
-
-/* TODO Do not replicate this in every backend */
-static icaltimezone *
-resolve_tzid (const gchar *tzid,
-              gpointer user_data)
-{
-       ETimezoneCache *timezone_cache;
-
-       timezone_cache = E_TIMEZONE_CACHE (user_data);
-
-       return e_timezone_cache_get_timezone (timezone_cache, tzid);
-}
-
-static gboolean
-put_component_to_store (ECalBackendCalDAV *cbdav,
-                        ECalComponent *comp)
-{
-       time_t time_start, time_end;
-
-       e_cal_util_get_component_occur_times (
-               comp, &time_start, &time_end,
-               resolve_tzid, cbdav,  icaltimezone_get_utc_timezone (),
-               e_cal_backend_get_kind (E_CAL_BACKEND (cbdav)));
-
-       return e_cal_backend_store_put_component_with_time_range (
-               cbdav->priv->store, comp, time_start, time_end);
-}
-
-static ECalBackendSyncClass *parent_class = NULL;
-
-static void caldav_source_changed_cb (ESource *source, ECalBackendCalDAV *cbdav);
-
-static gboolean remove_comp_from_cache (ECalBackendCalDAV *cbdav, const gchar *uid, const gchar *rid);
-static gboolean put_comp_to_cache (ECalBackendCalDAV *cbdav, icalcomponent *icalcomp, const gchar *href, 
const gchar *etag);
-static void put_server_comp_to_cache (ECalBackendCalDAV *cbdav, icalcomponent *icomp, const gchar *href, 
const gchar *etag, GTree *c_uid2complist);
-
-/* ************************************************************************* */
-/* Misc. utility functions */
-
-static void
-caldav_ensure_bearer_auth_usage (ECalBackendCalDAV *cbdav,
-                                ESoupAuthBearer *bearer)
-{
-       SoupSessionFeature *feature;
-       SoupURI *soup_uri;
-       ESourceWebdav *extension;
-       ESource *source;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
-
-       source = e_backend_get_source (E_BACKEND (cbdav));
-
-       /* Preload the SoupAuthManager with a valid "Bearer" token
-        * when using OAuth 2.0. This avoids an extra unauthorized
-        * HTTP round-trip, which apparently Google doesn't like. */
-
-       feature = soup_session_get_feature (cbdav->priv->session, SOUP_TYPE_AUTH_MANAGER);
-
-       if (!soup_session_feature_has_feature (feature, E_TYPE_SOUP_AUTH_BEARER)) {
-               /* Add the "Bearer" auth type to support OAuth 2.0. */
-               soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
-       }
-
-       extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
-       soup_uri = e_source_webdav_dup_soup_uri (extension);
-
-       soup_auth_manager_use_auth (
-               SOUP_AUTH_MANAGER (feature),
-               soup_uri, SOUP_AUTH (bearer));
-
-       soup_uri_free (soup_uri);
-}
-
-static gboolean
-caldav_setup_bearer_auth (ECalBackendCalDAV *cbdav,
-                         gboolean is_in_authenticate,
-                         ESoupAuthBearer *bearer,
-                         GCancellable *cancellable,
-                         GError **error)
-{
-       ESource *source;
-       gchar *access_token = NULL;
-       gint expires_in_seconds = -1;
-       gboolean success = FALSE;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-       g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
-
-       source = e_backend_get_source (E_BACKEND (cbdav));
-
-       success = e_util_get_source_oauth2_access_token_sync (source, cbdav->priv->credentials,
-               &access_token, &expires_in_seconds, cancellable, error);
-
-       if (success) {
-               e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
-               if (!is_in_authenticate)
-                       caldav_ensure_bearer_auth_usage (cbdav, bearer);
-       }
-
-       g_free (access_token);
-
-       return success;
-}
+G_DEFINE_TYPE (ECalBackendCalDAV, e_cal_backend_caldav, E_TYPE_CAL_META_BACKEND)
 
 static gboolean
-caldav_maybe_prepare_bearer_auth (ECalBackendCalDAV *cbdav,
-                                 GCancellable *cancellable,
-                                 GError **error)
+ecb_caldav_connect_sync (ECalMetaBackend *meta_backend,
+                        const ENamedParameters *credentials,
+                        ESourceAuthenticationResult *out_auth_result,
+                        gchar **out_certificate_pem,
+                        GTlsCertificateFlags *out_certificate_errors,
+                        GCancellable *cancellable,
+                        GError **error)
 {
+       ECalBackendCalDAV *cbdav;
+       GHashTable *capabilities = NULL, *allows = NULL;
        ESource *source;
-       gchar *auth_method = NULL;
        gboolean success;
+       GError *local_error = NULL;
 
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-
-       source = e_backend_get_source (E_BACKEND (cbdav));
-
-       if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
-               ESourceAuthentication *extension;
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
 
-               extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
-               auth_method = e_source_authentication_dup_method (extension);
-       } else {
-               return TRUE;
-       }
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
 
-       if (g_strcmp0 (auth_method, "OAuth2") != 0 && g_strcmp0 (auth_method, "Google") != 0) {
-               g_free (auth_method);
+       if (cbdav->priv->webdav)
                return TRUE;
-       }
-
-       g_free (auth_method);
-
-       if (cbdav->priv->using_bearer_auth) {
-               success = caldav_setup_bearer_auth (cbdav, FALSE, cbdav->priv->using_bearer_auth, 
cancellable, error);
-       } else {
-               ESourceWebdav *extension;
-               SoupAuth *soup_auth;
-               SoupURI *soup_uri;
-
-               extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
-               soup_uri = e_source_webdav_dup_soup_uri (extension);
 
-               soup_auth = g_object_new (
-                       E_TYPE_SOUP_AUTH_BEARER,
-                       SOUP_AUTH_HOST, soup_uri->host, NULL);
+       source = e_backend_get_source (E_BACKEND (meta_backend));
 
-               success = caldav_setup_bearer_auth (cbdav, FALSE, E_SOUP_AUTH_BEARER (soup_auth), 
cancellable, error);
-               if (success)
-                       cbdav->priv->using_bearer_auth = g_object_ref (soup_auth);
-
-               g_object_unref (soup_auth);
-               soup_uri_free (soup_uri);
-       }
+       cbdav->priv->webdav = e_webdav_session_new (source);
 
-       return success;
-}
+       e_soup_session_setup_logging (E_SOUP_SESSION (cbdav->priv->webdav), g_getenv ("CALDAV_DEBUG"));
 
-static void
-update_slave_cmd (ECalBackendCalDAVPrivate *priv,
-                  SlaveCommand slave_cmd)
-{
-       g_return_if_fail (priv != NULL);
-
-       if (priv->slave_cmd == SLAVE_SHOULD_DIE)
-               return;
-
-       priv->slave_cmd = slave_cmd;
-}
-
-#define X_E_CALDAV "X-EVOLUTION-CALDAV-"
-#define X_E_CALDAV_ATTACHMENT_NAME X_E_CALDAV "ATTACHMENT-NAME"
-
-static void
-icomp_x_prop_set (icalcomponent *comp,
-                  const gchar *key,
-                  const gchar *value)
-{
-       icalproperty *xprop;
-
-       /* Find the old one first */
-       xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
-       while (xprop) {
-               const gchar *str = icalproperty_get_x_name (xprop);
-
-               if (!strcmp (str, key)) {
-                       if (value) {
-                               icalproperty_set_value_from_string (xprop, value, "NO");
-                       } else {
-                               icalcomponent_remove_property (comp, xprop);
-                               icalproperty_free (xprop);
-                       }
-                       break;
-               }
-
-               xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
-       }
-
-       if (!xprop && value) {
-               xprop = icalproperty_new_x (value);
-               icalproperty_set_x_name (xprop, key);
-               icalcomponent_add_property (comp, xprop);
-       }
-}
-
-static gchar *
-icomp_x_prop_get (icalcomponent *comp,
-                  const gchar *key)
-{
-       icalproperty *xprop;
-
-       /* Find the old one first */
-       xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
-       while (xprop) {
-               const gchar *str = icalproperty_get_x_name (xprop);
+       e_binding_bind_property (
+               cbdav, "proxy-resolver",
+               cbdav->priv->webdav, "proxy-resolver",
+               G_BINDING_SYNC_CREATE);
 
-               if (!strcmp (str, key)) {
-                       break;
-               }
+       /* Thinks the 'getctag' extension is available the first time, but unset it when realizes it isn't. */
+       cbdav->priv->ctag_supported = TRUE;
 
-               xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
-       }
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
 
-       if (xprop) {
-               return icalproperty_get_value_as_string_r (xprop);
-       }
+       e_soup_session_set_credentials (E_SOUP_SESSION (cbdav->priv->webdav), credentials);
 
-       return NULL;
-}
+       success = e_webdav_session_options_sync (cbdav->priv->webdav, NULL,
+               &capabilities, &allows, cancellable, &local_error);
 
-/* passing NULL as 'href' removes the property */
-static void
-ecalcomp_set_href (ECalComponent *comp,
-                   const gchar *href)
-{
-       icalcomponent *icomp;
-
-       icomp = e_cal_component_get_icalcomponent (comp);
-       g_return_if_fail (icomp != NULL);
-
-       icomp_x_prop_set (icomp, X_E_CALDAV "HREF", href);
-}
+       if (success) {
+               ESourceWebdav *webdav_extension;
+               ECalCache *cal_cache;
+               SoupURI *soup_uri;
+               gboolean is_writable;
+               gboolean calendar_access;
 
-static gchar *
-ecalcomp_get_href (ECalComponent *comp)
-{
-       icalcomponent *icomp;
-       gchar          *str;
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+               cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
 
-       str = NULL;
-       icomp = e_cal_component_get_icalcomponent (comp);
-       g_return_val_if_fail (icomp != NULL, NULL);
+               /* The POST added for FastMail servers, which doesn't advertise PUT on collections. */
+               is_writable = allows && (
+                       g_hash_table_contains (allows, SOUP_METHOD_PUT) ||
+                       g_hash_table_contains (allows, SOUP_METHOD_POST) ||
+                       g_hash_table_contains (allows, SOUP_METHOD_DELETE));
 
-       str = icomp_x_prop_get (icomp, X_E_CALDAV "HREF");
+               cbdav->priv->calendar_schedule = capabilities && g_hash_table_contains (capabilities, 
E_WEBDAV_CAPABILITY_CALENDAR_SCHEDULE);
+               calendar_access = capabilities && g_hash_table_contains (capabilities, 
E_WEBDAV_CAPABILITY_CALENDAR_ACCESS);
 
-       return str;
-}
+               if (calendar_access) {
+                       e_cal_backend_set_writable (E_CAL_BACKEND (cbdav), is_writable);
 
-/* passing NULL as 'etag' removes the property */
-static void
-ecalcomp_set_etag (ECalComponent *comp,
-                   const gchar *etag)
-{
-       icalcomponent *icomp;
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
 
-       icomp = e_cal_component_get_icalcomponent (comp);
-       g_return_if_fail (icomp != NULL);
+                       cbdav->priv->is_google = soup_uri && soup_uri->host && (
+                               g_ascii_strcasecmp (soup_uri->host, "www.google.com") == 0 ||
+                               g_ascii_strcasecmp (soup_uri->host, "apidata.googleusercontent.com") == 0);
 
-       icomp_x_prop_set (icomp, X_E_CALDAV "ETAG", etag);
-}
-
-static gchar *
-ecalcomp_get_etag (ECalComponent *comp)
-{
-       icalcomponent *icomp;
-       gchar          *str;
-
-       str = NULL;
-       icomp = e_cal_component_get_icalcomponent (comp);
-       g_return_val_if_fail (icomp != NULL, NULL);
+                       cbdav->priv->is_icloud = soup_uri && soup_uri->host &&
+                               e_util_utf8_strstrcase (soup_uri->host, ".icloud.com");
+               } else {
+                       gchar *uri;
 
-       str = icomp_x_prop_get (icomp, X_E_CALDAV "ETAG");
+                       uri = soup_uri_to_string (soup_uri, FALSE);
 
-       /* libical 0.48 escapes quotes, thus unescape them */
-       if (str && strchr (str, '\\')) {
-               gint ii, jj;
+                       success = FALSE;
+                       g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                               _("Given URL “%s” doesn't reference CalDAV calendar"), uri);
 
-               for (ii = 0, jj = 0; str[ii]; ii++) {
-                       if (str[ii] == '\\') {
-                               ii++;
-                               if (!str[ii])
-                                       break;
-                       }
+                       g_free (uri);
 
-                       str[jj] = str[ii];
-                       jj++;
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
                }
 
-               str[jj] = 0;
-       }
-
-       return str;
-}
-
-/*typedef enum {
- *
-       / * object is in synch,
-        * now isnt that ironic? :) * /
-       ECALCOMP_IN_SYNCH = 0,
- *
-       / * local changes * /
-       ECALCOMP_LOCALLY_CREATED,
-       ECALCOMP_LOCALLY_DELETED,
-       ECALCOMP_LOCALLY_MODIFIED
- *
-} ECalCompSyncState;
- *
-/ * oos = out of synch * /
-static void
-ecalcomp_set_synch_state (ECalComponent *comp,
- *                        ECalCompSyncState state)
-{
-       icalcomponent *icomp;
-       gchar          *state_string;
- *
-       icomp = e_cal_component_get_icalcomponent (comp);
- *
-       state_string = g_strdup_printf ("%d", state);
- *
-       icomp_x_prop_set (icomp, X_E_CALDAV "ETAG", state_string);
- *
-       g_free (state_string);
-}*/
-
-static gchar *
-ecalcomp_gen_href (ECalComponent *comp)
-{
-       gchar *href, *uid, *tmp;
-       icalcomponent *icomp;
-
-       icomp = e_cal_component_get_icalcomponent (comp);
-       g_return_val_if_fail (icomp != NULL, NULL);
-
-       uid = g_strdup (icalcomponent_get_uid (icomp));
-       if (!uid || !*uid) {
-               g_free (uid);
-               uid = e_cal_component_gen_uid ();
-
-               tmp = uid ? strchr (uid, '@') : NULL;
-               if (tmp)
-                       *tmp = '\0';
-
-               tmp = NULL;
-       } else
-               tmp = isodate_from_time_t (time (NULL));
-
-       /* quite long, but ensures uniqueness quite well, without using UUIDs */
-       href = g_strconcat (uid ? uid : "no-uid", tmp ? "-" : "", tmp ? tmp : "", ".ics", NULL);
-
-       g_free (tmp);
-       g_free (uid);
-
-       icomp_x_prop_set (icomp, X_E_CALDAV "HREF", href);
-
-       return g_strdelimit (href, " /'\"`&();|<>$%{}!\\:*?#@", '_');
-}
-
-/* ensure etag is quoted (to workaround potential server bugs) */
-static gchar *
-quote_etag (const gchar *etag)
-{
-       gchar *ret;
-
-       if (etag && (strlen (etag) < 2 || etag[strlen (etag) - 1] != '\"')) {
-               ret = g_strdup_printf ("\"%s\"", etag);
-       } else {
-               ret = g_strdup (etag);
-       }
-
-       return ret;
-}
-
-/* ************************************************************************* */
-
-static gboolean
-status_code_to_result (SoupMessage *message,
-                       ECalBackendCalDAV *cbdav,
-                       gboolean is_opening,
-                       GError **perror)
-{
-       ECalBackendCalDAVPrivate *priv;
-       gchar *uri;
-
-       g_return_val_if_fail (cbdav != NULL, FALSE);
-       g_return_val_if_fail (message != NULL, FALSE);
-
-       priv = cbdav->priv;
-
-       if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
-               return TRUE;
+               g_clear_object (&cal_cache);
+               soup_uri_free (soup_uri);
        }
 
-       if (perror && *perror)
-               return FALSE;
-
-       switch (message->status_code) {
-       case SOUP_STATUS_CANT_RESOLVE:
-       case SOUP_STATUS_CANT_RESOLVE_PROXY:
-       case SOUP_STATUS_CANT_CONNECT:
-       case SOUP_STATUS_CANT_CONNECT_PROXY:
-               g_propagate_error (
-                       perror,
-                       e_data_cal_create_error_fmt (
-                               OtherError,
-                               _("Server is unreachable (%s)"),
-                                       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 (priv) {
-                       priv->opened = FALSE;
-                       e_cal_backend_set_writable (
-                               E_CAL_BACKEND (cbdav), FALSE);
-               }
-               break;
-       case SOUP_STATUS_NOT_FOUND:
-               if (is_opening)
-                       g_propagate_error (perror, EDC_ERROR (NoSuchCal));
-               else
-                       g_propagate_error (perror, EDC_ERROR (ObjectNotFound));
-               break;
+       if (success) {
+               gchar *ctag = NULL;
 
-       case SOUP_STATUS_FORBIDDEN:
-               if (cbdav->priv->using_bearer_auth && message->response_body &&
-                   message->response_body->data && message->response_body->length) {
-                       gchar *body = g_strndup (message->response_body->data, 
message->response_body->length);
-
-                       /* Do not localize this string, it is returned by the server. */
-                       if (body && (e_util_strstrcase (body, "Daily Limit") ||
-                           e_util_strstrcase (body, "https://console.developers.google.com/";))) {
-                               /* Special-case this condition and provide this error up to the UI. */
-                               g_propagate_error (perror,
-                                       e_data_cal_create_error_fmt (OtherError, _("Failed to login to the 
server: %s"), body));
-                       } else {
-                               g_propagate_error (perror, EDC_ERROR (AuthenticationRequired));
-                       }
+               /* Some servers, notably Google, allow OPTIONS when not
+                  authorized (aka without credentials), thus try something
+                  more aggressive, just in case.
 
-                       g_free (body);
+                  The 'getctag' extension is not required, thuch check
+                  for unauthorized error only. */
+               if (!e_webdav_session_getctag_sync (cbdav->priv->webdav, NULL, &ctag, cancellable, 
&local_error) &&
+                   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       success = FALSE;
                } else {
-                       g_propagate_error (perror, EDC_ERROR (AuthenticationRequired));
-               }
-               break;
-
-       case SOUP_STATUS_UNAUTHORIZED:
-               if (priv && priv->auth_required)
-                       g_propagate_error (perror, EDC_ERROR (AuthenticationFailed));
-               else
-                       g_propagate_error (perror, EDC_ERROR (AuthenticationRequired));
-               break;
-
-       case SOUP_STATUS_SSL_FAILED:
-               g_propagate_error (
-                       perror,
-                       e_data_cal_create_error_fmt ( OtherError,
-                       _("Failed to connect to a server using SSL/TLS: %s"),
-                       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 (is_opening && perror && *perror) {
-                       (*perror)->domain = SOUP_HTTP_ERROR;
-                       (*perror)->code = SOUP_STATUS_SSL_FAILED;
+                       g_clear_error (&local_error);
                }
-               break;
 
-       default:
-               d (g_debug ("CalDAV:%s: Unhandled status code %d\n", G_STRFUNC, status_code));
-               uri = soup_uri_to_string (soup_message_get_uri (message), FALSE);
-               g_propagate_error (
-                       perror,
-                       e_data_cal_create_error_fmt (
-                               OtherError,
-                               _("Unexpected HTTP status code %d returned (%s) for URI: %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")),
-                                       uri ? uri : "[null]"));
-               g_free (uri);
-               break;
+               g_free (ctag);
        }
 
-       return FALSE;
-}
+       if (!success) {
+               gboolean credentials_empty;
+               gboolean is_ssl_error;
 
-/* !TS, call with lock held */
-static gboolean
-check_state (ECalBackendCalDAV *cbdav,
-             gboolean *online,
-             GError **perror)
-{
-       *online = FALSE;
+               credentials_empty = !credentials || !e_named_parameters_count (credentials);
+               is_ssl_error = g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED);
 
-       if (!cbdav->priv->loaded) {
-               g_propagate_error (perror, EDC_ERROR_EX (OtherError, _("CalDAV backend is not loaded yet")));
-               return FALSE;
-       }
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
 
-       if (!e_backend_get_online (E_BACKEND (cbdav))) {
-
-               if (!cbdav->priv->do_offline) {
-                       g_propagate_error (perror, EDC_ERROR (RepositoryOffline));
-                       return FALSE;
+               /* because evolution knows only G_IO_ERROR_CANCELLED */
+               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_CANCELLED)) {
+                       local_error->domain = G_IO_ERROR;
+                       local_error->code = G_IO_ERROR_CANCELLED;
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) && 
credentials_empty) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       if (credentials_empty)
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       else
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+               } else if (local_error) {
+                       g_propagate_error (error, local_error);
+                       local_error = NULL;
+               } else {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               _("Unknown error"));
                }
 
-       } else {
-               *online = TRUE;
-       }
-
-       return TRUE;
-}
+               if (is_ssl_error) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
 
-/* ************************************************************************* */
-/* XML Parsing code */
-
-static xmlXPathObjectPtr
-xpath_eval (xmlXPathContextPtr ctx,
-            const gchar *format,
-            ...)
-{
-       xmlXPathObjectPtr  result;
-       va_list            args;
-       gchar              *expr;
-
-       if (ctx == NULL) {
-               return NULL;
-       }
-
-       va_start (args, format);
-       expr = g_strdup_vprintf (format, args);
-       va_end (args);
-
-       result = xmlXPathEvalExpression ((xmlChar *) expr, ctx);
-       g_free (expr);
-
-       if (result == NULL) {
-               return NULL;
-       }
-
-       if (result->type == XPATH_NODESET &&
-           xmlXPathNodeSetIsEmpty (result->nodesetval)) {
-               xmlXPathFreeObject (result);
-               return NULL;
-       }
-
-       return result;
-}
-
-#if 0
-static gboolean
-parse_status_node (xmlNodePtr node,
-                   guint *status_code)
-{
-       xmlChar  *content;
-       gboolean  res;
-
-       content = xmlNodeGetContent (node);
-
-       res = soup_headers_parse_status_line (
-               (gchar *) content,
-               NULL,
-               status_code,
-               NULL);
-       xmlFree (content);
-
-       return res;
-}
-#endif
-
-static gchar *
-xp_object_get_string (xmlXPathObjectPtr result)
-{
-       gchar *ret = NULL;
-
-       if (result == NULL)
-               return ret;
-
-       if (result->type == XPATH_STRING) {
-               ret = g_strdup ((gchar *) result->stringval);
-       }
-
-       xmlXPathFreeObject (result);
-       return ret;
-}
-
-/* like get_string but will quote the etag if necessary */
-static gchar *
-xp_object_get_etag (xmlXPathObjectPtr result)
-{
-       gchar *ret = NULL;
-       gchar *str;
-
-       if (result == NULL)
-               return ret;
-
-       if (result->type == XPATH_STRING) {
-               str = (gchar *) result->stringval;
-
-               ret = quote_etag (str);
-       }
-
-       xmlXPathFreeObject (result);
-       return ret;
-}
-
-static guint
-xp_object_get_status (xmlXPathObjectPtr result)
-{
-       gboolean res;
-       guint    ret = 0;
-
-       if (result == NULL)
-               return ret;
-
-       if (result->type == XPATH_STRING) {
-               res = soup_headers_parse_status_line (
-                       (gchar *) result->stringval,
-                       NULL,
-                       &ret,
-                       NULL);
-
-               if (!res) {
-                       ret = 0;
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+                       e_soup_session_get_ssl_error_details (E_SOUP_SESSION (cbdav->priv->webdav), 
out_certificate_pem, out_certificate_errors);
+               } else {
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
                }
        }
 
-       xmlXPathFreeObject (result);
-       return ret;
-}
-
-#if 0
-static gint
-xp_object_get_number (xmlXPathObjectPtr result)
-{
-       gint ret = -1;
-
-       if (result == NULL)
-               return ret;
-
-       if (result->type == XPATH_STRING) {
-               ret = result->boolval;
-       }
-
-       xmlXPathFreeObject (result);
-       return ret;
-}
-#endif
-
-/*** *** *** *** *** *** */
-#define XPATH_HREF "string(/D:multistatus/D:response[%d]/D:href)"
-#define XPATH_STATUS "string(/D:multistatus/D:response[%d]/D:propstat/D:status)"
-#define XPATH_GETETAG_STATUS 
"string(/D:multistatus/D:response[%d]/D:propstat/D:prop/D:getetag/../../D:status)"
-#define XPATH_GETETAG "string(/D:multistatus/D:response[%d]/D:propstat/D:prop/D:getetag)"
-#define XPATH_CALENDAR_DATA "string(/D:multistatus/D:response[%d]/D:propstat/D:prop/C:calendar-data)"
-#define XPATH_GETCTAG_STATUS "string(/D:multistatus/D:response/D:propstat/D:prop/CS:getctag/../../D:status)"
-#define XPATH_GETCTAG "string(/D:multistatus/D:response/D:propstat/D:prop/CS:getctag)"
-#define XPATH_OWNER_STATUS 
"string(/D:multistatus/D:response/D:propstat/D:prop/D:owner/D:href/../../../D:status)"
-#define XPATH_OWNER "string(/D:multistatus/D:response/D:propstat/D:prop/D:owner/D:href)"
-#define XPATH_SCHEDULE_OUTBOX_URL_STATUS 
"string(/D:multistatus/D:response/D:propstat/D:prop/C:schedule-outbox-URL/D:href/../../../D:status)"
-#define XPATH_SCHEDULE_OUTBOX_URL 
"string(/D:multistatus/D:response/D:propstat/D:prop/C:schedule-outbox-URL/D:href)"
-
-typedef struct _CalDAVObject CalDAVObject;
+       if (capabilities)
+               g_hash_table_destroy (capabilities);
+       if (allows)
+               g_hash_table_destroy (allows);
 
-struct _CalDAVObject {
+       if (!success)
+               g_clear_object (&cbdav->priv->webdav);
 
-       gchar *href;
-       gchar *etag;
-
-       guint status;
-
-       gchar *cdata;
-};
-
-static void
-caldav_object_free (CalDAVObject *object,
-                    gboolean free_object_itself)
-{
-       g_free (object->href);
-       g_free (object->etag);
-       g_free (object->cdata);
-
-       if (free_object_itself) {
-               g_free (object);
-       }
-}
-
-static gboolean
-parse_report_response (SoupMessage *soup_message,
-                       CalDAVObject **objs,
-                       gint *len)
-{
-       xmlXPathContextPtr xpctx;
-       xmlXPathObjectPtr  result;
-       xmlDocPtr          doc;
-       gint                i, n;
-       gboolean           res;
-
-       g_return_val_if_fail (soup_message != NULL, FALSE);
-       g_return_val_if_fail (objs != NULL || len != NULL, FALSE);
-
-       res = TRUE;
-       doc = xmlReadMemory (
-               soup_message->response_body->data,
-               soup_message->response_body->length,
-               "response.xml",
-               NULL,
-               0);
-
-       if (doc == NULL) {
-               return FALSE;
-       }
-
-       xpctx = xmlXPathNewContext (doc);
-
-       xmlXPathRegisterNs (
-               xpctx, (xmlChar *) "D",
-               (xmlChar *) "DAV:");
-
-       xmlXPathRegisterNs (
-               xpctx, (xmlChar *) "C",
-               (xmlChar *) "urn:ietf:params:xml:ns:caldav");
-
-       result = xpath_eval (xpctx, "/D:multistatus/D:response");
-
-       if (result == NULL || result->type != XPATH_NODESET) {
-               *len = 0;
-               res = FALSE;
-               goto out;
-       }
-
-       n = xmlXPathNodeSetGetLength (result->nodesetval);
-       *len = n;
-
-       *objs = g_new0 (CalDAVObject, n);
-
-       for (i = 0; i < n; i++) {
-               CalDAVObject *object;
-               xmlXPathObjectPtr xpres;
-
-               object = *objs + i;
-               /* see if we got a status child in the response element */
-
-               xpres = xpath_eval (xpctx, XPATH_HREF, i + 1);
-               /* use full path from a href, to let calendar-multiget work properly */
-               object->href = xp_object_get_string (xpres);
-
-               xpres = xpath_eval (xpctx,XPATH_STATUS , i + 1);
-               object->status = xp_object_get_status (xpres);
-
-               if (object->status && object->status != 200) {
-                       continue;
-               }
-
-               xpres = xpath_eval (xpctx, XPATH_GETETAG_STATUS, i + 1);
-               object->status = xp_object_get_status (xpres);
-
-               if (object->status != 200) {
-                       continue;
-               }
-
-               xpres = xpath_eval (xpctx, XPATH_GETETAG, i + 1);
-               object->etag = xp_object_get_etag (xpres);
-
-               xpres = xpath_eval (xpctx, XPATH_CALENDAR_DATA, i + 1);
-               object->cdata = xp_object_get_string (xpres);
-       }
-
-out:
-       if (result != NULL)
-               xmlXPathFreeObject (result);
-       xmlXPathFreeContext (xpctx);
-       xmlFreeDoc (doc);
-       return res;
+       return success;
 }
 
-/* returns whether was able to read the xpath_value from the server's response; *value contains the result */
 static gboolean
-parse_propfind_response (SoupMessage *message,
-                         const gchar *xpath_status,
-                         const gchar *xpath_value,
-                         gchar **value)
-{
-       xmlXPathContextPtr xpctx;
-       xmlDocPtr          doc;
-       gboolean           res = FALSE;
-
-       g_return_val_if_fail (message != NULL, FALSE);
-       g_return_val_if_fail (value != NULL, FALSE);
-
-       doc = xmlReadMemory (
-               message->response_body->data,
-               message->response_body->length,
-               "response.xml",
-               NULL,
-               0);
-
-       if (doc == NULL) {
-               return FALSE;
-       }
-
-       xpctx = xmlXPathNewContext (doc);
-       xmlXPathRegisterNs (xpctx, (xmlChar *) "D", (xmlChar *) "DAV:");
-       xmlXPathRegisterNs (xpctx, (xmlChar *) "C", (xmlChar *) "urn:ietf:params:xml:ns:caldav");
-       xmlXPathRegisterNs (xpctx, (xmlChar *) "CS", (xmlChar *) "http://calendarserver.org/ns/";);
-
-       if (xpath_status == NULL || xp_object_get_status (xpath_eval (xpctx, xpath_status)) == 200) {
-               gchar *txt = xp_object_get_string (xpath_eval (xpctx, xpath_value));
-
-               if (txt && *txt) {
-                       gint len = strlen (txt);
-
-                       if (*txt == '\"' && len > 2 && txt[len - 1] == '\"') {
-                               /* dequote */
-                               *value = g_strndup (txt + 1, len - 2);
-                       } else {
-                               *value = txt;
-                               txt = NULL;
-                       }
-
-                       res = (*value) != NULL;
-               }
-
-               g_free (txt);
-       }
-
-       xmlXPathFreeContext (xpctx);
-       xmlFreeDoc (doc);
-
-       return res;
-}
-
-/* ************************************************************************* */
-/* Authentication helpers for libsoup */
-
-static void
-soup_authenticate_bearer (SoupSession *session,
-                          SoupMessage *message,
-                          SoupAuth *auth,
-                          ECalBackendCalDAV *cbdav)
-{
-       GError *local_error = NULL;
-
-       caldav_setup_bearer_auth (cbdav, TRUE, E_SOUP_AUTH_BEARER (auth), NULL, &local_error);
-
-       /* Stash the error to be picked up by caldav_credentials_required_sync().
-        * There's no way to explicitly propagate a GError directly
-        * through libsoup, so we have to work around it. */
-       if (local_error != NULL) {
-               g_mutex_lock (&cbdav->priv->bearer_auth_error_lock);
-
-               /* Warn about an unclaimed error before we clear it.
-                * This is just to verify the errors we set here are
-                * actually making it back to the user. */
-               g_warn_if_fail (cbdav->priv->bearer_auth_error == NULL);
-               g_clear_error (&cbdav->priv->bearer_auth_error);
-
-               g_propagate_error (
-                       &cbdav->priv->bearer_auth_error, local_error);
-
-               g_mutex_unlock (&cbdav->priv->bearer_auth_error_lock);
-       }
-}
-
-static void
-soup_authenticate (SoupSession *session,
-                   SoupMessage *msg,
-                   SoupAuth *auth,
-                   gboolean retrying,
-                   gpointer data)
+ecb_caldav_disconnect_sync (ECalMetaBackend *meta_backend,
+                           GCancellable *cancellable,
+                           GError **error)
 {
        ECalBackendCalDAV *cbdav;
-       ESourceAuthentication *auth_extension;
-       ESource *source;
-       const gchar *extension_name;
-
-       cbdav = E_CAL_BACKEND_CALDAV (data);
-
-       source = e_backend_get_source (E_BACKEND (data));
-       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-       auth_extension = e_source_get_extension (source, extension_name);
-
-       if (E_IS_SOUP_AUTH_BEARER (auth)) {
-               g_object_ref (auth);
-               g_warn_if_fail ((gpointer) cbdav->priv->using_bearer_auth == (gpointer) auth);
-               g_clear_object (&cbdav->priv->using_bearer_auth);
-               cbdav->priv->using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
-       }
-
-       if (retrying)
-               return;
-
-       if (cbdav->priv->using_bearer_auth) {
-               soup_authenticate_bearer (session, msg, auth, cbdav);
-
-       /* do not send same password twice, but keep it for later use */
-       } else {
-               gchar *auth_user;
-               const gchar *username;
-
-               auth_user = e_source_authentication_dup_user (auth_extension);
-
-               username = cbdav->priv->credentials ? e_named_parameters_get (cbdav->priv->credentials, 
E_SOURCE_CREDENTIAL_USERNAME) : NULL;
-               if (!username || !*username)
-                       username = auth_user;
-
-               if (!username || !*username || !cbdav->priv->credentials ||
-                   !e_named_parameters_exists (cbdav->priv->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
-                       soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
-               else
-                       soup_auth_authenticate (auth, username, e_named_parameters_get 
(cbdav->priv->credentials, E_SOURCE_CREDENTIAL_PASSWORD));
-
-               g_free (auth_user);
-       }
-}
-
-/* ************************************************************************* */
-/* direct CalDAV server access functions */
-
-static void
-redirect_handler (SoupMessage *msg,
-                  gpointer user_data)
-{
-       if (SOUP_STATUS_IS_REDIRECTION (msg->status_code)) {
-               SoupSession *soup_session = user_data;
-               SoupURI *new_uri;
-               const gchar *new_loc;
-
-               new_loc = soup_message_headers_get_list (msg->response_headers, "Location");
-               if (!new_loc)
-                       return;
-
-               new_uri = soup_uri_new_with_base (soup_message_get_uri (msg), new_loc);
-               if (!new_uri) {
-                       soup_message_set_status_full (
-                               msg,
-                               SOUP_STATUS_MALFORMED,
-                               _("Invalid Redirect URL"));
-                       return;
-               }
-
-               if (new_uri->host && g_str_has_suffix (new_uri->host, "yahoo.com")) {
-                       /* yahoo! returns port 7070, which is unreachable;
-                        * it also requires https being used (below call resets port as well) */
-                       soup_uri_set_scheme (new_uri, SOUP_URI_SCHEME_HTTPS);
-               }
-
-               soup_message_set_uri (msg, new_uri);
-               soup_session_requeue_message (soup_session, msg);
-
-               soup_uri_free (new_uri);
-       }
-}
-
-static void
-send_and_handle_redirection (ECalBackendCalDAV *cbdav,
-                             SoupMessage *msg,
-                             gchar **new_location,
-                             GCancellable *cancellable,
-                             GError **error)
-{
-       gchar *old_uri = NULL;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
-       g_return_if_fail (msg != NULL);
-
-       if (new_location)
-               old_uri = soup_uri_to_string (soup_message_get_uri (msg), FALSE);
-
-       e_soup_ssl_trust_connect (msg, e_backend_get_source (E_BACKEND (cbdav)));
-
-       if (cbdav->priv->using_bearer_auth &&
-           e_soup_auth_bearer_is_expired (cbdav->priv->using_bearer_auth)) {
-               GError *local_error = NULL;
-
-               if (!caldav_setup_bearer_auth (cbdav, FALSE, cbdav->priv->using_bearer_auth, cancellable, 
&local_error)) {
-                       if (local_error) {
-                               soup_message_set_status_full (msg, SOUP_STATUS_BAD_REQUEST, 
local_error->message);
-                               g_propagate_error (error, local_error);
-                       } else {
-                               soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
-                       }
-                       return;
-               }
-       }
-
-       soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT);
-       soup_message_add_header_handler (msg, "got_body", "Location", G_CALLBACK (redirect_handler), 
cbdav->priv->session);
-       soup_message_headers_append (msg->request_headers, "Connection", "close");
-       soup_session_send_message (cbdav->priv->session, msg);
-
-       if (new_location) {
-               gchar *new_loc = soup_uri_to_string (soup_message_get_uri (msg), FALSE);
-
-               if (new_loc && old_uri && !g_str_equal (new_loc, old_uri))
-                       *new_location = new_loc;
-               else
-                       g_free (new_loc);
-       }
-
-       g_free (old_uri);
-
-       if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code))
-               e_backend_ensure_source_status_connected (E_BACKEND (cbdav));
-}
-
-static gchar *
-caldav_generate_uri (ECalBackendCalDAV *cbdav,
-                     const gchar *target)
-{
-       gchar *uri;
-       const gchar *slash;
-
-       slash = strrchr (target, '/');
-       if (slash)
-               target = slash + 1;
-
-       /* uri *have* trailing slash already */
-       uri = g_strconcat (cbdav->priv->uri, target, NULL);
-
-       return uri;
-}
-
-static gboolean
-caldav_server_open_calendar (ECalBackendCalDAV *cbdav,
-                             gboolean *server_unreachable,
-                            gchar **out_certificate_pem,
-                            GTlsCertificateFlags *out_certificate_errors,
-                             GCancellable *cancellable,
-                             GError **perror)
-{
-       SoupMessage *message;
-       const gchar *header;
-       gboolean calendar_access;
-       gboolean put_allowed;
-       gboolean delete_allowed;
        ESource *source;
 
-       g_return_val_if_fail (cbdav != NULL, FALSE);
-       g_return_val_if_fail (server_unreachable != NULL, FALSE);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
 
-       message = soup_message_new (SOUP_METHOD_OPTIONS, cbdav->priv->uri);
-       if (message == NULL) {
-               g_propagate_error (perror, EDC_ERROR (NoSuchCal));
-               return FALSE;
-       }
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
 
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
+       if (cbdav->priv->webdav)
+               soup_session_abort (SOUP_SESSION (cbdav->priv->webdav));
 
-       source = e_backend_get_source (E_BACKEND (cbdav));
-       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
-
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, perror);
-
-       if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
-               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-
-               switch (message->status_code) {
-               case SOUP_STATUS_CANT_RESOLVE:
-               case SOUP_STATUS_CANT_RESOLVE_PROXY:
-               case SOUP_STATUS_CANT_CONNECT:
-               case SOUP_STATUS_CANT_CONNECT_PROXY:
-                       *server_unreachable = TRUE;
-                       break;
-               case SOUP_STATUS_SSL_FAILED:
-                       if (out_certificate_pem && out_certificate_errors) {
-                               GTlsCertificate *certificate = NULL;
-
-                               g_object_get (G_OBJECT (message),
-                                       "tls-certificate", &certificate,
-                                       "tls-errors", out_certificate_errors,
-                                       NULL);
-
-                               if (certificate) {
-                                       g_object_get (certificate, "certificate-pem", out_certificate_pem, 
NULL);
-                                       g_object_unref (certificate);
-                               }
-                       }
-                       break;
-               }
-
-               status_code_to_result (message, cbdav, TRUE, perror);
-
-               g_object_unref (message);
-               return FALSE;
-       }
-
-       /* parse the dav header, we are intreseted in the
-        * calendar-access bit only at the moment */
-       header = soup_message_headers_get_list (message->response_headers, "DAV");
-       if (header) {
-               calendar_access = soup_header_contains (header, "calendar-access");
-               cbdav->priv->calendar_schedule = soup_header_contains (header, "calendar-schedule");
-       } else {
-               calendar_access = FALSE;
-               cbdav->priv->calendar_schedule = FALSE;
-       }
-
-       /* parse the Allow header and look for PUT, DELETE at the
-        * moment (maybe we should check more here, for REPORT eg) */
-       header = soup_message_headers_get_list (message->response_headers, "Allow");
-       if (header) {
-               /* The POST added for FastMail servers, which doesn't advertise PUT on collections. */
-               put_allowed = soup_header_contains (header, "PUT") || soup_header_contains (header, "POST");
-               delete_allowed = soup_header_contains (header, "DELETE");
-       } else
-               put_allowed = delete_allowed = FALSE;
-
-       g_object_unref (message);
-
-       if (calendar_access) {
-               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
-               e_cal_backend_set_writable (
-                       E_CAL_BACKEND (cbdav),
-                       put_allowed && delete_allowed);
-               return TRUE;
-       }
+       g_clear_object (&cbdav->priv->webdav);
 
+       source = e_backend_get_source (E_BACKEND (meta_backend));
        e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-       g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-       return FALSE;
-}
-
-static gpointer
-caldav_unref_thread (gpointer cbdav)
-{
-       g_object_unref (cbdav);
-
-       return NULL;
-}
-
-static void
-caldav_unref_in_thread (ECalBackendCalDAV *cbdav)
-{
-       GThread *thread;
 
-       g_return_if_fail (cbdav != NULL);
-
-       thread = g_thread_new (NULL, caldav_unref_thread, cbdav);
-       g_thread_unref (thread);
-}
-
-static gboolean
-caldav_credentials_required_sync (ECalBackendCalDAV *cbdav,
-                                 gboolean ref_cbdav,
-                                 gboolean first_attempt,
-                                 GCancellable *cancellable,
-                                 GError **error)
-{
-       gboolean success = TRUE;
-
-       if (ref_cbdav)
-               g_object_ref (cbdav);
-
-       /* This function is called when we receive a 4xx response code for
-        * authentication failures.  If we're using Bearer authentication,
-        * there should be a GError available.  Return the GError to avoid
-        * inappropriately prompting for a password. */
-       g_mutex_lock (&cbdav->priv->bearer_auth_error_lock);
-       if (cbdav->priv->bearer_auth_error != NULL) {
-               g_propagate_error (error, cbdav->priv->bearer_auth_error);
-               cbdav->priv->bearer_auth_error = NULL;
-               success = FALSE;
-       }
-       g_mutex_unlock (&cbdav->priv->bearer_auth_error_lock);
-
-       if (success) {
-               success = e_backend_credentials_required_sync (E_BACKEND (cbdav),
-                       (first_attempt || !cbdav->priv->credentials ||
-                        !e_named_parameters_exists (cbdav->priv->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
-                       ? E_SOURCE_CREDENTIALS_REASON_REQUIRED :
-                       E_SOURCE_CREDENTIALS_REASON_REJECTED,
-                       NULL, 0, NULL, cancellable, error);
-       }
-
-       if (ref_cbdav)
-               caldav_unref_in_thread (cbdav);
-
-       return success;
-}
-
-static gconstpointer
-compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
-                                         gsize *out_len)
-{
-#ifdef LIBXML2_NEW_BUFFER
-       *out_len = xmlOutputBufferGetSize (buf);
-       return xmlOutputBufferGetContent (buf);
-#else
-       *out_len = buf->buffer->use;
-       return buf->buffer->content;
-#endif
+       return TRUE;
 }
 
-/* Returns whether calendar changed on the server. This works only when server
- * supports 'getctag' extension. */
-static gboolean
-check_calendar_changed_on_server (ECalBackendCalDAV *cbdav,
-                                 gboolean save_ctag,
-                                 GCancellable *cancellable)
+static const gchar *
+ecb_caldav_get_vcalendar_uid (icalcomponent *vcalendar)
 {
-       xmlOutputBufferPtr        buf;
-       SoupMessage              *message;
-       xmlDocPtr                 doc;
-       xmlNodePtr                root, node;
-       xmlNsPtr                  ns, nsdav;
-       gconstpointer             buf_content;
-       gsize                     buf_size;
-       gboolean                  result = TRUE;
-
-       g_return_val_if_fail (cbdav != NULL, TRUE);
-
-       /* no support for 'getctag', thus update cache */
-       if (!cbdav->priv->ctag_supported)
-               return TRUE;
-
-       /* Prepare the soup message */
-       message = soup_message_new ("PROPFIND", cbdav->priv->uri);
-       if (message == NULL)
-               return FALSE;
-
-       doc = xmlNewDoc ((xmlChar *) "1.0");
-       root = xmlNewDocNode (doc, NULL, (xmlChar *) "propfind", NULL);
-       xmlDocSetRootElement (doc, root);
-       nsdav = xmlNewNs (root, (xmlChar *) "DAV:", NULL);
-       ns = xmlNewNs (root, (xmlChar *) "http://calendarserver.org/ns/";, (xmlChar *) "CS");
-
-       node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
-       node = xmlNewTextChild (node, nsdav, (xmlChar *) "getctag", NULL);
-       xmlSetNs (node, ns);
-
-       buf = xmlAllocOutputBuffer (NULL);
-       xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
-       xmlOutputBufferFlush (buf);
-
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
-       soup_message_headers_append (
-               message->request_headers,
-               "Depth", "0");
-
-       buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
-       soup_message_set_request (
-               message,
-               "application/xml",
-               SOUP_MEMORY_COPY,
-               buf_content, buf_size);
-
-       /* Send the request now */
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, NULL);
-
-       /* Clean up the memory */
-       xmlOutputBufferClose (buf);
-       xmlFreeDoc (doc);
-
-       /* Check the result */
-       if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == 
SOUP_STATUS_FORBIDDEN) {
-               caldav_credentials_required_sync (cbdav, TRUE, FALSE, NULL, NULL);
-       } else if (message->status_code != SOUP_STATUS_MULTI_STATUS) {
-               /* does not support it, but report calendar changed to update cache */
-               cbdav->priv->ctag_supported = FALSE;
-       } else {
-               gchar *ctag = NULL;
-
-               if (parse_propfind_response (message, XPATH_GETCTAG_STATUS, XPATH_GETCTAG, &ctag)) {
-                       const gchar *my_ctag;
+       const gchar *uid = NULL;
+       icalcomponent *subcomp;
 
-                       my_ctag = e_cal_backend_store_get_key_value (
-                               cbdav->priv->store, CALDAV_CTAG_KEY);
+       g_return_val_if_fail (vcalendar != NULL, NULL);
+       g_return_val_if_fail (icalcomponent_isa (vcalendar) == ICAL_VCALENDAR_COMPONENT, NULL);
 
-                       if (ctag && my_ctag && g_str_equal (ctag, my_ctag)) {
-                               /* ctag is same, no change in the calendar */
-                               result = FALSE;
-                       } else if (save_ctag) {
-                               /* do not store ctag now, do it rather after complete sync */
-                               g_free (cbdav->priv->ctag_to_store);
-                               cbdav->priv->ctag_to_store = ctag;
-                               ctag = NULL;
-                       }
+       for (subcomp = icalcomponent_get_first_component (vcalendar, ICAL_ANY_COMPONENT);
+            subcomp && !uid;
+            subcomp = icalcomponent_get_next_component (vcalendar, ICAL_ANY_COMPONENT)) {
+               icalcomponent_kind kind = icalcomponent_isa (subcomp);
 
-                       g_free (ctag);
-               } else {
-                       cbdav->priv->ctag_supported = FALSE;
+               if (kind == ICAL_VEVENT_COMPONENT ||
+                   kind == ICAL_VJOURNAL_COMPONENT ||
+                   kind == ICAL_VTODO_COMPONENT) {
+                       uid = icalcomponent_get_uid (subcomp);
+                       if (uid && !*uid)
+                               uid = NULL;
                }
        }
 
-       g_object_unref (message);
-
-       return result;
+       return uid;
 }
 
-/* only_hrefs is a list of requested objects to fetch; it has precedence from
- * start_time/end_time, which are used only when both positive.
- * Times are supposed to be in UTC, if set.
- */
-static gboolean
-caldav_server_list_objects (ECalBackendCalDAV *cbdav,
-                            CalDAVObject **objs,
-                            gint *len,
-                            GSList *only_hrefs,
-                            time_t start_time,
-                            time_t end_time,
-                           GCancellable *cancellable)
+static void
+ecb_caldav_update_nfo_with_vcalendar (ECalMetaBackendInfo *nfo,
+                                     icalcomponent *vcalendar,
+                                     const gchar *etag)
 {
-       xmlOutputBufferPtr   buf;
-       SoupMessage         *message;
-       xmlNodePtr           node;
-       xmlNodePtr           sn;
-       xmlNodePtr           root;
-       xmlDocPtr            doc;
-       xmlNsPtr             nsdav;
-       xmlNsPtr             nscd;
-       gconstpointer        buf_content;
-       gsize                buf_size;
-       gboolean             result;
-
-       /* Allocate the soup message */
-       message = soup_message_new ("REPORT", cbdav->priv->uri);
-       if (message == NULL)
-               return FALSE;
-
-       /* Maybe we should just do a g_strdup_printf here? */
-       /* Prepare request body */
-       doc = xmlNewDoc ((xmlChar *) "1.0");
-       if (!only_hrefs)
-               root = xmlNewDocNode (doc, NULL, (xmlChar *) "calendar-query", NULL);
-       else
-               root = xmlNewDocNode (doc, NULL, (xmlChar *) "calendar-multiget", NULL);
-       nscd = xmlNewNs (root, (xmlChar *) "urn:ietf:params:xml:ns:caldav", (xmlChar *) "C");
-       xmlSetNs (root, nscd);
-       xmlDocSetRootElement (doc, root);
-
-       /* Add webdav tags */
-       nsdav = xmlNewNs (root, (xmlChar *) "DAV:", (xmlChar *) "D");
-       node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
-       xmlNewTextChild (node, nsdav, (xmlChar *) "getetag", NULL);
-       if (only_hrefs) {
-               GSList *l;
-
-               xmlNewTextChild (node, nscd, (xmlChar *) "calendar-data", NULL);
-               for (l = only_hrefs; l; l = l->next) {
-                       if (l->data) {
-                               xmlNewTextChild (root, nsdav, (xmlChar *) "href", (xmlChar *) l->data);
-                       }
-               }
-       } else {
-               node = xmlNewTextChild (root, nscd, (xmlChar *) "filter", NULL);
-               node = xmlNewTextChild (node, nscd, (xmlChar *) "comp-filter", NULL);
-               xmlSetProp (node, (xmlChar *) "name", (xmlChar *) "VCALENDAR");
-
-               sn = xmlNewTextChild (node, nscd, (xmlChar *) "comp-filter", NULL);
-               switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbdav))) {
-                       default:
-                       case ICAL_VEVENT_COMPONENT:
-                               xmlSetProp (sn, (xmlChar *) "name", (xmlChar *) "VEVENT");
-                               break;
-                       case ICAL_VJOURNAL_COMPONENT:
-                               xmlSetProp (sn, (xmlChar *) "name", (xmlChar *) "VJOURNAL");
-                               break;
-                       case ICAL_VTODO_COMPONENT:
-                               xmlSetProp (sn, (xmlChar *) "name", (xmlChar *) "VTODO");
-                               break;
-               }
+       icalcomponent *subcomp;
+       const gchar *uid;
 
-               if (start_time > 0 || end_time > 0) {
-                       gchar *tmp;
+       g_return_if_fail (nfo != NULL);
+       g_return_if_fail (vcalendar != NULL);
 
-                       sn = xmlNewTextChild (sn, nscd, (xmlChar *) "time-range", NULL);
+       uid = ecb_caldav_get_vcalendar_uid (vcalendar);
 
-                       if (start_time > 0) {
-                               tmp = isodate_from_time_t (start_time);
-                               xmlSetProp (sn, (xmlChar *) "start", (xmlChar *) tmp);
-                               g_free (tmp);
-                       }
+       if (!etag || !*etag)
+               etag = nfo->revision;
 
-                       if (end_time > 0) {
-                               tmp = isodate_from_time_t (end_time);
-                               xmlSetProp (sn, (xmlChar *) "end", (xmlChar *) tmp);
-                               g_free (tmp);
-                       }
-               }
-       }
+       for (subcomp = icalcomponent_get_first_component (vcalendar, ICAL_ANY_COMPONENT);
+            subcomp;
+            subcomp = icalcomponent_get_next_component (vcalendar, ICAL_ANY_COMPONENT)) {
+               icalcomponent_kind kind = icalcomponent_isa (subcomp);
 
-       buf = xmlAllocOutputBuffer (NULL);
-       xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
-       xmlOutputBufferFlush (buf);
-
-       /* Prepare the soup message */
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
-       soup_message_headers_append (
-               message->request_headers,
-               "Depth", "1");
-
-       buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
-       soup_message_set_request (
-               message,
-               "application/xml",
-               SOUP_MEMORY_COPY,
-               buf_content, buf_size);
-
-       /* Send the request now */
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, NULL);
-
-       /* Clean up the memory */
-       xmlOutputBufferClose (buf);
-       xmlFreeDoc (doc);
-
-       /* Check the result */
-       if (message->status_code != SOUP_STATUS_MULTI_STATUS) {
-               switch (message->status_code) {
-               case SOUP_STATUS_CANT_RESOLVE:
-               case SOUP_STATUS_CANT_RESOLVE_PROXY:
-               case SOUP_STATUS_CANT_CONNECT:
-               case SOUP_STATUS_CANT_CONNECT_PROXY:
-                       cbdav->priv->opened = FALSE;
-                       update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
-                       e_cal_backend_set_writable (
-                               E_CAL_BACKEND (cbdav), FALSE);
-                       break;
-               case SOUP_STATUS_UNAUTHORIZED:
-               case SOUP_STATUS_FORBIDDEN:
-                       caldav_credentials_required_sync (cbdav, TRUE, FALSE, NULL, NULL);
-                       break;
-               default:
-                       g_warning ("Server did not response with SOUP_STATUS_MULTI_STATUS, but with code %d 
(%s)", message->status_code, soup_status_get_phrase (message->status_code) ? soup_status_get_phrase 
(message->status_code) : "Unknown code");
-                       break;
+               if (kind == ICAL_VEVENT_COMPONENT ||
+                   kind == ICAL_VJOURNAL_COMPONENT ||
+                   kind == ICAL_VTODO_COMPONENT) {
+                       e_cal_util_set_x_property (subcomp, E_CALDAV_X_ETAG, etag);
                }
-
-               g_object_unref (message);
-               return FALSE;
-       }
-
-       /* Parse the response body */
-       result = parse_report_response (message, objs, len);
-
-       g_object_unref (message);
-       return result;
-}
-
-static gboolean
-caldav_server_download_attachment (ECalBackendCalDAV *cbdav,
-                                   const gchar *attachment_uri,
-                                   gchar **content,
-                                   gsize *len,
-                                   GError **error)
-{
-       SoupMessage *message;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-       g_return_val_if_fail (attachment_uri != NULL, FALSE);
-       g_return_val_if_fail (content != NULL, FALSE);
-       g_return_val_if_fail (len != NULL, FALSE);
-
-       message = soup_message_new (SOUP_METHOD_GET, attachment_uri);
-       if (message == NULL) {
-               g_propagate_error (error, EDC_ERROR (InvalidObject));
-               return FALSE;
-       }
-
-       soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
-       send_and_handle_redirection (cbdav, message, NULL, NULL, NULL);
-
-       if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
-               status_code_to_result (message, cbdav, FALSE, error);
-
-               if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == 
SOUP_STATUS_FORBIDDEN)
-                       caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-
-               g_object_unref (message);
-               return FALSE;
-       }
-
-       *len = message->response_body->length;
-       *content = g_memdup (message->response_body->data, *len);
-
-       g_object_unref (message);
-
-       return TRUE;
-}
-
-static gboolean
-caldav_server_get_object (ECalBackendCalDAV *cbdav,
-                          CalDAVObject *object,
-                          GCancellable *cancellable,
-                          GError **perror)
-{
-       SoupMessage              *message;
-       const gchar               *hdr;
-       gchar                     *uri;
-
-       g_return_val_if_fail (object != NULL && object->href != NULL, FALSE);
-
-       uri = caldav_generate_uri (cbdav, object->href);
-       message = soup_message_new (SOUP_METHOD_GET, uri);
-       if (message == NULL) {
-               g_free (uri);
-               g_propagate_error (perror, EDC_ERROR (NoSuchCal));
-               return FALSE;
-       }
-
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
-
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, perror);
-
-       if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
-               status_code_to_result (message, cbdav, FALSE, perror);
-
-               if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == 
SOUP_STATUS_FORBIDDEN)
-                       caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-               else if (message->status_code != SOUP_STATUS_NOT_FOUND)
-                       g_warning ("Could not fetch object '%s' from server, status:%d (%s)", uri, 
message->status_code, soup_status_get_phrase (message->status_code) ? soup_status_get_phrase 
(message->status_code) : "Unknown code");
-               g_object_unref (message);
-               g_free (uri);
-               return FALSE;
        }
 
-       hdr = soup_message_headers_get_list (message->response_headers, "Content-Type");
+       g_warn_if_fail (nfo->object == NULL);
+       nfo->object = icalcomponent_as_ical_string_r (vcalendar);
 
-       if (hdr == NULL || g_ascii_strncasecmp (hdr, "text/calendar", 13)) {
-               g_propagate_error (perror, EDC_ERROR (InvalidObject));
-               g_object_unref (message);
-               g_warning ("Object to fetch '%s' not of type text/calendar", uri);
-               g_free (uri);
-               return FALSE;
+       if (!nfo->uid || !*(nfo->uid)) {
+               g_free (nfo->uid);
+               nfo->uid = g_strdup (uid);
        }
 
-       hdr = soup_message_headers_get_list (message->response_headers, "ETag");
+       if (g_strcmp0 (etag, nfo->revision) != 0) {
+               gchar *copy = g_strdup (etag);
 
-       if (hdr != NULL) {
-               g_free (object->etag);
-               object->etag = quote_etag (hdr);
-       } else if (!object->etag) {
-               g_warning ("UUHH no ETag, now that's bad! (at '%s')", uri);
+               g_free (nfo->revision);
+               nfo->revision = copy;
        }
-       g_free (uri);
-
-       g_free (object->cdata);
-       object->cdata = g_strdup (message->response_body->data);
-
-       g_object_unref (message);
-
-       return TRUE;
-}
-
-static void
-caldav_post_freebusy (ECalBackendCalDAV *cbdav,
-                      const gchar *url,
-                      gchar **post_fb,
-                      GCancellable *cancellable,
-                      GError **error)
-{
-       SoupMessage *message;
-
-       message = soup_message_new (SOUP_METHOD_POST, url);
-       if (message == NULL) {
-               g_propagate_error (error, EDC_ERROR (NoSuchCal));
-               return;
-       }
-
-       soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
-       soup_message_set_request (
-               message,
-               "text/calendar; charset=utf-8",
-               SOUP_MEMORY_COPY,
-               *post_fb, strlen (*post_fb));
-
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, error);
-
-       if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
-               status_code_to_result (message, cbdav, FALSE, error);
-               if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == 
SOUP_STATUS_FORBIDDEN)
-                       caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-               else
-                       g_warning ("Could not post free/busy request to '%s', status:%d (%s)", url, 
message->status_code, soup_status_get_phrase (message->status_code) ? soup_status_get_phrase 
(message->status_code) : "Unknown code");
-
-               g_object_unref (message);
-
-               return;
-       }
-
-       g_free (*post_fb);
-       *post_fb = g_strdup (message->response_body->data);
-
-       g_object_unref (message);
-}
-
-static gchar *
-caldav_gen_file_from_uid (ECalBackendCalDAV *cbdav,
-                         const gchar *uid)
-{
-       gchar *filename, *res;
-
-       if (!uid)
-               return NULL;
-
-       filename = g_strconcat (uid, ".ics", NULL);
-       res = soup_uri_encode (filename, NULL);
-       g_free (filename);
-
-       return res;
-}
-
-static gchar *
-caldav_gen_file_from_uid_cal (ECalBackendCalDAV *cbdav,
-                              icalcomponent *icalcomp)
-{
-       icalcomponent_kind my_kind;
-       const gchar *uid = NULL;
-
-       g_return_val_if_fail (cbdav != NULL, NULL);
-       g_return_val_if_fail (icalcomp != NULL, NULL);
-
-       my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
-       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent *subcomp;
-
-               for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
-                    subcomp;
-                    subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
-                       uid = icalcomponent_get_uid (subcomp);
-                       if (uid && *uid)
-                               break;
-               }
-       } else if (icalcomponent_isa (icalcomp) == my_kind) {
-               uid = icalcomponent_get_uid (icalcomp);
-       }
-
-       return caldav_gen_file_from_uid (cbdav, uid);
 }
 
 static gboolean
-caldav_server_put_object (ECalBackendCalDAV *cbdav,
-                          CalDAVObject *object,
-                          icalcomponent *icalcomp,
-                          GCancellable *cancellable,
-                          GError **perror)
+ecb_caldav_multiget_response_cb (EWebDAVSession *webdav,
+                                xmlXPathContextPtr xpath_ctx,
+                                const gchar *xpath_prop_prefix,
+                                const SoupURI *request_uri,
+                                const gchar *href,
+                                guint status_code,
+                                gpointer user_data)
 {
-       SoupMessage              *message;
-       const gchar               *hdr;
-       gchar                     *uri;
-
-       hdr = NULL;
+       GSList **from_link = user_data;
 
-       g_return_val_if_fail (object != NULL && object->cdata != NULL, FALSE);
+       g_return_val_if_fail (from_link != NULL, FALSE);
 
-       uri = caldav_generate_uri (cbdav, object->href);
-       message = soup_message_new (SOUP_METHOD_PUT, uri);
-       g_free (uri);
-       if (message == NULL) {
-               g_propagate_error (perror, EDC_ERROR (NoSuchCal));
-               return FALSE;
-       }
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               gchar *calendar_data, *etag;
 
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
+               g_return_val_if_fail (href != NULL, FALSE);
 
-       /* For new items we use the If-None-Match so we don't
-        * acidently override resources, for item updates we
-        * use the If-Match header to avoid the Lost-update
-        * problem */
-       if (object->etag == NULL) {
-               soup_message_headers_append (message->request_headers, "If-None-Match", "*");
-       } else {
-               soup_message_headers_append (
-                       message->request_headers,
-                       "If-Match", object->etag);
-       }
+               calendar_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:calendar-data", 
xpath_prop_prefix);
+               etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:getetag", xpath_prop_prefix));
 
-       soup_message_set_request (
-               message,
-               "text/calendar; charset=utf-8",
-               SOUP_MEMORY_COPY,
-               object->cdata,
-               strlen (object->cdata));
+               if (calendar_data) {
+                       icalcomponent *vcalendar;
 
-       uri = NULL;
-       send_and_handle_redirection (cbdav, message, &uri, cancellable, perror);
+                       vcalendar = icalcomponent_new_from_string (calendar_data);
+                       if (vcalendar) {
+                               const gchar *uid;
 
-       if (uri) {
-               gchar *file = strrchr (uri, '/');
+                               uid = ecb_caldav_get_vcalendar_uid (vcalendar);
+                               if (uid) {
+                                       GSList *link;
 
-               /* there was a redirect, update href properly */
-               if (file) {
-                       gchar *decoded;
+                                       for (link = *from_link; link; link = g_slist_next (link)) {
+                                               ECalMetaBackendInfo *nfo = link->data;
 
-                       g_free (object->href);
+                                               if (!nfo)
+                                                       continue;
 
-                       decoded = soup_uri_decode (file + 1);
-                       object->href = soup_uri_encode (decoded ? decoded : (file + 1), NULL);
+                                               if (g_strcmp0 (nfo->extra, href) == 0) {
+                                                       /* If the server returns data in the same order as it 
had been requested,
+                                                          then this speeds up lookup for the matching 
object. */
+                                                       if (link == *from_link)
+                                                               *from_link = g_slist_next (*from_link);
 
-                       g_free (decoded);
-               }
+                                                       ecb_caldav_update_nfo_with_vcalendar (nfo, vcalendar, 
etag);
 
-               g_free (uri);
-       }
-
-       if (status_code_to_result (message, cbdav, FALSE, perror)) {
-               GError *local_error = NULL;
-
-               hdr = soup_message_headers_get_list (message->response_headers, "ETag");
-               if (hdr != NULL) {
-                       g_free (object->etag);
-                       object->etag = quote_etag (hdr);
-               }
-
-               /* "201 Created" can contain a Location with a link where the component was saved */
-               hdr = soup_message_headers_get_list (message->response_headers, "Location");
-               if (hdr) {
-                       /* reflect possible href change */
-                       gchar *file = strrchr (hdr, '/');
-
-                       if (file) {
-                               gchar *decoded;
-
-                               g_free (object->href);
-
-                               decoded = soup_uri_decode (file + 1);
-                               object->href = soup_uri_encode (decoded ? decoded : (file + 1), NULL);
-
-                               g_free (decoded);
-                       }
-               }
-
-               if (!caldav_server_get_object (cbdav, object, cancellable, &local_error)) {
-                       if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
-                               gchar *file;
-
-                               /* OK, the event was properly created, but cannot be found on the place
-                                * where it was PUT - why didn't server tell us where it saved it? */
-                               g_clear_error (&local_error);
-
-                               /* try whether it's saved as its UID.ics file */
-                               file = caldav_gen_file_from_uid_cal (cbdav, icalcomp);
-                               if (file) {
-                                       g_free (object->href);
-                                       object->href = file;
-
-                                       if (!caldav_server_get_object (cbdav, object, cancellable, 
&local_error)) {
-                                               if (g_error_matches (local_error, E_DATA_CAL_ERROR, 
ObjectNotFound)) {
-                                                       g_clear_error (&local_error);
-
-                                                       /* not sure what can happen, but do not need to guess 
for ever,
-                                                        * thus report success and update the calendar to get 
fresh info */
-                                                       update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
-                                                       g_cond_signal (&cbdav->priv->cond);
+                                                       break;
                                                }
                                        }
                                }
-                       }
-               }
-
-               if (!local_error) {
-                       icalcomponent *use_comp = NULL;
 
-                       if (object->cdata) {
-                               /* maybe server also modified component, thus rather store the server's */
-                               use_comp = icalparser_parse_string (object->cdata);
+                               icalcomponent_free (vcalendar);
                        }
-
-                       if (!use_comp)
-                               use_comp = icalcomp;
-
-                       put_comp_to_cache (cbdav, use_comp, object->href, object->etag);
-
-                       if (use_comp != icalcomp)
-                               icalcomponent_free (use_comp);
-               } else {
-                       g_propagate_error (perror, local_error);
                }
-       }
 
-       if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == 
SOUP_STATUS_FORBIDDEN) {
-               caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
+               g_free (calendar_data);
+               g_free (etag);
        }
 
-       g_object_unref (message);
-
        return TRUE;
 }
 
-static void
-caldav_server_delete_object (ECalBackendCalDAV *cbdav,
-                             CalDAVObject *object,
-                             GCancellable *cancellable,
-                             GError **perror)
-{
-       SoupMessage              *message;
-       gchar                     *uri;
-
-       g_return_if_fail (object != NULL && object->href != NULL);
-
-       uri = caldav_generate_uri (cbdav, object->href);
-       message = soup_message_new (SOUP_METHOD_DELETE, uri);
-       g_free (uri);
-       if (message == NULL) {
-               g_propagate_error (perror, EDC_ERROR (NoSuchCal));
-               return;
-       }
-
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
-
-       if (object->etag != NULL) {
-               soup_message_headers_append (
-                       message->request_headers,
-                       "If-Match", object->etag);
-       }
-
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, perror);
-
-       status_code_to_result (message, cbdav, FALSE, perror);
-
-       if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == SOUP_STATUS_FORBIDDEN)
-               caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-
-       g_object_unref (message);
-}
-
-static gboolean
-caldav_receive_schedule_outbox_url (ECalBackendCalDAV *cbdav,
-                                    GCancellable *cancellable,
-                                    GError **error)
-{
-       SoupMessage *message;
-       xmlOutputBufferPtr buf;
-       xmlDocPtr doc;
-       xmlNodePtr root, node;
-       xmlNsPtr nsdav;
-       gconstpointer buf_content;
-       gsize buf_size;
-       gchar *owner = NULL;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-       g_return_val_if_fail (cbdav->priv->schedule_outbox_url == NULL, TRUE);
-
-       /* Prepare the soup message */
-       message = soup_message_new ("PROPFIND", cbdav->priv->uri);
-       if (message == NULL)
-               return FALSE;
-
-       doc = xmlNewDoc ((xmlChar *) "1.0");
-       root = xmlNewDocNode (doc, NULL, (xmlChar *) "propfind", NULL);
-       xmlDocSetRootElement (doc, root);
-       nsdav = xmlNewNs (root, (xmlChar *) "DAV:", NULL);
-
-       node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
-       xmlNewTextChild (node, nsdav, (xmlChar *) "owner", NULL);
-
-       buf = xmlAllocOutputBuffer (NULL);
-       xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
-       xmlOutputBufferFlush (buf);
-
-       soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
-       soup_message_headers_append (message->request_headers, "Depth", "0");
-
-       buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
-       soup_message_set_request (
-               message,
-               "application/xml",
-               SOUP_MEMORY_COPY,
-               buf_content, buf_size);
-
-       /* Send the request now */
-       send_and_handle_redirection (cbdav, message, NULL, cancellable, error);
-
-       /* Clean up the memory */
-       xmlOutputBufferClose (buf);
-       xmlFreeDoc (doc);
-
-       /* Check the result */
-       if (message->status_code == SOUP_STATUS_MULTI_STATUS && parse_propfind_response (message, 
XPATH_OWNER_STATUS, XPATH_OWNER, &owner) && owner && *owner) {
-               xmlNsPtr nscd;
-               SoupURI *suri;
-
-               g_object_unref (message);
-
-               /* owner is a full path to the user's URL, thus change it in
-                * calendar's uri when asking for schedule-outbox-URL */
-               suri = soup_uri_new (cbdav->priv->uri);
-               soup_uri_set_path (suri, owner);
-               g_free (owner);
-               owner = soup_uri_to_string (suri, FALSE);
-               soup_uri_free (suri);
-
-               message = soup_message_new ("PROPFIND", owner);
-               if (message == NULL) {
-                       g_free (owner);
-                       return FALSE;
-               }
-
-               doc = xmlNewDoc ((xmlChar *) "1.0");
-               root = xmlNewDocNode (doc, NULL, (xmlChar *) "propfind", NULL);
-               xmlDocSetRootElement (doc, root);
-               nsdav = xmlNewNs (root, (xmlChar *) "DAV:", NULL);
-               nscd = xmlNewNs (root, (xmlChar *) "urn:ietf:params:xml:ns:caldav", (xmlChar *) "C");
-
-               node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
-               xmlNewTextChild (node, nscd, (xmlChar *) "schedule-outbox-URL", NULL);
-
-               buf = xmlAllocOutputBuffer (NULL);
-               xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
-               xmlOutputBufferFlush (buf);
-
-               soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
-               soup_message_headers_append (message->request_headers, "Depth", "0");
-
-               buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
-               soup_message_set_request (
-                       message,
-                       "application/xml",
-                       SOUP_MEMORY_COPY,
-                       buf_content, buf_size);
-
-               /* Send the request now */
-               send_and_handle_redirection (cbdav, message, NULL, cancellable, error);
-
-               if (message->status_code == SOUP_STATUS_MULTI_STATUS && parse_propfind_response (message, 
XPATH_SCHEDULE_OUTBOX_URL_STATUS, XPATH_SCHEDULE_OUTBOX_URL, &cbdav->priv->schedule_outbox_url)) {
-                       if (!*cbdav->priv->schedule_outbox_url) {
-                               g_free (cbdav->priv->schedule_outbox_url);
-                               cbdav->priv->schedule_outbox_url = NULL;
-                       } else {
-                               /* make it a full URI */
-                               suri = soup_uri_new (cbdav->priv->uri);
-                               soup_uri_set_path (suri, cbdav->priv->schedule_outbox_url);
-                               g_free (cbdav->priv->schedule_outbox_url);
-                               cbdav->priv->schedule_outbox_url = soup_uri_to_string (suri, FALSE);
-                               soup_uri_free (suri);
-                       }
-               }
-
-               /* Clean up the memory */
-               xmlOutputBufferClose (buf);
-               xmlFreeDoc (doc);
-       } else if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == 
SOUP_STATUS_FORBIDDEN) {
-               caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-       }
-
-       if (message)
-               g_object_unref (message);
-
-       g_free (owner);
-
-       return cbdav->priv->schedule_outbox_url != NULL;
-}
-
-/* ************************************************************************* */
-/* Synchronization foo */
-
-static gboolean extract_timezones (ECalBackendCalDAV *cbdav, icalcomponent *icomp);
-
-struct cache_comp_list
-{
-       GSList *slist;
-};
-
 static gboolean
-remove_complist_from_cache_and_notify_cb (gpointer key,
-                                          gpointer value,
-                                          gpointer data)
-{
-       GSList *l;
-       struct cache_comp_list *ccl = value;
-       ECalBackendCalDAV *cbdav = data;
-
-       for (l = ccl->slist; l; l = l->next) {
-               ECalComponent *old_comp = l->data;
-               ECalComponentId *id;
-
-               id = e_cal_component_get_id (old_comp);
-               if (!id) {
-                       continue;
-               }
-
-               if (e_cal_backend_store_remove_component (cbdav->priv->store, id->uid, id->rid)) {
-                       e_cal_backend_notify_component_removed ((ECalBackend *) cbdav, id, old_comp, NULL);
-               }
-
-               e_cal_component_free_id (id);
-       }
-       remove_cached_attachment (cbdav, (const gchar *) key);
-
-       return FALSE;
-}
-
-static void
-free_comp_list (gpointer cclist)
-{
-       struct cache_comp_list *ccl = cclist;
-
-       g_return_if_fail (ccl != NULL);
-
-       g_slist_foreach (ccl->slist, (GFunc) g_object_unref, NULL);
-       g_slist_free (ccl->slist);
-       g_free (ccl);
-}
-
-#define etags_match(_tag1, _tag2) ((_tag1 == _tag2) ? TRUE : \
-                                  g_str_equal (_tag1 != NULL ? _tag1 : "", \
-                                               _tag2 != NULL ? _tag2 : ""))
-
-/* start_time/end_time is an interval for checking changes. If both greater than zero,
- * only the interval is checked and the removed items are not notified, as they can
- * be still there.
-*/
-static void
-caldav_synchronize_cache (ECalBackendCalDAV *cbdav,
-                         time_t start_time,
-                         time_t end_time,
-                         gboolean can_check_ctag,
-                         GCancellable *cancellable)
-{
-       CalDAVObject *sobjs, *object;
-       GSList *c_objs, *c_iter; /* list of all items known from our cache */
-       GTree *c_uid2complist;  /* cache components list (with detached instances) sorted by (master's) uid */
-       GHashTable *c_href2uid; /* connection between href and a (master's) uid */
-       GSList *hrefs_to_update, *htu; /* list of href-s to update */
-       gint i, len;
-
-       /* intentionally do server-side checking first, and then the bool test,
-          to store actual ctag value first, and then update the content, to not
-          do it again the next time this function is called */
-       if (!check_calendar_changed_on_server (cbdav, start_time == (time_t) 0, cancellable) && 
can_check_ctag) {
-               /* no changes on the server, no update required */
-               return;
-       }
-
-       len = 0;
-       sobjs = NULL;
+ecb_caldav_multiget_from_sets_sync (ECalBackendCalDAV *cbdav,
+                                   GSList **in_link,
+                                   GSList **set2,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       EXmlDocument *xml;
+       gint left_to_go = E_CALDAV_MAX_MULTIGET_AMOUNT;
+       GSList *link;
+       gboolean success = TRUE;
 
-       /* get list of server objects */
-       if (!caldav_server_list_objects (cbdav, &sobjs, &len, NULL, start_time, end_time, cancellable))
-               return;
+       g_return_val_if_fail (in_link != NULL, FALSE);
+       g_return_val_if_fail (*in_link != NULL, FALSE);
+       g_return_val_if_fail (set2 != NULL, FALSE);
 
-       c_objs = e_cal_backend_store_get_components (cbdav->priv->store);
+       xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "calendar-multiget");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
-               printf ("CalDAV - found %d objects on the server, locally stored %d objects\n", len, 
g_slist_length (c_objs)); fflush (stdout);
-       }
+       e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
 
-       /* do not store changes in cache immediately - makes things significantly quicker */
-       e_cal_backend_store_freeze_changes (cbdav->priv->store);
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-data");
+       e_xml_document_end_element (xml); /* prop */
 
-       c_uid2complist = g_tree_new_full ((GCompareDataFunc) g_strcmp0, NULL, g_free, free_comp_list);
-       c_href2uid = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+       link = *in_link;
 
-       /* fill indexed hash and tree with cached components */
-       for (c_iter = c_objs; c_iter; c_iter = g_slist_next (c_iter)) {
-               ECalComponent *ccomp = E_CAL_COMPONENT (c_iter->data);
-               const gchar *uid = NULL;
-               struct cache_comp_list *ccl;
-               gchar *href;
+       while (link && left_to_go > 0) {
+               ECalMetaBackendInfo *nfo = link->data;
 
-               e_cal_component_get_uid (ccomp, &uid);
-               if (!uid) {
-                       g_warning ("broken component with NULL Id");
-                       continue;
+               link = g_slist_next (link);
+               if (!link) {
+                       link = *set2;
+                       *set2 = NULL;
                }
 
-               href = ecalcomp_get_href (ccomp);
-
-               if (href == NULL) {
-                       g_warning ("href of object NULL :(");
+               if (!nfo)
                        continue;
-               }
 
-               ccl = g_tree_lookup (c_uid2complist, uid);
-               if (ccl) {
-                       ccl->slist = g_slist_prepend (ccl->slist, g_object_ref (ccomp));
-               } else {
-                       ccl = g_new0 (struct cache_comp_list, 1);
-                       ccl->slist = g_slist_append (NULL, g_object_ref (ccomp));
-
-                       /* make a copy, which will be used in the c_href2uid too */
-                       uid = g_strdup (uid);
+               left_to_go--;
 
-                       g_tree_insert (c_uid2complist, (gpointer) uid, ccl);
-               }
+               /* iCloud returns broken calendar-multiget responses, with
+                  empty <DAV:href> elements, thus read one-by-one for it.
+                  This is confirmed as of 2017-04-11. */
+               if (cbdav->priv->is_icloud) {
+                       gchar *calendar_data = NULL, *etag = NULL;
 
-               if (g_hash_table_lookup (c_href2uid, href) == NULL) {
-                       /* uid is from a component or c_uid2complist key, thus will not be
-                        * freed before a removal from c_uid2complist, thus do not duplicate it,
-                        * rather save memory */
-                       g_hash_table_insert (c_href2uid, href, (gpointer) uid);
-               } else {
-                       g_free (href);
-               }
-       }
+                       success = e_webdav_session_get_data_sync (cbdav->priv->webdav,
+                               nfo->extra, NULL, &etag, &calendar_data, NULL, cancellable, error);
 
-       /* clear it now, we do not need it later */
-       g_slist_foreach (c_objs, (GFunc) g_object_unref, NULL);
-       g_slist_free (c_objs);
-       c_objs = NULL;
+                       if (success && calendar_data) {
+                               icalcomponent *vcalendar;
 
-       hrefs_to_update = NULL;
-
-       /* see if we have to update or add some objects */
-       for (i = 0, object = sobjs; i < len && cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK; i++, object++) {
-               ECalComponent *ccomp = NULL;
-               gchar *etag = NULL;
-               const gchar *uid;
-               struct cache_comp_list *ccl;
-
-               if (object->status != 200) {
-                       /* just continue here, so that the object
-                        * doesnt get removed from the cobjs list
-                        * - therefore it will be removed */
-                       continue;
-               }
-
-               uid = g_hash_table_lookup (c_href2uid, object->href);
-               if (uid) {
-                       ccl = g_tree_lookup (c_uid2complist, uid);
-                       if (ccl) {
-                               GSList *sl;
-                               for (sl = ccl->slist; sl && !etag; sl = sl->next) {
-                                       ccomp = sl->data;
-                                       if (ccomp)
-                                               etag = ecalcomp_get_etag (ccomp);
+                               vcalendar = icalcomponent_new_from_string (calendar_data);
+                               if (vcalendar) {
+                                       ecb_caldav_update_nfo_with_vcalendar (nfo, vcalendar, etag);
+                                       icalcomponent_free (vcalendar);
                                }
-
-                               if (!etag)
-                                       ccomp = NULL;
-                       }
-               }
-
-               if (!etag || !etags_match (etag, object->etag)) {
-                       hrefs_to_update = g_slist_prepend (hrefs_to_update, object->href);
-               } else if (uid && ccl) {
-                       /* all components cover by this uid are up-to-date */
-                       GSList *p;
-
-                       for (p = ccl->slist; p; p = p->next) {
-                               g_object_unref (p->data);
                        }
 
-                       g_slist_free (ccl->slist);
-                       ccl->slist = NULL;
-               }
-
-               g_free (etag);
-       }
-
-       /* free hash table, as it is not used anymore */
-       g_hash_table_destroy (c_href2uid);
-       c_href2uid = NULL;
-
-       if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
-               printf ("CalDAV - recognized %d items to update\n", g_slist_length (hrefs_to_update)); fflush 
(stdout);
-       }
-
-       htu = hrefs_to_update;
-       while (htu && cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK) {
-               gint count = 0;
-               GSList *to_fetch = NULL;
-
-               while (count < CALDAV_MAX_MULTIGET_AMOUNT && htu) {
-                       to_fetch = g_slist_prepend (to_fetch, htu->data);
-                       htu = htu->next;
-                       count++;
-               }
-
-               if (to_fetch && cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK) {
-                       CalDAVObject *up_sobjs = NULL;
+                       g_free (calendar_data);
+                       g_free (etag);
 
-                       if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
-                               printf ("CalDAV - going to fetch %d items\n", g_slist_length (to_fetch)); 
fflush (stdout);
-                       }
-
-                       count = 0;
-                       if (!caldav_server_list_objects (cbdav, &up_sobjs, &count, to_fetch, 0, 0, 
cancellable)) {
-                               fprintf (stderr, "CalDAV - failed to retrieve bunch of items\n"); fflush 
(stderr);
+                       if (!success)
                                break;
-                       }
+               } else {
+                       SoupURI *suri;
+                       gchar *path = NULL;
 
-                       if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
-                               printf ("CalDAV - fetched bunch of %d items\n", count); fflush (stdout);
+                       suri = soup_uri_new (nfo->extra);
+                       if (suri) {
+                               path = soup_uri_to_string (suri, TRUE);
+                               soup_uri_free (suri);
                        }
 
-                       /* we are going to update cache */
-                       /* they are downloaded, so process them */
-                       for (i = 0, object = up_sobjs; i < count /*&& cbdav->priv->slave_cmd == 
SLAVE_SHOULD_WORK */; i++, object++) {
-                               if (object->status == 200 && object->href && object->etag && object->cdata && 
*object->cdata) {
-                                       icalcomponent *icomp = icalparser_parse_string (object->cdata);
+                       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "href");
+                       e_xml_document_write_string (xml, path ? path : nfo->extra);
+                       e_xml_document_end_element (xml); /* href */
 
-                                       if (icomp) {
-                                               put_server_comp_to_cache (cbdav, icomp, object->href, 
object->etag, c_uid2complist);
-                                               icalcomponent_free (icomp);
-                                       }
-                               }
-
-                               /* these free immediately */
-                               caldav_object_free (object, FALSE);
-                       }
-
-                       /* cache update done for fetched items */
-                       g_free (up_sobjs);
+                       g_free (path);
                }
-
-               /* do not free 'data' itself, it's part of 'sobjs' */
-               g_slist_free (to_fetch);
        }
 
-       /* if not interrupted and not using the time range... */
-       if (cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK && (!start_time || !end_time)) {
-               /* ...remove old (not on server anymore) items from our cache and notify of a removal */
-               g_tree_foreach (c_uid2complist, remove_complist_from_cache_and_notify_cb, cbdav);
-       }
-
-       if (cbdav->priv->ctag_to_store) {
-               /* store only when wasn't interrupted */
-               if (cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK && start_time == 0 && end_time == 0) {
-                       e_cal_backend_store_put_key_value (cbdav->priv->store, CALDAV_CTAG_KEY, 
cbdav->priv->ctag_to_store);
-               }
+       if (left_to_go != E_CALDAV_MAX_MULTIGET_AMOUNT &&
+           !cbdav->priv->is_icloud && success) {
+               GSList *from_link = *in_link;
 
-               g_free (cbdav->priv->ctag_to_store);
-               cbdav->priv->ctag_to_store = NULL;
+               success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, NULL, xml,
+                       ecb_caldav_multiget_response_cb, &from_link, NULL, NULL, cancellable, error);
        }
 
-       /* save cache changes to disk finally */
-       e_cal_backend_store_thaw_changes (cbdav->priv->store);
+       g_object_unref (xml);
 
-       for (i = 0, object = sobjs; i < len; i++, object++) {
-               caldav_object_free (object, FALSE);
-       }
+       *in_link = link;
 
-       g_tree_destroy (c_uid2complist);
-       g_slist_free (hrefs_to_update);
-       g_free (sobjs);
+       return success;
 }
 
-static void
-check_server_tweaks (ECalBackendCalDAV *cbdav)
+static gboolean
+ecb_caldav_get_calendar_items_cb (EWebDAVSession *webdav,
+                                 xmlXPathContextPtr xpath_ctx,
+                                 const gchar *xpath_prop_prefix,
+                                 const SoupURI *request_uri,
+                                 const gchar *href,
+                                 guint status_code,
+                                 gpointer user_data)
 {
-       SoupURI *suri;
+       GHashTable *known_items = user_data; /* gchar *href ~> ECalMetaBackendInfo * */
 
-       g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (known_items != NULL, FALSE);
 
-       cbdav->priv->is_google = FALSE;
-       cbdav->priv->is_icloud = FALSE;
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               ECalMetaBackendInfo *nfo;
+               gchar *etag;
 
-       g_return_if_fail (cbdav->priv->uri);
+               g_return_val_if_fail (href != NULL, FALSE);
 
-       suri = soup_uri_new (cbdav->priv->uri);
-       g_return_if_fail (suri != NULL);
+               /* Skip collection resource, if returned by the server (like iCloud.com does) */
+               if (g_str_has_suffix (href, "/") ||
+                   (request_uri && request_uri->path && g_str_has_suffix (href, request_uri->path)))
+                       return TRUE;
 
-       cbdav->priv->is_google = suri->host && (
-               g_ascii_strcasecmp (suri->host, "www.google.com") == 0 ||
-               g_ascii_strcasecmp (suri->host, "apidata.googleusercontent.com") == 0);
+               etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:getetag", xpath_prop_prefix));
+               /* Return 'TRUE' to not stop on faulty data from the server */
+               g_return_val_if_fail (etag != NULL, TRUE);
 
-       cbdav->priv->is_icloud = suri->host && e_util_utf8_strstrcase (suri->host, ".icloud.com");
+               /* UID is unknown at this moment */
+               nfo = e_cal_meta_backend_info_new ("", etag, NULL, href);
 
-       soup_uri_free (suri);
-}
-
-static void
-time_to_refresh_caldav_calendar_cb (ESource *source,
-                                    gpointer user_data)
-{
-       ECalBackendCalDAV *cbdav = user_data;
+               g_free (etag);
+               g_return_val_if_fail (nfo != NULL, FALSE);
 
-       g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
+               g_hash_table_insert (known_items, g_strdup (href), nfo);
+       }
 
-       g_cond_signal (&cbdav->priv->cond);
+       return TRUE;
 }
 
-/* ************************************************************************* */
-
-static gpointer
-caldav_synch_slave_loop (gpointer data)
-{
-       ECalBackendCalDAV *cbdav;
-       time_t now;
-       icaltimezone *utc = icaltimezone_get_utc_timezone ();
-       gboolean know_unreachable;
-
-       cbdav = E_CAL_BACKEND_CALDAV (data);
-
-       g_mutex_lock (&cbdav->priv->busy_lock);
-
-       know_unreachable = !cbdav->priv->opened;
-
-       while (cbdav->priv->slave_cmd != SLAVE_SHOULD_DIE) {
-               gboolean can_check_ctag = TRUE;
-
-               if (cbdav->priv->slave_cmd == SLAVE_SHOULD_SLEEP) {
-                       /* just sleep until we get woken up again */
-                       g_cond_wait (&cbdav->priv->cond, &cbdav->priv->busy_lock);
-
-                       /* This means to honor SLAVE_SHOULD_SLEEP only if the backend is opened */
-                       if (cbdav->priv->slave_cmd == SLAVE_SHOULD_DIE ||
-                           cbdav->priv->opened) {
-                               /* check if we should die, work or sleep again */
-                               continue;
-                       }
-               }
+typedef struct _CalDAVChangesData {
+       gboolean is_repeat;
+       GSList **out_modified_objects;
+       GSList **out_removed_objects;
+       GHashTable *known_items; /* gchar *href ~> ECalMetaBackendInfo * */
+} CalDAVChangesData;
 
-               /* Ok here we go, do some real work
-                * Synch it baby one more time ...
-                */
-               cbdav->priv->slave_busy = TRUE;
-               if (cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK_NO_CTAG_CHECK) {
-                       cbdav->priv->slave_cmd = SLAVE_SHOULD_WORK;
-                       can_check_ctag = FALSE;
-               }
-
-               if (!cbdav->priv->opened) {
-                       gchar *certificate_pem = NULL;
-                       GTlsCertificateFlags certificate_errors = 0;
-                       GError *local_error = NULL;
+static gboolean
+ecb_caldav_search_changes_cb (ECalCache *cal_cache,
+                             const gchar *uid,
+                             const gchar *rid,
+                             const gchar *revision,
+                             const gchar *object,
+                             const gchar *extra,
+                             EOfflineState offline_state,
+                             gpointer user_data)
+{
+       CalDAVChangesData *ccd = user_data;
+
+       g_return_val_if_fail (ccd != NULL, FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
 
-                       if (open_calendar_wrapper (cbdav, NULL, &local_error, TRUE, &know_unreachable, 
&certificate_pem, &certificate_errors)) {
-                               cbdav->priv->opened = TRUE;
-                               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
-                               g_cond_signal (&cbdav->priv->cond);
+       /* Can be NULL for added components in offline mode */
+       if (extra && *extra && (!rid || !*rid)) {
+               ECalMetaBackendInfo *nfo;
 
-                               check_server_tweaks (cbdav);
-                               know_unreachable = FALSE;
+               nfo = g_hash_table_lookup (ccd->known_items, extra);
+               if (nfo) {
+                       if (g_strcmp0 (revision, nfo->revision) == 0) {
+                               g_hash_table_remove (ccd->known_items, extra);
                        } else {
-                               ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
-                               GError *local_error2 = NULL;
-
-                               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-                                       reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
-                               }
-
-                               if (!e_backend_credentials_required_sync (E_BACKEND (cbdav), reason, 
certificate_pem, certificate_errors,
-                                       local_error, NULL, &local_error2)) {
-                                       g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC, 
local_error2 ? local_error2->message : "Unknown error");
+                               if (!nfo->uid || !*(nfo->uid)) {
+                                       g_free (nfo->uid);
+                                       nfo->uid = g_strdup (uid);
                                }
 
-                               g_clear_error (&local_error2);
-                       }
-
-                       g_clear_error (&local_error);
-                       g_free (certificate_pem);
-               }
-
-               if (cbdav->priv->opened) {
-                       time (&now);
-                       /* check for events in the month before/after today first,
-                        * to show user actual data as soon as possible */
-                       caldav_synchronize_cache (cbdav, time_add_week_with_zone (now, -5, utc), 
time_add_week_with_zone (now, +5, utc), can_check_ctag, NULL);
-
-                       if (cbdav->priv->slave_cmd != SLAVE_SHOULD_SLEEP) {
-                               /* and then check for changes in a whole calendar */
-                               caldav_synchronize_cache (cbdav, 0, 0, can_check_ctag, NULL);
-                       }
-
-                       if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
-                               GSList *c_objs;
-
-                               c_objs = e_cal_backend_store_get_components (cbdav->priv->store);
+                               *(ccd->out_modified_objects) = g_slist_prepend (*(ccd->out_modified_objects),
+                                       e_cal_meta_backend_info_copy (nfo));
 
-                               printf ("CalDAV - finished syncing with %d items in a cache\n", 
g_slist_length (c_objs)); fflush (stdout);
-
-                               g_slist_foreach (c_objs, (GFunc) g_object_unref, NULL);
-                               g_slist_free (c_objs);
+                               g_hash_table_remove (ccd->known_items, extra);
                        }
+               } else if (ccd->is_repeat) {
+                       *(ccd->out_removed_objects) = g_slist_prepend (*(ccd->out_removed_objects),
+                               e_cal_meta_backend_info_new (uid, revision, object, extra));
                }
-
-               cbdav->priv->slave_busy = FALSE;
-
-               /* puhh that was hard, get some rest :) */
-               g_cond_wait (&cbdav->priv->cond, &cbdav->priv->busy_lock);
        }
 
-       cbdav->priv->synch_slave = NULL;
-
-       /* signal we are done */
-       g_cond_signal (&cbdav->priv->slave_gone_cond);
-
-       /* we got killed ... */
-       g_mutex_unlock (&cbdav->priv->busy_lock);
-       return NULL;
-}
-
-static gchar *
-maybe_append_email_domain (const gchar *username,
-                           const gchar *may_append)
-{
-       if (!username || !*username)
-               return NULL;
-
-       if (strchr (username, '@'))
-               return g_strdup (username);
-
-       return g_strconcat (username, may_append, NULL);
+       return TRUE;
 }
 
-static gchar *
-get_usermail (ECalBackend *backend)
+static gboolean
+ecb_caldav_get_changes_sync (ECalMetaBackend *meta_backend,
+                            const gchar *last_sync_tag,
+                            gboolean is_repeat,
+                            gchar **out_new_sync_tag,
+                            gboolean *out_repeat,
+                            GSList **out_created_objects,
+                            GSList **out_modified_objects,
+                            GSList **out_removed_objects,
+                            GCancellable *cancellable,
+                            GError **error)
 {
        ECalBackendCalDAV *cbdav;
-       ESource *source;
-       ESourceAuthentication *auth_extension;
-       ESourceWebdav *webdav_extension;
-       const gchar *extension_name;
-       gchar *usermail;
-       gchar *username;
-       gchar *res = NULL;
-
-       g_return_val_if_fail (backend != NULL, NULL);
+       EXmlDocument *xml;
+       GHashTable *known_items; /* gchar *href ~> ECalMetaBackendInfo * */
+       GHashTableIter iter;
+       gpointer key = NULL, value = NULL;
+       gboolean success;
 
-       source = e_backend_get_source (E_BACKEND (backend));
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+       g_return_val_if_fail (out_repeat, FALSE);
+       g_return_val_if_fail (out_new_sync_tag, FALSE);
+       g_return_val_if_fail (out_created_objects, FALSE);
+       g_return_val_if_fail (out_modified_objects, FALSE);
+       g_return_val_if_fail (out_removed_objects, FALSE);
+
+       *out_new_sync_tag = NULL;
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
+
+       if (cbdav->priv->ctag_supported) {
+               gchar *new_sync_tag = NULL;
+
+               success = e_webdav_session_getctag_sync (cbdav->priv->webdav, NULL, &new_sync_tag, 
cancellable, NULL);
+               if (!success) {
+                       cbdav->priv->ctag_supported = g_cancellable_set_error_if_cancelled (cancellable, 
error);
+                       if (cbdav->priv->ctag_supported || !cbdav->priv->webdav)
+                               return FALSE;
+               } else if (!is_repeat && new_sync_tag && last_sync_tag && g_strcmp0 (last_sync_tag, 
new_sync_tag) == 0) {
+                       *out_new_sync_tag = new_sync_tag;
+                       return TRUE;
+               }
+
+               /* Do not advertise the new ctag in the first go, otherwise a failure
+                  in the second go might hide some events. */
+               if (is_repeat)
+                       *out_new_sync_tag = new_sync_tag;
+               else
+                       g_free (new_sync_tag);
+       }
 
-       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-       webdav_extension = e_source_get_extension (source, extension_name);
+       xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "calendar-query");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       /* This will never return an empty string. */
-       usermail = e_source_webdav_dup_email_address (webdav_extension);
+       e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
 
-       if (usermail != NULL)
-               return usermail;
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+       e_xml_document_end_element (xml); /* prop */
 
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
+       e_xml_document_start_element (xml, NULL, "filter");
+       e_xml_document_start_element (xml, NULL, "comp-filter");
+       e_xml_document_add_attribute (xml, NULL, "name", "VCALENDAR");
+       e_xml_document_start_element (xml, NULL, "comp-filter");
 
-       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-       auth_extension = e_source_get_extension (source, extension_name);
-       username = e_source_authentication_dup_user (auth_extension);
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbdav))) {
+       default:
+       case ICAL_VEVENT_COMPONENT:
+               e_xml_document_add_attribute (xml, NULL, "name", "VEVENT");
+               break;
+       case ICAL_VJOURNAL_COMPONENT:
+               e_xml_document_add_attribute (xml, NULL, "name", "VJOURNAL");
+               break;
+       case ICAL_VTODO_COMPONENT:
+               e_xml_document_add_attribute (xml, NULL, "name", "VTODO");
+               break;
+       }
 
-       if (cbdav->priv && cbdav->priv->is_google)
-               res = maybe_append_email_domain (username, "@gmail.com");
+       if (!is_repeat) {
+               icaltimezone *utc = icaltimezone_get_utc_timezone ();
+               time_t now;
+               gchar *tmp;
 
-       g_free (username);
+               time (&now);
 
-       return res;
-}
+               *out_repeat = TRUE;
 
-/* ************************************************************************* */
-/* ********** ECalBackendSync virtual function implementation *************  */
+               /* Check for events in the month before/after today first,
+                  to show user actual data as soon as possible. */
+               e_xml_document_start_element (xml, NULL, "time-range");
 
-static gchar *
-caldav_get_backend_property (ECalBackend *backend,
-                             const gchar *prop_name)
-{
-       g_return_val_if_fail (prop_name != NULL, FALSE);
+               tmp = isodate_from_time_t (time_add_week_with_zone (now, -5, utc));
+               e_xml_document_add_attribute (xml, NULL, "start", tmp);
+               g_free (tmp);
 
-       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
-               ESourceWebdav *extension;
-               ESource *source;
-               GString *caps;
-               gchar *usermail;
-               const gchar *extension_name;
+               tmp = isodate_from_time_t (time_add_week_with_zone (now, +5, utc));
+               e_xml_document_add_attribute (xml, NULL, "end", tmp);
+               g_free (tmp);
 
-               caps = g_string_new (
-                       CAL_STATIC_CAPABILITY_NO_THISANDPRIOR ","
-                       CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED);
+               e_xml_document_end_element (xml); /* time-range */
+       }
 
-               usermail = get_usermail (E_CAL_BACKEND (backend));
-               if (!usermail || !*usermail)
-                       g_string_append (caps, "," CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS);
-               g_free (usermail);
+       e_xml_document_end_element (xml); /* comp-filter / VEVENT|VJOURNAL|VTODO */
+       e_xml_document_end_element (xml); /* comp-filter / VCALENDAR*/
+       e_xml_document_end_element (xml); /* filter */
 
-               source = e_backend_get_source (E_BACKEND (backend));
+       known_items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_cal_meta_backend_info_free);
 
-               extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-               extension = e_source_get_extension (source, extension_name);
+       success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS_AND_CHILDREN, 
xml,
+               ecb_caldav_get_calendar_items_cb, known_items, NULL, NULL, cancellable, error);
 
-               if (e_source_webdav_get_calendar_auto_schedule (extension)) {
-                       g_string_append (
-                               caps,
-                               "," CAL_STATIC_CAPABILITY_CREATE_MESSAGES
-                               "," CAL_STATIC_CAPABILITY_SAVE_SCHEDULES);
-               }
+       g_object_unref (xml);
 
-               return g_string_free (caps, FALSE);
+       if (success) {
+               ECalCache *cal_cache;
+               CalDAVChangesData ccd;
 
-       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
-                  g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
-               return get_usermail (E_CAL_BACKEND (backend));
-
-       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
-               ECalComponent *comp;
-               gchar *prop_value;
-
-               comp = e_cal_component_new ();
-
-               switch (e_cal_backend_get_kind (E_CAL_BACKEND (backend))) {
-               case ICAL_VEVENT_COMPONENT:
-                       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_EVENT);
-                       break;
-               case ICAL_VTODO_COMPONENT:
-                       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_TODO);
-                       break;
-               case ICAL_VJOURNAL_COMPONENT:
-                       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_JOURNAL);
-                       break;
-               default:
-                       g_object_unref (comp);
-                       return NULL;
-               }
+               ccd.is_repeat = is_repeat;
+               ccd.out_modified_objects = out_modified_objects;
+               ccd.out_removed_objects = out_removed_objects;
+               ccd.known_items = known_items;
 
-               prop_value = e_cal_component_get_as_string (comp);
+               cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
 
-               g_object_unref (comp);
+               success = e_cal_cache_search_with_callback (cal_cache, NULL, ecb_caldav_search_changes_cb, 
&ccd, cancellable, error);
 
-               return prop_value;
+               g_clear_object (&cal_cache);
        }
 
-       /* Chain up to parent's get_backend_property() method. */
-       return E_CAL_BACKEND_CLASS (e_cal_backend_caldav_parent_class)->
-               get_backend_property (backend, prop_name);
-}
-
-static void
-caldav_shutdown (ECalBackend *backend)
-{
-       ECalBackendCalDAVPrivate *priv;
-       ESource *source;
-
-       priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (backend);
-
-       /* Chain up to parent's shutdown() method. */
-       E_CAL_BACKEND_CLASS (e_cal_backend_caldav_parent_class)->shutdown (backend);
+       if (!success) {
+               g_hash_table_destroy (known_items);
+               return FALSE;
+       }
 
-       /* tell the slave to stop before acquiring a lock,
-        * as it can work at the moment, and lock can be locked */
-       update_slave_cmd (priv, SLAVE_SHOULD_DIE);
+       g_hash_table_iter_init (&iter, known_items);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               *out_created_objects = g_slist_prepend (*out_created_objects, e_cal_meta_backend_info_copy 
(value));
+       }
 
-       g_mutex_lock (&priv->busy_lock);
+       g_hash_table_destroy (known_items);
 
-       /* XXX Not sure if this really needs to be part of
-        *     shutdown or if we can just do it in dispose(). */
-       source = e_backend_get_source (E_BACKEND (backend));
-       if (source) {
-               g_signal_handlers_disconnect_by_func (G_OBJECT (source), caldav_source_changed_cb, backend);
+       if (*out_created_objects || *out_modified_objects) {
+               GSList *link, *set2 = *out_modified_objects;
 
-               if (priv->refresh_id) {
-                       e_source_refresh_remove_timeout (source, priv->refresh_id);
-                       priv->refresh_id = 0;
+               if (*out_created_objects) {
+                       link = *out_created_objects;
+               } else {
+                       link = set2;
+                       set2 = NULL;
                }
-       }
-
-       /* stop the slave  */
-       while (priv->synch_slave) {
-               g_cond_signal (&priv->cond);
 
-               /* wait until the slave died */
-               g_cond_wait (&priv->slave_gone_cond, &priv->busy_lock);
+               do {
+                       success = ecb_caldav_multiget_from_sets_sync (cbdav, &link, &set2, cancellable, 
error);
+               } while (success && link);
        }
 
-       g_mutex_unlock (&priv->busy_lock);
+       return success;
 }
 
 static gboolean
-initialize_backend (ECalBackendCalDAV *cbdav,
-                    GError **perror)
+ecb_caldav_extract_existing_cb (EWebDAVSession *webdav,
+                               xmlXPathContextPtr xpath_ctx,
+                               const gchar *xpath_prop_prefix,
+                               const SoupURI *request_uri,
+                               const gchar *href,
+                               guint status_code,
+                               gpointer user_data)
 {
-       ESourceAuthentication    *auth_extension;
-       ESourceOffline           *offline_extension;
-       ESourceWebdav            *webdav_extension;
-       ECalBackend              *backend;
-       SoupURI                  *soup_uri;
-       ESource                  *source;
-       gsize                     len;
-       const gchar              *cache_dir;
-       const gchar              *extension_name;
-
-       backend = E_CAL_BACKEND (cbdav);
-       cache_dir = e_cal_backend_get_cache_dir (backend);
-       source = e_backend_get_source (E_BACKEND (backend));
-
-       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-       auth_extension = e_source_get_extension (source, extension_name);
-
-       extension_name = E_SOURCE_EXTENSION_OFFLINE;
-       offline_extension = e_source_get_extension (source, extension_name);
-
-       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-       webdav_extension = e_source_get_extension (source, extension_name);
+       GSList **out_existing_objects = user_data;
 
-       if (!g_signal_handler_find (G_OBJECT (source), G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, 
caldav_source_changed_cb, cbdav))
-               g_signal_connect (G_OBJECT (source), "changed", G_CALLBACK (caldav_source_changed_cb), cbdav);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
 
-       cbdav->priv->loaded = TRUE;
-       cbdav->priv->do_offline = e_source_offline_get_stay_synchronized (offline_extension);
-       cbdav->priv->auth_required = e_source_authentication_required (auth_extension);
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               gchar *etag;
+               gchar *calendar_data;
 
-       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
-
-       /* properly encode uri */
-       if (soup_uri != NULL && soup_uri->path != NULL) {
-               gchar *tmp, *path;
-
-               if (strchr (soup_uri->path, '%')) {
-                       /* If path contains anything already encoded, then
-                        * decode it first, thus it'll be managed properly.
-                        * For example, the '#' in a path is in URI shown as
-                        * %23 and not doing this decode makes it being like
-                        * %2523, which is not what is wanted here. */
-                       tmp = soup_uri_decode (soup_uri->path);
-                       soup_uri_set_path (soup_uri, tmp);
-                       g_free (tmp);
-               }
-
-               tmp = soup_uri_encode (soup_uri->path, NULL);
-               path = soup_uri_normalize (tmp, "/");
+               g_return_val_if_fail (href != NULL, FALSE);
 
-               soup_uri_set_path (soup_uri, path);
+               etag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:getetag", xpath_prop_prefix);
+               calendar_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:calendar-data", 
xpath_prop_prefix);
 
-               g_free (tmp);
-               g_free (path);
-       }
+               if (calendar_data) {
+                       icalcomponent *vcalendar;
 
-       g_free (cbdav->priv->uri);
-       cbdav->priv->uri = soup_uri_to_string (soup_uri, FALSE);
+                       vcalendar = icalcomponent_new_from_string (calendar_data);
+                       if (vcalendar) {
+                               const gchar *uid;
 
-       soup_uri_free (soup_uri);
+                               uid = ecb_caldav_get_vcalendar_uid (vcalendar);
 
-       g_return_val_if_fail (cbdav->priv->uri != NULL, FALSE);
+                               if (uid) {
+                                       etag = e_webdav_session_util_maybe_dequote (etag);
+                                       *out_existing_objects = g_slist_prepend (*out_existing_objects,
+                                               e_cal_meta_backend_info_new (uid, etag, NULL, href));
+                               }
 
-       /* remove trailing slashes... */
-       if (cbdav->priv->uri != NULL) {
-               len = strlen (cbdav->priv->uri);
-               while (len--) {
-                       if (cbdav->priv->uri[len] == '/') {
-                               cbdav->priv->uri[len] = '\0';
-                       } else {
-                               break;
+                               icalcomponent_free (vcalendar);
                        }
                }
-       }
-
-       /* ...and append exactly one slash */
-       if (cbdav->priv->uri && *cbdav->priv->uri) {
-               gchar *tmp = cbdav->priv->uri;
 
-               cbdav->priv->uri = g_strconcat (cbdav->priv->uri, "/", NULL);
-
-               g_free (tmp);
-       }
-
-       if (cbdav->priv->store == NULL) {
-               /* remove the old cache while migrating to ECalBackendStore */
-               e_cal_backend_cache_remove (cache_dir, "cache.xml");
-               cbdav->priv->store = e_cal_backend_store_new (
-                       cache_dir, E_TIMEZONE_CACHE (cbdav));
-               e_cal_backend_store_load (cbdav->priv->store);
-       }
-
-       /* Set the local attachment store */
-       if (g_mkdir_with_parents (cache_dir, 0700) < 0) {
-               g_propagate_error (perror, e_data_cal_create_error_fmt (OtherError, _("Cannot create local 
cache folder “%s”"), cache_dir));
-               return FALSE;
-       }
-
-       if (!cbdav->priv->synch_slave) {
-               GThread *slave;
-
-               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
-               slave = g_thread_new (NULL, caldav_synch_slave_loop, cbdav);
-
-               cbdav->priv->synch_slave = slave;
-               g_thread_unref (slave);
-       }
-
-       if (cbdav->priv->refresh_id == 0) {
-               cbdav->priv->refresh_id = e_source_refresh_add_timeout (
-                       source, NULL, time_to_refresh_caldav_calendar_cb, cbdav, NULL);
+               g_free (calendar_data);
+               g_free (etag);
        }
 
        return TRUE;
 }
 
 static gboolean
-caldav_was_ever_connected (ECalBackendCalDAV *cbdav)
+ecb_caldav_list_existing_sync (ECalMetaBackend *meta_backend,
+                              gchar **out_new_sync_tag,
+                              GSList **out_existing_objects,
+                              GCancellable *cancellable,
+                              GError **error)
 {
-       gboolean has_components;
-       GSList *uids;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-
-       if (!cbdav->priv->store)
-               return FALSE;
-
-       uids = e_cal_backend_store_get_component_ids (cbdav->priv->store);
-
-       /* Assume the calendar was connected if it has any events stored;
-          obviously, empty calendars will fail this check. */
-       has_components = uids != NULL;
-
-       g_slist_free_full (uids, (GDestroyNotify) e_cal_component_free_id);
-
-       return has_components;
-}
-
-static gboolean
-open_calendar_wrapper (ECalBackendCalDAV *cbdav,
-                      GCancellable *cancellable,
-                      GError **error,
-                      gboolean first_attempt,
-                      gboolean *know_unreachable,
-                      gchar **out_certificate_pem,
-                      GTlsCertificateFlags *out_certificate_errors)
-{
-       gboolean server_unreachable = FALSE;
-       gboolean awaiting_credentials = FALSE;
+       ECalBackendCalDAV *cbdav;
+       icalcomponent_kind kind;
+       EXmlDocument *xml;
        gboolean success;
-       GError *local_error = NULL;
 
-       g_return_val_if_fail (cbdav != NULL, FALSE);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
 
-       if (!cbdav->priv->loaded && !initialize_backend (cbdav, error))
-               return FALSE;
+       *out_existing_objects = NULL;
 
-       if (!caldav_maybe_prepare_bearer_auth (cbdav, cancellable, error))
-               return FALSE;
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
+       kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
 
-       success = caldav_server_open_calendar (cbdav, &server_unreachable, out_certificate_pem, 
out_certificate_errors, cancellable, &local_error);
+       xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "calendar-query");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       if (first_attempt && g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed)) {
-               g_clear_error (&local_error);
-               awaiting_credentials = TRUE;
-               success = caldav_credentials_required_sync (cbdav, FALSE, first_attempt, cancellable, 
&local_error);
-       }
-
-       if (success) {
-               check_server_tweaks (cbdav);
-
-               if (!awaiting_credentials) {
-                       update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
-                       g_cond_signal (&cbdav->priv->cond);
-               }
-       } else if (server_unreachable) {
-               cbdav->priv->opened = FALSE;
-               e_cal_backend_set_writable (E_CAL_BACKEND (cbdav), FALSE);
-               if (local_error) {
-                       if (know_unreachable && !*know_unreachable) {
-                               gchar *msg = g_strdup_printf (_("Server is unreachable, calendar is opened in 
read-only mode.\nError message: %s"), local_error->message);
-                               e_cal_backend_notify_error (E_CAL_BACKEND (cbdav), msg);
-                               g_free (msg);
-                               g_clear_error (&local_error);
-
-                               *know_unreachable = TRUE;
-                       } else if (caldav_was_ever_connected (cbdav)) {
-                               /* This allows to open the calendar in read-only mode, which can be done
-                                  if it was ever connected to the server. */
-                               g_clear_error (&local_error);
-                               success = TRUE;
-                       }
-               }
-       }
+       e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
 
-       if (local_error != NULL)
-               g_propagate_error (error, local_error);
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "calendar-data");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp");
+       e_xml_document_add_attribute (xml, NULL, "name", "VCALENDAR");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp");
+       if (kind == ICAL_VEVENT_COMPONENT)
+               e_xml_document_add_attribute (xml, NULL, "name", "VEVENT");
+       else if (kind == ICAL_VJOURNAL_COMPONENT)
+               e_xml_document_add_attribute (xml, NULL, "name", "VJOURNAL");
+       else if (kind == ICAL_VTODO_COMPONENT)
+               e_xml_document_add_attribute (xml, NULL, "name", "VTODO");
+       else
+               g_warn_if_reached ();
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "prop");
+       e_xml_document_add_attribute (xml, NULL, "name", "UID");
+       e_xml_document_end_element (xml); /* prop / UID */
+       e_xml_document_end_element (xml); /* comp / VEVENT|VJOURNAL|VTODO */
+       e_xml_document_end_element (xml); /* comp / VCALENDAR */
+       e_xml_document_end_element (xml); /* calendar-data */
+       e_xml_document_end_element (xml); /* prop */
+
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "filter");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp-filter");
+       e_xml_document_add_attribute (xml, NULL, "name", "VCALENDAR");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp-filter");
+       if (kind == ICAL_VEVENT_COMPONENT)
+               e_xml_document_add_attribute (xml, NULL, "name", "VEVENT");
+       else if (kind == ICAL_VJOURNAL_COMPONENT)
+               e_xml_document_add_attribute (xml, NULL, "name", "VJOURNAL");
+       else if (kind == ICAL_VTODO_COMPONENT)
+               e_xml_document_add_attribute (xml, NULL, "name", "VTODO");
+       e_xml_document_end_element (xml); /* comp-filter / VEVENT|VJOURNAL|VTODO */
+       e_xml_document_end_element (xml); /* comp-filter / VCALENDAR */
+       e_xml_document_end_element (xml); /* filter */
+
+       success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS, xml,
+               ecb_caldav_extract_existing_cb, out_existing_objects, NULL, NULL, cancellable, error);
+
+       g_object_unref (xml);
+
+       if (success)
+               *out_existing_objects = g_slist_reverse (*out_existing_objects);
 
        return success;
 }
 
-static void
-caldav_do_open (ECalBackendSync *backend,
-                EDataCal *cal,
-                GCancellable *cancellable,
-                gboolean only_if_exists,
-                GError **perror)
+static gchar *
+ecb_caldav_uid_to_uri (ECalBackendCalDAV *cbdav,
+                      const gchar *uid,
+                      const gchar *extension)
 {
-       ECalBackendCalDAV *cbdav;
        ESourceWebdav *webdav_extension;
-       ESourceAuthentication *auth_extension;
-       ESource *source;
-       gboolean online;
-
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
-
-       g_mutex_lock (&cbdav->priv->busy_lock);
-
-       source = e_backend_get_source (E_BACKEND (cbdav));
-       webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
-       auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
-       e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
-
-       /* let it decide the 'getctag' extension availability again */
-       cbdav->priv->ctag_supported = TRUE;
-
-       if (!cbdav->priv->loaded && !initialize_backend (cbdav, perror)) {
-               g_mutex_unlock (&cbdav->priv->busy_lock);
-               return;
-       }
-
-       online = e_backend_get_online (E_BACKEND (backend));
-
-       if (!cbdav->priv->do_offline && !online) {
-               g_mutex_unlock (&cbdav->priv->busy_lock);
-               g_propagate_error (perror, EDC_ERROR (RepositoryOffline));
-               return;
-       }
-
-       cbdav->priv->opened = TRUE;
-       cbdav->priv->is_google = FALSE;
-
-       if (online) {
-               gchar *certificate_pem = NULL, *auth_method;
-               GTlsCertificateFlags certificate_errors = 0;
-               GError *local_error = NULL;
-
-               auth_method = e_source_authentication_dup_method (auth_extension);
-
-               if ((g_strcmp0 (auth_method, "Google") == 0 ||
-                   !open_calendar_wrapper (cbdav, cancellable, &local_error, TRUE, NULL, &certificate_pem, 
&certificate_errors)) &&
-                   !g_cancellable_is_cancelled (cancellable)) {
-                       ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
-                       GError *local_error2 = NULL;
-
-                       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-                               reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
-                       }
+       SoupURI *soup_uri;
+       gchar *uri, *tmp, *filename;
 
-                       if (!e_backend_credentials_required_sync (E_BACKEND (backend), reason, 
certificate_pem, certificate_errors,
-                               local_error, cancellable, &local_error2)) {
-                               g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC, 
local_error2 ? local_error2->message : "Unknown error");
-                       }
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
 
-                       if (!local_error2 && (
-                           g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED) ||
-                           g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationRequired) ||
-                           g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed))) {
-                               /* These errors are treated through the authentication */
-                               g_clear_error (&local_error);
-                       } else {
-                               if (local_error)
-                                       g_propagate_error (perror, local_error);
-                               local_error = NULL;
-                       }
-                       g_clear_error (&local_error2);
-               }
+       webdav_extension = e_source_get_extension (e_backend_get_source (E_BACKEND (cbdav)), 
E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+       g_return_val_if_fail (soup_uri != NULL, NULL);
 
-               g_clear_error (&local_error);
-               g_free (certificate_pem);
-               g_free (auth_method);
+       if (extension) {
+               tmp = g_strconcat (uid, extension, NULL);
+               filename = soup_uri_encode (tmp, NULL);
+               g_free (tmp);
        } else {
-               e_cal_backend_set_writable (E_CAL_BACKEND (cbdav), FALSE);
+               filename = soup_uri_encode (uid, NULL);
        }
 
-       g_mutex_unlock (&cbdav->priv->busy_lock);
-}
+       if (soup_uri->path) {
+               gchar *slash = strrchr (soup_uri->path, '/');
 
-static void
-caldav_refresh (ECalBackendSync *backend,
-                EDataCal *cal,
-                GCancellable *cancellable,
-                GError **perror)
-{
-       ECalBackendCalDAV        *cbdav;
-       gboolean                  online;
-
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
-
-       g_mutex_lock (&cbdav->priv->busy_lock);
-
-       if (!cbdav->priv->loaded
-           || cbdav->priv->slave_cmd == SLAVE_SHOULD_DIE) {
-               g_mutex_unlock (&cbdav->priv->busy_lock);
-               return;
+               if (slash && !slash[1])
+                       *slash = '\0';
        }
 
-       if (!e_backend_get_online (E_BACKEND (backend)) &&
-           e_backend_is_destination_reachable (E_BACKEND (backend), cancellable, NULL)) {
-               e_backend_set_online (E_BACKEND (backend), TRUE);
-       }
+       soup_uri_set_user (soup_uri, NULL);
+       soup_uri_set_password (soup_uri, NULL);
 
-       if (!check_state (cbdav, &online, NULL) || !online) {
-               g_mutex_unlock (&cbdav->priv->busy_lock);
-               return;
-       }
-
-       update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK_NO_CTAG_CHECK);
-
-       /* wake it up */
-       g_cond_signal (&cbdav->priv->cond);
-       g_mutex_unlock (&cbdav->priv->busy_lock);
-}
-
-static void
-remove_comp_from_cache_cb (gpointer value,
-                           gpointer user_data)
-{
-       ECalComponent *comp = value;
-       ECalBackendStore *store = user_data;
-       ECalComponentId *id;
+       tmp = g_strconcat (soup_uri->path && *soup_uri->path ? soup_uri->path : "", "/", filename, NULL);
+       soup_uri_set_path (soup_uri, tmp);
+       g_free (tmp);
 
-       g_return_if_fail (comp != NULL);
-       g_return_if_fail (store != NULL);
+       uri = soup_uri_to_string (soup_uri, FALSE);
 
-       id = e_cal_component_get_id (comp);
-       g_return_if_fail (id != NULL);
+       soup_uri_free (soup_uri);
+       g_free (filename);
 
-       e_cal_backend_store_remove_component (store, id->uid, id->rid);
-       e_cal_component_free_id (id);
+       return uri;
 }
 
 static gboolean
-remove_comp_from_cache (ECalBackendCalDAV *cbdav,
-                        const gchar *uid,
-                        const gchar *rid)
-{
-       gboolean res = FALSE;
-
-       if (!rid || !*rid) {
-               /* get with detached instances */
-               GSList *objects = e_cal_backend_store_get_components_by_uid (cbdav->priv->store, uid);
-
-               if (objects) {
-                       g_slist_foreach (objects, (GFunc) remove_comp_from_cache_cb, cbdav->priv->store);
-                       g_slist_foreach (objects, (GFunc) g_object_unref, NULL);
-                       g_slist_free (objects);
-
-                       res = TRUE;
-               }
-       } else {
-               res = e_cal_backend_store_remove_component (cbdav->priv->store, uid, rid);
-       }
-
-       return res;
-}
-
-static void
-add_detached_recur_to_vcalendar_cb (gpointer value,
-                                    gpointer user_data)
+ecb_caldav_load_component_sync (ECalMetaBackend *meta_backend,
+                               const gchar *uid,
+                               const gchar *extra,
+                               icalcomponent **out_component,
+                               gchar **out_extra,
+                               GCancellable *cancellable,
+                               GError **error)
 {
-       icalcomponent *recurrence = e_cal_component_get_icalcomponent (value);
-       icalcomponent *vcalendar = user_data;
-
-       icalcomponent_add_component (
-               vcalendar,
-               icalcomponent_new_clone (recurrence));
-}
-
-static gint
-sort_master_first (gconstpointer a,
-                   gconstpointer b)
-{
-       icalcomponent *ca, *cb;
-
-       ca = e_cal_component_get_icalcomponent ((ECalComponent *) a);
-       cb = e_cal_component_get_icalcomponent ((ECalComponent *) b);
-
-       if (!ca) {
-               if (!cb)
-                       return 0;
-               else
-                       return -1;
-       } else if (!cb) {
-               return 1;
-       }
-
-       return icaltime_compare (icalcomponent_get_recurrenceid (ca), icalcomponent_get_recurrenceid (cb));
-}
-
-/* Returns new icalcomponent, with all detached instances stored in a cache.
- * The cache lock should be locked when called this function.
-*/
-static icalcomponent *
-get_comp_from_cache (ECalBackendCalDAV *cbdav,
-                     const gchar *uid,
-                     const gchar *rid,
-                     gchar **href,
-                     gchar **etag)
-{
-       icalcomponent *icalcomp = NULL;
-
-       if (rid == NULL || !*rid) {
-               /* get with detached instances */
-               GSList *objects = e_cal_backend_store_get_components_by_uid (cbdav->priv->store, uid);
-
-               if (!objects) {
-                       return NULL;
-               }
-
-               if (g_slist_length (objects) == 1) {
-                       ECalComponent *comp = objects->data;
+       ECalBackendCalDAV *cbdav;
+       gchar *uri = NULL, *href = NULL, *etag = NULL, *bytes = NULL;
+       gsize length = -1;
+       gboolean success = FALSE;
+       GError *local_error = NULL;
 
-                       /* will be unreffed a bit later */
-                       if (comp)
-                               icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
-               } else {
-                       /* if we have detached recurrences, return a VCALENDAR */
-                       icalcomp = e_cal_util_new_top_level ();
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_component != NULL, FALSE);
 
-                       objects = g_slist_sort (objects, sort_master_first);
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
 
-                       /* add all detached recurrences and the master object */
-                       g_slist_foreach (objects, add_detached_recur_to_vcalendar_cb, icalcomp);
-               }
+       if (extra && *extra) {
+               uri = g_strdup (extra);
 
-               /* every component has set same href and etag, thus it doesn't matter where it will be read */
-               if (href)
-                       *href = ecalcomp_get_href (objects->data);
-               if (etag)
-                       *etag = ecalcomp_get_etag (objects->data);
+               success = e_webdav_session_get_data_sync (cbdav->priv->webdav, uri, &href, &etag, &bytes, 
&length, cancellable, &local_error);
 
-               g_slist_foreach (objects, (GFunc) g_object_unref, NULL);
-               g_slist_free (objects);
-       } else {
-               /* get the exact object */
-               ECalComponent *comp = e_cal_backend_store_get_component (cbdav->priv->store, uid, rid);
-
-               if (comp) {
-                       icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
-                       if (href)
-                               *href = ecalcomp_get_href (comp);
-                       if (etag)
-                               *etag = ecalcomp_get_etag (comp);
-                       g_object_unref (comp);
+               if (!success) {
+                       g_free (uri);
+                       uri = NULL;
                }
        }
 
-       return icalcomp;
-}
-
-static void
-put_server_comp_to_cache (ECalBackendCalDAV *cbdav,
-                          icalcomponent *icomp,
-                          const gchar *href,
-                          const gchar *etag,
-                          GTree *c_uid2complist)
-{
-       icalcomponent_kind kind;
-       ECalBackend *cal_backend;
-
-       g_return_if_fail (cbdav != NULL);
-       g_return_if_fail (icomp != NULL);
-
-       cal_backend = E_CAL_BACKEND (cbdav);
-       kind = icalcomponent_isa (icomp);
-       extract_timezones (cbdav, icomp);
-
-       if (kind == ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent *subcomp;
-
-               kind = e_cal_backend_get_kind (cal_backend);
-
-               for (subcomp = icalcomponent_get_first_component (icomp, kind);
-                    subcomp;
-                    subcomp = icalcomponent_get_next_component (icomp, kind)) {
-                       ECalComponent *new_comp, *old_comp;
+       if (!success) {
+               uri = ecb_caldav_uid_to_uri (cbdav, uid, ".ics");
+               g_return_val_if_fail (uri != NULL, FALSE);
 
-                       convert_to_url_attachment (cbdav, subcomp);
-                       new_comp = e_cal_component_new ();
-                       if (e_cal_component_set_icalcomponent (new_comp, icalcomponent_new_clone (subcomp))) {
-                               const gchar *uid = NULL;
-                               struct cache_comp_list *ccl;
-
-                               e_cal_component_get_uid (new_comp, &uid);
-                               if (!uid) {
-                                       g_warning ("%s: no UID on component!", G_STRFUNC);
-                                       g_object_unref (new_comp);
-                                       continue;
-                               }
-
-                               if (href)
-                                       ecalcomp_set_href (new_comp, href);
-                               if (etag)
-                                       ecalcomp_set_etag (new_comp, etag);
-
-                               old_comp = NULL;
-                               if (c_uid2complist) {
-                                       ccl = g_tree_lookup (c_uid2complist, uid);
-                                       if (ccl) {
-                                               gchar *nc_rid = e_cal_component_get_recurid_as_string 
(new_comp);
-                                               GSList *p;
-
-                                               for (p = ccl->slist; p && !old_comp; p = p->next) {
-                                                       gchar *oc_rid;
-
-                                                       old_comp = p->data;
-
-                                                       oc_rid = e_cal_component_get_recurid_as_string 
(old_comp);
-                                                       if (g_strcmp0 (nc_rid, oc_rid) != 0) {
-                                                               old_comp = NULL;
-                                                       }
-
-                                                       g_free (oc_rid);
-                                               }
-
-                                               g_free (nc_rid);
-                                       }
-                               }
+               g_clear_error (&local_error);
 
-                               put_component_to_store (cbdav, new_comp);
+               success = e_webdav_session_get_data_sync (cbdav->priv->webdav, uri, &href, &etag, &bytes, 
&length, cancellable, &local_error);
+               if (!success && !g_cancellable_is_cancelled (cancellable) &&
+                   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+                       g_free (uri);
+                       uri = ecb_caldav_uid_to_uri (cbdav, uid, NULL);
 
-                               if (old_comp == NULL) {
-                                       e_cal_backend_notify_component_created (cal_backend, new_comp);
-                               } else {
-                                       e_cal_backend_notify_component_modified (cal_backend, old_comp, 
new_comp);
+                       if (uri) {
+                               g_clear_error (&local_error);
 
-                                       if (ccl)
-                                               ccl->slist = g_slist_remove (ccl->slist, old_comp);
-                                       g_object_unref (old_comp);
-                               }
+                               success = e_webdav_session_get_data_sync (cbdav->priv->webdav, uri, &href, 
&etag, &bytes, &length, cancellable, &local_error);
                        }
-
-                       g_object_unref (new_comp);
                }
        }
-}
 
-static gboolean
-put_comp_to_cache (ECalBackendCalDAV *cbdav,
-                   icalcomponent *icalcomp,
-                   const gchar *href,
-                   const gchar *etag)
-{
-       icalcomponent_kind my_kind;
-       ECalComponent *comp;
-       gboolean res = FALSE;
+       if (success) {
+               *out_component = NULL;
 
-       g_return_val_if_fail (cbdav != NULL, FALSE);
-       g_return_val_if_fail (icalcomp != NULL, FALSE);
+               if (href && etag && bytes && length != ((gsize) -1)) {
+                       icalcomponent *icalcomp, *subcomp;
 
-       my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
-       comp = e_cal_component_new ();
+                       icalcomp = icalcomponent_new_from_string (bytes);
+                       if (icalcomp) {
+                               e_cal_util_set_x_property (icalcomp, E_CALDAV_X_ETAG, etag);
 
-       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent *subcomp;
+                               for (subcomp = icalcomponent_get_first_component (icalcomp, 
ICAL_ANY_COMPONENT);
+                                    subcomp;
+                                    subcomp = icalcomponent_get_next_component (icalcomp, 
ICAL_ANY_COMPONENT)) {
+                                       icalcomponent_kind kind = icalcomponent_isa (subcomp);
 
-               /* remove all old components from the cache first */
-               for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
-                    subcomp;
-                    subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
-                       remove_comp_from_cache (cbdav, icalcomponent_get_uid (subcomp), NULL);
-               }
+                                       if (kind == ICAL_VEVENT_COMPONENT ||
+                                           kind == ICAL_VJOURNAL_COMPONENT ||
+                                           kind == ICAL_VTODO_COMPONENT) {
+                                               e_cal_util_set_x_property (subcomp, E_CALDAV_X_ETAG, etag);
+                                       }
+                               }
 
-               /* then put new. It's because some detached instances could be removed on the server. */
-               for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
-                    subcomp;
-                    subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
-                       /* because reusing the same comp doesn't clear recur_id member properly */
-                       g_object_unref (comp);
-                       comp = e_cal_component_new ();
-
-                       if (e_cal_component_set_icalcomponent (comp, icalcomponent_new_clone (subcomp))) {
-                               if (href)
-                                       ecalcomp_set_href (comp, href);
-                               if (etag)
-                                       ecalcomp_set_etag (comp, etag);
-
-                               if (put_component_to_store (cbdav, comp))
-                                       res = TRUE;
+                               *out_component = icalcomp;
                        }
                }
-       } else if (icalcomponent_isa (icalcomp) == my_kind) {
-               remove_comp_from_cache (cbdav, icalcomponent_get_uid (icalcomp), NULL);
-
-               if (e_cal_component_set_icalcomponent (comp, icalcomponent_new_clone (icalcomp))) {
-                       if (href)
-                               ecalcomp_set_href (comp, href);
-                       if (etag)
-                               ecalcomp_set_etag (comp, etag);
 
-                       res = put_component_to_store (cbdav, comp);
+               if (!*out_component) {
+                       success = FALSE;
+                       g_propagate_error (&local_error, EDC_ERROR (InvalidObject));
                }
        }
 
-       g_object_unref (comp);
-
-       return res;
-}
-
-static void
-remove_property (gpointer prop,
-                 gpointer icomp)
-{
-       icalcomponent_remove_property (icomp, prop);
-       icalproperty_free (prop);
-}
-
-static void
-strip_unneeded_x_props (icalcomponent *icomp)
-{
-       icalproperty *prop;
-       GSList *to_remove = NULL;
-
-       g_return_if_fail (icomp != NULL);
-       g_return_if_fail (icalcomponent_isa (icomp) != ICAL_VCALENDAR_COMPONENT);
-
-       for (prop = icalcomponent_get_first_property (icomp, ICAL_X_PROPERTY);
-            prop;
-            prop = icalcomponent_get_next_property (icomp, ICAL_X_PROPERTY)) {
-               if (g_str_has_prefix (icalproperty_get_x_name (prop), X_E_CALDAV)) {
-                       to_remove = g_slist_prepend (to_remove, prop);
-               }
-       }
+       g_free (uri);
+       g_free (href);
+       g_free (etag);
+       g_free (bytes);
 
-       for (prop = icalcomponent_get_first_property (icomp, ICAL_XLICERROR_PROPERTY);
-            prop;
-            prop = icalcomponent_get_next_property (icomp, ICAL_XLICERROR_PROPERTY)) {
-               to_remove = g_slist_prepend (to_remove, prop);
-       }
+       if (local_error)
+               g_propagate_error (error, local_error);
 
-       g_slist_foreach (to_remove, remove_property, icomp);
-       g_slist_free (to_remove);
+       return success;
 }
 
 static gboolean
-is_stored_on_server (ECalBackendCalDAV *cbdav,
-                     const gchar *uri)
+ecb_caldav_save_component_sync (ECalMetaBackend *meta_backend,
+                               gboolean overwrite_existing,
+                               EConflictResolution conflict_resolution,
+                               const GSList *instances,
+                               const gchar *extra,
+                               gchar **out_new_uid,
+                               gchar **out_new_extra,
+                               GCancellable *cancellable,
+                               GError **error)
 {
-       SoupURI *my_uri, *test_uri;
-       gboolean res;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-       g_return_val_if_fail (cbdav->priv->uri != NULL, FALSE);
-       g_return_val_if_fail (uri != NULL, FALSE);
-
-       my_uri = soup_uri_new (cbdav->priv->uri);
-       g_return_val_if_fail (my_uri != NULL, FALSE);
-
-       test_uri = soup_uri_new (uri);
-       if (!test_uri) {
-               soup_uri_free (my_uri);
-               return FALSE;
-       }
-
-       res = my_uri->host && test_uri->host && g_ascii_strcasecmp (my_uri->host, test_uri->host) == 0;
-
-       soup_uri_free (my_uri);
-       soup_uri_free (test_uri);
-
-       return res;
-}
+       ECalBackendCalDAV *cbdav;
+       icalcomponent *vcalendar, *subcomp;
+       gchar *href = NULL, *etag = NULL, *uid = NULL;
+       gchar *ical_string = NULL;
+       gboolean success;
 
-static void
-convert_to_inline_attachment (ECalBackendCalDAV *cbdav,
-                              icalcomponent *icalcomp)
-{
-       icalcomponent *cclone;
-       icalproperty *p;
-       GSList *to_remove = NULL;
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+       g_return_val_if_fail (instances != NULL, FALSE);
+       g_return_val_if_fail (out_new_uid, FALSE);
+       g_return_val_if_fail (out_new_extra, FALSE);
 
-       g_return_if_fail (icalcomp != NULL);
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
 
-       cclone = icalcomponent_new_clone (icalcomp);
+       vcalendar = e_cal_meta_backend_merge_instances (meta_backend, instances, cbdav->priv->is_icloud);
+       g_return_val_if_fail (vcalendar != NULL, FALSE);
 
-       /* Remove local url attachments first */
-       for (p = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
-            p;
-            p = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY)) {
-               icalattach *attach;
+       for (subcomp = icalcomponent_get_first_component (vcalendar, ICAL_ANY_COMPONENT);
+            subcomp;
+            subcomp = icalcomponent_get_next_component (vcalendar, ICAL_ANY_COMPONENT)) {
+               icalcomponent_kind kind = icalcomponent_isa (subcomp);
 
-               attach = icalproperty_get_attach ((const icalproperty *) p);
-               if (icalattach_get_is_url (attach)) {
-                       const gchar *url;
+               if (kind == ICAL_VEVENT_COMPONENT ||
+                   kind == ICAL_VJOURNAL_COMPONENT ||
+                   kind == ICAL_VTODO_COMPONENT) {
+                       if (!etag)
+                               etag = e_cal_util_dup_x_property (subcomp, E_CALDAV_X_ETAG);
+                       if (!uid)
+                               uid = g_strdup (icalcomponent_get_uid (subcomp));
 
-                       url = icalattach_get_url (attach);
-                       if (g_str_has_prefix (url, LOCAL_PREFIX))
-                               to_remove = g_slist_prepend (to_remove, p);
+                       e_cal_util_remove_x_property (subcomp, E_CALDAV_X_ETAG);
                }
        }
-       g_slist_foreach (to_remove, remove_property, icalcomp);
-       g_slist_free (to_remove);
-
-       /* convert local url attachments to inline attachments now */
-       for (p = icalcomponent_get_first_property (cclone, ICAL_ATTACH_PROPERTY);
-            p;
-            p = icalcomponent_get_next_property (cclone, ICAL_ATTACH_PROPERTY)) {
-               icalattach *attach;
-               GFile *file;
-               GError *error = NULL;
-               const gchar *uri;
-               gchar *basename;
-               gchar *content;
-               gsize len;
-
-               attach = icalproperty_get_attach ((const icalproperty *) p);
-               if (!icalattach_get_is_url (attach))
-                       continue;
-
-               uri = icalattach_get_url (attach);
-               if (!g_str_has_prefix (uri, LOCAL_PREFIX))
-                       continue;
-
-               file = g_file_new_for_uri (uri);
-               basename = g_file_get_basename (file);
-               if (g_file_load_contents (file, NULL, &content, &len, NULL, &error)) {
-                       icalproperty *prop;
-                       icalparameter *param;
-                       gchar *encoded;
-
-                       /*
-                        * do a base64 encoding so it can
-                        * be embedded in a soap message
-                        */
-                       encoded = g_base64_encode ((guchar *) content, len);
-                       attach = icalattach_new_from_data (encoded, NULL, NULL);
-                       g_free (content);
-                       g_free (encoded);
-
-                       prop = icalproperty_new_attach (attach);
-                       icalattach_unref (attach);
-
-                       param = icalparameter_new_value (ICAL_VALUE_BINARY);
-                       icalproperty_add_parameter (prop, param);
 
-                       param = icalparameter_new_encoding (ICAL_ENCODING_BASE64);
-                       icalproperty_add_parameter (prop, param);
+       ical_string = icalcomponent_as_ical_string_r (vcalendar);
+       icalcomponent_free (vcalendar);
 
-                       param = icalparameter_new_x (basename);
-                       icalparameter_set_xname (param, X_E_CALDAV_ATTACHMENT_NAME);
-                       icalproperty_add_parameter (prop, param);
+       if (uid && ical_string && (!overwrite_existing || (extra && *extra))) {
+               gboolean force_write = FALSE;
 
-                       icalcomponent_add_property (icalcomp, prop);
-               } else {
-                       g_warning ("%s\n", error->message);
-                       g_clear_error (&error);
-               }
-               g_free (basename);
-               g_object_unref (file);
-       }
-       icalcomponent_free (cclone);
-}
+               if (!extra || !*extra)
+                       href = ecb_caldav_uid_to_uri (cbdav, uid, ".ics");
 
-static void
-convert_to_url_attachment (ECalBackendCalDAV *cbdav,
-                           icalcomponent *icalcomp)
-{
-       ECalBackend *backend;
-       GSList *to_remove = NULL, *to_remove_after_download = NULL;
-       icalcomponent *cclone;
-       icalproperty *p;
-       gint fileindex;
-
-       g_return_if_fail (cbdav != NULL);
-       g_return_if_fail (icalcomp != NULL);
-
-       backend = E_CAL_BACKEND (cbdav);
-       cclone = icalcomponent_new_clone (icalcomp);
-
-       /* Remove all inline attachments first */
-       for (p = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
-            p;
-            p = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY)) {
-               icalattach *attach;
-
-               attach = icalproperty_get_attach ((const icalproperty *) p);
-               if (!icalattach_get_is_url (attach))
-                       to_remove = g_slist_prepend (to_remove, p);
-               else if (is_stored_on_server (cbdav, icalattach_get_url (attach)))
-                       to_remove_after_download = g_slist_prepend (to_remove_after_download, p);
-       }
-       g_slist_foreach (to_remove, remove_property, icalcomp);
-       g_slist_free (to_remove);
-
-       /* convert inline attachments to url attachments now */
-       for (p = icalcomponent_get_first_property (cclone, ICAL_ATTACH_PROPERTY), fileindex = 0;
-            p;
-            p = icalcomponent_get_next_property (cclone, ICAL_ATTACH_PROPERTY), fileindex++) {
-               icalattach *attach;
-               gsize len = -1;
-               gchar *decoded = NULL;
-               gchar *basename, *local_filename;
-
-               attach = icalproperty_get_attach ((const icalproperty *) p);
-               if (icalattach_get_is_url (attach)) {
-                       const gchar *attach_url = icalattach_get_url (attach);
-                       GError *error = NULL;
-
-                       if (!is_stored_on_server (cbdav, attach_url))
-                               continue;
-
-                       if (!caldav_server_download_attachment (cbdav, attach_url, &decoded, &len, &error)) {
-                               if (caldav_debug_show (DEBUG_ATTACHMENTS))
-                                       g_print ("CalDAV::%s: Failed to download from a server: %s\n", 
G_STRFUNC, error ? error->message : "Unknown error");
-                               continue;
+               if (overwrite_existing) {
+                       switch (conflict_resolution) {
+                       case E_CONFLICT_RESOLUTION_FAIL:
+                       case E_CONFLICT_RESOLUTION_USE_NEWER:
+                       case E_CONFLICT_RESOLUTION_KEEP_SERVER:
+                       case E_CONFLICT_RESOLUTION_WRITE_COPY:
+                               break;
+                       case E_CONFLICT_RESOLUTION_KEEP_LOCAL:
+                               force_write = TRUE;
+                               break;
                        }
                }
 
-               basename = icalproperty_get_parameter_as_string_r (p, X_E_CALDAV_ATTACHMENT_NAME);
-               local_filename = e_cal_backend_create_cache_filename (backend, icalcomponent_get_uid 
(icalcomp), basename, fileindex);
-               g_free (basename);
-
-               if (local_filename) {
-                       GError *error = NULL;
+               success = e_webdav_session_put_data_sync (cbdav->priv->webdav, (extra && *extra) ? extra : 
href,
+                       force_write ? "" : overwrite_existing ? etag : NULL, E_WEBDAV_CONTENT_TYPE_CALENDAR,
+                       ical_string, -1, out_new_extra, NULL, cancellable, error);
 
-                       if (decoded == NULL) {
-                               gchar *content;
-
-                               content = (gchar *) icalattach_get_data (attach);
-                               decoded = (gchar *) g_base64_decode (content, &len);
-                       }
-
-                       if (g_file_set_contents (local_filename, decoded, len, &error)) {
-                               icalproperty *prop;
-                               gchar *url;
-
-                               url = g_filename_to_uri (local_filename, NULL, NULL);
-                               attach = icalattach_new_from_url (url);
-                               prop = icalproperty_new_attach (attach);
-                               icalattach_unref (attach);
-                               icalcomponent_add_property (icalcomp, prop);
-                               g_free (url);
-                       } else {
-                               g_warning ("%s\n", error->message);
-                               g_clear_error (&error);
-                       }
-
-                       g_free (local_filename);
-               }
+               /* To read the component back, because server can change it */
+               if (success)
+                       *out_new_uid = g_strdup (uid);
+       } else {
+               success = FALSE;
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
        }
 
-       icalcomponent_free (cclone);
+       g_free (ical_string);
+       g_free (href);
+       g_free (etag);
+       g_free (uid);
 
-       g_slist_foreach (to_remove_after_download, remove_property, icalcomp);
-       g_slist_free (to_remove_after_download);
+       return success;
 }
 
-static void
-remove_files (const gchar *dir,
-              const gchar *fileprefix)
+static gboolean
+ecb_caldav_remove_component_sync (ECalMetaBackend *meta_backend,
+                                 EConflictResolution conflict_resolution,
+                                 const gchar *uid,
+                                 const gchar *extra,
+                                 const gchar *object,
+                                 GCancellable *cancellable,
+                                 GError **error)
 {
-       GDir *d;
-
-       g_return_if_fail (dir != NULL);
-       g_return_if_fail (fileprefix != NULL);
-       g_return_if_fail (*fileprefix != '\0');
+       ECalBackendCalDAV *cbdav;
+       icalcomponent *icalcomp;
+       gchar *etag = NULL;
+       gboolean success;
+       GError *local_error = NULL;
 
-       d = g_dir_open (dir, 0, NULL);
-       if (d) {
-               const gchar *entry;
-               gint len = strlen (fileprefix);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
 
-               while ((entry = g_dir_read_name (d)) != NULL) {
-                       if (entry && strncmp (entry, fileprefix, len) == 0) {
-                               gchar *path;
+       cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
 
-                               path = g_build_filename (dir, entry, NULL);
-                               if (!g_file_test (path, G_FILE_TEST_IS_DIR))
-                                       g_unlink (path);
-                               g_free (path);
-                       }
-               }
-               g_dir_close (d);
+       if (!extra || !*extra) {
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               return FALSE;
        }
-}
 
-static void
-remove_cached_attachment (ECalBackendCalDAV *cbdav,
-                          const gchar *uid)
-{
-       GSList *l;
-       guint len;
-       gchar *dir;
-       gchar *fileprefix;
-
-       g_return_if_fail (cbdav != NULL);
-       g_return_if_fail (uid != NULL);
-
-       l = e_cal_backend_store_get_components_by_uid (cbdav->priv->store, uid);
-       len = g_slist_length (l);
-       g_slist_foreach (l, (GFunc) g_object_unref, NULL);
-       g_slist_free (l);
-       if (len > 0)
-               return;
-
-       dir = e_cal_backend_create_cache_filename (E_CAL_BACKEND (cbdav), uid, "a", 0);
-       if (!dir)
-               return;
-
-       fileprefix = g_strrstr (dir, G_DIR_SEPARATOR_S);
-       if (fileprefix) {
-               *fileprefix = '\0';
-               fileprefix++;
-
-               if (*fileprefix)
-                       fileprefix[strlen (fileprefix) - 1] = '\0';
-
-               remove_files (dir, fileprefix);
+       icalcomp = icalcomponent_new_from_string (object);
+       if (!icalcomp) {
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               return FALSE;
        }
 
-       g_free (dir);
-}
-
-/* callback for icalcomponent_foreach_tzid */
-typedef struct {
-       ECalBackendStore *store;
-       icalcomponent *vcal_comp;
-       icalcomponent *icalcomp;
-} ForeachTzidData;
+       if (conflict_resolution == E_CONFLICT_RESOLUTION_FAIL)
+               etag = e_cal_util_dup_x_property (icalcomp, E_CALDAV_X_ETAG);
 
-static void
-add_timezone_cb (icalparameter *param,
-                 gpointer data)
-{
-       icaltimezone *tz;
-       const gchar *tzid;
-       icalcomponent *vtz_comp;
-       ForeachTzidData *f_data = (ForeachTzidData *) data;
-       ETimezoneCache *cache;
-
-       tzid = icalparameter_get_tzid (param);
-       if (!tzid)
-               return;
+       success = e_webdav_session_delete_sync (cbdav->priv->webdav, extra,
+               NULL, etag, cancellable, &local_error);
 
-       tz = icalcomponent_get_timezone (f_data->vcal_comp, tzid);
-       if (tz)
-               return;
-
-       cache = e_cal_backend_store_ref_timezone_cache (f_data->store);
-
-       tz = icalcomponent_get_timezone (f_data->icalcomp, tzid);
-       if (tz == NULL)
-               tz = icaltimezone_get_builtin_timezone_from_tzid (tzid);
-       if (tz == NULL)
-               tz = e_timezone_cache_get_timezone (cache, tzid);
-
-       vtz_comp = icaltimezone_get_component (tz);
-
-       if (tz != NULL && vtz_comp != NULL)
-               icalcomponent_add_component (
-                       f_data->vcal_comp,
-                       icalcomponent_new_clone (vtz_comp));
-
-       g_object_unref (cache);
-}
-
-static void
-add_timezones_from_component (ECalBackendCalDAV *cbdav,
-                              icalcomponent *vcal_comp,
-                              icalcomponent *icalcomp)
-{
-       ForeachTzidData f_data;
-
-       g_return_if_fail (cbdav != NULL);
-       g_return_if_fail (vcal_comp != NULL);
-       g_return_if_fail (icalcomp != NULL);
-
-       f_data.store = cbdav->priv->store;
-       f_data.vcal_comp = vcal_comp;
-       f_data.icalcomp = icalcomp;
-
-       icalcomponent_foreach_tzid (icalcomp, add_timezone_cb, &f_data);
-}
-
-/* also removes X-EVOLUTION-CALDAV from all the components */
-static gchar *
-pack_cobj (ECalBackendCalDAV *cbdav,
-           icalcomponent *icomp)
-{
-       icalcomponent *calcomp;
-       gchar          *objstr;
-
-       if (icalcomponent_isa (icomp) != ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent *cclone;
+       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+               gchar *href;
 
-               calcomp = e_cal_util_new_top_level ();
+               href = ecb_caldav_uid_to_uri (cbdav, uid, ".ics");
+               if (href) {
+                       g_clear_error (&local_error);
+                       success = e_webdav_session_delete_sync (cbdav->priv->webdav, href,
+                               NULL, etag, cancellable, &local_error);
 
-               cclone = icalcomponent_new_clone (icomp);
-               strip_unneeded_x_props (cclone);
-               convert_to_inline_attachment (cbdav, cclone);
-               icalcomponent_add_component (calcomp, cclone);
-               add_timezones_from_component (cbdav, calcomp, cclone);
-       } else {
-               icalcomponent *subcomp;
-               icalcomponent_kind my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
-
-               calcomp = icalcomponent_new_clone (icomp);
-               for (subcomp = icalcomponent_get_first_component (calcomp, my_kind);
-                    subcomp;
-                    subcomp = icalcomponent_get_next_component (calcomp, my_kind)) {
-                       strip_unneeded_x_props (subcomp);
-                       convert_to_inline_attachment (cbdav, subcomp);
-                       add_timezones_from_component (cbdav, calcomp, subcomp);
+                       g_free (href);
                }
-       }
-
-       objstr = icalcomponent_as_ical_string_r (calcomp);
-       icalcomponent_free (calcomp);
-
-       g_return_val_if_fail (objstr != NULL, NULL);
 
-       return objstr;
-
-}
+               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+                       href = ecb_caldav_uid_to_uri (cbdav, uid, NULL);
+                       if (href) {
+                               g_clear_error (&local_error);
+                               success = e_webdav_session_delete_sync (cbdav->priv->webdav, href,
+                                       NULL, etag, cancellable, &local_error);
 
-static void
-maybe_correct_tzid (ECalBackendCalDAV *cbdav,
-                   ECalComponentDateTime *dt)
-{
-       icaltimezone *zone;
-
-       zone = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cbdav), dt->tzid);
-       if (!zone) {
-               g_free ((gchar *) dt->tzid);
-               dt->tzid = g_strdup ("UTC");
-       } else if (cbdav->priv->is_icloud && !dt->value->is_date) {
-               const gchar *location = icaltimezone_get_location (zone);
-
-               if (location && *location) {
-                       g_free ((gchar *) dt->tzid);
-                       dt->tzid = g_strdup (location);
-               } else {
-                       /* No location available for this timezone, convert to UTC */
-                       dt->value->zone = zone;
-                       *dt->value = icaltime_convert_to_zone (*dt->value, icaltimezone_get_utc_timezone ());
-                       g_free ((gchar *) dt->tzid);
-                       dt->tzid = g_strdup ("UTC");
+                               g_free (href);
+                       }
                }
        }
-}
 
-static void
-sanitize_component (ECalBackendCalDAV *cbdav,
-                    ECalComponent *comp)
-{
-       ECalComponentDateTime dt;
-
-       /* Check dtstart, dtend and due's timezone, and convert it to local
-        * default timezone if the timezone is not in our builtin timezone
-        * list */
-       e_cal_component_get_dtstart (comp, &dt);
-       if (dt.value && dt.tzid) {
-               maybe_correct_tzid (cbdav, &dt);
-               e_cal_component_set_dtstart (comp, &dt);
-       }
-       e_cal_component_free_datetime (&dt);
-
-       e_cal_component_get_dtend (comp, &dt);
-       if (dt.value && dt.tzid) {
-               maybe_correct_tzid (cbdav, &dt);
-               e_cal_component_set_dtend (comp, &dt);
-       }
-       e_cal_component_free_datetime (&dt);
+       icalcomponent_free (icalcomp);
+       g_free (etag);
 
-       e_cal_component_get_due (comp, &dt);
-       if (dt.value && dt.tzid) {
-               maybe_correct_tzid (cbdav, &dt);
-               e_cal_component_set_due (comp, &dt);
-       }
-       e_cal_component_free_datetime (&dt);
+       if (local_error)
+               g_propagate_error (error, local_error);
 
-       e_cal_component_abort_sequence (comp);
+       return success;
 }
 
 static gboolean
-cache_contains (ECalBackendCalDAV *cbdav,
-                const gchar *uid,
-                const gchar *rid)
+ecb_caldav_propfind_get_owner_cb (EWebDAVSession *webdav,
+                                 xmlXPathContextPtr xpath_ctx,
+                                 const gchar *xpath_prop_prefix,
+                                 const SoupURI *request_uri,
+                                 const gchar *href,
+                                 guint status_code,
+                                 gpointer user_data)
 {
-       gboolean res;
-       ECalComponent *comp;
-
-       g_return_val_if_fail (cbdav != NULL, FALSE);
-       g_return_val_if_fail (uid != NULL, FALSE);
-
-       g_return_val_if_fail (cbdav->priv->store != NULL, FALSE);
+       gchar **out_owner_href = user_data;
 
-       comp = e_cal_backend_store_get_component (cbdav->priv->store, uid, rid);
-       res = comp != NULL;
+       g_return_val_if_fail (out_owner_href != NULL, FALSE);
 
-       if (comp)
-               g_object_unref (comp);
-
-       return res;
-}
-
-/* Returns subcomponent of icalcomp, which is a master object, or icalcomp itself, if it's not a VCALENDAR;
- * Do not free returned pointer, it'll be freed together with the icalcomp.
-*/
-static icalcomponent *
-get_master_comp (ECalBackendCalDAV *cbdav,
-                 icalcomponent *icalcomp)
-{
-       icalcomponent *master = icalcomp;
-
-       g_return_val_if_fail (cbdav != NULL, NULL);
-       g_return_val_if_fail (icalcomp != NULL, NULL);
+       if (xpath_prop_prefix &&
+           status_code == SOUP_STATUS_OK) {
+               gchar *tmp = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:owner/D:href", xpath_prop_prefix);
 
-       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent *subcomp;
-               icalcomponent_kind my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
+               if (tmp && *tmp)
+                       *out_owner_href = e_webdav_session_ensure_full_uri (webdav, request_uri, tmp);
 
-               master = NULL;
-
-               for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
-                    subcomp;
-                    subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
-                       struct icaltimetype sub_rid = icalcomponent_get_recurrenceid (subcomp);
+               g_free (tmp);
 
-                       if (icaltime_is_null_time (sub_rid)) {
-                               master = subcomp;
-                               break;
-                       }
-               }
+               return FALSE;
        }
 
-       return master;
+       return TRUE;
 }
 
 static gboolean
-remove_instance (ECalBackendCalDAV *cbdav,
-                 icalcomponent *icalcomp,
-                 struct icaltimetype rid,
-                 ECalObjModType mod,
-                 gboolean also_exdate)
-{
-       icalcomponent *master = icalcomp;
-       struct icaltimetype master_dtstart;
-       gboolean res = FALSE;
-
-       g_return_val_if_fail (icalcomp != NULL, res);
-       g_return_val_if_fail (!icaltime_is_null_time (rid), res);
-
-       master_dtstart = icalcomponent_get_dtstart (master);
-       if (master_dtstart.zone && master_dtstart.zone != rid.zone)
-               rid = icaltime_convert_to_zone (rid, (icaltimezone *) master_dtstart.zone);
-
-       rid = icaltime_convert_to_zone (rid, icaltimezone_get_utc_timezone ());
-
-       /* remove an instance only */
-       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent *subcomp;
-               icalcomponent_kind my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
-               gint left = 0;
-               gboolean start_first = FALSE;
-
-               master = NULL;
-
-               /* remove old instance first */
-               for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
-                    subcomp;
-                    subcomp = start_first ? icalcomponent_get_first_component (icalcomp, my_kind) : 
icalcomponent_get_next_component (icalcomp, my_kind)) {
-                       struct icaltimetype sub_rid = icalcomponent_get_recurrenceid (subcomp);
-
-                       start_first = FALSE;
-
-                       if (icaltime_is_null_time (sub_rid)) {
-                               master = subcomp;
-                               left++;
-                       } else if (icaltime_compare (sub_rid, rid) == 0) {
-                               icalcomponent_remove_component (icalcomp, subcomp);
-                               icalcomponent_free (subcomp);
-                               if (master) {
-                                       break;
-                               } else {
-                                       /* either no master or master not as the first component, thus rescan 
*/
-                                       left = 0;
-                                       start_first = TRUE;
-                               }
-                       } else {
-                               left++;
-                       }
-               }
-
-               /* whether left at least one instance or a master object */
-               res = left > 0;
-       } else {
-               res = TRUE;
-       }
-
-       if (master && also_exdate) {
-               e_cal_util_remove_instances (master, rid, mod);
-       }
-
-       return res;
-}
-
-static icalcomponent *
-replace_master (ECalBackendCalDAV *cbdav,
-                icalcomponent *old_comp,
-                icalcomponent *new_master)
-{
-       icalcomponent *old_master;
-       if (icalcomponent_isa (old_comp) != ICAL_VCALENDAR_COMPONENT) {
-               icalcomponent_free (old_comp);
-               return new_master;
-       }
-
-       old_master = get_master_comp (cbdav, old_comp);
-       if (!old_master) {
-               /* no master, strange */
-               icalcomponent_free (new_master);
-       } else {
-               icalcomponent_remove_component (old_comp, old_master);
-               icalcomponent_free (old_master);
-
-               icalcomponent_add_component (old_comp, new_master);
-       }
-
-       return old_comp;
-}
-
-/* the resulting component should be unreffed when done with it;
- * the fallback_comp is cloned, if used */
-static ECalComponent *
-get_ecalcomp_master_from_cache_or_fallback (ECalBackendCalDAV *cbdav,
-                                            const gchar *uid,
-                                            const gchar *rid,
-                                            ECalComponent *fallback_comp)
+ecb_caldav_propfind_get_schedule_outbox_url_cb (EWebDAVSession *webdav,
+                                               xmlXPathContextPtr xpath_ctx,
+                                               const gchar *xpath_prop_prefix,
+                                               const SoupURI *request_uri,
+                                               const gchar *href,
+                                               guint status_code,
+                                               gpointer user_data)
 {
-       ECalComponent *comp = NULL;
-       icalcomponent *icalcomp;
-
-       g_return_val_if_fail (cbdav != NULL, NULL);
-       g_return_val_if_fail (uid != NULL, NULL);
-
-       icalcomp = get_comp_from_cache (cbdav, uid, rid, NULL, NULL);
-       if (icalcomp) {
-               icalcomponent *master = get_master_comp (cbdav, icalcomp);
+       gchar **out_schedule_outbox_url = user_data;
 
-               if (master) {
-                       comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master));
-               }
+       g_return_val_if_fail (out_schedule_outbox_url != NULL, FALSE);
 
-               icalcomponent_free (icalcomp);
-       }
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               gchar *tmp = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:schedule-outbox-URL/D:href", 
xpath_prop_prefix);
 
-       if (!comp && fallback_comp)
-               comp = e_cal_component_clone (fallback_comp);
+               if (tmp && *tmp)
+                       *out_schedule_outbox_url = e_webdav_session_ensure_full_uri (webdav, request_uri, 
tmp);
 
-       return comp;
-}
-
-/* a busy_lock is supposed to be locked already, when calling this function */
-static void
-do_create_objects (ECalBackendCalDAV *cbdav,
-                   const GSList *in_calobjs,
-                   GSList **uids,
-                   GSList **new_components,
-                   GCancellable *cancellable,
-                   GError **perror)
-{
-       ECalComponent            *comp;
-       gboolean                  online, did_put = FALSE;
-       struct icaltimetype current;
-       icalcomponent *icalcomp;
-       const gchar *in_calobj = in_calobjs->data;
-       const gchar *comp_uid;
-
-       if (!check_state (cbdav, &online, perror))
-               return;
-
-       /* We make the assumption that the in_calobjs list we're passed is always exactly one element long, 
since we haven't specified "bulk-adds"
-        * in our static capability list. This simplifies a lot of the logic, especially around asynchronous 
results. */
-       if (in_calobjs->next != NULL) {
-               g_propagate_error (perror, e_data_cal_create_error (UnsupportedMethod, _("CalDAV does not 
support bulk additions")));
-               return;
-       }
-
-       comp = e_cal_component_new_from_string (in_calobj);
-
-       if (comp == NULL) {
-               g_propagate_error (perror, EDC_ERROR (InvalidObject));
-               return;
-       }
-
-       icalcomp = e_cal_component_get_icalcomponent (comp);
-       if (icalcomp == NULL) {
-               g_object_unref (comp);
-               g_propagate_error (perror, EDC_ERROR (InvalidObject));
-               return;
-       }
-
-       comp_uid = icalcomponent_get_uid (icalcomp);
-       if (!comp_uid) {
-               gchar *new_uid;
-
-               new_uid = e_cal_component_gen_uid ();
-               if (!new_uid) {
-                       g_object_unref (comp);
-                       g_propagate_error (perror, EDC_ERROR (InvalidObject));
-                       return;
-               }
-
-               icalcomponent_set_uid (icalcomp, new_uid);
-               comp_uid = icalcomponent_get_uid (icalcomp);
-
-               g_free (new_uid);
-       }
-
-       /* check the object is not in our cache */
-       if (cache_contains (cbdav, comp_uid, NULL)) {
-               g_object_unref (comp);
-               g_propagate_error (perror, EDC_ERROR (ObjectIdAlreadyExists));
-               return;
-       }
-
-       /* Set the created and last modified times on the component */
-       current = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
-       e_cal_component_set_created (comp, &current);
-       e_cal_component_set_last_modified (comp, &current);
-
-       /* sanitize the component*/
-       sanitize_component (cbdav, comp);
-
-       if (online) {
-               CalDAVObject object;
-
-               object.href = ecalcomp_gen_href (comp);
-               object.etag = NULL;
-               object.cdata = pack_cobj (cbdav, icalcomp);
-
-               did_put = caldav_server_put_object (cbdav, &object, icalcomp, cancellable, perror);
-
-               caldav_object_free (&object, FALSE);
-       } else {
-               /* mark component as out of synch */
-               /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_CREATED); */
-       }
-
-       if (did_put) {
-               if (uids)
-                       *uids = g_slist_prepend (*uids, g_strdup (comp_uid));
+               g_free (tmp);
 
-               if (new_components)
-                       *new_components = g_slist_prepend(*new_components, 
get_ecalcomp_master_from_cache_or_fallback (cbdav, comp_uid, NULL, comp));
+               return FALSE;
        }
 
-       g_object_unref (comp);
+       return TRUE;
 }
 
-/* a busy_lock is supposed to be locked already, when calling this function */
-static void
-do_modify_objects (ECalBackendCalDAV *cbdav,
-                   const GSList *calobjs,
-                   ECalObjModType mod,
-                   GSList **old_components,
-                   GSList **new_components,
-                   GCancellable *cancellable,
-                   GError **error)
+static gboolean
+ecb_caldav_receive_schedule_outbox_url_sync (ECalBackendCalDAV *cbdav,
+                                            GCancellable *cancellable,
+                                            GError **error)
 {
-       ECalComponent            *comp;
-       icalcomponent            *cache_comp, *master_comp;
-       gboolean                  online, did_put = FALSE, success = TRUE;
-       ECalComponentId          *id;
-       struct icaltimetype current;
-       gchar *href = NULL, *etag = NULL;
-       const gchar *calobj = calobjs->data;
-
-       if (new_components)
-               *new_components = NULL;
-
-       if (!check_state (cbdav, &online, error))
-               return;
-
-       /* We make the assumption that the calobjs list we're passed is always exactly one element long, 
since we haven't specified "bulk-modifies"
-        * in our static capability list. This simplifies a lot of the logic, especially around asynchronous 
results. */
-       if (calobjs->next != NULL) {
-               g_propagate_error (error, e_data_cal_create_error (UnsupportedMethod, _("CalDAV does not 
support bulk modifications")));
-               return;
-       }
-
-       comp = e_cal_component_new_from_string (calobj);
-
-       if (comp == NULL) {
-               g_propagate_error (error, EDC_ERROR (InvalidObject));
-               return;
-       }
-
-       if (!e_cal_component_get_icalcomponent (comp) ||
-           icalcomponent_isa (e_cal_component_get_icalcomponent (comp)) != e_cal_backend_get_kind 
(E_CAL_BACKEND (cbdav))) {
-               g_object_unref (comp);
-               g_propagate_error (error, EDC_ERROR (InvalidObject));
-               return;
-       }
+       EXmlDocument *xml;
+       gchar *owner_href = NULL, *schedule_outbox_url = NULL;
+       gboolean success;
 
-       /* Set the last modified time on the component */
-       current = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
-       e_cal_component_set_last_modified (comp, &current);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
+       g_return_val_if_fail (cbdav->priv->schedule_outbox_url == NULL, TRUE);
 
-       /* sanitize the component */
-       sanitize_component (cbdav, comp);
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       id = e_cal_component_get_id (comp);
-       if (id == NULL) {
-               g_set_error_literal (
-                       error, E_CAL_CLIENT_ERROR,
-                       E_CAL_CLIENT_ERROR_INVALID_OBJECT,
-                       e_cal_client_error_to_string (
-                       E_CAL_CLIENT_ERROR_INVALID_OBJECT));
-               return;
-       }
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_add_empty_element (xml, NULL, "owner");
+       e_xml_document_end_element (xml); /* prop */
 
-       /* fetch full component from cache, it will be pushed to the server */
-       cache_comp = get_comp_from_cache (cbdav, id->uid, NULL, &href, &etag);
+       success = e_webdav_session_propfind_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS, xml,
+               ecb_caldav_propfind_get_owner_cb, &owner_href, cancellable, error);
 
-       if (cache_comp == NULL) {
-               e_cal_component_free_id (id);
-               g_object_unref (comp);
-               g_free (href);
-               g_free (etag);
-               g_propagate_error (error, EDC_ERROR (ObjectNotFound));
-               return;
-       }
+       g_object_unref (xml);
 
-       if (!online) {
-               /* mark component as out of synch */
-               /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_MODIFIED);*/
+       if (!success || !owner_href || !*owner_href) {
+               g_free (owner_href);
+               return FALSE;
        }
 
-       if (old_components) {
-               *old_components = NULL;
-
-               if (e_cal_component_is_instance (comp)) {
-                       /* set detached instance as the old object, if any */
-                       ECalComponent *old_instance = e_cal_backend_store_get_component (cbdav->priv->store, 
id->uid, id->rid);
-
-                       /* This will give a reference to 'old_component' */
-                       if (old_instance) {
-                               *old_components = g_slist_prepend (*old_components, e_cal_component_clone 
(old_instance));
-                               g_object_unref (old_instance);
-                       }
-               }
-
-               if (!*old_components) {
-                       icalcomponent *master = get_master_comp (cbdav, cache_comp);
-
-                       if (master) {
-                               /* set full component as the old object */
-                               *old_components = g_slist_prepend (*old_components, 
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
-                       }
-               }
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       if (!xml) {
+               g_warn_if_fail (xml != NULL);
+               g_free (owner_href);
+               return FALSE;
        }
 
-       switch (mod) {
-       case E_CAL_OBJ_MOD_ONLY_THIS:
-       case E_CAL_OBJ_MOD_THIS:
-               if (e_cal_component_is_instance (comp)) {
-                       icalcomponent *new_comp = e_cal_component_get_icalcomponent (comp);
-
-                       /* new object is only this instance */
-                       if (new_components)
-                               *new_components = g_slist_prepend (*new_components, e_cal_component_clone 
(comp));
-
-                       /* add the detached instance */
-                       if (icalcomponent_isa (cache_comp) == ICAL_VCALENDAR_COMPONENT) {
-                               /* do not modify the EXDATE, as the component will be put back */
-                               remove_instance (cbdav, cache_comp, icalcomponent_get_recurrenceid 
(new_comp), mod, FALSE);
-                       } else {
-                               /* this is only a master object, thus make is a VCALENDAR component */
-                               icalcomponent *icomp;
-
-                               icomp = e_cal_util_new_top_level ();
-                               icalcomponent_add_component (icomp, cache_comp);
+       e_xml_document_add_namespaces (xml, "C", E_WEBDAV_NS_CALDAV, NULL);
 
-                               /* no need to free the cache_comp, as it is inside icomp */
-                               cache_comp = icomp;
-                       }
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "schedule-outbox-URL");
+       e_xml_document_end_element (xml); /* prop */
 
-                       if (cache_comp && cbdav->priv->is_google) {
-                               icalcomponent_set_sequence (cache_comp, icalcomponent_get_sequence 
(cache_comp) + 1);
-                               icalcomponent_set_sequence (new_comp, icalcomponent_get_sequence (new_comp) + 
1);
-                       }
+       success = e_webdav_session_propfind_sync (cbdav->priv->webdav, owner_href, E_WEBDAV_DEPTH_THIS, xml,
+               ecb_caldav_propfind_get_schedule_outbox_url_cb, &schedule_outbox_url, cancellable, error);
 
-                       /* add the detached instance finally */
-                       icalcomponent_add_component (cache_comp, icalcomponent_new_clone (new_comp));
-               } else {
-                       cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone 
(e_cal_component_get_icalcomponent (comp)));
-               }
-               break;
-       case E_CAL_OBJ_MOD_ALL:
-               e_cal_recur_ensure_end_dates (comp, TRUE, resolve_tzid, cbdav);
-               cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone 
(e_cal_component_get_icalcomponent (comp)));
-               break;
-       case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
-       case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
-               master_comp = get_master_comp (cbdav, cache_comp);
-               if (e_cal_component_is_instance (comp) && master_comp) {
-                       ECalComponent *mcomp;
-                       struct icaltimetype rid, master_dtstart;
-                       icalcomponent *icalcomp = e_cal_component_get_icalcomponent (comp);
-                       icalcomponent *split_icalcomp;
-                       icalproperty *prop;
-
-                       rid = icalcomponent_get_recurrenceid (icalcomp);
-                       mcomp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone 
(master_comp));
-
-                       if (mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE &&
-                           e_cal_util_is_first_instance (mcomp, icalcomponent_get_recurrenceid (icalcomp), 
resolve_tzid, cbdav)) {
-                               icalproperty *prop = icalcomponent_get_first_property (icalcomp, 
ICAL_RECURRENCEID_PROPERTY);
-
-                               if (prop)
-                                       icalcomponent_remove_property (icalcomp, prop);
-
-                               e_cal_component_rescan (comp);
-                               e_cal_recur_ensure_end_dates (comp, TRUE, resolve_tzid, cbdav);
-
-                               /* Then do it like for "mod_all" */
-                               cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone 
(e_cal_component_get_icalcomponent (comp)));
-                               g_clear_object (&mcomp);
-
-                               if (new_components) {
-                                       /* read the comp from cache again, as some servers can modify it on 
put */
-                                       *new_components = g_slist_prepend (*new_components, 
get_ecalcomp_master_from_cache_or_fallback (cbdav, id->uid, NULL, comp));
-                               }
-                               break;
-                       }
-
-                       prop = icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY);
-                       if (prop)
-                               icalcomponent_remove_property (icalcomp, prop);
-                       e_cal_component_rescan (comp);
-
-                       master_dtstart = icalcomponent_get_dtstart (master_comp);
-                       if (master_dtstart.zone && master_dtstart.zone != rid.zone)
-                               rid = icaltime_convert_to_zone (rid, (icaltimezone *) master_dtstart.zone);
-                       split_icalcomp = e_cal_util_split_at_instance (icalcomp, rid, master_dtstart);
-                       if (split_icalcomp) {
-                               ECalComponent *prev_comp;
-
-                               prev_comp = e_cal_component_clone (mcomp);
+       g_object_unref (xml);
+       g_free (owner_href);
 
-                               rid = icaltime_convert_to_zone (rid, icaltimezone_get_utc_timezone ());
-                               e_cal_util_remove_instances (master_comp, rid, mod);
-                               e_cal_component_rescan (mcomp);
-                               e_cal_recur_ensure_end_dates (mcomp, TRUE, resolve_tzid, cbdav);
+       if (success && schedule_outbox_url && *schedule_outbox_url) {
+               g_free (cbdav->priv->schedule_outbox_url);
+               cbdav->priv->schedule_outbox_url = schedule_outbox_url;
 
-                               if (new_components) {
-                                       *new_components = g_slist_prepend (*new_components,
-                                               get_ecalcomp_master_from_cache_or_fallback (cbdav, id->uid, 
NULL, mcomp));
-                               }
-
-                               g_clear_object (&prev_comp);
-                       }
-
-                       cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone 
(master_comp));
-                       if (split_icalcomp) {
-                               gchar *new_uid;
-
-                               new_uid = e_cal_component_gen_uid ();
-                               icalcomponent_set_uid (split_icalcomp, new_uid);
-                               g_free (new_uid);
-
-                               g_warn_if_fail (e_cal_component_set_icalcomponent (comp, split_icalcomp));
-
-                               e_cal_recur_ensure_end_dates (comp, TRUE, resolve_tzid, cbdav);
-
-                               /* sanitize the component */
-                               sanitize_component (cbdav, comp);
-
-                               if (online) {
-                                       CalDAVObject object;
-
-                                       object.href = ecalcomp_gen_href (comp);
-                                       object.etag = NULL;
-                                       object.cdata = pack_cobj (cbdav, split_icalcomp);
-
-                                       success = caldav_server_put_object (cbdav, &object, split_icalcomp, 
cancellable, error);
-                                       if (success && new_components) {
-                                               ECalComponent *new_comp;
-
-                                               /* read the comp from cache again, as some servers can modify 
it on put */
-                                               new_comp = get_ecalcomp_master_from_cache_or_fallback (cbdav, 
icalcomponent_get_uid (split_icalcomp), NULL, comp);
-                                               if (new_comp)
-                                                       e_cal_backend_notify_component_created (E_CAL_BACKEND 
(cbdav), new_comp);
-
-                                               g_clear_object (&new_comp);
-                                       }
-
-                                       caldav_object_free (&object, FALSE);
-                               } else {
-                                       /* mark component as out of synch */
-                                       /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_CREATED); */
-                               }
-                       }
-
-                       g_clear_object (&mcomp);
-               } else {
-                       cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone 
(e_cal_component_get_icalcomponent (comp)));
-               }
-               break;
-       }
-
-       if (online) {
-               CalDAVObject object;
-
-               object.href = href;
-               object.etag = etag;
-               object.cdata = pack_cobj (cbdav, cache_comp);
-
-               did_put = success && caldav_server_put_object (cbdav, &object, cache_comp, cancellable, 
error);
-
-               caldav_object_free (&object, FALSE);
-               href = NULL;
-               etag = NULL;
+               schedule_outbox_url = NULL;
        } else {
-               /* mark component as out of synch */
-               /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_MODIFIED);*/
-       }
-
-       if (did_put) {
-               if (new_components && !*new_components) {
-                       /* read the comp from cache again, as some servers can modify it on put */
-                       *new_components = g_slist_prepend (*new_components, 
get_ecalcomp_master_from_cache_or_fallback (cbdav, id->uid, id->rid, NULL));
-               }
-       }
-
-       e_cal_component_free_id (id);
-       icalcomponent_free (cache_comp);
-       g_object_unref (comp);
-       g_free (href);
-       g_free (etag);
-}
-
-/* a busy_lock is supposed to be locked already, when calling this function */
-static void
-do_remove_objects (ECalBackendCalDAV *cbdav,
-                   const GSList *ids,
-                   ECalObjModType mod,
-                   GSList **old_components,
-                   GSList **new_components,
-                   GCancellable *cancellable,
-                   GError **perror)
-{
-       icalcomponent            *cache_comp;
-       gboolean                  online;
-       gchar *href = NULL, *etag = NULL;
-       const gchar *uid = ((ECalComponentId *) ids->data)->uid;
-       const gchar *rid = ((ECalComponentId *) ids->data)->rid;
-
-       if (new_components)
-               *new_components = NULL;
-
-       if (!check_state (cbdav, &online, perror))
-               return;
-
-       /* We make the assumption that the ids list we're passed is always exactly one element long, since we 
haven't specified "bulk-removes"
-        * in our static capability list. This simplifies a lot of the logic, especially around asynchronous 
results. */
-       if (ids->next != NULL) {
-               g_propagate_error (perror, e_data_cal_create_error (UnsupportedMethod, _("CalDAV does not 
support bulk removals")));
-               return;
-       }
-
-       cache_comp = get_comp_from_cache (cbdav, uid, NULL, &href, &etag);
-
-       if (cache_comp == NULL) {
-               g_propagate_error (perror, EDC_ERROR (ObjectNotFound));
-               return;
-       }
-
-       if (old_components) {
-               ECalComponent *old = e_cal_backend_store_get_component (cbdav->priv->store, uid, rid);
-
-               if (old) {
-                       *old_components = g_slist_prepend (*old_components, e_cal_component_clone (old));
-                       g_object_unref (old);
-               } else {
-                       icalcomponent *master = get_master_comp (cbdav, cache_comp);
-                       if (master) {
-                               *old_components = g_slist_prepend (*old_components, 
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
-                       }
-               }
-       }
-
-       switch (mod) {
-       case E_CAL_OBJ_MOD_ONLY_THIS:
-       case E_CAL_OBJ_MOD_THIS:
-               if (rid && *rid) {
-                       /* remove one instance from the component */
-                       if (remove_instance (cbdav, cache_comp, icaltime_from_string (rid), mod, mod != 
E_CAL_OBJ_MOD_ONLY_THIS)) {
-                               if (new_components) {
-                                       icalcomponent *master = get_master_comp (cbdav, cache_comp);
-                                       if (master) {
-                                               *new_components = g_slist_prepend (*new_components, 
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
-                                       }
-                               }
-                       } else {
-                               /* this was the last instance, thus delete whole component */
-                               rid = NULL;
-                               remove_comp_from_cache (cbdav, uid, NULL);
-                       }
-               } else {
-                       /* remove whole object */
-                       remove_comp_from_cache (cbdav, uid, NULL);
-               }
-               break;
-       case E_CAL_OBJ_MOD_ALL:
-               remove_comp_from_cache (cbdav, uid, NULL);
-               break;
-       case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
-       case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
-               if (remove_instance (cbdav, cache_comp, icaltime_from_string (rid), mod, TRUE)) {
-                       if (new_components) {
-                               icalcomponent *master = get_master_comp (cbdav, cache_comp);
-                               if (master) {
-                                       *new_components = g_slist_prepend (*new_components, 
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
-                               }
-                       }
-               }
-               break;
+               success = FALSE;
        }
 
-       if (online) {
-               CalDAVObject caldav_object;
-
-               caldav_object.href = href;
-               caldav_object.etag = etag;
-               caldav_object.cdata = NULL;
-
-               if (mod == E_CAL_OBJ_MOD_THIS && rid && *rid) {
-                       caldav_object.cdata = pack_cobj (cbdav, cache_comp);
-
-                       caldav_server_put_object (cbdav, &caldav_object, cache_comp, cancellable, perror);
-               } else
-                       caldav_server_delete_object (cbdav, &caldav_object, cancellable, perror);
-
-               caldav_object_free (&caldav_object, FALSE);
-               href = NULL;
-               etag = NULL;
-       } else {
-               /* mark component as out of synch */
-               /*if (mod == E_CAL_OBJ_MOD_THIS && rid && *rid)
-                       ecalcomp_set_synch_state (cache_comp_master, ECALCOMP_LOCALLY_MODIFIED);
-               else
-                       ecalcomp_set_synch_state (cache_comp_master, ECALCOMP_LOCALLY_DELETED);*/
-       }
-       remove_cached_attachment (cbdav, uid);
+       g_free (schedule_outbox_url);
 
-       icalcomponent_free (cache_comp);
-       g_free (href);
-       g_free (etag);
+       return success;
 }
 
 static void
-extract_objects (icalcomponent *icomp,
-                 icalcomponent_kind ekind,
-                 GSList **objects,
-                 GError **error)
+ecb_caldav_extract_objects (icalcomponent *icomp,
+                           icalcomponent_kind ekind,
+                           GSList **out_objects,
+                           GError **error)
 {
-       icalcomponent         *scomp;
-       icalcomponent_kind     kind;
+       icalcomponent *scomp;
+       icalcomponent_kind kind;
        GSList *link;
 
+       g_return_if_fail (icomp != NULL);
+       g_return_if_fail (out_objects != NULL);
+
        kind = icalcomponent_isa (icomp);
 
        if (kind == ekind) {
-               *objects = g_slist_prepend (NULL, icomp);
+               *out_objects = g_slist_prepend (NULL, icalcomponent_new_clone (icomp));
                return;
        }
 
@@ -4636,648 +1294,107 @@ extract_objects (icalcomponent *icomp,
                return;
        }
 
-       *objects = NULL;
+       *out_objects = NULL;
        scomp = icalcomponent_get_first_component (icomp, ekind);
 
        while (scomp) {
-               *objects = g_slist_prepend (*objects, scomp);
+               *out_objects = g_slist_prepend (*out_objects, scomp);
 
                scomp = icalcomponent_get_next_component (icomp, ekind);
        }
 
-       for (link = *objects; link; link = g_slist_next (link)) {
+       for (link = *out_objects; link; link = g_slist_next (link)) {
                /* Remove components from toplevel here */
                icalcomponent_remove_component (icomp, link->data);
        }
-}
-
-static gboolean
-extract_timezones (ECalBackendCalDAV *cbdav,
-                   icalcomponent *icomp)
-{
-       ETimezoneCache *timezone_cache;
-       GSList *timezones = NULL, *iter;
-       icaltimezone *zone;
-       GError *err = NULL;
 
-       g_return_val_if_fail (cbdav != NULL, FALSE);
-       g_return_val_if_fail (icomp != NULL, FALSE);
-
-       timezone_cache = E_TIMEZONE_CACHE (cbdav);
-
-       extract_objects (icomp, ICAL_VTIMEZONE_COMPONENT, &timezones, &err);
-       if (err) {
-               g_error_free (err);
-               return FALSE;
-       }
-
-       zone = icaltimezone_new ();
-       for (iter = timezones; iter; iter = iter->next) {
-               if (icaltimezone_set_component (zone, iter->data)) {
-                       e_timezone_cache_add_timezone (timezone_cache, zone);
-               } else {
-                       icalcomponent_free (iter->data);
-               }
-       }
-
-       icaltimezone_free (zone, TRUE);
-       g_slist_free (timezones);
-
-       return TRUE;
+       *out_objects = g_slist_reverse (*out_objects);
 }
 
-static void
-process_object (ECalBackendCalDAV *cbdav,
-                ECalComponent *ecomp,
-                gboolean online,
-                icalproperty_method method,
-                GCancellable *cancellable,
-                GError **error)
+static gchar *
+ecb_caldav_maybe_append_email_domain (const gchar *username,
+                                     const gchar *may_append)
 {
-       ESourceRegistry *registry;
-       ECalBackend              *backend;
-       struct icaltimetype       now;
-       gchar *new_obj_str;
-       gboolean is_declined, is_in_cache;
-       ECalObjModType mod;
-       ECalComponentId *id = e_cal_component_get_id (ecomp);
-       GError *err = NULL;
-
-       backend = E_CAL_BACKEND (cbdav);
-
-       if (id == NULL) {
-               g_set_error_literal (
-                       error, E_CAL_CLIENT_ERROR,
-                       E_CAL_CLIENT_ERROR_INVALID_OBJECT,
-                       e_cal_client_error_to_string (
-                       E_CAL_CLIENT_ERROR_INVALID_OBJECT));
-               return;
-       }
-
-       registry = e_cal_backend_get_registry (E_CAL_BACKEND (cbdav));
-
-       /* ctime, mtime */
-       now = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
-       e_cal_component_set_created (ecomp, &now);
-       e_cal_component_set_last_modified (ecomp, &now);
-
-       /* just to check whether component exists in a cache */
-       is_in_cache = cache_contains (cbdav, id->uid, NULL) || cache_contains (cbdav, id->uid, id->rid);
-
-       new_obj_str = e_cal_component_get_as_string (ecomp);
-       mod = e_cal_component_is_instance (ecomp) ? E_CAL_OBJ_MOD_THIS : E_CAL_OBJ_MOD_ALL;
-
-       switch (method) {
-       case ICAL_METHOD_PUBLISH:
-       case ICAL_METHOD_REQUEST:
-       case ICAL_METHOD_REPLY:
-               is_declined = e_cal_backend_user_declined (
-                       registry, e_cal_component_get_icalcomponent (ecomp));
-               if (is_in_cache) {
-                       if (!is_declined) {
-                               GSList *new_components = NULL, *old_components = NULL;
-                               GSList new_obj_strs = {0,};
-
-                               new_obj_strs.data = new_obj_str;
-                               do_modify_objects (cbdav, &new_obj_strs, mod,
-                                                 &old_components, &new_components, cancellable, &err);
-                               if (!err && new_components && new_components->data) {
-                                       if (!old_components || !old_components->data) {
-                                               e_cal_backend_notify_component_created (backend, 
new_components->data);
-                                       } else {
-                                               e_cal_backend_notify_component_modified (backend, 
old_components->data, new_components->data);
-                                       }
-                               }
-
-                               e_util_free_nullable_object_slist (old_components);
-                               e_util_free_nullable_object_slist (new_components);
-                       } else {
-                               GSList *new_components = NULL, *old_components = NULL;
-                               GSList ids = {0,};
-
-                               ids.data = id;
-                               do_remove_objects (cbdav, &ids, mod, &old_components, &new_components, 
cancellable, &err);
-                               if (!err && old_components && old_components->data) {
-                                       if (new_components && new_components->data) {
-                                               e_cal_backend_notify_component_modified (backend, 
old_components->data, new_components->data);
-                                       } else {
-                                               e_cal_backend_notify_component_removed (backend, id, 
old_components->data, NULL);
-                                       }
-                               }
-
-                               e_util_free_nullable_object_slist (old_components);
-                               e_util_free_nullable_object_slist (new_components);
-                       }
-               } else if (!is_declined) {
-                       GSList *new_components = NULL;
-                       GSList new_objs = {0,};
-
-                       new_objs.data = new_obj_str;
-
-                       do_create_objects (cbdav, &new_objs, NULL, &new_components, cancellable, &err);
-
-                       if (!err) {
-                               if (new_components && new_components->data)
-                                       e_cal_backend_notify_component_created (backend, 
new_components->data);
-                       }
-
-                       e_util_free_nullable_object_slist (new_components);
-               }
-               break;
-       case ICAL_METHOD_CANCEL:
-               if (is_in_cache) {
-                       GSList *new_components = NULL, *old_components = NULL;
-                       GSList ids = {0,};
-
-                       ids.data = id;
-                       do_remove_objects (cbdav, &ids, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, 
cancellable, &err);
-                       if (!err && old_components && old_components->data) {
-                               if (new_components && new_components->data) {
-                                       e_cal_backend_notify_component_modified (backend, 
old_components->data, new_components->data);
-                               } else {
-                                       e_cal_backend_notify_component_removed (backend, id, 
old_components->data, NULL);
-                               }
-                       }
-
-                       e_util_free_nullable_object_slist (old_components);
-                       e_util_free_nullable_object_slist (new_components);
-               } else {
-                       err = EDC_ERROR (ObjectNotFound);
-               }
-               break;
-
-       default:
-               err = EDC_ERROR (UnsupportedMethod);
-               break;
-       }
+       if (!username || !*username)
+               return NULL;
 
-       e_cal_component_free_id (id);
-       g_free (new_obj_str);
+       if (strchr (username, '@'))
+               return g_strdup (username);
 
-       if (err)
-               g_propagate_error (error, err);
+       return g_strconcat (username, may_append, NULL);
 }
 
-static void
-do_receive_objects (ECalBackendSync *backend,
-                    EDataCal *cal,
-                    GCancellable *cancellable,
-                    const gchar *calobj,
-                    GError **perror)
+static gchar *
+ecb_caldav_get_usermail (ECalBackendCalDAV *cbdav)
 {
-       ECalBackendCalDAV        *cbdav;
-       icalcomponent            *icomp;
-       icalcomponent_kind        kind;
-       icalproperty_method       tmethod;
-       gboolean                  online;
-       GSList                   *objects, *iter;
-       GError *err = NULL;
-
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
-
-       if (!check_state (cbdav, &online, perror))
-               return;
-
-       icomp = icalparser_parse_string (calobj);
-
-       /* Try to parse cal object string */
-       if (icomp == NULL) {
-               g_propagate_error (perror, EDC_ERROR (InvalidObject));
-               return;
-       }
-
-       kind = e_cal_backend_get_kind (E_CAL_BACKEND (backend));
-       extract_objects (icomp, kind, &objects, &err);
-
-       if (err) {
-               icalcomponent_free (icomp);
-               g_propagate_error (perror, err);
-               return;
-       }
-
-       /* Extract optional timezone compnents */
-       extract_timezones (cbdav, icomp);
-
-       if (icalcomponent_get_first_property (icomp, ICAL_METHOD_PROPERTY))
-               tmethod = icalcomponent_get_method (icomp);
-       else
-               tmethod = ICAL_METHOD_PUBLISH;
-
-       for (iter = objects; iter && !err; iter = iter->next) {
-               icalcomponent       *scomp;
-               ECalComponent       *ecomp;
-               icalproperty_method  method;
-
-               scomp = (icalcomponent *) iter->data;
-               ecomp = e_cal_component_new ();
+       ESource *source;
+       ESourceAuthentication *auth_extension;
+       ESourceWebdav *webdav_extension;
+       const gchar *extension_name;
+       gchar *usermail;
+       gchar *username;
+       gchar *res = NULL;
 
-               e_cal_component_set_icalcomponent (ecomp, scomp);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), NULL);
 
-               if (icalcomponent_get_first_property (scomp, ICAL_METHOD_PROPERTY)) {
-                       method = icalcomponent_get_method (scomp);
-               } else {
-                       method = tmethod;
-               }
+       source = e_backend_get_source (E_BACKEND (cbdav));
 
-               process_object (cbdav, ecomp, online, method, cancellable, &err);
-               g_object_unref (ecomp);
-       }
+       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+       webdav_extension = e_source_get_extension (source, extension_name);
 
-       g_slist_free (objects);
+       /* This will never return an empty string. */
+       usermail = e_source_webdav_dup_email_address (webdav_extension);
 
-       icalcomponent_free (icomp);
+       if (usermail)
+               return usermail;
 
-       if (err)
-               g_propagate_error (perror, err);
-}
+       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
+       auth_extension = e_source_get_extension (source, extension_name);
+       username = e_source_authentication_dup_user (auth_extension);
 
-#define caldav_busy_stub(_func_name, _params, _call_func, _call_params) \
-static void \
-_func_name _params \
-{ \
-       ECalBackendCalDAV        *cbdav; \
-       SlaveCommand              old_slave_cmd; \
-       gboolean                  was_slave_busy; \
- \
-       cbdav = E_CAL_BACKEND_CALDAV (backend); \
- \
-       /* this is done before locking */ \
-       old_slave_cmd = cbdav->priv->slave_cmd; \
-       was_slave_busy = cbdav->priv->slave_busy; \
-       if (was_slave_busy) { \
-               /* let it pause its work and do our job */ \
-               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP); \
-       } \
- \
-       g_mutex_lock (&cbdav->priv->busy_lock); \
-       _call_func _call_params; \
- \
-       /* this is done before unlocking */ \
-       if (was_slave_busy) { \
-               update_slave_cmd (cbdav->priv, old_slave_cmd); \
-               g_cond_signal (&cbdav->priv->cond); \
-       } \
- \
-       g_mutex_unlock (&cbdav->priv->busy_lock); \
-}
+       if (cbdav->priv->is_google)
+               res = ecb_caldav_maybe_append_email_domain (username, "@gmail.com");
 
-caldav_busy_stub (
-        caldav_create_objects,
-                  (ECalBackendSync *backend,
-                  EDataCal *cal,
-                  GCancellable *cancellable,
-                  const GSList *in_calobjs,
-                  GSList **uids,
-                  GSList **new_components,
-                  GError **perror),
-        do_create_objects,
-                  (cbdav,
-                  in_calobjs,
-                  uids,
-                  new_components,
-                  cancellable,
-                  perror))
-
-caldav_busy_stub (
-        caldav_modify_objects,
-                  (ECalBackendSync *backend,
-                  EDataCal *cal,
-                  GCancellable *cancellable,
-                  const GSList *calobjs,
-                  ECalObjModType mod,
-                  GSList **old_components,
-                  GSList **new_components,
-                  GError **perror),
-        do_modify_objects,
-                  (cbdav,
-                  calobjs,
-                  mod,
-                  old_components,
-                  new_components,
-                  cancellable,
-                  perror))
-
-caldav_busy_stub (
-        caldav_remove_objects,
-                  (ECalBackendSync *backend,
-                  EDataCal *cal,
-                  GCancellable *cancellable,
-                  const GSList *ids,
-                  ECalObjModType mod,
-                  GSList **old_components,
-                  GSList **new_components,
-                  GError **perror),
-        do_remove_objects,
-                  (cbdav,
-                  ids,
-                  mod,
-                  old_components,
-                  new_components,
-                  cancellable,
-                  perror))
-
-caldav_busy_stub (
-        caldav_receive_objects,
-                  (ECalBackendSync *backend,
-                  EDataCal *cal,
-                  GCancellable *cancellable,
-                  const gchar *calobj,
-                  GError **perror),
-        do_receive_objects,
-                  (backend,
-                  cal,
-                  cancellable,
-                  calobj,
-                  perror))
+       g_free (username);
 
-static void
-caldav_send_objects (ECalBackendSync *backend,
-                     EDataCal *cal,
-                     GCancellable *cancellable,
-                     const gchar *calobj,
-                     GSList **users,
-                     gchar **modified_calobj,
-                     GError **perror)
-{
-       *users = NULL;
-       *modified_calobj = g_strdup (calobj);
+       return res;
 }
 
 static gboolean
-caldav_server_download_uid (ECalBackendCalDAV *cbdav,
-                           const gchar *uid,
-                           GCancellable *cancellable,
-                           GError **error)
+ecb_caldav_get_free_busy_from_schedule_outbox_sync (ECalBackendCalDAV *cbdav,
+                                                   const GSList *users,
+                                                   time_t start,
+                                                   time_t end,
+                                                   GSList **out_freebusy,
+                                                   GCancellable *cancellable,
+                                                   GError **error)
 {
-       CalDAVObject obj;
-       GError *local_error = NULL;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-       g_return_val_if_fail (uid != NULL, FALSE);
-
-       obj.href = g_strdup (uid);
-       obj.etag = NULL;
-       obj.status = 0;
-       obj.cdata = NULL;
-
-       if (!caldav_server_get_object (cbdav, &obj, cancellable, &local_error)) {
-               if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
-                       gchar *file;
-
-                       /* OK, the event was properly created, but cannot be found on the place
-                        * where it was PUT - why didn't server tell us where it saved it? */
-                       g_clear_error (&local_error);
-
-                       /* try whether it's saved as its UID.ics file */
-                       file = caldav_gen_file_from_uid (cbdav, uid);
-                       if (file) {
-                               g_free (obj.href);
-                               obj.href = file;
-
-                               if (!caldav_server_get_object (cbdav, &obj, cancellable, &local_error)) {
-                               }
-                       }
-               }
-       }
-
-       if (!local_error) {
-               icalcomponent *use_comp = NULL;
-
-               if (obj.cdata) {
-                       /* maybe server also modified component, thus rather store the server's */
-                       use_comp = icalparser_parse_string (obj.cdata);
-                       put_comp_to_cache (cbdav, use_comp, obj.href, obj.etag);
-               }
-
-               if (use_comp)
-                       icalcomponent_free (use_comp);
-               else
-                       local_error = EDC_ERROR (ObjectNotFound);
-       }
-
-       if (local_error) {
-               g_propagate_error (error, local_error);
-
-               return FALSE;
-       }
-
-       g_free (obj.href);
-       g_free (obj.etag);
-       g_free (obj.cdata);
-
-       return TRUE;
-}
-
-static void
-caldav_get_object (ECalBackendSync *backend,
-                   EDataCal *cal,
-                   GCancellable *cancellable,
-                   const gchar *uid,
-                   const gchar *rid,
-                   gchar **object,
-                   GError **perror)
-{
-       ECalBackendCalDAV        *cbdav;
-       icalcomponent            *icalcomp;
-
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
-
-       *object = NULL;
-       icalcomp = get_comp_from_cache (cbdav, uid, rid, NULL, NULL);
-
-       if (!icalcomp && e_backend_get_online (E_BACKEND (backend))) {
-               /* try to fetch from the server, maybe the event was received only recently */
-               if (caldav_server_download_uid (cbdav, uid, cancellable, NULL)) {
-                       icalcomp = get_comp_from_cache (cbdav, uid, rid, NULL, NULL);
-               }
-       }
-
-       if (!icalcomp) {
-               g_propagate_error (perror, EDC_ERROR (ObjectNotFound));
-               return;
-       }
-
-       *object = icalcomponent_as_ical_string_r (icalcomp);
-       icalcomponent_free (icalcomp);
-}
-
-static void
-caldav_add_timezone (ECalBackendSync *backend,
-                     EDataCal *cal,
-                     GCancellable *cancellable,
-                     const gchar *tzobj,
-                     GError **error)
-{
-       ETimezoneCache *timezone_cache;
-       icalcomponent *tz_comp;
-
-       timezone_cache = E_TIMEZONE_CACHE (backend);
-
-       tz_comp = icalparser_parse_string (tzobj);
-       if (!tz_comp) {
-               g_propagate_error (error, EDC_ERROR (InvalidObject));
-               return;
-       }
-
-       if (icalcomponent_isa (tz_comp) == ICAL_VTIMEZONE_COMPONENT) {
-               icaltimezone *zone;
-
-               zone = icaltimezone_new ();
-               icaltimezone_set_component (zone, tz_comp);
-
-               e_timezone_cache_add_timezone (timezone_cache, zone);
-
-               icaltimezone_free (zone, TRUE);
-       } else {
-               icalcomponent_free (tz_comp);
-       }
-}
-
-static void
-caldav_get_object_list (ECalBackendSync *backend,
-                        EDataCal *cal,
-                        GCancellable *cancellable,
-                        const gchar *sexp_string,
-                        GSList **objects,
-                        GError **perror)
-{
-       ECalBackendCalDAV        *cbdav;
-       ECalBackendSExp  *sexp;
-       ETimezoneCache *cache;
-       gboolean                  do_search;
-       GSList                   *list, *iter;
-       time_t occur_start = -1, occur_end = -1;
-       gboolean prunning_by_time;
-
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
-
-       sexp = e_cal_backend_sexp_new (sexp_string);
-
-       if (sexp == NULL) {
-               g_propagate_error (perror, EDC_ERROR (InvalidQuery));
-               return;
-       }
-
-       if (g_str_equal (sexp_string, "#t")) {
-               do_search = FALSE;
-       } else {
-               do_search = TRUE;
-       }
-
-       *objects = NULL;
-
-       prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (sexp, &occur_start, &occur_end);
-
-       cache = E_TIMEZONE_CACHE (backend);
-
-       list = prunning_by_time ?
-               e_cal_backend_store_get_components_occuring_in_range (cbdav->priv->store, occur_start, 
occur_end)
-               : e_cal_backend_store_get_components (cbdav->priv->store);
-
-       for (iter = list; iter; iter = g_slist_next (iter)) {
-               ECalComponent *comp = E_CAL_COMPONENT (iter->data);
-
-               if (!do_search ||
-                   e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
-                       *objects = g_slist_prepend (*objects, e_cal_component_get_as_string (comp));
-               }
-
-               g_object_unref (comp);
-       }
-
-       g_object_unref (sexp);
-       g_slist_free (list);
-}
-
-static void
-caldav_start_view (ECalBackend *backend,
-                   EDataCalView *query)
-{
-       ECalBackendCalDAV        *cbdav;
-       ECalBackendSExp  *sexp;
-       ETimezoneCache *cache;
-       gboolean                  do_search;
-       GSList                   *list, *iter;
-       const gchar               *sexp_string;
-       time_t occur_start = -1, occur_end = -1;
-       gboolean prunning_by_time;
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
-
-       sexp = e_data_cal_view_get_sexp (query);
-       sexp_string = e_cal_backend_sexp_text (sexp);
-
-       if (g_str_equal (sexp_string, "#t")) {
-               do_search = FALSE;
-       } else {
-               do_search = TRUE;
-       }
-       prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (
-               sexp,
-               &occur_start,
-               &occur_end);
-
-       cache = E_TIMEZONE_CACHE (backend);
-
-       list = prunning_by_time ?
-               e_cal_backend_store_get_components_occuring_in_range (cbdav->priv->store, occur_start, 
occur_end)
-               : e_cal_backend_store_get_components (cbdav->priv->store);
-
-       for (iter = list; iter; iter = g_slist_next (iter)) {
-               ECalComponent *comp = E_CAL_COMPONENT (iter->data);
-
-               if (!do_search ||
-                   e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
-                       e_data_cal_view_notify_components_added_1 (query, comp);
-               }
-
-               g_object_unref (comp);
-       }
-
-       g_slist_free (list);
-
-       e_data_cal_view_notify_complete (query, NULL /* Success */);
-}
-
-static void
-caldav_get_free_busy (ECalBackendSync *backend,
-                      EDataCal *cal,
-                      GCancellable *cancellable,
-                      const GSList *users,
-                      time_t start,
-                      time_t end,
-                      GSList **freebusy,
-                      GError **error)
-{
-       ECalBackendCalDAV *cbdav;
        icalcomponent *icalcomp;
        ECalComponent *comp;
        ECalComponentDateTime dt;
-       ECalComponentOrganizer organizer = {NULL};
+       ECalComponentOrganizer organizer = { NULL };
        ESourceAuthentication *auth_extension;
        ESource *source;
        struct icaltimetype dtvalue;
        icaltimezone *utc;
        gchar *str;
-       const GSList *u;
+       GSList *link;
        GSList *attendees = NULL, *to_free = NULL;
        const gchar *extension_name;
        gchar *usermail;
-       GError *err = NULL;
+       GByteArray *response = NULL;
+       GError *local_error = NULL;
 
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
 
-       if (!cbdav->priv->calendar_schedule) {
-               g_propagate_error (error, EDC_ERROR_EX (OtherError, _("Calendar doesn’t support Free/Busy")));
-               return;
-       }
+       if (!cbdav->priv->calendar_schedule)
+               return FALSE;
 
        if (!cbdav->priv->schedule_outbox_url) {
-               caldav_receive_schedule_outbox_url (cbdav, cancellable, error);
-               if (!cbdav->priv->schedule_outbox_url) {
+               if (!ecb_caldav_receive_schedule_outbox_url_sync (cbdav, cancellable, error) ||
+                   !cbdav->priv->schedule_outbox_url) {
                        cbdav->priv->calendar_schedule = FALSE;
-                       if (error && !*error)
-                               g_propagate_error (error, EDC_ERROR_EX (OtherError, _("Schedule outbox url 
not found")));
-                       return;
+                       return FALSE;
                }
        }
 
@@ -5301,17 +1418,17 @@ caldav_get_free_busy (ECalBackendSync *backend,
        dtvalue = icaltime_from_timet_with_zone (end, FALSE, utc);
        e_cal_component_set_dtend (comp, &dt);
 
-       usermail = get_usermail (E_CAL_BACKEND (backend));
+       usermail = ecb_caldav_get_usermail (cbdav);
        if (usermail != NULL && *usermail == '\0') {
                g_free (usermail);
                usermail = NULL;
        }
 
-       source = e_backend_get_source (E_BACKEND (backend));
+       source = e_backend_get_source (E_BACKEND (cbdav));
        extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
        auth_extension = e_source_get_extension (source, extension_name);
 
-       if (usermail == NULL)
+       if (!usermail)
                usermail = e_source_authentication_dup_user (auth_extension);
 
        organizer.value = g_strconcat ("mailto:";, usermail, NULL);
@@ -5320,9 +1437,9 @@ caldav_get_free_busy (ECalBackendSync *backend,
 
        g_free (usermail);
 
-       for (u = users; u; u = u->next) {
+       for (link = (GSList *) users; link; link = g_slist_next (link)) {
                ECalComponentAttendee *ca;
-               gchar *temp = g_strconcat ("mailto:";, (const gchar *) u->data, NULL);
+               gchar *temp = g_strconcat ("mailto:";, (const gchar *) link->data, NULL);
 
                ca = g_new0 (ECalComponentAttendee, 1);
 
@@ -5337,11 +1454,8 @@ caldav_get_free_busy (ECalBackendSync *backend,
 
        e_cal_component_set_attendee_list (comp, attendees);
 
-       g_slist_foreach (attendees, (GFunc) g_free, NULL);
-       g_slist_free (attendees);
-
-       g_slist_foreach (to_free, (GFunc) g_free, NULL);
-       g_slist_free (to_free);
+       g_slist_free_full (attendees, g_free);
+       g_slist_free_full (to_free, g_free);
 
        e_cal_component_abort_sequence (comp);
 
@@ -5355,387 +1469,361 @@ caldav_get_free_busy (ECalBackendSync *backend,
        icalcomponent_free (icalcomp);
        g_object_unref (comp);
 
-       caldav_post_freebusy (cbdav, cbdav->priv->schedule_outbox_url, &str, cancellable, &err);
-
-       if (!err) {
+       if (e_webdav_session_post_sync (cbdav->priv->webdav, cbdav->priv->schedule_outbox_url, str, -1, NULL, 
&response, cancellable, &local_error) &&
+           response) {
                /* parse returned xml */
                xmlDocPtr doc;
+               xmlXPathContextPtr xpath_ctx = NULL;
+
+               doc = e_xml_parse_data (response->data, response->len);
+
+               if (!doc) {
+                       g_set_error_literal (&local_error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                               _("Failed to parse response data"));
+               } else {
+                       xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+                               "D", E_WEBDAV_NS_DAV,
+                               "C", E_WEBDAV_NS_CALDAV,
+                               NULL);
+               }
 
-               doc = xmlReadMemory (str, strlen (str), "response.xml", NULL, 0);
-               if (doc != NULL) {
-                       xmlXPathContextPtr xpctx;
-                       xmlXPathObjectPtr result;
+               if (xpath_ctx) {
+                       xmlXPathObjectPtr xpath_obj_response;
 
-                       xpctx = xmlXPathNewContext (doc);
-                       xmlXPathRegisterNs (xpctx, (xmlChar *) "D", (xmlChar *) "DAV:");
-                       xmlXPathRegisterNs (xpctx, (xmlChar *) "C", (xmlChar *) 
"urn:ietf:params:xml:ns:caldav");
+                       xpath_obj_response = e_xml_xpath_eval (xpath_ctx, "/C:schedule-response/C:response");
 
-                       result = xpath_eval (xpctx, "/C:schedule-response/C:response");
+                       if (xpath_obj_response) {
+                               gint response_index, response_length;
 
-                       if (result == NULL || result->type != XPATH_NODESET) {
-                               err = EDC_ERROR_EX (OtherError, _("Unexpected result in schedule-response"));
-                       } else {
-                               gint i, n;
+                               response_length = xmlXPathNodeSetGetLength (xpath_obj_response->nodesetval);
 
-                               n = xmlXPathNodeSetGetLength (result->nodesetval);
-                               for (i = 0; i < n; i++) {
+                               for (response_index = 0; response_index < response_length; response_index++) {
                                        gchar *tmp;
 
-                                       tmp = xp_object_get_string (xpath_eval (xpctx, 
"string(/C:schedule-response/C:response[%d]/C:calendar-data)", i + 1));
+                                       tmp = e_xml_xpath_eval_as_string 
(xpath_ctx,"/C:schedule-response/C:response[%d]/C:calendar-data", response_index + 1);
                                        if (tmp && *tmp) {
-                                               GSList *objects = NULL, *o;
+                                               GSList *objects = NULL;
 
                                                icalcomp = icalparser_parse_string (tmp);
                                                if (icalcomp)
-                                                       extract_objects (icalcomp, ICAL_VFREEBUSY_COMPONENT, 
&objects, &err);
-                                               if (icalcomp && !err) {
-                                                       for (o = objects; o; o = o->next) {
-                                                               gchar *obj_str = 
icalcomponent_as_ical_string_r (o->data);
+                                                       ecb_caldav_extract_objects (icalcomp, 
ICAL_VFREEBUSY_COMPONENT, &objects, &local_error);
+                                               if (icalcomp && !local_error) {
+                                                       for (link = objects; link; link = g_slist_next 
(link)) {
+                                                               gchar *obj_str = 
icalcomponent_as_ical_string_r (link->data);
 
                                                                if (obj_str && *obj_str)
-                                                                       *freebusy = g_slist_append 
(*freebusy, obj_str);
+                                                                       *out_freebusy = g_slist_prepend 
(*out_freebusy, obj_str);
                                                                else
                                                                        g_free (obj_str);
                                                        }
                                                }
 
-                                               g_slist_foreach (objects, (GFunc) icalcomponent_free, NULL);
-                                               g_slist_free (objects);
+                                               g_slist_free_full (objects, (GDestroyNotify) 
icalcomponent_free);
 
                                                if (icalcomp)
                                                        icalcomponent_free (icalcomp);
-                                               if (err)
-                                                       g_error_free (err);
-                                               err = NULL;
+                                               g_clear_error (&local_error);
                                        }
 
                                        g_free (tmp);
                                }
+
+                               xmlXPathFreeObject (xpath_obj_response);
                        }
 
-                       if (result != NULL)
-                               xmlXPathFreeObject (result);
-                       xmlXPathFreeContext (xpctx);
-                       xmlFreeDoc (doc);
+                       xmlXPathFreeContext (xpath_ctx);
                }
+
+               if (doc)
+                       xmlFreeDoc (doc);
        }
 
+       if (response)
+               g_byte_array_free (response, TRUE);
        g_free (str);
 
-       if (err)
-               g_propagate_error (error, err);
-}
+       if (local_error)
+               g_propagate_error (error, local_error);
 
-static void
-caldav_notify_online_cb (ECalBackend *backend,
-                         GParamSpec *pspec)
-{
-       ECalBackendCalDAV        *cbdav;
-       gboolean online;
+       return local_error != NULL;
+}
 
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
+static gboolean
+ecb_caldav_get_free_busy_from_principal_sync (ECalBackendCalDAV *cbdav,
+                                             const gchar *usermail,
+                                             time_t start,
+                                             time_t end,
+                                             GSList **out_freebusy,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       EWebDAVResource *resource;
+       GSList *principals = NULL;
+       EXmlDocument *xml;
+       gchar *href;
+       gchar *content_type = NULL;
+       GByteArray *content = NULL;
+       gboolean success;
 
-       /*g_mutex_lock (&cbdav->priv->busy_lock);*/
+       g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
+       g_return_val_if_fail (usermail != NULL, FALSE);
+       g_return_val_if_fail (out_freebusy != NULL, FALSE);
 
-       online = e_backend_get_online (E_BACKEND (backend));
+       if (!e_webdav_session_principal_property_search_sync (cbdav->priv->webdav, NULL, TRUE,
+               E_WEBDAV_NS_CALDAV, "calendar-user-address-set", usermail, &principals, cancellable, error)) {
+               return FALSE;
+       }
 
-       if (!cbdav->priv->loaded) {
-               /*g_mutex_unlock (&cbdav->priv->busy_lock);*/
-               return;
+       if (!principals || principals->next || !principals->data) {
+               g_slist_free_full (principals, e_webdav_resource_free);
+               return FALSE;
        }
 
-       if (online) {
-               /* Wake up the slave thread */
-               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
-               g_cond_signal (&cbdav->priv->cond);
-       } else {
-               soup_session_abort (cbdav->priv->session);
-               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
+       resource = principals->data;
+       href = g_strdup (resource->href);
+
+       g_slist_free_full (principals, e_webdav_resource_free);
+
+       if (!href || !*href) {
+               g_free (href);
+               return FALSE;
        }
 
-       /*g_mutex_unlock (&cbdav->priv->busy_lock);*/
-}
+       xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "free-busy-query");
 
-static gpointer
-caldav_source_changed_thread (gpointer data)
-{
-       ECalBackendCalDAV *cbdav = data;
-       SlaveCommand old_slave_cmd;
-       gboolean old_slave_busy;
+       e_xml_document_start_element (xml, NULL, "time-range");
+       e_xml_document_add_attribute_time (xml, NULL, "start", start);
+       e_xml_document_add_attribute_time (xml, NULL, "end", end);
+       e_xml_document_end_element (xml); /* time-range */
 
-       g_return_val_if_fail (cbdav != NULL, NULL);
+       success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_INFINITY, xml, 
NULL, NULL, &content_type, &content, cancellable, error);
 
-       old_slave_cmd = cbdav->priv->slave_cmd;
-       old_slave_busy = cbdav->priv->slave_busy;
-       if (old_slave_busy)
-               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
+       g_object_unref (xml);
 
-       g_mutex_lock (&cbdav->priv->busy_lock);
+       if (success && content_type && content && content->data && content->len &&
+           g_ascii_strcasecmp (content_type, "text/calendar") == 0) {
+               icalcomponent *vcalendar;
 
-       /* guard the call with busy_lock, thus the two threads (this 'source changed'
-        * thread and the 'backend open' thread) will not clash on internal data
-        * when they are called in once */
-       initialize_backend (cbdav, NULL);
+               vcalendar = icalcomponent_new_from_string ((const gchar *) content->data);
+               if (vcalendar) {
+                       GSList *comps = NULL, *link;
 
-       /* always wakeup thread, even when it was sleeping */
-       g_cond_signal (&cbdav->priv->cond);
+                       ecb_caldav_extract_objects (vcalendar, ICAL_VFREEBUSY_COMPONENT, &comps, NULL);
 
-       if (old_slave_busy)
-               update_slave_cmd (cbdav->priv, old_slave_cmd);
+                       for (link = comps; link; link = g_slist_next (link)) {
+                               icalcomponent *subcomp = link->data;
+                               gchar *obj_str;
 
-       g_mutex_unlock (&cbdav->priv->busy_lock);
+                               if (!icalcomponent_get_first_property (subcomp, ICAL_ATTENDEE_PROPERTY)) {
+                                       icalproperty *prop;
+                                       gchar *mailto;
 
-       cbdav->priv->updating_source = FALSE;
+                                       mailto = g_strconcat ("mailto:";, usermail, NULL);
+                                       prop = icalproperty_new_attendee (mailto);
+                                       g_free (mailto);
 
-       g_object_unref (cbdav);
+                                       icalcomponent_add_property (subcomp, prop);
+                               }
 
-       return NULL;
-}
+                               obj_str = icalcomponent_as_ical_string_r (subcomp);
 
-static void
-caldav_source_changed_cb (ESource *source,
-                          ECalBackendCalDAV *cbdav)
-{
-       GThread *thread;
+                               if (obj_str && *obj_str)
+                                       *out_freebusy = g_slist_prepend (*out_freebusy, obj_str);
+                               else
+                                       g_free (obj_str);
+                       }
 
-       g_return_if_fail (source != NULL);
-       g_return_if_fail (cbdav != NULL);
+                       success = comps != NULL;
 
-       if (cbdav->priv->updating_source ||
-           !cbdav->priv->loaded ||
-           !e_cal_backend_is_opened (E_CAL_BACKEND (cbdav)))
-               return;
+                       g_slist_free_full (comps, (GDestroyNotify) icalcomponent_free);
+               } else {
+                       success = FALSE;
+               }
+       }
 
-       cbdav->priv->updating_source = TRUE;
+       if (content)
+               g_byte_array_free (content, TRUE);
+       g_free (content_type);
+       g_free (href);
 
-       thread = g_thread_new (NULL, caldav_source_changed_thread, g_object_ref (cbdav));
-       g_thread_unref (thread);
+       return success;
 }
 
-static ESourceAuthenticationResult
-caldav_authenticate_sync (EBackend *backend,
-                         const ENamedParameters *credentials,
-                         gchar **out_certificate_pem,
-                         GTlsCertificateFlags *out_certificate_errors,
-                         GCancellable *cancellable,
-                         GError **error)
+static void
+ecb_caldav_get_free_busy_sync (ECalBackendSync *sync_backend,
+                              EDataCal *cal,
+                              GCancellable *cancellable,
+                              const GSList *users,
+                              time_t start,
+                              time_t end,
+                              GSList **out_freebusy,
+                              GError **error)
 {
        ECalBackendCalDAV *cbdav;
-       ESourceAuthenticationResult result;
-       GError *local_error = NULL;
 
-       cbdav = E_CAL_BACKEND_CALDAV (backend);
+       g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (sync_backend));
+       g_return_if_fail (out_freebusy != NULL);
 
-       g_mutex_lock (&cbdav->priv->busy_lock);
+       cbdav = E_CAL_BACKEND_CALDAV (sync_backend);
 
-       e_named_parameters_free (cbdav->priv->credentials);
-       cbdav->priv->credentials = e_named_parameters_new_clone (credentials);
-
-       open_calendar_wrapper (cbdav, cancellable, &local_error, FALSE, NULL, out_certificate_pem, 
out_certificate_errors);
-
-       if (local_error == NULL) {
-               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+       if (e_backend_get_online (E_BACKEND (cbdav)) &&
+           cbdav->priv->webdav) {
+               const GSList *link;
+               GError *local_error = NULL;
 
-               update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
-               g_cond_signal (&cbdav->priv->cond);
-       } else if (g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed) ||
-                  g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationRequired)) {
-               const gchar *username;
-               gchar *auth_user = NULL;
+               if (ecb_caldav_get_free_busy_from_schedule_outbox_sync (cbdav, users, start, end, 
out_freebusy, cancellable, &local_error))
+                       return;
 
-               username = e_named_parameters_get (cbdav->priv->credentials, E_SOURCE_CREDENTIAL_USERNAME);
+               g_clear_error (&local_error);
 
-               if (!username || !*username) {
-                       ESource *source;
-                       ESourceAuthentication *auth_extension;
+               if (g_cancellable_set_error_if_cancelled (cancellable, error))
+                       return;
 
-                       source = e_backend_get_source (backend);
-                       auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
-                       auth_user = e_source_authentication_dup_user (auth_extension);
+               *out_freebusy = NULL;
 
-                       username = auth_user;
+               for (link = users; link && !g_cancellable_is_cancelled (cancellable); link = g_slist_next 
(link)) {
+                       if (!ecb_caldav_get_free_busy_from_principal_sync (cbdav, link->data, start, end, 
out_freebusy, cancellable, &local_error))
+                               g_clear_error (&local_error);
                }
 
-               if (username && *username) {
-                       if (!cbdav->priv->using_bearer_auth &&
-                           !e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
-                               result = E_SOURCE_AUTHENTICATION_REQUIRED;
-                       else
-                               result = E_SOURCE_AUTHENTICATION_REJECTED;
-                       g_clear_error (&local_error);
-               } else {
-                       result = E_SOURCE_AUTHENTICATION_ERROR;
-                       g_propagate_error (error, local_error);
-               }
+               g_clear_error (&local_error);
 
-               g_free (auth_user);
-       } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-               result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
-               g_propagate_error (error, local_error);
-       } else {
-               result = E_SOURCE_AUTHENTICATION_ERROR;
-               g_propagate_error (error, local_error);
+               if (*out_freebusy || g_cancellable_set_error_if_cancelled (cancellable, error))
+                       return;
        }
 
-       g_mutex_unlock (&cbdav->priv->busy_lock);
-
-       return result;
+       /* Chain up to parent's method. */
+       E_CAL_BACKEND_SYNC_CLASS (e_cal_backend_caldav_parent_class)->get_free_busy_sync (sync_backend, cal, 
cancellable,
+               users, start, end, out_freebusy, error);
 }
 
-/* ************************************************************************* */
-/* ***************************** GObject Foo ******************************* */
-
-static void
-e_cal_backend_caldav_dispose (GObject *object)
+static gchar *
+ecb_caldav_get_backend_property (ECalBackend *backend,
+                                const gchar *prop_name)
 {
-       ECalBackendCalDAVPrivate *priv;
-
-       priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (object);
+       g_return_val_if_fail (prop_name != NULL, NULL);
 
-       g_clear_object (&priv->store);
-       g_clear_object (&priv->session);
-       g_clear_object (&priv->using_bearer_auth);
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               ESourceWebdav *extension;
+               ESource *source;
+               GString *caps;
+               gchar *usermail;
+               const gchar *extension_name;
 
-       /* Chain up to parent's dispose() method. */
-       G_OBJECT_CLASS (parent_class)->dispose (object);
-}
+               caps = g_string_new (
+                       CAL_STATIC_CAPABILITY_NO_THISANDPRIOR ","
+                       CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED);
+               g_string_append (caps, ",");
+               g_string_append (caps, e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (backend)));
 
-static void
-e_cal_backend_caldav_finalize (GObject *object)
-{
-       ECalBackendCalDAVPrivate *priv;
+               usermail = ecb_caldav_get_usermail (E_CAL_BACKEND_CALDAV (backend));
+               if (!usermail || !*usermail)
+                       g_string_append (caps, "," CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS);
+               g_free (usermail);
 
-       priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (object);
+               source = e_backend_get_source (E_BACKEND (backend));
 
-       g_mutex_clear (&priv->busy_lock);
-       g_cond_clear (&priv->cond);
-       g_cond_clear (&priv->slave_gone_cond);
+               extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+               extension = e_source_get_extension (source, extension_name);
 
-       g_free (priv->uri);
-       e_named_parameters_free (priv->credentials);
-       priv->credentials = NULL;
-       g_free (priv->schedule_outbox_url);
+               if (e_source_webdav_get_calendar_auto_schedule (extension)) {
+                       g_string_append (
+                               caps,
+                               "," CAL_STATIC_CAPABILITY_CREATE_MESSAGES
+                               "," CAL_STATIC_CAPABILITY_SAVE_SCHEDULES);
+               }
 
-       if (priv->ctag_to_store) {
-               g_free (priv->ctag_to_store);
-               priv->ctag_to_store = NULL;
+               return g_string_free (caps, FALSE);
+       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
+                  g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
+               return ecb_caldav_get_usermail (E_CAL_BACKEND_CALDAV (backend));
        }
 
-       g_clear_error (&priv->bearer_auth_error);
-       g_mutex_clear (&priv->bearer_auth_error_lock);
-
-       /* Chain up to parent's finalize() method. */
-       G_OBJECT_CLASS (parent_class)->finalize (object);
+       /* Chain up to parent's method. */
+       return E_CAL_BACKEND_CLASS (e_cal_backend_caldav_parent_class)->get_backend_property (backend, 
prop_name);
 }
 
-static gboolean
-caldav_backend_initable_init (GInitable *initable,
-                              GCancellable *cancellable,
-                              GError **error)
+static gchar *
+ecb_caldav_dup_component_revision_cb (ECalCache *cal_cache,
+                                     icalcomponent *icalcomp)
 {
-       ECalBackendCalDAVPrivate *priv;
-
-       priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (initable);
-
-       g_mutex_init (&priv->bearer_auth_error_lock);
+       g_return_val_if_fail (icalcomp != NULL, NULL);
 
-       return TRUE;
+       return e_cal_util_dup_x_property (icalcomp, E_CALDAV_X_ETAG);
 }
 
 static void
-e_cal_backend_caldav_init (ECalBackendCalDAV *cbdav)
+e_cal_backend_caldav_constructed (GObject *object)
 {
-       cbdav->priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (cbdav);
-       cbdav->priv->session = soup_session_sync_new ();
-       g_object_set (
-               cbdav->priv->session,
-               SOUP_SESSION_TIMEOUT, 90,
-               SOUP_SESSION_SSL_STRICT, TRUE,
-               SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
-               SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
-               NULL);
+       ECalBackendCalDAV *cbdav = E_CAL_BACKEND_CALDAV (object);
+       ECalCache *cal_cache;
 
-       e_binding_bind_property (
-               cbdav, "proxy-resolver",
-               cbdav->priv->session, "proxy-resolver",
-               G_BINDING_SYNC_CREATE);
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_caldav_parent_class)->constructed (object);
 
-       if (G_UNLIKELY (caldav_debug_show (DEBUG_MESSAGE)))
-               caldav_debug_setup (cbdav->priv->session);
+       cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbdav));
 
-       cbdav->priv->loaded = FALSE;
-       cbdav->priv->opened = FALSE;
+       g_signal_connect (cal_cache, "dup-component-revision",
+               G_CALLBACK (ecb_caldav_dup_component_revision_cb), NULL);
 
-       /* Thinks the 'getctag' extension is available the first time, but unset it when realizes it isn't. */
-       cbdav->priv->ctag_supported = TRUE;
-       cbdav->priv->ctag_to_store = NULL;
+       g_clear_object (&cal_cache);
+}
+
+static void
+e_cal_backend_caldav_dispose (GObject *object)
+{
+       ECalBackendCalDAV *cbdav = E_CAL_BACKEND_CALDAV (object);
 
-       cbdav->priv->schedule_outbox_url = NULL;
+       g_clear_object (&cbdav->priv->webdav);
 
-       cbdav->priv->is_google = FALSE;
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_caldav_parent_class)->dispose (object);
+}
 
-       g_mutex_init (&cbdav->priv->busy_lock);
-       g_cond_init (&cbdav->priv->cond);
-       g_cond_init (&cbdav->priv->slave_gone_cond);
+static void
+e_cal_backend_caldav_finalize (GObject *object)
+{
+       ECalBackendCalDAV *cbdav = E_CAL_BACKEND_CALDAV (object);
 
-       /* Slave control ... */
-       cbdav->priv->slave_cmd = SLAVE_SHOULD_SLEEP;
-       cbdav->priv->slave_busy = FALSE;
+       g_clear_pointer (&cbdav->priv->schedule_outbox_url, g_free);
 
-       g_signal_connect (
-               cbdav->priv->session, "authenticate",
-               G_CALLBACK (soup_authenticate), cbdav);
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_caldav_parent_class)->finalize (object);
+}
 
-       g_signal_connect (
-               cbdav, "notify::online",
-               G_CALLBACK (caldav_notify_online_cb), NULL);
+static void
+e_cal_backend_caldav_init (ECalBackendCalDAV *cbdav)
+{
+       cbdav->priv = G_TYPE_INSTANCE_GET_PRIVATE (cbdav, E_TYPE_CAL_BACKEND_CALDAV, 
ECalBackendCalDAVPrivate);
 }
 
 static void
-e_cal_backend_caldav_class_init (ECalBackendCalDAVClass *class)
+e_cal_backend_caldav_class_init (ECalBackendCalDAVClass *klass)
 {
        GObjectClass *object_class;
-       EBackendClass *backend_class;
        ECalBackendClass *cal_backend_class;
-       ECalBackendSyncClass *sync_class;
+       ECalBackendSyncClass *cal_backend_sync_class;
+       ECalMetaBackendClass *cal_meta_backend_class;
+
+       g_type_class_add_private (klass, sizeof (ECalBackendCalDAVPrivate));
 
-       object_class = G_OBJECT_CLASS (class);
-       backend_class = E_BACKEND_CLASS (class);
-       cal_backend_class = E_CAL_BACKEND_CLASS (class);
-       sync_class = E_CAL_BACKEND_SYNC_CLASS (class);
+       cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+       cal_meta_backend_class->connect_sync = ecb_caldav_connect_sync;
+       cal_meta_backend_class->disconnect_sync = ecb_caldav_disconnect_sync;
+       cal_meta_backend_class->get_changes_sync = ecb_caldav_get_changes_sync;
+       cal_meta_backend_class->list_existing_sync = ecb_caldav_list_existing_sync;
+       cal_meta_backend_class->load_component_sync = ecb_caldav_load_component_sync;
+       cal_meta_backend_class->save_component_sync = ecb_caldav_save_component_sync;
+       cal_meta_backend_class->remove_component_sync = ecb_caldav_remove_component_sync;
 
-       caldav_debug_init ();
+       cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+       cal_backend_sync_class->get_free_busy_sync = ecb_caldav_get_free_busy_sync;
 
-       parent_class = (ECalBackendSyncClass *) g_type_class_peek_parent (class);
-       g_type_class_add_private (class, sizeof (ECalBackendCalDAVPrivate));
+       cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+       cal_backend_class->get_backend_property = ecb_caldav_get_backend_property;
 
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_cal_backend_caldav_constructed;
        object_class->dispose = e_cal_backend_caldav_dispose;
        object_class->finalize = e_cal_backend_caldav_finalize;
-
-       backend_class->authenticate_sync = caldav_authenticate_sync;
-
-       cal_backend_class->get_backend_property = caldav_get_backend_property;
-       cal_backend_class->shutdown = caldav_shutdown;
-       cal_backend_class->start_view = caldav_start_view;
-
-       sync_class->open_sync = caldav_do_open;
-       sync_class->refresh_sync = caldav_refresh;
-
-       sync_class->create_objects_sync = caldav_create_objects;
-       sync_class->modify_objects_sync = caldav_modify_objects;
-       sync_class->remove_objects_sync = caldav_remove_objects;
-
-       sync_class->receive_objects_sync = caldav_receive_objects;
-       sync_class->send_objects_sync = caldav_send_objects;
-       sync_class->get_object_sync = caldav_get_object;
-       sync_class->get_object_list_sync = caldav_get_object_list;
-       sync_class->add_timezone_sync = caldav_add_timezone;
-       sync_class->get_free_busy_sync = caldav_get_free_busy;
 }
-
-static void
-e_caldav_backend_initable_init (GInitableIface *interface)
-{
-       interface->init = caldav_backend_initable_init;
-}
-
diff --git a/src/calendar/backends/caldav/e-cal-backend-caldav.h 
b/src/calendar/backends/caldav/e-cal-backend-caldav.h
index 3ea3273..3a8b639 100644
--- a/src/calendar/backends/caldav/e-cal-backend-caldav.h
+++ b/src/calendar/backends/caldav/e-cal-backend-caldav.h
@@ -47,12 +47,12 @@ typedef struct _ECalBackendCalDAVClass ECalBackendCalDAVClass;
 typedef struct _ECalBackendCalDAVPrivate ECalBackendCalDAVPrivate;
 
 struct _ECalBackendCalDAV {
-       ECalBackendSync parent;
+       ECalMetaBackend parent;
        ECalBackendCalDAVPrivate *priv;
 };
 
 struct _ECalBackendCalDAVClass {
-       ECalBackendSyncClass parent_class;
+       ECalMetaBackendClass parent_class;
 };
 
 GType          e_cal_backend_caldav_get_type   (void);
diff --git a/src/calendar/backends/gtasks/e-cal-backend-gtasks.c 
b/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
index 51f5372..fd2e964 100644
--- a/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
+++ b/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
@@ -26,238 +26,50 @@
 
 #define d(x)
 
-#define E_CAL_BACKEND_GTASKS_GET_PRIVATE(obj) \
-       (G_TYPE_INSTANCE_GET_PRIVATE \
-       ((obj), E_TYPE_CAL_BACKEND_GTASKS, ECalBackendGTasksPrivate))
-
 #define EDC_ERROR(_code) e_data_cal_create_error (_code, NULL)
 #define EDC_ERROR_EX(_code, _msg) e_data_cal_create_error (_code, _msg)
 
-#define GTASKS_KEY_LAST_UPDATED "last-updated"
-#define GTASKS_KEY_VERSION     "version"
+#define GTASKS_DEFAULT_TASKLIST_NAME "@default"
 #define X_EVO_GTASKS_SELF_LINK "X-EVOLUTION-GTASKS-SELF-LINK"
 
 /* Current data version; when doesn't match with the stored,
    then fetches everything again. */
-#define GTASKS_DATA_VERSION    "1"
-
-#define PROPERTY_LOCK(_gtasks) g_mutex_lock (&(_gtasks)->priv->property_mutex)
-#define PROPERTY_UNLOCK(_gtasks) g_mutex_unlock (&(_gtasks)->priv->property_mutex)
+#define GTASKS_DATA_VERSION    1
+#define GTASKS_DATA_VERSION_KEY        "gtasks-data-version"
 
 struct _ECalBackendGTasksPrivate {
        GDataAuthorizer *authorizer;
        GDataTasksService *service;
        GDataTasksTasklist *tasklist;
-
-       ECalBackendStore *store;
-       GCancellable *cancellable;
-       GMutex property_mutex;
-
-       guint refresh_id;
+       GHashTable *preloaded; /* gchar *uid ~> ECalComponent * */
 };
 
-G_DEFINE_TYPE (ECalBackendGTasks, e_cal_backend_gtasks, E_TYPE_CAL_BACKEND)
+G_DEFINE_TYPE (ECalBackendGTasks, e_cal_backend_gtasks, E_TYPE_CAL_META_BACKEND)
 
 static gboolean
-ecb_gtasks_check_data_version_locked (ECalBackendGTasks *gtasks)
+ecb_gtasks_check_data_version (ECalCache *cal_cache)
 {
 #ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
-       const gchar *key;
-       gboolean data_version_correct;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), FALSE);
-
-       key = e_cal_backend_store_get_key_value (gtasks->priv->store, GTASKS_KEY_VERSION);
-       data_version_correct = g_strcmp0 (key, GTASKS_DATA_VERSION) == 0;
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
 
-       return data_version_correct;
+       return GTASKS_DATA_VERSION == e_cache_get_key_int (E_CACHE (cal_cache), GTASKS_DATA_VERSION_KEY, 
NULL);
 #else
        return TRUE;
 #endif
 }
 
 static void
-ecb_gtasks_store_data_version_locked (ECalBackendGTasks *gtasks)
+ecb_gtasks_store_data_version (ECalCache *cal_cache)
 {
 #ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
-       e_cal_backend_store_put_key_value (gtasks->priv->store, GTASKS_KEY_VERSION, GTASKS_DATA_VERSION);
-#endif
-}
-
-static GCancellable *
-ecb_gtasks_ref_cancellable (ECalBackendGTasks *gtasks)
-{
-       GCancellable *cancellable = NULL;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), NULL);
-
-       PROPERTY_LOCK (gtasks);
-
-       if (gtasks->priv->cancellable)
-               cancellable = g_object_ref (gtasks->priv->cancellable);
-
-       PROPERTY_UNLOCK (gtasks);
-
-       return cancellable;
-}
-
-static void
-ecb_gtasks_take_cancellable (ECalBackendGTasks *gtasks,
-                            GCancellable *cancellable)
-{
-       GCancellable *old_cancellable;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-
-       PROPERTY_LOCK (gtasks);
-
-       old_cancellable = gtasks->priv->cancellable;
-       gtasks->priv->cancellable = cancellable;
-
-       PROPERTY_UNLOCK (gtasks);
-
-       if (old_cancellable) {
-               g_cancellable_cancel (old_cancellable);
-               g_clear_object (&old_cancellable);
-       }
-}
-
-static void
-ecb_gtasks_icomp_x_prop_set (icalcomponent *comp,
-                            const gchar *key,
-                            const gchar *value)
-{
-       icalproperty *xprop;
-
-       /* Find the old one first */
-       xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
-       while (xprop) {
-               const gchar *str = icalproperty_get_x_name (xprop);
-
-               if (!strcmp (str, key)) {
-                       if (value) {
-                               icalproperty_set_value_from_string (xprop, value, "NO");
-                       } else {
-                               icalcomponent_remove_property (comp, xprop);
-                               icalproperty_free (xprop);
-                       }
-                       break;
-               }
-
-               xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
-       }
-
-       if (!xprop && value) {
-               xprop = icalproperty_new_x (value);
-               icalproperty_set_x_name (xprop, key);
-               icalcomponent_add_property (comp, xprop);
-       }
-}
-
-static gchar *
-ecb_gtasks_icomp_x_prop_get (icalcomponent *comp,
-                            const gchar *key)
-{
-       icalproperty *xprop;
-
-       /* Find the old one first */
-       xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
-       while (xprop) {
-               const gchar *str = icalproperty_get_x_name (xprop);
-
-               if (!strcmp (str, key)) {
-                       break;
-               }
-
-               xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
-       }
-
-       if (xprop) {
-               return icalproperty_get_value_as_string_r (xprop);
-       }
-
-       return NULL;
-}
-
-/* May hold PROPERTY_LOCK() when calling this */
-static ECalComponent *
-ecb_gtasks_get_cached_comp (ECalBackendGTasks *gtasks,
-                           const gchar *uid)
-{
-       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), NULL);
-       g_return_val_if_fail (uid != NULL, NULL);
-
-       return e_cal_backend_store_get_component (gtasks->priv->store, uid, NULL);
-}
-
-static gboolean
-ecb_gtasks_is_authorized (ECalBackend *backend)
-{
-       ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (backend);
-
-       if (!gtasks->priv->service ||
-           !gtasks->priv->tasklist)
-               return FALSE;
-
-       return gdata_service_is_authorized (GDATA_SERVICE (gtasks->priv->service));
-}
-
-static void
-ecb_gtasks_prepare_tasklist (ECalBackendGTasks *gtasks,
-                            GCancellable *cancellable,
-                            GError **error)
-{
-       ESourceResource *resource;
-       ESource *source;
-       GDataFeed *feed;
-       GDataQuery *query;
-       gchar *id;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-       g_return_if_fail (gtasks->priv->service != NULL);
-       g_return_if_fail (gdata_service_is_authorized (GDATA_SERVICE (gtasks->priv->service)));
-
-       source = e_backend_get_source (E_BACKEND (gtasks));
-       resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
-       id = e_source_resource_dup_identity (resource);
-
-       query = gdata_query_new_with_limits (NULL, 0, 1);
-       /* This also verifies that the service can connect to the server with given credentials */
-       feed = gdata_tasks_service_query_all_tasklists (gtasks->priv->service, query, cancellable, NULL, 
NULL, error);
-       if (feed) {
-               /* If the tasklist ID is not set, then pick the first from the list, most likely the "Default 
List" */
-               if (!id || !*id) {
-                       GList *entries;
+       GError *local_error = NULL;
 
-                       entries = gdata_feed_get_entries (feed);
-                       if (entries) {
-                               GDataEntry *entry = entries->data;
-                               if (entry) {
-                                       g_free (id);
-                                       id = g_strdup (gdata_entry_get_id (entry));
-                               }
-                       }
-               }
-       }
-       g_clear_object (&feed);
-       g_object_unref (query);
+       g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
 
-       if (!id || !*id) {
-               /* But the tests for change will not work */
-               g_free (id);
-               id = g_strdup ("@default");
+       if (!e_cache_set_key_int (E_CACHE (cal_cache), GTASKS_DATA_VERSION_KEY, GTASKS_DATA_VERSION, 
&local_error)) {
+               g_warning ("%s: Failed to store data version: %s\n", G_STRFUNC, local_error ? 
local_error->message : "Unknown error");
        }
-
-       g_clear_object (&gtasks->priv->tasklist);
-
-       if (g_str_has_prefix (id, "gtasks::"))
-               gtasks->priv->tasklist = gdata_tasks_tasklist_new (id + 8);
-       else
-               gtasks->priv->tasklist = gdata_tasks_tasklist_new (id);
-
-       g_free (id);
+#endif
 }
 
 static void
@@ -348,7 +160,7 @@ ecb_gtasks_gdata_to_comp (GDataTasksTask *task)
 
        data_link = gdata_entry_look_up_link (entry, GDATA_LINK_SELF);
        if (data_link)
-               ecb_gtasks_icomp_x_prop_set (icomp, X_EVO_GTASKS_SELF_LINK, gdata_link_get_uri (data_link));
+               e_cal_util_set_x_property (icomp, X_EVO_GTASKS_SELF_LINK, gdata_link_get_uri (data_link));
 
        comp = e_cal_component_new_from_icalcomponent (icomp);
        g_warn_if_fail (comp != NULL);
@@ -358,7 +170,8 @@ ecb_gtasks_gdata_to_comp (GDataTasksTask *task)
 
 static GDataTasksTask *
 ecb_gtasks_comp_to_gdata (ECalComponent *comp,
-                         ECalComponent *cached_comp)
+                         ECalComponent *cached_comp,
+                         gboolean ignore_uid)
 {
        GDataEntry *entry;
        GDataTasksTask *task;
@@ -374,7 +187,7 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
        g_return_val_if_fail (icomp != NULL, NULL);
 
        text = icalcomponent_get_uid (icomp);
-       task = gdata_tasks_task_new (text && *text ? text : NULL);
+       task = gdata_tasks_task_new ((!ignore_uid && text && *text) ? text : NULL);
        entry = GDATA_ENTRY (task);
 
        tt = icalcomponent_get_due (icomp);
@@ -412,7 +225,7 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
        else if (icalcomponent_get_status (icomp) == ICAL_STATUS_NEEDSACTION)
                gdata_tasks_task_set_status (task, "needsAction");
 
-       tmp = ecb_gtasks_icomp_x_prop_get (icomp, X_EVO_GTASKS_SELF_LINK);
+       tmp = e_cal_util_dup_x_property (icomp, X_EVO_GTASKS_SELF_LINK);
        if (!tmp || !*tmp) {
                g_free (tmp);
                tmp = NULL;
@@ -420,7 +233,7 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
                /* If the passed-in component doesn't contain the libgdata self link,
                   then get it from the cached comp */
                if (cached_comp) {
-                       tmp = ecb_gtasks_icomp_x_prop_get (
+                       tmp = e_cal_util_dup_x_property (
                                e_cal_component_get_icalcomponent (cached_comp),
                                X_EVO_GTASKS_SELF_LINK);
                }
@@ -439,351 +252,146 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
        return task;
 }
 
-struct EGTasksUpdateData
+static gboolean
+ecb_gtasks_is_authorized (ECalBackendGTasks *cbgtasks)
 {
-       ECalBackendGTasks *gtasks;
-       gint64 taskslist_time;
-};
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cbgtasks), FALSE);
 
-static gpointer
-ecb_gtasks_update_thread (gpointer user_data)
-{
-       struct EGTasksUpdateData *update_data = user_data;
-       ECalBackendGTasks *gtasks;
-       GTimeVal last_updated;
-       GDataFeed *feed;
-       GDataTasksQuery *tasks_query;
-       const gchar *key;
-       GCancellable *cancellable;
-       GError *local_error = NULL;
-
-       g_return_val_if_fail (update_data != NULL, NULL);
-
-       gtasks = update_data->gtasks;
-
-       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), NULL);
-
-       if (!ecb_gtasks_is_authorized (E_CAL_BACKEND (gtasks))) {
-               g_clear_object (&gtasks);
-               g_free (update_data);
-               return NULL;
-       }
-
-       PROPERTY_LOCK (gtasks);
+       if (!cbgtasks->priv->service ||
+           !cbgtasks->priv->tasklist)
+               return FALSE;
 
-       if (ecb_gtasks_check_data_version_locked (gtasks)) {
-               key = e_cal_backend_store_get_key_value (gtasks->priv->store, GTASKS_KEY_LAST_UPDATED);
-               if (!key || !g_time_val_from_iso8601 (key, &last_updated))
-                       last_updated.tv_sec = 0;
-       } else {
-               last_updated.tv_sec = 0;
-       }
+       return gdata_service_is_authorized (GDATA_SERVICE (cbgtasks->priv->service));
+}
 
-       PROPERTY_UNLOCK (gtasks);
+static gboolean
+ecb_gtasks_request_authorization (ECalBackendGTasks *cbgtasks,
+                                 const ENamedParameters *credentials,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       /* Make sure we have the GDataService configured
+        * before requesting authorization. */
 
-       cancellable = ecb_gtasks_ref_cancellable (gtasks);
+       if (!cbgtasks->priv->authorizer) {
+               ESource *source;
+               EGDataOAuth2Authorizer *authorizer;
 
-       tasks_query = gdata_tasks_query_new (NULL);
-       gdata_query_set_max_results (GDATA_QUERY (tasks_query), 100);
-       gdata_tasks_query_set_show_completed (tasks_query, TRUE);
-       gdata_tasks_query_set_show_hidden (tasks_query, TRUE);
+               source = e_backend_get_source (E_BACKEND (cbgtasks));
 
-       if (last_updated.tv_sec > 0) {
-               gdata_query_set_updated_min (GDATA_QUERY (tasks_query), last_updated.tv_sec);
-               gdata_tasks_query_set_show_deleted (tasks_query, TRUE);
+               /* Only OAuth2 is supported with Google Tasks */
+               authorizer = e_gdata_oauth2_authorizer_new (source);
+               cbgtasks->priv->authorizer = GDATA_AUTHORIZER (authorizer);
        }
 
-       feed = gdata_tasks_service_query_tasks (gtasks->priv->service, gtasks->priv->tasklist,
-               GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
-
-       if (!local_error)
-               e_backend_ensure_source_status_connected (E_BACKEND (gtasks));
-
-#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
-       while (feed && !g_cancellable_is_cancelled (cancellable) && !local_error) {
-#else
-       if (feed) {
-#endif
-               GList *link;
-               const gchar *uid;
-
-               PROPERTY_LOCK (gtasks);
-
-               e_cal_backend_store_freeze_changes (gtasks->priv->store);
-
-               for (link = gdata_feed_get_entries (feed); link; link = g_list_next (link)) {
-                       GDataTasksTask *task = link->data;
-                       ECalComponent *cached_comp;
-
-                       if (!GDATA_IS_TASKS_TASK (task))
-                               continue;
-
-                       uid = gdata_entry_get_id (GDATA_ENTRY (task));
-                       if (!uid || !*uid)
-                               continue;
-
-                       cached_comp = ecb_gtasks_get_cached_comp (gtasks, uid);
-
-                       if (gdata_tasks_task_is_deleted (task)) {
-                               ECalComponentId id;
-
-                               id.uid = (gchar *) uid;
-                               id.rid = NULL;
-
-                               e_cal_backend_notify_component_removed ((ECalBackend *) gtasks, &id, 
cached_comp, NULL);
-                               if (cached_comp)
-                                       e_cal_backend_store_remove_component (gtasks->priv->store, uid, NULL);
-                       } else {
-                               ECalComponent *new_comp;
-
-                               new_comp = ecb_gtasks_gdata_to_comp (task);
-                               if (new_comp) {
-                                       if (cached_comp) {
-                                               struct icaltimetype *cached_tt = NULL, *new_tt = NULL;
-
-                                               e_cal_component_get_last_modified (cached_comp, &cached_tt);
-                                               e_cal_component_get_last_modified (new_comp, &new_tt);
-
-                                               if (!cached_tt || !new_tt ||
-                                                   icaltime_compare (*cached_tt, *new_tt) != 0) {
-                                                       /* Google doesn't store/provide 'created', thus use 
'created,
-                                                          as first seen by the backend' */
-                                                       if (cached_tt)
-                                                               e_cal_component_set_created (new_comp, 
cached_tt);
-
-                                                       e_cal_backend_store_put_component 
(gtasks->priv->store, new_comp);
-                                                       e_cal_backend_notify_component_modified ((ECalBackend 
*) gtasks, cached_comp, new_comp);
-                                               }
-
-                                               if (cached_tt)
-                                                       e_cal_component_free_icaltimetype (cached_tt);
-                                               if (new_tt)
-                                                       e_cal_component_free_icaltimetype (new_tt);
-                                       } else {
-                                               e_cal_backend_store_put_component (gtasks->priv->store, 
new_comp);
-                                               e_cal_backend_notify_component_created ((ECalBackend *) 
gtasks, new_comp);
-                                       }
-                               }
-
-                               g_clear_object (&new_comp);
-                       }
-
-                       g_clear_object (&cached_comp);
-               }
-
-               e_cal_backend_store_thaw_changes (gtasks->priv->store);
-
-               PROPERTY_UNLOCK (gtasks);
-
-#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
-               if (!gdata_feed_get_entries (feed))
-                       break;
-
-               gdata_query_next_page (GDATA_QUERY (tasks_query));
-
-               g_clear_object (&feed);
-
-               feed = gdata_tasks_service_query_tasks (gtasks->priv->service, gtasks->priv->tasklist,
-                       GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
-#endif
+       if (E_IS_GDATA_OAUTH2_AUTHORIZER (cbgtasks->priv->authorizer)) {
+               e_gdata_oauth2_authorizer_set_credentials (E_GDATA_OAUTH2_AUTHORIZER 
(cbgtasks->priv->authorizer), credentials);
        }
 
-       g_clear_object (&tasks_query);
-       g_clear_object (&feed);
-
-       if (!g_cancellable_is_cancelled (cancellable) && !local_error) {
-               gchar *strtm;
-
-               PROPERTY_LOCK (gtasks);
-
-               last_updated.tv_sec = update_data->taskslist_time;
-               last_updated.tv_usec = 0;
-
-               strtm = g_time_val_to_iso8601 (&last_updated);
-               e_cal_backend_store_put_key_value (gtasks->priv->store, GTASKS_KEY_LAST_UPDATED, strtm);
-               g_free (strtm);
+       if (!cbgtasks->priv->service) {
+               GDataTasksService *tasks_service;
 
-               ecb_gtasks_store_data_version_locked (gtasks);
+               tasks_service = gdata_tasks_service_new (cbgtasks->priv->authorizer);
+               cbgtasks->priv->service = tasks_service;
 
-               PROPERTY_UNLOCK (gtasks);
+               e_binding_bind_property (
+                       cbgtasks, "proxy-resolver",
+                       cbgtasks->priv->service, "proxy-resolver",
+                       G_BINDING_SYNC_CREATE);
        }
 
-       g_clear_object (&cancellable);
-       g_clear_object (&gtasks);
-       g_clear_error (&local_error);
-       g_free (update_data);
+       /* If we're using OAuth tokens, then as far as the backend
+        * is concerned it's always authorized.  The GDataAuthorizer
+        * will take care of everything in the background. */
+       if (!GDATA_IS_CLIENT_LOGIN_AUTHORIZER (cbgtasks->priv->authorizer))
+               return TRUE;
 
-       return NULL;
+       /* Otherwise it's up to us to obtain a login secret, but
+          there is currently no way to do it, thus simply fail. */
+       return FALSE;
 }
 
-static void
-ecb_gtasks_start_update (ECalBackendGTasks *gtasks)
+static gboolean
+ecb_gtasks_prepare_tasklist (ECalBackendGTasks *cbgtasks,
+                            GCancellable *cancellable,
+                            GError **error)
 {
+       ESourceResource *resource;
+       ESource *source;
        GDataFeed *feed;
-       GCancellable *cancellable;
+       GDataQuery *query;
+       gchar *id;
        GError *local_error = NULL;
-       gchar *id = NULL;
-       gint64 taskslist_time = 0;
-       gboolean changed = TRUE;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-
-       if (!ecb_gtasks_is_authorized ((ECalBackend *) gtasks))
-               return;
 
-       cancellable = ecb_gtasks_ref_cancellable (gtasks);
-       g_return_if_fail (cancellable != NULL);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cbgtasks), FALSE);
+       g_return_val_if_fail (cbgtasks->priv->service != NULL, FALSE);
+       g_return_val_if_fail (gdata_service_is_authorized (GDATA_SERVICE (cbgtasks->priv->service)), FALSE);
 
-       g_object_get (gtasks->priv->tasklist, "id", &id, NULL);
-       g_return_if_fail (id != NULL);
-
-       /* Check whether the tasklist changed */
-       feed = gdata_tasks_service_query_all_tasklists (gtasks->priv->service, NULL, cancellable, NULL, NULL, 
&local_error);
+       source = e_backend_get_source (E_BACKEND (cbgtasks));
+       resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+       id = e_source_resource_dup_identity (resource);
 
-       if (!local_error)
-               e_backend_ensure_source_status_connected (E_BACKEND (gtasks));
+       query = gdata_query_new_with_limits (NULL, 0, 1);
 
+       /* This also verifies that the service can connect to the server with given credentials */
+       feed = gdata_tasks_service_query_all_tasklists (cbgtasks->priv->service, query, cancellable, NULL, 
NULL, &local_error);
        if (feed) {
-               GList *link;
-
-               for (link = gdata_feed_get_entries (feed); link; link = g_list_next (link)) {
-                       GDataEntry *entry = link->data;
-
-                       if (entry && g_strcmp0 (id, gdata_entry_get_id (entry)) == 0) {
-                               taskslist_time = gdata_entry_get_updated (entry);
-
-                               if (taskslist_time > 0) {
-                                       PROPERTY_LOCK (gtasks);
-
-                                       if (ecb_gtasks_check_data_version_locked (gtasks)) {
-                                               GTimeVal stored;
-                                               const gchar *key;
-
-                                               key = e_cal_backend_store_get_key_value (gtasks->priv->store, 
GTASKS_KEY_LAST_UPDATED);
-                                               if (key && g_time_val_from_iso8601 (key, &stored))
-                                                       changed = taskslist_time != stored.tv_sec;
-                                       }
+               /* If the tasklist ID is not set, then pick the first from the list, most likely the "Default 
List" */
+               if (!id || !*id) {
+                       GList *entries;
 
-                                       PROPERTY_UNLOCK (gtasks);
+                       entries = gdata_feed_get_entries (feed);
+                       if (entries) {
+                               GDataEntry *entry = entries->data;
+                               if (entry) {
+                                       g_free (id);
+                                       id = g_strdup (gdata_entry_get_id (entry));
                                }
-
-                               break;
                        }
                }
-
-               g_clear_object (&feed);
        }
+       g_clear_object (&feed);
+       g_object_unref (query);
 
-       if (changed && !g_cancellable_is_cancelled (cancellable)) {
-               GThread *thread;
-               struct EGTasksUpdateData *data;
-
-               data = g_new0 (struct EGTasksUpdateData, 1);
-               data->gtasks = g_object_ref (gtasks);
-               data->taskslist_time = taskslist_time;
-
-               thread = g_thread_new (NULL, ecb_gtasks_update_thread, data);
-               g_thread_unref (thread);
+       if (!id || !*id) {
+               /* But the tests for change will not work */
+               g_free (id);
+               id = g_strdup (GTASKS_DEFAULT_TASKLIST_NAME);
        }
 
-       if (local_error) {
-               g_debug ("%s: Failed to get all tasklists: %s", G_STRFUNC, local_error->message);
-               g_clear_error (&local_error);
-       }
+       g_clear_object (&cbgtasks->priv->tasklist);
+       cbgtasks->priv->tasklist = gdata_tasks_tasklist_new (id);
 
-       g_clear_object (&cancellable);
        g_free (id);
-}
-
-static void
-ecb_gtasks_time_to_refresh_data_cb (ESource *source,
-                                   gpointer user_data)
-{
-       ECalBackendGTasks *gtasks = user_data;
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-
-       if (!ecb_gtasks_is_authorized (E_CAL_BACKEND (gtasks)) ||
-           !e_backend_get_online (E_BACKEND (gtasks))) {
-               return;
-       }
-
-       ecb_gtasks_start_update (gtasks);
-}
-
-static gboolean
-ecb_gtasks_request_authorization (ECalBackend *backend,
-                                 const ENamedParameters *credentials,
-                                 GCancellable *cancellable,
-                                 GError **error)
-{
-       ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (backend);
-
-       /* Make sure we have the GDataService configured
-        * before requesting authorization. */
-
-       if (!gtasks->priv->authorizer) {
-               ESource *source;
-               EGDataOAuth2Authorizer *authorizer;
-
-               source = e_backend_get_source (E_BACKEND (backend));
-
-               /* Only OAuth2 is supported with Google Tasks */
-               authorizer = e_gdata_oauth2_authorizer_new (source);
-               gtasks->priv->authorizer = GDATA_AUTHORIZER (authorizer);
-       }
-
-       if (E_IS_GDATA_OAUTH2_AUTHORIZER (gtasks->priv->authorizer)) {
-               e_gdata_oauth2_authorizer_set_credentials (E_GDATA_OAUTH2_AUTHORIZER 
(gtasks->priv->authorizer), credentials);
-       }
-
-       if (!gtasks->priv->service) {
-               GDataTasksService *tasks_service;
-
-               tasks_service = gdata_tasks_service_new (gtasks->priv->authorizer);
-               gtasks->priv->service = tasks_service;
-
-               e_binding_bind_property (
-                       backend, "proxy-resolver",
-                       gtasks->priv->service, "proxy-resolver",
-                       G_BINDING_SYNC_CREATE);
+       if (local_error) {
+               g_propagate_error (error, local_error);
+               return FALSE;
        }
 
-       /* If we're using OAuth tokens, then as far as the backend
-        * is concerned it's always authorized.  The GDataAuthorizer
-        * will take care of everything in the background. */
-       if (!GDATA_IS_CLIENT_LOGIN_AUTHORIZER (gtasks->priv->authorizer))
-               return TRUE;
-
-       /* Otherwise it's up to us to obtain a login secret, but
-          there is currently no way to do it, thus simply fail. */
-       return FALSE;
+       return TRUE;
 }
 
 static gchar *
-ecb_gtasks_get_backend_property (ECalBackend *backend,
+ecb_gtasks_get_backend_property (ECalBackend *cal_backend,
                                 const gchar *prop_name)
 {
-       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (backend), NULL);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cal_backend), NULL);
        g_return_val_if_fail (prop_name != NULL, NULL);
 
        if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
-               GString *caps;
-
-               caps = g_string_new (
-                       CAL_STATIC_CAPABILITY_NO_THISANDFUTURE ","
-                       CAL_STATIC_CAPABILITY_NO_THISANDPRIOR ","
-                       CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED);
-
-               return g_string_free (caps, FALSE);
-
+               return g_strjoin (",",
+                       CAL_STATIC_CAPABILITY_NO_THISANDFUTURE,
+                       CAL_STATIC_CAPABILITY_NO_THISANDPRIOR,
+                       e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cal_backend)),
+                       NULL);
        } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
                   g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
                ESourceAuthentication *authentication;
                ESource *source;
                const gchar *user;
 
-               source = e_backend_get_source (E_BACKEND (backend));
+               source = e_backend_get_source (E_BACKEND (cal_backend));
                authentication = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
                user = e_source_authentication_get_user (authentication);
 
@@ -791,821 +399,612 @@ ecb_gtasks_get_backend_property (ECalBackend *backend,
                        return NULL;
 
                return g_strdup (user);
-
-       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
-               ECalComponent *comp;
-               gchar *prop_value;
-
-               comp = e_cal_component_new ();
-               e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_TODO);
-
-               prop_value = e_cal_component_get_as_string (comp);
-
-               g_object_unref (comp);
-
-               return prop_value;
        }
 
        /* Chain up to parent's method. */
-       return E_CAL_BACKEND_CLASS (e_cal_backend_gtasks_parent_class)->get_backend_property (backend, 
prop_name);
+       return E_CAL_BACKEND_CLASS (e_cal_backend_gtasks_parent_class)->get_backend_property (cal_backend, 
prop_name);
 }
 
-static void
-ecb_gtasks_update_connection_sync (ECalBackendGTasks *gtasks,
-                                  const ENamedParameters *credentials,
-                                  GCancellable *cancellable,
-                                  GError **error)
+static gboolean
+ecb_gtasks_connect_sync (ECalMetaBackend *meta_backend,
+                        const ENamedParameters *credentials,
+                        ESourceAuthenticationResult *out_auth_result,
+                        gchar **out_certificate_pem,
+                        GTlsCertificateFlags *out_certificate_errors,
+                        GCancellable *cancellable,
+                        GError **error)
 {
-       ECalBackend *backend;
+       ECalBackendGTasks *cbgtasks;
        gboolean success;
        GError *local_error = NULL;
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
 
-       backend = E_CAL_BACKEND (gtasks);
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
 
-       success = ecb_gtasks_request_authorization (backend, credentials, cancellable, &local_error);
-       if (success)
-               success = gdata_authorizer_refresh_authorization (gtasks->priv->authorizer, cancellable, 
&local_error);
+       *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
 
-       if (success) {
-               e_cal_backend_set_writable (backend, TRUE);
+       if (ecb_gtasks_is_authorized (cbgtasks))
+               return TRUE;
 
-               ecb_gtasks_prepare_tasklist (gtasks, cancellable, &local_error);
-               if (!local_error)
-                       ecb_gtasks_start_update (gtasks);
-       } else {
-               e_cal_backend_set_writable (backend, FALSE);
+       success = ecb_gtasks_request_authorization (cbgtasks, credentials, cancellable, &local_error);
+       if (success)
+               success = gdata_authorizer_refresh_authorization (cbgtasks->priv->authorizer, cancellable, 
&local_error);
+       if (success)
+               success = ecb_gtasks_prepare_tasklist (cbgtasks, cancellable, &local_error);
+
+       if (!success) {
+               if (g_error_matches (local_error, GDATA_SERVICE_ERROR, 
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
+                       if (!e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       else
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+                       g_clear_error (&local_error);
+               } else {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+                       g_propagate_error (error, local_error);
+               }
        }
 
-       if (local_error)
-               g_propagate_error (error, local_error);
+       return success;
 }
 
-static ESourceAuthenticationResult
-ecb_gtasks_authenticate_sync (EBackend *backend,
-                             const ENamedParameters *credentials,
-                             gchar **out_certificate_pem,
-                             GTlsCertificateFlags *out_certificate_errors,
-                             GCancellable *cancellable,
-                             GError **error)
+static gboolean
+ecb_gtasks_disconnect_sync (ECalMetaBackend *meta_backend,
+                           GCancellable *cancellable,
+                           GError **error)
 {
-       ECalBackendGTasks *gtasks;
-       ESourceAuthenticationResult result;
-       GError *local_error = NULL;
+       ECalBackendGTasks *cbgtasks;
 
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
 
-       ecb_gtasks_update_connection_sync (gtasks, credentials, cancellable, &local_error);
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
 
-       if (local_error == NULL) {
-               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+       g_clear_object (&cbgtasks->priv->service);
+       g_clear_object (&cbgtasks->priv->authorizer);
+       g_clear_object (&cbgtasks->priv->tasklist);
 
-       } else if (g_error_matches (local_error, GDATA_SERVICE_ERROR, 
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
-               if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
-                       result = E_SOURCE_AUTHENTICATION_REQUIRED;
-               else
-                       result = E_SOURCE_AUTHENTICATION_REJECTED;
-               g_clear_error (&local_error);
-       } else {
-               result = E_SOURCE_AUTHENTICATION_ERROR;
-               g_propagate_error (error, local_error);
-       }
-
-       return result;
+       return TRUE;
 }
 
-static void
-ecb_gtasks_open (ECalBackend *backend,
-                EDataCal *cal,
-                guint32 opid,
-                GCancellable *cancellable,
-                gboolean only_if_exists)
+static gboolean
+ecb_gtasks_check_tasklist_changed_sync (ECalBackendGTasks *cbgtasks,
+                                       const gchar *last_sync_tag,
+                                       gboolean *out_changed,
+                                       gint64 *out_taskslist_time,
+                                       GCancellable *cancellable,
+                                       GError **error)
 {
-       ECalBackendGTasks *gtasks;
+       GDataFeed *feed;
+       gchar *id = NULL;
+       gint64 taskslist_time = 0;
        GError *local_error = NULL;
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cbgtasks), FALSE);
+       g_return_val_if_fail (out_changed != NULL, FALSE);
+       g_return_val_if_fail (out_taskslist_time != NULL, FALSE);
 
-       if (ecb_gtasks_is_authorized (backend)) {
-               e_data_cal_respond_open (cal, opid, NULL);
-               return;
-       }
+       *out_changed = TRUE;
+       *out_taskslist_time = 0;
 
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
+       g_object_get (cbgtasks->priv->tasklist, "id", &id, NULL);
+       g_return_val_if_fail (id != NULL, FALSE);
 
-       e_cal_backend_set_writable (backend, FALSE);
+       /* Check whether the tasklist changed */
+       feed = gdata_tasks_service_query_all_tasklists (cbgtasks->priv->service, NULL, cancellable, NULL, 
NULL, &local_error);
 
-       ecb_gtasks_take_cancellable (gtasks, g_cancellable_new ());
+       if (local_error) {
+               g_propagate_error (error, local_error);
+               return FALSE;
+       }
 
-       if (e_backend_get_online (E_BACKEND (backend))) {
-               ESource *source;
-               gchar *auth_method = NULL;
+       if (feed) {
+               GList *link;
 
-               source = e_backend_get_source (E_BACKEND (backend));
+               for (link = gdata_feed_get_entries (feed); link; link = g_list_next (link)) {
+                       GDataEntry *entry = link->data;
 
-               if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
-                       ESourceAuthentication *auth_extension;
+                       if (entry && g_strcmp0 (id, gdata_entry_get_id (entry)) == 0) {
+                               ECalCache *cal_cache;
 
-                       auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
-                       auth_method = e_source_authentication_dup_method (auth_extension);
-               }
+                               cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbgtasks));
+                               taskslist_time = gdata_entry_get_updated (entry);
 
-               if (g_strcmp0 (auth_method, "Google") == 0) {
-                       e_backend_credentials_required_sync (
-                               E_BACKEND (backend), E_SOURCE_CREDENTIALS_REASON_REQUIRED,
-                               NULL, 0, NULL, cancellable, &local_error);
-               } else {
-                       ecb_gtasks_update_connection_sync (gtasks, NULL, cancellable, &local_error);
-               }
+                               if (taskslist_time > 0 && last_sync_tag && ecb_gtasks_check_data_version 
(cal_cache)) {
+                                       GTimeVal stored;
 
-               g_free (auth_method);
-       }
+                                       if (g_time_val_from_iso8601 (last_sync_tag, &stored))
+                                               *out_changed = taskslist_time != stored.tv_sec;
+                               }
 
-       e_data_cal_respond_open (cal, opid, local_error);
-}
+                               g_clear_object (&cal_cache);
 
-static void
-ecb_gtasks_refresh (ECalBackend *backend,
-                   EDataCal *cal,
-                   guint32 opid,
-                   GCancellable *cancellable)
-{
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+                               break;
+                       }
+               }
 
-       if (!ecb_gtasks_is_authorized (backend) ||
-           !e_backend_get_online (E_BACKEND (backend))) {
-               e_data_cal_respond_refresh (cal, opid, EDC_ERROR (RepositoryOffline));
-               return;
+               g_clear_object (&feed);
        }
 
-       ecb_gtasks_start_update (E_CAL_BACKEND_GTASKS (backend));
+       g_free (id);
 
-       e_data_cal_respond_refresh (cal, opid, NULL);
+       *out_taskslist_time = taskslist_time;
+
+       return TRUE;
 }
 
-static void
-ecb_gtasks_get_object (ECalBackend *backend,
-                      EDataCal *cal,
-                      guint32 opid,
-                      GCancellable *cancellable,
-                      const gchar *uid,
-                      const gchar *rid)
+static gboolean
+ecb_gtasks_get_changes_sync (ECalMetaBackend *meta_backend,
+                            const gchar *last_sync_tag,
+                            gboolean is_repeat,
+                            gchar **out_new_sync_tag,
+                            gboolean *out_repeat,
+                            GSList **out_created_objects, /* ECalMetaBackendInfo * */
+                            GSList **out_modified_objects, /* ECalMetaBackendInfo * */
+                            GSList **out_removed_objects, /* ECalMetaBackendInfo * */
+                            GCancellable *cancellable,
+                            GError **error)
 {
-       ECalBackendGTasks *gtasks;
-       ECalComponent *cached_comp;
-       gchar *comp_str = NULL;
+       ECalBackendGTasks *cbgtasks;
+       ECalCache *cal_cache;
+       gint64 taskslist_time = 0;
+       GTimeVal last_updated;
+       GDataFeed *feed;
+       GDataTasksQuery *tasks_query;
+       gboolean changed = TRUE;
        GError *local_error = NULL;
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
-
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
 
-       PROPERTY_LOCK (gtasks);
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
 
-       cached_comp = ecb_gtasks_get_cached_comp (gtasks, uid);
-       if (cached_comp)
-               comp_str = e_cal_component_get_as_string (cached_comp);
-       else
-               local_error = EDC_ERROR (ObjectNotFound);
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
 
-       PROPERTY_UNLOCK (gtasks);
+       if (!ecb_gtasks_check_tasklist_changed_sync (cbgtasks, last_sync_tag, &changed, &taskslist_time, 
cancellable, error))
+               return FALSE;
 
-       e_data_cal_respond_get_object (cal, opid, local_error, comp_str);
+       if (!changed)
+               return TRUE;
 
-       g_clear_object (&cached_comp);
-       g_free (comp_str);
-}
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
 
-static void
-ecb_gtasks_get_object_list (ECalBackend *backend,
-                           EDataCal *cal,
-                           guint32 opid,
-                           GCancellable *cancellable,
-                           const gchar *sexp_str)
-{
-       ECalBackendGTasks *gtasks;
-       ECalBackendSExp *sexp;
-       ETimezoneCache *cache;
-       gboolean do_search;
-       GSList *list, *iter, *calobjs = NULL;
-       time_t occur_start = -1, occur_end = -1;
-       gboolean prunning_by_time;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
-
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
-
-       sexp = e_cal_backend_sexp_new (sexp_str);
-       if (sexp == NULL) {
-               e_data_cal_respond_get_object_list (cal, opid, EDC_ERROR (InvalidQuery), NULL);
-               return;
+       if (!ecb_gtasks_check_data_version (cal_cache) ||
+           !last_sync_tag ||
+           !g_time_val_from_iso8601 (last_sync_tag, &last_updated)) {
+               last_updated.tv_sec = 0;
        }
 
-       do_search = !g_str_equal (sexp_str, "#t");
-       prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (sexp, &occur_start, &occur_end);
-
-       cache = E_TIMEZONE_CACHE (backend);
-
-       PROPERTY_LOCK (gtasks);
-
-       list = prunning_by_time ?
-               e_cal_backend_store_get_components_occuring_in_range (gtasks->priv->store, occur_start, 
occur_end)
-               : e_cal_backend_store_get_components (gtasks->priv->store);
-
-       PROPERTY_UNLOCK (gtasks);
-
-       for (iter = list; iter; iter = g_slist_next (iter)) {
-               ECalComponent *comp = E_CAL_COMPONENT (iter->data);
-
-               if (!do_search || e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
-                       calobjs = g_slist_prepend (calobjs, e_cal_component_get_as_string (comp));
-               }
+       tasks_query = gdata_tasks_query_new (NULL);
+       gdata_query_set_max_results (GDATA_QUERY (tasks_query), 100);
+       gdata_tasks_query_set_show_completed (tasks_query, TRUE);
+       gdata_tasks_query_set_show_hidden (tasks_query, TRUE);
 
-               g_object_unref (comp);
+       if (last_updated.tv_sec > 0) {
+               gdata_query_set_updated_min (GDATA_QUERY (tasks_query), last_updated.tv_sec);
+               gdata_tasks_query_set_show_deleted (tasks_query, TRUE);
        }
 
-       g_object_unref (sexp);
-       g_slist_free (list);
-
-       e_data_cal_respond_get_object_list (cal, opid, NULL, calobjs);
+       feed = gdata_tasks_service_query_tasks (cbgtasks->priv->service, cbgtasks->priv->tasklist,
+               GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
 
-       g_slist_foreach (calobjs, (GFunc) g_free, NULL);
-       g_slist_free (calobjs);
-}
+#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
+       while (feed && !g_cancellable_is_cancelled (cancellable) && !local_error) {
+#else
+       if (feed) {
+#endif
+               GList *link;
 
-static void
-ecb_gtasks_get_free_busy (ECalBackend *backend,
-                         EDataCal *cal,
-                         guint32 opid,
-                         GCancellable *cancellable,
-                         const GSList *users,
-                         time_t start,
-                         time_t end)
-{
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+               for (link = gdata_feed_get_entries (feed); link && !g_cancellable_is_cancelled (cancellable); 
link = g_list_next (link)) {
+                       GDataTasksTask *task = link->data;
+                       ECalComponent *cached_comp = NULL;
+                       gchar *uid;
 
-       e_data_cal_respond_get_free_busy (cal, opid, EDC_ERROR (NotSupported), NULL);
-}
+                       if (!GDATA_IS_TASKS_TASK (task))
+                               continue;
 
-static void
-ecb_gtasks_create_objects (ECalBackend *backend,
-                          EDataCal *cal,
-                          guint32 opid,
-                          GCancellable *cancellable,
-                          const GSList *calobjs)
-{
-       ECalBackendGTasks *gtasks;
-       GSList *new_uids = NULL, *new_calcomps = NULL;
-       const GSList *link;
-       GError *local_error = NULL;
+                       uid = g_strdup (gdata_entry_get_id (GDATA_ENTRY (task)));
+                       if (!uid || !*uid) {
+                               g_free (uid);
+                               continue;
+                       }
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+                       if (!e_cal_cache_get_component (cal_cache, uid, NULL, &cached_comp, cancellable, 
NULL))
+                               cached_comp = NULL;
 
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
+                       if (gdata_tasks_task_is_deleted (task)) {
+                               *out_removed_objects = g_slist_prepend (*out_removed_objects,
+                                       e_cal_meta_backend_info_new (uid, NULL, NULL, NULL));
+                       } else {
+                               ECalComponent *new_comp;
 
-       if (!ecb_gtasks_is_authorized (backend) ||
-           !e_backend_get_online (E_BACKEND (backend))) {
-               e_data_cal_respond_create_objects (cal, opid, EDC_ERROR (RepositoryOffline), NULL, NULL);
-               return;
-       }
+                               new_comp = ecb_gtasks_gdata_to_comp (task);
+                               if (new_comp) {
+                                       gchar *revision, *object;
 
-       for (link = calobjs; link && !local_error; link = link->next) {
-               const gchar *icalstr = link->data;
-               ECalComponent *comp;
-               icalcomponent *icomp;
-               const gchar *uid;
-               GDataTasksTask *new_task, *comp_task;
+                                       revision = e_cal_cache_dup_component_revision (cal_cache, 
e_cal_component_get_icalcomponent (new_comp));
+                                       object = e_cal_component_get_as_string (new_comp);
 
-               if (!icalstr) {
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+                                       if (cached_comp) {
+                                               struct icaltimetype *cached_tt = NULL, *new_tt = NULL;
 
-               comp = e_cal_component_new_from_string (icalstr);
-               if (comp == NULL) {
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+                                               e_cal_component_get_last_modified (cached_comp, &cached_tt);
+                                               e_cal_component_get_last_modified (new_comp, &new_tt);
 
-               icomp = e_cal_component_get_icalcomponent (comp);
-               if (!icomp) {
-                       g_object_unref (comp);
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+                                               if (!cached_tt || !new_tt ||
+                                                   icaltime_compare (*cached_tt, *new_tt) != 0) {
+                                                       /* Google doesn't store/provide 'created', thus use 
'created,
+                                                          as first seen by the backend' */
+                                                       if (cached_tt)
+                                                               e_cal_component_set_created (new_comp, 
cached_tt);
 
-               uid = icalcomponent_get_uid (icomp);
-               if (uid) {
-                       PROPERTY_LOCK (gtasks);
+                                                       *out_modified_objects = g_slist_prepend 
(*out_modified_objects,
+                                                               e_cal_meta_backend_info_new (uid, revision, 
object, NULL));
+                                               }
 
-                       if (e_cal_backend_store_has_component (gtasks->priv->store, uid, NULL)) {
-                               PROPERTY_UNLOCK (gtasks);
-                               g_object_unref (comp);
-                               local_error = EDC_ERROR (ObjectIdAlreadyExists);
-                               break;
-                       }
+                                               if (cached_tt)
+                                                       e_cal_component_free_icaltimetype (cached_tt);
+                                               if (new_tt)
+                                                       e_cal_component_free_icaltimetype (new_tt);
+                                       } else {
+                                               *out_created_objects = g_slist_prepend (*out_created_objects,
+                                                       e_cal_meta_backend_info_new (uid, revision, object, 
NULL));
+                                       }
 
-                       PROPERTY_UNLOCK (gtasks);
+                                       g_free (revision);
+                                       g_free (object);
+                               }
 
-                       icalcomponent_set_uid (icomp, "");
-               }
+                               g_clear_object (&new_comp);
+                       }
 
-               comp_task = ecb_gtasks_comp_to_gdata (comp, NULL);
-               if (!comp_task) {
-                       g_object_unref (comp);
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
+                       g_clear_object (&cached_comp);
+                       g_free (uid);
                }
 
-               new_task = gdata_tasks_service_insert_task (gtasks->priv->service, comp_task, 
gtasks->priv->tasklist, cancellable, &local_error);
-
-               g_object_unref (comp_task);
-               g_object_unref (comp);
-
-               if (!new_task)
+#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
+               if (!gdata_feed_get_entries (feed))
                        break;
 
-               comp = ecb_gtasks_gdata_to_comp (new_task);
-               g_object_unref (new_task);
+               gdata_query_next_page (GDATA_QUERY (tasks_query));
 
-               if (!comp) {
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+               g_clear_object (&feed);
 
-               icomp = e_cal_component_get_icalcomponent (comp);
-               uid = icalcomponent_get_uid (icomp);
+               feed = gdata_tasks_service_query_tasks (cbgtasks->priv->service, cbgtasks->priv->tasklist,
+                       GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
+#endif
+       }
 
-               if (!uid) {
-                       g_object_unref (comp);
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+       g_clear_object (&tasks_query);
+       g_clear_object (&feed);
 
-               PROPERTY_LOCK (gtasks);
-               e_cal_backend_store_put_component (gtasks->priv->store, comp);
-               PROPERTY_UNLOCK (gtasks);
+       if (!g_cancellable_is_cancelled (cancellable) && !local_error) {
+               last_updated.tv_sec = taskslist_time;
+               last_updated.tv_usec = 0;
 
-               e_cal_backend_notify_component_created (backend, comp);
+               *out_new_sync_tag = g_time_val_to_iso8601 (&last_updated);
 
-               new_uids = g_slist_prepend (new_uids, g_strdup (uid));
-               new_calcomps = g_slist_prepend (new_calcomps, comp);
+               ecb_gtasks_store_data_version (cal_cache);
        }
 
-       new_uids = g_slist_reverse (new_uids);
-       new_calcomps = g_slist_reverse (new_calcomps);
+       g_clear_object (&cal_cache);
 
-       e_data_cal_respond_create_objects (cal, opid, local_error, new_uids, new_calcomps);
+       if (local_error) {
+               g_propagate_error (error, local_error);
+               return FALSE;
+       }
 
-       g_slist_free_full (new_uids, g_free);
-       e_util_free_nullable_object_slist (new_calcomps);
+       return TRUE;
 }
 
-static void
-ecb_gtasks_modify_objects (ECalBackend *backend,
-                          EDataCal *cal,
-                          guint32 opid,
-                          GCancellable *cancellable,
-                          const GSList *calobjs,
-                          ECalObjModType mod)
+static gboolean
+ecb_gtasks_load_component_sync (ECalMetaBackend *meta_backend,
+                               const gchar *uid,
+                               const gchar *extra,
+                               icalcomponent **out_instances,
+                               gchar **out_extra,
+                               GCancellable *cancellable,
+                               GError **error)
 {
-       ECalBackendGTasks *gtasks;
-       GSList *old_calcomps = NULL, *new_calcomps = NULL;
-       const GSList *link;
-       GError *local_error = NULL;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
-
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
-
-       if (!ecb_gtasks_is_authorized (backend) ||
-           !e_backend_get_online (E_BACKEND (backend))) {
-               e_data_cal_respond_modify_objects (cal, opid, EDC_ERROR (RepositoryOffline), NULL, NULL);
-               return;
-       }
+       ECalBackendGTasks *cbgtasks;
 
-       for (link = calobjs; link && !local_error; link = link->next) {
-               const gchar *icalstr = link->data;
-               ECalComponent *comp, *cached_comp;
-               icalcomponent *icomp;
-               const gchar *uid;
-               GDataTasksTask *new_task, *comp_task;
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_instances != NULL, FALSE);
 
-               if (!icalstr) {
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
-
-               comp = e_cal_component_new_from_string (icalstr);
-               if (comp == NULL) {
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
-
-               icomp = e_cal_component_get_icalcomponent (comp);
-               if (!icomp) {
-                       g_object_unref (comp);
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
-
-               uid = icalcomponent_get_uid (icomp);
-               if (!uid) {
-                       g_object_unref (comp);
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
 
-               PROPERTY_LOCK (gtasks);
+       /* Only "load" preloaded during save, otherwise fail with an error,
+          because the backend provides objects within get_changes_sync() */
 
-               cached_comp = ecb_gtasks_get_cached_comp (gtasks, uid);
-
-               PROPERTY_UNLOCK (gtasks);
-
-               if (!cached_comp) {
-                       g_object_unref (comp);
-                       local_error = EDC_ERROR (ObjectNotFound);
-                       break;
-               }
-
-               comp_task = ecb_gtasks_comp_to_gdata (comp, cached_comp);
-               g_object_unref (comp);
+       if (cbgtasks->priv->preloaded) {
+               ECalComponent *comp;
 
-               if (!comp_task) {
-                       g_object_unref (cached_comp);
-                       local_error = EDC_ERROR (ObjectNotFound);
-                       break;
-               }
+               comp = g_hash_table_lookup (cbgtasks->priv->preloaded, uid);
+               if (comp) {
+                       icalcomponent *icalcomp;
 
-               new_task = gdata_tasks_service_update_task (gtasks->priv->service, comp_task, cancellable, 
&local_error);
-               g_object_unref (comp_task);
+                       icalcomp = e_cal_component_get_icalcomponent (comp);
+                       if (icalcomp)
+                               *out_instances = icalcomponent_new_clone (icalcomp);
 
-               if (!local_error)
-                       e_backend_ensure_source_status_connected (E_BACKEND (backend));
+                       g_hash_table_remove (cbgtasks->priv->preloaded, uid);
 
-               if (!new_task) {
-                       g_object_unref (cached_comp);
-                       break;
+                       if (icalcomp)
+                               return TRUE;
                }
-
-               comp = ecb_gtasks_gdata_to_comp (new_task);
-               g_object_unref (new_task);
-
-               PROPERTY_LOCK (gtasks);
-               e_cal_backend_store_put_component (gtasks->priv->store, comp);
-               PROPERTY_UNLOCK (gtasks);
-
-               e_cal_backend_notify_component_modified (backend, cached_comp, comp);
-
-               old_calcomps = g_slist_prepend (old_calcomps, cached_comp);
-               new_calcomps = g_slist_prepend (new_calcomps, comp);
        }
 
-       old_calcomps = g_slist_reverse (old_calcomps);
-       new_calcomps = g_slist_reverse (new_calcomps);
-
-       e_data_cal_respond_modify_objects (cal, opid, local_error, old_calcomps, new_calcomps);
+       g_propagate_error (error, EDC_ERROR (ObjectNotFound));
 
-       e_util_free_nullable_object_slist (old_calcomps);
-       e_util_free_nullable_object_slist (new_calcomps);
+       return FALSE;
 }
 
-static void
-ecb_gtasks_remove_objects (ECalBackend *backend,
-                          EDataCal *cal,
-                          guint32 opid,
-                          GCancellable *cancellable,
-                          const GSList *ids,
-                          ECalObjModType mod)
+static gboolean
+ecb_gtasks_save_component_sync (ECalMetaBackend *meta_backend,
+                               gboolean overwrite_existing,
+                               EConflictResolution conflict_resolution,
+                               const GSList *instances, /* ECalComponent * */
+                               const gchar *extra,
+                               gchar **out_new_uid,
+                               gchar **out_new_extra,
+                               GCancellable *cancellable,
+                               GError **error)
 {
-       ECalBackendGTasks *gtasks;
-       GSList *old_calcomps = NULL, *removed_ids = NULL;
-       const GSList *link;
-       GError *local_error = NULL;
+       ECalBackendGTasks *cbgtasks;
+       ECalCache *cal_cache;
+       GDataTasksTask *new_task, *comp_task;
+       ECalComponent *comp, *cached_comp = NULL;
+       icalcomponent *icalcomp;
+       const gchar *uid;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
 
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
 
-       if (!ecb_gtasks_is_authorized (backend) ||
-           !e_backend_get_online (E_BACKEND (backend))) {
-               e_data_cal_respond_remove_objects (cal, opid, EDC_ERROR (RepositoryOffline), NULL, NULL, 
NULL);
-               return;
+       if (g_slist_length ((GSList *) instances) != 1) {
+               g_propagate_error (error, EDC_ERROR (InvalidArg));
+               g_clear_object (&cal_cache);
+               return FALSE;
        }
 
-       for (link = ids; link; link = link->next) {
-               const ECalComponentId *id = link->data;
-               ECalComponentId *tmp_id;
-               ECalComponent *cached_comp;
-               GDataTasksTask *task;
+       comp = instances->data;
 
-               if (!id || !id->uid) {
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
+       if (!comp) {
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               g_clear_object (&cal_cache);
+               return FALSE;
+       }
 
-               PROPERTY_LOCK (gtasks);
-               cached_comp = ecb_gtasks_get_cached_comp (gtasks, id->uid);
-               PROPERTY_UNLOCK (gtasks);
+       if (!overwrite_existing || !e_cal_cache_get_component (cal_cache,
+               icalcomponent_get_uid (e_cal_component_get_icalcomponent (comp)),
+               NULL, &cached_comp, cancellable, NULL)) {
+               cached_comp = NULL;
+       }
 
-               if (!cached_comp) {
-                       local_error = EDC_ERROR (ObjectNotFound);
-                       break;
-               }
+       comp_task = ecb_gtasks_comp_to_gdata (comp, cached_comp, !overwrite_existing);
 
-               task = ecb_gtasks_comp_to_gdata (cached_comp, NULL);
-               if (!task) {
-                       g_object_unref (cached_comp);
-                       local_error = EDC_ERROR (InvalidObject);
-                       break;
-               }
-
-               /* Ignore protocol errors here, libgdata 0.15.1 results with "Error code 204 when deleting an 
entry: No Content",
-                  while the delete succeeded */
-               if (!gdata_tasks_service_delete_task (gtasks->priv->service, task, cancellable, &local_error) 
&&
-                   !g_error_matches (local_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR)) {
-                       g_object_unref (cached_comp);
-                       g_object_unref (task);
-                       break;
-               }
+       g_clear_object (&cached_comp);
+       g_clear_object (&cal_cache);
 
-               if (!local_error)
-                       e_backend_ensure_source_status_connected (E_BACKEND (backend));
+       if (!comp_task) {
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               return FALSE;
+       }
 
-               g_clear_error (&local_error);
+       if (overwrite_existing)
+               new_task = gdata_tasks_service_update_task (cbgtasks->priv->service, comp_task, cancellable, 
error);
+       else
+               new_task = gdata_tasks_service_insert_task (cbgtasks->priv->service, comp_task, 
cbgtasks->priv->tasklist, cancellable, error);
 
-               g_object_unref (task);
+       g_object_unref (comp_task);
 
-               PROPERTY_LOCK (gtasks);
-               e_cal_backend_store_remove_component (gtasks->priv->store, id->uid, NULL);
-               PROPERTY_UNLOCK (gtasks);
+       if (!new_task)
+               return FALSE;
 
-               tmp_id = e_cal_component_id_new (id->uid, NULL);
-               e_cal_backend_notify_component_removed (backend, tmp_id, cached_comp, NULL);
+       comp = ecb_gtasks_gdata_to_comp (new_task);
+       g_object_unref (new_task);
 
-               old_calcomps = g_slist_prepend (old_calcomps, cached_comp);
-               removed_ids = g_slist_prepend (removed_ids, tmp_id);
+       if (!comp) {
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               return FALSE;
        }
 
-       old_calcomps = g_slist_reverse (old_calcomps);
-       removed_ids = g_slist_reverse (removed_ids);
-
-       e_data_cal_respond_remove_objects (cal, opid, local_error, removed_ids, old_calcomps, NULL);
-
-       g_slist_free_full (removed_ids, (GDestroyNotify) e_cal_component_free_id);
-       e_util_free_nullable_object_slist (old_calcomps);
-}
-
-static void
-ecb_gtasks_receive_objects (ECalBackend *backend,
-                           EDataCal *cal,
-                           guint32 opid,
-                           GCancellable *cancellable,
-                           const gchar *calobj)
-{
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+       icalcomp = e_cal_component_get_icalcomponent (comp);
+       uid = icalcomp ? icalcomponent_get_uid (icalcomp) : NULL;
 
-       e_data_cal_respond_receive_objects (cal, opid, EDC_ERROR (NotSupported));
-}
+       if (!icalcomp || !uid) {
+               g_object_unref (comp);
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               return FALSE;
+       }
 
-static void
-ecb_gtasks_send_objects (ECalBackend *backend,
-                        EDataCal *cal,
-                        guint32 opid,
-                        GCancellable *cancellable,
-                        const gchar *calobj)
-{
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+       if (cbgtasks->priv->preloaded) {
+               *out_new_uid = g_strdup (uid);
+               g_hash_table_insert (cbgtasks->priv->preloaded, g_strdup (uid), comp);
+       } else {
+               g_object_unref (comp);
+       }
 
-       e_data_cal_respond_send_objects (cal, opid, EDC_ERROR (NotSupported), NULL, NULL);
+       return TRUE;
 }
 
-static void
-ecb_gtasks_get_attachment_uris (ECalBackend *backend,
-                               EDataCal *cal,
-                               guint32 opid,
-                               GCancellable *cancellable,
-                               const gchar *uid,
-                               const gchar *rid)
+static gboolean
+ecb_gtasks_remove_component_sync (ECalMetaBackend *meta_backend,
+                                 EConflictResolution conflict_resolution,
+                                 const gchar *uid,
+                                 const gchar *extra,
+                                 const gchar *object,
+                                 GCancellable *cancellable,
+                                 GError **error)
 {
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
-
-       e_data_cal_respond_get_attachment_uris (cal, opid, EDC_ERROR (NotSupported), NULL);
-}
+       ECalBackendGTasks *cbgtasks;
+       GDataTasksTask *task;
+       ECalComponent *cached_comp = NULL;
+       GError *local_error = NULL;
 
-static void
-ecb_gtasks_discard_alarm (ECalBackend *backend,
-                         EDataCal *cal,
-                         guint32 opid,
-                         GCancellable *cancellable,
-                         const gchar *uid,
-                         const gchar *rid,
-                         const gchar *auid)
-{
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL (cal));
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
 
-       e_data_cal_respond_discard_alarm (cal, opid, EDC_ERROR (NotSupported));
-}
+       cached_comp = e_cal_component_new_from_string (object);
+       if (!cached_comp) {
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
+               return FALSE;
+       }
 
-static void
-ecb_gtasks_start_view (ECalBackend *backend,
-                      EDataCalView *view)
-{
-       ECalBackendGTasks *gtasks;
-       ECalBackendSExp *sexp;
-       ETimezoneCache *cache;
-       const gchar *sexp_str;
-       gboolean do_search;
-       GSList *list, *iter;
-       time_t occur_start = -1, occur_end = -1;
-       gboolean prunning_by_time;
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
 
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL_VIEW (view));
+       task = ecb_gtasks_comp_to_gdata (cached_comp, NULL, FALSE);
+       if (!task) {
+               g_object_unref (cached_comp);
+               g_propagate_error (error, EDC_ERROR (InvalidObject));
 
-       g_object_ref (view);
+               return FALSE;
+       }
 
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
-       sexp = e_data_cal_view_get_sexp (view);
-       sexp_str = e_cal_backend_sexp_text (sexp);
-       do_search = !g_str_equal (sexp_str, "#t");
-       prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (sexp, &occur_start, &occur_end);
+       /* Ignore protocol errors here, libgdata 0.15.1 results with "Error code 204 when deleting an entry: 
No Content",
+          while the delete succeeded */
+       if (!gdata_tasks_service_delete_task (cbgtasks->priv->service, task, cancellable, &local_error) &&
+           !g_error_matches (local_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR)) {
+               g_object_unref (cached_comp);
+               g_object_unref (task);
+               g_propagate_error (error, local_error);
 
-       cache = E_TIMEZONE_CACHE (backend);
+               return FALSE;
+       } else {
+               g_clear_error (&local_error);
+       }
 
-       list = prunning_by_time ?
-               e_cal_backend_store_get_components_occuring_in_range (gtasks->priv->store, occur_start, 
occur_end)
-               : e_cal_backend_store_get_components (gtasks->priv->store);
+       g_object_unref (cached_comp);
+       g_object_unref (task);
 
-       for (iter = list; iter; iter = g_slist_next (iter)) {
-               ECalComponent *comp = E_CAL_COMPONENT (iter->data);
+       return TRUE;
+}
 
-               if (!do_search || e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
-                       e_data_cal_view_notify_components_added_1 (view, comp);
-               }
+static gboolean
+ecb_gtasks_requires_reconnect (ECalMetaBackend *meta_backend)
+{
+       ESource *source;
+       ESourceResource *resource;
+       gchar *id;
+       ECalBackendGTasks *cbgtasks;
+       gboolean changed;
 
-               g_object_unref (comp);
-       }
+       g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
 
-       g_slist_free (list);
+       cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
+       if (!cbgtasks->priv->tasklist)
+               return TRUE;
 
-       e_data_cal_view_notify_complete (view, NULL /* Success */);
+       source = e_backend_get_source (E_BACKEND (cbgtasks));
+       resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+       id = e_source_resource_dup_identity (resource);
 
-       g_object_unref (view);
-}
+       changed = id && *id && g_strcmp0 (id, gdata_entry_get_id (GDATA_ENTRY (cbgtasks->priv->tasklist))) != 
0 &&
+               g_strcmp0 (GTASKS_DEFAULT_TASKLIST_NAME, gdata_entry_get_id (GDATA_ENTRY 
(cbgtasks->priv->tasklist))) != 0;
 
-static void
-ecb_gtasks_stop_view (ECalBackend *backend,
-                     EDataCalView *view)
-{
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-       g_return_if_fail (E_IS_DATA_CAL_VIEW (view));
-}
+       g_free (id);
 
-static void
-ecb_gtasks_add_timezone (ECalBackend *backend,
-                        EDataCal *cal,
-                        guint32 opid,
-                        GCancellable *cancellable,
-                        const gchar *tzobject)
-{
-       /* Nothing to do, times are in UTC */
-       e_data_cal_respond_add_timezone (cal, opid, NULL);
+       return changed;
 }
 
-static void
-ecb_gtasks_shutdown (ECalBackend *backend)
+static gchar *
+ecb_gtasks_dup_component_revision (ECalCache *cal_cache,
+                                  icalcomponent *icalcomp,
+                                  gpointer user_data)
 {
-       ECalBackendGTasks *gtasks;
-
-       g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-
-       gtasks = E_CAL_BACKEND_GTASKS (backend);
+       icalproperty *prop;
+       gchar *revision = NULL;
 
-       ecb_gtasks_take_cancellable (gtasks, NULL);
+       g_return_val_if_fail (icalcomp != NULL, NULL);
 
-       if (gtasks->priv->refresh_id) {
-               ESource *source = e_backend_get_source (E_BACKEND (backend));
-               if (source)
-                       e_source_refresh_remove_timeout (source, gtasks->priv->refresh_id);
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_LASTMODIFIED_PROPERTY);
+       if (prop) {
+               struct icaltimetype itt;
 
-               gtasks->priv->refresh_id = 0;
+               itt = icalproperty_get_lastmodified (prop);
+               revision = icaltime_as_ical_string_r (itt);
        }
 
-       /* Chain up to parent's method. */
-       E_CAL_BACKEND_CLASS (e_cal_backend_gtasks_parent_class)->shutdown (backend);
+       return revision;
 }
 
 static void
-e_cal_backend_gtasks_init (ECalBackendGTasks *gtasks)
+e_cal_backend_gtasks_init (ECalBackendGTasks *cbgtasks)
 {
-       gtasks->priv = E_CAL_BACKEND_GTASKS_GET_PRIVATE (gtasks);
-       gtasks->priv->cancellable = NULL;
-
-       g_mutex_init (&gtasks->priv->property_mutex);
+       cbgtasks->priv = G_TYPE_INSTANCE_GET_PRIVATE (cbgtasks, E_TYPE_CAL_BACKEND_GTASKS, 
ECalBackendGTasksPrivate);
+       cbgtasks->priv->preloaded = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
 }
 
 static void
 ecb_gtasks_constructed (GObject *object)
 {
-       ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (object);
-       ESource *source;
+       ECalBackendGTasks *cbgtasks = E_CAL_BACKEND_GTASKS (object);
+       ECalCache *cal_cache;
 
        /* Chain up to parent's method. */
        G_OBJECT_CLASS (e_cal_backend_gtasks_parent_class)->constructed (object);
 
-       gtasks->priv->store = e_cal_backend_store_new (
-               e_cal_backend_get_cache_dir (E_CAL_BACKEND (gtasks)),
-               E_TIMEZONE_CACHE (gtasks));
-       e_cal_backend_store_load (gtasks->priv->store);
+       cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbgtasks));
+       g_return_if_fail (cal_cache != NULL);
+
+       g_signal_connect (cal_cache, "dup-component-revision", G_CALLBACK 
(ecb_gtasks_dup_component_revision), NULL);
 
-       source = e_backend_get_source (E_BACKEND (gtasks));
-       gtasks->priv->refresh_id = e_source_refresh_add_timeout (
-               source, NULL, ecb_gtasks_time_to_refresh_data_cb, gtasks, NULL);
+       g_clear_object (&cal_cache);
+
+       /* Set it as always writable, regardless online/offline state */
+       e_cal_backend_set_writable (E_CAL_BACKEND (cbgtasks), TRUE);
 }
 
 static void
 ecb_gtasks_dispose (GObject *object)
 {
-       ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (object);
-
-       ecb_gtasks_take_cancellable (gtasks, NULL);
+       ECalBackendGTasks *cbgtasks = E_CAL_BACKEND_GTASKS (object);
 
-       g_clear_object (&gtasks->priv->cancellable);
-       g_clear_object (&gtasks->priv->service);
-       g_clear_object (&gtasks->priv->authorizer);
-       g_clear_object (&gtasks->priv->tasklist);
-       g_clear_object (&gtasks->priv->store);
+       g_clear_object (&cbgtasks->priv->service);
+       g_clear_object (&cbgtasks->priv->authorizer);
+       g_clear_object (&cbgtasks->priv->tasklist);
 
-       if (gtasks->priv->refresh_id) {
-               ESource *source = e_backend_get_source (E_BACKEND (object));
-               if (source)
-                       e_source_refresh_remove_timeout (source, gtasks->priv->refresh_id);
-
-               gtasks->priv->refresh_id = 0;
-       }
+       g_hash_table_destroy (cbgtasks->priv->preloaded);
+       cbgtasks->priv->preloaded = NULL;
 
        /* Chain up to parent's method. */
        G_OBJECT_CLASS (e_cal_backend_gtasks_parent_class)->dispose (object);
 }
 
 static void
-ecb_gtasks_finalize (GObject *object)
-{
-       ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (object);
-
-       g_mutex_clear (&gtasks->priv->property_mutex);
-
-       /* Chain up to parent's method. */
-       G_OBJECT_CLASS (e_cal_backend_gtasks_parent_class)->finalize (object);
-}
-
-static void
-e_cal_backend_gtasks_class_init (ECalBackendGTasksClass *class)
+e_cal_backend_gtasks_class_init (ECalBackendGTasksClass *klass)
 {
        GObjectClass *object_class;
-       EBackendClass *backend_class;
        ECalBackendClass *cal_backend_class;
+       ECalMetaBackendClass *cal_meta_backend_class;
 
-       g_type_class_add_private (class, sizeof (ECalBackendGTasksPrivate));
-
-       object_class = (GObjectClass *) class;
-       object_class->constructed = ecb_gtasks_constructed;
-       object_class->dispose = ecb_gtasks_dispose;
-       object_class->finalize = ecb_gtasks_finalize;
+       g_type_class_add_private (klass, sizeof (ECalBackendGTasksPrivate));
 
-       backend_class = (EBackendClass *) class;
-       backend_class->authenticate_sync = ecb_gtasks_authenticate_sync;
+       cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+       cal_meta_backend_class->connect_sync = ecb_gtasks_connect_sync;
+       cal_meta_backend_class->disconnect_sync = ecb_gtasks_disconnect_sync;
+       cal_meta_backend_class->get_changes_sync = ecb_gtasks_get_changes_sync;
+       cal_meta_backend_class->load_component_sync = ecb_gtasks_load_component_sync;
+       cal_meta_backend_class->save_component_sync = ecb_gtasks_save_component_sync;
+       cal_meta_backend_class->remove_component_sync = ecb_gtasks_remove_component_sync;
+       cal_meta_backend_class->requires_reconnect = ecb_gtasks_requires_reconnect;
 
-       cal_backend_class = (ECalBackendClass *) class;
+       cal_backend_class = E_CAL_BACKEND_CLASS (klass);
        cal_backend_class->get_backend_property = ecb_gtasks_get_backend_property;
-       cal_backend_class->open = ecb_gtasks_open;
-       cal_backend_class->refresh = ecb_gtasks_refresh;
-       cal_backend_class->get_object = ecb_gtasks_get_object;
-       cal_backend_class->get_object_list = ecb_gtasks_get_object_list;
-       cal_backend_class->get_free_busy = ecb_gtasks_get_free_busy;
-       cal_backend_class->create_objects = ecb_gtasks_create_objects;
-       cal_backend_class->modify_objects = ecb_gtasks_modify_objects;
-       cal_backend_class->remove_objects = ecb_gtasks_remove_objects;
-       cal_backend_class->receive_objects = ecb_gtasks_receive_objects;
-       cal_backend_class->send_objects = ecb_gtasks_send_objects;
-       cal_backend_class->get_attachment_uris = ecb_gtasks_get_attachment_uris;
-       cal_backend_class->discard_alarm = ecb_gtasks_discard_alarm;
-       cal_backend_class->start_view = ecb_gtasks_start_view;
-       cal_backend_class->stop_view = ecb_gtasks_stop_view;
-       cal_backend_class->add_timezone = ecb_gtasks_add_timezone;
-       cal_backend_class->shutdown = ecb_gtasks_shutdown;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = ecb_gtasks_constructed;
+       object_class->dispose = ecb_gtasks_dispose;
 }
diff --git a/src/calendar/backends/gtasks/e-cal-backend-gtasks.h 
b/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
index 6823a98..747d044 100644
--- a/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
+++ b/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
@@ -47,12 +47,12 @@ typedef struct _ECalBackendGTasksClass ECalBackendGTasksClass;
 typedef struct _ECalBackendGTasksPrivate ECalBackendGTasksPrivate;
 
 struct _ECalBackendGTasks {
-       ECalBackend parent;
+       ECalMetaBackend parent;
        ECalBackendGTasksPrivate *priv;
 };
 
 struct _ECalBackendGTasksClass {
-       ECalBackendClass parent_class;
+       ECalMetaBackendClass parent_class;
 };
 
 GType          e_cal_backend_gtasks_get_type   (void);
diff --git a/src/calendar/backends/http/e-cal-backend-http.c b/src/calendar/backends/http/e-cal-backend-http.c
index 1cb7f13..51a4246 100644
--- a/src/calendar/backends/http/e-cal-backend-http.c
+++ b/src/calendar/backends/http/e-cal-backend-http.c
@@ -26,227 +26,25 @@
 
 #include <libsoup/soup.h>
 #include <libedata-cal/libedata-cal.h>
-#include "e-cal-backend-http.h"
 
-#define E_CAL_BACKEND_HTTP_GET_PRIVATE(obj) \
-       (G_TYPE_INSTANCE_GET_PRIVATE \
-       ((obj), E_TYPE_CAL_BACKEND_HTTP, ECalBackendHttpPrivate))
+#include "e-cal-backend-http.h"
 
 #define EDC_ERROR(_code) e_data_cal_create_error (_code, NULL)
 #define EDC_ERROR_EX(_code, _msg) e_data_cal_create_error (_code, _msg)
 
-G_DEFINE_TYPE (ECalBackendHttp, e_cal_backend_http, E_TYPE_CAL_BACKEND_SYNC)
+G_DEFINE_TYPE (ECalBackendHttp, e_cal_backend_http, E_TYPE_CAL_META_BACKEND)
 
-/* Private part of the ECalBackendHttp structure */
 struct _ECalBackendHttpPrivate {
-       /* signal handler id for source's 'changed' signal */
-       gulong source_changed_id;
-       /* URI to get remote calendar data from */
-       gchar *uri;
-
-       /* The file cache */
-       ECalBackendStore *store;
-
-       /* Soup handles for remote file */
-       SoupSession *soup_session;
-
-       /* Reload */
-       guint reload_timeout_id;
-       guint is_loading : 1;
-
-       /* Flags */
-       gboolean opened;
-       gboolean requires_auth;
+       ESoupSession *session;
 
-       gchar *username;
-       gchar *password;
+       SoupRequestHTTP *request;
+       GInputStream *input_stream;
+       GHashTable *components; /* gchar *uid ~> icalcomponent * */
 };
 
-#define d(x)
-
-static void    e_cal_backend_http_add_timezone (ECalBackendSync *backend,
-                                                EDataCal *cal,
-                                                GCancellable *cancellable,
-                                                const gchar *tzobj,
-                                                GError **perror);
-
-static void
-soup_authenticate (SoupSession *session,
-                   SoupMessage *msg,
-                   SoupAuth *auth,
-                   gboolean retrying,
-                   gpointer data)
-{
-       ECalBackendHttp *cbhttp;
-       ESourceAuthentication *auth_extension;
-       ESource *source;
-       const gchar *extension_name;
-       const gchar *username;
-       gchar *auth_user;
-
-       if (retrying)
-               return;
-
-       cbhttp = E_CAL_BACKEND_HTTP (data);
-
-       source = e_backend_get_source (E_BACKEND (data));
-       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
-       auth_extension = e_source_get_extension (source, extension_name);
-
-       auth_user = e_source_authentication_dup_user (auth_extension);
-
-       username = cbhttp->priv->username;
-       if (!username || !*username)
-               username = auth_user;
-
-       if (!username || !*username || !cbhttp->priv->password)
-               soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
-       else
-               soup_auth_authenticate (auth, username, cbhttp->priv->password);
-
-       g_free (auth_user);
-}
-
-/* Dispose handler for the file backend */
-static void
-e_cal_backend_http_dispose (GObject *object)
-{
-       ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-
-       cbhttp = E_CAL_BACKEND_HTTP (object);
-       priv = cbhttp->priv;
-
-       if (priv->reload_timeout_id) {
-               ESource *source = e_backend_get_source (E_BACKEND (cbhttp));
-               e_source_refresh_remove_timeout (source, priv->reload_timeout_id);
-               priv->reload_timeout_id = 0;
-       }
-
-       if (priv->soup_session) {
-               soup_session_abort (priv->soup_session);
-               g_object_unref (priv->soup_session);
-               priv->soup_session = NULL;
-       }
-       if (priv->source_changed_id) {
-               g_signal_handler_disconnect (
-                       e_backend_get_source (E_BACKEND (cbhttp)),
-                       priv->source_changed_id);
-               priv->source_changed_id = 0;
-       }
-
-       /* Chain up to parent's dispose() method. */
-       G_OBJECT_CLASS (e_cal_backend_http_parent_class)->dispose (object);
-}
-
-/* Finalize handler for the file backend */
-static void
-e_cal_backend_http_finalize (GObject *object)
-{
-       ECalBackendHttpPrivate *priv;
-
-       priv = E_CAL_BACKEND_HTTP_GET_PRIVATE (object);
-
-       /* Clean up */
-
-       if (priv->store) {
-               g_object_unref (priv->store);
-               priv->store = NULL;
-       }
-
-       g_free (priv->uri);
-       g_free (priv->username);
-       g_free (priv->password);
-
-       /* Chain up to parent's finalize() method. */
-       G_OBJECT_CLASS (e_cal_backend_http_parent_class)->finalize (object);
-}
-
-static void
-e_cal_backend_http_constructed (GObject *object)
-{
-       ECalBackendHttp *backend;
-       SoupSession *soup_session;
-
-       /* Chain up to parent's constructed() method. */
-       G_OBJECT_CLASS (e_cal_backend_http_parent_class)->constructed (object);
-
-       soup_session = soup_session_sync_new ();
-       g_object_set (
-               soup_session,
-               SOUP_SESSION_TIMEOUT, 90,
-               SOUP_SESSION_SSL_STRICT, TRUE,
-               SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
-               SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
-               NULL);
-
-       backend = E_CAL_BACKEND_HTTP (object);
-       backend->priv->soup_session = soup_session;
-
-       e_binding_bind_property (
-               backend, "proxy-resolver",
-               backend->priv->soup_session, "proxy-resolver",
-               G_BINDING_SYNC_CREATE);
-
-       g_signal_connect (
-               backend->priv->soup_session, "authenticate",
-               G_CALLBACK (soup_authenticate), backend);
-
-       if (g_getenv ("WEBCAL_DEBUG") != NULL) {
-               SoupLogger *logger;
-
-               logger = soup_logger_new (
-                       SOUP_LOGGER_LOG_BODY, 1024 * 1024);
-               soup_session_add_feature (
-                       backend->priv->soup_session,
-                       SOUP_SESSION_FEATURE (logger));
-               g_object_unref (logger);
-       }
-}
-
-/* Calendar backend methods */
-
 static gchar *
-e_cal_backend_http_get_backend_property (ECalBackend *backend,
-                                         const gchar *prop_name)
-{
-       g_return_val_if_fail (prop_name != NULL, NULL);
-
-       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
-               return g_strjoin (
-                       ","
-                       CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS,
-                       CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED,
-                       NULL);
-
-       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
-                  g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
-               /* A HTTP backend has no particular email address associated
-                * with it (although that would be a useful feature some day).
-                */
-               return NULL;
-
-       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
-               icalcomponent *icalcomp;
-               icalcomponent_kind kind;
-               gchar *prop_value;
-
-               kind = e_cal_backend_get_kind (E_CAL_BACKEND (backend));
-               icalcomp = e_cal_util_new_component (kind);
-               prop_value = icalcomponent_as_ical_string_r (icalcomp);
-               icalcomponent_free (icalcomp);
-
-               return prop_value;
-       }
-
-       /* Chain up to parent's get_backend_property() method. */
-       return E_CAL_BACKEND_CLASS (e_cal_backend_http_parent_class)->
-               get_backend_property (backend, prop_name);
-}
-
-static gchar *
-webcal_to_http_method (const gchar *webcal_str,
-                       gboolean secure)
+ecb_http_webcal_to_http_method (const gchar *webcal_str,
+                               gboolean secure)
 {
        if (secure && (strncmp ("http://";, webcal_str, sizeof ("http://";) - 1) == 0))
                return g_strconcat ("https://";, webcal_str + sizeof ("http://";) - 1, NULL);
@@ -260,1321 +58,556 @@ webcal_to_http_method (const gchar *webcal_str,
                return g_strconcat ("http://";, webcal_str + sizeof ("webcal://") - 1, NULL);
 }
 
-static gboolean
-notify_and_remove_from_cache (gpointer key,
-                              gpointer value,
-                              gpointer user_data)
-{
-       const gchar *calobj = value;
-       ECalBackendHttp *cbhttp = E_CAL_BACKEND_HTTP (user_data);
-       ECalComponent *comp = e_cal_component_new_from_string (calobj);
-       ECalComponentId *id = e_cal_component_get_id (comp);
-
-       if (id) {
-               e_cal_backend_store_remove_component (cbhttp->priv->store, id->uid, id->rid);
-               e_cal_backend_notify_component_removed (E_CAL_BACKEND (cbhttp), id, comp, NULL);
-
-               e_cal_component_free_id (id);
-       }
-
-       g_object_unref (comp);
-
-       return TRUE;
-}
-
-static void
-empty_cache (ECalBackendHttp *cbhttp)
-{
-       ECalBackendHttpPrivate *priv;
-       GSList *comps, *l;
-
-       priv = cbhttp->priv;
-
-       if (!priv->store)
-               return;
-
-       comps = e_cal_backend_store_get_components (priv->store);
-
-       for (l = comps; l != NULL; l = g_slist_next (l)) {
-               ECalComponentId *id;
-               ECalComponent *comp = l->data;
-
-               id = e_cal_component_get_id (comp);
-
-               e_cal_backend_notify_component_removed ((ECalBackend *) cbhttp, id, comp, NULL);
-
-               e_cal_component_free_id (id);
-               g_object_unref (comp);
-       }
-       g_slist_free (comps);
-
-       e_cal_backend_store_put_key_value (priv->store, "ETag", NULL);
-       e_cal_backend_store_clean (priv->store);
-}
-
-/* TODO Do not replicate this in every backend */
-static icaltimezone *
-resolve_tzid (const gchar *tzid,
-              gpointer user_data)
-{
-       ETimezoneCache *timezone_cache;
-
-       timezone_cache = E_TIMEZONE_CACHE (user_data);
-
-       return e_timezone_cache_get_timezone (timezone_cache, tzid);
-}
-
-static gboolean
-put_component_to_store (ECalBackendHttp *cb,
-                        ECalComponent *comp)
+static gchar *
+ecb_http_dup_uri (ECalBackendHttp *cbhttp)
 {
-       time_t time_start, time_end;
-       ECalBackendHttpPrivate *priv;
-       ECalComponent *cache_comp;
-       const gchar *uid;
-       gchar *rid;
-
-       priv = cb->priv;
-
-       e_cal_component_get_uid (comp, &uid);
-       rid = e_cal_component_get_recurid_as_string (comp);
-       cache_comp = e_cal_backend_store_get_component (priv->store, uid, rid);
-       g_free (rid);
-
-       if (cache_comp) {
-               gboolean changed = TRUE;
-               struct icaltimetype stamp1, stamp2;
-
-               stamp1 = icaltime_null_time ();
-               stamp2 = icaltime_null_time ();
-
-               e_cal_component_get_dtstamp (comp, &stamp1);
-               e_cal_component_get_dtstamp (cache_comp, &stamp2);
-
-               changed = (icaltime_is_null_time (stamp1) && !icaltime_is_null_time (stamp2)) ||
-                         (!icaltime_is_null_time (stamp1) && icaltime_is_null_time (stamp2)) ||
-                         (icaltime_compare (stamp1, stamp2) != 0);
-
-               if (!changed) {
-                       struct icaltimetype *last_modified1 = NULL, *last_modified2 = NULL;
-
-                       e_cal_component_get_last_modified (comp, &last_modified1);
-                       e_cal_component_get_last_modified (cache_comp, &last_modified2);
-
-                       changed = (last_modified1 != NULL && last_modified2 == NULL) ||
-                                 (last_modified1 == NULL && last_modified2 != NULL) ||
-                                 (last_modified1 != NULL && last_modified2 != NULL && icaltime_compare 
(*last_modified1, *last_modified2) != 0);
-
-                       if (last_modified1)
-                               e_cal_component_free_icaltimetype (last_modified1);
-                       if (last_modified2)
-                               e_cal_component_free_icaltimetype (last_modified2);
-
-                       if (!changed) {
-                               gint *sequence1 = NULL, *sequence2 = NULL;
-
-                               e_cal_component_get_sequence (comp, &sequence1);
-                               e_cal_component_get_sequence (cache_comp, &sequence2);
-
-                               changed = (sequence1 != NULL && sequence2 == NULL) ||
-                                         (sequence1 == NULL && sequence2 != NULL) ||
-                                         (sequence1 != NULL && sequence2 != NULL && *sequence1 != 
*sequence2);
-
-                               if (sequence1)
-                                       e_cal_component_free_sequence (sequence1);
-                               if (sequence2)
-                                       e_cal_component_free_sequence (sequence2);
-                       }
-               }
-
-               g_object_unref (cache_comp);
-
-               if (!changed)
-                       return FALSE;
-       }
-
-       e_cal_util_get_component_occur_times (
-               comp, &time_start, &time_end,
-               resolve_tzid, cb, icaltimezone_get_utc_timezone (),
-               e_cal_backend_get_kind (E_CAL_BACKEND (cb)));
-
-       e_cal_backend_store_put_component_with_time_range (priv->store, comp, time_start, time_end);
+       ESource *source;
+       ESourceSecurity *security_extension;
+       ESourceWebdav *webdav_extension;
+       SoupURI *soup_uri;
+       gboolean secure_connection;
+       const gchar *extension_name;
+       gchar *uri_string, *uri;
 
-       return TRUE;
-}
+       g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (cbhttp), NULL);
 
-static SoupMessage *
-cal_backend_http_new_message (ECalBackendHttp *backend,
-                              const gchar *uri)
-{
-       SoupMessage *soup_message;
-
-       /* create message to be sent to server */
-       soup_message = soup_message_new (SOUP_METHOD_GET, uri);
-       if (soup_message == NULL)
-               return NULL;
-
-       soup_message_headers_append (
-               soup_message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
-       soup_message_headers_append (
-               soup_message->request_headers,
-               "Connection", "close");
-       soup_message_set_flags (
-               soup_message, SOUP_MESSAGE_NO_REDIRECT);
-       if (backend->priv->store != NULL) {
-               const gchar *etag;
-
-               etag = e_cal_backend_store_get_key_value (
-                       backend->priv->store, "ETag");
-
-               if (etag != NULL && *etag != '\0')
-                       soup_message_headers_append (
-                               soup_message->request_headers,
-                               "If-None-Match", etag);
-       }
+       source = e_backend_get_source (E_BACKEND (cbhttp));
 
-       return soup_message;
-}
+       extension_name = E_SOURCE_EXTENSION_SECURITY;
+       security_extension = e_source_get_extension (source, extension_name);
 
-static void
-cal_backend_http_cancelled (GCancellable *cancellable,
-                            gpointer user_data)
-{
-       struct {
-               SoupSession *soup_session;
-               SoupMessage *soup_message;
-       } *cancel_data = user_data;
-
-       soup_session_cancel_message (
-               cancel_data->soup_session,
-               cancel_data->soup_message,
-               SOUP_STATUS_CANCELLED);
-}
+       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+       webdav_extension = e_source_get_extension (source, extension_name);
 
-static void
-cal_backend_http_extract_ssl_failed_data (SoupMessage *msg,
-                                         gchar **out_certificate_pem,
-                                         GTlsCertificateFlags *out_certificate_errors)
-{
-       GTlsCertificate *certificate = NULL;
+       secure_connection = e_source_security_get_secure (security_extension);
 
-       g_return_if_fail (SOUP_IS_MESSAGE (msg));
+       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+       uri_string = soup_uri_to_string (soup_uri, FALSE);
+       soup_uri_free (soup_uri);
 
-       if (!out_certificate_pem || !out_certificate_errors)
-               return;
+       uri = ecb_http_webcal_to_http_method (uri_string, secure_connection);
 
-       g_object_get (G_OBJECT (msg),
-               "tls-certificate", &certificate,
-               "tls-errors", out_certificate_errors,
-               NULL);
+       g_free (uri_string);
 
-       if (certificate) {
-               g_object_get (certificate, "certificate-pem", out_certificate_pem, NULL);
-               g_object_unref (certificate);
-       }
+       return uri;
 }
 
 static gboolean
-cal_backend_http_load (ECalBackendHttp *backend,
-                       const gchar *uri,
+ecb_http_connect_sync (ECalMetaBackend *meta_backend,
+                      const ENamedParameters *credentials,
+                      ESourceAuthenticationResult *out_auth_result,
                       gchar **out_certificate_pem,
                       GTlsCertificateFlags *out_certificate_errors,
-                       GCancellable *cancellable,
-                       GError **error)
+                      GCancellable *cancellable,
+                      GError **error)
 {
-       ECalBackendHttpPrivate *priv = backend->priv;
-       ETimezoneCache *timezone_cache;
-       SoupMessage *soup_message;
-       SoupSession *soup_session;
-       icalcomponent *icalcomp, *subcomp;
-       icalcomponent_kind kind;
-       const gchar *newuri;
-       SoupURI *uri_parsed;
-       GHashTable *old_cache;
-       GSList *comps_in_cache;
+       ECalBackendHttp *cbhttp;
        ESource *source;
-       guint status_code;
-       gulong cancel_id = 0;
-
-       struct {
-               SoupSession *soup_session;
-               SoupMessage *soup_message;
-       } cancel_data;
-
-       timezone_cache = E_TIMEZONE_CACHE (backend);
-
-       soup_session = backend->priv->soup_session;
-       soup_message = cal_backend_http_new_message (backend, uri);
-
-       if (soup_message == NULL) {
-               g_set_error (
-                       error, SOUP_HTTP_ERROR,
-                       SOUP_STATUS_MALFORMED,
-                       _("Malformed URI: %s"), uri);
-               return FALSE;
-       }
+       SoupRequestHTTP *request;
+       GInputStream *input_stream = NULL;
+       gchar *uri;
+       gboolean success;
+       GError *local_error = NULL;
 
-       if (G_IS_CANCELLABLE (cancellable)) {
-               cancel_data.soup_session = soup_session;
-               cancel_data.soup_message = soup_message;
+       g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
 
-               cancel_id = g_cancellable_connect (
-                       cancellable,
-                       G_CALLBACK (cal_backend_http_cancelled),
-                       &cancel_data, (GDestroyNotify) NULL);
-       }
+       cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
 
-       source = e_backend_get_source (E_BACKEND (backend));
-
-       e_soup_ssl_trust_connect (soup_message, source);
+       if (cbhttp->priv->request && cbhttp->priv->input_stream)
+               return TRUE;
 
-       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+       source = e_backend_get_source (E_BACKEND (meta_backend));
 
-       status_code = soup_session_send_message (soup_session, soup_message);
+       g_clear_object (&cbhttp->priv->input_stream);
+       g_clear_object (&cbhttp->priv->request);
 
-       if (G_IS_CANCELLABLE (cancellable))
-               g_cancellable_disconnect (cancellable, cancel_id);
+       uri = ecb_http_dup_uri (cbhttp);
 
-       if (status_code == SOUP_STATUS_NOT_MODIFIED) {
-               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
+       if (!uri || !*uri) {
+               g_free (uri);
 
-               /* attempts with ETag can result in 304 status code */
-               g_object_unref (soup_message);
-               priv->opened = TRUE;
-               return TRUE;
+               g_propagate_error (error, EDC_ERROR_EX (OtherError, _("URI not set")));
+               return FALSE;
        }
 
-       /* Handle redirection ourselves */
-       if (SOUP_STATUS_IS_REDIRECTION (status_code)) {
-               gboolean success;
-
-               newuri = soup_message_headers_get_list (
-                       soup_message->response_headers, "Location");
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
 
-               d (g_message ("Redirected from %s to %s\n", async_context->uri, newuri));
+       e_soup_session_set_credentials (cbhttp->priv->session, credentials);
 
-               if (newuri != NULL) {
-                       gchar *redirected_uri;
+       request = e_soup_session_new_request (cbhttp->priv->session, SOUP_METHOD_GET, uri, &local_error);
+       success = request != NULL;
 
-                       if (newuri[0]=='/') {
-                               g_warning ("Hey! Relative URI returned! Working around...\n");
+       if (success) {
+               SoupMessage *message;
 
-                               uri_parsed = soup_uri_new (uri);
-                               soup_uri_set_path (uri_parsed, newuri);
-                               soup_uri_set_query (uri_parsed, NULL);
-                               /* g_free (newuri); */
+               message = soup_request_http_get_message (request);
 
-                               newuri = soup_uri_to_string (uri_parsed, FALSE);
-                               g_message ("Translated URI: %s\n", newuri);
-                               soup_uri_free (uri_parsed);
-                       }
+               input_stream = e_soup_session_send_request_sync (cbhttp->priv->session, request, cancellable, 
&local_error);
 
-                       redirected_uri =
-                               webcal_to_http_method (newuri, FALSE);
-                       success = cal_backend_http_load (
-                               backend, redirected_uri, out_certificate_pem, out_certificate_errors, 
cancellable, error);
-                       g_free (redirected_uri);
+               success = input_stream != NULL;
 
-               } else {
-                       g_set_error (
-                               error, SOUP_HTTP_ERROR,
-                               SOUP_STATUS_BAD_REQUEST,
-                               _("Redirected to Invalid URI"));
+               if (success && message && !SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
+                       g_clear_object (&input_stream);
                        success = FALSE;
                }
 
                if (success) {
                        e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
                } else {
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-               }
+                       guint status_code = message ? message->status_code : SOUP_STATUS_MALFORMED;
+                       gboolean credentials_empty;
+
+                       credentials_empty = !credentials || !e_named_parameters_count (credentials);
+
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+
+                       /* because evolution knows only G_IO_ERROR_CANCELLED */
+                       if (status_code == SOUP_STATUS_CANCELLED) {
+                               g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
+                                       "%s", message->reason_phrase);
+                       } else if (status_code == SOUP_STATUS_FORBIDDEN && credentials_empty) {
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       } else if (status_code == SOUP_STATUS_UNAUTHORIZED) {
+                               if (credentials_empty)
+                                       *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                               else
+                                       *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+                       } else if (local_error) {
+                               g_propagate_error (error, local_error);
+                               local_error = NULL;
+                       } else {
+                               g_set_error_literal (error, SOUP_HTTP_ERROR, status_code,
+                                       message ? message->reason_phrase : soup_status_get_phrase 
(status_code));
+                       }
 
-               g_object_unref (soup_message);
-               return success;
-       }
+                       if (status_code == SOUP_STATUS_SSL_FAILED) {
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
 
-       /* check status code */
-       if (!SOUP_STATUS_IS_SUCCESSFUL (status_code)) {
-               /* because evolution knows only G_IO_ERROR_CANCELLED */
-               if (status_code == SOUP_STATUS_CANCELLED)
-                       g_set_error (
-                               error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
-                               "%s", soup_message->reason_phrase);
-               else
-                       g_set_error (
-                               error, SOUP_HTTP_ERROR, status_code,
-                               "%s", soup_message->reason_phrase);
-
-               if (status_code == SOUP_STATUS_SSL_FAILED) {
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
-                       cal_backend_http_extract_ssl_failed_data (soup_message, out_certificate_pem, 
out_certificate_errors);
-               } else {
-                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+                               e_source_set_connection_status (source, 
E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+                               e_soup_session_get_ssl_error_details (cbhttp->priv->session, 
out_certificate_pem, out_certificate_errors);
+                       } else {
+                               e_source_set_connection_status (source, 
E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+                       }
                }
 
-               g_object_unref (soup_message);
-               empty_cache (backend);
-               return FALSE;
-       }
-
-       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
-
-       if (priv->store) {
-               const gchar *etag;
-
-               etag = soup_message_headers_get_one (
-                       soup_message->response_headers, "ETag");
-
-               if (etag != NULL && *etag == '\0')
-                       etag = NULL;
-
-               e_cal_backend_store_put_key_value (priv->store, "ETag", etag);
-       }
-
-       /* get the calendar from the response */
-       icalcomp = icalparser_parse_string (soup_message->response_body->data);
-
-       if (!icalcomp) {
-               g_set_error (
-                       error, SOUP_HTTP_ERROR,
-                       SOUP_STATUS_MALFORMED,
-                       _("Bad file format."));
-               g_object_unref (soup_message);
-               empty_cache (backend);
-               return FALSE;
-       }
-
-       if (icalcomponent_isa (icalcomp) != ICAL_VCALENDAR_COMPONENT) {
-               g_set_error (
-                       error, SOUP_HTTP_ERROR,
-                       SOUP_STATUS_MALFORMED,
-                       _("Not a calendar."));
-               icalcomponent_free (icalcomp);
-               g_object_unref (soup_message);
-               empty_cache (backend);
-               return FALSE;
-       }
-
-       g_object_unref (soup_message);
-       soup_message = NULL;
-
-       /* Update cache */
-       old_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
-
-       comps_in_cache = e_cal_backend_store_get_components (priv->store);
-       while (comps_in_cache != NULL) {
-               const gchar *uid;
-               ECalComponent *comp = comps_in_cache->data;
-
-               e_cal_component_get_uid (comp, &uid);
-               g_hash_table_insert (old_cache, g_strdup (uid), e_cal_component_get_as_string (comp));
+               g_clear_object (&message);
+       } else {
+               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
 
-               comps_in_cache = g_slist_remove (comps_in_cache, comps_in_cache->data);
-               g_object_unref (comp);
+               g_set_error (error, E_DATA_CAL_ERROR, OtherError, _("Malformed URI “%s“: %s"),
+                       uri, local_error ? local_error->message : _("Unknown error"));
        }
 
-       kind = e_cal_backend_get_kind (E_CAL_BACKEND (backend));
-       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_ANY_COMPONENT);
-       e_cal_backend_store_freeze_changes (priv->store);
-       while (subcomp) {
-               ECalComponent *comp;
-               icalcomponent_kind subcomp_kind;
-               icalproperty *prop = NULL;
-
-               subcomp_kind = icalcomponent_isa (subcomp);
-               prop = icalcomponent_get_first_property (subcomp, ICAL_UID_PROPERTY);
-               if (!prop && subcomp_kind == kind) {
-                       gchar *new_uid = e_cal_component_gen_uid ();
-                       icalcomponent_set_uid (subcomp, new_uid);
-                       g_free (new_uid);
-               }
-
-               if (subcomp_kind == kind) {
-                       comp = e_cal_component_new ();
-                       if (e_cal_component_set_icalcomponent (comp, icalcomponent_new_clone (subcomp))) {
-                               const gchar *uid;
-                               gpointer orig_key, orig_value;
-
-                               e_cal_component_get_uid (comp, &uid);
-
-                               if (!put_component_to_store (backend, comp)) {
-                                       g_hash_table_remove (old_cache, uid);
-                               } else if (g_hash_table_lookup_extended (old_cache, uid, &orig_key, 
&orig_value)) {
-                                       ECalComponent *orig_comp = e_cal_component_new_from_string 
(orig_value);
-
-                                       e_cal_backend_notify_component_modified (E_CAL_BACKEND (backend), 
orig_comp, comp);
-
-                                       g_hash_table_remove (old_cache, uid);
-                                       if (orig_comp)
-                                               g_object_unref (orig_comp);
-                               } else {
-                                       e_cal_backend_notify_component_created (E_CAL_BACKEND (backend), 
comp);
-                               }
-                       }
-
-                       g_object_unref (comp);
-               } else if (subcomp_kind == ICAL_VTIMEZONE_COMPONENT) {
-                       icaltimezone *zone;
+       if (success) {
+               cbhttp->priv->request = request;
+               cbhttp->priv->input_stream = input_stream;
 
-                       zone = icaltimezone_new ();
-                       icaltimezone_set_component (zone, icalcomponent_new_clone (subcomp));
-                       e_timezone_cache_add_timezone (timezone_cache, zone);
-
-                       icaltimezone_free (zone, 1);
-               }
-
-               subcomp = icalcomponent_get_next_component (icalcomp, ICAL_ANY_COMPONENT);
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+       } else {
+               g_clear_object (&request);
+               g_clear_object (&input_stream);
        }
 
-       e_cal_backend_store_thaw_changes (priv->store);
-
-       /* notify the removals */
-       g_hash_table_foreach_remove (old_cache, (GHRFunc) notify_and_remove_from_cache, backend);
-       g_hash_table_destroy (old_cache);
-
-       /* free memory */
-       icalcomponent_free (icalcomp);
-
-       priv->opened = TRUE;
+       g_clear_error (&local_error);
+       g_free (uri);
 
-       return TRUE;
+       return success;
 }
 
-static const gchar *
-cal_backend_http_ensure_uri (ECalBackendHttp *backend)
+static gboolean
+ecb_http_disconnect_sync (ECalMetaBackend *meta_backend,
+                         GCancellable *cancellable,
+                         GError **error)
 {
+       ECalBackendHttp *cbhttp;
        ESource *source;
-       ESourceSecurity *security_extension;
-       ESourceWebdav *webdav_extension;
-       SoupURI *soup_uri;
-       gboolean secure_connection;
-       const gchar *extension_name;
-       gchar *uri_string;
 
-       if (backend->priv->uri != NULL)
-               return backend->priv->uri;
+       g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
 
-       source = e_backend_get_source (E_BACKEND (backend));
+       cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
 
-       extension_name = E_SOURCE_EXTENSION_SECURITY;
-       security_extension = e_source_get_extension (source, extension_name);
+       if (cbhttp->priv->session)
+               soup_session_abort (SOUP_SESSION (cbhttp->priv->session));
 
-       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-       webdav_extension = e_source_get_extension (source, extension_name);
+       g_clear_object (&cbhttp->priv->input_stream);
+       g_clear_object (&cbhttp->priv->request);
 
-       secure_connection = e_source_security_get_secure (security_extension);
-
-       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
-       uri_string = soup_uri_to_string (soup_uri, FALSE);
-       soup_uri_free (soup_uri);
-
-       backend->priv->uri = webcal_to_http_method (
-               uri_string, secure_connection);
+       if (cbhttp->priv->components) {
+               g_hash_table_destroy (cbhttp->priv->components);
+               cbhttp->priv->components = NULL;
+       }
 
-       g_free (uri_string);
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
 
-       return backend->priv->uri;
+       return TRUE;
 }
 
-static void
-begin_retrieval_cb (GTask *task,
-                   gpointer source_object,
-                   gpointer task_tada,
-                   GCancellable *cancellable)
+static gchar *
+ecb_http_read_stream_sync (GInputStream *input_stream,
+                          goffset expected_length,
+                          GCancellable *cancellable,
+                          GError **error)
 {
-       ECalBackendHttp *backend = source_object;
-       const gchar *uri;
-       gchar *certificate_pem = NULL;
-       GTlsCertificateFlags certificate_errors = 0;
-       GError *error = NULL;
-
-       if (!e_backend_get_online (E_BACKEND (backend)) ||
-           backend->priv->is_loading)
-               return;
-
-       d (g_message ("Starting retrieval...\n"));
-
-       backend->priv->is_loading = TRUE;
-
-       uri = cal_backend_http_ensure_uri (backend);
-       cal_backend_http_load (backend, uri, &certificate_pem, &certificate_errors, cancellable, &error);
-
-       if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
-           g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-               GError *local_error = NULL;
-               ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
-
-               if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-                       reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
-               }
-
-               e_backend_credentials_required_sync (E_BACKEND (backend),
-                       reason, certificate_pem, certificate_errors, error, cancellable, &local_error);
-
-               g_clear_error (&error);
-               error = local_error;
-       } else if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
-               GError *local_error = NULL;
+       GString *icalstr;
+       void *buffer;
+       gsize nread = 0;
+       gboolean success = FALSE;
 
-               e_backend_credentials_required_sync (E_BACKEND (backend), 
E_SOURCE_CREDENTIALS_REASON_REJECTED,
-                       certificate_pem, certificate_errors, error, cancellable, &local_error);
+       g_return_val_if_fail (G_IS_INPUT_STREAM (input_stream), NULL);
 
-               g_clear_error (&error);
-               error = local_error;
-       }
-
-       g_free (certificate_pem);
-       backend->priv->is_loading = FALSE;
+       icalstr = g_string_sized_new (expected_length > 0 ? expected_length + 1 : 1024);
 
-       /* Ignore cancellations. */
-       if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
-               g_error_free (error);
+       buffer = g_malloc (16384);
 
-       } else if (error != NULL) {
-               e_cal_backend_notify_error (
-                       E_CAL_BACKEND (backend),
-                       error->message);
-               empty_cache (backend);
-               g_error_free (error);
+       while (success = g_input_stream_read_all (input_stream, buffer, 16384, &nread, cancellable, error),
+              success && nread > 0) {
+               g_string_append_len (icalstr, (const gchar *) buffer, nread);
        }
 
-       d (g_message ("Retrieval really done.\n"));
-}
-
-static void
-http_cal_schedule_begin_retrieval (ECalBackendHttp *cbhttp)
-{
-       GTask *task;
-
-       task = g_task_new (cbhttp, NULL, NULL, NULL);
+       g_free (buffer);
 
-       g_task_run_in_thread (task, begin_retrieval_cb);
-
-       g_object_unref (task);
+       return g_string_free (icalstr, !success);
 }
 
-static void
-source_changed_cb (ESource *source,
-                   ECalBackendHttp *cbhttp)
+static gboolean
+ecb_http_get_changes_sync (ECalMetaBackend *meta_backend,
+                          const gchar *last_sync_tag,
+                          gboolean is_repeat,
+                          gchar **out_new_sync_tag,
+                          gboolean *out_repeat,
+                          GSList **out_created_objects,
+                          GSList **out_modified_objects,
+                          GSList **out_removed_objects,
+                          GCancellable *cancellable,
+                          GError **error)
 {
-       g_return_if_fail (E_IS_CAL_BACKEND_HTTP (cbhttp));
-
-       g_object_ref (cbhttp);
-
-       if (cbhttp->priv->uri != NULL) {
-               gboolean uri_changed;
-               const gchar *new_uri;
-               gchar *old_uri;
-
-               old_uri = g_strdup (cbhttp->priv->uri);
-
-               g_free (cbhttp->priv->uri);
-               cbhttp->priv->uri = NULL;
-
-               new_uri = cal_backend_http_ensure_uri (cbhttp);
+       ECalBackendHttp *cbhttp;
+       SoupMessage *message;
+       gchar *icalstring;
+       icalcomponent *vcalendar;
+       gboolean success = TRUE;
 
-               uri_changed = (g_strcmp0 (old_uri, new_uri) != 0);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
 
-               if (uri_changed && !cbhttp->priv->is_loading)
-                       http_cal_schedule_begin_retrieval (cbhttp);
+       cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
 
-               g_free (old_uri);
+       if (!cbhttp->priv->request || !cbhttp->priv->input_stream) {
+               g_propagate_error (error, EDC_ERROR (RepositoryOffline));
+               return FALSE;
        }
 
-       g_object_unref (cbhttp);
-}
+       message = soup_request_http_get_message (cbhttp->priv->request);
+       if (message) {
+               const gchar *new_etag;
 
-static void
-http_cal_reload_cb (ESource *source,
-                    gpointer user_data)
-{
-       ECalBackendHttp *cbhttp = user_data;
+               new_etag = soup_message_headers_get_one (message->response_headers, "ETag");
+               if (new_etag && !*new_etag) {
+                       new_etag = NULL;
+               } else if (g_strcmp0 (last_sync_tag, new_etag) == 0) {
+                       /* Nothing changed */
+                       g_object_unref (message);
 
-       g_return_if_fail (E_IS_CAL_BACKEND_HTTP (cbhttp));
+                       ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
 
-       if (!e_backend_get_online (E_BACKEND (cbhttp)))
-               return;
-
-       http_cal_schedule_begin_retrieval (cbhttp);
-}
+                       return TRUE;
+               }
 
-/* Open handler for the file backend */
-static void
-e_cal_backend_http_open (ECalBackendSync *backend,
-                         EDataCal *cal,
-                         GCancellable *cancellable,
-                         gboolean only_if_exists,
-                         GError **perror)
-{
-       ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-       ESource *source;
-       ESourceWebdav *webdav_extension;
-       const gchar *extension_name;
-       const gchar *cache_dir;
-       gboolean opened = TRUE;
-       gchar *tmp;
-       GError *local_error = NULL;
+               *out_new_sync_tag = g_strdup (new_etag);
+       }
 
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
-       priv = cbhttp->priv;
+       g_clear_object (&message);
 
-       /* already opened, thus can skip all this initialization */
-       if (priv->opened)
-               return;
+       icalstring = ecb_http_read_stream_sync (cbhttp->priv->input_stream,
+               soup_request_get_content_length (SOUP_REQUEST (cbhttp->priv->request)), cancellable, error);
 
-       source = e_backend_get_source (E_BACKEND (backend));
-       cache_dir = e_cal_backend_get_cache_dir (E_CAL_BACKEND (backend));
+       if (!icalstring) {
+               /* The error is already set */
+               e_cal_meta_backend_empty_cache_sync (meta_backend, cancellable, NULL);
+               ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
+               return FALSE;
+       }
 
-       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
-       webdav_extension = e_source_get_extension (source, extension_name);
+       vcalendar = icalparser_parse_string (icalstring);
 
-       e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
+       g_free (icalstring);
 
-       if (priv->source_changed_id == 0) {
-               priv->source_changed_id = g_signal_connect (
-                       source, "changed",
-                       G_CALLBACK (source_changed_cb), cbhttp);
+       if (!vcalendar) {
+               g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Bad file format."));
+               e_cal_meta_backend_empty_cache_sync (meta_backend, cancellable, NULL);
+               ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
+               return FALSE;
        }
 
-       /* always read uri again */
-       tmp = priv->uri;
-       priv->uri = NULL;
-       g_free (tmp);
-
-       if (priv->store == NULL) {
-               /* remove the old cache while migrating to ECalBackendStore */
-               e_cal_backend_cache_remove (cache_dir, "cache.xml");
-               priv->store = e_cal_backend_store_new (
-                       cache_dir, E_TIMEZONE_CACHE (backend));
-               e_cal_backend_store_load (priv->store);
-
-               if (!priv->store) {
-                       g_propagate_error (
-                               perror, EDC_ERROR_EX (OtherError,
-                               _("Could not create cache file")));
-                       return;
-               }
+       if (icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT) {
+               icalcomponent_free (vcalendar);
+               g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Not a calendar."));
+               e_cal_meta_backend_empty_cache_sync (meta_backend, cancellable, NULL);
+               ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
+               return FALSE;
        }
 
-       e_cal_backend_set_writable (E_CAL_BACKEND (backend), FALSE);
-
-       if (e_backend_get_online (E_BACKEND (backend))) {
-               gchar *certificate_pem = NULL;
-               GTlsCertificateFlags certificate_errors = 0;
-               const gchar *uri;
+       success = e_cal_meta_backend_gather_timezones_sync (meta_backend, vcalendar, TRUE, cancellable, 
error);
+       if (success) {
+               icalcomponent_kind kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+               icalcomponent *subcomp;
+               GHashTable *components;
 
-               uri = cal_backend_http_ensure_uri (cbhttp);
+               components = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) 
icalcomponent_free);
 
-               opened = cal_backend_http_load (cbhttp, uri, &certificate_pem,
-                       &certificate_errors, cancellable, &local_error);
+               while (subcomp = icalcomponent_get_first_component (vcalendar, kind), subcomp) {
+                       icalcomponent *existing_icalcomp;
+                       gpointer orig_key, orig_value;
+                       const gchar *uid;
 
-               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
-                   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED) ||
-                   (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) &&
-                   !cbhttp->priv->password)) {
-                       GError *local_error2 = NULL;
-                       ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+                       icalcomponent_remove_component (vcalendar, subcomp);
 
-                       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-                               reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+                       if (!icalcomponent_get_first_property (subcomp, ICAL_UID_PROPERTY)) {
+                               gchar *new_uid = e_cal_component_gen_uid ();
+                               icalcomponent_set_uid (subcomp, new_uid);
+                               g_free (new_uid);
                        }
 
-                       e_backend_credentials_required_sync (E_BACKEND (cbhttp), reason, certificate_pem,
-                               certificate_errors, local_error, cancellable, &local_error2);
-                       g_clear_error (&local_error);
-                       local_error = local_error2;
-               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
-                       GError *local_error2 = NULL;
+                       uid = icalcomponent_get_uid (subcomp);
 
-                       e_backend_credentials_required_sync (E_BACKEND (cbhttp), 
E_SOURCE_CREDENTIALS_REASON_REJECTED,
-                               certificate_pem, certificate_errors, local_error, cancellable, &local_error2);
-
-                       g_clear_error (&local_error);
-                       local_error = local_error2;
-               }
-
-               g_free (certificate_pem);
-
-               if (local_error != NULL)
-                       g_propagate_error (perror, local_error);
-       }
-
-       if (opened) {
-               if (!priv->reload_timeout_id)
-                       priv->reload_timeout_id = e_source_refresh_add_timeout (source, NULL, 
http_cal_reload_cb, backend, NULL);
-       }
-}
-
-static void
-e_cal_backend_http_refresh (ECalBackendSync *backend,
-                            EDataCal *cal,
-                            GCancellable *cancellable,
-                            GError **perror)
-{
-       ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-       ESource *source;
-
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
-       priv = cbhttp->priv;
-
-       if (!priv->opened ||
-           priv->is_loading)
-               return;
-
-       source = e_backend_get_source (E_BACKEND (cbhttp));
-       g_return_if_fail (source != NULL);
-
-       e_source_refresh_force_timeout (source);
-}
+                       if (!g_hash_table_lookup_extended (components, uid, &orig_key, &orig_value)) {
+                               orig_key = NULL;
+                               orig_value = NULL;
+                       }
 
-/* Set_mode handler for the http backend */
-static void
-e_cal_backend_http_notify_online_cb (ECalBackend *backend,
-                                     GParamSpec *pspec)
-{
-       gboolean loaded;
-       gboolean online;
+                       existing_icalcomp = orig_value;
+                       if (existing_icalcomp) {
+                               if (icalcomponent_isa (existing_icalcomp) != ICAL_VCALENDAR_COMPONENT) {
+                                       icalcomponent *vcal;
 
-       online = e_backend_get_online (E_BACKEND (backend));
-       loaded = e_cal_backend_is_opened (backend);
+                                       vcal = e_cal_util_new_top_level ();
 
-       if (online && loaded)
-               http_cal_schedule_begin_retrieval (E_CAL_BACKEND_HTTP (backend));
-}
+                                       g_warn_if_fail (g_hash_table_steal (components, uid));
 
-/* Get_object_component handler for the http backend */
-static void
-e_cal_backend_http_get_object (ECalBackendSync *backend,
-                               EDataCal *cal,
-                               GCancellable *cancellable,
-                               const gchar *uid,
-                               const gchar *rid,
-                               gchar **object,
-                               GError **error)
-{
-       ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-       ECalComponent *comp = NULL;
+                                       icalcomponent_add_component (vcal, existing_icalcomp);
+                                       g_hash_table_insert (components, g_strdup (uid), vcal);
 
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
-       priv = cbhttp->priv;
+                                       g_free (orig_key);
 
-       if (!priv->store) {
-               g_propagate_error (error, EDC_ERROR (ObjectNotFound));
-               return;
-       }
+                                       existing_icalcomp = vcal;
+                               }
 
-       if (rid && *rid) {
-               comp = e_cal_backend_store_get_component (priv->store, uid, rid);
-               if (!comp) {
-                       g_propagate_error (error, EDC_ERROR (ObjectNotFound));
-                       return;
+                               icalcomponent_add_component (existing_icalcomp, subcomp);
+                       } else {
+                               g_hash_table_insert (components, g_strdup (uid), subcomp);
+                       }
                }
 
-               *object = e_cal_component_get_as_string (comp);
-               g_object_unref (comp);
-       } else {
-               *object = e_cal_backend_store_get_components_by_uid_as_ical_string (priv->store, uid);
-               if (!*object)
-                       g_propagate_error (error, EDC_ERROR (ObjectNotFound));
-       }
-}
-
-/* Add_timezone handler for the file backend */
-static void
-e_cal_backend_http_add_timezone (ECalBackendSync *backend,
-                                 EDataCal *cal,
-                                 GCancellable *cancellable,
-                                 const gchar *tzobj,
-                                 GError **error)
-{
-       ETimezoneCache *timezone_cache;
-       icalcomponent *tz_comp;
-       icaltimezone *zone;
+               g_warn_if_fail (cbhttp->priv->components == NULL);
+               cbhttp->priv->components = components;
 
-       timezone_cache = E_TIMEZONE_CACHE (backend);
+               icalcomponent_free (vcalendar);
 
-       tz_comp = icalparser_parse_string (tzobj);
-       if (!tz_comp) {
-               g_propagate_error (error, EDC_ERROR (InvalidObject));
-               return;
+               success = E_CAL_META_BACKEND_CLASS (e_cal_backend_http_parent_class)->get_changes_sync 
(meta_backend,
+                       last_sync_tag, is_repeat, out_new_sync_tag, out_repeat, out_created_objects,
+                       out_modified_objects, out_removed_objects, cancellable, error);
+       } else {
+               icalcomponent_free (vcalendar);
        }
 
-       if (icalcomponent_isa (tz_comp) != ICAL_VTIMEZONE_COMPONENT) {
-               icalcomponent_free (tz_comp);
-               g_propagate_error (error, EDC_ERROR (InvalidObject));
-               return;
-       }
+       if (!success)
+               ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
 
-       zone = icaltimezone_new ();
-       icaltimezone_set_component (zone, tz_comp);
-       e_timezone_cache_add_timezone (timezone_cache, zone);
+       return success;
 }
 
-/* Get_objects_in_range handler for the file backend */
-static void
-e_cal_backend_http_get_object_list (ECalBackendSync *backend,
-                                    EDataCal *cal,
-                                    GCancellable *cancellable,
-                                    const gchar *sexp,
-                                    GSList **objects,
-                                    GError **perror)
+static gboolean
+ecb_http_list_existing_sync (ECalMetaBackend *meta_backend,
+                            gchar **out_new_sync_tag,
+                            GSList **out_existing_objects, /* ECalMetaBackendInfo * */
+                            GCancellable *cancellable,
+                            GError **error)
 {
        ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-       GSList *components, *l;
-       ECalBackendSExp *cbsexp;
-       ETimezoneCache *timezone_cache;
-       time_t occur_start = -1, occur_end = -1;
-       gboolean prunning_by_time;
-
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
-       priv = cbhttp->priv;
-
-       timezone_cache = E_TIMEZONE_CACHE (backend);
-
-       if (!priv->store) {
-               g_propagate_error (perror, EDC_ERROR (NoSuchCal));
-               return;
-       }
-
-       /* process all components in the cache */
-       cbsexp = e_cal_backend_sexp_new (sexp);
-
-       *objects = NULL;
-       prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (
-               cbsexp,
-               &occur_start,
-               &occur_end);
+       ECalCache *cal_cache;
+       icalcomponent_kind kind;
+       GHashTableIter iter;
+       gpointer key, value;
 
-       components = prunning_by_time ?
-               e_cal_backend_store_get_components_occuring_in_range (priv->store, occur_start, occur_end)
-               : e_cal_backend_store_get_components (priv->store);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
 
-       for (l = components; l != NULL; l = g_slist_next (l)) {
-               if (e_cal_backend_sexp_match_comp (cbsexp, E_CAL_COMPONENT (l->data), timezone_cache)) {
-                       *objects = g_slist_append (*objects, e_cal_component_get_as_string (l->data));
-               }
-       }
+       cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
 
-       g_slist_foreach (components, (GFunc) g_object_unref, NULL);
-       g_slist_free (components);
-       g_object_unref (cbsexp);
-}
+       *out_existing_objects = NULL;
 
-static void
-e_cal_backend_http_start_view (ECalBackend *backend,
-                               EDataCalView *query)
-{
-       ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-       GSList *components, *l;
-       GSList *objects = NULL;
-       ECalBackendSExp *cbsexp;
-       ETimezoneCache *timezone_cache;
-       time_t occur_start = -1, occur_end = -1;
-       gboolean prunning_by_time;
+       g_return_val_if_fail (cbhttp->priv->components != NULL, FALSE);
 
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
-       priv = cbhttp->priv;
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
 
-       timezone_cache = E_TIMEZONE_CACHE (backend);
+       kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
 
-       cbsexp = e_data_cal_view_get_sexp (query);
+       g_hash_table_iter_init (&iter, cbhttp->priv->components);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               icalcomponent *icalcomp = value;
+               ECalMetaBackendInfo *nfo;
+               const gchar *uid;
+               gchar *revision, *object;
 
-       d (g_message (G_STRLOC ": Starting query (%s)", e_cal_backend_sexp_text (cbsexp)));
+               if (icalcomp && icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT)
+                       icalcomp = icalcomponent_get_first_component (icalcomp, kind);
 
-       if (!priv->store) {
-               GError *error = EDC_ERROR (NoSuchCal);
-               e_data_cal_view_notify_complete (query, error);
-               g_error_free (error);
-               return;
-       }
+               if (!icalcomp)
+                       continue;
 
-       /* process all components in the cache */
-       objects = NULL;
-       prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (
-               cbsexp,
-               &occur_start,
-               &occur_end);
+               uid = icalcomponent_get_uid (icalcomp);
+               revision = e_cal_cache_dup_component_revision (cal_cache, icalcomp);
+               object = icalcomponent_as_ical_string_r (value);
 
-       components = prunning_by_time ?
-               e_cal_backend_store_get_components_occuring_in_range (priv->store, occur_start, occur_end)
-               : e_cal_backend_store_get_components (priv->store);
+               nfo = e_cal_meta_backend_info_new (uid, revision, object, NULL);
 
-       for (l = components; l != NULL; l = g_slist_next (l)) {
-               ECalComponent *comp = l->data;
+               *out_existing_objects = g_slist_prepend (*out_existing_objects, nfo);
 
-               if (e_cal_backend_sexp_match_comp (cbsexp, comp, timezone_cache)) {
-                       objects = g_slist_append (objects, comp);
-               }
+               g_free (revision);
+               g_free (object);
        }
 
-       e_data_cal_view_notify_components_added (query, objects);
+       g_object_unref (cal_cache);
 
-       g_slist_free_full (components, g_object_unref);
-       g_slist_free (objects);
+       ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
 
-       e_data_cal_view_notify_complete (query, NULL /* Success */);
+       return TRUE;
 }
 
-/***** static icaltimezone *
-resolve_tzid (const gchar *tzid,
- *            gpointer user_data)
-{
-       icalcomponent *vcalendar_comp = user_data;
- *
-       if (!tzid || !tzid[0])
-               return NULL;
- *      else if (!strcmp (tzid, "UTC"))
-               return icaltimezone_get_utc_timezone ();
- *
-       return icalcomponent_get_timezone (vcalendar_comp, tzid);
-} *****/
-
 static gboolean
-free_busy_instance (ECalComponent *comp,
-                    time_t instance_start,
-                    time_t instance_end,
-                    gpointer data)
+ecb_http_load_component_sync (ECalMetaBackend *meta_backend,
+                             const gchar *uid,
+                             const gchar *extra,
+                             icalcomponent **out_component,
+                             gchar **out_extra,
+                             GCancellable *cancellable,
+                             GError **error)
 {
-       icalcomponent *vfb = data;
-       icalproperty *prop;
-       icalparameter *param;
-       struct icalperiodtype ipt;
-       icaltimezone *utc_zone;
-
-       utc_zone = icaltimezone_get_utc_timezone ();
-
-       ipt.start = icaltime_from_timet_with_zone (instance_start, FALSE, utc_zone);
-       ipt.end = icaltime_from_timet_with_zone (instance_end, FALSE, utc_zone);
-       ipt.duration = icaldurationtype_null_duration ();
-
-        /* add busy information to the vfb component */
-       prop = icalproperty_new (ICAL_FREEBUSY_PROPERTY);
-       icalproperty_set_freebusy (prop, ipt);
+       ECalBackendHttp *cbhttp;
+       gpointer key = NULL, value = NULL;
 
-       param = icalparameter_new_fbtype (ICAL_FBTYPE_BUSY);
-       icalproperty_add_parameter (prop, param);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_component != NULL, FALSE);
 
-       icalcomponent_add_property (vfb, prop);
+       cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
+       g_return_val_if_fail (cbhttp->priv->components != NULL, FALSE);
 
-       return TRUE;
-}
-
-static icalcomponent *
-create_user_free_busy (ECalBackendHttp *cbhttp,
-                       const gchar *address,
-                       const gchar *cn,
-                       time_t start,
-                       time_t end)
-{
-       GSList *slist = NULL, *l;
-       icalcomponent *vfb;
-       icaltimezone *utc_zone;
-       ECalBackendSExp *obj_sexp;
-       ECalBackendHttpPrivate *priv;
-       ECalBackendStore *store;
-       gchar *query, *iso_start, *iso_end;
-
-       priv = cbhttp->priv;
-       store = priv->store;
-
-        /* create the (unique) VFREEBUSY object that we'll return */
-       vfb = icalcomponent_new_vfreebusy ();
-       if (address != NULL) {
-               icalproperty *prop;
-               icalparameter *param;
-
-               prop = icalproperty_new_organizer (address);
-               if (prop != NULL && cn != NULL) {
-                       param = icalparameter_new_cn (cn);
-                       icalproperty_add_parameter (prop, param);
-               }
-               if (prop != NULL)
-                       icalcomponent_add_property (vfb, prop);
+       if (!cbhttp->priv->components ||
+           !g_hash_table_contains (cbhttp->priv->components, uid)) {
+               g_propagate_error (error, EDC_ERROR (ObjectNotFound));
+               return FALSE;
        }
-       utc_zone = icaltimezone_get_utc_timezone ();
-       icalcomponent_set_dtstart (vfb, icaltime_from_timet_with_zone (start, FALSE, utc_zone));
-       icalcomponent_set_dtend (vfb, icaltime_from_timet_with_zone (end, FALSE, utc_zone));
-
-        /* add all objects in the given interval */
-       iso_start = isodate_from_time_t (start);
-       iso_end = isodate_from_time_t (end);
-       query = g_strdup_printf (
-               "occur-in-time-range? (make-time \"%s\") (make-time \"%s\")",
-               iso_start, iso_end);
-       obj_sexp = e_cal_backend_sexp_new (query);
-       g_free (query);
-       g_free (iso_start);
-       g_free (iso_end);
-
-       if (!obj_sexp)
-               return vfb;
-
-       slist = e_cal_backend_store_get_components (store);
-
-       for (l = slist; l; l = g_slist_next (l)) {
-               ECalComponent *comp = l->data;
-               icalcomponent *icalcomp, *vcalendar_comp;
-               icalproperty *prop;
-
-               icalcomp = e_cal_component_get_icalcomponent (comp);
-               if (!icalcomp)
-                       continue;
 
-                /* If the event is TRANSPARENT, skip it. */
-               prop = icalcomponent_get_first_property (
-                       icalcomp,
-                       ICAL_TRANSP_PROPERTY);
-               if (prop) {
-                       icalproperty_transp transp_val = icalproperty_get_transp (prop);
-                       if (transp_val == ICAL_TRANSP_TRANSPARENT ||
-                           transp_val == ICAL_TRANSP_TRANSPARENTNOCONFLICT)
-                               continue;
-               }
+       g_warn_if_fail (g_hash_table_lookup_extended (cbhttp->priv->components, uid, &key, &value));
+       g_warn_if_fail (g_hash_table_steal (cbhttp->priv->components, uid));
 
-               if (!e_cal_backend_sexp_match_comp (
-                       obj_sexp, l->data,
-                       E_TIMEZONE_CACHE (cbhttp)))
-                       continue;
-
-               vcalendar_comp = icalcomponent_get_parent (icalcomp);
-               if (!vcalendar_comp)
-                       vcalendar_comp = icalcomp;
-               e_cal_recur_generate_instances (
-                       comp, start, end,
-                       free_busy_instance,
-                       vfb,
-                       resolve_tzid,
-                       vcalendar_comp,
-                       icaltimezone_get_utc_timezone ());
-       }
-       g_object_unref (obj_sexp);
+       *out_component = value;
 
-       return vfb;
-}
+       g_free (key);
 
-/* Get_free_busy handler for the file backend */
-static void
-e_cal_backend_http_get_free_busy (ECalBackendSync *backend,
-                                  EDataCal *cal,
-                                  GCancellable *cancellable,
-                                  const GSList *users,
-                                  time_t start,
-                                  time_t end,
-                                  GSList **freebusy,
-                                  GError **error)
-{
-       ESourceRegistry *registry;
-       ECalBackendHttp *cbhttp;
-       ECalBackendHttpPrivate *priv;
-       gchar *address, *name;
-       icalcomponent *vfb;
-       gchar *calobj;
+       if (!g_hash_table_size (cbhttp->priv->components)) {
+               g_hash_table_destroy (cbhttp->priv->components);
+               cbhttp->priv->components = NULL;
 
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
-       priv = cbhttp->priv;
-
-       if (!priv->store) {
-               g_propagate_error (error, EDC_ERROR (NoSuchCal));
-               return;
+               ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
        }
 
-       registry = e_cal_backend_get_registry (E_CAL_BACKEND (backend));
-
-       if (users == NULL) {
-               if (e_cal_backend_mail_account_get_default (registry, &address, &name)) {
-                       vfb = create_user_free_busy (cbhttp, address, name, start, end);
-                       calobj = icalcomponent_as_ical_string_r (vfb);
-                        *freebusy = g_slist_append (*freebusy, calobj);
-                       icalcomponent_free (vfb);
-                       g_free (address);
-                       g_free (name);
-               }
-       } else {
-               const GSList *l;
-               for (l = users; l != NULL; l = l->next ) {
-                       address = l->data;
-                       if (e_cal_backend_mail_account_is_valid (registry, address, &name)) {
-                               vfb = create_user_free_busy (cbhttp, address, name, start, end);
-                               calobj = icalcomponent_as_ical_string_r (vfb);
-                                *freebusy = g_slist_append (*freebusy, calobj);
-                               icalcomponent_free (vfb);
-                               g_free (name);
-                       }
-               }
-       }
+       return value != NULL;
 }
 
 static void
-e_cal_backend_http_create_objects (ECalBackendSync *backend,
-                                   EDataCal *cal,
-                                   GCancellable *cancellable,
-                                   const GSList *calobjs,
-                                   GSList **uids,
-                                   GSList **new_components,
-                                   GError **perror)
+e_cal_backend_http_constructed (GObject *object)
 {
-       g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
+       ECalBackendHttp *cbhttp = E_CAL_BACKEND_HTTP (object);
 
-static void
-e_cal_backend_http_modify_objects (ECalBackendSync *backend,
-                                   EDataCal *cal,
-                                   GCancellable *cancellable,
-                                   const GSList *calobjs,
-                                   ECalObjModType mod,
-                                   GSList **old_components,
-                                   GSList **new_components,
-                                   GError **perror)
-{
-       g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_http_parent_class)->constructed (object);
 
-/* Remove_objects handler for the file backend */
-static void
-e_cal_backend_http_remove_objects (ECalBackendSync *backend,
-                                   EDataCal *cal,
-                                   GCancellable *cancellable,
-                                   const GSList *ids,
-                                   ECalObjModType mod,
-                                   GSList **old_components,
-                                   GSList **new_components,
-                                   GError **perror)
-{
-       *old_components = *new_components = NULL;
+       cbhttp->priv->session = e_soup_session_new (e_backend_get_source (E_BACKEND (cbhttp)));
 
-       g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
+       e_soup_session_setup_logging (cbhttp->priv->session, g_getenv ("WEBCAL_DEBUG"));
 
-/* Update_objects handler for the file backend. */
-static void
-e_cal_backend_http_receive_objects (ECalBackendSync *backend,
-                                    EDataCal *cal,
-                                    GCancellable *cancellable,
-                                    const gchar *calobj,
-                                    GError **perror)
-{
-       g_propagate_error (perror, EDC_ERROR (PermissionDenied));
+       e_binding_bind_property (
+               cbhttp, "proxy-resolver",
+               cbhttp->priv->session, "proxy-resolver",
+               G_BINDING_SYNC_CREATE);
 }
 
 static void
-e_cal_backend_http_send_objects (ECalBackendSync *backend,
-                                 EDataCal *cal,
-                                 GCancellable *cancellable,
-                                 const gchar *calobj,
-                                 GSList **users,
-                                 gchar **modified_calobj,
-                                 GError **perror)
-{
-       *users = NULL;
-       *modified_calobj = NULL;
-
-       g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
-
-static ESourceAuthenticationResult
-e_cal_backend_http_authenticate_sync (EBackend *backend,
-                                     const ENamedParameters *credentials,
-                                     gchar **out_certificate_pem,
-                                     GTlsCertificateFlags *out_certificate_errors,
-                                     GCancellable *cancellable,
-                                     GError **error)
+e_cal_backend_http_dispose (GObject *object)
 {
        ECalBackendHttp *cbhttp;
-       ESourceAuthenticationResult result;
-       const gchar *uri, *username;
-       GError *local_error = NULL;
 
-       cbhttp = E_CAL_BACKEND_HTTP (backend);
+       cbhttp = E_CAL_BACKEND_HTTP (object);
 
-       g_free (cbhttp->priv->username);
-       cbhttp->priv->username = NULL;
+       g_clear_object (&cbhttp->priv->request);
+       g_clear_object (&cbhttp->priv->input_stream);
 
-       g_free (cbhttp->priv->password);
-       cbhttp->priv->password = g_strdup (e_named_parameters_get (credentials, 
E_SOURCE_CREDENTIAL_PASSWORD));
+       if (cbhttp->priv->session) {
+               soup_session_abort (SOUP_SESSION (cbhttp->priv->session));
+               g_clear_object (&cbhttp->priv->session);
+       }
 
-       username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
-       if (username && *username) {
-               cbhttp->priv->username = g_strdup (username);
+       if (cbhttp->priv->components) {
+               g_hash_table_destroy (cbhttp->priv->components);
+               cbhttp->priv->components = NULL;
        }
 
-       uri = cal_backend_http_ensure_uri (cbhttp);
-       if (cal_backend_http_load (cbhttp, uri, out_certificate_pem, out_certificate_errors, cancellable, 
&local_error)) {
-               if (!cbhttp->priv->reload_timeout_id) {
-                       ESource *source = e_backend_get_source (backend);
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_http_parent_class)->dispose (object);
+}
 
-                       cbhttp->priv->reload_timeout_id = e_source_refresh_add_timeout (source, NULL, 
http_cal_reload_cb, backend, NULL);
-               }
-       }
+static void
+e_cal_backend_http_finalize (GObject *object)
+{
+       ECalBackendHttp *cbhttp = E_CAL_BACKEND_HTTP (object);
 
-       if (local_error == NULL) {
-               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
-       } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
-               result = E_SOURCE_AUTHENTICATION_REJECTED;
-               g_clear_error (&local_error);
-       } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
-               result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
-               g_propagate_error (error, local_error);
-       } else {
-               result = E_SOURCE_AUTHENTICATION_ERROR;
-               g_propagate_error (error, local_error);
-       }
+       g_clear_object (&cbhttp->priv->session);
 
-       return result;
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_http_parent_class)->finalize (object);
 }
 
-/* Object initialization function for the file backend */
 static void
 e_cal_backend_http_init (ECalBackendHttp *cbhttp)
 {
-       cbhttp->priv = E_CAL_BACKEND_HTTP_GET_PRIVATE (cbhttp);
+       cbhttp->priv = G_TYPE_INSTANCE_GET_PRIVATE (cbhttp, E_TYPE_CAL_BACKEND_HTTP, ECalBackendHttpPrivate);
 
-       g_signal_connect (
-               cbhttp, "notify::online",
-               G_CALLBACK (e_cal_backend_http_notify_online_cb), NULL);
+       e_cal_backend_set_writable (E_CAL_BACKEND (cbhttp), FALSE);
 }
 
-/* Class initialization function for the file backend */
 static void
-e_cal_backend_http_class_init (ECalBackendHttpClass *class)
+e_cal_backend_http_class_init (ECalBackendHttpClass *klass)
 {
        GObjectClass *object_class;
-       EBackendClass *backend_class;
-       ECalBackendClass *cal_backend_class;
-       ECalBackendSyncClass *sync_class;
-
-       g_type_class_add_private (class, sizeof (ECalBackendHttpPrivate));
-
-       object_class = (GObjectClass *) class;
-       backend_class = E_BACKEND_CLASS (class);
-       cal_backend_class = (ECalBackendClass *) class;
-       sync_class = (ECalBackendSyncClass *) class;
-
+       ECalBackendSyncClass *cal_backend_sync_class;
+       ECalMetaBackendClass *cal_meta_backend_class;
+
+       g_type_class_add_private (klass, sizeof (ECalBackendHttpPrivate));
+
+       cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+       cal_meta_backend_class->connect_sync = ecb_http_connect_sync;
+       cal_meta_backend_class->disconnect_sync = ecb_http_disconnect_sync;
+       cal_meta_backend_class->get_changes_sync = ecb_http_get_changes_sync;
+       cal_meta_backend_class->list_existing_sync = ecb_http_list_existing_sync;
+       cal_meta_backend_class->load_component_sync = ecb_http_load_component_sync;
+
+       /* Setting these methods to NULL will cause "Not supported" error,
+          which is more accurate than "Permission denied" error */
+       cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+       cal_backend_sync_class->create_objects_sync = NULL;
+       cal_backend_sync_class->modify_objects_sync = NULL;
+       cal_backend_sync_class->remove_objects_sync = NULL;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_cal_backend_http_constructed;
        object_class->dispose = e_cal_backend_http_dispose;
        object_class->finalize = e_cal_backend_http_finalize;
-       object_class->constructed = e_cal_backend_http_constructed;
-
-       backend_class->authenticate_sync = e_cal_backend_http_authenticate_sync;
-
-       /* Execute one method at a time. */
-       cal_backend_class->use_serial_dispatch_queue = TRUE;
-       cal_backend_class->get_backend_property = e_cal_backend_http_get_backend_property;
-       cal_backend_class->start_view = e_cal_backend_http_start_view;
-
-       sync_class->open_sync = e_cal_backend_http_open;
-       sync_class->refresh_sync = e_cal_backend_http_refresh;
-       sync_class->create_objects_sync = e_cal_backend_http_create_objects;
-       sync_class->modify_objects_sync = e_cal_backend_http_modify_objects;
-       sync_class->remove_objects_sync = e_cal_backend_http_remove_objects;
-       sync_class->receive_objects_sync = e_cal_backend_http_receive_objects;
-       sync_class->send_objects_sync = e_cal_backend_http_send_objects;
-       sync_class->get_object_sync = e_cal_backend_http_get_object;
-       sync_class->get_object_list_sync = e_cal_backend_http_get_object_list;
-       sync_class->add_timezone_sync = e_cal_backend_http_add_timezone;
-       sync_class->get_free_busy_sync = e_cal_backend_http_get_free_busy;
 }
diff --git a/src/calendar/backends/http/e-cal-backend-http.h b/src/calendar/backends/http/e-cal-backend-http.h
index b45ff33..b7c5df3 100644
--- a/src/calendar/backends/http/e-cal-backend-http.h
+++ b/src/calendar/backends/http/e-cal-backend-http.h
@@ -47,12 +47,12 @@ typedef struct _ECalBackendHttpClass ECalBackendHttpClass;
 typedef struct _ECalBackendHttpPrivate ECalBackendHttpPrivate;
 
 struct _ECalBackendHttp {
-       ECalBackendSync backend;
+       ECalMetaBackend backend;
        ECalBackendHttpPrivate *priv;
 };
 
 struct _ECalBackendHttpClass {
-       ECalBackendSyncClass parent_class;
+       ECalMetaBackendClass parent_class;
 };
 
 GType          e_cal_backend_http_get_type     (void);
diff --git a/src/calendar/libecal/e-cal-util.c b/src/calendar/libecal/e-cal-util.c
index bd10022..a9426f8 100644
--- a/src/calendar/libecal/e-cal-util.c
+++ b/src/calendar/libecal/e-cal-util.c
@@ -1496,7 +1496,6 @@ componenttime_to_utc_timet (const ECalComponentDateTime *dt_time,
                if (dt_time->tzid)
                        zone = tz_cb (dt_time->tzid, tz_cb_data);
 
-               // zone = icaltimezone_get_utc_timezone ();
                timet = icaltime_as_timet_with_zone (
                        *dt_time->value, zone ? zone : default_zone);
        }
@@ -1530,6 +1529,7 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
 {
        struct icalrecurrencetype ir;
        ECalComponentDateTime dt_start, dt_end;
+       time_t duration;
 
        g_return_if_fail (comp != NULL);
        g_return_if_fail (start != NULL);
@@ -1545,6 +1545,14 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
 
        e_cal_component_free_datetime (&dt_start);
 
+       e_cal_component_get_dtend (comp, &dt_end);
+       duration = componenttime_to_utc_timet (&dt_end, tz_cb, tz_cb_data, default_timezone);
+       if (duration <= 0 || *start == _TIME_MIN || *start > duration)
+               duration = 0;
+       else
+               duration = duration - *start;
+       e_cal_component_free_datetime (&dt_end);
+
        /* find out end date of component */
        *end = _TIME_MAX;
 
@@ -1578,6 +1586,8 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
        } else {
                /* ALARMS, EVENTS: DTEND and reccurences */
 
+               time_t may_end = _TIME_MIN;
+
                if (e_cal_component_has_recurrences (comp)) {
                        GSList *rrules = NULL;
                        GSList *exrules = NULL;
@@ -1600,9 +1610,9 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
                                        &ir, prop, utc_zone, TRUE);
 
                                if (rule_end == -1) /* repeats forever */
-                                       *end = _TIME_MAX;
-                               else if (rule_end > *end) /* new maximum */
-                                       *end = rule_end;
+                                       may_end = _TIME_MAX;
+                               else if (rule_end + duration > may_end) /* new maximum */
+                                       may_end = rule_end + duration;
                        }
 
                        /* Do the EXRULEs. */
@@ -1617,9 +1627,9 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
                                        &ir, prop, utc_zone, TRUE);
 
                                if (rule_end == -1) /* repeats forever */
-                                       *end = _TIME_MAX;
-                               else if (rule_end > *end)
-                                       *end = rule_end;
+                                       may_end = _TIME_MAX;
+                               else if (rule_end + duration > may_end)
+                                       may_end = rule_end + duration;
                        }
 
                        /* Do the RDATEs */
@@ -1639,12 +1649,14 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
                                        rdate_end = icaltime_as_timet (p->u.end);
 
                                if (rdate_end == -1) /* repeats forever */
-                                       *end = _TIME_MAX;
-                               else if (rdate_end > *end)
-                                       *end = rdate_end;
+                                       may_end = _TIME_MAX;
+                               else if (rdate_end > may_end)
+                                       may_end = rdate_end;
                        }
 
                        e_cal_component_free_period_list (rdates);
+               } else if (*start != _TIME_MIN) {
+                       may_end = *start;
                }
 
                /* Get dtend of the component and convert it to UTC */
@@ -1656,11 +1668,179 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
                        dtend_time = componenttime_to_utc_timet (
                                &dt_end, tz_cb, tz_cb_data, default_timezone);
 
-                       if (dtend_time == -1 || (dtend_time > *end))
-                               *end = dtend_time;
+                       if (dtend_time == -1 || (dtend_time > may_end))
+                               may_end = dtend_time;
+               } else {
+                       may_end = _TIME_MAX;
                }
 
                e_cal_component_free_datetime (&dt_end);
+
+               *end = may_end == _TIME_MIN ? _TIME_MAX : may_end;
+       }
+}
+
+/**
+ * e_cal_util_find_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Searches for an X property named @x_name within X properties
+ * of @icalcomp and returns it.
+ *
+ * Returns: (nullable) (transfer none): the first X icalproperty named
+ *    @x_name, or %NULL, when none found. The returned structure is owned
+ *    by @icalcomp.
+ *
+ * Since: 3.26
+ **/
+icalproperty *
+e_cal_util_find_x_property (icalcomponent *icalcomp,
+                           const gchar *x_name)
+{
+       icalproperty *prop;
+
+       g_return_val_if_fail (icalcomp != NULL, NULL);
+       g_return_val_if_fail (x_name != NULL, NULL);
+
+       for (prop = icalcomponent_get_first_property (icalcomp, ICAL_X_PROPERTY);
+            prop;
+            prop = icalcomponent_get_next_property (icalcomp, ICAL_X_PROPERTY)) {
+               const gchar *prop_name = icalproperty_get_x_name (prop);
+
+               if (g_strcmp0 (prop_name, x_name) == 0)
+                       break;
+       }
+
+       return prop;
+}
+
+/**
+ * e_cal_util_dup_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Searches for an X property named @x_name within X properties
+ * of @icalcomp and returns its value as a newly allocated string.
+ * Free it with g_free(), when no longer needed.
+ *
+ * Returns: (nullable) (transfer full): Newly allocated value of the first @x_name
+ *    X property in @icalcomp, or %NULL, if not found.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cal_util_dup_x_property (icalcomponent *icalcomp,
+                          const gchar *x_name)
+{
+       icalproperty *prop;
+
+       g_return_val_if_fail (icalcomp != NULL, NULL);
+       g_return_val_if_fail (x_name != NULL, NULL);
+
+       prop = e_cal_util_find_x_property (icalcomp, x_name);
+
+       if (!prop)
+               return NULL;
+
+       return icalproperty_get_value_as_string_r (prop);
+}
+
+/**
+ * e_cal_util_get_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Searches for an X property named @x_name within X properties
+ * of @icalcomp and returns its value. The returned string is
+ * owned by libical. See e_cal_util_dup_x_property().
+ *
+ * Returns: (nullable) (transfer none): Value of the first @x_name
+ *    X property in @icalcomp, or %NULL, if not found.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cal_util_get_x_property (icalcomponent *icalcomp,
+                          const gchar *x_name)
+{
+       icalproperty *prop;
+
+       g_return_val_if_fail (icalcomp != NULL, NULL);
+       g_return_val_if_fail (x_name != NULL, NULL);
+
+       prop = e_cal_util_find_x_property (icalcomp, x_name);
+
+       if (!prop)
+               return NULL;
+
+       return icalproperty_get_value_as_string (prop);
+}
+
+/**
+ * e_cal_util_set_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ * @value: (nullable): a value to set, or %NULL
+ *
+ * Sets a value of the first X property named @x_name in @icalcomp,
+ * if any such already exists, or adds a new property with this name
+ * and value. As a special case, if @value is %NULL, then removes
+ * the first X property names @x_name from @icalcomp instead.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_util_set_x_property (icalcomponent *icalcomp,
+                          const gchar *x_name,
+                          const gchar *value)
+{
+       icalproperty *prop;
+
+       g_return_if_fail (icalcomp != NULL);
+       g_return_if_fail (x_name != NULL);
+
+       if (!value) {
+               e_cal_util_remove_x_property (icalcomp, x_name);
+               return;
+       }
+
+       prop = e_cal_util_find_x_property (icalcomp, x_name);
+       if (prop) {
+               icalproperty_set_value_from_string (prop, value, "NO");
+       } else {
+               prop = icalproperty_new_x (value);
+               icalproperty_set_x_name (prop, x_name);
+               icalcomponent_add_property (icalcomp, prop);
        }
 }
 
+/**
+ * e_cal_util_remove_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Removes the first X property named @x_name in @icalcomp.
+ *
+ * Returns: %TRUE, when any such had been found and removed, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_util_remove_x_property (icalcomponent *icalcomp,
+                             const gchar *x_name)
+{
+       icalproperty *prop;
+
+       g_return_val_if_fail (icalcomp != NULL, FALSE);
+       g_return_val_if_fail (x_name != NULL, FALSE);
+
+       prop = e_cal_util_find_x_property (icalcomp, x_name);
+       if (!prop)
+               return FALSE;
+
+       icalcomponent_remove_property (icalcomp, prop);
+       icalproperty_free (prop);
+
+       return TRUE;
+}
diff --git a/src/calendar/libecal/e-cal-util.h b/src/calendar/libecal/e-cal-util.h
index 86c90cd..ce4caab 100644
--- a/src/calendar/libecal/e-cal-util.h
+++ b/src/calendar/libecal/e-cal-util.h
@@ -248,6 +248,18 @@ void               e_cal_util_get_component_occur_times
                                                 const icaltimezone *default_timezone,
                                                 icalcomponent_kind kind);
 
+icalproperty * e_cal_util_find_x_property      (icalcomponent *icalcomp,
+                                                const gchar *x_name);
+gchar *                e_cal_util_dup_x_property       (icalcomponent *icalcomp,
+                                                const gchar *x_name);
+const gchar *  e_cal_util_get_x_property       (icalcomponent *icalcomp,
+                                                const gchar *x_name);
+void           e_cal_util_set_x_property       (icalcomponent *icalcomp,
+                                                const gchar *x_name,
+                                                const gchar *value);
+gboolean       e_cal_util_remove_x_property    (icalcomponent *icalcomp,
+                                                const gchar *x_name);
+
 #ifndef EDS_DISABLE_DEPRECATED
 /* Used for mode stuff */
 typedef enum {
diff --git a/src/calendar/libedata-cal/CMakeLists.txt b/src/calendar/libedata-cal/CMakeLists.txt
index 484c21c..15df5d2 100644
--- a/src/calendar/libedata-cal/CMakeLists.txt
+++ b/src/calendar/libedata-cal/CMakeLists.txt
@@ -17,6 +17,8 @@ set(SOURCES
        e-cal-backend-sync.c
        e-cal-backend-util.c
        e-cal-backend-store.c
+       e-cal-cache.c
+       e-cal-meta-backend.c
        e-data-cal.c
        e-data-cal-factory.c
        e-data-cal-view.c
@@ -32,9 +34,11 @@ set(HEADERS
        e-cal-backend-sync.h
        e-cal-backend-util.h
        e-cal-backend-sexp.h
+       e-cal-backend-store.h
+       e-cal-cache.h
+       e-cal-meta-backend.h
        e-data-cal.h
        e-data-cal-factory.h
-       e-cal-backend-store.h
        e-data-cal-view.h
        e-subprocess-cal-factory.h
 )
diff --git a/src/calendar/libedata-cal/e-cal-backend-sexp.c b/src/calendar/libedata-cal/e-cal-backend-sexp.c
index 5da3e40..466d9d2 100644
--- a/src/calendar/libedata-cal/e-cal-backend-sexp.c
+++ b/src/calendar/libedata-cal/e-cal-backend-sexp.c
@@ -427,8 +427,8 @@ matches_attendee (ECalComponent *comp,
        for (l = a_list; l; l = l->next) {
                ECalComponentAttendee *att = l->data;
 
-               if ((att->value && e_util_strstrcase (att->value, str)) || (att->cn != NULL &&
-                                       e_util_strstrcase (att->cn, str))) {
+               if ((att->value && e_util_utf8_strstrcasedecomp (att->value, str)) ||
+                   (att->cn != NULL && e_util_utf8_strstrcasedecomp (att->cn, str))) {
                        matches = TRUE;
                        break;
                }
@@ -451,8 +451,8 @@ matches_organizer (ECalComponent *comp,
        if (str && !*str)
                return TRUE;
 
-       if ((org.value && e_util_strstrcase (org.value, str)) ||
-                       (org.cn && e_util_strstrcase (org.cn, str)))
+       if ((org.value && e_util_utf8_strstrcasedecomp (org.value, str)) ||
+           (org.cn && e_util_utf8_strstrcasedecomp (org.cn, str)))
                return TRUE;
 
        return FALSE;
@@ -536,23 +536,24 @@ matches_any (ECalComponent *comp,
 static gboolean
 matches_priority (ECalComponent *comp ,const gchar *pr)
 {
+       gboolean res = FALSE;
        gint *priority = NULL;
 
        e_cal_component_get_priority (comp, &priority);
 
-       if (!priority || !*priority)
-               return FALSE;
+       if (!priority)
+               return g_str_equal (pr, "UNDEFINED");
 
        if (g_str_equal (pr, "HIGH") && *priority <= 4)
-               return TRUE;
+               res = TRUE;
        else if (g_str_equal (pr, "NORMAL") && *priority == 5)
-               return TRUE;
+               res = TRUE;
        else if (g_str_equal (pr, "LOW") && *priority > 5)
-               return TRUE;
-       else if (g_str_equal (pr, "UNDEFINED") && (!priority || !*priority))
-               return TRUE;
+               res = TRUE;
 
-       return FALSE;
+       e_cal_component_free_priority (priority);
+
+       return res;
 }
 
 static gboolean
@@ -565,24 +566,34 @@ matches_status (ECalComponent *comp ,const gchar *str)
 
        e_cal_component_get_status (comp, &status);
 
-       if (g_str_equal (str, "NOT STARTED") && status == ICAL_STATUS_NONE)
-                       return TRUE;
-       else if (g_str_equal (str, "COMPLETED") && status == ICAL_STATUS_COMPLETED)
-                       return TRUE;
-       else if (g_str_equal (str, "CANCELLED") && status == ICAL_STATUS_CANCELLED)
-                       return TRUE;
-       else if (g_str_equal (str, "IN PROGRESS") && status == ICAL_STATUS_INPROCESS)
-                       return TRUE;
-       else if (g_str_equal (str, "NEEDS ACTION") && status == ICAL_STATUS_NEEDSACTION)
-                       return TRUE;
-       else if (g_str_equal (str, "TENTATIVE") && status == ICAL_STATUS_TENTATIVE)
-                       return TRUE;
-       else if (g_str_equal (str, "CONFIRMED") && status == ICAL_STATUS_CONFIRMED)
-                       return TRUE;
-       else if (g_str_equal (str, "DRAFT") && status == ICAL_STATUS_DRAFT)
-                       return TRUE;
-       else if (g_str_equal (str, "FINAL") && status == ICAL_STATUS_FINAL)
-                       return TRUE;
+       switch (status) {
+       case ICAL_STATUS_NONE:
+               return g_str_equal (str, "NOT STARTED");
+       case ICAL_STATUS_COMPLETED:
+               return g_str_equal (str, "COMPLETED");
+       case ICAL_STATUS_CANCELLED:
+               return g_str_equal (str, "CANCELLED");
+       case ICAL_STATUS_INPROCESS:
+               return g_str_equal (str, "IN PROGRESS");
+       case ICAL_STATUS_NEEDSACTION:
+               return g_str_equal (str, "NEEDS ACTION");
+       case ICAL_STATUS_TENTATIVE:
+               return g_str_equal (str, "TENTATIVE");
+       case ICAL_STATUS_CONFIRMED:
+               return g_str_equal (str, "CONFIRMED");
+       case ICAL_STATUS_DRAFT:
+               return g_str_equal (str, "DRAFT");
+       case ICAL_STATUS_FINAL:
+               return g_str_equal (str, "FINAL");
+       case ICAL_STATUS_SUBMITTED:
+               return g_str_equal (str, "SUBMITTED");
+       case ICAL_STATUS_PENDING:
+               return g_str_equal (str, "PENDING");
+       case ICAL_STATUS_FAILED:
+               return g_str_equal (str, "FAILED");
+       case ICAL_STATUS_X:
+               break;
+       }
 
        return FALSE;
 }
@@ -628,10 +639,13 @@ func_percent_complete (ESExp *esexp,
 
        e_cal_component_get_percent (ctx->comp, &percent);
 
-       if (percent && *percent) {
-               result = e_sexp_result_new (esexp, ESEXP_RES_INT);
-               result->value.number = *percent;
+       result = e_sexp_result_new (esexp, ESEXP_RES_INT);
 
+       if (percent) {
+               result->value.number = *percent;
+               e_cal_component_free_percent (percent);
+       } else {
+               result->value.number = -1;
        }
 
        return result;
@@ -925,6 +939,8 @@ func_has_categories (ESExp *esexp,
                result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
                result->value.boolean = FALSE;
 
+               e_cal_component_free_categories_list (categories);
+
                return result;
        }
 
@@ -1199,9 +1215,6 @@ e_cal_backend_sexp_new (const gchar *text)
        e_sexp_input_text (sexp->priv->search_sexp, text, strlen (text));
 
        if (e_sexp_parse (sexp->priv->search_sexp) == -1) {
-               g_warning (
-                       "%s: Error in parsing: %s",
-                       G_STRFUNC, e_sexp_get_error (sexp->priv->search_sexp));
                g_object_unref (sexp);
                sexp = NULL;
        }
diff --git a/src/calendar/libedata-cal/e-cal-backend.c b/src/calendar/libedata-cal/e-cal-backend.c
index 18115e8..4f1934c 100644
--- a/src/calendar/libedata-cal/e-cal-backend.c
+++ b/src/calendar/libedata-cal/e-cal-backend.c
@@ -68,6 +68,7 @@ struct _ECalBackendPrivate {
        GQueue pending_operations;
        guint32 next_operation_id;
        GSimpleAsyncResult *blocked;
+       gboolean blocked_by_custom_op;
 };
 
 struct _AsyncContext {
@@ -102,6 +103,11 @@ struct _DispatchNode {
 
        GSimpleAsyncResult *simple;
        GCancellable *cancellable;
+
+       GWeakRef *cal_backend_weak_ref;
+       ECalBackendCustomOpFunc custom_func;
+       gpointer custom_func_user_data;
+       GDestroyNotify custom_func_user_data_free;
 };
 
 struct _SignalClosure {
@@ -176,6 +182,12 @@ dispatch_node_free (DispatchNode *dispatch_node)
        g_clear_object (&dispatch_node->simple);
        g_clear_object (&dispatch_node->cancellable);
 
+       if (dispatch_node->custom_func_user_data_free)
+               dispatch_node->custom_func_user_data_free (dispatch_node->custom_func_user_data);
+
+       if (dispatch_node->cal_backend_weak_ref)
+               e_weak_ref_free (dispatch_node->cal_backend_weak_ref);
+
        g_slice_free (DispatchNode, dispatch_node);
 }
 
@@ -223,13 +235,35 @@ cal_backend_push_operation (ECalBackend *backend,
        g_mutex_unlock (&backend->priv->operation_lock);
 }
 
+static void cal_backend_unblock_operations (ECalBackend *backend, GSimpleAsyncResult *simple);
+
 static void
 cal_backend_dispatch_thread (DispatchNode *node)
 {
        GCancellable *cancellable = node->cancellable;
        GError *local_error = NULL;
 
-       if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
+       if (node->custom_func) {
+               ECalBackend *cal_backend;
+
+               cal_backend = g_weak_ref_get (node->cal_backend_weak_ref);
+               if (cal_backend &&
+                   !g_cancellable_is_cancelled (cancellable)) {
+                       node->custom_func (cal_backend, node->custom_func_user_data, cancellable, 
&local_error);
+
+                       if (local_error) {
+                               if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                                       e_cal_backend_notify_error (cal_backend, local_error->message);
+
+                               g_clear_error (&local_error);
+                       }
+               }
+
+               if (cal_backend) {
+                       cal_backend_unblock_operations (cal_backend, NULL);
+                       e_util_unref_in_thread (cal_backend);
+               }
+       } else if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
                g_simple_async_result_take_error (node->simple, local_error);
                g_simple_async_result_complete_in_idle (node->simple);
        } else {
@@ -254,7 +288,8 @@ cal_backend_dispatch_next_operation (ECalBackend *backend)
 
        /* We can't dispatch additional operations
         * while a blocking operation is in progress. */
-       if (backend->priv->blocked != NULL) {
+       if (backend->priv->blocked != NULL ||
+           backend->priv->blocked_by_custom_op) {
                g_mutex_unlock (&backend->priv->operation_lock);
                return FALSE;
        }
@@ -268,8 +303,12 @@ cal_backend_dispatch_next_operation (ECalBackend *backend)
 
        /* If this a blocking operation, block any
         * further dispatching until this finishes. */
-       if (node->blocking_operation)
-               backend->priv->blocked = g_object_ref (node->simple);
+       if (node->blocking_operation) {
+               if (node->simple)
+                       backend->priv->blocked = g_object_ref (node->simple);
+               else
+                       backend->priv->blocked_by_custom_op = TRUE;
+       }
 
        g_mutex_unlock (&backend->priv->operation_lock);
 
@@ -291,6 +330,7 @@ cal_backend_unblock_operations (ECalBackend *backend,
        g_mutex_lock (&backend->priv->operation_lock);
        if (backend->priv->blocked == simple)
                g_clear_object (&backend->priv->blocked);
+       backend->priv->blocked_by_custom_op = FALSE;
        g_mutex_unlock (&backend->priv->operation_lock);
 
        while (cal_backend_dispatch_next_operation (backend))
@@ -913,6 +953,7 @@ e_cal_backend_class_init (ECalBackendClass *class)
        backend_class = E_BACKEND_CLASS (class);
        backend_class->prepare_shutdown = cal_backend_prepare_shutdown;
 
+       class->use_serial_dispatch_queue = TRUE;
        class->get_backend_property = cal_backend_get_backend_property;
        class->shutdown = cal_backend_shutdown;
 
@@ -4574,3 +4615,52 @@ e_cal_backend_prepare_for_completion (ECalBackend *backend,
        return simple;
 }
 
+/**
+ * e_cal_backend_schedule_custom_operation:
+ * @cal_backend: an #ECalBackend
+ * @use_cancellable: (nullable): an optional #GCancellable to use for @func
+ * @func: a function to call in a dedicated thread
+ * @user_data: user data being passed to @func
+ * @user_data_free: (nullable): optional destroy call back for @user_data
+ *
+ * Schedules user function @func to be run in a dedicated thread as
+ * a blocking operation.
+ *
+ * The function adds its own reference to @use_cancellable, if not %NULL.
+ *
+ * The error returned from @func is propagated to client using
+ * e_cal_backend_notify_error() function. If it's not desired,
+ * then left the error unchanged and notify about errors manually.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_backend_schedule_custom_operation (ECalBackend *cal_backend,
+                                        GCancellable *use_cancellable,
+                                        ECalBackendCustomOpFunc func,
+                                        gpointer user_data,
+                                        GDestroyNotify user_data_free)
+{
+       DispatchNode *node;
+
+       g_return_if_fail (E_IS_CAL_BACKEND (cal_backend));
+       g_return_if_fail (func != NULL);
+
+       g_mutex_lock (&cal_backend->priv->operation_lock);
+
+       node = g_slice_new0 (DispatchNode);
+       node->blocking_operation = TRUE;
+       node->cal_backend_weak_ref = e_weak_ref_new (cal_backend);
+       node->custom_func = func;
+       node->custom_func_user_data = user_data;
+       node->custom_func_user_data_free = user_data_free;
+
+       if (G_IS_CANCELLABLE (use_cancellable))
+               node->cancellable = g_object_ref (use_cancellable);
+
+       g_queue_push_tail (&cal_backend->priv->pending_operations, node);
+
+       g_mutex_unlock (&cal_backend->priv->operation_lock);
+
+       cal_backend_dispatch_next_operation (cal_backend);
+}
diff --git a/src/calendar/libedata-cal/e-cal-backend.h b/src/calendar/libedata-cal/e-cal-backend.h
index e7bc505..91d52c7 100644
--- a/src/calendar/libedata-cal/e-cal-backend.h
+++ b/src/calendar/libedata-cal/e-cal-backend.h
@@ -116,7 +116,7 @@ struct _ECalBackend {
 /**
  * ECalBackendClass:
  * @use_serial_dispatch_queue: Whether a serial dispatch queue should
- *                             be used for this backend or not.
+ *                             be used for this backend or not. The default is %TRUE.
  * @get_backend_property: Fetch a property value by name from the backend
  * @open: Open the backend
  * @refresh: Refresh the backend
@@ -529,6 +529,30 @@ GSimpleAsyncResult *
                                                 guint opid,
                                                 GQueue **result_queue);
 
+/**
+ * ECalBackendCustomOpFunc:
+ * @cal_backend: an #ECalBackend
+ * @user_data: a function user data, as provided to e_cal_backend_schedule_custom_operation()
+ * @cancellable: an optional #GCancellable, as provided to e_cal_backend_schedule_custom_operation()
+ * @error: return location for a #GError, or %NULL
+ *
+ * A callback prototype being called in a dedicated thread, scheduled
+ * by e_cal_backend_schedule_custom_operation().
+ *
+ * Since: 3.26
+ **/
+typedef void   (* ECalBackendCustomOpFunc)     (ECalBackend *cal_backend,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+void           e_cal_backend_schedule_custom_operation
+                                               (ECalBackend *cal_backend,
+                                                GCancellable *use_cancellable,
+                                                ECalBackendCustomOpFunc func,
+                                                gpointer user_data,
+                                                GDestroyNotify user_data_free);
+
 G_END_DECLS
 
 #endif /* E_CAL_BACKEND_H */
diff --git a/src/calendar/libedata-cal/e-cal-cache.c b/src/calendar/libedata-cal/e-cal-cache.c
new file mode 100644
index 0000000..fd97743
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-cache.c
@@ -0,0 +1,3651 @@
+/* -*- 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-cal-cache
+ * @include: libedata-cal/libedata-cal.h
+ * @short_description: An #ECache descendant for calendars
+ *
+ * The #ECalCache is an API for storing and looking up calendar
+ * components in an #ECache.
+ *
+ * The API is thread safe, in the similar way as the #ECache is.
+ *
+ * Any operations which can take a lot of time to complete (depending
+ * on the size of your calendar) can be cancelled using a #GCancellable.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+#include <sqlite3.h>
+
+#include <libebackend/libebackend.h>
+#include <libecal/libecal.h>
+
+#include "e-cal-backend-sexp.h"
+
+#include "e-cal-cache.h"
+
+#define E_CAL_CACHE_VERSION            1
+
+#define ECC_TABLE_TIMEZONES            "timezones"
+
+#define ECC_COLUMN_OCCUR_START         "occur_start"
+#define ECC_COLUMN_OCCUR_END           "occur_end"
+#define ECC_COLUMN_DUE                 "due"
+#define ECC_COLUMN_COMPLETED           "completed"
+#define ECC_COLUMN_SUMMARY             "summary"
+#define ECC_COLUMN_COMMENT             "comment"
+#define ECC_COLUMN_DESCRIPTION         "description"
+#define ECC_COLUMN_LOCATION            "location"
+#define ECC_COLUMN_ATTENDEES           "attendees"
+#define ECC_COLUMN_ORGANIZER           "organizer"
+#define ECC_COLUMN_CLASSIFICATION      "classification"
+#define ECC_COLUMN_STATUS              "status"
+#define ECC_COLUMN_PRIORITY            "priority"
+#define ECC_COLUMN_PERCENT_COMPLETE    "percent_complete"
+#define ECC_COLUMN_CATEGORIES          "categories"
+#define ECC_COLUMN_HAS_ALARM           "has_alarm"
+#define ECC_COLUMN_HAS_ATTACHMENT      "has_attachment"
+#define ECC_COLUMN_HAS_START           "has_start"
+#define ECC_COLUMN_HAS_RECURRENCES     "has_recurrences"
+#define ECC_COLUMN_EXTRA               "bdata"
+
+struct _ECalCachePrivate {
+       GHashTable *loaded_timezones; /* gchar *tzid ~> icaltimezone * */
+       GHashTable *modified_timezones; /* gchar *tzid ~> icaltimezone * */
+       GRecMutex timezones_lock;
+
+       GHashTable *sexps; /* gint ~> ECalBackendSExp * */
+       GMutex sexps_lock;
+};
+
+enum {
+       DUP_COMPONENT_REVISION,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void ecc_timezone_cache_init (ETimezoneCacheInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (ECalCache, e_cal_cache, E_TYPE_CACHE,
+                        G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)
+                        G_IMPLEMENT_INTERFACE (E_TYPE_TIMEZONE_CACHE, ecc_timezone_cache_init))
+
+G_DEFINE_BOXED_TYPE (ECalCacheOfflineChange, e_cal_cache_offline_change, e_cal_cache_offline_change_copy, 
e_cal_cache_offline_change_free)
+G_DEFINE_BOXED_TYPE (ECalCacheSearchData, e_cal_cache_search_data, e_cal_cache_search_data_copy, 
e_cal_cache_search_data_free)
+
+/**
+ * e_cal_cache_offline_change_new:
+ * @uid: a unique component identifier
+ * @rid: (nullable):  a Recurrence-ID of the component
+ * @revision: (nullable): a revision of the component
+ * @object: (nullable): component itself
+ * @state: an #EOfflineState
+ *
+ * Creates a new #ECalCacheOfflineChange with the offline @state
+ * information for the given @uid.
+ *
+ * Returns: (transfer full): A new #ECalCacheOfflineChange. Free it with
+ *    e_cal_cache_offline_change_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCacheOfflineChange *
+e_cal_cache_offline_change_new (const gchar *uid,
+                               const gchar *rid,
+                               const gchar *revision,
+                               const gchar *object,
+                               EOfflineState state)
+{
+       ECalCacheOfflineChange *change;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       change = g_new0 (ECalCacheOfflineChange, 1);
+       change->uid = g_strdup (uid);
+       change->rid = g_strdup (rid);
+       change->revision = g_strdup (revision);
+       change->object = g_strdup (object);
+       change->state = state;
+
+       return change;
+}
+
+/**
+ * e_cal_cache_offline_change_copy:
+ * @change: (nullable): a source #ECalCacheOfflineChange to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @change. Free it with
+ *    e_cal_cache_offline_change_free() when no longer needed.
+ *    If the @change is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalCacheOfflineChange *
+e_cal_cache_offline_change_copy (const ECalCacheOfflineChange *change)
+{
+       if (!change)
+               return NULL;
+
+       return e_cal_cache_offline_change_new (change->uid, change->rid, change->revision, change->object, 
change->state);
+}
+
+/**
+ * e_cal_cache_offline_change_free:
+ * @change: (nullable): an #ECalCacheOfflineChange
+ *
+ * Frees the @change structure, previously allocated with e_cal_cache_offline_change_new()
+ * or e_cal_cache_offline_change_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_cache_offline_change_free (gpointer change)
+{
+       ECalCacheOfflineChange *chng = change;
+
+       if (chng) {
+               g_free (chng->uid);
+               g_free (chng->rid);
+               g_free (chng->revision);
+               g_free (chng->object);
+               g_free (chng);
+       }
+}
+
+/**
+ * e_cal_cache_search_data_new:
+ * @uid: a component UID; cannot be %NULL
+ * @rid: (nullable): a component Recurrence-ID; can be %NULL
+ * @object: the component as an iCal string; cannot be %NULL
+ * @extra: (nullable): any extra data stored with the component, or %NULL
+ *
+ * Creates a new #ECalCacheSearchData prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #ECalCacheSearchData. Free it with
+ *    e_cal_cache_search_data_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCacheSearchData *
+e_cal_cache_search_data_new (const gchar *uid,
+                            const gchar *rid,
+                            const gchar *object,
+                            const gchar *extra)
+{
+       ECalCacheSearchData *data;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+       g_return_val_if_fail (object != NULL, NULL);
+
+       data = g_new0 (ECalCacheSearchData, 1);
+       data->uid = g_strdup (uid);
+       data->rid = (rid && *rid) ? g_strdup (rid) : NULL;
+       data->object = g_strdup (object);
+       data->extra = g_strdup (extra);
+
+       return data;
+}
+
+/**
+ * e_cal_cache_search_data_copy:
+ * @data: (nullable): a source #ECalCacheSearchData to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @data. Free it with
+ *    e_cal_cache_search_data_free() when no longer needed.
+ *    If the @data is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalCacheSearchData *
+e_cal_cache_search_data_copy (const ECalCacheSearchData *data)
+{
+       if (!data)
+               return NULL;
+
+       return e_cal_cache_search_data_new (data->uid, data->rid, data->object, data->extra);
+}
+
+/**
+ * e_cal_cache_search_data_free:
+ * @ptr: (nullable): an #ECalCacheSearchData
+ *
+ * Frees the @ptr structure, previously allocated with e_cal_cache_search_data_new()
+ * or e_cal_cache_search_data_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_cache_search_data_free (gpointer ptr)
+{
+       ECalCacheSearchData *data = ptr;
+
+       if (data) {
+               g_free (data->uid);
+               g_free (data->rid);
+               g_free (data->object);
+               g_free (data->extra);
+               g_free (data);
+       }
+}
+
+static gint
+ecc_take_sexp_object (ECalCache *cal_cache,
+                     ECalBackendSExp *sexp)
+{
+       gint sexp_id;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), 0);
+       g_return_val_if_fail (E_IS_CAL_BACKEND_SEXP (sexp), 0);
+
+       g_mutex_lock (&cal_cache->priv->sexps_lock);
+
+       sexp_id = GPOINTER_TO_INT (sexp);
+       while (g_hash_table_contains (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id))) {
+               sexp_id++;
+       }
+
+       g_hash_table_insert (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id), sexp);
+
+       g_mutex_unlock (&cal_cache->priv->sexps_lock);
+
+       return sexp_id;
+}
+
+static void
+ecc_free_sexp_object (ECalCache *cal_cache,
+                     gint sexp_id)
+{
+       g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+
+       g_mutex_lock (&cal_cache->priv->sexps_lock);
+
+       g_warn_if_fail (g_hash_table_remove (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id)));
+
+       g_mutex_unlock (&cal_cache->priv->sexps_lock);
+}
+
+static ECalBackendSExp *
+ecc_ref_sexp_object (ECalCache *cal_cache,
+                    gint sexp_id)
+{
+       ECalBackendSExp *sexp;
+
+       g_mutex_lock (&cal_cache->priv->sexps_lock);
+
+       sexp = g_hash_table_lookup (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id));
+       if (sexp)
+               g_object_ref (sexp);
+
+       g_mutex_unlock (&cal_cache->priv->sexps_lock);
+
+       return sexp;
+}
+
+/* check_sexp(sexp_id, icalstring) */
+static void
+ecc_check_sexp_func (sqlite3_context *context,
+                    gint argc,
+                    sqlite3_value **argv)
+{
+       ECalCache *cal_cache;
+       ECalBackendSExp *sexp_obj;
+       gint sexp_id;
+       const gchar *icalstring;
+
+       g_return_if_fail (context != NULL);
+       g_return_if_fail (argc == 2);
+
+       cal_cache = sqlite3_user_data (context);
+       sexp_id = sqlite3_value_int (argv[0]);
+       icalstring = (const gchar *) sqlite3_value_text (argv[1]);
+
+       if (!E_IS_CAL_CACHE (cal_cache) || !icalstring || !*icalstring) {
+               sqlite3_result_int (context, 0);
+               return;
+       }
+
+       sexp_obj = ecc_ref_sexp_object (cal_cache, sexp_id);
+       if (!sexp_obj) {
+               sqlite3_result_int (context, 0);
+               return;
+       }
+
+       if (e_cal_backend_sexp_match_object (sexp_obj, icalstring, E_TIMEZONE_CACHE (cal_cache)))
+               sqlite3_result_int (context, 1);
+       else
+               sqlite3_result_int (context, 0);
+
+       g_object_unref (sexp_obj);
+}
+
+/* negate(x) */
+static void
+ecc_negate_func (sqlite3_context *context,
+                gint argc,
+                sqlite3_value **argv)
+{
+       gint val;
+
+       g_return_if_fail (context != NULL);
+       g_return_if_fail (argc == 1);
+
+       val = sqlite3_value_int (argv[0]);
+       sqlite3_result_int (context, !val);
+}
+
+static gboolean
+e_cal_cache_get_string (ECache *cache,
+                       gint ncols,
+                       const gchar **column_names,
+                       const gchar **column_values,
+                       gpointer user_data)
+{
+       gchar **pvalue = user_data;
+
+       g_return_val_if_fail (ncols == 1, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+       g_return_val_if_fail (pvalue != NULL, FALSE);
+
+       if (!*pvalue)
+               *pvalue = g_strdup (column_values[0]);
+
+       return TRUE;
+}
+
+static gboolean
+e_cal_cache_get_strings (ECache *cache,
+                        gint ncols,
+                        const gchar **column_names,
+                        const gchar **column_values,
+                        gpointer user_data)
+{
+       GSList **pstrings = user_data;
+
+       g_return_val_if_fail (ncols == 1, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+       g_return_val_if_fail (pstrings != NULL, FALSE);
+
+       *pstrings = g_slist_prepend (*pstrings, g_strdup (column_values[0]));
+
+       return TRUE;
+}
+
+static void
+e_cal_cache_populate_other_columns (ECalCache *cal_cache,
+                                   GSList **out_other_columns)
+{
+       g_return_if_fail (out_other_columns != NULL);
+
+       *out_other_columns = NULL;
+
+       #define add_column(name, type, idx_name) \
+               *out_other_columns = g_slist_prepend (*out_other_columns, \
+                       e_cache_column_info_new (name, type, idx_name))
+
+       add_column (ECC_COLUMN_OCCUR_START, "TEXT", "IDX_OCCURSTART");
+       add_column (ECC_COLUMN_OCCUR_END, "TEXT", "IDX_OCCUREND");
+       add_column (ECC_COLUMN_DUE, "TEXT", "IDX_DUE");
+       add_column (ECC_COLUMN_COMPLETED, "TEXT", "IDX_COMPLETED");
+       add_column (ECC_COLUMN_SUMMARY, "TEXT", "IDX_SUMMARY");
+       add_column (ECC_COLUMN_COMMENT, "TEXT", NULL);
+       add_column (ECC_COLUMN_DESCRIPTION, "TEXT", NULL);
+       add_column (ECC_COLUMN_LOCATION, "TEXT", NULL);
+       add_column (ECC_COLUMN_ATTENDEES, "TEXT", NULL);
+       add_column (ECC_COLUMN_ORGANIZER, "TEXT", NULL);
+       add_column (ECC_COLUMN_CLASSIFICATION, "TEXT", NULL);
+       add_column (ECC_COLUMN_STATUS, "TEXT", NULL);
+       add_column (ECC_COLUMN_PRIORITY, "INTEGER", NULL);
+       add_column (ECC_COLUMN_PERCENT_COMPLETE, "INTEGER", NULL);
+       add_column (ECC_COLUMN_CATEGORIES, "TEXT", NULL);
+       add_column (ECC_COLUMN_HAS_ALARM, "INTEGER", NULL);
+       add_column (ECC_COLUMN_HAS_ATTACHMENT, "INTEGER", NULL);
+       add_column (ECC_COLUMN_HAS_START, "INTEGER", NULL);
+       add_column (ECC_COLUMN_HAS_RECURRENCES, "INTEGER", NULL);
+       add_column (ECC_COLUMN_EXTRA, "TEXT", NULL);
+
+       #undef add_column
+
+       *out_other_columns = g_slist_reverse (*out_other_columns);
+}
+
+static gchar *
+ecc_encode_id_sql (const gchar *uid,
+                  const gchar *rid)
+{
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       if (rid && *rid)
+               return g_strdup_printf ("%s\n%s", uid, rid);
+
+       return g_strdup (uid);
+}
+
+static gboolean
+ecc_decode_id_sql (const gchar *id,
+                  gchar **out_uid,
+                  gchar **out_rid)
+{
+       gchar **split;
+
+       g_return_val_if_fail (id != NULL, FALSE);
+       g_return_val_if_fail (out_uid != NULL, FALSE);
+       g_return_val_if_fail (out_rid != NULL, FALSE);
+
+       *out_uid = NULL;
+       *out_rid = NULL;
+
+       if (!*id)
+               return FALSE;
+
+       split = g_strsplit (id, "\n", 2);
+
+       if (!split || !split[0] || !*split[0]) {
+               g_strfreev (split);
+               return FALSE;
+       }
+
+       *out_uid = split[0];
+
+       if (split[1])
+               *out_rid = split[1];
+
+       /* array elements are taken by the out arguments */
+       g_free (split);
+
+       return TRUE;
+}
+
+static icaltimezone *
+ecc_resolve_tzid_cb (const gchar *tzid,
+                    gpointer user_data)
+{
+       ECalCache *cal_cache = user_data;
+       icaltimezone *zone = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+       if (e_cal_cache_get_timezone (cal_cache, tzid, &zone, NULL, NULL) && zone)
+               return zone;
+
+       zone = icaltimezone_get_builtin_timezone (tzid);
+       if (!zone)
+               zone = icaltimezone_get_builtin_timezone_from_tzid (tzid);
+       if (!zone) {
+               tzid = e_cal_match_tzid (tzid);
+               zone = icaltimezone_get_builtin_timezone (tzid);
+       }
+
+       if (!zone)
+               zone = icaltimezone_get_builtin_timezone_from_tzid (tzid);
+
+       return zone;
+}
+
+static gchar *
+ecc_encode_itt_to_sql (struct icaltimetype itt)
+{
+       return g_strdup_printf ("%04d%02d%02d%02d%02d%02d",
+               itt.year, itt.month, itt.day,
+               itt.hour, itt.minute, itt.second);
+}
+
+static gchar *
+ecc_encode_time_to_sql (ECalCache *cal_cache,
+                       const ECalComponentDateTime *dt)
+{
+       struct icaltimetype itt;
+       icaltimezone *zone = NULL;
+
+       if (!dt || !dt->value || (!dt->value->is_utc && (!dt->tzid || !*dt->tzid)))
+               return NULL;
+
+       itt = *dt->value;
+       zone = ecc_resolve_tzid_cb (dt->tzid, cal_cache);
+
+       icaltimezone_convert_time (&itt, zone, icaltimezone_get_utc_timezone ());
+
+       return ecc_encode_itt_to_sql (itt);
+}
+
+static gchar *
+ecc_encode_timet_to_sql (ECalCache *cal_cache,
+                        time_t tt)
+{
+       struct icaltimetype itt;
+
+       if (tt <= 0)
+               return NULL;
+
+       itt = icaltime_from_timet_with_zone (tt, FALSE, icaltimezone_get_utc_timezone ());
+
+       return ecc_encode_itt_to_sql (itt);
+}
+
+static gchar *
+ecc_extract_text_list (const GSList *list)
+{
+       const GSList *link;
+       GString *value;
+
+       if (!list)
+               return NULL;
+
+       value = g_string_new ("");
+
+       for (link = list; link; link = g_slist_next (link)) {
+               ECalComponentText *text = link->data;
+
+               if (text && text->value) {
+                       gchar *str;
+
+                       str = e_util_utf8_decompose (text->value);
+                       if (str)
+                               g_string_append (value, str);
+                       g_free (str);
+               }
+       }
+
+       return g_string_free (value, !value->len);
+}
+
+static gchar *
+ecc_extract_comment (ECalComponent *comp)
+{
+       GSList *list = NULL;
+       gchar *value;
+
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+       e_cal_component_get_comment_list (comp, &list);
+       value = ecc_extract_text_list (list);
+       e_cal_component_free_text_list (list);
+
+       return value;
+}
+
+static gchar *
+ecc_extract_description (ECalComponent *comp)
+{
+       GSList *list = NULL;
+       gchar *value;
+
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+       e_cal_component_get_description_list (comp, &list);
+       value = ecc_extract_text_list (list);
+       e_cal_component_free_text_list (list);
+
+       return value;
+}
+
+static void
+ecc_encode_mail (GString *out_value,
+                const gchar *in_cn,
+                const gchar *in_val)
+{
+       gchar *cn = NULL, *val = NULL;
+
+       g_return_if_fail (in_val != NULL);
+
+       if (in_cn && *in_cn)
+               cn = e_util_utf8_decompose (in_cn);
+
+       if (in_val) {
+               const gchar *str = in_val;
+
+               if (g_ascii_strncasecmp (str, "mailto:";, 7) == 0) {
+                       str += 7;
+               }
+
+               if (*str)
+                       val = e_util_utf8_decompose (str);
+       }
+
+       if ((cn && *cn) || (val && *val)) {
+               if (out_value->len)
+                       g_string_append_c (out_value, '\n');
+               if (cn && *cn)
+                       g_string_append (out_value, cn);
+               if (val && *val) {
+                       if (cn && *cn)
+                               g_string_append_c (out_value, '\t');
+                       g_string_append (out_value, val);
+               }
+       }
+
+       g_free (cn);
+       g_free (val);
+}
+
+static gchar *
+ecc_extract_attendees (ECalComponent *comp)
+{
+       GSList *attendees = NULL, *link;
+       GString *value;
+
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+       e_cal_component_get_attendee_list (comp, &attendees);
+       if (!attendees)
+               return NULL;
+
+       value = g_string_new ("");
+
+       for (link = attendees; link; link = g_slist_next (link)) {
+               ECalComponentAttendee *att = link->data;
+
+               if (!att)
+                       continue;
+
+               ecc_encode_mail (value, att->cn, att->value);
+       }
+
+       e_cal_component_free_attendee_list (attendees);
+
+       if (value->len) {
+               /* This way it is encoded as:
+                  <\n> <common-name> <\t> <mail> <\n> <common-name> <\t> <mail> <\n> ... </n> */
+               g_string_prepend (value, "\n");
+               g_string_append (value, "\n");
+       }
+
+       return g_string_free (value, !value->len);
+}
+
+static gchar *
+ecc_extract_organizer (ECalComponent *comp)
+{
+       ECalComponentOrganizer org;
+       GString *value;
+
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+       e_cal_component_get_organizer (comp, &org);
+
+       if (!org.value)
+               return NULL;
+
+       value = g_string_new ("");
+
+       ecc_encode_mail (value, org.cn, org.value);
+
+       return g_string_free (value, !value->len);
+}
+
+static gchar *
+ecc_extract_categories (ECalComponent *comp)
+{
+       GSList *categories, *link;
+       GString *value;
+
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+       e_cal_component_get_categories_list (comp, &categories);
+
+       if (!categories)
+               return NULL;
+
+       value = g_string_new ("");
+
+       for (link = categories; link; link = g_slist_next (link)) {
+               const gchar *category = link->data;
+
+               if (category && *category) {
+                       if (value->len)
+                               g_string_append_c (value, '\n');
+                       g_string_append (value, category);
+               }
+       }
+
+       e_cal_component_free_categories_list (categories);
+
+       if (value->len) {
+               /* This way it is encoded as:
+                  <\n> <category> <\n> <category> <\n> ... </n>
+                  which allows to search for exact category with: LIKE "%\ncategory\n%"
+               */
+               g_string_prepend (value, "\n");
+               g_string_append (value, "\n");
+       }
+
+       return g_string_free (value, !value->len);
+}
+
+static const gchar *
+ecc_get_classification_as_string (ECalComponentClassification classification)
+{
+       const gchar *str;
+
+       switch (classification) {
+       case E_CAL_COMPONENT_CLASS_PUBLIC:
+               str = "public";
+               break;
+       case E_CAL_COMPONENT_CLASS_PRIVATE:
+               str = "private";
+               break;
+       case E_CAL_COMPONENT_CLASS_CONFIDENTIAL:
+               str = "confidential";
+               break;
+       default:
+               str = NULL;
+               break;
+       }
+
+       return str;
+}
+
+static const gchar *
+ecc_get_status_as_string (icalproperty_status status)
+{
+       switch (status) {
+       case ICAL_STATUS_NONE:
+               return "not started";
+       case ICAL_STATUS_COMPLETED:
+               return "completed";
+       case ICAL_STATUS_CANCELLED:
+               return "cancelled";
+       case ICAL_STATUS_INPROCESS:
+               return "in progress";
+       case ICAL_STATUS_NEEDSACTION:
+               return "needs action";
+       case ICAL_STATUS_TENTATIVE:
+               return "tentative";
+       case ICAL_STATUS_CONFIRMED:
+               return "confirmed";
+       case ICAL_STATUS_DRAFT:
+               return "draft";
+       case ICAL_STATUS_FINAL:
+               return "final";
+       case ICAL_STATUS_SUBMITTED:
+               return "submitted";
+       case ICAL_STATUS_PENDING:
+               return "pending";
+       case ICAL_STATUS_FAILED:
+               return "failed";
+       case ICAL_STATUS_X:
+               break;
+       }
+
+       return NULL;
+}
+
+static void
+ecc_fill_other_columns (ECalCache *cal_cache,
+                       ECacheColumnValues *other_columns,
+                       ECalComponent *comp)
+{
+       time_t occur_start = -1, occur_end = -1;
+       ECalComponentDateTime dt;
+       ECalComponentText text;
+       ECalComponentClassification classification = E_CAL_COMPONENT_CLASS_PUBLIC;
+       icalcomponent *icalcomp;
+       icalproperty_status status = ICAL_STATUS_NONE;
+       struct icaltimetype *itt;
+       const gchar *str = NULL;
+       gint *pint = NULL;
+       gboolean has;
+
+       g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+       g_return_if_fail (other_columns != NULL);
+       g_return_if_fail (E_IS_CAL_COMPONENT (comp));
+
+       #define add_value(_col, _val) e_cache_column_values_take_value (other_columns, _col, _val)
+
+       icalcomp = e_cal_component_get_icalcomponent (comp);
+
+       e_cal_util_get_component_occur_times (
+               comp, &occur_start, &occur_end,
+               ecc_resolve_tzid_cb, cal_cache, icaltimezone_get_utc_timezone (),
+               icalcomponent_isa (icalcomp));
+
+       e_cal_component_get_dtstart (comp, &dt);
+       add_value (ECC_COLUMN_OCCUR_START, dt.value && ((dt.tzid && *dt.tzid) || dt.value->is_utc) ? 
ecc_encode_timet_to_sql (cal_cache, occur_start) : NULL);
+
+       has = dt.value != NULL;
+       add_value (ECC_COLUMN_HAS_START, g_strdup (has ? "1" : "0"));
+       e_cal_component_free_datetime (&dt);
+
+       e_cal_component_get_dtend (comp, &dt);
+       add_value (ECC_COLUMN_OCCUR_END, dt.value && ((dt.tzid && *dt.tzid) || dt.value->is_utc) ? 
ecc_encode_timet_to_sql (cal_cache, occur_end) : NULL);
+       e_cal_component_free_datetime (&dt);
+
+       e_cal_component_get_due (comp, &dt);
+       add_value (ECC_COLUMN_DUE, ecc_encode_time_to_sql (cal_cache, &dt));
+       e_cal_component_free_datetime (&dt);
+
+       itt = NULL;
+       e_cal_component_get_completed (comp, &itt);
+       add_value (ECC_COLUMN_COMPLETED, itt ? ecc_encode_itt_to_sql (*itt) : NULL);
+       if (itt)
+               e_cal_component_free_icaltimetype (itt);
+
+       text.value = NULL;
+       e_cal_component_get_summary (comp, &text);
+       add_value (ECC_COLUMN_SUMMARY, text.value ? e_util_utf8_decompose (text.value) : NULL);
+
+       e_cal_component_get_location (comp, &str);
+       add_value (ECC_COLUMN_LOCATION, str ? e_util_utf8_decompose (str) : NULL);
+
+       e_cal_component_get_classification (comp, &classification);
+       add_value (ECC_COLUMN_CLASSIFICATION, g_strdup (ecc_get_classification_as_string (classification)));
+
+       e_cal_component_get_status (comp, &status);
+       add_value (ECC_COLUMN_STATUS, g_strdup (ecc_get_status_as_string (status)));
+
+       e_cal_component_get_priority (comp, &pint);
+       add_value (ECC_COLUMN_PRIORITY, pint && *pint ? g_strdup_printf ("%d", *pint) : NULL);
+       if (pint)
+               e_cal_component_free_priority (pint);
+
+       e_cal_component_get_percent (comp, &pint);
+       add_value (ECC_COLUMN_PERCENT_COMPLETE, pint && *pint ? g_strdup_printf ("%d", *pint) : NULL);
+       if (pint)
+               e_cal_component_free_percent (pint);
+
+       has = e_cal_component_has_alarms (comp);
+       add_value (ECC_COLUMN_HAS_ALARM, g_strdup (has ? "1" : "0"));
+
+       has = e_cal_component_has_attachments (comp);
+       add_value (ECC_COLUMN_HAS_ATTACHMENT, g_strdup (has ? "1" : "0"));
+
+       has = e_cal_component_has_recurrences (comp) ||
+             e_cal_component_is_instance (comp);
+       add_value (ECC_COLUMN_HAS_RECURRENCES, g_strdup (has ? "1" : "0"));
+
+       add_value (ECC_COLUMN_COMMENT, ecc_extract_comment (comp));
+       add_value (ECC_COLUMN_DESCRIPTION, ecc_extract_description (comp));
+       add_value (ECC_COLUMN_ATTENDEES, ecc_extract_attendees (comp));
+       add_value (ECC_COLUMN_ORGANIZER, ecc_extract_organizer (comp));
+       add_value (ECC_COLUMN_CATEGORIES, ecc_extract_categories (comp));
+}
+
+static gchar *
+ecc_range_as_where_clause (const gchar *start_str,
+                          const gchar *end_str)
+{
+       GString *stmt;
+
+       if (!start_str && !end_str)
+               return NULL;
+
+       stmt = g_string_sized_new (64);
+
+       if (start_str) {
+               e_cache_sqlite_stmt_append_printf (stmt,
+                       "(" ECC_COLUMN_OCCUR_END " IS NULL OR " ECC_COLUMN_OCCUR_END ">=%Q)",
+                       start_str);
+       }
+
+       if (end_str) {
+               if (start_str) {
+                       g_string_prepend (stmt, "(");
+                       g_string_append (stmt, " AND ");
+               }
+
+               e_cache_sqlite_stmt_append_printf (stmt,
+                       "(" ECC_COLUMN_OCCUR_START " IS NULL OR " ECC_COLUMN_OCCUR_START "<=%Q)",
+                       end_str);
+
+               if (start_str)
+                       g_string_append (stmt, ")");
+       }
+
+       return g_string_free (stmt, FALSE);
+}
+
+typedef struct _SExpToSqlContext {
+       ECalCache *cal_cache;
+       guint not_level;
+       gboolean requires_check_sexp;
+} SExpToSqlContext;
+
+static ESExpResult *
+ecc_sexp_func_and_or (ESExp *esexp,
+                     gint argc,
+                     ESExpTerm **argv,
+                     gpointer user_data,
+                     const gchar *oper)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result, *r1;
+       GString *stmt;
+       gint ii;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       stmt = g_string_new ("(");
+
+       for (ii = 0; ii < argc; ii++) {
+               r1 = e_sexp_term_eval (esexp, argv[ii]);
+
+               if (r1 && r1->type == ESEXP_RES_STRING && r1->value.string) {
+                       if (stmt->len > 1)
+                               g_string_append_printf (stmt, " %s ", oper);
+
+                       g_string_append_printf (stmt, "(%s)", r1->value.string);
+               } else {
+                       ctx->requires_check_sexp = TRUE;
+               }
+
+               e_sexp_result_free (esexp, r1);
+       }
+
+       if (stmt->len == 1 && !ctx->not_level) {
+               if (g_str_equal (oper, "AND"))
+                       g_string_append_c (stmt, '1');
+               else
+                       g_string_append_c (stmt, '0');
+       }
+
+       g_string_append_c (stmt, ')');
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_string_free (stmt, stmt->len <= 2);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_and (ESExp *esexp,
+                  gint argc,
+                  ESExpTerm **argv,
+                  gpointer user_data)
+{
+       return ecc_sexp_func_and_or (esexp, argc, argv, user_data, "AND");
+}
+
+static ESExpResult *
+ecc_sexp_func_or (ESExp *esexp,
+                  gint argc,
+                  ESExpTerm **argv,
+                  gpointer user_data)
+{
+       return ecc_sexp_func_and_or (esexp, argc, argv, user_data, "OR");
+}
+
+static ESExpResult *
+ecc_sexp_func_not (ESExp *esexp,
+                  gint argc,
+                  ESExpTerm **argv,
+                  gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result, *r1;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (argc != 1)
+               return NULL;
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+       ctx->not_level++;
+
+       r1 = e_sexp_term_eval (esexp, argv[0]);
+
+       ctx->not_level--;
+
+       if (r1 && r1->type == ESEXP_RES_STRING && r1->value.string) {
+               result->value.string = g_strdup_printf ("negate(%s)", r1->value.string);
+       } else {
+               ctx->requires_check_sexp = TRUE;
+       }
+
+       e_sexp_result_free (esexp, r1);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_uid (ESExp *esexp,
+                  gint argc,
+                  ESExpResult **argv,
+                  gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+       const gchar *uid;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (argc != 1 ||
+           argv[0]->type != ESEXP_RES_STRING) {
+               return NULL;
+       }
+
+       uid = argv[0]->value.string;
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+       if (!uid) {
+               result->value.string = g_strdup (E_CACHE_COLUMN_UID " IS NULL");
+       } else {
+               gchar *stmt;
+
+               stmt = e_cache_sqlite_stmt_printf (E_CACHE_COLUMN_UID "=%Q OR " E_CACHE_COLUMN_UID " LIKE 
'%q\n%%'", uid, uid);
+
+               result->value.string = g_strdup (stmt);
+
+               e_cache_sqlite_stmt_free (stmt);
+       }
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_occur_in_time_range (ESExp *esexp,
+                                  gint argc,
+                                  ESExpResult **argv,
+                                  gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if ((argc != 2 && argc != 3) ||
+           argv[0]->type != ESEXP_RES_TIME ||
+           argv[1]->type != ESEXP_RES_TIME ||
+           (argc == 3 && argv[2]->type != ESEXP_RES_STRING)) {
+               return NULL;
+       }
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+       if (!ctx->not_level) {
+               struct icaltimetype itt_start, itt_end;
+               gchar *start_str, *end_str;
+
+               /* The default zone argument, if any, is ignored here */
+               itt_start = icaltime_from_timet_with_zone (argv[0]->value.time, 0, NULL);
+               itt_end = icaltime_from_timet_with_zone (argv[1]->value.time, 0, NULL);
+
+               start_str = ecc_encode_itt_to_sql (itt_start);
+               end_str = ecc_encode_itt_to_sql (itt_end);
+
+               result->value.string = ecc_range_as_where_clause (start_str, end_str);
+
+               if (!result->value.string)
+                       result->value.string = g_strdup ("1=1");
+
+               g_free (start_str);
+               g_free (end_str);
+       } else {
+               result->value.string = NULL;
+       }
+
+       ctx->requires_check_sexp = TRUE;
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_due_in_time_range (ESExp *esexp,
+                                gint argc,
+                                ESExpResult **argv,
+                                gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+       gchar *start_str, *end_str;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (argc != 2 ||
+           argv[0]->type != ESEXP_RES_TIME ||
+           argv[1]->type != ESEXP_RES_TIME) {
+               return NULL;
+       }
+
+       start_str = ecc_encode_timet_to_sql (ctx->cal_cache, argv[0]->value.time);
+       end_str = ecc_encode_timet_to_sql (ctx->cal_cache, argv[1]->value.time);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("(%s NOT NULL AND %s>='%s' AND %s<='%s')",
+               ECC_COLUMN_DUE, ECC_COLUMN_DUE, start_str,
+               ECC_COLUMN_DUE, end_str);
+
+       g_free (start_str);
+       g_free (end_str);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_contains (ESExp *esexp,
+                       gint argc,
+                       ESExpResult **argv,
+                       gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+       const gchar *field, *column = NULL;
+       gchar *str;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (argc != 2 ||
+           argv[0]->type != ESEXP_RES_STRING ||
+           argv[1]->type != ESEXP_RES_STRING) {
+               return NULL;
+       }
+
+       field = argv[0]->value.string;
+       str = e_util_utf8_decompose (argv[1]->value.string);
+
+       if (g_str_equal (field, "comment"))
+               column = ECC_COLUMN_COMMENT;
+       else if (g_str_equal (field, "description"))
+               column = ECC_COLUMN_DESCRIPTION;
+       else if (g_str_equal (field, "summary"))
+               column = ECC_COLUMN_SUMMARY;
+       else if (g_str_equal (field, "location"))
+               column = ECC_COLUMN_LOCATION;
+       else if (g_str_equal (field, "attendee"))
+               column = ECC_COLUMN_ATTENDEES;
+       else if (g_str_equal (field, "organizer"))
+               column = ECC_COLUMN_ORGANIZER;
+       else if (g_str_equal (field, "classification"))
+               column = ECC_COLUMN_CLASSIFICATION;
+       else if (g_str_equal (field, "status"))
+               column = ECC_COLUMN_STATUS;
+       else if (g_str_equal (field, "priority"))
+               column = ECC_COLUMN_PRIORITY;
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+       /* everything matches an empty string */
+       if (!str || !*str) {
+               result->value.string = g_strdup ("1=1");
+       } else if (column) {
+               gchar *stmt;
+
+               if (g_str_equal (column, ECC_COLUMN_PRIORITY)) {
+                       if (g_ascii_strcasecmp (str, "UNDEFINED") == 0)
+                               stmt = e_cache_sqlite_stmt_printf ("%s IS NULL", column);
+                       else if (g_ascii_strcasecmp (str, "HIGH") == 0)
+                               stmt = e_cache_sqlite_stmt_printf ("%s<=4", column);
+                       else if (g_ascii_strcasecmp (str, "NORMAL") == 0)
+                               stmt = e_cache_sqlite_stmt_printf ("%s=5", column);
+                       else if (g_ascii_strcasecmp (str, "LOW") == 0)
+                               stmt = e_cache_sqlite_stmt_printf ("%s>5", column);
+                       else
+                               stmt = e_cache_sqlite_stmt_printf ("%s IS NOT NULL", column);
+               } else if (g_str_equal (column, ECC_COLUMN_CLASSIFICATION) ||
+                          g_str_equal (column, ECC_COLUMN_STATUS)) {
+                       stmt = e_cache_sqlite_stmt_printf ("%s='%q'", column, str);
+               } else {
+                       stmt = e_cache_sqlite_stmt_printf ("%s LIKE '%%%q%%'", column, str);
+               }
+               result->value.string = g_strdup (stmt);
+               e_cache_sqlite_stmt_free (stmt);
+       } else if (g_str_equal (field, "any")) {
+               GString *stmt;
+
+               stmt = g_string_new ("");
+
+               e_cache_sqlite_stmt_append_printf (stmt, "(%s LIKE '%%%q%%'", ECC_COLUMN_COMMENT, str);
+               e_cache_sqlite_stmt_append_printf (stmt, " OR %s LIKE '%%%q%%'", ECC_COLUMN_DESCRIPTION, str);
+               e_cache_sqlite_stmt_append_printf (stmt, " OR %s LIKE '%%%q%%'", ECC_COLUMN_SUMMARY, str);
+               e_cache_sqlite_stmt_append_printf (stmt, " OR %s LIKE '%%%q%%')", ECC_COLUMN_LOCATION, str);
+
+               result->value.string = g_string_free (stmt, FALSE);
+       } else {
+               ctx->requires_check_sexp = TRUE;
+       }
+
+       g_free (str);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_start (ESExp *esexp,
+                        gint argc,
+                        ESExpResult **argv,
+                        gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+               ECC_COLUMN_HAS_START, ECC_COLUMN_HAS_START);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_alarms (ESExp *esexp,
+                         gint argc,
+                         ESExpResult **argv,
+                         gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+               ECC_COLUMN_HAS_ALARM, ECC_COLUMN_HAS_ALARM);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_alarms_in_range (ESExp *esexp,
+                                  gint argc,
+                                  ESExpResult **argv,
+                                  gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       ctx->requires_check_sexp = TRUE;
+
+       if (!ctx->not_level)
+               return ecc_sexp_func_has_alarms (esexp, argc, argv, user_data);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = NULL;
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_recurrences (ESExp *esexp,
+                              gint argc,
+                              ESExpResult **argv,
+                              gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+               ECC_COLUMN_HAS_RECURRENCES, ECC_COLUMN_HAS_RECURRENCES);
+
+       return result;
+}
+
+/* (has-categories? STR+)
+ * (has-categories? #f)
+ */
+static ESExpResult *
+ecc_sexp_func_has_categories (ESExp *esexp,
+                             gint argc,
+                             ESExpResult **argv,
+                             gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+       gboolean unfiled;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (argc < 1)
+               return NULL;
+
+       unfiled = argc == 1 && argv[0]->type == ESEXP_RES_BOOL;
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+       if (unfiled) {
+               result->value.string = g_strdup_printf ("%s IS NULL",
+                       ECC_COLUMN_CATEGORIES);
+       } else {
+               GString *tmp;
+               gint ii;
+
+               tmp = g_string_new ("(" ECC_COLUMN_CATEGORIES " NOT NULL");
+
+               for (ii = 0; ii < argc; ii++) {
+                       if (argv[ii]->type != ESEXP_RES_STRING) {
+                               g_warn_if_reached ();
+                               continue;
+                       }
+
+                       e_cache_sqlite_stmt_append_printf (tmp, " AND " ECC_COLUMN_CATEGORIES " LIKE 
'%%\n%q\n%%'",
+                               argv[ii]->value.string);
+               }
+
+               g_string_append_c (tmp, ')');
+
+               result->value.string = g_string_free (tmp, FALSE);
+       }
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_is_completed (ESExp *esexp,
+                           gint argc,
+                           ESExpResult **argv,
+                           gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("%s NOT NULL",
+               ECC_COLUMN_COMPLETED);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_completed_before (ESExp *esexp,
+                               gint argc,
+                               ESExpResult **argv,
+                               gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       gchar *tmp;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       if (argc != 1 ||
+           argv[0]->type != ESEXP_RES_TIME) {
+               return NULL;
+       }
+
+       tmp = ecc_encode_timet_to_sql (ctx->cal_cache, argv[0]->value.time);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("(%s NOT NULL AND %s<'%s')",
+               ECC_COLUMN_COMPLETED, ECC_COLUMN_COMPLETED, tmp);
+
+       g_free (tmp);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_attachment (ESExp *esexp,
+                             gint argc,
+                             ESExpResult **argv,
+                             gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+               ECC_COLUMN_HAS_ATTACHMENT, ECC_COLUMN_HAS_ATTACHMENT);
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_percent_complete (ESExp *esexp,
+                               gint argc,
+                               ESExpResult **argv,
+                               gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = g_strdup (ECC_COLUMN_PERCENT_COMPLETE);
+
+       return result;
+}
+
+/* check_sexp(sexp_id, icalstring); that's a fallback for anything
+   not being part of the summary */
+static ESExpResult *
+ecc_sexp_func_check_sexp (ESExp *esexp,
+                         gint argc,
+                         ESExpResult **argv,
+                         gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = NULL;
+
+       ctx->requires_check_sexp = TRUE;
+
+       return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_icheck_sexp (ESExp *esexp,
+                          gint argc,
+                          ESExpTerm **argv,
+                          gpointer user_data)
+{
+       SExpToSqlContext *ctx = user_data;
+       ESExpResult *result;
+
+       g_return_val_if_fail (ctx != NULL, NULL);
+
+       result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+       result->value.string = NULL;
+
+       ctx->requires_check_sexp = TRUE;
+
+       return result;
+}
+
+static struct {
+       const gchar *name;
+       gpointer func;
+       gint type; /* 1 for term-function, 0 for result-function */
+} symbols[] = {
+       { "and",                        ecc_sexp_func_and, 1 },
+       { "or",                         ecc_sexp_func_or, 1 },
+       { "not",                        ecc_sexp_func_not, 1 },
+       { "<",                          ecc_sexp_func_icheck_sexp, 1 },
+       { ">",                          ecc_sexp_func_icheck_sexp, 1 },
+       { "=",                          ecc_sexp_func_icheck_sexp, 1 },
+       { "+",                          ecc_sexp_func_check_sexp, 0 },
+       { "-",                          ecc_sexp_func_check_sexp, 0 },
+       { "cast-int",                   ecc_sexp_func_check_sexp, 0 },
+       { "cast-string",                ecc_sexp_func_check_sexp, 0 },
+       { "if",                         ecc_sexp_func_icheck_sexp, 1 },
+       { "begin",                      ecc_sexp_func_icheck_sexp, 1 },
+
+       /* Time-related functions */
+       { "time-now",                   e_cal_backend_sexp_func_time_now, 0 },
+       { "make-time",                  e_cal_backend_sexp_func_make_time, 0 },
+       { "time-add-day",               e_cal_backend_sexp_func_time_add_day, 0 },
+       { "time-day-begin",             e_cal_backend_sexp_func_time_day_begin, 0 },
+       { "time-day-end",               e_cal_backend_sexp_func_time_day_end, 0 },
+
+       /* Component-related functions */
+       { "uid?",                       ecc_sexp_func_uid, 0 },
+       { "occur-in-time-range?",       ecc_sexp_func_occur_in_time_range, 0 },
+       { "due-in-time-range?",         ecc_sexp_func_due_in_time_range, 0 },
+       { "contains?",                  ecc_sexp_func_contains, 0 },
+       { "has-start?",                 ecc_sexp_func_has_start, 0 },
+       { "has-alarms?",                ecc_sexp_func_has_alarms, 0 },
+       { "has-alarms-in-range?",       ecc_sexp_func_has_alarms_in_range, 0 },
+       { "has-recurrences?",           ecc_sexp_func_has_recurrences, 0 },
+       { "has-categories?",            ecc_sexp_func_has_categories, 0 },
+       { "is-completed?",              ecc_sexp_func_is_completed, 0 },
+       { "completed-before?",          ecc_sexp_func_completed_before, 0 },
+       { "has-attachments?",           ecc_sexp_func_has_attachment, 0 },
+       { "percent-complete?",          ecc_sexp_func_percent_complete, 0 },
+       { "occurrences-count?",         ecc_sexp_func_check_sexp, 0 }
+};
+
+static gboolean
+ecc_convert_sexp_to_sql (ECalCache *cal_cache,
+                        const gchar *sexp_str,
+                        gint sexp_id,
+                        gchar **out_where_clause,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       SExpToSqlContext ctx;
+       ESExp *sexp_parser;
+       gint esexp_error, ii;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (out_where_clause != NULL, FALSE);
+
+       *out_where_clause = NULL;
+
+       /* Include everything */
+       if (!sexp_str || !*sexp_str)
+               return TRUE;
+
+       ctx.cal_cache = cal_cache;
+       ctx.not_level = 0;
+       ctx.requires_check_sexp = FALSE;
+
+       sexp_parser = e_sexp_new ();
+
+       for (ii = 0; ii < G_N_ELEMENTS (symbols); ii++) {
+               if (symbols[ii].type == 1) {
+                       e_sexp_add_ifunction (sexp_parser, 0, symbols[ii].name, symbols[ii].func, &ctx);
+               } else {
+                       e_sexp_add_function (sexp_parser, 0, symbols[ii].name, symbols[ii].func, &ctx);
+               }
+       }
+
+       e_sexp_input_text (sexp_parser, sexp_str, strlen (sexp_str));
+       esexp_error = e_sexp_parse (sexp_parser);
+
+       if (esexp_error != -1) {
+               ESExpResult *result;
+
+               result = e_sexp_eval (sexp_parser);
+
+               if (result) {
+                       if (result->type == ESEXP_RES_STRING) {
+                               if (ctx.requires_check_sexp) {
+                                       if (result->value.string) {
+                                               *out_where_clause = g_strdup_printf ("((%s) AND 
check_sexp(%d,%s))",
+                                                       result->value.string, sexp_id, E_CACHE_COLUMN_OBJECT);
+                                       } else {
+                                               *out_where_clause = g_strdup_printf ("check_sexp(%d,%s)",
+                                                       sexp_id, E_CACHE_COLUMN_OBJECT);
+                                       }
+                               } else {
+                                       /* Just steal the string from the ESExpResult */
+                                       *out_where_clause = result->value.string;
+                                       result->value.string = NULL;
+                               }
+                               success = TRUE;
+                       }
+               }
+
+               e_sexp_result_free (sexp_parser, result);
+       }
+
+       g_object_unref (sexp_parser);
+
+       if (!success) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+                       _("Invalid query: %s"), sexp_str);
+       }
+
+       return success;
+}
+
+typedef struct {
+       gint extra_idx;
+       ECalCacheSearchFunc func;
+       gpointer func_user_data;
+} SearchContext;
+
+static gboolean
+ecc_search_foreach_cb (ECache *cache,
+                      const gchar *uid,
+                      const gchar *revision,
+                      const gchar *object,
+                      EOfflineState offline_state,
+                      gint ncols,
+                      const gchar *column_names[],
+                      const gchar *column_values[],
+                      gpointer user_data)
+{
+       SearchContext *ctx = user_data;
+       gchar *comp_uid = NULL, *comp_rid = NULL;
+       gboolean can_continue;
+
+       g_return_val_if_fail (ctx != NULL, FALSE);
+       g_return_val_if_fail (ctx->func != NULL, FALSE);
+
+       if (ctx->extra_idx == -1) {
+               gint ii;
+
+               for (ii = 0; ii < ncols; ii++) {
+                       if (column_names[ii] && g_ascii_strcasecmp (column_names[ii], ECC_COLUMN_EXTRA) == 0) 
{
+                               ctx->extra_idx = ii;
+                               break;
+                       }
+               }
+       }
+
+       g_return_val_if_fail (ctx->extra_idx != -1, FALSE);
+
+       g_warn_if_fail (ecc_decode_id_sql (uid, &comp_uid, &comp_rid));
+
+       /* This type-cast for performance reason */
+       can_continue = ctx->func ((ECalCache *) cache, comp_uid, comp_rid, revision, object,
+               column_values[ctx->extra_idx], offline_state, ctx->func_user_data);
+
+       g_free (comp_uid);
+       g_free (comp_rid);
+
+       return can_continue;
+}
+
+static gboolean
+ecc_search_internal (ECalCache *cal_cache,
+                    const gchar *sexp_str,
+                    gint sexp_id,
+                    ECalCacheSearchFunc func,
+                    gpointer user_data,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       gchar *where_clause = NULL;
+       SearchContext ctx;
+       gboolean success;
+
+       if (!ecc_convert_sexp_to_sql (cal_cache, sexp_str, sexp_id, &where_clause, cancellable, error)) {
+               return FALSE;
+       }
+
+       ctx.extra_idx = -1;
+       ctx.func = func;
+       ctx.func_user_data = user_data;
+
+       success = e_cache_foreach (E_CACHE (cal_cache), E_CACHE_EXCLUDE_DELETED,
+               where_clause, ecc_search_foreach_cb, &ctx,
+               cancellable, error);
+
+       g_free (where_clause);
+
+       return success;
+}
+
+static gboolean
+ecc_init_aux_tables (ECalCache *cal_cache,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       stmt = e_cache_sqlite_stmt_printf ("CREATE TABLE IF NOT EXISTS %Q ("
+               "tzid TEXT PRIMARY KEY, "
+               "zone TEXT)",
+               ECC_TABLE_TIMEZONES);
+       success = e_cache_sqlite_exec (E_CACHE (cal_cache), stmt, cancellable, error);
+       e_cache_sqlite_stmt_free (stmt);
+
+       return success;
+}
+
+static gboolean
+ecc_init_sqlite_functions (ECalCache *cal_cache,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       gint ret;
+       gpointer sqlitedb;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       sqlitedb = e_cache_get_sqlitedb (E_CACHE (cal_cache));
+       g_return_val_if_fail (sqlitedb != NULL, FALSE);
+
+       /* check_sexp(sexp_id, icalstring) */
+       ret = sqlite3_create_function (sqlitedb,
+               "check_sexp", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC,
+               cal_cache, ecc_check_sexp_func,
+               NULL, NULL);
+
+       if (ret == SQLITE_OK) {
+               /* negate(x) */
+               ret = sqlite3_create_function (sqlitedb,
+                       "negate", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC,
+                       NULL, ecc_negate_func,
+                       NULL, NULL);
+       }
+
+       if (ret != SQLITE_OK) {
+               const gchar *errmsg = sqlite3_errmsg (sqlitedb);
+
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE,
+                       _("Failed to create SQLite function, error code '%d': %s"),
+                       ret, errmsg ? errmsg : _("Unknown error"));
+
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_cal_cache_migrate (ECache *cache,
+                    gint from_version,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       /* ECalCache *cal_cache = E_CAL_CACHE (cache); */
+       gboolean success = TRUE;
+
+       /* Add any version-related changes here */
+       /*if (from_version < E_CAL_CACHE_VERSION) {
+       }*/
+
+       return success;
+}
+
+static gboolean
+e_cal_cache_initialize (ECalCache *cal_cache,
+                       const gchar *filename,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       ECache *cache;
+       GSList *other_columns = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (filename != NULL, FALSE);
+
+       cache = E_CACHE (cal_cache);
+
+       e_cal_cache_populate_other_columns (cal_cache, &other_columns);
+
+       success = e_cache_initialize_sync (cache, filename, other_columns, cancellable, error);
+       if (!success)
+               goto exit;
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       success = success && ecc_init_aux_tables (cal_cache, cancellable, error);
+
+       success = success && ecc_init_sqlite_functions (cal_cache, cancellable, error);
+
+       /* Check for data migration */
+       success = success && e_cal_cache_migrate (cache, e_cache_get_version (cache), cancellable, error);
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       if (!success)
+               goto exit;
+
+       if (e_cache_get_version (cache) != E_CAL_CACHE_VERSION)
+               e_cache_set_version (cache, E_CAL_CACHE_VERSION);
+
+ exit:
+       g_slist_free_full (other_columns, e_cache_column_info_free);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_new:
+ * @filename: file name to load or create the new cache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #ECalCache.
+ *
+ * Returns: (transfer full) (nullable): A new #ECalCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+ECalCache *
+e_cal_cache_new (const gchar *filename,
+                GCancellable *cancellable,
+                GError **error)
+{
+       ECalCache *cal_cache;
+
+       g_return_val_if_fail (filename != NULL, NULL);
+
+       cal_cache = g_object_new (E_TYPE_CAL_CACHE, NULL);
+
+       if (!e_cal_cache_initialize (cal_cache, filename, cancellable, error)) {
+               g_object_unref (cal_cache);
+               cal_cache = NULL;
+       }
+
+       return cal_cache;
+}
+
+/**
+ * e_cal_cache_dup_component_revision:
+ * @cal_cache: an #ECalCache
+ * @icalcomp: an icalcomponent
+ *
+ * Returns the @icalcomp revision, used to detect changes.
+ * The returned string should be freed with g_free(), when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly allocated string containing
+ *    revision of the @icalcomp.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cal_cache_dup_component_revision (ECalCache *cal_cache,
+                                   icalcomponent *icalcomp)
+{
+       gchar *revision = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+       g_return_val_if_fail (icalcomp != NULL, NULL);
+
+       g_signal_emit (cal_cache, signals[DUP_COMPONENT_REVISION], 0, icalcomp, &revision);
+
+       return revision;
+}
+
+/**
+ * e_cal_cache_contains:
+ * @cal_cache: an #ECalCache
+ * @uid: component UID
+ * @rid: (nullable): optional component Recurrence-ID or %NULL
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ *
+ * Checkes whether the @cal_cache contains an object with
+ * the given @uid and @rid. The @rid can be an empty string
+ * or %NULL to search for the master object, otherwise the check
+ * is done for a detached instance, not for a recurrence instance.
+ *
+ * Returns: Whether the the object had been found.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_contains (ECalCache *cal_cache,
+                     const gchar *uid,
+                     const gchar *rid,
+                     ECacheDeletedFlag deleted_flag)
+{
+       gchar *id;
+       gboolean found;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       id = ecc_encode_id_sql (uid, rid);
+
+       found = e_cache_contains (E_CACHE (cal_cache), id, deleted_flag);
+
+       g_free (id);
+
+       return found;
+}
+
+/**
+ * e_cal_cache_put_component:
+ * @cal_cache: an #ECalCache
+ * @component: an #ECalComponent to put into the @cal_cache
+ * @extra: (nullable): an extra data to store in association with the @component
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting this component in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds a @component into the @cal_cache. Any existing with the same UID
+ * and RID is replaced.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_put_component (ECalCache *cal_cache,
+                          ECalComponent *component,
+                          const gchar *extra,
+                          ECacheOfflineFlag offline_flag,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       GSList *components = NULL;
+       GSList *extras = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       components = g_slist_prepend (components, component);
+       if (extra)
+               extras = g_slist_prepend (extras, (gpointer) extra);
+
+       success = e_cal_cache_put_components (cal_cache, components, extras, offline_flag, cancellable, 
error);
+
+       g_slist_free (components);
+       g_slist_free (extras);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_put_components:
+ * @cal_cache: an #ECalCache
+ * @components: (element-type ECalComponent): a #GSList of #ECalComponent to put into the @cal_cache
+ * @extras: (nullable) (element-type utf8): an extra data to store in association with the @components
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting these components in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds a list of @components into the @cal_cache. Any existing with the same UID
+ * and RID are replaced.
+ *
+ * If @extras is not %NULL, it's length should be the same as the length
+ * of the @components.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_put_components (ECalCache *cal_cache,
+                           const GSList *components,
+                           const GSList *extras,
+                           ECacheOfflineFlag offline_flag,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       const GSList *clink, *elink;
+       ECache *cache;
+       ECacheColumnValues *other_columns;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (extras == NULL || g_slist_length ((GSList *) components) == g_slist_length 
((GSList *) extras), FALSE);
+
+       cache = E_CACHE (cal_cache);
+       other_columns = e_cache_column_values_new ();
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+       e_cache_freeze_revision_change (cache);
+
+       for (clink = components, elink = extras; clink; clink = g_slist_next (clink), elink = g_slist_next 
(elink)) {
+               ECalComponent *component = clink->data;
+               const gchar *extra = elink ? elink->data : NULL;
+               ECalComponentId *id;
+               gchar *uid, *rev, *icalstring;
+
+               g_return_val_if_fail (E_IS_CAL_COMPONENT (component), FALSE);
+
+               icalstring = e_cal_component_get_as_string (component);
+               g_return_val_if_fail (icalstring != NULL, FALSE);
+
+               e_cache_column_values_remove_all (other_columns);
+
+               if (extra)
+                       e_cache_column_values_take_value (other_columns, ECC_COLUMN_EXTRA, g_strdup (extra));
+
+               id = e_cal_component_get_id (component);
+               if (id) {
+                       uid = ecc_encode_id_sql (id->uid, id->rid);
+               } else {
+                       g_warn_if_reached ();
+                       uid = g_strdup ("");
+               }
+               e_cal_component_free_id (id);
+
+               rev = e_cal_cache_dup_component_revision (cal_cache, e_cal_component_get_icalcomponent 
(component));
+
+               success = e_cache_put (cache, uid, rev, icalstring, other_columns, offline_flag, cancellable, 
error);
+
+               g_free (icalstring);
+               g_free (rev);
+               g_free (uid);
+
+               if (!success)
+                       break;
+       }
+
+       e_cache_thaw_revision_change (cache);
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       e_cache_column_values_free (other_columns);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_remove_component:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component to remove
+ * @rid: (nullable): an optional Recurrence-ID to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing this component in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes a component identified by @uid and @rid from the @cal_cache.
+ * When the @rid is %NULL, or an empty string, then removes the master
+ * object only, without any detached instance.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_remove_component (ECalCache *cal_cache,
+                             const gchar *uid,
+                             const gchar *rid,
+                             ECacheOfflineFlag offline_flag,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       ECalComponentId id;
+       GSList *ids = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       id.uid = (gchar *) uid;
+       id.rid = (gchar *) rid;
+
+       ids = g_slist_prepend (ids, &id);
+
+       success = e_cal_cache_remove_components (cal_cache, ids, offline_flag, cancellable, error);
+
+       g_slist_free (ids);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_remove_components:
+ * @cal_cache: an #ECalCache
+ * @ids: (element-type ECalComponentId): a #GSList of components to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing these comonents in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes components identified by @uid and @rid from the @cal_cache
+ * in the @ids list. When the @rid is %NULL, or an empty string, then
+ * removes the master object only, without any detached instance.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_remove_components (ECalCache *cal_cache,
+                              const GSList *ids,
+                              ECacheOfflineFlag offline_flag,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       ECache *cache;
+       const GSList *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       cache = E_CACHE (cal_cache);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+       e_cache_freeze_revision_change (cache);
+
+       for (link = ids; success && link; link = g_slist_next (link)) {
+               const ECalComponentId *id = link->data;
+               gchar *uid;
+
+               g_warn_if_fail (id != NULL);
+
+               if (!id)
+                       continue;
+
+               uid = ecc_encode_id_sql (id->uid, id->rid);
+
+               success = e_cache_remove (cache, uid, offline_flag, cancellable, error);
+
+               g_free (uid);
+       }
+
+       e_cache_thaw_revision_change (cache);
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_get_component:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @out_component: (out) (transfer full): return location for an #ECalComponent
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a component identified by @uid, and optionally by the @rid,
+ * from the @cal_cache. The returned @out_component should be freed with
+ * g_object_unref(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_component (ECalCache *cal_cache,
+                          const gchar *uid,
+                          const gchar *rid,
+                          ECalComponent **out_component,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       gchar *icalstring = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_component != NULL, FALSE);
+
+       success = e_cal_cache_get_component_as_string (cal_cache, uid, rid, &icalstring, cancellable, error);
+       if (success) {
+               *out_component = e_cal_component_new_from_string (icalstring);
+               g_free (icalstring);
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_cache_get_component_as_string:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @out_icalstring: (out) (transfer full): return location for an iCalendar string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a component identified by @uid, and optionally by the @rid,
+ * from the @cal_cache. The returned @out_icalstring should be freed with
+ * g_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_component_as_string (ECalCache *cal_cache,
+                                    const gchar *uid,
+                                    const gchar *rid,
+                                    gchar **out_icalstring,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       gchar *id;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_icalstring != NULL, FALSE);
+
+       id = ecc_encode_id_sql (uid, rid);
+
+       *out_icalstring = e_cache_get (E_CACHE (cal_cache), id, NULL, NULL, cancellable, error);
+
+       g_free (id);
+
+       return *out_icalstring != NULL;
+}
+
+/**
+ * e_cal_cache_set_component_extra:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @extra: (nullable): extra data to set for the component
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets or replaces the extra data associated with a component
+ * identified by @uid and optionally @rid.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_set_component_extra (ECalCache *cal_cache,
+                                const gchar *uid,
+                                const gchar *rid,
+                                const gchar *extra,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       gchar *id, *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       id = ecc_encode_id_sql (uid, rid);
+
+       if (!e_cache_contains (E_CACHE (cal_cache), id, E_CACHE_INCLUDE_DELETED)) {
+               g_free (id);
+
+               if (rid && *rid)
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s”, “%s” not 
found"), uid, rid);
+               else
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not 
found"), uid);
+
+               return FALSE;
+       }
+
+       if (extra) {
+               stmt = e_cache_sqlite_stmt_printf (
+                       "UPDATE " E_CACHE_TABLE_OBJECTS " SET " ECC_COLUMN_EXTRA "=%Q"
+                       " WHERE " E_CACHE_COLUMN_UID "=%Q",
+                       extra, id);
+       } else {
+               stmt = e_cache_sqlite_stmt_printf (
+                       "UPDATE " E_CACHE_TABLE_OBJECTS " SET " ECC_COLUMN_EXTRA "=NULL"
+                       " WHERE " E_CACHE_COLUMN_UID "=%Q",
+                       id);
+       }
+
+       success = e_cache_sqlite_exec (E_CACHE (cal_cache), stmt, cancellable, error);
+
+       e_cache_sqlite_stmt_free (stmt);
+       g_free (id);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_get_component_extra:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @out_extra: (out) (transfer full): return location to store the extra data
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the extra data previously set for @uid and @rid, either with
+ * e_cal_cache_set_component_extra() or when adding components.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_component_extra (ECalCache *cal_cache,
+                                const gchar *uid,
+                                const gchar *rid,
+                                gchar **out_extra,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       gchar *id, *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       id = ecc_encode_id_sql (uid, rid);
+
+       if (!e_cache_contains (E_CACHE (cal_cache), id, E_CACHE_INCLUDE_DELETED)) {
+               g_free (id);
+
+               if (rid && *rid)
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s”, “%s” not 
found"), uid, rid);
+               else
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not 
found"), uid);
+
+               return FALSE;
+       }
+
+       stmt = e_cache_sqlite_stmt_printf (
+               "SELECT " ECC_COLUMN_EXTRA " FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID "=%Q",
+               id);
+
+       success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt, e_cal_cache_get_string, out_extra, 
cancellable, error);
+
+       e_cache_sqlite_stmt_free (stmt);
+       g_free (id);
+
+       return success;
+}
+
+static GSList *
+ecc_icalstrings_to_components (GSList *icalstrings)
+{
+       GSList *link;
+
+       for (link = icalstrings; link; link = g_slist_next (link)) {
+               gchar *icalstring = link->data;
+
+               link->data = e_cal_component_new_from_string (icalstring);
+
+               g_free (icalstring);
+       }
+
+       return icalstrings;
+}
+
+/**
+ * e_cal_cache_get_components_by_uid:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @out_components: (out) (transfer full) (element-type ECalComponent): return location for the components
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the master object and all detached instances for a component
+ * identified by the @uid. Free the returned #GSList with
+ * g_slist_free_full (components, g_object_unref); when
+ * no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_by_uid (ECalCache *cal_cache,
+                                  const gchar *uid,
+                                  GSList **out_components,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       GSList *icalstrings = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_components != NULL, FALSE);
+
+       success = e_cal_cache_get_components_by_uid_as_string (cal_cache, uid, &icalstrings, cancellable, 
error);
+       if (success) {
+               *out_components = ecc_icalstrings_to_components (icalstrings);
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_cache_get_components_by_uid_as_string:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @out_icalstrings: (out) (transfer full) (element-type utf8): return location for the iCal strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the master object and all detached instances as string
+ * for a component identified by the @uid. Free the returned #GSList
+ * with g_slist_free_full (icalstrings, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_by_uid_as_string (ECalCache *cal_cache,
+                                             const gchar *uid,
+                                             GSList **out_icalstrings,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+       *out_icalstrings = NULL;
+
+       /* Using 'ORDER BY' to get the master object first */
+       stmt = e_cache_sqlite_stmt_printf (
+               "SELECT " E_CACHE_COLUMN_OBJECT " FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID "=%Q OR " E_CACHE_COLUMN_UID " LIKE '%q\n%%'"
+               " ORDER BY " E_CACHE_COLUMN_UID,
+               uid, uid);
+
+       success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt, e_cal_cache_get_strings, out_icalstrings, 
cancellable, error);
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       if (success && !*out_icalstrings) {
+               success = FALSE;
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+       } else if (success) {
+               *out_icalstrings = g_slist_reverse (*out_icalstrings);
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_cache_get_components_in_range:
+ * @cal_cache: an #ECalCache
+ * @range_start: start of the range, as time_t, inclusive
+ * @range_end: end of the range, as time_t, exclusive
+ * @out_components: (out) (transfer full) (element-type ECalComponent): return location for the components
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of components which occur in the given time range.
+ * It's not an error if none is found.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_in_range (ECalCache *cal_cache,
+                                    time_t range_start,
+                                    time_t range_end,
+                                    GSList **out_components,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       GSList *icalstrings = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (out_components != NULL, FALSE);
+
+       success = e_cal_cache_get_components_in_range_as_strings (cal_cache, range_start, range_end, 
&icalstrings, cancellable, error);
+       if (success)
+               *out_components = ecc_icalstrings_to_components (icalstrings);
+
+       return success;
+}
+
+static gboolean
+ecc_search_icalstrings_cb (ECalCache *cal_cache,
+                          const gchar *uid,
+                          const gchar *rid,
+                          const gchar *revision,
+                          const gchar *object,
+                          const gchar *extra,
+                          EOfflineState offline_state,
+                          gpointer user_data)
+{
+       GSList **out_icalstrings = user_data;
+
+       g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       *out_icalstrings = g_slist_prepend (*out_icalstrings, g_strdup (object));
+
+       return TRUE;
+}
+
+/**
+ * e_cal_cache_get_components_in_range_as_strings:
+ * @cal_cache: an #ECalCache
+ * @range_start: start of the range, as time_t, inclusive
+ * @range_end: end of the range, as time_t, exclusive
+ * @out_icalstrings: (out) (transfer full) (element-type utf8): return location for the iCal strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of components, as iCal strings, which occur in the given time range.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_in_range_as_strings (ECalCache *cal_cache,
+                                               time_t range_start,
+                                               time_t range_end,
+                                               GSList **out_icalstrings,
+                                               GCancellable *cancellable,
+                                               GError **error)
+{
+       gchar *sexp;
+       struct icaltimetype itt_start, itt_end;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+       *out_icalstrings = NULL;
+
+       itt_start = icaltime_from_timet_with_zone (range_start, FALSE, NULL);
+       itt_end = icaltime_from_timet_with_zone (range_end, FALSE, NULL);
+
+       sexp = g_strdup_printf ("(occur-in-time-range? (make-time \"%04d%02d%02dT%02d%02d%02dZ\") (make-time 
\"%04d%02d%02dT%02d%02d%02dZ\"))",
+               itt_start.year, itt_start.month, itt_start.day, itt_start.hour, itt_start.minute, 
itt_start.second,
+               itt_end.year, itt_end.month, itt_end.day, itt_end.hour, itt_end.minute, itt_end.second);
+
+       success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_icalstrings_cb,
+               out_icalstrings, cancellable, error);
+
+       g_free (sexp);
+
+       if (success) {
+               *out_icalstrings = g_slist_reverse (*out_icalstrings);
+       } else {
+               g_slist_free_full (*out_icalstrings, g_free);
+               *out_icalstrings = NULL;
+       }
+
+       return success;
+}
+
+static gboolean
+ecc_search_data_cb (ECalCache *cal_cache,
+                   const gchar *uid,
+                   const gchar *rid,
+                   const gchar *revision,
+                   const gchar *object,
+                   const gchar *extra,
+                   EOfflineState offline_state,
+                   gpointer user_data)
+{
+       GSList **out_data = user_data;
+
+       g_return_val_if_fail (out_data != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       *out_data = g_slist_prepend (*out_data,
+               e_cal_cache_search_data_new (uid, rid, object, extra));
+
+       return TRUE;
+}
+
+static gboolean
+ecc_search_components_cb (ECalCache *cal_cache,
+                         const gchar *uid,
+                         const gchar *rid,
+                         const gchar *revision,
+                         const gchar *object,
+                         const gchar *extra,
+                         EOfflineState offline_state,
+                         gpointer user_data)
+{
+       GSList **out_components = user_data;
+
+       g_return_val_if_fail (out_components != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       *out_components = g_slist_prepend (*out_components,
+               e_cal_component_new_from_string (object));
+
+       return TRUE;
+}
+
+static gboolean
+ecc_search_ids_cb (ECalCache *cal_cache,
+                  const gchar *uid,
+                  const gchar *rid,
+                  const gchar *revision,
+                  const gchar *object,
+                  const gchar *extra,
+                  EOfflineState offline_state,
+                  gpointer user_data)
+{
+       GSList **out_ids = user_data;
+
+       g_return_val_if_fail (out_ids != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       *out_ids = g_slist_prepend (*out_ids, e_cal_component_id_new (uid, rid));
+
+       return TRUE;
+}
+
+/**
+ * e_cal_cache_search:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @out_data: (out) (transfer full) (element-type ECalCacheSearchData): stored components, as search data, 
satisfied by @sexp
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and
+ * returns those components which satisfy the search
+ * expression as a #GSList of #ECalCacheSearchData.
+ * The @out_data should be freed with
+ * g_slist_free_full (data, e_cal_cache_search_data_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search (ECalCache *cal_cache,
+                   const gchar *sexp,
+                   GSList **out_data,
+                   GCancellable *cancellable,
+                   GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (out_data != NULL, FALSE);
+
+       *out_data = NULL;
+
+       success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_data_cb,
+               out_data, cancellable, error);
+       if (success) {
+               *out_data = g_slist_reverse (*out_data);
+       } else {
+               g_slist_free_full (*out_data, e_cal_cache_search_data_free);
+               *out_data = NULL;
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_cache_search_components:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @out_components: (out) (transfer full) (element-type ECalComponent): stored components satisfied by @sexp
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and
+ * returns those components which satisfy the search
+ * expression. The @out_components should be freed with
+ * g_slist_free_full (components, g_object_unref); when
+ * no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search_components (ECalCache *cal_cache,
+                              const gchar *sexp,
+                              GSList **out_components,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (out_components != NULL, FALSE);
+
+       *out_components = NULL;
+
+       success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_components_cb,
+               out_components, cancellable, error);
+       if (success) {
+               *out_components = g_slist_reverse (*out_components);
+       } else {
+               g_slist_free_full (*out_components, g_object_unref);
+               *out_components = NULL;
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_cache_search_ids:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @out_ids: (out) (transfer full) (element-type ECalComponentId): IDs of stored components satisfied by 
@sexp
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and returns ECalComponentId
+ * for those components which satisfy the search expression.
+ * The @out_ids should be freed with
+ * g_slist_free_full (components, (GDestroyNotify) e_cal_component_free_id);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search_ids (ECalCache *cal_cache,
+                       const gchar *sexp,
+                       GSList **out_ids,
+                       GCancellable *cancellable,
+                       GError **error)
+
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (out_ids != NULL, FALSE);
+
+       *out_ids = NULL;
+
+       success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_ids_cb,
+               out_ids, cancellable, error);
+       if (success) {
+               *out_ids = g_slist_reverse (*out_ids);
+       } else {
+               g_slist_free_full (*out_ids, g_object_unref);
+               *out_ids = NULL;
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_cache_search_with_callback:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @func: an #ECalCacheSearchFunc callback to call for each row which satisfies @sexp
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and calls @func for each
+ * row which satisfy the search expression.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search_with_callback (ECalCache *cal_cache,
+                                 const gchar *sexp,
+                                 ECalCacheSearchFunc func,
+                                 gpointer user_data,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       ECalBackendSExp *bsexp = NULL;
+       gint sexp_id = -1;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       if (sexp && *sexp) {
+               bsexp = e_cal_backend_sexp_new (sexp);
+               if (!bsexp) {
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+                               _("Invalid query: %s"), sexp);
+                       return FALSE;
+               }
+
+               sexp_id = ecc_take_sexp_object (cal_cache, bsexp);
+       }
+
+       success = ecc_search_internal (cal_cache, sexp, sexp_id, func, user_data, cancellable, error);
+
+       if (bsexp)
+               ecc_free_sexp_object (cal_cache, sexp_id);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_get_offline_changes:
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * The same as e_cache_get_offline_changes(), only splits the saved UID
+ * into UID and RID and saved the data into #ECalCacheOfflineChange structure.
+ *
+ * Returns: (transfer full) (element-type ECalCacheOfflineChange): A newly allocated list of all
+ *    offline changes. Free it with g_slist_free_full (slist, e_cal_cache_offline_change_free);
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_cal_cache_get_offline_changes        (ECalCache *cal_cache,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       GSList *changes, *link;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+       changes = e_cache_get_offline_changes (E_CACHE (cal_cache), cancellable, error);
+
+       for (link = changes; link; link = g_slist_next (link)) {
+               ECacheOfflineChange *cache_change = link->data;
+               ECalCacheOfflineChange *cal_change;
+               gchar *uid = NULL, *rid = NULL;
+
+               if (!cache_change || !ecc_decode_id_sql (cache_change->uid, &uid, &rid)) {
+                       g_warn_if_reached ();
+
+                       e_cache_offline_change_free (cache_change);
+                       link->data = NULL;
+
+                       continue;
+               }
+
+               cal_change = e_cal_cache_offline_change_new (uid, rid, cache_change->revision, 
cache_change->object, cache_change->state);
+               link->data = cal_change;
+
+               e_cache_offline_change_free (cache_change);
+               g_free (uid);
+               g_free (rid);
+       }
+
+       return changes;
+}
+
+/**
+ * e_cal_cache_delete_attachments:
+ * @cal_cache: an #ECalCache
+ * @component: an icalcomponent
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Deletes all locally stored attachments beside the cache file from the disk.
+ * This doesn't modify the @component. It's usually called before the @component
+ * is being removed from the @cal_cache.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_delete_attachments (ECalCache *cal_cache,
+                               icalcomponent *component,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       icalproperty *prop;
+       gchar *cache_dirname = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (component != NULL, FALSE);
+
+       for (prop = icalcomponent_get_first_property (component, ICAL_ATTACH_PROPERTY);
+            prop;
+            prop = icalcomponent_get_next_property (component, ICAL_ATTACH_PROPERTY)) {
+               icalattach *attach = icalproperty_get_attach (prop);
+
+               if (attach && icalattach_get_is_url (attach)) {
+                       const gchar *url;
+
+                       url = icalattach_get_url (attach);
+                       if (url) {
+                               gsize buf_size;
+                               gchar *buf;
+
+                               buf_size = strlen (url);
+                               buf = g_malloc0 (buf_size + 1);
+
+                               icalvalue_decode_ical_string (url, buf, buf_size);
+
+                               if (g_str_has_prefix (buf, "file://")) {
+                                       gchar *filename;
+
+                                       filename = g_filename_from_uri (buf, NULL, NULL);
+                                       if (filename) {
+                                               if (!cache_dirname)
+                                                       cache_dirname = g_path_get_dirname 
(e_cache_get_filename (E_CACHE (cal_cache)));
+
+                                               if (g_str_has_prefix (filename, cache_dirname) &&
+                                                   g_unlink (filename) == -1) {
+                                                       /* Ignore these errors */
+                                               }
+
+                                               g_free (filename);
+                                       }
+                               }
+
+                               g_free (buf);
+                       }
+               }
+       }
+
+       g_free (cache_dirname);
+
+       return TRUE;
+}
+
+/**
+ * e_cal_cache_put_timezone:
+ * @cal_cache: an #ECalCache
+ * @zone: an icaltimezone to put
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Puts the @zone into the @cal_cache using its timezone ID as
+ * an identificator. The function adds a new or replaces existing,
+ * if any such already exists in the @cal_cache.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_put_timezone (ECalCache *cal_cache,
+                         const icaltimezone *zone,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       gboolean success;
+       gchar *stmt;
+       const gchar *tzid;
+       gchar *component_str;
+       icalcomponent *component;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (zone != NULL, FALSE);
+
+       tzid = icaltimezone_get_tzid ((icaltimezone *) zone);
+       if (!tzid) {
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Cannot add timezone 
without tzid"));
+               return FALSE;
+       }
+
+       component = icaltimezone_get_component ((icaltimezone *) zone);
+       if (!component) {
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Cannot add timezone 
without component"));
+               return FALSE;
+       }
+
+       component_str = icalcomponent_as_ical_string_r (component);
+       if (!component_str) {
+               g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Cannot add timezone 
with invalid component"));
+               return FALSE;
+       }
+
+       stmt = e_cache_sqlite_stmt_printf (
+               "INSERT or REPLACE INTO " ECC_TABLE_TIMEZONES " (tzid, zone) VALUES (%Q, %Q)",
+               tzid, component_str);
+
+       success = e_cache_sqlite_exec (E_CACHE (cal_cache), stmt, cancellable, error);
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       g_free (component_str);
+
+       return success;
+}
+
+static icaltimezone *
+ecc_timezone_from_string (const gchar *icalstring)
+{
+       icalcomponent *component;
+
+       g_return_val_if_fail (icalstring != NULL, NULL);
+
+       component = icalcomponent_new_from_string (icalstring);
+       if (component) {
+               icaltimezone *zone;
+
+               zone = icaltimezone_new ();
+               if (!icaltimezone_set_component (zone, component)) {
+                       icalcomponent_free (component);
+                       icaltimezone_free (zone, 1);
+               } else {
+                       return zone;
+               }
+       }
+
+       return NULL;
+}
+
+/**
+ * e_cal_cache_get_timezone:
+ * @cal_cache: an #ECalCache
+ * @tzid: a timezone ID to get
+ * @out_zone: (out) (transfer none): return location for the icaltimezone
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a timezone with given @tzid, which had been previously put
+ * into the @cal_cache with e_cal_cache_put_timezone().
+ * The returned icaltimezone is owned by the @cal_cache and should
+ * not be freed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_timezone (ECalCache *cal_cache,
+                         const gchar *tzid,
+                         icaltimezone **out_zone,
+                         GCancellable *cancellable,
+                         GError **error)
+
+{
+       gchar *zone_str = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (tzid != NULL, FALSE);
+       g_return_val_if_fail (out_zone != NULL, FALSE);
+
+       g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+       *out_zone = g_hash_table_lookup (cal_cache->priv->loaded_timezones, tzid);
+       if (*out_zone) {
+               g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+               return TRUE;
+       }
+
+       *out_zone = g_hash_table_lookup (cal_cache->priv->modified_timezones, tzid);
+       if (*out_zone) {
+               g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+               return TRUE;
+       }
+
+       success = e_cal_cache_dup_timezone_as_string (cal_cache, tzid, &zone_str, cancellable, error);
+
+       if (success && zone_str) {
+               icaltimezone *zone;
+
+               zone = ecc_timezone_from_string (zone_str);
+               if (zone) {
+                       g_hash_table_insert (cal_cache->priv->loaded_timezones, g_strdup (tzid), zone);
+                       *out_zone = zone;
+               } else {
+                       success = FALSE;
+               }
+       }
+
+       g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+       g_free (zone_str);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_dup_timezone_as_string:
+ * @cal_cache: an #ECalCache
+ * @tzid: a timezone ID to get
+ * @out_zone_string: (out) (transfer full): return location for the icaltimezone as iCal string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a timezone with given @tzid, which had been previously put
+ * into the @cal_cache with e_cal_cache_put_timezone().
+ * The returned string is an iCal string for that icaltimezone and
+ * should be freed with g_free() when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_dup_timezone_as_string (ECalCache *cal_cache,
+                                   const gchar *tzid,
+                                   gchar **out_zone_string,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (tzid != NULL, FALSE);
+       g_return_val_if_fail (out_zone_string, FALSE);
+
+       *out_zone_string = NULL;
+
+       stmt = e_cache_sqlite_stmt_printf (
+               "SELECT zone FROM " ECC_TABLE_TIMEZONES " WHERE tzid=%Q",
+               tzid);
+
+       success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt, e_cal_cache_get_string, out_zone_string, 
cancellable, error) &&
+               *out_zone_string != NULL;
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       return success;
+}
+
+static gboolean
+e_cal_cache_get_uint64_cb (ECache *cache,
+                          gint ncols,
+                          const gchar **column_names,
+                          const gchar **column_values,
+                          gpointer user_data)
+{
+       guint64 *pui64 = user_data;
+
+       g_return_val_if_fail (pui64 != NULL, FALSE);
+
+       if (ncols == 1) {
+               *pui64 = column_values[0] ? g_ascii_strtoull (column_values[0], NULL, 10) : 0;
+       } else {
+               *pui64 = 0;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_cal_cache_load_zones_cb (ECache *cache,
+                          gint ncols,
+                          const gchar *column_names[],
+                          const gchar *column_values[],
+                          gpointer user_data)
+{
+       GHashTable *loaded_zones = user_data;
+
+       g_return_val_if_fail (loaded_zones != NULL, FALSE);
+       g_return_val_if_fail (ncols == 2, FALSE);
+
+       /* Do not overwrite already loaded timezones, they can be used anywhere around */
+       if (!g_hash_table_lookup (loaded_zones, column_values[0])) {
+               icaltimezone *zone;
+
+               zone = ecc_timezone_from_string (column_values[1]);
+               if (zone) {
+                       g_hash_table_insert (loaded_zones, g_strdup (column_values[0]), zone);
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cal_cache_list_timezones:
+ * @cal_cache: an #ECalCache
+ * @out_timezones: (out) (transfer container) (element-type icaltimezone): return location for the list of 
stored timezones
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of all stored timezones by the @cal_cache.
+ * Only the returned list should be freed with g_list_free()
+ * when no longer needed; the icaltimezone-s are owned
+ * by the @cal_cache.
+ *
+ * Note: The list can contain timezones previously stored
+ * in the cache, but removed from it since they were loaded,
+ * because these are freed only when also the @cal_cache is freed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_list_timezones (ECalCache *cal_cache,
+                           GList **out_timezones,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       guint64 n_stored = 0;
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (out_timezones != NULL, FALSE);
+
+       g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+       success = e_cache_sqlite_select (E_CACHE (cal_cache),
+               "SELECT COUNT(*) FROM " ECC_TABLE_TIMEZONES,
+               e_cal_cache_get_uint64_cb, &n_stored, cancellable, error);
+
+       if (success && n_stored != g_hash_table_size (cal_cache->priv->loaded_timezones)) {
+               if (n_stored == 0) {
+                       g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+                       *out_timezones = NULL;
+
+                       return TRUE;
+               }
+
+               stmt = e_cache_sqlite_stmt_printf ("SELECT tzid, zone FROM " ECC_TABLE_TIMEZONES);
+               success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt,
+                       e_cal_cache_load_zones_cb, cal_cache->priv->loaded_timezones, cancellable, error);
+               e_cache_sqlite_stmt_free (stmt);
+       }
+
+       if (success) {
+               GList *loaded, *modified;
+
+               loaded = g_hash_table_get_values (cal_cache->priv->loaded_timezones);
+               modified = g_hash_table_get_values (cal_cache->priv->modified_timezones);
+
+               if (loaded && modified)
+                       *out_timezones = g_list_concat (loaded, modified);
+               else
+                       *out_timezones = loaded ? loaded : modified;
+       }
+
+       g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+       return success;
+}
+
+/**
+ * e_cal_cache_remove_timezones:
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes all stored timezones from the @cal_cache.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_remove_timezones (ECalCache *cal_cache,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       e_cache_lock (E_CACHE (cal_cache), E_CACHE_LOCK_WRITE);
+
+       g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+       success = e_cache_sqlite_exec (E_CACHE (cal_cache), "DELETE FROM " ECC_TABLE_TIMEZONES, cancellable, 
error);
+
+       g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+       e_cache_unlock (E_CACHE (cal_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+void _e_cal_cache_remove_loaded_timezones (ECalCache *cal_cache);
+
+/* Private function, not meant to be part of the public API */
+void
+_e_cal_cache_remove_loaded_timezones (ECalCache *cal_cache)
+{
+       g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+
+       g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+       g_hash_table_remove_all (cal_cache->priv->loaded_timezones);
+       g_hash_table_remove_all (cal_cache->priv->modified_timezones);
+
+       g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+}
+
+/**
+ * e_cal_cache_resolve_timezone_cb:
+ * @tzid: a timezone ID
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * An #ECalRecurResolveTimezoneCb callback, which can be used
+ * with e_cal_recur_generate_instances_sync(). The @cal_cache
+ * is supposed to be an #ECalCache instance. See also
+ * e_cal_cache_resolve_timezone_simple_cb().
+ *
+ * Returns: (transfer none) (nullable): the resolved icaltimezone, or %NULL, if not found
+ *
+ * Since: 3.26
+ **/
+icaltimezone *
+e_cal_cache_resolve_timezone_cb (const gchar *tzid,
+                                gpointer cal_cache,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+       return e_cal_cache_resolve_timezone_simple_cb (tzid, cal_cache);
+}
+
+/**
+ * e_cal_cache_resolve_timezone_simple_cb:
+ * @tzid: a timezone ID
+ * @cal_cache: an #ECalCache
+ *
+ * An #ECalRecurResolveTimezoneFn callback, which can be used
+ * with e_cal_recur_ensure_end_dates() and simialr functions.
+ * The @cal_cache is supposed to be an #ECalCache instance. See
+ * also e_cal_cache_resolve_timezone_cb().
+ *
+ * Returns: (transfer none) (nullable): the resolved icaltimezone, or %NULL, if not found
+ *
+ * Since: 3.26
+ **/
+icaltimezone *
+e_cal_cache_resolve_timezone_simple_cb (const gchar *tzid,
+                                       gpointer cal_cache)
+{
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+       return e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cal_cache), tzid);
+}
+
+static gboolean
+ecc_search_delete_attachment_cb (ECalCache *cal_cache,
+                                const gchar *uid,
+                                const gchar *rid,
+                                const gchar *revision,
+                                const gchar *object,
+                                const gchar *extra,
+                                EOfflineState offline_state,
+                                gpointer user_data)
+{
+       icalcomponent *icalcomp;
+       GCancellable *cancellable = user_data;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       icalcomp = icalcomponent_new_from_string (object);
+       if (!icalcomp)
+               return TRUE;
+
+       if (!e_cal_cache_delete_attachments (cal_cache, icalcomp, cancellable, &local_error)) {
+               if (rid && !*rid)
+                       rid = NULL;
+
+               g_debug ("%s: Failed to remove attachments for '%s%s%s': %s", G_STRFUNC,
+                       uid, rid ? "|" : "", rid ? rid : "", local_error ? local_error->message : "Unknown 
error");
+               g_clear_error (&local_error);
+       }
+
+       icalcomponent_free (icalcomp);
+
+       return !g_cancellable_is_cancelled (cancellable);
+}
+
+static gboolean
+ecc_empty_aux_tables (ECache *cache,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       gchar *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cache), FALSE);
+
+       stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q", ECC_TABLE_TIMEZONES);
+       success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+       e_cache_sqlite_stmt_free (stmt);
+
+       return success;
+}
+
+/* The default revision is a concatenation of
+   <DTSTAMP> "-" <LAST-MODIFIED> "-" <SEQUENCE> */
+static gchar *
+ecc_dup_component_revision (ECalCache *cal_cache,
+                           icalcomponent *icalcomp)
+{
+       struct icaltimetype itt;
+       icalproperty *prop;
+       GString *revision;
+
+       g_return_val_if_fail (icalcomp != NULL, NULL);
+
+       revision = g_string_sized_new (48);
+
+       itt = icalcomponent_get_dtstamp (icalcomp);
+       if (icaltime_is_null_time (itt) || !icaltime_is_valid_time (itt)) {
+               g_string_append_c (revision, 'x');
+       } else {
+               g_string_append_printf (revision, "%04d%02d%02d%02d%02d%02d",
+                       itt.year, itt.month, itt.day,
+                       itt.hour, itt.minute, itt.second);
+       }
+
+       g_string_append_c (revision, '-');
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_LASTMODIFIED_PROPERTY);
+       if (prop)
+               itt = icalproperty_get_lastmodified (prop);
+
+       if (!prop || icaltime_is_null_time (itt) || !icaltime_is_valid_time (itt)) {
+               g_string_append_c (revision, 'x');
+       } else {
+               g_string_append_printf (revision, "%04d%02d%02d%02d%02d%02d",
+                       itt.year, itt.month, itt.day,
+                       itt.hour, itt.minute, itt.second);
+       }
+
+       g_string_append_c (revision, '-');
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_SEQUENCE_PROPERTY);
+       if (!prop) {
+               g_string_append_c (revision, 'x');
+       } else {
+               g_string_append_printf (revision, "%d", icalproperty_get_sequence (prop));
+       }
+
+       return g_string_free (revision, FALSE);
+}
+
+static gboolean
+e_cal_cache_put_locked (ECache *cache,
+                       const gchar *uid,
+                       const gchar *revision,
+                       const gchar *object,
+                       ECacheColumnValues *other_columns,
+                       EOfflineState offline_state,
+                       gboolean is_replace,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       ECalCache *cal_cache;
+       ECalComponent *comp;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cache), FALSE);
+       g_return_val_if_fail (E_CACHE_CLASS (e_cal_cache_parent_class)->put_locked != NULL, FALSE);
+
+       cal_cache = E_CAL_CACHE (cache);
+
+       comp = e_cal_component_new_from_string (object);
+       if (!comp)
+               return FALSE;
+
+       ecc_fill_other_columns (cal_cache, other_columns, comp);
+
+       success = E_CACHE_CLASS (e_cal_cache_parent_class)->put_locked (cache, uid, revision, object, 
other_columns, offline_state,
+               is_replace, cancellable, error);
+
+       g_clear_object (&comp);
+
+       return success;
+}
+
+static gboolean
+e_cal_cache_remove_all_locked (ECache *cache,
+                              const GSList *uids,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_CACHE (cache), FALSE);
+       g_return_val_if_fail (E_CACHE_CLASS (e_cal_cache_parent_class)->remove_all_locked != NULL, FALSE);
+
+       /* Cannot free content of priv->loaded_timezones and priv->modified_timezones,
+          because those can be used anywhere */
+       success = ecc_empty_aux_tables (cache, cancellable, error) &&
+               e_cal_cache_search_with_callback (E_CAL_CACHE (cache), NULL,
+               ecc_search_delete_attachment_cb, cancellable, cancellable, error);
+
+       success = success && E_CACHE_CLASS (e_cal_cache_parent_class)->remove_all_locked (cache, uids, 
cancellable, error);
+
+       return success;
+}
+
+static void
+cal_cache_free_zone (gpointer ptr)
+{
+       icaltimezone *zone = ptr;
+
+       if (zone)
+               icaltimezone_free (zone, 1);
+}
+
+static void
+ecc_add_cached_timezone (ETimezoneCache *cache,
+                        icaltimezone *zone)
+{
+       ECalCache *cal_cache;
+       const gchar *tzid;
+
+       cal_cache = E_CAL_CACHE (cache);
+
+       tzid = icaltimezone_get_tzid (zone);
+       if (tzid == NULL)
+               return;
+
+       e_cal_cache_put_timezone (cal_cache, zone, NULL, NULL);
+}
+
+static icaltimezone *
+ecc_get_cached_timezone (ETimezoneCache *cache,
+                        const gchar *tzid)
+{
+       ECalCache *cal_cache;
+       icaltimezone *zone = NULL;
+       icaltimezone *builtin_zone = NULL;
+       icalcomponent *icalcomp;
+       icalproperty *prop;
+       const gchar *builtin_tzid;
+
+       cal_cache = E_CAL_CACHE (cache);
+
+       if (g_str_equal (tzid, "UTC"))
+               return icaltimezone_get_utc_timezone ();
+
+       g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+       /* See if we already have it in the cache. */
+       zone = g_hash_table_lookup (cal_cache->priv->loaded_timezones, tzid);
+       if (zone)
+               goto exit;
+
+       zone = g_hash_table_lookup (cal_cache->priv->modified_timezones, tzid);
+       if (zone)
+               goto exit;
+
+       /* Try the location first */
+       /*zone = icaltimezone_get_builtin_timezone (tzid);
+       if (zone)
+               goto exit;*/
+
+       /* Try to replace the original time zone with a more complete
+        * and/or potentially updated built-in time zone.  Note this also
+        * applies to TZIDs which match built-in time zones exactly: they
+        * are extracted via icaltimezone_get_builtin_timezone_from_tzid(). */
+
+       builtin_tzid = e_cal_match_tzid (tzid);
+
+       if (builtin_tzid)
+               builtin_zone = icaltimezone_get_builtin_timezone_from_tzid (builtin_tzid);
+
+       if (!builtin_zone) {
+               e_cal_cache_get_timezone (cal_cache, tzid, &zone, NULL, NULL);
+               goto exit;
+       }
+
+       /* Use the built-in time zone *and* rename it.  Likely the caller
+        * is asking for a specific TZID because it has an event with such
+        * a TZID.  Returning an icaltimezone with a different TZID would
+        * lead to broken VCALENDARs in the caller. */
+
+       icalcomp = icaltimezone_get_component (builtin_zone);
+       icalcomp = icalcomponent_new_clone (icalcomp);
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_ANY_PROPERTY);
+
+       while (prop != NULL) {
+               if (icalproperty_isa (prop) == ICAL_TZID_PROPERTY) {
+                       icalproperty_set_value_from_string (prop, tzid, "NO");
+                       break;
+               }
+
+               prop = icalcomponent_get_next_property (icalcomp, ICAL_ANY_PROPERTY);
+       }
+
+       if (icalcomp != NULL) {
+               zone = icaltimezone_new ();
+               if (icaltimezone_set_component (zone, icalcomp)) {
+                       tzid = icaltimezone_get_tzid (zone);
+                       g_hash_table_insert (cal_cache->priv->modified_timezones, g_strdup (tzid), zone);
+               } else {
+                       icalcomponent_free (icalcomp);
+                       icaltimezone_free (zone, 1);
+                       zone = NULL;
+               }
+       }
+
+ exit:
+       g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+       return zone;
+}
+
+static GList *
+ecc_list_cached_timezones (ETimezoneCache *cache)
+{
+       GList *timezones = NULL;
+
+       e_cal_cache_list_timezones (E_CAL_CACHE (cache), &timezones, NULL, NULL);
+
+       return timezones;
+}
+
+static void
+e_cal_cache_finalize (GObject *object)
+{
+       ECalCache *cal_cache = E_CAL_CACHE (object);
+
+       g_hash_table_destroy (cal_cache->priv->loaded_timezones);
+       g_hash_table_destroy (cal_cache->priv->modified_timezones);
+       g_hash_table_destroy (cal_cache->priv->sexps);
+
+       g_rec_mutex_clear (&cal_cache->priv->timezones_lock);
+       g_mutex_clear (&cal_cache->priv->sexps_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_cache_parent_class)->finalize (object);
+}
+
+static void
+e_cal_cache_class_init (ECalCacheClass *klass)
+{
+       GObjectClass *object_class;
+       ECacheClass *cache_class;
+
+       g_type_class_add_private (klass, sizeof (ECalCachePrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = e_cal_cache_finalize;
+
+       cache_class = E_CACHE_CLASS (klass);
+       cache_class->put_locked = e_cal_cache_put_locked;
+       cache_class->remove_all_locked = e_cal_cache_remove_all_locked;
+
+       klass->dup_component_revision = ecc_dup_component_revision;
+
+       /**
+        * @ECalCache:dup-component-revision:
+        * A signal being called to get revision of an icalcomponent.
+        * The default implementation uses a concatenation of
+        * DTSTAMP '-' LASTMODIFIED '-' SEQUENCE.
+        **/
+       signals[DUP_COMPONENT_REVISION] = g_signal_new (
+               "dup-component-revision",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+               G_STRUCT_OFFSET (ECalCacheClass, dup_component_revision),
+               g_signal_accumulator_first_wins,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_STRING, 1,
+               G_TYPE_POINTER);
+}
+
+static void
+ecc_timezone_cache_init (ETimezoneCacheInterface *iface)
+{
+       iface->add_timezone = ecc_add_cached_timezone;
+       iface->get_timezone = ecc_get_cached_timezone;
+       iface->list_timezones = ecc_list_cached_timezones;
+}
+
+static void
+e_cal_cache_init (ECalCache *cal_cache)
+{
+       cal_cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (cal_cache, E_TYPE_CAL_CACHE, ECalCachePrivate);
+       cal_cache->priv->loaded_timezones = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
cal_cache_free_zone);
+       cal_cache->priv->modified_timezones = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
cal_cache_free_zone);
+
+       cal_cache->priv->sexps = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref);
+
+       g_rec_mutex_init (&cal_cache->priv->timezones_lock);
+       g_mutex_init (&cal_cache->priv->sexps_lock);
+}
diff --git a/src/calendar/libedata-cal/e-cal-cache.h b/src/calendar/libedata-cal/e-cal-cache.h
new file mode 100644
index 0000000..ede88a7
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-cache.h
@@ -0,0 +1,335 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2016 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 (__LIBEDATA_CAL_H_INSIDE__) && !defined (LIBEDATA_CAL_COMPILATION)
+#error "Only <libedata-cal/libedata-cal.h> should be included directly."
+#endif
+
+#ifndef E_CAL_CACHE_H
+#define E_CAL_CACHE_H
+
+#include <libebackend/libebackend.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CAL_CACHE \
+       (e_cal_cache_get_type ())
+#define E_CAL_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CAL_CACHE, ECalCache))
+#define E_CAL_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_CAL_CACHE, ECalCacheClass))
+#define E_IS_CAL_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CAL_CACHE))
+#define E_IS_CAL_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_CAL_CACHE))
+#define E_CAL_CACHE_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_CAL_CACHE, ECalCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ECalCache ECalCache;
+typedef struct _ECalCacheClass ECalCacheClass;
+typedef struct _ECalCachePrivate ECalCachePrivate;
+
+/**
+ * ECalCacheOfflineChange:
+ * @uid: UID of the component
+ * @rid: Recurrence-ID of the component
+ * @revision: stored revision of the component
+ * @object: the component itself, as iCalalendar string
+ * @state: an #EOfflineState of the component
+ *
+ * Holds the information about offline change for one component.
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+       gchar *uid;
+       gchar *rid;
+       gchar *revision;
+       gchar *object;
+       EOfflineState state;
+} ECalCacheOfflineChange;
+
+#define E_TYPE_CAL_CACHE_OFFLINE_CHANGE (e_cal_cache_offline_change_get_type ())
+
+GType          e_cal_cache_offline_change_get_type
+                                               (void) G_GNUC_CONST;
+ECalCacheOfflineChange *
+               e_cal_cache_offline_change_new  (const gchar *uid,
+                                                const gchar *rid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                EOfflineState state);
+ECalCacheOfflineChange *
+               e_cal_cache_offline_change_copy (const ECalCacheOfflineChange *change);
+void           e_cal_cache_offline_change_free (/* ECalCacheOfflineChange */ gpointer change);
+
+/**
+ * ECalCacheSearchData:
+ * @uid: the UID of this component
+ * @rid: (nullable): the Recurrence-ID of this component
+ * @object: the component string
+ * @extra: any extra data associated with the component
+ *
+ * This structure is used to represent components returned
+ * by the #ECalCache from various functions
+ * such as e_cal_cache_search().
+ *
+ * The @extra parameter will contain any data which was
+ * previously passed for this component in e_cal_cache_add_component()
+ * or set with e_cal_cache_set_component_extra().
+ *
+ * These should be freed with e_cal_cache_search_data_free().
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+       gchar *uid;
+       gchar *rid;
+       gchar *object;
+       gchar *extra;
+} ECalCacheSearchData;
+
+#define E_TYPE_CAL_CACHE_SEARCH_DATA (e_cal_cache_search_data_get_type ())
+
+GType          e_cal_cache_search_data_get_type
+                                               (void) G_GNUC_CONST;
+ECalCacheSearchData *
+               e_cal_cache_search_data_new     (const gchar *uid,
+                                                const gchar *rid,
+                                                const gchar *object,
+                                                const gchar *extra);
+ECalCacheSearchData *
+               e_cal_cache_search_data_copy    (const ECalCacheSearchData *data);
+void           e_cal_cache_search_data_free    (/* ECalCacheSearchData * */ gpointer data);
+
+/**
+ * ECalCacheSearchFunc:
+ * @cal_cache: an #ECalCache
+ * @uid: a unique object identifier
+ * @rid: (nullable): an optional Recurrence-ID of the object
+ * @revision: the object revision
+ * @object: the object itself
+ * @extra: extra data stored with the object
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @user_data: user data, as used in e_cache_cache_search_with_callback()
+ *
+ * A callback called for each object row when using
+ * e_cal_cache_search_with_callback() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECalCacheSearchFunc)       (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const gchar *extra,
+                                                EOfflineState offline_state,
+                                                gpointer user_data);
+
+/**
+ * ECalCache:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ECalCache {
+       /*< private >*/
+       ECache parent;
+       ECalCachePrivate *priv;
+};
+
+/**
+ * ECalCacheClass:
+ *
+ * Class structure for the #ECalCache class.
+ *
+ * Since: 3.26
+ */
+struct _ECalCacheClass {
+       /*< private >*/
+       ECacheClass parent_class;
+
+       /* Signals */
+       gchar *         (* dup_component_revision)
+                                               (ECalCache *cal_cache,
+                                                icalcomponent *icalcomp);
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_cal_cache_get_type            (void) G_GNUC_CONST;
+
+ECalCache *    e_cal_cache_new                 (const gchar *filename,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gchar *                e_cal_cache_dup_component_revision
+                                               (ECalCache *cal_cache,
+                                                icalcomponent *icalcomp);
+gboolean       e_cal_cache_contains            (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                ECacheDeletedFlag deleted_flag);
+gboolean       e_cal_cache_put_component       (ECalCache *cal_cache,
+                                                ECalComponent *component,
+                                                const gchar *extra,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_put_components      (ECalCache *cal_cache,
+                                                const GSList *components, /* ECalComponent * */
+                                                const GSList *extras, /* gchar * */
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_remove_component    (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_remove_components   (ECalCache *cal_cache,
+                                                const GSList *ids, /* ECalComponentId * */
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_component       (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                ECalComponent **out_component,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_component_as_string
+                                               (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                gchar **out_icalstring,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_set_component_extra (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                const gchar *extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_component_extra (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                const gchar *rid,
+                                                gchar **out_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_components_by_uid
+                                               (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                GSList **out_components, /* ECalComponent * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_components_by_uid_as_string
+                                               (ECalCache *cal_cache,
+                                                const gchar *uid,
+                                                GSList **out_icalstrings, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_components_in_range
+                                               (ECalCache *cal_cache,
+                                                time_t range_start,
+                                                time_t range_end,
+                                                GSList **out_components, /* ECalComponent * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_components_in_range_as_strings
+                                               (ECalCache *cal_cache,
+                                                time_t range_start,
+                                                time_t range_end,
+                                                GSList **out_icalstrings, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_search              (ECalCache *cal_cache,
+                                                const gchar *sexp,
+                                                GSList **out_data, /* ECalCacheSearchData * * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_search_components   (ECalCache *cal_cache,
+                                                const gchar *sexp,
+                                                GSList **out_components, /* ECalComponent * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_search_ids          (ECalCache *cal_cache,
+                                                const gchar *sexp,
+                                                GSList **out_ids, /* ECalComponentId * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_search_with_callback
+                                               (ECalCache *cal_cache,
+                                                const gchar *sexp,
+                                                ECalCacheSearchFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+GSList *       e_cal_cache_get_offline_changes (ECalCache *cal_cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_delete_attachments  (ECalCache *cal_cache,
+                                                icalcomponent *component,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+gboolean       e_cal_cache_put_timezone        (ECalCache *cal_cache,
+                                                const icaltimezone *zone,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_get_timezone        (ECalCache *cal_cache,
+                                                const gchar *tzid,
+                                                icaltimezone **out_zone,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_dup_timezone_as_string
+                                               (ECalCache *cal_cache,
+                                                const gchar *tzid,
+                                                gchar **out_zone_string,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_list_timezones      (ECalCache *cal_cache,
+                                                GList **out_timezones,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_cache_remove_timezones    (ECalCache *cal_cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+icaltimezone * e_cal_cache_resolve_timezone_cb (const gchar *tzid,
+                                                gpointer cal_cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+icaltimezone * e_cal_cache_resolve_timezone_simple_cb
+                                               (const gchar *tzid,
+                                                gpointer cal_cache);
+
+G_END_DECLS
+
+#endif /* E_CAL_CACHE_H */
diff --git a/src/calendar/libedata-cal/e-cal-meta-backend.c b/src/calendar/libedata-cal/e-cal-meta-backend.c
new file mode 100644
index 0000000..49ee547
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.c
@@ -0,0 +1,4570 @@
+/* -*- 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-cal-meta-backend
+ * @include: libedata-cal/libedata-cal.h
+ * @short_description: An #ECalBackend descendant for calendar backends
+ *
+ * The #ECalMetaBackend is an abstract #ECalBackend descendant which
+ * aims to implement all evolution-data-server internals for the backend
+ * itself and lefts the backend do as minimum work as possible, like
+ * loading and saving components, listing available components and so on,
+ * thus the backend implementation can focus on things like converting
+ * (possibly) remote data into iCalendar objects and back.
+ *
+ * As the #ECalMetaBackend uses an #ECalCache, the offline support
+ * is provided by default.
+ *
+ * The structure is thread safe.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-cal-backend-sexp.h"
+#include "e-cal-backend-sync.h"
+#include "e-cal-backend-util.h"
+#include "e-cal-meta-backend.h"
+
+#define ECMB_KEY_SYNC_TAG              "ecmb::sync-tag"
+#define ECMB_KEY_EVER_CONNECTED                "ecmb::ever-connected"
+#define ECMB_KEY_CONNECTED_WRITABLE    "ecmb::connected-writable"
+
+#define LOCAL_PREFIX "file://"
+
+struct _ECalMetaBackendPrivate {
+       GMutex connect_lock;
+       GMutex property_lock;
+       GError *create_cache_error;
+       ECalCache *cache;
+       ENamedParameters *last_credentials;
+       GHashTable *view_cancellables;
+       GCancellable *refresh_cancellable;      /* Set when refreshing the content */
+       GCancellable *source_changed_cancellable; /* Set when processing source changed signal */
+       GCancellable *go_offline_cancellable;   /* Set when going offline */
+       gboolean current_online_state;          /* The only state of the internal structures;
+                                                  used to detect false notifications on EBackend::online */
+       gulong source_changed_id;
+       gulong notify_online_id;
+       gulong revision_changed_id;
+       guint refresh_timeout_id;
+
+       gboolean refresh_after_authenticate;
+       gint ever_connected;
+       gint connected_writable;
+
+       /* Last successful connect data, for some extensions */
+       guint16 authentication_port;
+       gchar *authentication_host;
+       gchar *authentication_user;
+       gchar *authentication_method;
+       gchar *authentication_proxy_uid;
+       gchar *authentication_credential_name;
+       SoupURI *webdav_soup_uri;
+};
+
+enum {
+       PROP_0,
+       PROP_CACHE
+};
+
+enum {
+       REFRESH_COMPLETED,
+       SOURCE_CHANGED,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_ABSTRACT_TYPE (ECalMetaBackend, e_cal_meta_backend, E_TYPE_CAL_BACKEND_SYNC)
+
+G_DEFINE_BOXED_TYPE (ECalMetaBackendInfo, e_cal_meta_backend_info, e_cal_meta_backend_info_copy, 
e_cal_meta_backend_info_free)
+
+static void ecmb_schedule_refresh (ECalMetaBackend *meta_backend);
+static void ecmb_schedule_source_changed (ECalMetaBackend *meta_backend);
+static void ecmb_schedule_go_offline (ECalMetaBackend *meta_backend);
+static gboolean ecmb_load_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                                 ECalCache *cal_cache,
+                                                 const gchar *uid,
+                                                 const gchar *preloaded_object,
+                                                 const gchar *preloaded_extra,
+                                                 gchar **out_new_uid,
+                                                 GCancellable *cancellable,
+                                                 GError **error);
+static gboolean ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                                 ECalCache *cal_cache,
+                                                 gboolean overwrite_existing,
+                                                 EConflictResolution conflict_resolution,
+                                                 const GSList *in_instances,
+                                                 const gchar *extra,
+                                                 const gchar *orig_uid,
+                                                 gboolean *out_requires_put,
+                                                 gchar **out_new_uid,
+                                                 gchar **out_new_extra,
+                                                 GCancellable *cancellable,
+                                                 GError **error);
+
+/**
+ * e_cal_meta_backend_info_new:
+ * @uid: a component UID; cannot be %NULL
+ * @revision: (nullable): the component revision; can be %NULL
+ * @object: (nullable): the component object as an iCalendar string; can be %NULL
+ * @extra: (nullable): extra backend-specific data; can be %NULL
+ *
+ * Creates a new #ECalMetaBackendInfo prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #ECalMetaBackendInfo. Free it with
+ *    e_cal_meta_backend_info_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalMetaBackendInfo *
+e_cal_meta_backend_info_new (const gchar *uid,
+                            const gchar *revision,
+                            const gchar *object,
+                            const gchar *extra)
+{
+       ECalMetaBackendInfo *info;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       info = g_new0 (ECalMetaBackendInfo, 1);
+       info->uid = g_strdup (uid);
+       info->revision = g_strdup (revision);
+       info->object = g_strdup (object);
+       info->extra = g_strdup (extra);
+
+       return info;
+}
+
+/**
+ * e_cal_meta_backend_info_copy:
+ * @src: (nullable): a source ECalMetaBackendInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @src. Free it with
+ *    e_cal_meta_backend_info_free() when no longer needed.
+ *    If the @src is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalMetaBackendInfo *
+e_cal_meta_backend_info_copy (const ECalMetaBackendInfo *src)
+{
+       if (!src)
+               return NULL;
+
+       return e_cal_meta_backend_info_new (src->uid, src->revision, src->object, src->extra);
+}
+
+/**
+ * e_cal_meta_backend_info_free:
+ * @ptr: (nullable): an #ECalMetaBackendInfo
+ *
+ * Frees the @ptr structure, previously allocated with e_cal_meta_backend_info_new()
+ * or e_cal_meta_backend_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_info_free (gpointer ptr)
+{
+       ECalMetaBackendInfo *info = ptr;
+
+       if (info) {
+               g_free (info->uid);
+               g_free (info->revision);
+               g_free (info->object);
+               g_free (info->extra);
+               g_free (info);
+       }
+}
+
+/* Unref returned cancellable with g_object_unref(), when done with it */
+static GCancellable *
+ecmb_create_view_cancellable (ECalMetaBackend *meta_backend,
+                             EDataCalView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (E_IS_DATA_CAL_VIEW (view), NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       cancellable = g_cancellable_new ();
+       g_hash_table_insert (meta_backend->priv->view_cancellables, view, g_object_ref (cancellable));
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return cancellable;
+}
+
+static GCancellable *
+ecmb_steal_view_cancellable (ECalMetaBackend *meta_backend,
+                            EDataCalView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (E_IS_DATA_CAL_VIEW (view), NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       cancellable = g_hash_table_lookup (meta_backend->priv->view_cancellables, view);
+       if (cancellable) {
+               g_object_ref (cancellable);
+               g_hash_table_remove (meta_backend->priv->view_cancellables, view);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return cancellable;
+}
+
+static void
+ecmb_update_connection_values (ECalMetaBackend *meta_backend)
+{
+       ESource *source;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       meta_backend->priv->authentication_port = 0;
+       g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+       g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+       if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *auth_extension;
+
+               auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+               meta_backend->priv->authentication_port = e_source_authentication_get_port (auth_extension);
+               meta_backend->priv->authentication_host = e_source_authentication_dup_host (auth_extension);
+               meta_backend->priv->authentication_user = e_source_authentication_dup_user (auth_extension);
+               meta_backend->priv->authentication_method = e_source_authentication_dup_method 
(auth_extension);
+               meta_backend->priv->authentication_proxy_uid = e_source_authentication_dup_proxy_uid 
(auth_extension);
+               meta_backend->priv->authentication_credential_name = 
e_source_authentication_dup_credential_name (auth_extension);
+       }
+
+       if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               ESourceWebdav *webdav_extension;
+
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+
+               meta_backend->priv->webdav_soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_cal_meta_backend_set_ever_connected (meta_backend, TRUE);
+       e_cal_meta_backend_set_connected_writable (meta_backend, e_cal_backend_get_writable (E_CAL_BACKEND 
(meta_backend)));
+}
+
+static gboolean
+ecmb_connect_wrapper_sync (ECalMetaBackend *meta_backend,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       ENamedParameters *credentials;
+       ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+       ESourceCredentialsReason creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+       gchar *certificate_pem = NULL;
+       GTlsCertificateFlags certificate_errors = 0;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+                       e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+               return FALSE;
+       }
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+       credentials = e_named_parameters_new_clone (meta_backend->priv->last_credentials);
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       g_mutex_lock (&meta_backend->priv->connect_lock);
+
+       if (e_cal_meta_backend_connect_sync (meta_backend, credentials, &auth_result, &certificate_pem, 
&certificate_errors,
+               cancellable, &local_error)) {
+               ecmb_update_connection_values (meta_backend);
+               g_mutex_unlock (&meta_backend->priv->connect_lock);
+               e_named_parameters_free (credentials);
+
+               return TRUE;
+       }
+
+       g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+       e_named_parameters_free (credentials);
+
+       g_warn_if_fail (auth_result != E_SOURCE_AUTHENTICATION_ACCEPTED);
+
+       switch (auth_result) {
+       case E_SOURCE_AUTHENTICATION_UNKNOWN:
+               if (local_error)
+                       g_propagate_error (error, local_error);
+               g_free (certificate_pem);
+               return FALSE;
+       case E_SOURCE_AUTHENTICATION_ERROR:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+               break;
+       case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+               break;
+       case E_SOURCE_AUTHENTICATION_ACCEPTED:
+               g_warn_if_reached ();
+               break;
+       case E_SOURCE_AUTHENTICATION_REJECTED:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_REJECTED;
+               break;
+       case E_SOURCE_AUTHENTICATION_REQUIRED:
+               creds_reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+               break;
+       }
+
+       e_backend_schedule_credentials_required (E_BACKEND (meta_backend), creds_reason, certificate_pem, 
certificate_errors,
+               local_error, cancellable, G_STRFUNC);
+
+       g_clear_error (&local_error);
+       g_free (certificate_pem);
+
+       return FALSE;
+}
+
+static gboolean
+ecmb_gather_locally_cached_objects_cb (ECalCache *cal_cache,
+                                      const gchar *uid,
+                                      const gchar *rid,
+                                      const gchar *revision,
+                                      const gchar *object,
+                                      const gchar *extra,
+                                      EOfflineState offline_state,
+                                      gpointer user_data)
+{
+       GHashTable *locally_cached = user_data;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (locally_cached != NULL, FALSE);
+
+       if (offline_state == E_OFFLINE_STATE_SYNCED) {
+               g_hash_table_insert (locally_cached,
+                       e_cal_component_id_new (uid, rid),
+                       g_strdup (revision));
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ecmb_get_changes_sync (ECalMetaBackend *meta_backend,
+                      const gchar *last_sync_tag,
+                      gboolean is_repeat,
+                      gchar **out_new_sync_tag,
+                      gboolean *out_repeat,
+                      GSList **out_created_objects,
+                      GSList **out_modified_objects,
+                      GSList **out_removed_objects,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       GHashTable *locally_cached; /* ECalComponentId * ~> gchar *revision */
+       GHashTableIter iter;
+       GSList *existing_objects = NULL, *link;
+       ECalCache *cal_cache;
+       gpointer key, value;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_created_objects, FALSE);
+       g_return_val_if_fail (out_modified_objects, FALSE);
+       g_return_val_if_fail (out_removed_objects, FALSE);
+
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend)))
+               return TRUE;
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+       if (!ecmb_connect_wrapper_sync (meta_backend, cancellable, error) ||
+           !e_cal_meta_backend_list_existing_sync (meta_backend, out_new_sync_tag, &existing_objects, 
cancellable, error)) {
+               g_object_unref (cal_cache);
+               return FALSE;
+       }
+
+       locally_cached = g_hash_table_new_full (
+               (GHashFunc) e_cal_component_id_hash,
+               (GEqualFunc) e_cal_component_id_equal,
+               (GDestroyNotify) e_cal_component_free_id,
+               g_free);
+
+       g_warn_if_fail (e_cal_cache_search_with_callback (cal_cache, NULL,
+               ecmb_gather_locally_cached_objects_cb, locally_cached, cancellable, error));
+
+       for (link = existing_objects; link; link = g_slist_next (link)) {
+               ECalMetaBackendInfo *nfo = link->data;
+               ECalComponentId id;
+
+               if (!nfo)
+                       continue;
+
+               id.uid = nfo->uid;
+               id.rid = NULL;
+
+               if (!g_hash_table_contains (locally_cached, &id)) {
+                       link->data = NULL;
+
+                       *out_created_objects = g_slist_prepend (*out_created_objects, nfo);
+               } else {
+                       const gchar *local_revision = g_hash_table_lookup (locally_cached, &id);
+
+                       if (g_strcmp0 (local_revision, nfo->revision) != 0) {
+                               link->data = NULL;
+
+                               *out_modified_objects = g_slist_prepend (*out_modified_objects, nfo);
+                       }
+
+                       g_hash_table_remove (locally_cached, &id);
+               }
+       }
+
+       /* What left in the hash table is removed from the remote side */
+       g_hash_table_iter_init (&iter, locally_cached);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               const ECalComponentId *id = key;
+               const gchar *revision = value;
+               ECalMetaBackendInfo *nfo;
+
+               if (!id) {
+                       g_warn_if_reached ();
+                       continue;
+               }
+
+               /* Skit detached instances, if the master object is still in the cache */
+               if (id->rid && *id->rid) {
+                       ECalComponentId master_id;
+
+                       master_id.uid = id->uid;
+                       master_id.rid = NULL;
+
+                       if (!g_hash_table_contains (locally_cached, &master_id))
+                               continue;
+               }
+
+               nfo = e_cal_meta_backend_info_new (id->uid, revision, NULL, NULL);
+               *out_removed_objects = g_slist_prepend (*out_removed_objects, nfo);
+       }
+
+       g_slist_free_full (existing_objects, e_cal_meta_backend_info_free);
+       g_hash_table_destroy (locally_cached);
+       g_object_unref (cal_cache);
+
+       *out_created_objects = g_slist_reverse (*out_created_objects);
+       *out_modified_objects = g_slist_reverse (*out_modified_objects);
+       *out_removed_objects = g_slist_reverse (*out_removed_objects);
+
+       return TRUE;
+}
+
+static gboolean
+ecmb_search_sync (ECalMetaBackend *meta_backend,
+                 const gchar *expr,
+                 GSList **out_icalstrings,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       ECalCache *cal_cache;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+       *out_icalstrings = NULL;
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+       success = e_cal_cache_search (cal_cache, expr, out_icalstrings, cancellable, error);
+
+       if (success) {
+               GSList *link;
+
+               for (link = *out_icalstrings; link; link = g_slist_next (link)) {
+                       ECalCacheSearchData *search_data = link->data;
+                       gchar *icalstring = NULL;
+
+                       if (search_data) {
+                               icalstring = g_strdup (search_data->object);
+                               e_cal_cache_search_data_free (search_data);
+                       }
+
+                       link->data = icalstring;
+               }
+       }
+
+       g_object_unref (cal_cache);
+
+       return success;
+}
+
+static gboolean
+ecmb_search_components_sync (ECalMetaBackend *meta_backend,
+                            const gchar *expr,
+                            GSList **out_components,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       ECalCache *cal_cache;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_components != NULL, FALSE);
+
+       *out_components = NULL;
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+       success = e_cal_cache_search_components (cal_cache, expr, out_components, cancellable, error);
+
+       g_object_unref (cal_cache);
+
+       return success;
+}
+
+static gboolean
+ecmb_requires_reconnect (ECalMetaBackend *meta_backend)
+{
+       ESource *source;
+       gboolean requires = FALSE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+       g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *auth_extension;
+
+               auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+               e_source_extension_property_lock (E_SOURCE_EXTENSION (auth_extension));
+
+               requires = meta_backend->priv->authentication_port != e_source_authentication_get_port 
(auth_extension) ||
+                       g_strcmp0 (meta_backend->priv->authentication_host, e_source_authentication_get_host 
(auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_user, e_source_authentication_get_user 
(auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_method, 
e_source_authentication_get_method (auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_proxy_uid, 
e_source_authentication_get_proxy_uid (auth_extension)) != 0 ||
+                       g_strcmp0 (meta_backend->priv->authentication_credential_name, 
e_source_authentication_get_credential_name (auth_extension)) != 0;
+
+               e_source_extension_property_unlock (E_SOURCE_EXTENSION (auth_extension));
+       }
+
+       if (!requires && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               ESourceWebdav *webdav_extension;
+               SoupURI *soup_uri;
+
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+
+               requires = (!meta_backend->priv->webdav_soup_uri && soup_uri) ||
+                       (soup_uri && meta_backend->priv->webdav_soup_uri &&
+                       !soup_uri_equal (meta_backend->priv->webdav_soup_uri, soup_uri));
+
+               if (soup_uri)
+                       soup_uri_free (soup_uri);
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return requires;
+}
+
+static void
+ecmb_start_view_thread_func (ECalBackend *cal_backend,
+                            gpointer user_data,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       EDataCalView *view = user_data;
+       ECalBackendSExp *sexp;
+       GSList *components = NULL;
+       const gchar *expr = NULL;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+       g_return_if_fail (E_IS_DATA_CAL_VIEW (view));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       /* Fill the view with known (locally stored) components satisfying the expression */
+       sexp = e_data_cal_view_get_sexp (view);
+       if (sexp)
+               expr = e_cal_backend_sexp_text (sexp);
+
+       if (e_cal_meta_backend_search_components_sync (E_CAL_META_BACKEND (cal_backend), expr, &components, 
cancellable, &local_error) && components) {
+               if (!g_cancellable_is_cancelled (cancellable))
+                       e_data_cal_view_notify_components_added (view, components);
+
+               g_slist_free_full (components, g_object_unref);
+       }
+
+       e_data_cal_view_notify_complete (view, local_error);
+
+       g_clear_error (&local_error);
+}
+
+static gboolean
+ecmb_upload_local_changes_sync (ECalMetaBackend *meta_backend,
+                               ECalCache *cal_cache,
+                               EConflictResolution conflict_resolution,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       GSList *offline_changes, *link;
+       GHashTable *covered_uids;
+       ECache *cache;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       cache = E_CACHE (cal_cache);
+       covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+       offline_changes = e_cal_cache_get_offline_changes (cal_cache, cancellable, error);
+       for (link = offline_changes; link && success; link = g_slist_next (link)) {
+               ECalCacheOfflineChange *change = link->data;
+               gchar *extra = NULL;
+
+               success = !g_cancellable_set_error_if_cancelled (cancellable, error);
+               if (!success)
+                       break;
+
+               if (!change || g_hash_table_contains (covered_uids, change->uid))
+                       continue;
+
+               g_hash_table_insert (covered_uids, change->uid, NULL);
+
+               if (!e_cal_cache_get_component_extra (cal_cache, change->uid, NULL, &extra, cancellable, 
NULL))
+                       extra = NULL;
+
+               if (change->state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+                   change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED) {
+                       GSList *instances = NULL;
+
+                       success = e_cal_cache_get_components_by_uid (cal_cache, change->uid, &instances, 
cancellable, error);
+                       if (success) {
+                               success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache,
+                                       change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED,
+                                       conflict_resolution, instances, extra, change->uid, NULL, NULL, NULL, 
cancellable, error);
+                       }
+
+                       g_slist_free_full (instances, g_object_unref);
+               } else if (change->state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+                       GError *local_error = NULL;
+
+                       success = e_cal_meta_backend_remove_component_sync (meta_backend, conflict_resolution,
+                               change->uid, extra, change->object, cancellable, &local_error);
+
+                       if (!success) {
+                               if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+               } else {
+                       g_warn_if_reached ();
+               }
+
+               g_free (extra);
+       }
+
+       g_slist_free_full (offline_changes, e_cal_cache_offline_change_free);
+       g_hash_table_destroy (covered_uids);
+
+       if (success)
+               success = e_cache_clear_offline_changes (cache, cancellable, error);
+
+       return success;
+}
+
+static gboolean
+ecmb_maybe_remove_from_cache (ECalMetaBackend *meta_backend,
+                             ECalCache *cal_cache,
+                             ECacheOfflineFlag offline_flag,
+                             const gchar *uid,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       ECalBackend *cal_backend;
+       GSList *comps = NULL, *link;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (!e_cal_cache_get_components_by_uid (cal_cache, uid, &comps, cancellable, &local_error)) {
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       return TRUE;
+               }
+
+               g_propagate_error (error, local_error);
+               return FALSE;
+       }
+
+       cal_backend = E_CAL_BACKEND (meta_backend);
+
+       for (link = comps; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+               ECalComponentId *id;
+
+               g_warn_if_fail (E_IS_CAL_COMPONENT (comp));
+
+               if (!E_IS_CAL_COMPONENT (comp))
+                       continue;
+
+               id = e_cal_component_get_id (comp);
+               if (id) {
+                       if (!e_cal_cache_delete_attachments (cal_cache, e_cal_component_get_icalcomponent 
(comp), cancellable, error) ||
+                           !e_cal_cache_remove_component (cal_cache, id->uid, id->rid, offline_flag, 
cancellable, error)) {
+                               e_cal_component_free_id (id);
+                               g_slist_free_full (comps, g_object_unref);
+
+                               return FALSE;
+                       }
+
+                       e_cal_backend_notify_component_removed (cal_backend, id, comp, NULL);
+                       e_cal_component_free_id (id);
+               }
+       }
+
+       g_slist_free_full (comps, g_object_unref);
+
+       return TRUE;
+}
+
+static void
+ecmb_refresh_thread_func (ECalBackend *cal_backend,
+                         gpointer user_data,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       gboolean success, repeat = TRUE, is_repeat = FALSE;
+       GString *invalid_objects = NULL;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               goto done;
+
+       meta_backend = E_CAL_META_BACKEND (cal_backend);
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend)) ||
+           !ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+               /* Ignore connection errors here */
+               g_mutex_lock (&meta_backend->priv->property_lock);
+               meta_backend->priv->refresh_after_authenticate = TRUE;
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               goto done;
+       }
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+       meta_backend->priv->refresh_after_authenticate = FALSE;
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       if (!cal_cache) {
+               g_warn_if_reached ();
+               goto done;
+       }
+
+       success = ecmb_upload_local_changes_sync (meta_backend, cal_cache, E_CONFLICT_RESOLUTION_FAIL, 
cancellable, error);
+
+       while (repeat && success &&
+              !g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               GSList *created_objects = NULL, *modified_objects = NULL, *removed_objects = NULL, *link;
+               gchar *last_sync_tag, *new_sync_tag = NULL;
+
+               repeat = FALSE;
+
+               last_sync_tag = e_cache_dup_key (E_CACHE (cal_cache), ECMB_KEY_SYNC_TAG, NULL);
+               if (last_sync_tag && !*last_sync_tag) {
+                       g_free (last_sync_tag);
+                       last_sync_tag = NULL;
+               }
+
+               success = e_cal_meta_backend_get_changes_sync (meta_backend, last_sync_tag, is_repeat, 
&new_sync_tag, &repeat,
+                       &created_objects, &modified_objects, &removed_objects, cancellable, error);
+
+               if (success) {
+                       GHashTable *covered_uids;
+
+                       covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+                       /* Removed objects first */
+                       for (link = removed_objects; link && success; link = g_slist_next (link)) {
+                               ECalMetaBackendInfo *nfo = link->data;
+
+                               if (!nfo) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache, 
E_CACHE_IS_ONLINE, nfo->uid, cancellable, error);
+                       }
+
+                       /* Then modified objects */
+                       for (link = modified_objects; link && success; link = g_slist_next (link)) {
+                               ECalMetaBackendInfo *nfo = link->data;
+                               GError *local_error = NULL;
+
+                               if (!nfo || !nfo->uid) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               if (!*nfo->uid ||
+                                   g_hash_table_contains (covered_uids, nfo->uid))
+                                       continue;
+
+                               g_hash_table_insert (covered_uids, nfo->uid, NULL);
+
+                               success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, 
nfo->uid, nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+                               /* Do not stop on invalid objects, just notify about them later, and load as 
many as possible */
+                               if (!success && g_error_matches (local_error, E_DATA_CAL_ERROR, 
InvalidObject)) {
+                                       if (!invalid_objects) {
+                                               invalid_objects = g_string_new (local_error->message);
+                                       } else {
+                                               g_string_append_c (invalid_objects, '\n');
+                                               g_string_append (invalid_objects, local_error->message);
+                                       }
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+
+                       g_hash_table_remove_all (covered_uids);
+
+                       /* Finally created objects */
+                       for (link = created_objects; link && success; link = g_slist_next (link)) {
+                               ECalMetaBackendInfo *nfo = link->data;
+                               GError *local_error = NULL;
+
+                               if (!nfo || !nfo->uid) {
+                                       g_warn_if_reached ();
+                                       continue;
+                               }
+
+                               if (!*nfo->uid)
+                                       continue;
+
+                               success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, 
nfo->uid, nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+                               /* Do not stop on invalid objects, just notify about them later, and load as 
many as possible */
+                               if (!success && g_error_matches (local_error, E_DATA_CAL_ERROR, 
InvalidObject)) {
+                                       if (!invalid_objects) {
+                                               invalid_objects = g_string_new (local_error->message);
+                                       } else {
+                                               g_string_append_c (invalid_objects, '\n');
+                                               g_string_append (invalid_objects, local_error->message);
+                                       }
+                                       g_clear_error (&local_error);
+                                       success = TRUE;
+                               } else if (local_error) {
+                                       g_propagate_error (error, local_error);
+                               }
+                       }
+
+                       g_hash_table_destroy (covered_uids);
+               }
+
+               if (success && new_sync_tag)
+                       e_cache_set_key (E_CACHE (cal_cache), ECMB_KEY_SYNC_TAG, new_sync_tag, NULL);
+
+               g_slist_free_full (created_objects, e_cal_meta_backend_info_free);
+               g_slist_free_full (modified_objects, e_cal_meta_backend_info_free);
+               g_slist_free_full (removed_objects, e_cal_meta_backend_info_free);
+               g_free (last_sync_tag);
+               g_free (new_sync_tag);
+
+               is_repeat = TRUE;
+       }
+
+       g_object_unref (cal_cache);
+
+ done:
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->refresh_cancellable == cancellable)
+               g_clear_object (&meta_backend->priv->refresh_cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       if (invalid_objects) {
+               e_cal_backend_notify_error (E_CAL_BACKEND (meta_backend), invalid_objects->str);
+
+               g_string_free (invalid_objects, TRUE);
+       }
+
+       g_signal_emit (meta_backend, signals[REFRESH_COMPLETED], 0, NULL);
+}
+
+static void
+ecmb_source_refresh_timeout_cb (ESource *source,
+                               gpointer user_data)
+{
+       GWeakRef *weak_ref = user_data;
+       ECalMetaBackend *meta_backend;
+
+       g_return_if_fail (weak_ref != NULL);
+
+       meta_backend = g_weak_ref_get (weak_ref);
+       if (meta_backend) {
+               ecmb_schedule_refresh (meta_backend);
+               g_object_unref (meta_backend);
+       }
+}
+
+static void
+ecmb_source_changed_thread_func (ECalBackend *cal_backend,
+                                gpointer user_data,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       ECalMetaBackend *meta_backend;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       meta_backend = E_CAL_META_BACKEND (cal_backend);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+       if (!meta_backend->priv->refresh_timeout_id) {
+               ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+               if (e_source_has_extension (source, E_SOURCE_EXTENSION_REFRESH)) {
+                       meta_backend->priv->refresh_timeout_id = e_source_refresh_add_timeout (source, NULL,
+                               ecmb_source_refresh_timeout_cb, e_weak_ref_new (meta_backend), 
(GDestroyNotify) e_weak_ref_free);
+               }
+       }
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       g_signal_emit (meta_backend, signals[SOURCE_CHANGED], 0, NULL);
+
+       if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+           e_cal_meta_backend_requires_reconnect (meta_backend)) {
+               gboolean can_refresh;
+
+               g_mutex_lock (&meta_backend->priv->connect_lock);
+               can_refresh = e_cal_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+               g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+               if (can_refresh)
+                       ecmb_schedule_refresh (meta_backend);
+       }
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->source_changed_cancellable == cancellable)
+               g_clear_object (&meta_backend->priv->source_changed_cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static void
+ecmb_go_offline_thread_func (ECalBackend *cal_backend,
+                            gpointer user_data,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       ECalMetaBackend *meta_backend;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       meta_backend = E_CAL_META_BACKEND (cal_backend);
+
+       g_mutex_lock (&meta_backend->priv->connect_lock);
+       e_cal_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+       g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->go_offline_cancellable == cancellable)
+               g_clear_object (&meta_backend->priv->go_offline_cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static ECalComponent *
+ecmb_find_in_instances (const GSList *instances, /* ECalComponent * */
+                       const gchar *uid,
+                       const gchar *rid)
+{
+       GSList *link;
+
+       for (link = (GSList *) instances; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+               ECalComponentId *id;
+
+               if (!comp)
+                       continue;
+
+               id = e_cal_component_get_id (comp);
+               if (!id)
+                       continue;
+
+               if (g_strcmp0 (id->uid, uid) == 0 &&
+                   g_strcmp0 (id->rid, rid) == 0) {
+                       e_cal_component_free_id (id);
+                       return comp;
+               }
+
+               e_cal_component_free_id (id);
+       }
+
+       return NULL;
+}
+
+static gboolean
+ecmb_put_one_component (ECalMetaBackend *meta_backend,
+                       ECalCache *cal_cache,
+                       ECacheOfflineFlag offline_flag,
+                       ECalComponent *comp,
+                       const gchar *extra,
+                       GSList **inout_cache_instances,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (comp != NULL, FALSE);
+       g_return_val_if_fail (inout_cache_instances != NULL, FALSE);
+
+       if (e_cal_component_has_attachments (comp)) {
+               success = e_cal_meta_backend_store_inline_attachments_sync (meta_backend,
+                       e_cal_component_get_icalcomponent (comp), cancellable, error);
+               e_cal_component_rescan (comp);
+       }
+
+       success = success && e_cal_cache_put_component (cal_cache, comp, extra, offline_flag, cancellable, 
error);
+
+       if (success) {
+               ECalComponent *existing = NULL;
+               ECalComponentId *id;
+
+               id = e_cal_component_get_id (comp);
+               if (id) {
+                       existing = ecmb_find_in_instances (*inout_cache_instances, id->uid, id->rid);
+
+                       e_cal_component_free_id (id);
+               }
+
+               if (existing) {
+                       e_cal_backend_notify_component_modified (E_CAL_BACKEND (meta_backend), existing, 
comp);
+                       *inout_cache_instances = g_slist_remove (*inout_cache_instances, existing);
+
+                       g_clear_object (&existing);
+               } else {
+                       e_cal_backend_notify_component_created (E_CAL_BACKEND (meta_backend), comp);
+               }
+       }
+
+       return success;
+}
+
+static gboolean
+ecmb_put_instances (ECalMetaBackend *meta_backend,
+                   ECalCache *cal_cache,
+                   const gchar *uid,
+                   ECacheOfflineFlag offline_flag,
+                   const GSList *new_instances, /* ECalComponent * */
+                   const gchar *extra,
+                   GCancellable *cancellable,
+                   GError **error)
+{
+       GSList *cache_instances = NULL, *link;
+       gboolean success = TRUE;
+       GError *local_error = NULL;
+
+       if (!e_cal_cache_get_components_by_uid (cal_cache, uid, &cache_instances, cancellable, &local_error)) 
{
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+               } else {
+                       g_propagate_error (error, local_error);
+
+                       return FALSE;
+               }
+       }
+
+       for (link = (GSList *) new_instances; link && success; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+
+               success = ecmb_put_one_component (meta_backend, cal_cache, offline_flag, comp, extra, 
&cache_instances, cancellable, error);
+       }
+
+       /* What left got removed from the remote side, notify about it */
+       if (success && cache_instances) {
+               ECalBackend *cal_backend = E_CAL_BACKEND (meta_backend);
+               GSList *link;
+
+               for (link = cache_instances; link && success; link = g_slist_next (link)) {
+                       ECalComponent *comp = link->data;
+                       ECalComponentId *id;
+
+                       id = e_cal_component_get_id (comp);
+                       if (!id)
+                               continue;
+
+                       success = e_cal_cache_delete_attachments (cal_cache, 
e_cal_component_get_icalcomponent (comp), cancellable, error);
+                       if (!success)
+                               break;
+
+                       success = e_cal_cache_remove_component (cal_cache, id->uid, id->rid, offline_flag, 
cancellable, error);
+
+                       e_cal_backend_notify_component_removed (cal_backend, id, comp, NULL);
+
+                       e_cal_component_free_id (id);
+               }
+       }
+
+       g_slist_free_full (cache_instances, g_object_unref);
+
+       return success;
+}
+
+static void
+ecmb_gather_timezones (ECalMetaBackend *meta_backend,
+                      ETimezoneCache *timezone_cache,
+                      icalcomponent *icalcomp)
+{
+       icalcomponent *subcomp;
+       icaltimezone *zone;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+       g_return_if_fail (E_IS_TIMEZONE_CACHE (timezone_cache));
+       g_return_if_fail (icalcomp != NULL);
+
+       zone = icaltimezone_new ();
+
+       for (subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+            subcomp;
+            subcomp = icalcomponent_get_next_component (icalcomp, ICAL_VTIMEZONE_COMPONENT)) {
+               icalcomponent *clone;
+
+               clone = icalcomponent_new_clone (subcomp);
+
+               if (icaltimezone_set_component (zone, clone)) {
+                       e_timezone_cache_add_timezone (timezone_cache, zone);
+               } else {
+                       icalcomponent_free (clone);
+               }
+       }
+
+       icaltimezone_free (zone, TRUE);
+}
+
+static gboolean
+ecmb_load_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                 ECalCache *cal_cache,
+                                 const gchar *uid,
+                                 const gchar *preloaded_object,
+                                 const gchar *preloaded_extra,
+                                 gchar **out_new_uid,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       ECacheOfflineFlag offline_flag = E_CACHE_IS_ONLINE;
+       icalcomponent *icalcomp = NULL;
+       GSList *new_instances = NULL;
+       gchar *extra = NULL;
+       const gchar *loaded_uid = NULL;
+       gboolean success = TRUE;
+
+       if (preloaded_object && *preloaded_object) {
+               icalcomp = icalcomponent_new_from_string (preloaded_object);
+               if (!icalcomp) {
+                       g_propagate_error (error, e_data_cal_create_error_fmt (InvalidObject, _("Preloaded 
object for UID “%s” is invalid"), uid));
+                       return FALSE;
+               }
+       } else if (!e_cal_meta_backend_load_component_sync (meta_backend, uid, preloaded_extra, &icalcomp, 
&extra, cancellable, error)) {
+               g_free (extra);
+               return FALSE;
+       } else if (!icalcomp) {
+               g_propagate_error (error, e_data_cal_create_error_fmt (InvalidObject, _("Received object for 
UID “%s” is invalid"), uid));
+               g_free (extra);
+               return FALSE;
+       }
+
+       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
+               icalcomponent_kind kind;
+               icalcomponent *subcomp;
+
+               ecmb_gather_timezones (meta_backend, E_TIMEZONE_CACHE (cal_cache), icalcomp);
+
+               kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+               for (subcomp = icalcomponent_get_first_component (icalcomp, kind);
+                    subcomp && success;
+                    subcomp = icalcomponent_get_next_component (icalcomp, kind)) {
+                       ECalComponent *comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone 
(subcomp));
+
+                       if (comp) {
+                               new_instances = g_slist_prepend (new_instances, comp);
+
+                               if (!loaded_uid)
+                                       loaded_uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent 
(comp));
+                       }
+               }
+       } else {
+               ECalComponent *comp = e_cal_component_new_from_icalcomponent (icalcomp);
+
+               icalcomp = NULL;
+
+               if (comp) {
+                       new_instances = g_slist_prepend (new_instances, comp);
+
+                       if (!loaded_uid)
+                               loaded_uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (comp));
+               }
+       }
+
+       if (new_instances) {
+               new_instances = g_slist_reverse (new_instances);
+
+               success = ecmb_put_instances (meta_backend, cal_cache, loaded_uid ? loaded_uid : uid, 
offline_flag,
+                       new_instances, extra ? extra : preloaded_extra, cancellable, error);
+
+               if (success && out_new_uid)
+                       *out_new_uid = g_strdup (loaded_uid ? loaded_uid : uid);
+       } else {
+               g_propagate_error (error, e_data_cal_create_error_fmt (InvalidObject, _("Received object for 
UID “%s” doesn't contain any expected component"), uid));
+               success = FALSE;
+       }
+
+       g_slist_free_full (new_instances, g_object_unref);
+       if (icalcomp)
+               icalcomponent_free (icalcomp);
+       g_free (extra);
+
+       return success;
+}
+
+static gboolean
+ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
+                                 ECalCache *cal_cache,
+                                 gboolean overwrite_existing,
+                                 EConflictResolution conflict_resolution,
+                                 const GSList *in_instances,
+                                 const gchar *extra,
+                                 const gchar *orig_uid,
+                                 gboolean *out_requires_put,
+                                 gchar **out_new_uid,
+                                 gchar **out_new_extra,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       GSList *link, *instances = NULL;
+       gchar *new_uid = NULL, *new_extra = NULL;
+       gboolean has_attachments = FALSE, success = TRUE;
+
+       if (out_requires_put)
+               *out_requires_put = TRUE;
+
+       if (out_new_uid)
+               *out_new_uid = NULL;
+
+       for (link = (GSList *) in_instances; link && !has_attachments; link = g_slist_next (link)) {
+               has_attachments = e_cal_component_has_attachments (link->data);
+       }
+
+       if (has_attachments) {
+               instances = g_slist_copy ((GSList *) in_instances);
+
+               for (link = instances; link; link = g_slist_next (link)) {
+                       ECalComponent *comp = link->data;
+
+                       if (success && e_cal_component_has_attachments (comp)) {
+                               comp = e_cal_component_clone (comp);
+                               link->data = comp;
+
+                               success = e_cal_meta_backend_inline_local_attachments_sync (meta_backend,
+                                       e_cal_component_get_icalcomponent (comp), cancellable, error);
+                               e_cal_component_rescan (comp);
+                       } else {
+                               g_object_ref (comp);
+                       }
+               }
+       }
+
+       success = success && e_cal_meta_backend_save_component_sync (meta_backend, overwrite_existing, 
conflict_resolution,
+               instances ? instances : in_instances, extra, &new_uid, &new_extra, cancellable, error);
+
+       if (success && new_uid && *new_uid) {
+               gchar *loaded_uid = NULL;
+
+               success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, new_uid, NULL,
+                       new_extra ? new_extra : extra, &loaded_uid, cancellable, error);
+
+               if (success && g_strcmp0 (loaded_uid, orig_uid) != 0)
+                       success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache, E_CACHE_IS_ONLINE, 
orig_uid, cancellable, error);
+
+               if (success && out_new_uid)
+                       *out_new_uid = loaded_uid;
+               else
+                       g_free (loaded_uid);
+
+               if (out_requires_put)
+                       *out_requires_put = FALSE;
+       }
+
+       g_free (new_uid);
+
+       if (success && out_new_extra)
+               *out_new_extra = new_extra;
+       else
+               g_free (new_extra);
+
+       g_slist_free_full (instances, g_object_unref);
+
+       return success;
+}
+
+static void
+ecmb_open_sync (ECalBackendSync *sync_backend,
+               EDataCal *cal,
+               GCancellable *cancellable,
+               gboolean only_if_exists,
+               GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ESource *source;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+
+       if (e_cal_backend_is_opened (E_CAL_BACKEND (sync_backend)))
+               return;
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       if (meta_backend->priv->create_cache_error) {
+               g_propagate_error (error, meta_backend->priv->create_cache_error);
+               meta_backend->priv->create_cache_error = NULL;
+               return;
+       }
+
+       source = e_backend_get_source (E_BACKEND (sync_backend));
+
+       if (!meta_backend->priv->source_changed_id) {
+               meta_backend->priv->source_changed_id = g_signal_connect_swapped (source, "changed",
+                       G_CALLBACK (ecmb_schedule_source_changed), meta_backend);
+       }
+
+       if (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               ESourceWebdav *webdav_extension;
+
+               webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
+       }
+
+       if (e_cal_meta_backend_get_ever_connected (meta_backend)) {
+               e_cal_backend_set_writable (E_CAL_BACKEND (meta_backend),
+                       e_cal_meta_backend_get_connected_writable (meta_backend));
+       } else {
+               if (!ecmb_connect_wrapper_sync (meta_backend, cancellable, error)) {
+                       g_mutex_lock (&meta_backend->priv->property_lock);
+                       meta_backend->priv->refresh_after_authenticate = TRUE;
+                       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+                       return;
+               }
+       }
+
+       ecmb_schedule_refresh (E_CAL_META_BACKEND (sync_backend));
+}
+
+static void
+ecmb_refresh_sync (ECalBackendSync *sync_backend,
+                  EDataCal *cal,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+       ECalMetaBackend *meta_backend;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+
+       if (!e_backend_get_online (E_BACKEND (sync_backend)))
+               return;
+
+       if (ecmb_connect_wrapper_sync (meta_backend, cancellable, error))
+               ecmb_schedule_refresh (meta_backend);
+}
+
+static void
+ecmb_get_object_sync (ECalBackendSync *sync_backend,
+                     EDataCal *cal,
+                     GCancellable *cancellable,
+                     const gchar *uid,
+                     const gchar *rid,
+                     gchar **calobj,
+                     GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (uid && *uid);
+       g_return_if_fail (calobj != NULL);
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+
+       g_return_if_fail (cal_cache != NULL);
+
+       if (!e_cal_cache_get_component_as_string (cal_cache, uid, rid, calobj, cancellable, &local_error) &&
+           g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+               gchar *loaded_uid = NULL;
+               gboolean found = FALSE;
+
+               g_clear_error (&local_error);
+
+               /* Ignore errors here, just try whether it's on the remote side, but not in the local cache */
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL) &&
+                   ecmb_load_component_wrapper_sync (meta_backend, cal_cache, uid, NULL, NULL, &loaded_uid, 
cancellable, NULL)) {
+                       found = e_cal_cache_get_component_as_string (cal_cache, loaded_uid, rid, calobj, 
cancellable, NULL);
+               }
+
+               if (!found)
+                       g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+
+               g_free (loaded_uid);
+       } else if (local_error) {
+               g_propagate_error (error, e_data_cal_create_error (OtherError, local_error->message));
+               g_clear_error (&local_error);
+       }
+
+       g_object_unref (cal_cache);
+}
+
+static void
+ecmb_get_object_list_sync (ECalBackendSync *sync_backend,
+                          EDataCal *cal,
+                          GCancellable *cancellable,
+                          const gchar *sexp,
+                          GSList **calobjs,
+                          GError **error)
+{
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (calobjs != NULL);
+
+       *calobjs = NULL;
+
+       e_cal_meta_backend_search_sync (E_CAL_META_BACKEND (sync_backend), sexp, calobjs, cancellable, error);
+}
+
+static gboolean
+ecmb_add_free_busy_instance_cb (icalcomponent *icalcomp,
+                               struct icaltimetype instance_start,
+                               struct icaltimetype instance_end,
+                               gpointer user_data,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       icalcomponent *vfreebusy = user_data;
+       icalproperty *prop, *classification;
+       icalparameter *param;
+       struct icalperiodtype ipt;
+
+       ipt.start = instance_start;
+       ipt.end = instance_end;
+       ipt.duration = icaldurationtype_null_duration ();
+
+        /* Add busy information to the VFREEBUSY component */
+       prop = icalproperty_new (ICAL_FREEBUSY_PROPERTY);
+       icalproperty_set_freebusy (prop, ipt);
+
+       param = icalparameter_new_fbtype (ICAL_FBTYPE_BUSY);
+       icalproperty_add_parameter (prop, param);
+
+       classification = icalcomponent_get_first_property (icalcomp, ICAL_CLASS_PROPERTY);
+       if (!classification || icalproperty_get_class (classification) == ICAL_CLASS_PUBLIC) {
+               const gchar *str;
+
+               str = icalcomponent_get_summary (icalcomp);
+               if (str && *str) {
+                       param = icalparameter_new_x (str);
+                       icalparameter_set_xname (param, "X-SUMMARY");
+                       icalproperty_add_parameter (prop, param);
+               }
+
+               str = icalcomponent_get_location (icalcomp);
+               if (str && *str) {
+                       param = icalparameter_new_x (str);
+                       icalparameter_set_xname (param, "X-LOCATION");
+                       icalproperty_add_parameter (prop, param);
+               }
+       }
+
+       icalcomponent_add_property (vfreebusy, prop);
+
+       return TRUE;
+}
+
+static void
+ecmb_get_free_busy_sync (ECalBackendSync *sync_backend,
+                        EDataCal *cal,
+                        GCancellable *cancellable,
+                        const GSList *users,
+                        time_t start,
+                        time_t end,
+                        GSList **out_freebusy,
+                        GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       GSList *link, *components = NULL;
+       gchar *cal_email_address, *mailto;
+       icalcomponent *vfreebusy, *icalcomp;
+       icalproperty *prop;
+       icaltimezone *utc_zone;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (out_freebusy != NULL);
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+
+       *out_freebusy = NULL;
+
+       if (!users)
+               return;
+
+       cal_email_address = e_cal_backend_get_backend_property (E_CAL_BACKEND (meta_backend), 
CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS);
+       if (!cal_email_address)
+               return;
+
+       for (link = (GSList *) users; link; link = g_slist_next (link)) {
+               const gchar *user = link->data;
+
+               if (user && g_ascii_strcasecmp (user, cal_email_address) == 0)
+                       break;
+       }
+
+       if (!link) {
+               g_free (cal_email_address);
+               return;
+       }
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       if (!cal_cache) {
+               g_warn_if_reached ();
+               g_free (cal_email_address);
+               return;
+       }
+
+       if (!e_cal_cache_get_components_in_range (cal_cache, start, end, &components, cancellable, error)) {
+               g_clear_object (&cal_cache);
+               g_free (cal_email_address);
+               return;
+       }
+
+       vfreebusy = icalcomponent_new_vfreebusy ();
+
+       mailto = g_strconcat ("mailto:";, cal_email_address, NULL);
+       prop = icalproperty_new_organizer (mailto);
+       g_free (mailto);
+
+       if (prop)
+               icalcomponent_add_property (vfreebusy, prop);
+
+       utc_zone = icaltimezone_get_utc_timezone ();
+       icalcomponent_set_dtstart (vfreebusy, icaltime_from_timet_with_zone (start, FALSE, utc_zone));
+       icalcomponent_set_dtend (vfreebusy, icaltime_from_timet_with_zone (end, FALSE, utc_zone));
+
+       for (link = components; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+
+               if (!E_IS_CAL_COMPONENT (comp)) {
+                       g_warn_if_reached ();
+                       continue;
+               }
+
+               icalcomp = e_cal_component_get_icalcomponent (comp);
+               if (!icalcomp)
+                       continue;
+
+               /* If the event is TRANSPARENT, skip it. */
+               prop = icalcomponent_get_first_property (icalcomp, ICAL_TRANSP_PROPERTY);
+               if (prop) {
+                       icalproperty_transp transp_val = icalproperty_get_transp (prop);
+                       if (transp_val == ICAL_TRANSP_TRANSPARENT ||
+                           transp_val == ICAL_TRANSP_TRANSPARENTNOCONFLICT)
+                               continue;
+               }
+
+               if (!e_cal_recur_generate_instances_sync (icalcomp,
+                       icaltime_from_timet_with_zone (start, FALSE, NULL),
+                       icaltime_from_timet_with_zone (end, FALSE, NULL),
+                       ecmb_add_free_busy_instance_cb, vfreebusy,
+                       e_cal_cache_resolve_timezone_cb, cal_cache,
+                       utc_zone, cancellable, error)) {
+                       break;
+               }
+       }
+
+       *out_freebusy = g_slist_prepend (*out_freebusy, icalcomponent_as_ical_string_r (vfreebusy));
+
+       g_slist_free_full (components, g_object_unref);
+       icalcomponent_free (vfreebusy);
+       g_object_unref (cal_cache);
+       g_free (cal_email_address);
+}
+
+static gboolean
+ecmb_create_object_sync (ECalMetaBackend *meta_backend,
+                        ECalCache *cal_cache,
+                        ECacheOfflineFlag *offline_flag,
+                        EConflictResolution conflict_resolution,
+                        ECalComponent *comp,
+                        gchar **out_new_uid,
+                        ECalComponent **out_new_comp,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       icalcomponent *icalcomp;
+       struct icaltimetype itt;
+       const gchar *uid;
+       gchar *new_uid = NULL, *new_extra = NULL;
+       gboolean success, requires_put = TRUE;
+
+       g_return_val_if_fail (comp != NULL, FALSE);
+
+       icalcomp = e_cal_component_get_icalcomponent (comp);
+       if (!icalcomp) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+               return FALSE;
+       }
+
+       uid = icalcomponent_get_uid (icalcomp);
+       if (!uid) {
+               gchar *new_uid;
+
+               new_uid = e_cal_component_gen_uid ();
+               if (!new_uid) {
+                       g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+                       return FALSE;
+               }
+
+               icalcomponent_set_uid (icalcomp, new_uid);
+               uid = icalcomponent_get_uid (icalcomp);
+
+               g_free (new_uid);
+       }
+
+       if (e_cal_cache_contains (cal_cache, uid, NULL, E_CACHE_EXCLUDE_DELETED)) {
+               g_propagate_error (error, e_data_cal_create_error (ObjectIdAlreadyExists, NULL));
+               return FALSE;
+       }
+
+       /* Set the created and last modified times on the component */
+       itt = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
+       e_cal_component_set_created (comp, &itt);
+       e_cal_component_set_last_modified (comp, &itt);
+
+       if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+                       *offline_flag = E_CACHE_IS_ONLINE;
+               } else {
+                       *offline_flag = E_CACHE_IS_OFFLINE;
+               }
+       }
+
+       if (*offline_flag == E_CACHE_IS_ONLINE) {
+               GSList *instances;
+
+               instances = g_slist_prepend (NULL, comp);
+
+               if (!ecmb_save_component_wrapper_sync (meta_backend, cal_cache, FALSE, conflict_resolution, 
instances, NULL, uid,
+                       &requires_put, &new_uid, &new_extra, cancellable, error)) {
+                       g_slist_free (instances);
+                       return FALSE;
+               }
+
+               g_slist_free (instances);
+       }
+
+       if (requires_put) {
+               success = e_cal_cache_put_component (cal_cache, comp, new_extra, *offline_flag, cancellable, 
error);
+               if (success && !out_new_comp) {
+                       e_cal_backend_notify_component_created (E_CAL_BACKEND (meta_backend), comp);
+               }
+       } else {
+               success = TRUE;
+       }
+
+       if (success) {
+               if (out_new_uid)
+                       *out_new_uid = g_strdup (new_uid ? new_uid : icalcomponent_get_uid 
(e_cal_component_get_icalcomponent (comp)));
+               if (out_new_comp) {
+                       if (new_uid) {
+                               if (!e_cal_cache_get_component (cal_cache, new_uid, NULL, out_new_comp, 
cancellable, NULL))
+                                       *out_new_comp = g_object_ref (comp);
+                       } else {
+                               *out_new_comp = g_object_ref (comp);
+                       }
+               }
+       }
+
+       g_free (new_uid);
+       g_free (new_extra);
+
+       return success;
+}
+
+static void
+ecmb_create_objects_sync (ECalBackendSync *sync_backend,
+                         EDataCal *cal,
+                         GCancellable *cancellable,
+                         const GSList *calobjs,
+                         GSList **out_uids,
+                         GSList **out_new_components,
+                         GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       icalcomponent_kind backend_kind;
+       GSList *link;
+       gboolean success = TRUE;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (calobjs != NULL);
+       g_return_if_fail (out_uids != NULL);
+       g_return_if_fail (out_new_components != NULL);
+
+       if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+               g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+               return;
+       }
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_if_fail (cal_cache != NULL);
+
+       backend_kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+       for (link = (GSList *) calobjs; link && success; link = g_slist_next (link)) {
+               ECalComponent *comp, *new_comp = NULL;
+               gchar *new_uid = NULL;
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error))
+                       break;
+
+               comp = e_cal_component_new_from_string (link->data);
+               if (!comp ||
+                   !e_cal_component_get_icalcomponent (comp) ||
+                   backend_kind != icalcomponent_isa (e_cal_component_get_icalcomponent (comp))) {
+                       g_clear_object (&comp);
+
+                       g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+                       break;
+               }
+
+               success = ecmb_create_object_sync (meta_backend, cal_cache, &offline_flag, 
conflict_resolution,
+                       comp, &new_uid, &new_comp, cancellable, error);
+
+               if (success) {
+                       *out_uids = g_slist_prepend (*out_uids, new_uid);
+                       *out_new_components = g_slist_prepend (*out_new_components, new_comp);
+               }
+
+               g_object_unref (comp);
+       }
+
+       *out_uids = g_slist_reverse (*out_uids);
+       *out_new_components = g_slist_reverse (*out_new_components);
+
+       g_object_unref (cal_cache);
+}
+
+static gboolean
+ecmb_modify_object_sync (ECalMetaBackend *meta_backend,
+                        ECalCache *cal_cache,
+                        ECacheOfflineFlag *offline_flag,
+                        EConflictResolution conflict_resolution,
+                        ECalObjModType mod,
+                        ECalComponent *comp,
+                        ECalComponent **out_old_comp,
+                        ECalComponent **out_new_comp,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       struct icaltimetype itt;
+       ECalComponentId *id;
+       ECalComponent *old_comp = NULL, *new_comp = NULL, *master_comp, *existing_comp = NULL;
+       GSList *instances = NULL;
+       gchar *extra = NULL, *new_uid = NULL, *new_extra = NULL;
+       gboolean success = TRUE, requires_put = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (comp != NULL, FALSE);
+
+       id = e_cal_component_get_id (comp);
+       if (!id) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+               return FALSE;
+       }
+
+       if (!e_cal_cache_get_components_by_uid (cal_cache, id->uid, &instances, cancellable, &local_error)) {
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+               }
+
+               g_propagate_error (error, local_error);
+               e_cal_component_free_id (id);
+
+               return FALSE;
+       }
+
+       master_comp = ecmb_find_in_instances (instances, id->uid, NULL);
+       if (e_cal_component_is_instance (comp)) {
+               /* Set detached instance as the old object */
+               existing_comp = ecmb_find_in_instances (instances, id->uid, id->rid);
+
+               if (!existing_comp && mod == E_CAL_OBJ_MOD_ONLY_THIS) {
+                       g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+
+                       g_slist_free_full (instances, g_object_unref);
+                       e_cal_component_free_id (id);
+
+                       return FALSE;
+               }
+       }
+
+       if (!existing_comp)
+               existing_comp = master_comp;
+
+       if (!e_cal_cache_get_component_extra (cal_cache, id->uid, id->rid, &extra, cancellable, NULL) && 
id->rid) {
+               if (!e_cal_cache_get_component_extra (cal_cache, id->uid, NULL, &extra, cancellable, NULL))
+                       extra = NULL;
+       }
+
+       /* Set the last modified time on the component */
+       itt = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
+       e_cal_component_set_last_modified (comp, &itt);
+
+       /* Remember old and new components */
+       if (out_old_comp && existing_comp)
+               old_comp = e_cal_component_clone (existing_comp);
+
+       if (out_new_comp)
+               new_comp = e_cal_component_clone (comp);
+
+       switch (mod) {
+       case E_CAL_OBJ_MOD_ONLY_THIS:
+       case E_CAL_OBJ_MOD_THIS:
+               if (e_cal_component_is_instance (comp)) {
+                       if (existing_comp != master_comp) {
+                               instances = g_slist_remove (instances, existing_comp);
+                               g_clear_object (&existing_comp);
+                       }
+               } else {
+                       instances = g_slist_remove (instances, master_comp);
+                       g_clear_object (&master_comp);
+                       existing_comp = NULL;
+               }
+
+               instances = g_slist_append (instances, e_cal_component_clone (comp));
+               break;
+       case E_CAL_OBJ_MOD_ALL:
+               e_cal_recur_ensure_end_dates (comp, TRUE, e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+               /* Replace master object */
+               instances = g_slist_remove (instances, master_comp);
+               g_clear_object (&master_comp);
+               existing_comp = NULL;
+
+               instances = g_slist_prepend (instances, e_cal_component_clone (comp));
+               break;
+       case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
+       case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
+               if (e_cal_component_is_instance (comp) && master_comp) {
+                       struct icaltimetype rid, master_dtstart;
+                       icalcomponent *icalcomp = e_cal_component_get_icalcomponent (comp);
+                       icalcomponent *split_icalcomp;
+                       icalproperty *prop;
+
+                       rid = icalcomponent_get_recurrenceid (icalcomp);
+
+                       if (mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE &&
+                           e_cal_util_is_first_instance (master_comp, icalcomponent_get_recurrenceid 
(icalcomp),
+                               e_cal_cache_resolve_timezone_simple_cb, cal_cache)) {
+                               icalproperty *prop = icalcomponent_get_first_property (icalcomp, 
ICAL_RECURRENCEID_PROPERTY);
+
+                               if (prop)
+                                       icalcomponent_remove_property (icalcomp, prop);
+
+                               e_cal_component_rescan (comp);
+
+                               /* Then do it like for "mod_all" */
+                               e_cal_recur_ensure_end_dates (comp, TRUE, 
e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+                               /* Replace master */
+                               instances = g_slist_remove (instances, master_comp);
+                               g_clear_object (&master_comp);
+                               existing_comp = NULL;
+
+                               instances = g_slist_prepend (instances, e_cal_component_clone (comp));
+
+                               if (out_new_comp) {
+                                       g_clear_object (&new_comp);
+                                       new_comp = e_cal_component_clone (comp);
+                               }
+                               break;
+                       }
+
+                       prop = icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY);
+                       if (prop)
+                               icalcomponent_remove_property (icalcomp, prop);
+                       e_cal_component_rescan (comp);
+
+                       master_dtstart = icalcomponent_get_dtstart (e_cal_component_get_icalcomponent 
(master_comp));
+                       split_icalcomp = e_cal_util_split_at_instance (icalcomp, rid, master_dtstart);
+                       if (split_icalcomp) {
+                               rid = icaltime_convert_to_zone (rid, icaltimezone_get_utc_timezone ());
+                               e_cal_util_remove_instances (e_cal_component_get_icalcomponent (master_comp), 
rid, mod);
+                               e_cal_component_rescan (master_comp);
+                               e_cal_recur_ensure_end_dates (master_comp, TRUE, 
e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+                               if (out_new_comp) {
+                                       g_clear_object (&new_comp);
+                                       new_comp = e_cal_component_clone (master_comp);
+                               }
+                       }
+
+                       if (split_icalcomp) {
+                               gchar *new_uid;
+
+                               new_uid = e_cal_component_gen_uid ();
+                               icalcomponent_set_uid (split_icalcomp, new_uid);
+                               g_free (new_uid);
+
+                               g_warn_if_fail (e_cal_component_set_icalcomponent (comp, split_icalcomp));
+
+                               e_cal_recur_ensure_end_dates (comp, TRUE, 
e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+                               success = ecmb_create_object_sync (meta_backend, cal_cache, offline_flag, 
E_CONFLICT_RESOLUTION_FAIL,
+                                       comp, NULL, NULL, cancellable, error);
+                       }
+               } else {
+                       /* Replace master */
+                       instances = g_slist_remove (instances, master_comp);
+                       g_clear_object (&master_comp);
+                       existing_comp = NULL;
+
+                       instances = g_slist_prepend (instances, e_cal_component_clone (comp));
+               }
+               break;
+       }
+
+       if (success && *offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+                       *offline_flag = E_CACHE_IS_ONLINE;
+               } else {
+                       *offline_flag = E_CACHE_IS_OFFLINE;
+               }
+       }
+
+       if (success && *offline_flag == E_CACHE_IS_ONLINE) {
+               success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache, TRUE, 
conflict_resolution,
+                       instances, extra, id->uid, &requires_put, &new_uid, &new_extra, cancellable, error);
+       }
+
+       if (success && requires_put)
+               success = ecmb_put_instances (meta_backend, cal_cache, id->uid, *offline_flag, instances, 
new_extra ? new_extra : extra, cancellable, error);
+
+       if (!success) {
+               g_clear_object (&old_comp);
+               g_clear_object (&new_comp);
+       }
+
+       if (out_old_comp)
+               *out_old_comp = old_comp;
+       if (out_new_comp) {
+               if (new_uid) {
+                       if (!e_cal_cache_get_component (cal_cache, new_uid, id->rid, out_new_comp, 
cancellable, NULL))
+                               *out_new_comp = NULL;
+               } else {
+                       *out_new_comp = new_comp ? g_object_ref (new_comp) : NULL;
+               }
+       }
+
+       g_slist_free_full (instances, g_object_unref);
+       e_cal_component_free_id (id);
+       g_clear_object (&new_comp);
+       g_free (new_extra);
+       g_free (new_uid);
+       g_free (extra);
+
+       return success;
+}
+
+static void
+ecmb_modify_objects_sync (ECalBackendSync *sync_backend,
+                         EDataCal *cal,
+                         GCancellable *cancellable,
+                         const GSList *calobjs,
+                         ECalObjModType mod,
+                         GSList **out_old_components,
+                         GSList **out_new_components,
+                         GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       icalcomponent_kind backend_kind;
+       GSList *link;
+       gboolean success = TRUE;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (calobjs != NULL);
+       g_return_if_fail (out_old_components != NULL);
+       g_return_if_fail (out_new_components != NULL);
+
+       if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+               g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+               return;
+       }
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_if_fail (cal_cache != NULL);
+
+       backend_kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+       for (link = (GSList *) calobjs; link && success; link = g_slist_next (link)) {
+               ECalComponent *comp, *old_comp = NULL, *new_comp = NULL;
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error))
+                       break;
+
+               comp = e_cal_component_new_from_string (link->data);
+               if (!comp ||
+                   !e_cal_component_get_icalcomponent (comp) ||
+                   backend_kind != icalcomponent_isa (e_cal_component_get_icalcomponent (comp))) {
+                       g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+                       break;
+               }
+
+               success = ecmb_modify_object_sync (meta_backend, cal_cache, &offline_flag, 
conflict_resolution,
+                       mod, comp, &old_comp, &new_comp, cancellable, error);
+
+               if (success) {
+                       *out_old_components = g_slist_prepend (*out_old_components, old_comp);
+                       *out_new_components = g_slist_prepend (*out_new_components, new_comp);
+               }
+
+               g_object_unref (comp);
+       }
+
+       *out_old_components = g_slist_reverse (*out_old_components);
+       *out_new_components = g_slist_reverse (*out_new_components);
+
+       g_object_unref (cal_cache);
+}
+
+static gboolean
+ecmb_remove_object_sync (ECalMetaBackend *meta_backend,
+                        ECalCache *cal_cache,
+                        ECacheOfflineFlag *offline_flag,
+                        EConflictResolution conflict_resolution,
+                        ECalObjModType mod,
+                        const gchar *uid,
+                        const gchar *rid,
+                        ECalComponent **out_old_comp,
+                        ECalComponent **out_new_comp,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       struct icaltimetype itt;
+       ECalComponent *old_comp = NULL, *new_comp = NULL, *master_comp, *existing_comp = NULL;
+       GSList *instances = NULL;
+       gboolean success = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (rid && !*rid)
+               rid = NULL;
+
+       if ((mod == E_CAL_OBJ_MOD_THIS_AND_PRIOR ||
+           mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE) && !rid) {
+               /* Require Recurrence-ID for these types */
+               g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+               return FALSE;
+       }
+
+       if (!e_cal_cache_get_components_by_uid (cal_cache, uid, &instances, cancellable, &local_error)) {
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+               }
+
+               g_propagate_error (error, local_error);
+
+               return FALSE;
+       }
+
+       master_comp = ecmb_find_in_instances (instances, uid, NULL);
+       if (rid) {
+               /* Set detached instance as the old object */
+               existing_comp = ecmb_find_in_instances (instances, uid, rid);
+       }
+
+       if (!existing_comp)
+               existing_comp = master_comp;
+
+       /* Remember old and new components */
+       if (out_old_comp && existing_comp)
+               old_comp = e_cal_component_clone (existing_comp);
+
+       if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+               if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+                   ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+                       *offline_flag = E_CACHE_IS_ONLINE;
+               } else {
+                       *offline_flag = E_CACHE_IS_OFFLINE;
+               }
+       }
+
+       switch (mod) {
+       case E_CAL_OBJ_MOD_ALL:
+               /* Will remove the whole component below */
+               break;
+       case E_CAL_OBJ_MOD_ONLY_THIS:
+       case E_CAL_OBJ_MOD_THIS:
+               if (rid) {
+                       if (existing_comp != master_comp) {
+                               instances = g_slist_remove (instances, existing_comp);
+                               g_clear_object (&existing_comp);
+                       } else if (mod == E_CAL_OBJ_MOD_ONLY_THIS) {
+                               success = FALSE;
+                               g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+                       } else {
+                               itt = icaltime_from_string (rid);
+                               if (!itt.zone) {
+                                       ECalComponentDateTime dt;
+
+                                       e_cal_component_get_dtstart (master_comp, &dt);
+                                       if (dt.value && dt.tzid) {
+                                               icaltimezone *zone = e_cal_cache_resolve_timezone_simple_cb 
(dt.tzid, cal_cache);
+
+                                               if (zone)
+                                                       itt = icaltime_convert_to_zone (itt, zone);
+                                       }
+                                       e_cal_component_free_datetime (&dt);
+
+                                       itt = icaltime_convert_to_zone (itt, icaltimezone_get_utc_timezone 
());
+                               }
+
+                               e_cal_util_remove_instances (e_cal_component_get_icalcomponent (master_comp), 
itt, mod);
+                       }
+
+                       if (success && out_new_comp)
+                               new_comp = e_cal_component_clone (master_comp);
+               } else {
+                       mod = E_CAL_OBJ_MOD_ALL;
+               }
+               break;
+       case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
+       case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
+               if (master_comp) {
+                       time_t fromtt, instancett;
+                       GSList *link, *previous = instances;
+
+                       itt = icaltime_from_string (rid);
+                       if (!itt.zone) {
+                               ECalComponentDateTime dt;
+
+                               e_cal_component_get_dtstart (master_comp, &dt);
+                               if (dt.value && dt.tzid) {
+                                       icaltimezone *zone = e_cal_cache_resolve_timezone_simple_cb (dt.tzid, 
cal_cache);
+
+                                       if (zone)
+                                               itt = icaltime_convert_to_zone (itt, zone);
+                               }
+                               e_cal_component_free_datetime (&dt);
+
+                               itt = icaltime_convert_to_zone (itt, icaltimezone_get_utc_timezone ());
+                       }
+
+                       e_cal_util_remove_instances (e_cal_component_get_icalcomponent (master_comp), itt, 
mod);
+
+                       fromtt = icaltime_as_timet (itt);
+
+                       /* Remove detached instances */
+                       for (link = instances; link && fromtt > 0;) {
+                               ECalComponent *comp = link->data;
+                               ECalComponentRange range;
+
+                               if (!e_cal_component_is_instance (comp)) {
+                                       previous = link;
+                                       link = g_slist_next (link);
+                                       continue;
+                               }
+
+                               e_cal_component_get_recurid (comp, &range);
+                               if (range.datetime.value)
+                                       instancett = icaltime_as_timet (*range.datetime.value);
+                               else
+                                       instancett = 0;
+                               e_cal_component_free_range (&range);
+
+                               if (instancett > 0 && (
+                                   (mod == E_CAL_OBJ_MOD_THIS_AND_PRIOR && instancett <= fromtt) ||
+                                   (mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE && instancett >= fromtt))) {
+                                       GSList *prev_instances = instances;
+
+                                       instances = g_slist_remove (instances, comp);
+                                       g_clear_object (&comp);
+
+                                       /* Restart the lookup */
+                                       if (previous == prev_instances)
+                                               previous = instances;
+
+                                       link = previous;
+                               } else {
+                                       previous = link;
+                                       link = g_slist_next (link);
+                               }
+                       }
+               } else {
+                       mod = E_CAL_OBJ_MOD_ALL;
+               }
+               break;
+       }
+
+       if (success) {
+               gchar *extra = NULL;
+
+               if (!e_cal_cache_get_component_extra (cal_cache, uid, NULL, &extra, cancellable, NULL))
+                       extra = NULL;
+
+               if (mod == E_CAL_OBJ_MOD_ALL) {
+                       if (*offline_flag == E_CACHE_IS_ONLINE) {
+                               gchar *ical_string = NULL;
+
+                               g_warn_if_fail (e_cal_cache_get_component_as_string (cal_cache, uid, NULL, 
&ical_string, cancellable, NULL));
+
+                               success = e_cal_meta_backend_remove_component_sync (meta_backend, 
conflict_resolution, uid, extra, ical_string, cancellable, error);
+
+                               g_free (ical_string);
+                       }
+
+                       success = success && ecmb_maybe_remove_from_cache (meta_backend, cal_cache, 
*offline_flag, uid, cancellable, error);
+               } else {
+                       gboolean requires_put = TRUE;
+                       gchar *new_uid = NULL, *new_extra = NULL;
+
+                       if (master_comp) {
+                               icalcomponent *icalcomp = e_cal_component_get_icalcomponent (master_comp);
+
+                               icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 
1);
+
+                               e_cal_component_rescan (master_comp);
+
+                               /* Set the last modified time on the component */
+                               itt = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
+                               e_cal_component_set_last_modified (master_comp, &itt);
+                       }
+
+                       if (*offline_flag == E_CACHE_IS_ONLINE) {
+                               success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache, TRUE, 
conflict_resolution,
+                                       instances, extra, uid, &requires_put, &new_uid, &new_extra, 
cancellable, error);
+                       }
+
+                       if (success && requires_put)
+                               success = ecmb_put_instances (meta_backend, cal_cache, uid, *offline_flag, 
instances, new_extra ? new_extra : extra, cancellable, error);
+
+                       if (success && new_uid && !requires_put) {
+                               g_clear_object (&new_comp);
+
+                               if (!e_cal_cache_get_component (cal_cache, new_uid, NULL, &new_comp, 
cancellable, NULL))
+                                       new_comp = NULL;
+                       }
+
+                       g_free (new_uid);
+                       g_free (new_extra);
+               }
+
+               g_free (extra);
+       }
+
+       if (!success) {
+               g_clear_object (&old_comp);
+               g_clear_object (&new_comp);
+       }
+
+       if (out_old_comp)
+               *out_old_comp = old_comp;
+       if (out_new_comp)
+               *out_new_comp = new_comp;
+
+       g_slist_free_full (instances, g_object_unref);
+
+       return success;
+}
+
+static void
+ecmb_remove_objects_sync (ECalBackendSync *sync_backend,
+                         EDataCal *cal,
+                         GCancellable *cancellable,
+                         const GSList *ids,
+                         ECalObjModType mod,
+                         GSList **out_old_components,
+                         GSList **out_new_components,
+                         GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       GSList *link;
+       gboolean success = TRUE;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (ids != NULL);
+       g_return_if_fail (out_old_components != NULL);
+       g_return_if_fail (out_new_components != NULL);
+
+       if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+               g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+               return;
+       }
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_if_fail (cal_cache != NULL);
+
+       for (link = (GSList *) ids; link && success; link = g_slist_next (link)) {
+               ECalComponent *old_comp = NULL, *new_comp = NULL;
+               ECalComponentId *id = link->data;
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error))
+                       break;
+
+               if (!id) {
+                       g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+                       break;
+               }
+
+               success = ecmb_remove_object_sync (meta_backend, cal_cache, &offline_flag, 
conflict_resolution,
+                       mod, id->uid, id->rid, &old_comp, &new_comp, cancellable, error);
+
+               if (success) {
+                       *out_old_components = g_slist_prepend (*out_old_components, old_comp);
+                       *out_new_components = g_slist_prepend (*out_new_components, new_comp);
+               }
+       }
+
+       *out_old_components = g_slist_reverse (*out_old_components);
+       *out_new_components = g_slist_reverse (*out_new_components);
+
+       g_object_unref (cal_cache);
+}
+
+static gboolean
+ecmb_receive_object_sync (ECalMetaBackend *meta_backend,
+                         ECalCache *cal_cache,
+                         ECacheOfflineFlag *offline_flag,
+                         EConflictResolution conflict_resolution,
+                         ECalComponent *comp,
+                         icalproperty_method method,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       ESourceRegistry *registry;
+       ECalBackend *cal_backend;
+       gboolean is_declined, is_in_cache;
+       ECalObjModType mod;
+       ECalComponentId *id;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), FALSE);
+
+       id = e_cal_component_get_id (comp);
+
+       if (!id && method == ICAL_METHOD_PUBLISH) {
+               gchar *new_uid;
+
+               new_uid = e_cal_component_gen_uid ();
+               e_cal_component_set_uid (comp, new_uid);
+               g_free (new_uid);
+
+               id = e_cal_component_get_id (comp);
+       }
+
+       if (!id) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+               return FALSE;
+       }
+
+       cal_backend = E_CAL_BACKEND (meta_backend);
+       registry = e_cal_backend_get_registry (cal_backend);
+
+       /* just to check whether component exists in a cache */
+       is_in_cache = e_cal_cache_contains (cal_cache, id->uid, NULL, E_CACHE_EXCLUDE_DELETED) ||
+               (id->rid && *id->rid && e_cal_cache_contains (cal_cache, id->uid, id->rid, 
E_CACHE_EXCLUDE_DELETED));
+
+       mod = e_cal_component_is_instance (comp) ? E_CAL_OBJ_MOD_THIS : E_CAL_OBJ_MOD_ALL;
+
+       switch (method) {
+       case ICAL_METHOD_PUBLISH:
+       case ICAL_METHOD_REQUEST:
+       case ICAL_METHOD_REPLY:
+               is_declined = e_cal_backend_user_declined (registry, e_cal_component_get_icalcomponent 
(comp));
+               if (is_in_cache) {
+                       if (!is_declined) {
+                               success = ecmb_modify_object_sync (meta_backend, cal_cache, offline_flag, 
conflict_resolution,
+                                       mod, comp, NULL, NULL, cancellable, error);
+                       } else {
+                               success = ecmb_remove_object_sync (meta_backend, cal_cache, offline_flag, 
conflict_resolution,
+                                       mod, id->uid, id->rid, NULL, NULL, cancellable, error);
+                       }
+               } else if (!is_declined) {
+                       success = ecmb_create_object_sync (meta_backend, cal_cache, offline_flag, 
conflict_resolution,
+                               comp, NULL, NULL, cancellable, error);
+               }
+               break;
+       case ICAL_METHOD_CANCEL:
+               if (is_in_cache) {
+                       success = ecmb_remove_object_sync (meta_backend, cal_cache, offline_flag, 
conflict_resolution,
+                               E_CAL_OBJ_MOD_THIS, id->uid, id->rid, NULL, NULL, cancellable, error);
+               } else {
+                       g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+               }
+               break;
+
+       default:
+               g_propagate_error (error, e_data_cal_create_error (UnsupportedMethod, NULL));
+               break;
+       }
+
+       e_cal_component_free_id (id);
+
+       return success;
+}
+
+static void
+ecmb_receive_objects_sync (ECalBackendSync *sync_backend,
+                          EDataCal *cal,
+                          GCancellable *cancellable,
+                          const gchar *calobj,
+                          GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+       EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+       ECalCache *cal_cache;
+       ECalComponent *comp;
+       icalcomponent *icalcomp, *subcomp;
+       icalcomponent_kind kind;
+       icalproperty_method top_method;
+       GSList *comps = NULL, *link;
+       gboolean success = TRUE;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (calobj != NULL);
+
+       if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+               g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+               return;
+       }
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_if_fail (cal_cache != NULL);
+
+       icalcomp = icalparser_parse_string (calobj);
+       if (!icalcomp) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+               g_object_unref (cal_cache);
+               return;
+       }
+
+       kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
+               for (subcomp = icalcomponent_get_first_component (icalcomp, kind);
+                    subcomp && success;
+                    subcomp = icalcomponent_get_next_component (icalcomp, kind)) {
+                       comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (subcomp));
+
+                       if (comp)
+                               comps = g_slist_prepend (comps, comp);
+               }
+       } else if (icalcomponent_isa (icalcomp) == kind) {
+               comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (icalcomp));
+
+               if (comp)
+                       comps = g_slist_prepend (comps, comp);
+       }
+
+       if (!comps) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+               icalcomponent_free (icalcomp);
+               g_object_unref (cal_cache);
+               return;
+       }
+
+       comps = g_slist_reverse (comps);
+
+       if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT)
+               ecmb_gather_timezones (meta_backend, E_TIMEZONE_CACHE (cal_cache), icalcomp);
+
+       if (icalcomponent_get_first_property (icalcomp, ICAL_METHOD_PROPERTY))
+               top_method = icalcomponent_get_method (icalcomp);
+       else
+               top_method = ICAL_METHOD_PUBLISH;
+
+       for (link = comps; link && success; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+               icalproperty_method method;
+
+               subcomp = e_cal_component_get_icalcomponent (comp);
+
+               if (icalcomponent_get_first_property (subcomp, ICAL_METHOD_PROPERTY)) {
+                       method = icalcomponent_get_method (subcomp);
+               } else {
+                       method = top_method;
+               }
+
+               success = ecmb_receive_object_sync (meta_backend, cal_cache, &offline_flag, 
conflict_resolution,
+                       comp, method, cancellable, error);
+       }
+
+       g_slist_free_full (comps, g_object_unref);
+       icalcomponent_free (icalcomp);
+       g_object_unref (cal_cache);
+}
+
+static void
+ecmb_send_objects_sync (ECalBackendSync *sync_backend,
+                       EDataCal *cal,
+                       GCancellable *cancellable,
+                       const gchar *calobj,
+                       GSList **out_users,
+                       gchar **out_modified_calobj,
+                       GError **error)
+{
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (calobj != NULL);
+       g_return_if_fail (out_users != NULL);
+       g_return_if_fail (out_modified_calobj != NULL);
+
+       *out_users = NULL;
+       *out_modified_calobj = g_strdup (calobj);
+}
+
+static void
+ecmb_add_attachment_uris (ECalComponent *comp,
+                         GSList **out_uris)
+{
+       icalcomponent *icalcomp;
+       icalproperty *prop;
+
+       g_return_if_fail (E_IS_CAL_COMPONENT (comp));
+       g_return_if_fail (out_uris != NULL);
+
+       icalcomp = e_cal_component_get_icalcomponent (comp);
+       g_return_if_fail (icalcomp != NULL);
+
+       for (prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+            prop;
+            prop = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY)) {
+               icalattach *attach = icalproperty_get_attach (prop);
+
+               if (attach && icalattach_get_is_url (attach)) {
+                       const gchar *url;
+
+                       url = icalattach_get_url (attach);
+                       if (url) {
+                               gsize buf_size;
+                               gchar *buf;
+
+                               buf_size = strlen (url);
+                               buf = g_malloc0 (buf_size + 1);
+
+                               icalvalue_decode_ical_string (url, buf, buf_size);
+
+                               *out_uris = g_slist_prepend (*out_uris, g_strdup (buf));
+
+                               g_free (buf);
+                       }
+               }
+       }
+}
+
+static void
+ecmb_get_attachment_uris_sync (ECalBackendSync *sync_backend,
+                              EDataCal *cal,
+                              GCancellable *cancellable,
+                              const gchar *uid,
+                              const gchar *rid,
+                              GSList **out_uris,
+                              GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ECalCache *cal_cache;
+       ECalComponent *comp;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (uid != NULL);
+       g_return_if_fail (out_uris != NULL);
+
+       *out_uris = NULL;
+
+       meta_backend = E_CAL_META_BACKEND (sync_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_if_fail (cal_cache != NULL);
+
+       if (rid && *rid) {
+               if (e_cal_cache_get_component (cal_cache, uid, rid, &comp, cancellable, &local_error) && 
comp) {
+                       ecmb_add_attachment_uris (comp, out_uris);
+                       g_object_unref (comp);
+               }
+       } else {
+               GSList *comps = NULL, *link;
+
+               if (e_cal_cache_get_components_by_uid (cal_cache, uid, &comps, cancellable, &local_error)) {
+                       for (link = comps; link; link = g_slist_next (link)) {
+                               comp = link->data;
+
+                               ecmb_add_attachment_uris (comp, out_uris);
+                       }
+
+                       g_slist_free_full (comps, g_object_unref);
+               }
+       }
+
+       g_object_unref (cal_cache);
+
+       *out_uris = g_slist_reverse (*out_uris);
+
+       if (local_error) {
+               if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+                       g_clear_error (&local_error);
+                       local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+               }
+
+               g_propagate_error (error, local_error);
+       }
+}
+
+static void
+ecmb_discard_alarm_sync (ECalBackendSync *sync_backend,
+                        EDataCal *cal,
+                        GCancellable *cancellable,
+                        const gchar *uid,
+                        const gchar *rid,
+                        const gchar *auid,
+                        GError **error)
+{
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (uid != NULL);
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED,
+               e_client_error_to_string (E_CLIENT_ERROR_NOT_SUPPORTED));
+}
+
+static void
+ecmb_get_timezone_sync (ECalBackendSync *sync_backend,
+                       EDataCal *cal,
+                       GCancellable *cancellable,
+                       const gchar *tzid,
+                       gchar **tzobject,
+                       GError **error)
+{
+       ECalCache *cal_cache;
+       icaltimezone *zone;
+       gchar *timezone_str = NULL;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (tzid != NULL);
+       g_return_if_fail (tzobject != NULL);
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (sync_backend));
+       g_return_if_fail (cal_cache != NULL);
+
+       zone = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cal_cache), tzid);
+       if (zone) {
+               icalcomponent *icalcomp;
+
+               icalcomp = icaltimezone_get_component (zone);
+
+               if (!icalcomp) {
+                       local_error = e_data_cal_create_error (InvalidObject, NULL);
+               } else {
+                       timezone_str = icalcomponent_as_ical_string_r (icalcomp);
+               }
+       }
+
+       g_object_unref (cal_cache);
+
+       if (!local_error && !timezone_str)
+               local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+
+       *tzobject = timezone_str;
+
+       if (local_error)
+               g_propagate_error (error, local_error);
+}
+
+static void
+ecmb_add_timezone_sync (ECalBackendSync *sync_backend,
+                       EDataCal *cal,
+                       GCancellable *cancellable,
+                       const gchar *tzobject,
+                       GError **error)
+{
+       icalcomponent *tz_comp;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return;
+
+       if (!tzobject || !*tzobject) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+               return;
+       }
+
+       tz_comp = icalparser_parse_string (tzobject);
+       if (!tz_comp ||
+           icalcomponent_isa (tz_comp) != ICAL_VTIMEZONE_COMPONENT) {
+               g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+       } else {
+               ECalCache *cal_cache;
+               icaltimezone *zone;
+
+               zone = icaltimezone_new ();
+               icaltimezone_set_component (zone, tz_comp);
+
+               tz_comp = NULL;
+
+               cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (sync_backend));
+               if (cal_cache) {
+                       e_timezone_cache_add_timezone (E_TIMEZONE_CACHE (cal_cache), zone);
+                       icaltimezone_free (zone, 1);
+                       g_object_unref (cal_cache);
+               } else {
+                       g_warn_if_reached ();
+               }
+       }
+
+       if (tz_comp)
+               icalcomponent_free (tz_comp);
+}
+
+static gchar *
+ecmb_get_backend_property (ECalBackend *cal_backend,
+                          const gchar *prop_name)
+{
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (cal_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_REVISION)) {
+               ECalCache *cal_cache;
+               gchar *revision = NULL;
+
+               cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cal_backend));
+               if (cal_cache) {
+                       revision = e_cache_dup_revision (E_CACHE (cal_cache));
+                       g_object_unref (cal_cache);
+               } else {
+                       g_warn_if_reached ();
+               }
+
+               return revision;
+       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
+               ECalComponent *comp;
+               gchar *prop_value;
+
+               comp = e_cal_component_new ();
+
+               switch (e_cal_backend_get_kind (cal_backend)) {
+               case ICAL_VEVENT_COMPONENT:
+                       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_EVENT);
+                       break;
+               case ICAL_VTODO_COMPONENT:
+                       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_TODO);
+                       break;
+               case ICAL_VJOURNAL_COMPONENT:
+                       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_JOURNAL);
+                       break;
+               default:
+                       g_object_unref (comp);
+                       return NULL;
+               }
+
+               prop_value = e_cal_component_get_as_string (comp);
+
+               g_object_unref (comp);
+
+               return prop_value;
+       } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strdup (e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cal_backend)));
+       }
+
+       /* Chain up to parent's method. */
+       return E_CAL_BACKEND_CLASS (e_cal_meta_backend_parent_class)->get_backend_property (cal_backend, 
prop_name);
+}
+
+static void
+ecmb_start_view (ECalBackend *cal_backend,
+                EDataCalView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+       cancellable = ecmb_create_view_cancellable (E_CAL_META_BACKEND (cal_backend), view);
+
+       e_cal_backend_schedule_custom_operation (cal_backend, cancellable,
+               ecmb_start_view_thread_func, g_object_ref (view), g_object_unref);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ecmb_stop_view (ECalBackend *cal_backend,
+               EDataCalView *view)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+       cancellable = ecmb_steal_view_cancellable (E_CAL_META_BACKEND (cal_backend), view);
+       if (cancellable) {
+               g_cancellable_cancel (cancellable);
+               g_object_unref (cancellable);
+       }
+}
+
+static ESourceAuthenticationResult
+ecmb_authenticate_sync (EBackend *backend,
+                       const ENamedParameters *credentials,
+                       gchar **out_certificate_pem,
+                       GTlsCertificateFlags *out_certificate_errors,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       ECalMetaBackend *meta_backend;
+       ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+       gboolean success, refresh_after_authenticate = FALSE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (backend), E_SOURCE_AUTHENTICATION_ERROR);
+
+       meta_backend = E_CAL_META_BACKEND (backend);
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+               g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+                       e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+               return E_SOURCE_AUTHENTICATION_ERROR;
+       }
+
+       g_mutex_lock (&meta_backend->priv->connect_lock);
+       success = e_cal_meta_backend_connect_sync (meta_backend, credentials, &auth_result,
+               out_certificate_pem, out_certificate_errors, cancellable, error);
+
+       if (success) {
+               ecmb_update_connection_values (meta_backend);
+               auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+       } else {
+               if (auth_result == E_SOURCE_AUTHENTICATION_UNKNOWN)
+                       auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+       }
+       g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       e_named_parameters_free (meta_backend->priv->last_credentials);
+       if (success) {
+               meta_backend->priv->last_credentials = e_named_parameters_new_clone (credentials);
+
+               refresh_after_authenticate = meta_backend->priv->refresh_after_authenticate;
+               meta_backend->priv->refresh_after_authenticate = FALSE;
+       } else {
+               meta_backend->priv->last_credentials = NULL;
+       }
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       if (refresh_after_authenticate)
+               ecmb_schedule_refresh (meta_backend);
+
+       return auth_result;
+}
+
+static void
+ecmb_schedule_refresh (ECalMetaBackend *meta_backend)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->refresh_cancellable) {
+               /* Already refreshing the content */
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       cancellable = g_cancellable_new ();
+       meta_backend->priv->refresh_cancellable = g_object_ref (cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_cal_backend_schedule_custom_operation (E_CAL_BACKEND (meta_backend), cancellable,
+               ecmb_refresh_thread_func, NULL, NULL);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ecmb_schedule_source_changed (ECalMetaBackend *meta_backend)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->source_changed_cancellable) {
+               /* Already updating */
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       cancellable = g_cancellable_new ();
+       meta_backend->priv->source_changed_cancellable = g_object_ref (cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_cal_backend_schedule_custom_operation (E_CAL_BACKEND (meta_backend), cancellable,
+               ecmb_source_changed_thread_func, NULL, NULL);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ecmb_schedule_go_offline (ECalMetaBackend *meta_backend)
+{
+       GCancellable *cancellable;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       /* Cancel anything ongoing now, but disconnect in a dedicated thread */
+       if (meta_backend->priv->refresh_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+               g_clear_object (&meta_backend->priv->refresh_cancellable);
+       }
+
+       if (meta_backend->priv->source_changed_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+               g_clear_object (&meta_backend->priv->source_changed_cancellable);
+       }
+
+       if (meta_backend->priv->go_offline_cancellable) {
+               /* Already going offline */
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       cancellable = g_cancellable_new ();
+       meta_backend->priv->go_offline_cancellable = g_object_ref (cancellable);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       e_cal_backend_schedule_custom_operation (E_CAL_BACKEND (meta_backend), cancellable,
+               ecmb_go_offline_thread_func, NULL, NULL);
+
+       g_object_unref (cancellable);
+}
+
+static void
+ecmb_notify_online_cb (GObject *object,
+                      GParamSpec *param,
+                      gpointer user_data)
+{
+       ECalMetaBackend *meta_backend = user_data;
+       gboolean new_value;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       new_value = e_backend_get_online (E_BACKEND (meta_backend));
+       if (!new_value == !meta_backend->priv->current_online_state)
+               return;
+
+       meta_backend->priv->current_online_state = new_value;
+
+       if (new_value)
+               ecmb_schedule_refresh (meta_backend);
+       else
+               ecmb_schedule_go_offline (meta_backend);
+}
+
+static void
+ecmb_cancel_view_cb (gpointer key,
+                    gpointer value,
+                    gpointer user_data)
+{
+       GCancellable *cancellable = value;
+
+       g_return_if_fail (G_IS_CANCELLABLE (cancellable));
+
+       g_cancellable_cancel (cancellable);
+}
+
+static void
+e_cal_meta_backend_set_property (GObject *object,
+                                guint property_id,
+                                const GValue *value,
+                                GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CACHE:
+                       e_cal_meta_backend_set_cache (
+                               E_CAL_META_BACKEND (object),
+                               g_value_get_object (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_cal_meta_backend_get_property (GObject *object,
+                                guint property_id,
+                                GValue *value,
+                                GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CACHE:
+                       g_value_take_object (
+                               value,
+                               e_cal_meta_backend_ref_cache (
+                               E_CAL_META_BACKEND (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_cal_meta_backend_constructed (GObject *object)
+{
+       ECalMetaBackend *meta_backend = E_CAL_META_BACKEND (object);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_meta_backend_parent_class)->constructed (object);
+
+       meta_backend->priv->current_online_state = e_backend_get_online (E_BACKEND (meta_backend));
+
+       meta_backend->priv->notify_online_id = g_signal_connect (meta_backend, "notify::online",
+               G_CALLBACK (ecmb_notify_online_cb), meta_backend);
+
+       if (!meta_backend->priv->cache) {
+               ECalCache *cache;
+               gchar *filename;
+
+               filename = g_build_filename (e_cal_backend_get_cache_dir (E_CAL_BACKEND (meta_backend)), 
"cache.db", NULL);
+               cache = e_cal_cache_new (filename, NULL, &meta_backend->priv->create_cache_error);
+               g_prefix_error (&meta_backend->priv->create_cache_error, _("Failed to create cache ”%s”:"), 
filename);
+
+               g_free (filename);
+
+               if (cache) {
+                       e_cal_meta_backend_set_cache (meta_backend, cache);
+                       g_clear_object (&cache);
+               }
+       }
+}
+
+static void
+e_cal_meta_backend_dispose (GObject *object)
+{
+       ECalMetaBackend *meta_backend = E_CAL_META_BACKEND (object);
+       ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->refresh_timeout_id) {
+               if (source)
+                       e_source_refresh_remove_timeout (source, meta_backend->priv->refresh_timeout_id);
+               meta_backend->priv->refresh_timeout_id = 0;
+       }
+
+       if (meta_backend->priv->source_changed_id) {
+               if (source)
+                       g_signal_handler_disconnect (source, meta_backend->priv->source_changed_id);
+               meta_backend->priv->source_changed_id = 0;
+       }
+
+       if (meta_backend->priv->notify_online_id) {
+               g_signal_handler_disconnect (meta_backend, meta_backend->priv->notify_online_id);
+               meta_backend->priv->notify_online_id = 0;
+       }
+
+       if (meta_backend->priv->revision_changed_id) {
+               if (meta_backend->priv->cache)
+                       g_signal_handler_disconnect (meta_backend->priv->cache, 
meta_backend->priv->revision_changed_id);
+               meta_backend->priv->revision_changed_id = 0;
+       }
+
+       g_hash_table_foreach (meta_backend->priv->view_cancellables, ecmb_cancel_view_cb, NULL);
+
+       if (meta_backend->priv->refresh_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+               g_clear_object (&meta_backend->priv->refresh_cancellable);
+       }
+
+       if (meta_backend->priv->source_changed_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+               g_clear_object (&meta_backend->priv->source_changed_cancellable);
+       }
+
+       if (meta_backend->priv->go_offline_cancellable) {
+               g_cancellable_cancel (meta_backend->priv->go_offline_cancellable);
+               g_clear_object (&meta_backend->priv->go_offline_cancellable);
+       }
+
+       e_named_parameters_free (meta_backend->priv->last_credentials);
+       meta_backend->priv->last_credentials = NULL;
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_meta_backend_parent_class)->dispose (object);
+}
+
+static void
+e_cal_meta_backend_finalize (GObject *object)
+{
+       ECalMetaBackend *meta_backend = E_CAL_META_BACKEND (object);
+
+       g_clear_object (&meta_backend->priv->cache);
+       g_clear_object (&meta_backend->priv->refresh_cancellable);
+       g_clear_object (&meta_backend->priv->source_changed_cancellable);
+       g_clear_object (&meta_backend->priv->go_offline_cancellable);
+       g_clear_error (&meta_backend->priv->create_cache_error);
+       g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+       g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+       g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+       g_mutex_clear (&meta_backend->priv->connect_lock);
+       g_mutex_clear (&meta_backend->priv->property_lock);
+       g_hash_table_destroy (meta_backend->priv->view_cancellables);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_meta_backend_parent_class)->finalize (object);
+}
+
+static void
+e_cal_meta_backend_class_init (ECalMetaBackendClass *klass)
+{
+       GObjectClass *object_class;
+       EBackendClass *backend_class;
+       ECalBackendClass *cal_backend_class;
+       ECalBackendSyncClass *cal_backend_sync_class;
+
+       g_type_class_add_private (klass, sizeof (ECalMetaBackendPrivate));
+
+       klass->get_changes_sync = ecmb_get_changes_sync;
+       klass->search_sync = ecmb_search_sync;
+       klass->search_components_sync = ecmb_search_components_sync;
+       klass->requires_reconnect = ecmb_requires_reconnect;
+
+       cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+       cal_backend_sync_class->open_sync = ecmb_open_sync;
+       cal_backend_sync_class->refresh_sync = ecmb_refresh_sync;
+       cal_backend_sync_class->get_object_sync = ecmb_get_object_sync;
+       cal_backend_sync_class->get_object_list_sync = ecmb_get_object_list_sync;
+       cal_backend_sync_class->get_free_busy_sync = ecmb_get_free_busy_sync;
+       cal_backend_sync_class->create_objects_sync = ecmb_create_objects_sync;
+       cal_backend_sync_class->modify_objects_sync = ecmb_modify_objects_sync;
+       cal_backend_sync_class->remove_objects_sync = ecmb_remove_objects_sync;
+       cal_backend_sync_class->receive_objects_sync = ecmb_receive_objects_sync;
+       cal_backend_sync_class->send_objects_sync = ecmb_send_objects_sync;
+       cal_backend_sync_class->get_attachment_uris_sync = ecmb_get_attachment_uris_sync;
+       cal_backend_sync_class->discard_alarm_sync = ecmb_discard_alarm_sync;
+       cal_backend_sync_class->get_timezone_sync = ecmb_get_timezone_sync;
+       cal_backend_sync_class->add_timezone_sync = ecmb_add_timezone_sync;
+
+       cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+       cal_backend_class->get_backend_property = ecmb_get_backend_property;
+       cal_backend_class->start_view = ecmb_start_view;
+       cal_backend_class->stop_view = ecmb_stop_view;
+
+       backend_class = E_BACKEND_CLASS (klass);
+       backend_class->authenticate_sync = ecmb_authenticate_sync;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->set_property = e_cal_meta_backend_set_property;
+       object_class->get_property = e_cal_meta_backend_get_property;
+       object_class->constructed = e_cal_meta_backend_constructed;
+       object_class->dispose = e_cal_meta_backend_dispose;
+       object_class->finalize = e_cal_meta_backend_finalize;
+
+       /**
+        * ECalMetaBackend:cache:
+        *
+        * The #ECalCache being used for this meta backend.
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_CACHE,
+               g_param_spec_object (
+                       "cache",
+                       "Cache",
+                       "Calendar Cache",
+                       E_TYPE_CAL_CACHE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_STATIC_STRINGS));
+
+       /* This signal is meant for testing purposes mainly */
+       signals[REFRESH_COMPLETED] = g_signal_new (
+               "refresh-completed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               0,
+               NULL, NULL, NULL,
+               G_TYPE_NONE, 0, G_TYPE_NONE);
+
+       /**
+        * ECalMetaBackend::source-changed
+        *
+        * This signal is emitted whenever the underlying backend #ESource
+        * changes. Unlike the #ESource's 'changed' signal this one is
+        * tight to the #ECalMetaBackend itself and is emitted from
+        * a dedicated thread, thus it doesn't block the main thread.
+        *
+        * Since: 3.26
+        **/
+       signals[SOURCE_CHANGED] = g_signal_new (
+               "source-changed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (ECalMetaBackendClass, source_changed),
+               NULL, NULL, NULL,
+               G_TYPE_NONE, 0, G_TYPE_NONE);
+}
+
+static void
+e_cal_meta_backend_init (ECalMetaBackend *meta_backend)
+{
+       meta_backend->priv = G_TYPE_INSTANCE_GET_PRIVATE (meta_backend, E_TYPE_CAL_META_BACKEND, 
ECalMetaBackendPrivate);
+
+       g_mutex_init (&meta_backend->priv->connect_lock);
+       g_mutex_init (&meta_backend->priv->property_lock);
+
+       meta_backend->priv->view_cancellables = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, 
g_object_unref);
+       meta_backend->priv->current_online_state = FALSE;
+       meta_backend->priv->refresh_after_authenticate = FALSE;
+       meta_backend->priv->ever_connected = -1;
+       meta_backend->priv->connected_writable = -1;
+}
+
+/**
+ * e_cal_meta_backend_get_capabilities:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Returns: an #ECalBackend::capabilities property to be used by
+ *    the descendant in conjunction to the descendant's capabilities
+ *    in the result of e_cal_backend_get_backend_property() with
+ *    #CLIENT_BACKEND_PROPERTY_CAPABILITIES.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cal_meta_backend_get_capabilities (ECalMetaBackend *meta_backend)
+{
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+
+       return CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED ","
+               CAL_STATIC_CAPABILITY_BULK_ADDS ","
+               CAL_STATIC_CAPABILITY_BULK_MODIFIES ","
+               CAL_STATIC_CAPABILITY_BULK_REMOVES;
+}
+
+/**
+ * e_cal_meta_backend_set_ever_connected:
+ * @meta_backend: an #ECalMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend ever made a successful connection
+ * to its destination.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * when it had not been connected yet, then it does so immediately, to
+ * eventually report settings error easily.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_set_ever_connected (ECalMetaBackend *meta_backend,
+                                      gboolean value)
+{
+       ECalCache *cal_cache;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       if ((value ? 1 : 0) == meta_backend->priv->ever_connected)
+               return;
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       meta_backend->priv->ever_connected = value ? 1 : 0;
+       e_cache_set_key_int (E_CACHE (cal_cache), ECMB_KEY_EVER_CONNECTED, 
meta_backend->priv->ever_connected, NULL);
+       g_clear_object (&cal_cache);
+}
+
+/**
+ * e_cal_meta_backend_get_ever_connected:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Returns: Whether the @meta_backend ever made a successful connection
+ *    to its destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_get_ever_connected (ECalMetaBackend *meta_backend)
+{
+       gboolean result;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       if (meta_backend->priv->ever_connected == -1) {
+               ECalCache *cal_cache;
+
+               cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+               result = e_cache_get_key_int (E_CACHE (cal_cache), ECMB_KEY_EVER_CONNECTED, NULL) == 1;
+               g_clear_object (&cal_cache);
+
+               meta_backend->priv->ever_connected = result ? 1 : 0;
+       } else {
+               result = meta_backend->priv->ever_connected == 1;
+       }
+
+       return result;
+}
+
+/**
+ * e_cal_meta_backend_set_connected_writable:
+ * @meta_backend: an #ECalMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend connected to a writable destination.
+ * This value has meaning only if e_cal_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * to set the backend writable or not also in the offline mode.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_set_connected_writable (ECalMetaBackend *meta_backend,
+                                          gboolean value)
+{
+       ECalCache *cal_cache;
+
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       if ((value ? 1 : 0) == meta_backend->priv->connected_writable)
+               return;
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       meta_backend->priv->connected_writable = value ? 1 : 0;
+       e_cache_set_key_int (E_CACHE (cal_cache), ECMB_KEY_CONNECTED_WRITABLE, 
meta_backend->priv->connected_writable, NULL);
+       g_clear_object (&cal_cache);
+}
+
+/**
+ * e_cal_meta_backend_get_connected_writable:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * This value has meaning only if e_cal_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * Returns: Whether the @meta_backend connected to a writable destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_get_connected_writable (ECalMetaBackend *meta_backend)
+{
+       gboolean result;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       if (meta_backend->priv->connected_writable == -1) {
+               ECalCache *cal_cache;
+
+               cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+               result = e_cache_get_key_int (E_CACHE (cal_cache), ECMB_KEY_CONNECTED_WRITABLE, NULL) == 1;
+               g_clear_object (&cal_cache);
+
+               meta_backend->priv->connected_writable = result ? 1 : 0;
+       } else {
+               result = meta_backend->priv->connected_writable == 1;
+       }
+
+       return result;
+}
+
+static void
+ecmb_cache_revision_changed_cb (ECache *cache,
+                               gpointer user_data)
+{
+       ECalMetaBackend *meta_backend = user_data;
+       gchar *revision;
+
+       g_return_if_fail (E_IS_CACHE (cache));
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+       revision = e_cache_dup_revision (cache);
+       if (revision) {
+               e_cal_backend_notify_property_changed (E_CAL_BACKEND (meta_backend),
+                       CAL_BACKEND_PROPERTY_REVISION, revision);
+               g_free (revision);
+       }
+}
+
+/**
+ * e_cal_meta_backend_set_cache:
+ * @meta_backend: an #ECalMetaBackend
+ * @cache: an #ECalCache to use
+ *
+ * Sets the @cache as the cache to be used by the @meta_backend.
+ * By default, a cache.db in ECalBackend::cache-dir is created
+ * in the constructed method. This function can be used to override
+ * the default.
+ *
+ * Note the @meta_backend adds its own reference to the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_set_cache (ECalMetaBackend *meta_backend,
+                             ECalCache *cache)
+{
+       g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+       g_return_if_fail (E_IS_CAL_CACHE (cache));
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->cache == cache) {
+               g_mutex_unlock (&meta_backend->priv->property_lock);
+               return;
+       }
+
+       g_clear_error (&meta_backend->priv->create_cache_error);
+
+       if (meta_backend->priv->cache) {
+               g_signal_handler_disconnect (meta_backend->priv->cache,
+                       meta_backend->priv->revision_changed_id);
+       }
+
+       g_clear_object (&meta_backend->priv->cache);
+       meta_backend->priv->cache = g_object_ref (cache);
+
+       meta_backend->priv->revision_changed_id = g_signal_connect_object (meta_backend->priv->cache,
+               "revision-changed", G_CALLBACK (ecmb_cache_revision_changed_cb), meta_backend, 0);
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       g_object_notify (G_OBJECT (meta_backend), "cache");
+}
+
+/**
+ * e_cal_meta_backend_ref_cache:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Returns: (transfer full): Referenced #ECalCache, which is used by @meta_backend.
+ *    Unref it with g_object_unref() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCache *
+e_cal_meta_backend_ref_cache (ECalMetaBackend *meta_backend)
+{
+       ECalCache *cache;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+
+       g_mutex_lock (&meta_backend->priv->property_lock);
+
+       if (meta_backend->priv->cache)
+               cache = g_object_ref (meta_backend->priv->cache);
+       else
+               cache = NULL;
+
+       g_mutex_unlock (&meta_backend->priv->property_lock);
+
+       return cache;
+}
+
+static gint
+sort_master_first_cb (gconstpointer a,
+                     gconstpointer b)
+{
+       icalcomponent *ca, *cb;
+
+       ca = e_cal_component_get_icalcomponent ((ECalComponent *) a);
+       cb = e_cal_component_get_icalcomponent ((ECalComponent *) b);
+
+       if (!ca) {
+               if (!cb)
+                       return 0;
+               else
+                       return -1;
+       } else if (!cb) {
+               return 1;
+       }
+
+       return icaltime_compare (icalcomponent_get_recurrenceid (ca), icalcomponent_get_recurrenceid (cb));
+}
+
+typedef struct {
+       ECalCache *cache;
+       gboolean replace_tzid_with_location;
+       icalcomponent *vcalendar;
+       icalcomponent *icalcomp;
+} ForeachTzidData;
+
+static void
+add_timezone_cb (icalparameter *param,
+                 gpointer user_data)
+{
+       icaltimezone *tz;
+       const gchar *tzid;
+       icalcomponent *vtz_comp;
+       ForeachTzidData *f_data = user_data;
+
+       tzid = icalparameter_get_tzid (param);
+       if (!tzid)
+               return;
+
+       tz = icalcomponent_get_timezone (f_data->vcalendar, tzid);
+       if (tz)
+               return;
+
+       tz = icalcomponent_get_timezone (f_data->icalcomp, tzid);
+       if (!tz)
+               tz = icaltimezone_get_builtin_timezone_from_tzid (tzid);
+       if (!tz && f_data->cache)
+               tz = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (f_data->cache), tzid);
+       if (!tz)
+               return;
+
+       if (f_data->replace_tzid_with_location) {
+               const gchar *location;
+
+               location = icaltimezone_get_location (tz);
+               if (location && *location) {
+                       icalparameter_set_tzid (param, location);
+                       tzid = location;
+
+                       if (icalcomponent_get_timezone (f_data->vcalendar, tzid))
+                               return;
+               }
+       }
+
+       vtz_comp = icaltimezone_get_component (tz);
+
+       if (vtz_comp) {
+               icalcomponent *clone = icalcomponent_new_clone (vtz_comp);
+
+               if (f_data->replace_tzid_with_location) {
+                       icalproperty *prop;
+
+                       prop = icalcomponent_get_first_property (clone, ICAL_TZID_PROPERTY);
+                       if (prop) {
+                               icalproperty_set_tzid (prop, tzid);
+                       }
+               }
+
+               icalcomponent_add_component (f_data->vcalendar, clone);
+       }
+}
+
+/**
+ * e_cal_meta_backend_merge_instances:
+ * @meta_backend: an #ECalMetaBackend
+ * @instances: (element-type ECalComponent): component instances to merge
+ * @replace_tzid_with_location: whether to replace TZID-s with locations
+ *
+ * Merges all the instances provided in @instances list into one VCALENDAR
+ * object, which would eventually contain also all the used timezones.
+ * The @instances list should contain the master object and eventually all
+ * the detached instances for one component (they all have the same UID).
+ *
+ * Any TZID property parameters can be replaced with corresponding timezone
+ * location, which will not influence the timezone itself.
+ *
+ * Returns: (transfer full): an #icalcomponent containing a VCALENDAR
+ *    component which consists of all the given instances. Free
+ *    the returned pointer with icalcomponent_free() when no longer needed.
+ *
+ * See: e_cal_meta_backend_save_component_sync()
+ *
+ * Since: 3.26
+ **/
+icalcomponent *
+e_cal_meta_backend_merge_instances (ECalMetaBackend *meta_backend,
+                                   const GSList *instances,
+                                   gboolean replace_tzid_with_location)
+{
+       ECalCache *cal_cache;
+       ForeachTzidData f_data;
+       icalcomponent *vcalendar;
+       GSList *link, *sorted;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+       g_return_val_if_fail (instances != NULL, NULL);
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, NULL);
+
+       sorted = g_slist_sort (g_slist_copy ((GSList *) instances), sort_master_first_cb);
+
+       vcalendar = e_cal_util_new_top_level ();
+
+       f_data.cache = cal_cache;
+       f_data.replace_tzid_with_location = replace_tzid_with_location;
+       f_data.vcalendar = vcalendar;
+
+       for (link = sorted; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+               icalcomponent *icalcomp;
+
+               if (!E_IS_CAL_COMPONENT (comp)) {
+                       g_warn_if_reached ();
+                       continue;
+               }
+
+               icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
+               icalcomponent_add_component (vcalendar, icalcomp);
+
+               f_data.icalcomp = icalcomp;
+
+               icalcomponent_foreach_tzid (icalcomp, add_timezone_cb, &f_data);
+       }
+
+       g_clear_object (&f_data.cache);
+       g_slist_free (sorted);
+
+       return vcalendar;
+}
+
+static void
+ecmb_remove_all_but_filename_parameter (icalproperty *prop)
+{
+       icalparameter *param;
+
+       g_return_if_fail (prop != NULL);
+
+       while (param = icalproperty_get_first_parameter (prop, ICAL_ANY_PARAMETER), param) {
+               if (icalparameter_isa (param) == ICAL_FILENAME_PARAMETER) {
+                       param = icalproperty_get_next_parameter (prop, ICAL_ANY_PARAMETER);
+                       if (!param)
+                               break;
+               }
+
+               icalproperty_remove_parameter_by_ref (prop, param);
+       }
+}
+
+/**
+ * e_cal_meta_backend_inline_local_attachments_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @component: an icalcomponent to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all URL attachments which point to a local file in @component
+ * to inline attachments, aka adds the file content into the @component.
+ * It also populates FILENAME parameter on the attachment.
+ * This is called automatically before e_cal_meta_backend_save_component_sync().
+ *
+ * The reverse operation is e_cal_meta_backend_store_inline_attachments_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_inline_local_attachments_sync (ECalMetaBackend *meta_backend,
+                                                 icalcomponent *component,
+                                                 GCancellable *cancellable,
+                                                 GError **error)
+{
+       icalproperty *prop;
+       const gchar *uid;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (component != NULL, FALSE);
+
+       uid = icalcomponent_get_uid (component);
+
+       for (prop = icalcomponent_get_first_property (component, ICAL_ATTACH_PROPERTY);
+            prop && success;
+            prop = icalcomponent_get_next_property (component, ICAL_ATTACH_PROPERTY)) {
+               icalattach *attach;
+
+               attach = icalproperty_get_attach (prop);
+               if (icalattach_get_is_url (attach)) {
+                       const gchar *url;
+
+                       url = icalattach_get_url (attach);
+                       if (g_str_has_prefix (url, LOCAL_PREFIX)) {
+                               GFile *file;
+                               gchar *basename;
+                               gchar *content;
+                               gsize len;
+
+                               file = g_file_new_for_uri (url);
+                               basename = g_file_get_basename (file);
+                               if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) {
+                                       icalattach *new_attach;
+                                       icalparameter *param;
+                                       gchar *base64;
+
+                                       base64 = g_base64_encode ((const guchar *) content, len);
+                                       new_attach = icalattach_new_from_data (base64, NULL, NULL);
+                                       g_free (content);
+                                       g_free (base64);
+
+                                       ecmb_remove_all_but_filename_parameter (prop);
+
+                                       icalproperty_set_attach (prop, new_attach);
+                                       icalattach_unref (new_attach);
+
+                                       param = icalparameter_new_value (ICAL_VALUE_BINARY);
+                                       icalproperty_add_parameter (prop, param);
+
+                                       param = icalparameter_new_encoding (ICAL_ENCODING_BASE64);
+                                       icalproperty_add_parameter (prop, param);
+
+                                       /* Preserve existing FILENAME parameter */
+                                       if (!icalproperty_get_first_parameter (prop, 
ICAL_FILENAME_PARAMETER)) {
+                                               const gchar *use_filename = basename;
+
+                                               /* generated filename by Evolution */
+                                               if (uid && g_str_has_prefix (use_filename, uid) &&
+                                                   use_filename[strlen (uid)] == '-') {
+                                                       use_filename += strlen (uid) + 1;
+                                               }
+
+                                               param = icalparameter_new_filename (use_filename);
+                                               icalproperty_add_parameter (prop, param);
+                                       }
+                               } else {
+                                       success = FALSE;
+                               }
+
+                               g_object_unref (file);
+                               g_free (basename);
+                       }
+               }
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_meta_backend_store_inline_attachments_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @component: an icalcomponent to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all inline attachments to URL attachments in @component, which
+ * will point to a local file instead. The function expects FILENAME parameter
+ * to be set on the attachment as the file name of it.
+ * This is called automatically after e_cal_meta_backend_load_component_sync().
+ *
+ * The reverse operation is e_cal_meta_backend_inline_local_attachments_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_store_inline_attachments_sync (ECalMetaBackend *meta_backend,
+                                                 icalcomponent *component,
+                                                 GCancellable *cancellable,
+                                                 GError **error)
+{
+       gint fileindex;
+       icalproperty *prop;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (component != NULL, FALSE);
+
+       for (prop = icalcomponent_get_first_property (component, ICAL_ATTACH_PROPERTY), fileindex = 0;
+            prop && success;
+            prop = icalcomponent_get_next_property (component, ICAL_ATTACH_PROPERTY), fileindex++) {
+               icalattach *attach;
+
+               attach = icalproperty_get_attach (prop);
+               if (!icalattach_get_is_url (attach)) {
+                       icalparameter *param;
+                       const gchar *basename;
+                       gsize len = -1;
+                       gchar *decoded = NULL;
+                       gchar *local_filename;
+
+                       param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+                       basename = param ? icalparameter_get_filename (param) : NULL;
+                       if (!basename || !*basename)
+                               basename = _("attachment.dat");
+
+                       local_filename = e_cal_backend_create_cache_filename (E_CAL_BACKEND (meta_backend), 
icalcomponent_get_uid (component), basename, fileindex);
+
+                       if (local_filename) {
+                               const gchar *content;
+
+                               content = (const gchar *) icalattach_get_data (attach);
+                               decoded = (gchar *) g_base64_decode (content, &len);
+
+                               if (g_file_set_contents (local_filename, decoded, len, error)) {
+                                       icalattach *new_attach;
+                                       gchar *url;
+
+                                       ecmb_remove_all_but_filename_parameter (prop);
+
+                                       url = g_filename_to_uri (local_filename, NULL, NULL);
+                                       new_attach = icalattach_new_from_url (url);
+
+                                       icalproperty_set_attach (prop, new_attach);
+
+                                       icalattach_unref (new_attach);
+                                       g_free (url);
+                               } else {
+                                       success = FALSE;
+                               }
+
+                               g_free (decoded);
+                       }
+
+                       g_free (local_filename);
+               }
+       }
+
+       return success;
+}
+
+/**
+ * e_cal_meta_backend_gather_timezones_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @vcalendar: a VCALENDAR icalcomponent
+ * @remove_existing: whether to remove any existing first
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Extracts all VTIMEZONE components from the @vcalendar and adds them
+ * to the cache, thus they are available when needed. The function does
+ * nothing when the @vcalendar doesn't hold a VCALENDAR component.
+ *
+ * Set the @remove_existing argument to %TRUE to remove all cached timezones
+ * first and then add the existing in the @vcalendar, or set it to %FALSE
+ * to preserver existing timezones and merge them with those in @vcalendar.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_gather_timezones_sync (ECalMetaBackend *meta_backend,
+                                         icalcomponent *vcalendar,
+                                         gboolean remove_existing,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       ECalCache *cal_cache;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (vcalendar != NULL, FALSE);
+
+       if (icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT)
+               return TRUE;
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+       e_cache_lock (E_CACHE (cal_cache), E_CACHE_LOCK_WRITE);
+
+       if (remove_existing)
+               success = e_cal_cache_remove_timezones (cal_cache, cancellable, error);
+
+       if (success)
+               ecmb_gather_timezones (meta_backend, E_TIMEZONE_CACHE (cal_cache), vcalendar);
+
+       e_cache_unlock (E_CACHE (cal_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       g_object_unref (cal_cache);
+
+       return TRUE;
+}
+
+/**
+ * e_cal_meta_backend_empty_cache_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Empties the local cache by removing all known components from it
+ * and notifies about such removal any opened views. It removes also
+ * all known time zones.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_empty_cache_sync (ECalMetaBackend *meta_backend,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       ECalBackend *cal_backend;
+       ECalCache *cal_cache;
+       GSList *ids = NULL, *link;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+       e_cache_lock (E_CACHE (cal_cache), E_CACHE_LOCK_WRITE);
+
+       cal_backend = E_CAL_BACKEND (meta_backend);
+
+       success = e_cal_cache_search_ids (cal_cache, NULL, &ids, cancellable, error);
+       if (success)
+               success = e_cache_remove_all (E_CACHE (cal_cache), cancellable, error);
+
+       e_cache_unlock (E_CACHE (cal_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       g_object_unref (cal_cache);
+
+       if (success) {
+               for (link = ids; link; link = g_slist_next (link)) {
+                       ECalComponentId *id = link->data;
+
+                       if (!id)
+                               continue;
+
+                       e_cal_backend_notify_component_removed (cal_backend, id, NULL, NULL);
+               }
+       }
+
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+
+       return success;
+}
+
+/**
+ * e_cal_meta_backend_connect_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @credentials: (nullable): an #ENamedParameters with previously used credentials, or %NULL
+ * @out_auth_result: (out): an #ESourceAuthenticationResult with an authentication result
+ * @out_certificate_pem: (out) (transfer full): a PEM encoded certificate on failure, or %NULL
+ * @out_certificate_errors: (out): a #GTlsCertificateFlags on failure, or 0
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called always before any operation which requires a connection
+ * to the remote side. It can fail with an #E_CLIENT_ERROR_REPOSITORY_OFFLINE
+ * error to indicate that the remote side cannot be currently reached. Other
+ * errors are propagated to the caller/client side. This method is not called
+ * when the backend is offline.
+ *
+ * The descendant should also call e_cal_backend_set_writable() after successful
+ * connect to the remote side. This value is stored for later use, when being
+ * opened offline.
+ *
+ * The @credentials parameter consists of the previously used credentials.
+ * It's always %NULL with the first connection attempt. To get the credentials,
+ * just set the @out_auth_result to %E_SOURCE_AUTHENTICATION_REQUIRED for
+ * the first time and the function will be called again once the credentials
+ * are available. See the documentation of #ESourceAuthenticationResult for
+ * other available results.
+ *
+ * The out parameters are passed to e_backend_schedule_credentials_required()
+ * and are ignored when the descendant returns %TRUE, aka they are used
+ * only if the connection fails. The @out_certificate_pem and @out_certificate_errors
+ * should be used together and they can be left untouched if the failure reason was
+ * not related to certificate. Use @out_auth_result %E_SOURCE_AUTHENTICATION_UNKNOWN
+ * to indicate other error than @credentials error, otherwise the @error is used
+ * according to @out_auth_result value.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_connect_sync (ECalMetaBackend *meta_backend,
+                                const ENamedParameters *credentials,
+                                ESourceAuthenticationResult *out_auth_result,
+                                gchar **out_certificate_pem,
+                                GTlsCertificateFlags *out_certificate_errors,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->connect_sync != NULL, FALSE);
+
+       return klass->connect_sync (meta_backend, credentials, out_auth_result, out_certificate_pem, 
out_certificate_errors, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_disconnect_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called when the backend goes into offline mode or
+ * when the disconnect is required. The implementation should
+ * not report any error when it is called and the @meta_backend
+ * is not connected.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_disconnect_sync (ECalMetaBackend *meta_backend,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->disconnect_sync != NULL, FALSE);
+
+       return klass->disconnect_sync (meta_backend, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_get_changes_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @last_sync_tag: (nullable): optional sync tag from the last check
+ * @is_repeat: set to %TRUE when this is the repeated call
+ * @out_new_sync_tag: (out) (transfer full): new sync tag to store on success
+ * @out_repeat: (out): whether to repeat this call again; default is %FALSE
+ * @out_created_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ *    a #GSList of #ECalMetaBackendInfo object infos which had been created since
+ *    the last check
+ * @out_modified_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ *    a #GSList of #ECalMetaBackendInfo object infos which had been modified since
+ *    the last check
+ * @out_removed_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ *    a #GSList of #ECalMetaBackendInfo object infos which had been removed since
+ *    the last check
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the changes since the last check which had been done
+ * on the remote side.
+ *
+ * The @last_sync_tag can be used as a tag of the last check. This can be %NULL,
+ * when there was no previous call or when the descendant doesn't store any
+ * such tags. The @out_new_sync_tag can be populated with a value to be stored
+ * and used the next time.
+ *
+ * The @out_repeat can be set to %TRUE when the descendant didn't finish
+ * read of all the changes. In that case the @meta_backend calls this
+ * function again with the @out_new_sync_tag as the @last_sync_tag, but also
+ * notifies about the found changes immediately. The @is_repeat is set
+ * to %TRUE as well in this case, otherwise it's %FALSE.
+ *
+ * The descendant can populate also ECalMetaBackendInfo::object of
+ * the @out_created_objects and @out_modified_objects, if known, in which
+ * case this will be used instead of loading it with e_cal_meta_backend_load_component_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation calls e_cal_meta_backend_list_existing_sync()
+ * and then compares the list with the current content of the local cache
+ * and populates the respective lists appropriately.
+ *
+ * Each output #GSList should be freed with
+ * g_slist_free_full (objects, e_cal_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_get_changes_sync (ECalMetaBackend *meta_backend,
+                                    const gchar *last_sync_tag,
+                                    gboolean is_repeat,
+                                    gchar **out_new_sync_tag,
+                                    gboolean *out_repeat,
+                                    GSList **out_created_objects,
+                                    GSList **out_modified_objects,
+                                    GSList **out_removed_objects,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_repeat != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->get_changes_sync != NULL, FALSE);
+
+       return klass->get_changes_sync (meta_backend,
+               last_sync_tag,
+               is_repeat,
+               out_new_sync_tag,
+               out_repeat,
+               out_created_objects,
+               out_modified_objects,
+               out_removed_objects,
+               cancellable,
+               error);
+}
+
+/**
+ * e_cal_meta_backend_list_existing_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @out_new_sync_tag: (out) (transfer full): optional return location for a new sync tag
+ * @out_existing_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ *    a #GSList of #ECalMetaBackendInfo object infos which are stored on the remote side
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Used to get list of all existing objects on the remote side. The descendant
+ * can optionally provide @out_new_sync_tag, which will be stored on success, if
+ * not %NULL. The descendant can populate also ECalMetaBackendInfo::object of
+ * the @out_existing_objects, if known, in which case this will be used instead
+ * of loading it with e_cal_meta_backend_load_component_sync().
+ *
+ * It is mandatory to implement this virtual method by the descendant, unless
+ * it implements its own get_changes_sync().
+ *
+ * The @out_existing_objects #GSList should be freed with
+ * g_slist_free_full (objects, e_cal_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_list_existing_sync (ECalMetaBackend *meta_backend,
+                                      gchar **out_new_sync_tag,
+                                      GSList **out_existing_objects,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->list_existing_sync != NULL, FALSE);
+
+       return klass->list_existing_sync (meta_backend, out_new_sync_tag, out_existing_objects, cancellable, 
error);
+}
+
+/**
+ * e_cal_meta_backend_load_component_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @uid: a component UID
+ * @extra: (nullable): optional extra data stored with the component, or %NULL
+ * @out_component: (out) (transfer full): a loaded component, as icalcomponent
+ * @out_extra: (out) (transfer full): an extra data to store to #ECalCache with this component
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Loads a component from the remote side. Any detached instances should be
+ * returned together with the master object. The @out_component can be either
+ * a VCALENDAR component, which would contain the master object and all of
+ * its detached instances, eventually also used time zones, or the requested
+ * component of type VEVENT, VJOURNAL or VTODO.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * The returned @out_component should be freed with icalcomponent_free(),
+ * when no longer needed.
+ *
+ * The returned @out_extra should be freed with g_free(), when no longer
+ * needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_load_component_sync (ECalMetaBackend *meta_backend,
+                                       const gchar *uid,
+                                       const gchar *extra,
+                                       icalcomponent **out_component,
+                                       gchar **out_extra,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_component != NULL, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->load_component_sync != NULL, FALSE);
+
+       return klass->load_component_sync (meta_backend, uid, extra, out_component, out_extra, cancellable, 
error);
+}
+
+/**
+ * e_cal_meta_backend_save_component_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @overwrite_existing: %TRUE when can overwrite existing components, %FALSE otherwise
+ * @conflict_resolution: one of #EConflictResolution, what to do on conflicts
+ * @instances: (element-type ECalComponent): instances of the component to save
+ * @extra: (nullable): extra data saved with the components in an #ECalCache
+ * @out_new_uid: (out) (transfer full): return location for the UID of the saved component
+ * @out_new_extra: (out) (transfer full): return location for the extra data to store with the component
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Saves one component into the remote side. The @instances contain the master
+ * object and all the detached instances of the same component (all have the same UID).
+ * When the @overwrite_existing is %TRUE, then the descendant can overwrite an object
+ * with the same UID on the remote side (usually used for modify). The @conflict_resolution
+ * defines what to do when the remote side had made any changes to the object since
+ * the last update.
+ *
+ * The descendant can use e_cal_meta_backend_merge_instances() to merge
+ * the instances into one VCALENDAR component, which will contain also
+ * used time zones.
+ *
+ * The components in @instances have already converted locally stored attachments
+ * into inline attachments, thus it's not needed to call
+ * e_cal_meta_backend_inline_local_attachments_sync() by the descendant.
+ *
+ * The @out_new_uid can be populated with a UID of the saved component as the server
+ * assigned it to it. This UID, if set, is loaded from the remote side afterwards,
+ * also to see whether any changes had been made to the component by the remote side.
+ *
+ * The @out_new_extra can be populated with a new extra data to save with the component.
+ * Left it %NULL, to keep the same value as the @extra.
+ *
+ * The descendant can use an #E_CLIENT_ERROR_OUT_OF_SYNC error to indicate that
+ * the save failed due to made changes on the remote side, and let the @meta_backend
+ * to resolve this conflict based on the @conflict_resolution on its own.
+ * The #E_CLIENT_ERROR_OUT_OF_SYNC error should not be used when the descendant
+ * is able to resolve the conflicts itself.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_save_component_sync (ECalMetaBackend *meta_backend,
+                                       gboolean overwrite_existing,
+                                       EConflictResolution conflict_resolution,
+                                       const GSList *instances,
+                                       const gchar *extra,
+                                       gchar **out_new_uid,
+                                       gchar **out_new_extra,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (instances != NULL, FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
+       g_return_val_if_fail (out_new_extra != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+
+       if (!klass->save_component_sync) {
+               g_propagate_error (error, e_data_cal_create_error (NotSupported, NULL));
+               return FALSE;
+       }
+
+       return klass->save_component_sync (meta_backend,
+               overwrite_existing,
+               conflict_resolution,
+               instances,
+               extra,
+               out_new_uid,
+               out_new_extra,
+               cancellable,
+               error);
+}
+
+/**
+ * e_cal_meta_backend_remove_component_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @conflict_resolution: an #EConflictResolution to use
+ * @uid: a component UID
+ * @extra: (nullable): extra data being saved with the component in the local cache, or %NULL
+ * @object: (nullable): corresponding iCalendar object, as stored in the local cache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes a component from the remote side, with all its detached instances.
+ * The @object is not %NULL when it's removing locally deleted object
+ * in offline mode. Being it %NULL, the descendant can obtain the object
+ * from the #ECalCache.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_remove_component_sync (ECalMetaBackend *meta_backend,
+                                         EConflictResolution conflict_resolution,
+                                         const gchar *uid,
+                                         const gchar *extra,
+                                         const gchar *object,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+
+       if (!klass->remove_component_sync) {
+               g_propagate_error (error, e_data_cal_create_error (NotSupported, NULL));
+               return FALSE;
+       }
+
+       return klass->remove_component_sync (meta_backend, conflict_resolution, uid, extra, object, 
cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_search_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @out_icalstrings: (out) (transfer full) (element-type utf8): return location for the found components as 
iCal strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found components as a #GSList of iCal strings @out_icalstrings.
+ * Free the returned @out_icalstrings with g_slist_free_full (icalstrings, g_free);
+ * when no longer needed.
+ * When the @expr is %NULL, all objects are returned. To get
+ * #ECalComponent-s instead, call e_cal_meta_backend_search_components_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_search_sync (ECalMetaBackend *meta_backend,
+                               const gchar *expr,
+                               GSList **out_icalstrings,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->search_sync != NULL, FALSE);
+
+       return klass->search_sync (meta_backend, expr, out_icalstrings, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_search_components_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @out_components: (out) (transfer full) (element-type ECalComponent): return location for the found 
#ECalComponent-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found components as a #GSList of #ECalComponont-s @out_components.
+ * Free the returned @out_components with g_slist_free_full (components, g_object_unref);
+ * when no longer needed.
+ * When the @expr is %NULL, all objects are returned. To get iCal
+ * strings instead, call e_cal_meta_backend_search_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_search_components_sync (ECalMetaBackend *meta_backend,
+                                          const gchar *expr,
+                                          GSList **out_components,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+       g_return_val_if_fail (out_components != NULL, FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->search_components_sync != NULL, FALSE);
+
+       return klass->search_components_sync (meta_backend, expr, out_components, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_requires_reconnect:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Determines, whether current source content requires reconnect of the backend.
+ *
+ * It is optional to implement this virtual method by the descendant. The default
+ * implementation compares %E_SOURCE_EXTENSION_AUTHENTICATION and
+ * %E_SOURCE_EXTENSION_WEBDAV_BACKEND, if existing in the source,
+ * with the values after the last successful connect and returns
+ * %TRUE when they changed. It always return %TRUE when there was
+ * no successful connect done yet.
+ *
+ * Returns: %TRUE, when reconnect is required, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_requires_reconnect (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendClass *klass;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+       klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->requires_reconnect != NULL, FALSE);
+
+       return klass->requires_reconnect (meta_backend);
+}
diff --git a/src/calendar/libedata-cal/e-cal-meta-backend.h b/src/calendar/libedata-cal/e-cal-meta-backend.h
new file mode 100644
index 0000000..5f97093
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.h
@@ -0,0 +1,282 @@
+/* -*- 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 (__LIBEDATA_CAL_H_INSIDE__) && !defined (LIBEDATA_CAL_COMPILATION)
+#error "Only <libedata-cal/libedata-cal.h> should be included directly."
+#endif
+
+#ifndef E_CAL_META_BACKEND_H
+#define E_CAL_META_BACKEND_H
+
+#include <libebackend/libebackend.h>
+#include <libedata-cal/e-cal-backend-sync.h>
+#include <libedata-cal/e-cal-cache.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CAL_META_BACKEND \
+       (e_cal_meta_backend_get_type ())
+#define E_CAL_META_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CAL_META_BACKEND, ECalMetaBackend))
+#define E_CAL_META_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_CAL_META_BACKEND, ECalMetaBackendClass))
+#define E_IS_CAL_META_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CAL_META_BACKEND))
+#define E_IS_CAL_META_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_CAL_META_BACKEND))
+#define E_CAL_META_BACKEND_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_CAL_META_BACKEND, ECalMetaBackendClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ECalMetaBackendInfo {
+       gchar *uid;
+       gchar *revision;
+       gchar *object;
+       gchar *extra;
+} ECalMetaBackendInfo;
+
+#define E_TYPE_CAL_META_BACKEND_INFO (e_cal_meta_backend_info_get_type ())
+
+GType          e_cal_meta_backend_info_get_type
+                                               (void) G_GNUC_CONST;
+ECalMetaBackendInfo *
+               e_cal_meta_backend_info_new     (const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                const gchar *extra);
+ECalMetaBackendInfo *
+               e_cal_meta_backend_info_copy    (const ECalMetaBackendInfo *src);
+void           e_cal_meta_backend_info_free    (gpointer ptr /* ECalMetaBackendInfo * */);
+
+typedef struct _ECalMetaBackend ECalMetaBackend;
+typedef struct _ECalMetaBackendClass ECalMetaBackendClass;
+typedef struct _ECalMetaBackendPrivate ECalMetaBackendPrivate;
+
+/**
+ * ECalMetaBackend:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ECalMetaBackend {
+       /*< private >*/
+       ECalBackendSync parent;
+       ECalMetaBackendPrivate *priv;
+};
+
+/**
+ * ECalMetaBackendClass:
+ *
+ * Class structure for the #ECalMetaBackend class.
+ *
+ * Since: 3.26
+ */
+struct _ECalMetaBackendClass {
+       /*< private >*/
+       ECalBackendSyncClass parent_class;
+
+       /* Virtual methods */
+       gboolean        (* connect_sync)        (ECalMetaBackend *meta_backend,
+                                                const ENamedParameters *credentials,
+                                                ESourceAuthenticationResult *out_auth_result,
+                                                gchar **out_certificate_pem,
+                                                GTlsCertificateFlags *out_certificate_errors,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* disconnect_sync)     (ECalMetaBackend *meta_backend,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+       gboolean        (* get_changes_sync)    (ECalMetaBackend *meta_backend,
+                                                const gchar *last_sync_tag,
+                                                gboolean is_repeat,
+                                                gchar **out_new_sync_tag,
+                                                gboolean *out_repeat,
+                                                GSList **out_created_objects, /* ECalMetaBackendInfo * */
+                                                GSList **out_modified_objects, /* ECalMetaBackendInfo * */
+                                                GSList **out_removed_objects, /* ECalMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* list_existing_sync)  (ECalMetaBackend *meta_backend,
+                                                gchar **out_new_sync_tag,
+                                                GSList **out_existing_objects, /* ECalMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* load_component_sync) (ECalMetaBackend *meta_backend,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                icalcomponent **out_component,
+                                                gchar **out_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* save_component_sync) (ECalMetaBackend *meta_backend,
+                                                gboolean overwrite_existing,
+                                                EConflictResolution conflict_resolution,
+                                                const GSList *instances, /* ECalComponent * */
+                                                const gchar *extra,
+                                                gchar **out_new_uid,
+                                                gchar **out_new_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* remove_component_sync)
+                                               (ECalMetaBackend *meta_backend,
+                                                EConflictResolution conflict_resolution,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                const gchar *object,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* search_sync)         (ECalMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                GSList **out_icalstrings, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* search_components_sync)
+                                               (ECalMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                GSList **out_components, /* ECalComponent * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* requires_reconnect)  (ECalMetaBackend *meta_backend);
+
+       /* Signals */
+       void            (* source_changed)      (ECalMetaBackend *meta_backend);
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_cal_meta_backend_get_type     (void) G_GNUC_CONST;
+
+const gchar *  e_cal_meta_backend_get_capabilities
+                                               (ECalMetaBackend *meta_backend);
+void           e_cal_meta_backend_set_ever_connected
+                                               (ECalMetaBackend *meta_backend,
+                                                gboolean value);
+gboolean       e_cal_meta_backend_get_ever_connected
+                                               (ECalMetaBackend *meta_backend);
+void           e_cal_meta_backend_set_connected_writable
+                                               (ECalMetaBackend *meta_backend,
+                                                gboolean value);
+gboolean       e_cal_meta_backend_get_connected_writable
+                                               (ECalMetaBackend *meta_backend);
+void           e_cal_meta_backend_set_cache    (ECalMetaBackend *meta_backend,
+                                                ECalCache *cache);
+ECalCache *    e_cal_meta_backend_ref_cache    (ECalMetaBackend *meta_backend);
+icalcomponent *        e_cal_meta_backend_merge_instances
+                                               (ECalMetaBackend *meta_backend,
+                                                const GSList *instances, /* ECalComponent * */
+                                                gboolean replace_tzid_with_location);
+gboolean       e_cal_meta_backend_inline_local_attachments_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                icalcomponent *component,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_store_inline_attachments_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                icalcomponent *component,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_gather_timezones_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                icalcomponent *vcalendar,
+                                                gboolean remove_existing,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_empty_cache_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_connect_sync (ECalMetaBackend *meta_backend,
+                                                const ENamedParameters *credentials,
+                                                ESourceAuthenticationResult *out_auth_result,
+                                                gchar **out_certificate_pem,
+                                                GTlsCertificateFlags *out_certificate_errors,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_disconnect_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_get_changes_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                const gchar *last_sync_tag,
+                                                gboolean is_repeat,
+                                                gchar **out_new_sync_tag,
+                                                gboolean *out_repeat,
+                                                GSList **out_created_objects, /* ECalMetaBackendInfo * */
+                                                GSList **out_modified_objects, /* ECalMetaBackendInfo * */
+                                                GSList **out_removed_objects, /* ECalMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_list_existing_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                gchar **out_new_sync_tag,
+                                                GSList **out_existing_objects, /* ECalMetaBackendInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_load_component_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                icalcomponent **out_component,
+                                                gchar **out_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_save_component_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                gboolean overwrite_existing,
+                                                EConflictResolution conflict_resolution,
+                                                const GSList *instances, /* ECalComponent * */
+                                                const gchar *extra,
+                                                gchar **out_new_uid,
+                                                gchar **out_new_extra,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_remove_component_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                EConflictResolution conflict_resolution,
+                                                const gchar *uid,
+                                                const gchar *extra,
+                                                const gchar *object,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_search_sync  (ECalMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                GSList **out_icalstrings, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_search_components_sync
+                                               (ECalMetaBackend *meta_backend,
+                                                const gchar *expr,
+                                                GSList **out_components, /* ECalComponent * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cal_meta_backend_requires_reconnect
+                                               (ECalMetaBackend *meta_backend);
+
+G_END_DECLS
+
+#endif /* E_CAL_META_BACKEND_H */
diff --git a/src/calendar/libedata-cal/libedata-cal.h b/src/calendar/libedata-cal/libedata-cal.h
index c37bef9..a754d70 100644
--- a/src/calendar/libedata-cal/libedata-cal.h
+++ b/src/calendar/libedata-cal/libedata-cal.h
@@ -31,6 +31,8 @@
 #include <libedata-cal/e-cal-backend-store.h>
 #include <libedata-cal/e-cal-backend-sync.h>
 #include <libedata-cal/e-cal-backend-util.h>
+#include <libedata-cal/e-cal-cache.h>
+#include <libedata-cal/e-cal-meta-backend.h>
 #include <libedata-cal/e-data-cal-factory.h>
 #include <libedata-cal/e-data-cal.h>
 #include <libedata-cal/e-data-cal-view.h>
diff --git a/src/libebackend/CMakeLists.txt b/src/libebackend/CMakeLists.txt
index f328f0b..844bcbc 100644
--- a/src/libebackend/CMakeLists.txt
+++ b/src/libebackend/CMakeLists.txt
@@ -10,6 +10,7 @@ set(DEPENDENCIES
 set(SOURCES
        e-backend.c
        e-backend-factory.c
+       e-cache.c
        e-cache-reaper.c
        e-cache-reaper-utils.c
        e-cache-reaper-utils.h
@@ -36,6 +37,7 @@ set(HEADERS
        e-backend.h
        e-backend-enums.h
        e-backend-factory.h
+       e-cache.h
        e-cache-reaper.h
        e-collection-backend.h
        e-collection-backend-factory.h
diff --git a/src/libebackend/e-backend-enums.h b/src/libebackend/e-backend-enums.h
index 9b53344..5c36569 100644
--- a/src/libebackend/e-backend-enums.h
+++ b/src/libebackend/e-backend-enums.h
@@ -82,4 +82,47 @@ typedef enum { /*< flags >*/
        E_SOURCE_PERMISSION_REMOVABLE = 1 << 1
 } ESourcePermissionFlags;
 
+/**
+ * EOfflineState:
+ * @E_OFFLINE_STATE_UNKNOWN: Unknown offline state.
+ * @E_OFFLINE_STATE_SYNCED: The object if synchnized with no local changes.
+ * @E_OFFLINE_STATE_LOCALLY_CREATED: The object is locally created.
+ * @E_OFFLINE_STATE_LOCALLY_MODIFIED: The object is locally modified.
+ * @E_OFFLINE_STATE_LOCALLY_DELETED: The object is locally deleted.
+ *
+ * Defines offline state of an object. Locally changed objects require
+ * synchronization with their remote storage.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_OFFLINE_STATE_UNKNOWN = -1,
+       E_OFFLINE_STATE_SYNCED,
+       E_OFFLINE_STATE_LOCALLY_CREATED,
+       E_OFFLINE_STATE_LOCALLY_MODIFIED,
+       E_OFFLINE_STATE_LOCALLY_DELETED
+} EOfflineState;
+
+/**
+ * EConflictResolution:
+ * @E_CONFLICT_RESOLUTION_FAIL: Fail when a write-conflict occurs.
+ * @E_CONFLICT_RESOLUTION_USE_NEWER: Use newer version of the object,
+ *    which can be either the server version or the local version of it.
+ * @E_CONFLICT_RESOLUTION_KEEP_SERVER: Keep server object on conflict.
+ * @E_CONFLICT_RESOLUTION_KEEP_LOCAL: Write local version of the object on conflict.
+ * @E_CONFLICT_RESOLUTION_WRITE_COPY: Create a new copy of the object on conflict.
+ *
+ * Defines what to do when a conflict between the locally stored and
+ * remotely stored object versions happen during object modify or remove.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_CONFLICT_RESOLUTION_FAIL = 0,
+       E_CONFLICT_RESOLUTION_USE_NEWER,
+       E_CONFLICT_RESOLUTION_KEEP_SERVER,
+       E_CONFLICT_RESOLUTION_KEEP_LOCAL,
+       E_CONFLICT_RESOLUTION_WRITE_COPY
+} EConflictResolution;
+
 #endif /* E_BACKEND_ENUMS_H */
diff --git a/src/libebackend/e-backend.c b/src/libebackend/e-backend.c
index 389b9b4..5c35f50 100644
--- a/src/libebackend/e-backend.c
+++ b/src/libebackend/e-backend.c
@@ -133,7 +133,10 @@ backend_update_online_state_timeout_cb (gpointer user_data)
        if (current_source && g_source_is_destroyed (current_source))
                return FALSE;
 
-       backend = E_BACKEND (user_data);
+       backend = g_weak_ref_get (user_data);
+       if (!backend)
+               return FALSE;
+
        connectable = e_backend_ref_connectable (backend);
 
        g_mutex_lock (&backend->priv->update_online_state_lock);
@@ -180,8 +183,8 @@ backend_update_online_state_timeout_cb (gpointer user_data)
                g_mutex_unlock (&backend->priv->network_monitor_cancellable_lock);
        }
 
-       if (connectable != NULL)
-               g_object_unref (connectable);
+       g_clear_object (&connectable);
+       g_clear_object (&backend);
 
        return FALSE;
 }
@@ -211,7 +214,7 @@ backend_update_online_state (EBackend *backend)
        g_source_set_callback (
                timeout_source,
                backend_update_online_state_timeout_cb,
-               backend, (GDestroyNotify) g_object_unref);
+               e_weak_ref_new (backend), (GDestroyNotify) e_weak_ref_free);
        g_source_attach (timeout_source, main_context);
        backend->priv->update_online_state =
                g_source_ref (timeout_source);
@@ -220,6 +223,8 @@ backend_update_online_state (EBackend *backend)
        g_main_context_unref (main_context);
 
        g_mutex_unlock (&backend->priv->update_online_state_lock);
+
+       g_object_unref (backend);
 }
 
 static void
@@ -314,6 +319,7 @@ backend_source_authenticate_thread (gpointer user_data)
                ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
 
                switch (auth_result) {
+               case E_SOURCE_AUTHENTICATION_UNKNOWN:
                case E_SOURCE_AUTHENTICATION_ERROR:
                        reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
                        break;
diff --git a/src/libebackend/e-cache.c b/src/libebackend/e-cache.c
new file mode 100644
index 0000000..f35e0f0
--- /dev/null
+++ b/src/libebackend/e-cache.c
@@ -0,0 +1,3068 @@
+/* -*- 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-cache
+ * @include: libebackend/libebackend.h
+ * @short_description: An SQLite data cache
+ *
+ * The #ECache is an abstract class which consists of the common
+ * parts which can be used by its descendants. It also allows
+ * storing offline state for the stored objects.
+ *
+ * The API is thread safe, with special considerations to be made
+ * around e_cache_lock() and e_cache_unlock() for
+ * the sake of isolating transactions across threads.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <errno.h>
+#include <sqlite3.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include <camel/camel.h>
+
+#include "e-sqlite3-vfs.h"
+
+#include "e-cache.h"
+
+#define E_CACHE_KEY_VERSION    "version"
+#define E_CACHE_KEY_REVISION   "revision"
+
+/* The number of SQLite virtual machine instructions that are
+ * evaluated at a time, the user passed GCancellable is
+ * checked between each batch of evaluated instructions.
+ */
+#define E_CACHE_CANCEL_BATCH_SIZE      200
+
+/* How many rows to read when e_cache_foreach_update() */
+#define E_CACHE_UPDATE_BATCH_SIZE      100
+
+struct _ECachePrivate {
+       gchar *filename;
+       sqlite3 *db;
+
+       GRecMutex lock;                 /* Main API lock */
+       guint32 in_transaction;         /* Nested transaction counter */
+       ECacheLockType lock_type;       /* The lock type acquired for the current transaction */
+       GCancellable *cancellable;      /* User passed GCancellable, we abort an operation if cancelled */
+
+       guint32 revision_change_frozen;
+       gint revision_counter;
+       gint64 last_revision_time;
+       gboolean needs_revision_change;
+};
+
+enum {
+       BEFORE_PUT,
+       BEFORE_REMOVE,
+       REVISION_CHANGED,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_QUARK (e-cache-error-quark, e_cache_error)
+
+G_DEFINE_ABSTRACT_TYPE (ECache, e_cache, G_TYPE_OBJECT)
+
+G_DEFINE_BOXED_TYPE (ECacheColumnValues, e_cache_column_values, e_cache_column_values_copy, 
e_cache_column_values_free)
+G_DEFINE_BOXED_TYPE (ECacheOfflineChange, e_cache_offline_change, e_cache_offline_change_copy, 
e_cache_offline_change_free)
+G_DEFINE_BOXED_TYPE (ECacheColumnInfo, e_cache_column_info, e_cache_column_info_copy, 
e_cache_column_info_free)
+
+/**
+ * e_cache_column_values_new:
+ *
+ * Creates a new #ECacheColumnValues to store values for additional columns.
+ * The column names are compared case insensitively.
+ *
+ * Returns: (transfer full): a new #ECacheColumnValues. Free with e_cache_column_values_free(),
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnValues *
+e_cache_column_values_new (void)
+{
+       return (ECacheColumnValues *) g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, 
g_free);
+}
+
+/**
+ * e_cache_column_values_copy:
+ * @other_columns: (nullable): an #ECacheColumnValues
+ *
+ * Returns: (transfer full): Copy of the @other_columns. Free with
+ *    e_cache_column_values_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnValues *
+e_cache_column_values_copy (ECacheColumnValues *other_columns)
+{
+       GHashTableIter iter;
+       gpointer name, value;
+       ECacheColumnValues *copy;
+
+       if (!other_columns)
+               return NULL;
+
+       copy = e_cache_column_values_new ();
+
+       e_cache_column_values_init_iter (other_columns, &iter);
+       while (g_hash_table_iter_next (&iter, &name, &value)) {
+               e_cache_column_values_put (copy, name, value);
+       }
+
+       return copy;
+}
+
+/**
+ * e_cache_column_values_free:
+ * @other_columns: (nullable): an #ECacheColumnValues
+ *
+ * Frees previously allocated @other_columns with
+ * e_cache_column_values_new() or e_cache_column_values_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_free (ECacheColumnValues *other_columns)
+{
+       if (other_columns)
+               g_hash_table_destroy ((GHashTable *) other_columns);
+}
+
+/**
+ * e_cache_column_values_put:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ * @value: (nullable): a column value
+ *
+ * Puts the @value for column @name. If contains a value for the same
+ * column, then it is replaced. This creates a copy of both @name
+ * and @value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_put (ECacheColumnValues *other_columns,
+                          const gchar *name,
+                          const gchar *value)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_if_fail (other_columns != NULL);
+       g_return_if_fail (name != NULL);
+
+       g_hash_table_insert (hash_table, g_strdup (name), g_strdup (value));
+}
+
+/**
+ * e_cache_column_values_take_value:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ * @value: (nullable) (in) (transfer full): a column value
+ *
+ * Puts the @value for column @name. If contains a value for the same
+ * column, then it is replaced. This creates a copy of the @name, but
+ * takes owner ship of the @value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_take_value (ECacheColumnValues *other_columns,
+                                 const gchar *name,
+                                 gchar *value)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_if_fail (other_columns != NULL);
+       g_return_if_fail (name != NULL);
+
+       g_hash_table_insert (hash_table, g_strdup (name), value);
+}
+
+/**
+ * e_cache_column_values_take:
+ * @other_columns: an #ECacheColumnValues
+ * @name: (in) (transfer full): a column name
+ * @value: (nullable) (in) (transfer full): a column value
+ *
+ * Puts the @value for column @name. If contains a value for the same
+ * column, then it is replaced. This creates takes ownership of both
+ * the @name and the @value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_take (ECacheColumnValues *other_columns,
+                           gchar *name,
+                           gchar *value)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_if_fail (other_columns != NULL);
+       g_return_if_fail (name != NULL);
+
+       g_hash_table_insert (hash_table, name, value);
+}
+
+/**
+ * e_cache_column_values_contains:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ *
+ * Returns: Whether @other_columns contains column named @name.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_column_values_contains (ECacheColumnValues *other_columns,
+                               const gchar *name)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_val_if_fail (other_columns != NULL, FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       return g_hash_table_contains (hash_table, name);
+}
+
+/**
+ * e_cache_column_values_remove:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ *
+ * Removes value for the column named @name from @other_columns.
+ *
+ * Returns: Whether such column existed and had been removed.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_column_values_remove (ECacheColumnValues *other_columns,
+                             const gchar *name)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_val_if_fail (other_columns != NULL, FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       return g_hash_table_remove (hash_table, name);
+}
+
+/**
+ * e_cache_column_values_remove_all:
+ * @other_columns: an #ECacheColumnValues
+ *
+ * Removes all values from the @other_columns, leaving it empty.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_remove_all (ECacheColumnValues *other_columns)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_if_fail (other_columns != NULL);
+
+       g_hash_table_remove_all (hash_table);
+}
+
+/**
+ * e_cache_column_values_lookup:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ *
+ * Looks up currently stored value for the column named @name.
+ * As the values can be %NULL one cannot distinguish between
+ * a column which doesn't have stored any value and a column
+ * which has stored %NULL value. Use e_cache_column_values_contains()
+ * to check whether such column exitst in the @other_columns.
+ * The returned pointer is owned by @other_columns and is valid until
+ * the value is overwritten of the @other_columns freed.
+ *
+ * Returns: Stored value for the column named @name, or %NULL, if
+ *    no such column values is stored.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cache_column_values_lookup (ECacheColumnValues *other_columns,
+                             const gchar *name)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_val_if_fail (other_columns != NULL, NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       return g_hash_table_lookup (hash_table, name);
+}
+
+/**
+ * e_cache_column_values_get_size:
+ * @other_columns: an #ECacheColumnValues
+ *
+ * Returns: How many columns are stored in the @other_columns.
+ *
+ * Since: 3.26
+ **/
+guint
+e_cache_column_values_get_size (ECacheColumnValues *other_columns)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_val_if_fail (other_columns != NULL, 0);
+
+       return g_hash_table_size (hash_table);
+}
+
+/**
+ * e_cache_column_values_init_iter:
+ * @other_columns: an #ECacheColumnValues
+ * @iter: a #GHashTableIter
+ *
+ * Initialized the @iter, thus the @other_columns can be traversed
+ * with g_hash_table_iter_next(). The key is a column name and
+ * the value is the corresponding column value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_init_iter (ECacheColumnValues *other_columns,
+                                GHashTableIter *iter)
+{
+       GHashTable *hash_table = (GHashTable *) other_columns;
+
+       g_return_if_fail (other_columns != NULL);
+       g_return_if_fail (iter != NULL);
+
+       g_hash_table_iter_init (iter, hash_table);
+}
+
+/**
+ * e_cache_offline_change_new:
+ * @uid: a unique object identifier
+ * @revision: (nullable): a revision of the object
+ * @object: (nullable): object itself
+ * @state: an #EOfflineState
+ *
+ * Creates a new #ECacheOfflineChange with the offline @state
+ * information for the given @uid.
+ *
+ * Returns: (transfer full): A new #ECacheOfflineChange. Free it with
+ *    e_cache_offline_change_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheOfflineChange *
+e_cache_offline_change_new (const gchar *uid,
+                           const gchar *revision,
+                           const gchar *object,
+                           EOfflineState state)
+{
+       ECacheOfflineChange *change;
+
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       change = g_new0 (ECacheOfflineChange, 1);
+       change->uid = g_strdup (uid);
+       change->revision = g_strdup (revision);
+       change->object = g_strdup (object);
+       change->state = state;
+
+       return change;
+}
+
+/**
+ * e_cache_offline_change_copy:
+ * @change: (nullable): a source #ECacheOfflineChange to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @change. Free it with
+ *    e_cache_offline_change_free() when no longer needed.
+ *    If the @change is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECacheOfflineChange *
+e_cache_offline_change_copy (const ECacheOfflineChange *change)
+{
+       if (!change)
+               return NULL;
+
+       return e_cache_offline_change_new (change->uid, change->revision, change->object, change->state);
+}
+
+/**
+ * e_cache_offline_change_free:
+ * @change: (nullable): an #ECacheOfflineChange
+ *
+ * Frees the @change structure, previously allocated with e_cache_offline_change_new()
+ * or e_cache_offline_change_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_offline_change_free (gpointer change)
+{
+       ECacheOfflineChange *chng = change;
+
+       if (chng) {
+               g_free (chng->uid);
+               g_free (chng->revision);
+               g_free (chng->object);
+               g_free (chng);
+       }
+}
+
+/**
+ * e_cache_column_info_new:
+ * @name: a column name
+ * @type: a column type
+ * @index_name: (nullable): an index name for this column, or %NULL
+ *
+ * Returns: (transfer full): A new #ECacheColumnInfo. Free it with
+ *    e_cache_column_info_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnInfo *
+e_cache_column_info_new (const gchar *name,
+                        const gchar *type,
+                        const gchar *index_name)
+{
+       ECacheColumnInfo *info;
+
+       g_return_val_if_fail (name != NULL, NULL);
+       g_return_val_if_fail (type != NULL, NULL);
+
+       info = g_new0 (ECacheColumnInfo, 1);
+       info->name = g_strdup (name);
+       info->type = g_strdup (type);
+       info->index_name = g_strdup (index_name);
+
+       return info;
+}
+
+/**
+ * e_cache_column_info_copy:
+ * @info: (nullable): a source #ECacheColumnInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @info. Free it with
+ *    e_cache_column_info_free() when no longer needed.
+ *    If the @info is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnInfo *
+e_cache_column_info_copy (const ECacheColumnInfo *info)
+{
+       if (!info)
+               return NULL;
+
+       return e_cache_column_info_new (info->name, info->type, info->index_name);
+}
+
+/**
+ * e_cache_column_info_free:
+ * @info: (nullable): an #ECacheColumnInfo
+ *
+ * Frees the @info structure, previously allocated with e_cache_column_info_new()
+ * or e_cache_column_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_info_free (gpointer info)
+{
+       ECacheColumnInfo *nfo = info;
+
+       if (nfo) {
+               g_free (nfo->name);
+               g_free (nfo->type);
+               g_free (nfo->index_name);
+               g_free (nfo);
+       }
+}
+
+#define E_CACHE_SET_ERROR_FROM_SQLITE(error, code, message, stmt) \
+       G_STMT_START { \
+               if (code == SQLITE_CONSTRAINT) { \
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT, message); \
+               } else if (code == SQLITE_ABORT) { \
+                       g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Operation cancelled: %s", 
message); \
+               } else { \
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, \
+                               "SQLite error code '%d': %s (statement:%s)", code, message, stmt); \
+               } \
+       } G_STMT_END
+
+struct CacheSQLiteExecData {
+       ECache *cache;
+       ECacheSelectFunc callback;
+       gpointer user_data;
+};
+
+static gint
+e_cache_sqlite_exec_cb (gpointer user_data,
+                       gint ncols,
+                       gchar **column_values,
+                       gchar **column_names)
+{
+       struct CacheSQLiteExecData *cse = user_data;
+
+       g_return_val_if_fail (cse != NULL, SQLITE_MISUSE);
+       g_return_val_if_fail (cse->callback != NULL, SQLITE_MISUSE);
+
+       if (!cse->callback (cse->cache, ncols, (const gchar **) column_names, (const gchar **) column_values, 
cse->user_data))
+               return SQLITE_ABORT;
+
+       return SQLITE_OK;
+}
+
+static gboolean
+e_cache_sqlite_exec_internal (ECache *cache,
+                             const gchar *stmt,
+                             ECacheSelectFunc callback,
+                             gpointer user_data,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       struct CacheSQLiteExecData cse;
+       GCancellable *previous_cancellable;
+       gchar *errmsg = NULL;
+       gint ret = -1, retries = 0;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (stmt != NULL, FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       previous_cancellable = cache->priv->cancellable;
+       if (cancellable)
+               cache->priv->cancellable = cancellable;
+
+       cse.cache = cache;
+       cse.callback = callback;
+       cse.user_data = user_data;
+
+       ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, &errmsg);
+
+       while (ret == SQLITE_BUSY || ret == SQLITE_LOCKED || ret == -1) {
+               /* try for ~15 seconds, then give up */
+               if (retries > 150)
+                       break;
+               retries++;
+
+               if (errmsg) {
+                       sqlite3_free (errmsg);
+                       errmsg = NULL;
+               }
+               g_thread_yield ();
+               g_usleep (100 * 1000); /* Sleep for 100 ms */
+
+               ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, 
&errmsg);
+       }
+
+       cache->priv->cancellable = previous_cancellable;
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       if (ret != SQLITE_OK) {
+               E_CACHE_SET_ERROR_FROM_SQLITE (error, ret, errmsg, stmt);
+               sqlite3_free (errmsg);
+               return FALSE;
+       }
+
+       if (errmsg)
+               sqlite3_free (errmsg);
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_sqlite_exec_printf (ECache *cache,
+                           const gchar *format,
+                           ECacheSelectFunc callback,
+                           gpointer user_data,
+                           GCancellable *cancellable,
+                           GError **error,
+                           ...)
+{
+       gboolean success;
+       va_list args;
+       gchar *stmt;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (format != NULL, FALSE);
+
+       va_start (args, error);
+       stmt = sqlite3_vmprintf (format, args);
+
+       success = e_cache_sqlite_exec_internal (cache, stmt, callback, user_data, cancellable, error);
+
+       sqlite3_free (stmt);
+       va_end (args);
+
+       return success;
+}
+
+static gboolean
+e_cache_read_key_value (ECache *cache,
+                       gint ncols,
+                       const gchar **column_names,
+                       const gchar **column_values,
+                       gpointer user_data)
+{
+       gchar **pvalue = user_data;
+
+       g_return_val_if_fail (ncols == 1, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+       g_return_val_if_fail (pvalue != NULL, FALSE);
+
+       if (!*pvalue)
+               *pvalue = g_strdup (column_values[0]);
+
+       return TRUE;
+}
+
+static gchar *
+e_cache_build_user_key (const gchar *key)
+{
+       return g_strconcat ("user::", key, NULL);
+}
+
+static gboolean
+e_cache_set_key_internal (ECache *cache,
+                         gboolean is_user_key,
+                         const gchar *key,
+                         const gchar *value,
+                         GError **error)
+{
+       gchar *tmp = NULL;
+       const gchar *usekey;
+       gboolean success;
+
+       if (is_user_key) {
+               tmp = e_cache_build_user_key (key);
+               usekey = tmp;
+       } else {
+               usekey = key;
+       }
+
+       if (value) {
+               success = e_cache_sqlite_exec_printf (cache,
+                       "INSERT or REPLACE INTO " E_CACHE_TABLE_KEYS " (key, value) VALUES (%Q, %Q)",
+                       NULL, NULL, NULL, error,
+                       usekey, value);
+       } else {
+               success = e_cache_sqlite_exec_printf (cache,
+                       "DELETE FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q",
+                       NULL, NULL, NULL, error,
+                       usekey);
+       }
+
+       g_free (tmp);
+
+       return success;
+}
+
+static gchar *
+e_cache_dup_key_internal (ECache *cache,
+                         gboolean is_user_key,
+                         const gchar *key,
+                         GError **error)
+{
+       gchar *tmp = NULL;
+       const gchar *usekey;
+       gchar *value = NULL;
+
+       if (is_user_key) {
+               tmp = e_cache_build_user_key (key);
+               usekey = tmp;
+       } else {
+               usekey = key;
+       }
+
+       if (!e_cache_sqlite_exec_printf (cache,
+               "SELECT value FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q",
+               e_cache_read_key_value, &value, NULL, error,
+               usekey)) {
+               g_warn_if_fail (value == NULL);
+       }
+
+       g_free (tmp);
+
+       return value;
+}
+
+static gint
+e_cache_check_cancelled_cb (gpointer user_data)
+{
+       ECache *cache = user_data;
+
+       /* Do not use E_IS_CACHE() here, for performance reasons */
+       g_return_val_if_fail (cache != NULL, SQLITE_ABORT);
+
+       if (cache->priv->cancellable &&
+           g_cancellable_is_cancelled (cache->priv->cancellable)) {
+               return SQLITE_ABORT;
+       }
+
+       return SQLITE_OK;
+}
+
+static gboolean
+e_cache_init_sqlite (ECache *cache,
+                    const gchar *filename,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       gint ret;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (filename != NULL, FALSE);
+       g_return_val_if_fail (cache->priv->filename == NULL, FALSE);
+
+       cache->priv->filename = g_strdup (filename);
+
+       ret = sqlite3_open (filename, &cache->priv->db);
+       if (ret != SQLITE_OK) {
+               if (!cache->priv->db) {
+                       g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Out of memory"));
+               } else {
+                       const gchar *errmsg = sqlite3_errmsg (cache->priv->db);
+
+                       g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE,
+                               _("Can't open database %s: %s"), filename, errmsg);
+
+                       sqlite3_close (cache->priv->db);
+                       cache->priv->db = NULL;
+               }
+
+               return FALSE;
+       }
+
+       /* Handle GCancellable */
+       sqlite3_progress_handler (
+               cache->priv->db,
+               E_CACHE_CANCEL_BATCH_SIZE,
+               e_cache_check_cancelled_cb,
+               cache);
+
+       return e_cache_sqlite_exec_internal (cache, "ATTACH DATABASE ':memory:' AS mem", NULL, NULL, 
cancellable, error) &&
+               e_cache_sqlite_exec_internal (cache, "PRAGMA foreign_keys = ON",          NULL, NULL, 
cancellable, error) &&
+               e_cache_sqlite_exec_internal (cache, "PRAGMA case_sensitive_like = ON",   NULL, NULL, 
cancellable, error);
+}
+
+static gboolean
+e_cache_garther_column_names_cb (ECache *cache,
+                                gint ncols,
+                                const gchar *column_names[],
+                                const gchar *column_values[],
+                                gpointer user_data)
+{
+       GHashTable *known_columns = user_data;
+       gint ii;
+
+       g_return_val_if_fail (known_columns != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       for (ii = 0; ii < ncols; ii++) {
+               if (column_names[ii] && camel_strcase_equal (column_names[ii], "name")) {
+                       if (column_values[ii])
+                               g_hash_table_insert (known_columns, g_strdup (column_values[ii]), NULL);
+                       break;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_init_tables (ECache *cache,
+                    const GSList *other_columns,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       GHashTable *known_columns;
+       GString *objects_stmt;
+       const GSList *link;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (cache->priv->db != NULL, FALSE);
+
+       if (!e_cache_sqlite_exec_internal (cache,
+               "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_KEYS " ("
+               "key TEXT PRIMARY KEY,"
+               "value TEXT)",
+               NULL, NULL, cancellable, error)) {
+               return FALSE;
+       }
+
+       objects_stmt = g_string_new ("");
+
+       g_string_append (objects_stmt, "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_OBJECTS " ("
+               E_CACHE_COLUMN_UID " TEXT PRIMARY KEY,"
+               E_CACHE_COLUMN_REVISION " TEXT,"
+               E_CACHE_COLUMN_OBJECT " TEXT,"
+               E_CACHE_COLUMN_STATE " INTEGER");
+
+       for (link = other_columns; link; link = g_slist_next (link)) {
+               const ECacheColumnInfo *info = link->data;
+
+               if (!info)
+                       continue;
+
+               g_string_append (objects_stmt, ",");
+               g_string_append (objects_stmt, info->name);
+               g_string_append (objects_stmt, " ");
+               g_string_append (objects_stmt, info->type);
+       }
+
+       g_string_append (objects_stmt, ")");
+
+       if (!e_cache_sqlite_exec_internal (cache, objects_stmt->str, NULL, NULL, cancellable, error)) {
+               g_string_free (objects_stmt, TRUE);
+
+               return FALSE;
+       }
+
+       g_string_free (objects_stmt, TRUE);
+
+       /* Verify that all other columns are there and remove those unused */
+       known_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL);
+
+       if (!e_cache_sqlite_exec_internal (cache, "PRAGMA table_info (" E_CACHE_TABLE_OBJECTS ")",
+               e_cache_garther_column_names_cb, known_columns, cancellable, error)) {
+               g_string_free (objects_stmt, TRUE);
+
+               return FALSE;
+       }
+
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_UID);
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_REVISION);
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_OBJECT);
+       g_hash_table_remove (known_columns, E_CACHE_COLUMN_STATE);
+
+       for (link = other_columns; link; link = g_slist_next (link)) {
+               const ECacheColumnInfo *info = link->data;
+
+               if (!info)
+                       continue;
+
+               if (g_hash_table_remove (known_columns, info->name))
+                       continue;
+
+               if (!e_cache_sqlite_exec_printf (cache,
+                       "ALTER TABLE " E_CACHE_TABLE_OBJECTS " ADD COLUMN %Q %s",
+                       NULL, NULL, cancellable, error,
+                       info->name, info->type)) {
+                       g_hash_table_destroy (known_columns);
+
+                       return FALSE;
+               }
+       }
+
+       g_hash_table_destroy (known_columns);
+
+       for (link = other_columns; link; link = g_slist_next (link)) {
+               const ECacheColumnInfo *info = link->data;
+
+               if (!info || !info->index_name)
+                       continue;
+
+               if (!e_cache_sqlite_exec_printf (cache,
+                       "CREATE INDEX IF NOT EXISTS %Q ON " E_CACHE_TABLE_OBJECTS " (%s)",
+                       NULL, NULL, cancellable, error,
+                       info->index_name, info->name)) {
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_initialize_sync:
+ * @cache: an #ECache
+ * @filename: a filename of an SQLite database to use
+ * @other_columns: (element-type ECacheColumnInfo) (nullable): an optional
+ *    #GSList with additional columns to add to the objects table
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Initializes the @cache and opens the @filename database.
+ * This should be called by the descendant.
+ *
+ * The @other_columns are added to the objects table (@E_CACHE_TABLE_OBJECTS).
+ * Values for these columns are returned by e_cache_get()
+ * and can be stored with e_cache_put().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_initialize_sync (ECache *cache,
+                        const gchar *filename,
+                        const GSList *other_columns,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       gchar *dirname;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (cache->priv->filename == NULL, FALSE);
+
+       /* Ensure existance of the directories leading up to 'filename' */
+       dirname = g_path_get_dirname (filename);
+       if (g_mkdir_with_parents (dirname, 0777) < 0) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD,
+                       _("Can not make parent directory: %s"),
+                       g_strerror (errno));
+               g_free (dirname);
+
+               return FALSE;
+       }
+
+       g_free (dirname);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       success = e_cache_init_sqlite (cache, filename, cancellable, error) &&
+               e_cache_init_tables (cache, other_columns, cancellable, error);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return success;
+}
+
+/**
+ * e_cache_get_filename:
+ * @cache: an #ECache
+ *
+ * Returns: a filename of the @cache, with which it had been initialized.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cache_get_filename (ECache *cache)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       return cache->priv->filename;
+}
+
+/**
+ * e_cache_get_version:
+ * @cache: an #ECache
+ *
+ * Returns: A cache data version. This is meant to be used by the descendants.
+ *
+ * Since: 3.26
+ **/
+gint
+e_cache_get_version (ECache *cache)
+{
+       gchar *value;
+       gint version = -1;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), -1);
+
+       value = e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, NULL);
+
+       if (value) {
+               version = g_ascii_strtoll (value, NULL, 10);
+               g_free (value);
+       }
+
+       return version;
+}
+
+/**
+ * e_cache_set_version:
+ * @cache: an #ECache
+ * @version: a cache data version to set
+ *
+ * Sets a cache data version. This is meant to be used by the descendants.
+ * The @version should be greater than zero.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_set_version (ECache *cache,
+                    gint version)
+{
+       gchar *value;
+
+       g_return_if_fail (E_IS_CACHE (cache));
+       g_return_if_fail (version > 0);
+
+       value = g_strdup_printf ("%d", version);
+       e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, value, NULL);
+       g_free (value);
+}
+
+/**
+ * e_cache_dup_revision:
+ * @cache: an #ECache
+ *
+ * Returns: (transfer full): A revision of the whole @cache. This is meant to be
+ *    used by the descendants. Free the returned pointer with g_free(), when no
+ *    longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_dup_revision (ECache *cache)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       return e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, NULL);
+}
+
+/**
+ * e_cache_set_revision:
+ * @cache: an #ECache
+ * @revision: (nullable): a revision to set; use %NULL to unset it
+ *
+ * Sets the @revision of the whole @cache. This is not meant to be
+ * used by the descendants, because the revision is updated automatically
+ * when needed. The descendants can listen to "revision-changed" signal.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_set_revision (ECache *cache,
+                     const gchar *revision)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, revision, NULL);
+
+       g_signal_emit (cache, signals[REVISION_CHANGED], 0, NULL);
+}
+
+/**
+ * e_cache_change_revision:
+ * @cache: an #ECache
+ *
+ * Instructs the @cache to change its revision. In case the revision
+ * change is frozen with e_cache_freeze_revision_change() it notes to
+ * change the revision once the revision change is fully thaw.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_change_revision (ECache *cache)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       if (e_cache_is_revision_change_frozen (cache)) {
+               cache->priv->needs_revision_change = TRUE;
+       } else {
+               gchar time_string[100] = { 0 };
+               const struct tm *tm = NULL;
+               time_t t;
+               gint64 revision_time;
+               gchar *revision;
+
+               revision_time = g_get_real_time () / (1000 * 1000);
+               t = (time_t) revision_time;
+
+               if (revision_time != cache->priv->last_revision_time) {
+                       cache->priv->revision_counter = 0;
+                       cache->priv->last_revision_time = revision_time;
+               }
+
+               tm = gmtime (&t);
+               if (tm)
+                       strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", tm);
+
+               revision = g_strdup_printf ("%s(%d)", time_string, cache->priv->revision_counter++);
+
+               e_cache_set_revision (cache, revision);
+
+               g_free (revision);
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_freeze_revision_change:
+ * @cache: an #ECache
+ *
+ * Freezes automatic revision change for the @cache. The function
+ * can be called multiple times, but each such call requires its
+ * pair function e_cache_thaw_revision_change() call. See also
+ * e_cache_change_revision().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_freeze_revision_change (ECache *cache)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       cache->priv->revision_change_frozen++;
+       g_warn_if_fail (cache->priv->revision_change_frozen != 0);
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_thaw_revision_change:
+ * @cache: an #ECache
+ *
+ * Thaws automatic revision change for the @cache. It's the pair
+ * function of e_cache_freeze_revision_change().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_thaw_revision_change (ECache *cache)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       if (!cache->priv->revision_change_frozen) {
+               g_warn_if_fail (cache->priv->revision_change_frozen > 0);
+       } else {
+               cache->priv->revision_change_frozen--;
+               if (!cache->priv->revision_change_frozen &&
+                   cache->priv->needs_revision_change) {
+                       cache->priv->needs_revision_change = FALSE;
+                       e_cache_change_revision (cache);
+               }
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_is_revision_change_frozen:
+ * @cache: an #ECache
+ *
+ * Returns: Whether automatic revision change for the @cache
+ *    is currently frozen.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_is_revision_change_frozen (ECache *cache)
+{
+       gboolean frozen;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+       frozen = cache->priv->revision_change_frozen > 0;
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       return frozen;
+}
+
+/**
+ * e_cache_erase:
+ * @cache: an #ECache
+ *
+ * Erases the cache and all of its content from the disk.
+ * The only valid operation after this is to free the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_erase (ECache *cache)
+{
+       ECacheClass *klass;
+
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       if (!cache->priv->db)
+               return;
+
+       klass = E_CACHE_GET_CLASS (cache);
+       g_return_if_fail (klass != NULL);
+
+       if (klass->erase)
+               klass->erase (cache);
+
+       sqlite3_close (cache->priv->db);
+       cache->priv->db = NULL;
+
+       g_unlink (cache->priv->filename);
+
+       g_free (cache->priv->filename);
+       cache->priv->filename = NULL;
+}
+
+static gboolean
+e_cache_count_rows_cb (ECache *cache,
+                      gint ncols,
+                      const gchar **column_names,
+                      const gchar **column_values,
+                      gpointer user_data)
+{
+       guint *pnrows = user_data;
+
+       g_return_val_if_fail (pnrows != NULL, FALSE);
+
+       *pnrows = (*pnrows) + 1;
+
+       return TRUE;
+}
+
+/**
+ * e_cache_contains:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ *
+ * Checkes whether the @cache contains an object with
+ * the given @uid.
+ *
+ * Returns: Whether the the object had been found.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_contains (ECache *cache,
+                 const gchar *uid,
+                 ECacheDeletedFlag deleted_flag)
+{
+       guint nrows = 0;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_UID " = %Q"
+                       " LIMIT 2",
+                       e_cache_count_rows_cb, &nrows, NULL, NULL,
+                       uid);
+       } else {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d"
+                       " LIMIT 2",
+                       e_cache_count_rows_cb, &nrows, NULL, NULL,
+                       uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+       }
+
+       g_warn_if_fail (nrows <= 1);
+
+       return nrows > 0;
+}
+
+struct GetObjectData {
+       gchar *object;
+       gchar **out_revision;
+       ECacheColumnValues **out_other_columns;
+};
+
+static gboolean
+e_cache_get_object_cb (ECache *cache,
+                      gint ncols,
+                      const gchar **column_names,
+                      const gchar **column_values,
+                      gpointer user_data)
+{
+       struct GetObjectData *gd = user_data;
+       gint ii;
+
+       g_return_val_if_fail (gd != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       for (ii = 0; ii < ncols; ii++) {
+               if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0 ||
+                   g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) {
+                       /* Skip these two */
+               } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) {
+                       if (gd->out_revision)
+                               *gd->out_revision = g_strdup (column_values[ii]);
+               } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) {
+                       gd->object = g_strdup (column_values[ii]);
+               } else if (gd->out_other_columns) {
+                       if (!*gd->out_other_columns)
+                               *gd->out_other_columns = e_cache_column_values_new ();
+
+                       e_cache_column_values_put (*gd->out_other_columns, column_names[ii], 
column_values[ii]);
+               } else if (gd->object && (!gd->out_revision || *gd->out_revision)) {
+                       /* Short-break the cycle when the other columns are not requested and
+                          the object/revision values were already read. */
+                       break;
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @out_revision: (out) (nullable) (transfer full): an out variable for a revision
+ *    of the object, or %NULL to ignore
+ * @out_other_columns: (out) (nullable) (transfer full): an out
+ *    variable for #ECacheColumnValues other columns, as defined when creating the @cache, or %NULL to ignore
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns an object with the given @uid. This function does not consider locally
+ * deleted objects. The @out_revision is set to the object revision, if not %NULL.
+ * Free it with g_free() when no longer needed. Similarly the @out_other_columns
+ * contains a column name to column value strings for additional columns which had
+ * been requested when calling e_cache_initialize_sync(), if not %NULL.
+ * Free the returned #ECacheColumnValues with e_cache_column_values_free(), when
+ * no longer needed.
+ *
+ * Returns: (nullable) (transfer full): An object with the given @uid. Free it
+ *    with g_free(), when no longer needed. Returns %NULL on error, like when
+ *    the object could not be found.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_get (ECache *cache,
+            const gchar *uid,
+            gchar **out_revision,
+            ECacheColumnValues **out_other_columns,
+            GCancellable *cancellable,
+            GError **error)
+{
+       struct GetObjectData gd;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       if (out_revision)
+               *out_revision = NULL;
+
+       if (out_other_columns)
+               *out_other_columns = NULL;
+
+       gd.object = NULL;
+       gd.out_revision = out_revision;
+       gd.out_other_columns = out_other_columns;
+
+       if (e_cache_sqlite_exec_printf (cache,
+               "SELECT * FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d",
+               e_cache_get_object_cb, &gd, cancellable, error,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED) &&
+           !gd.object) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+       }
+
+       return gd.object;
+}
+
+static gboolean
+e_cache_put_locked (ECache *cache,
+                   const gchar *uid,
+                   const gchar *revision,
+                   const gchar *object,
+                   ECacheColumnValues *other_columns,
+                   EOfflineState offline_state,
+                   gboolean is_replace,
+                   GCancellable *cancellable,
+                   GError **error)
+{
+       ECacheColumnValues *my_other_columns = NULL;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       if (!other_columns) {
+               my_other_columns = e_cache_column_values_new ();
+               other_columns = my_other_columns;
+       }
+
+       g_signal_emit (cache,
+                      signals[BEFORE_PUT],
+                      0,
+                      uid, revision, object, other_columns,
+                      is_replace, cancellable, error,
+                      &success);
+
+       if (success) {
+               ECacheClass *klass;
+
+               klass = E_CACHE_GET_CLASS (cache);
+               g_return_val_if_fail (klass != NULL, FALSE);
+               g_return_val_if_fail (klass->put_locked != NULL, FALSE);
+
+               success = klass->put_locked (cache, uid, revision, object, other_columns, offline_state, 
is_replace, cancellable, error);
+
+               if (success)
+                       e_cache_change_revision (cache);
+       }
+
+       e_cache_column_values_free (my_other_columns);
+
+       return success;
+}
+
+/**
+ * e_cache_put:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @revision: (nullable): a revision of the object
+ * @object: the object itself
+ * @other_columns: (nullable): an #ECacheColumnValues with other columns to set; can be %NULL
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting this object in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Stores an object into the cache. Depending on @offline_flag, this update
+ * the object's offline state accordingly. When the @offline_flag is set
+ * to %E_CACHE_IS_ONLINE, then it's set to #E_OFFLINE_STATE_SYNCED, like
+ * to be fully synchronized with the server, regardless of its previous
+ * offline state. Overwriting locally deleted object behaves like an addition
+ * of a completely new object.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_put (ECache *cache,
+            const gchar *uid,
+            const gchar *revision,
+            const gchar *object,
+            ECacheColumnValues *other_columns,
+            ECacheOfflineFlag offline_flag,
+            GCancellable *cancellable,
+            GError **error)
+{
+       EOfflineState offline_state;
+       gboolean success = TRUE, is_replace;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       if (offline_flag == E_CACHE_IS_ONLINE) {
+               is_replace = e_cache_contains (cache, uid, E_CACHE_EXCLUDE_DELETED);
+               offline_state = E_OFFLINE_STATE_SYNCED;
+       } else {
+               is_replace = e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED);
+               if (is_replace) {
+                       GError *local_error = NULL;
+
+                       offline_state = e_cache_get_offline_state (cache, uid, cancellable, &local_error);
+
+                       if (local_error) {
+                               success = FALSE;
+                               g_propagate_error (error, local_error);
+                       } else if (offline_state != E_OFFLINE_STATE_LOCALLY_CREATED) {
+                               offline_state = E_OFFLINE_STATE_LOCALLY_MODIFIED;
+                       }
+               } else {
+                       offline_state = E_OFFLINE_STATE_LOCALLY_CREATED;
+               }
+       }
+
+       success = success && e_cache_put_locked (cache, uid, revision, object, other_columns,
+               offline_state, is_replace, cancellable, error);
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+/**
+ * e_cache_remove:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing the object in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the object with the given @uid from the @cache. Based on the @offline_flag,
+ * it can remove also any information about locally made offline changes. Removing
+ * the object with %E_CACHE_IS_OFFLINE will still remember it for later use
+ * with e_cache_get_offline_changes().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove (ECache *cache,
+               const gchar *uid,
+               ECacheOfflineFlag offline_flag,
+               GCancellable *cancellable,
+               GError **error)
+{
+       ECacheClass *klass;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       klass = E_CACHE_GET_CLASS (cache);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->remove_locked != NULL, FALSE);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       if (offline_flag == E_CACHE_IS_ONLINE) {
+               success = klass->remove_locked (cache, uid, cancellable, error);
+       } else {
+               EOfflineState offline_state;
+
+               offline_state = e_cache_get_offline_state (cache, uid, cancellable, error);
+               if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+                       success = FALSE;
+               } else if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED) {
+                       success = klass->remove_locked (cache, uid, cancellable, error);
+               } else {
+                       g_signal_emit (cache,
+                                      signals[BEFORE_REMOVE],
+                                      0,
+                                      uid, cancellable, error,
+                                      &success);
+
+                       if (success) {
+                               success = e_cache_set_offline_state (cache, uid,
+                                       E_OFFLINE_STATE_LOCALLY_DELETED, cancellable, error);
+                       }
+               }
+       }
+
+       if (success)
+               e_cache_change_revision (cache);
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+/**
+ * e_cache_remove_all:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes all objects from the @cache in one call.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove_all (ECache *cache,
+                   GCancellable *cancellable,
+                   GError **error)
+{
+       ECacheClass *klass;
+       GSList *uids = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       klass = E_CACHE_GET_CLASS (cache);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->remove_all_locked != NULL, FALSE);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       success = e_cache_get_uids (cache, E_CACHE_INCLUDE_DELETED, &uids, NULL, cancellable, error);
+
+       if (success && uids)
+               success = klass->remove_all_locked (cache, uids, cancellable, error);
+
+       if (success) {
+               e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
+               e_cache_change_revision (cache);
+       }
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       g_slist_free_full (uids, g_free);
+
+       return success;
+}
+
+static gboolean
+e_cache_get_uint64_cb (ECache *cache,
+                      gint ncols,
+                      const gchar **column_names,
+                      const gchar **column_values,
+                      gpointer user_data)
+{
+       guint64 *pui64 = user_data;
+
+       g_return_val_if_fail (pui64 != NULL, FALSE);
+
+       if (ncols == 1) {
+               *pui64 = column_values[0] ? g_ascii_strtoull (column_values[0], NULL, 10) : 0;
+       } else {
+               *pui64 = 0;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_cache_get_int64_cb (ECache *cache,
+                     gint ncols,
+                     const gchar **column_names,
+                     const gchar **column_values,
+                     gpointer user_data)
+{
+       gint64 *pi64 = user_data;
+
+       g_return_val_if_fail (pi64 != NULL, FALSE);
+
+       if (ncols == 1) {
+               *pi64 = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0;
+       } else {
+               *pi64 = 0;
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get_count:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: Count of objects stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+guint
+e_cache_get_count (ECache *cache,
+                  ECacheDeletedFlag deleted_flag,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+       guint64 nobjects = 0;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), 0);
+
+       if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS,
+                       e_cache_get_uint64_cb, &nobjects, cancellable, error);
+       } else {
+               e_cache_sqlite_exec_printf (cache,
+                       "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS
+                       " WHERE " E_CACHE_COLUMN_STATE " != %d",
+                       e_cache_get_uint64_cb, &nobjects, NULL, NULL,
+                       E_OFFLINE_STATE_LOCALLY_DELETED);
+       }
+
+       return nobjects;
+}
+
+struct GatherRowsData {
+       GSList **out_uids;
+       GSList **out_revisions;
+       GSList **out_objects;
+};
+
+static gboolean
+e_cache_gather_rows_data_cb (ECache *cache,
+                            const gchar *uid,
+                            const gchar *revision,
+                            const gchar *object,
+                            EOfflineState offline_state,
+                            gint ncols,
+                            const gchar *column_names[],
+                            const gchar *column_values[],
+                            gpointer user_data)
+{
+       struct GatherRowsData *gd = user_data;
+
+       g_return_val_if_fail (gd != NULL, FALSE);
+
+       if (gd->out_uids)
+               *gd->out_uids = g_slist_prepend (*gd->out_uids, g_strdup (uid));
+
+       if (gd->out_revisions)
+               *gd->out_revisions = g_slist_prepend (*gd->out_revisions, g_strdup (revision));
+
+       if (gd->out_objects)
+               *gd->out_objects = g_slist_prepend (*gd->out_objects, g_strdup (object));
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get_uids:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @out_uids: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found uid to
+ * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store
+ *    the found revisions to, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of unique object identifiers stored in the @cache, optionally
+ * together with their revisions. The uids are not returned in any particular
+ * order, but the position between @out_uids and @out_revisions matches
+ * the same object.
+ *
+ * Both @out_uids and @out_revisions contain newly allocated #GSList, which
+ * should be freed with g_slist_free_full (slist, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded. It doesn't necessarily mean that there was
+ *    any object stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_get_uids (ECache *cache,
+                 ECacheDeletedFlag deleted_flag,
+                 GSList **out_uids,
+                 GSList **out_revisions,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       struct GatherRowsData gr;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (out_uids, FALSE);
+
+       gr.out_uids = out_uids;
+       gr.out_revisions = out_revisions;
+       gr.out_objects = NULL;
+
+       return e_cache_foreach (cache, deleted_flag, NULL,
+               e_cache_gather_rows_data_cb, &gr, cancellable, error);
+}
+
+/**
+ * e_cache_get_objects:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @out_objects: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found objects 
to
+ * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store
+ *    the found revisions to, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of objects stored in the @cache, optionally together with
+ * their revisions. The uids are not returned in any particular order,
+ * but the position between @out_objects and @out_revisions matches
+ * the same object.
+ *
+ * Both @out_objects and @out_revisions contain newly allocated #GSList, which
+ * should be freed with g_slist_free_full (slist, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded. It doesn't necessarily mean that there was
+ *    any object stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_get_objects (ECache *cache,
+                    ECacheDeletedFlag deleted_flag,
+                    GSList **out_objects,
+                    GSList **out_revisions,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       struct GatherRowsData gr;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (out_objects, FALSE);
+
+       gr.out_uids = NULL;
+       gr.out_revisions = out_revisions;
+       gr.out_objects = out_objects;
+
+       return e_cache_foreach (cache, deleted_flag, NULL,
+               e_cache_gather_rows_data_cb, &gr, cancellable, error);
+}
+
+struct ForeachData {
+       gint uid_index;
+       gint revision_index;
+       gint object_index;
+       gint state_index;
+       ECacheForeachFunc func;
+       gpointer user_data;
+};
+
+static gboolean
+e_cache_foreach_cb (ECache *cache,
+                   gint ncols,
+                   const gchar *column_names[],
+                   const gchar *column_values[],
+                   gpointer user_data)
+{
+       struct ForeachData *fe = user_data;
+       EOfflineState offline_state;
+
+       g_return_val_if_fail (fe != NULL, FALSE);
+       g_return_val_if_fail (fe->func != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       if (fe->uid_index == -1 ||
+           fe->revision_index == -1 ||
+           fe->object_index == -1 ||
+           fe->state_index == -1) {
+               gint ii;
+
+               for (ii = 0; ii < ncols && (fe->uid_index == -1 ||
+                    fe->revision_index == -1 ||
+                    fe->object_index == -1 ||
+                    fe->state_index == -1); ii++) {
+                       if (!column_names[ii])
+                               continue;
+
+                       if (fe->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) 
== 0) {
+                               fe->uid_index = ii;
+                       } else if (fe->revision_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_REVISION) == 0) {
+                               fe->revision_index = ii;
+                       } else if (fe->object_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_OBJECT) == 0) {
+                               fe->object_index = ii;
+                       } else if (fe->state_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_STATE) == 0) {
+                               fe->state_index = ii;
+                       }
+               }
+       }
+
+       g_return_val_if_fail (fe->uid_index >= 0 && fe->uid_index < ncols, FALSE);
+       g_return_val_if_fail (fe->revision_index >= 0 && fe->revision_index < ncols, FALSE);
+       g_return_val_if_fail (fe->object_index >= 0 && fe->object_index < ncols, FALSE);
+       g_return_val_if_fail (fe->state_index >= 0 && fe->state_index < ncols, FALSE);
+
+       if (!column_values[fe->state_index])
+               offline_state = E_OFFLINE_STATE_UNKNOWN;
+       else
+               offline_state = g_ascii_strtoull (column_values[fe->state_index], NULL, 10);
+
+       return fe->func (cache, column_values[fe->uid_index], column_values[fe->revision_index], 
column_values[fe->object_index],
+               offline_state, ncols, column_names, column_values, fe->user_data);
+}
+
+/**
+ * e_cache_foreach:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
+ * @func: an #ECacheForeachFunc function to call for each object
+ * @user_data: user data for the @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calls @func for each found object, which satisfies the criteria
+ * for both @deleted_flag and @where_clause.
+ *
+ * Note the @func should not call any SQLite commands, because it's invoked
+ * within a SELECT statement execution.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_foreach (ECache *cache,
+                ECacheDeletedFlag deleted_flag,
+                const gchar *where_clause,
+                ECacheForeachFunc func,
+                gpointer user_data,
+                GCancellable *cancellable,
+                GError **error)
+{
+       struct ForeachData fe;
+       GString *stmt;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (func, FALSE);
+
+       stmt = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS);
+
+       if (where_clause) {
+               g_string_append (stmt, " WHERE ");
+
+               if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+                       g_string_append (stmt, where_clause);
+               } else {
+                       g_string_append_printf (stmt, E_CACHE_COLUMN_STATE "!=%d AND (%s)",
+                               E_OFFLINE_STATE_LOCALLY_DELETED, where_clause);
+               }
+       } else if (deleted_flag != E_CACHE_INCLUDE_DELETED) {
+               g_string_append_printf (stmt, " WHERE " E_CACHE_COLUMN_STATE "!=%d", 
E_OFFLINE_STATE_LOCALLY_DELETED);
+       }
+
+       fe.func = func;
+       fe.user_data = user_data;
+       fe.uid_index = -1;
+       fe.revision_index = -1;
+       fe.object_index = -1;
+       fe.state_index = -1;
+
+       success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_cb, &fe, cancellable, 
error);
+
+       g_string_free (stmt, TRUE);
+
+       return success;
+}
+
+struct ForeachUpdateRowData {
+       gchar *uid;
+       gchar *revision;
+       gchar *object;
+       EOfflineState offline_state;
+       gint ncols;
+       GPtrArray *column_values;
+};
+
+static void
+foreach_update_row_data_free (gpointer ptr)
+{
+       struct ForeachUpdateRowData *fr = ptr;
+
+       if (fr) {
+               g_free (fr->uid);
+               g_free (fr->revision);
+               g_free (fr->object);
+               g_ptr_array_free (fr->column_values, TRUE);
+               g_free (fr);
+       }
+}
+
+struct ForeachUpdateData {
+       gint uid_index;
+       gint revision_index;
+       gint object_index;
+       gint state_index;
+       GSList *rows; /* struct ForeachUpdateRowData * */
+       GPtrArray *column_names;
+};
+
+static gboolean
+e_cache_foreach_update_cb (ECache *cache,
+                          gint ncols,
+                          const gchar *column_names[],
+                          const gchar *column_values[],
+                          gpointer user_data)
+{
+       struct ForeachUpdateData *fu = user_data;
+       struct ForeachUpdateRowData *rd;
+       EOfflineState offline_state;
+       GPtrArray *cnames, *cvalues;
+       gint ii;
+
+       g_return_val_if_fail (fu != NULL, FALSE);
+       g_return_val_if_fail (column_names != NULL, FALSE);
+       g_return_val_if_fail (column_values != NULL, FALSE);
+
+       if (fu->uid_index == -1 ||
+           fu->revision_index == -1 ||
+           fu->object_index == -1 ||
+           fu->state_index == -1) {
+               gint ii;
+
+               for (ii = 0; ii < ncols && (fu->uid_index == -1 ||
+                    fu->revision_index == -1 ||
+                    fu->object_index == -1 ||
+                    fu->state_index == -1); ii++) {
+                       if (!column_names[ii])
+                               continue;
+
+                       if (fu->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) 
== 0) {
+                               fu->uid_index = ii;
+                       } else if (fu->revision_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_REVISION) == 0) {
+                               fu->revision_index = ii;
+                       } else if (fu->object_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_OBJECT) == 0) {
+                               fu->object_index = ii;
+                       } else if (fu->state_index == -1 && g_ascii_strcasecmp (column_names[ii], 
E_CACHE_COLUMN_STATE) == 0) {
+                               fu->state_index = ii;
+                       }
+               }
+       }
+
+       g_return_val_if_fail (fu->uid_index >= 0 && fu->uid_index < ncols, FALSE);
+       g_return_val_if_fail (fu->revision_index >= 0 && fu->revision_index < ncols, FALSE);
+       g_return_val_if_fail (fu->object_index >= 0 && fu->object_index < ncols, FALSE);
+       g_return_val_if_fail (fu->state_index >= 0 && fu->state_index < ncols, FALSE);
+
+       if (!column_values[fu->state_index])
+               offline_state = E_OFFLINE_STATE_UNKNOWN;
+       else
+               offline_state = g_ascii_strtoull (column_values[fu->state_index], NULL, 10);
+
+       cnames = fu->column_names ? NULL : g_ptr_array_new_full (ncols, g_free);
+       cvalues = g_ptr_array_new_full (ncols, g_free);
+
+       for (ii = 0; ii < ncols; ii++) {
+               if (fu->uid_index == ii ||
+                   fu->revision_index == ii ||
+                   fu->object_index == ii ||
+                   fu->state_index == ii) {
+                       continue;
+               }
+
+               if (cnames)
+                       g_ptr_array_add (cnames, g_strdup (column_names[ii]));
+
+               g_ptr_array_add (cvalues, g_strdup (column_values[ii]));
+       }
+
+       rd = g_new0 (struct ForeachUpdateRowData, 1);
+       rd->uid = g_strdup (column_values[fu->uid_index]);
+       rd->revision = g_strdup (column_values[fu->revision_index]);
+       rd->object = g_strdup (column_values[fu->object_index]);
+       rd->offline_state = offline_state;
+       rd->ncols = ncols;
+       rd->column_values = cvalues;
+
+       if (cnames)
+               fu->column_names = cnames;
+
+       fu->rows = g_slist_prepend (fu->rows, rd);
+
+       g_return_val_if_fail ((gint) fu->column_names->len != ncols, FALSE);
+
+       return TRUE;
+}
+
+/**
+ * e_cache_foreach_update:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
+ * @func: an #ECacheUpdateFunc function to call for each object
+ * @user_data: user data for the @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calls @func for each found object, which satisfies the criteria for both
+ * @deleted_flag and @where_clause, letting the caller update values where
+ * necessary. The return value of @func is used to determine whether the call
+ * was successful, not whether there are any changes to be saved. If anything
+ * fails during the call then the all changes are reverted.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_foreach_update (ECache *cache,
+                       ECacheDeletedFlag deleted_flag,
+                       const gchar *where_clause,
+                       ECacheUpdateFunc func,
+                       gpointer user_data,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       GString *stmt_begin;
+       gchar *uid = NULL;
+       gint n_results;
+       gboolean has_where = TRUE;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (func, FALSE);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       stmt_begin = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS);
+
+       if (where_clause) {
+               g_string_append (stmt_begin, " WHERE ");
+
+               if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+                       g_string_append (stmt_begin, where_clause);
+               } else {
+                       g_string_append_printf (stmt_begin, E_CACHE_COLUMN_STATE "!=%d AND (%s)",
+                               E_OFFLINE_STATE_LOCALLY_DELETED, where_clause);
+               }
+       } else if (deleted_flag != E_CACHE_INCLUDE_DELETED) {
+               g_string_append_printf (stmt_begin, " WHERE " E_CACHE_COLUMN_STATE "!=%d", 
E_OFFLINE_STATE_LOCALLY_DELETED);
+       } else {
+               has_where = FALSE;
+       }
+
+       do {
+               GString *stmt;
+               GSList *link;
+               struct ForeachUpdateData fu;
+
+               fu.uid_index = -1;
+               fu.revision_index = -1;
+               fu.object_index = -1;
+               fu.state_index = -1;
+               fu.rows = NULL;
+               fu.column_names = NULL;
+
+               stmt = g_string_new (stmt_begin->str);
+
+               if (uid) {
+                       if (has_where)
+                               g_string_append (stmt, " AND ");
+                       else
+                               g_string_append (stmt, " WHERE ");
+
+                       e_cache_sqlite_stmt_append_printf (stmt, E_CACHE_COLUMN_UID ">%Q", uid);
+               }
+
+               g_string_append_printf (stmt, " ORDER BY " E_CACHE_COLUMN_UID " ASC LIMIT %d", 
E_CACHE_UPDATE_BATCH_SIZE);
+
+               success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_update_cb, &fu, 
cancellable, error);
+
+               g_string_free (stmt, TRUE);
+
+               if (success) {
+                       n_results = 0;
+                       fu.rows = g_slist_reverse (fu.rows);
+
+                       for (link = fu.rows; success && link; link = g_slist_next (link), n_results++) {
+                               struct ForeachUpdateRowData *fr = link->data;
+
+                               success = fr && fr->column_values && fu.column_names;
+                               if (success) {
+                                       gchar *new_revision = NULL;
+                                       gchar *new_object = NULL;
+                                       EOfflineState new_offline_state = fr->offline_state;
+                                       ECacheColumnValues *new_other_columns = NULL;
+
+                                       success = func (cache, fr->uid, fr->revision, fr->object, 
fr->offline_state,
+                                               fr->ncols, (const gchar **) fu.column_names->pdata,
+                                               (const gchar **) fr->column_values->pdata,
+                                               &new_revision, &new_object, &new_offline_state, 
&new_other_columns,
+                                               user_data);
+
+                                       if (success && (
+                                           (new_revision && g_strcmp0 (new_revision, fr->revision) != 0) ||
+                                           (new_object && g_strcmp0 (new_object, fr->object) != 0) ||
+                                           (new_offline_state != fr->offline_state) ||
+                                           (new_other_columns && e_cache_column_values_get_size 
(new_other_columns) > 0))) {
+                                               success = e_cache_put_locked (cache,
+                                                       fr->uid,
+                                                       new_revision ? new_revision : fr->revision,
+                                                       new_object ? new_object : fr->object,
+                                                       new_other_columns,
+                                                       new_offline_state,
+                                                       TRUE, cancellable, error);
+                                       }
+
+                                       g_free (new_revision);
+                                       g_free (new_object);
+                                       e_cache_column_values_free (new_other_columns);
+
+                                       if (!g_slist_next (link)) {
+                                               g_free (uid);
+                                               uid = g_strdup (fr->uid);
+                                       }
+                               }
+                       }
+               }
+
+               g_slist_free_full (fu.rows, foreach_update_row_data_free);
+               if (fu.column_names)
+                       g_ptr_array_free (fu.column_names, TRUE);
+       } while (success && n_results == E_CACHE_UPDATE_BATCH_SIZE);
+
+       g_string_free (stmt_begin, TRUE);
+       g_free (uid);
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+/**
+ * e_cache_get_offline_state:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: Current offline state #EOfflineState for the given object.
+ *    It returns %E_OFFLINE_STATE_UNKNOWN when the object could not be
+ *    found or other error happened.
+ *
+ * Since: 3.26
+ **/
+EOfflineState
+e_cache_get_offline_state (ECache *cache,
+                          const gchar *uid,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       EOfflineState offline_state = E_OFFLINE_STATE_UNKNOWN;
+       gint64 value = offline_state;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), E_OFFLINE_STATE_UNKNOWN);
+       g_return_val_if_fail (uid != NULL, E_OFFLINE_STATE_UNKNOWN);
+
+       if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+               return offline_state;
+       }
+
+       if (e_cache_sqlite_exec_printf (cache,
+               "SELECT " E_CACHE_COLUMN_STATE " FROM " E_CACHE_TABLE_OBJECTS
+               " WHERE " E_CACHE_COLUMN_UID " = %Q",
+               e_cache_get_int64_cb, &value, cancellable, error,
+               uid)) {
+               offline_state = value;
+       }
+
+       return offline_state;
+}
+
+/**
+ * e_cache_set_offline_state:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @state: an #EOfflineState to set
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets an offline @state for the object identified by @uid.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_offline_state (ECache *cache,
+                          const gchar *uid,
+                          EOfflineState state,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) {
+               g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+               return FALSE;
+       }
+
+       return e_cache_sqlite_exec_printf (cache,
+               "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d"
+               " WHERE " E_CACHE_COLUMN_UID " = %Q",
+               NULL, NULL, cancellable, error,
+               state, uid);
+}
+
+static gboolean
+e_cache_get_offline_changes_cb (ECache *cache,
+                               const gchar *uid,
+                               const gchar *revision,
+                               const gchar *object,
+                               EOfflineState offline_state,
+                               gint ncols,
+                               const gchar *column_names[],
+                               const gchar *column_values[],
+                               gpointer user_data)
+{
+       GSList **pchanges = user_data;
+
+       g_return_val_if_fail (pchanges != NULL, FALSE);
+
+       if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+           offline_state == E_OFFLINE_STATE_LOCALLY_MODIFIED ||
+           offline_state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+               *pchanges = g_slist_prepend (*pchanges, e_cache_offline_change_new (uid, revision, object, 
offline_state));
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_cache_get_offline_changes:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the list of all offline changes being done so far.
+ * The returned #GSList contains #ECacheOfflineChange structure.
+ * Use e_cache_clear_offline_changes() to clear all offline
+ * changes at once.
+ *
+ * Returns: (transfer full) (element-type ECacheOfflineChange): A newly allocated list of all
+ *    offline changes. Free it with g_slist_free_full (slist, e_cache_offline_change_free);
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_cache_get_offline_changes (ECache *cache,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       GSList *changes = NULL;
+       gchar *stmt;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       stmt = e_cache_sqlite_stmt_printf (E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_SYNCED);
+
+       if (!e_cache_foreach (cache, E_CACHE_INCLUDE_DELETED, stmt, e_cache_get_offline_changes_cb, &changes, 
cancellable, error)) {
+               g_slist_free_full (changes, e_cache_offline_change_free);
+               changes = NULL;
+       }
+
+       e_cache_sqlite_stmt_free (stmt);
+
+       return changes;
+}
+
+/**
+ * e_cache_clear_offline_changes:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Marks all objects as being fully synchronized with the server and
+ * removes those which are marked as locally deleted.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_clear_offline_changes (ECache *cache,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       ECacheClass *klass;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       klass = E_CACHE_GET_CLASS (cache);
+       g_return_val_if_fail (klass != NULL, FALSE);
+       g_return_val_if_fail (klass->clear_offline_changes_locked != NULL, FALSE);
+
+       e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+       success = klass->clear_offline_changes_locked (cache, cancellable, error);
+
+       e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+       return success;
+}
+
+/**
+ * e_cache_set_key:
+ * @cache: an #ECache
+ * @key: a key name
+ * @value: (nullable): a value to set, or %NULL to delete the key
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets a @value of the user @key, or deletes it, if the @value is %NULL.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_key (ECache *cache,
+                const gchar *key,
+                const gchar *value,
+                GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (key != NULL, FALSE);
+
+       return e_cache_set_key_internal (cache, TRUE, key, value, error);
+}
+
+/**
+ * e_cache_dup_key:
+ * @cache: an #ECache
+ * @key: a key name
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: (transfer full): a value of the @key. Free the returned string
+ *    with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_dup_key (ECache *cache,
+                const gchar *key,
+                GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+       g_return_val_if_fail (key != NULL, NULL);
+
+       return e_cache_dup_key_internal (cache, TRUE, key, error);
+}
+
+/**
+ * e_cache_set_key_int:
+ * @cache: an #ECache
+ * @key: a key name
+ * @value: an integer value to set
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets an integer @value for the user @key.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_key_int (ECache *cache,
+                    const gchar *key,
+                    gint value,
+                    GError **error)
+{
+       gchar *str_value;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (key != NULL, FALSE);
+
+       str_value = g_strdup_printf ("%d", value);
+       success = e_cache_set_key (cache, key, str_value, error);
+       g_free (str_value);
+
+       return success;
+}
+
+/**
+ * e_cache_get_key_int:
+ * @cache: an #ECache
+ * @key: a key name
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads the user @key value as an integer.
+ *
+ * Returns: The user @key value or -1 on error.
+ *
+ * Since: 3.26
+ **/
+gint
+e_cache_get_key_int (ECache *cache,
+                    const gchar *key,
+                    GError **error)
+{
+       gchar *str_value;
+       gint value;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), -1);
+
+       str_value = e_cache_dup_key (cache, key, error);
+       if (!str_value)
+               return -1;
+
+       value = g_ascii_strtoll (str_value, NULL, 10);
+       g_free (str_value);
+
+       return value;
+}
+
+/**
+ * e_cache_lock:
+ * @cache: an #ECache
+ * @lock_type: an #ECacheLockType
+ *
+ * Locks the @cache thus other threads cannot use it.
+ * This can be called recursively within one thread.
+ * Each call should have its pair e_cache_unlock().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_lock (ECache *cache,
+             ECacheLockType lock_type)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       cache->priv->in_transaction++;
+       g_return_if_fail (cache->priv->in_transaction > 0);
+
+       if (cache->priv->in_transaction == 1) {
+               /* It's important to make the distinction between a
+                * transaction which will read or one which will write.
+                *
+                * While it's not well documented, when receiving the SQLITE_BUSY
+                * error status, one can only safely retry at the beginning of
+                * the transaction.
+                *
+                * If a transaction is 'upgraded' to require a writer lock
+                * half way through the transaction and SQLITE_BUSY is returned,
+                * the whole transaction would need to be retried from the beginning.
+                */
+               cache->priv->lock_type = lock_type;
+
+               switch (lock_type) {
+               case E_CACHE_LOCK_READ:
+                       e_cache_sqlite_exec_internal (cache, "BEGIN", NULL, NULL, NULL, NULL);
+                       break;
+               case E_CACHE_LOCK_WRITE:
+                       e_cache_sqlite_exec_internal (cache, "BEGIN IMMEDIATE", NULL, NULL, NULL, NULL);
+                       break;
+               }
+       } else {
+               /* Warn about cases where where a read transaction might be upgraded */
+               if (lock_type == E_CACHE_LOCK_WRITE && cache->priv->lock_type == E_CACHE_LOCK_READ)
+                       g_warning (
+                               "A nested transaction wants to write, "
+                               "but the outermost transaction was started "
+                               "without a writer lock.");
+       }
+}
+
+/**
+ * e_cache_unlock:
+ * @cache: an #ECache
+ * @action: an #ECacheUnlockAction
+ *
+ * Unlocks the cache which was previouly locked with e_cache_lock().
+ * The cache locked with #E_CACHE_LOCK_WRITE should use either
+ * @action #E_CACHE_UNLOCK_COMMIT or #E_CACHE_UNLOCK_ROLLBACK,
+ * while the #E_CACHE_LOCK_READ should use #E_CACHE_UNLOCK_NONE @action.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_unlock (ECache *cache,
+               ECacheUnlockAction action)
+{
+       g_return_if_fail (E_IS_CACHE (cache));
+       g_return_if_fail (cache->priv->in_transaction > 0);
+
+       cache->priv->in_transaction--;
+
+       if (cache->priv->in_transaction == 0) {
+               switch (action) {
+               case E_CACHE_UNLOCK_NONE:
+               case E_CACHE_UNLOCK_COMMIT:
+                       e_cache_sqlite_exec_internal (cache, "COMMIT", NULL, NULL, NULL, NULL);
+                       break;
+               case E_CACHE_UNLOCK_ROLLBACK:
+                       e_cache_sqlite_exec_internal (cache, "ROLLBACK", NULL, NULL, NULL, NULL);
+                       break;
+               }
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_get_sqlitedb:
+ * @cache: an #ECache
+ *
+ * Returns: (transfer none): An SQLite3 database pointer. It is owned by the @cache.
+ *
+ * Since: 3.26
+ **/
+gpointer
+e_cache_get_sqlitedb (ECache *cache)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+       return cache->priv->db;
+}
+
+/**
+ * e_cache_sqlite_exec:
+ * @cache: an #ECache
+ * @sql_stmt: an SQLite statement to execute
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Executes an SQLite statement. Use e_cache_sqlite_select() for
+ * SELECT statements.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_exec (ECache *cache,
+                    const gchar *sql_stmt,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       return e_cache_sqlite_exec_internal (cache, sql_stmt, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_cache_sqlite_select:
+ * @cache: an #ECache
+ * @sql_stmt: an SQLite SELECT statement to execute
+ * @func: an #ECacheSelectFunc function to call for each row
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Executes a SELECT statement @sql_stmt and calls @func for each row of the result.
+ * Use e_cache_sqlite_exec() for statements which do not return row sets.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_select (ECache *cache,
+                      const gchar *sql_stmt,
+                      ECacheSelectFunc func,
+                      gpointer user_data,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (sql_stmt, FALSE);
+       g_return_val_if_fail (func, FALSE);
+
+       return e_cache_sqlite_exec_internal (cache, sql_stmt, func, user_data, cancellable, error);
+}
+
+/**
+ * e_cache_sqlite_stmt_append_printf:
+ * @stmt: a #GString statement to append to
+ * @format: a printf-like format
+ * @...: arguments for the @format
+ *
+ * Appends an SQLite statement fragment based on the @format and
+ * its arguments to the @stmt.
+ * The @format can contain any values recognized by sqlite3_mprintf().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_sqlite_stmt_append_printf (GString *stmt,
+                                  const gchar *format,
+                                  ...)
+{
+       va_list args;
+       gchar *tmp_stmt;
+
+       g_return_if_fail (stmt != NULL);
+       g_return_if_fail (format != NULL);
+
+       va_start (args, format);
+       tmp_stmt = sqlite3_vmprintf (format, args);
+       va_end (args);
+
+       g_string_append (stmt, tmp_stmt);
+
+       sqlite3_free (tmp_stmt);
+}
+
+/**
+ * e_cache_sqlite_stmt_printf:
+ * @format: a printf-like format
+ * @...: arguments for the @format
+ *
+ * Creates an SQLite statement based on the @format and its arguments.
+ * The @format can contain any values recognized by sqlite3_mprintf().
+ *
+ * Returns: (transfer full): A new SQLite statement. Free the returned
+ *    string with e_cache_sqlite_stmt_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_sqlite_stmt_printf (const gchar *format,
+                           ...)
+{
+       va_list args;
+       gchar *stmt;
+
+       g_return_val_if_fail (format != NULL, NULL);
+
+       va_start (args, format);
+       stmt = sqlite3_vmprintf (format, args);
+       va_end (args);
+
+       return stmt;
+}
+
+/**
+ * e_cache_sqlite_stmt_free:
+ * @stmt: a statement to free
+ *
+ * Frees a statement previously constructed with e_cache_sqlite_stmt_printf().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_sqlite_stmt_free (gchar *stmt)
+{
+       if (stmt)
+               sqlite3_free (stmt);
+}
+
+/**
+ * e_cache_sqlite_maybe_vacuum:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Runs vacuum (compacts the database file), if needed.
+ *
+ * Returns: Whether succeeded. It doesn't mean that the vacuum had been run,
+ *    only that no error happened during the call.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_maybe_vacuum (ECache *cache,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       guint64 page_count = 0, page_size = 0, freelist_count = 0;
+       gboolean success = FALSE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       g_rec_mutex_lock (&cache->priv->lock);
+
+       if (e_cache_sqlite_exec_internal (cache, "PRAGMA page_count;", e_cache_get_uint64_cb, &page_count, 
cancellable, &local_error) &&
+           e_cache_sqlite_exec_internal (cache, "PRAGMA page_size;", e_cache_get_uint64_cb, &page_size, 
cancellable, &local_error) &&
+           e_cache_sqlite_exec_internal (cache, "PRAGMA freelist_count;", e_cache_get_uint64_cb, 
&freelist_count, cancellable, &local_error)) {
+               /* Vacuum, if there's more than 5% of the free pages, or when free pages use more than 10MB */
+               success = !page_count || !freelist_count ||
+                       (freelist_count * page_size < 1024 * 1024 * 10 && freelist_count * 1000 / page_count 
<= 50) ||
+                       e_cache_sqlite_exec_internal (cache, "vacuum;", NULL, NULL, cancellable, 
&local_error);
+       }
+
+       g_rec_mutex_unlock (&cache->priv->lock);
+
+       if (local_error) {
+               g_propagate_error (error, local_error);
+               success = FALSE;
+       }
+
+       return success;
+}
+
+static gboolean
+e_cache_put_locked_default (ECache *cache,
+                           const gchar *uid,
+                           const gchar *revision,
+                           const gchar *object,
+                           ECacheColumnValues *other_columns,
+                           EOfflineState offline_state,
+                           gboolean is_replace,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       GString *statement, *other_names = NULL, *other_values = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       statement = g_string_sized_new (255);
+
+       e_cache_sqlite_stmt_append_printf (statement, "INSERT OR REPLACE INTO %Q ("
+               E_CACHE_COLUMN_UID ","
+               E_CACHE_COLUMN_REVISION ","
+               E_CACHE_COLUMN_OBJECT ","
+               E_CACHE_COLUMN_STATE,
+               E_CACHE_TABLE_OBJECTS);
+
+       if (other_columns) {
+               GHashTableIter iter;
+               gpointer key, value;
+
+               e_cache_column_values_init_iter (other_columns, &iter);
+               while (g_hash_table_iter_next (&iter, &key, &value)) {
+                       if (!other_names)
+                               other_names = g_string_new ("");
+                       g_string_append (other_names, ",");
+
+                       e_cache_sqlite_stmt_append_printf (other_names, "%Q", key);
+
+                       if (!other_values)
+                               other_values = g_string_new ("");
+
+                       g_string_append (other_values, ",");
+                       if (value) {
+                               e_cache_sqlite_stmt_append_printf (other_values, "%Q", value);
+                       } else {
+                               g_string_append (other_values, "NULL");
+                       }
+               }
+       }
+
+       if (other_names)
+               g_string_append (statement, other_names->str);
+
+       g_string_append (statement, ") VALUES (");
+
+       e_cache_sqlite_stmt_append_printf (statement, "%Q,%Q,%Q,%d", uid, revision ? revision : "", object, 
offline_state);
+
+       if (other_values)
+               g_string_append (statement, other_values->str);
+
+       g_string_append (statement, ")");
+
+       success = e_cache_sqlite_exec_internal (cache, statement->str, NULL, NULL, cancellable, error);
+
+       if (other_names)
+               g_string_free (other_names, TRUE);
+       if (other_values)
+               g_string_free (other_values, TRUE);
+       g_string_free (statement, TRUE);
+
+       return success;
+}
+
+static gboolean
+e_cache_remove_locked_default (ECache *cache,
+                              const gchar *uid,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       g_signal_emit (cache,
+                      signals[BEFORE_REMOVE],
+                      0,
+                      uid, cancellable, error,
+                      &success);
+
+       success = success && e_cache_sqlite_exec_printf (cache,
+               "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q",
+               NULL, NULL, cancellable, error,
+               uid);
+
+       return success;
+}
+
+static gboolean
+e_cache_remove_all_locked_default (ECache *cache,
+                                  const GSList *uids,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       const GSList *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       for (link = uids; link && success; link = g_slist_next (link)) {
+               const gchar *uid = link->data;
+
+               g_signal_emit (cache,
+                              signals[BEFORE_REMOVE],
+                              0,
+                              uid, cancellable, error,
+                              &success);
+       }
+
+       if (success) {
+               success = e_cache_sqlite_exec_printf (cache,
+                       "DELETE FROM " E_CACHE_TABLE_OBJECTS,
+                       NULL, NULL, cancellable, error);
+       }
+
+       return success;
+}
+
+static gboolean
+e_cache_clear_offline_changes_locked_default (ECache *cache,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+       success = e_cache_sqlite_exec_printf (cache,
+               "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_STATE "=%d",
+               NULL, NULL, cancellable, error,
+               E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       success = success && e_cache_sqlite_exec_printf (cache,
+               "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d"
+               " WHERE " E_CACHE_COLUMN_STATE "!=%d",
+               NULL, NULL, cancellable, error,
+               E_OFFLINE_STATE_SYNCED, E_OFFLINE_STATE_SYNCED);
+
+       return success;
+}
+
+static gboolean
+e_cache_signals_accumulator (GSignalInvocationHint *ihint,
+                            GValue *return_accu,
+                            const GValue *handler_return,
+                            gpointer data)
+{
+       gboolean handler_result;
+
+       handler_result = g_value_get_boolean (handler_return);
+       g_value_set_boolean (return_accu, handler_result);
+
+       return handler_result;
+}
+
+static gboolean
+e_cache_before_put_default (ECache *cache,
+                           const gchar *uid,
+                           const gchar *revision,
+                           const gchar *object,
+                           ECacheColumnValues *other_columns,
+                           gboolean is_replace,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       return TRUE;
+}
+
+static gboolean
+e_cache_before_remove_default (ECache *cache,
+                              const gchar *uid,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       return TRUE;
+}
+
+static void
+e_cache_finalize (GObject *object)
+{
+       ECache *cache = E_CACHE (object);
+
+       g_free (cache->priv->filename);
+       cache->priv->filename = NULL;
+
+       if (cache->priv->db) {
+               sqlite3_close (cache->priv->db);
+               cache->priv->db = NULL;
+       }
+
+       g_rec_mutex_clear (&cache->priv->lock);
+
+       g_warn_if_fail (cache->priv->cancellable == NULL);
+       g_clear_object (&cache->priv->cancellable);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cache_parent_class)->finalize (object);
+}
+
+static void
+e_cache_class_init (ECacheClass *klass)
+{
+       GObjectClass *object_class;
+
+       g_type_class_add_private (klass, sizeof (ECachePrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = e_cache_finalize;
+
+       klass->put_locked = e_cache_put_locked_default;
+       klass->remove_locked = e_cache_remove_locked_default;
+       klass->remove_all_locked = e_cache_remove_all_locked_default;
+       klass->clear_offline_changes_locked = e_cache_clear_offline_changes_locked_default;
+       klass->before_put = e_cache_before_put_default;
+       klass->before_remove = e_cache_before_remove_default;
+
+       signals[BEFORE_PUT] = g_signal_new (
+               "before-put",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (ECacheClass, before_put),
+               e_cache_signals_accumulator,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_BOOLEAN, 7,
+               G_TYPE_STRING,
+               G_TYPE_STRING,
+               G_TYPE_STRING,
+               G_TYPE_HASH_TABLE,
+               G_TYPE_BOOLEAN,
+               G_TYPE_CANCELLABLE,
+               G_TYPE_POINTER);
+
+       signals[BEFORE_REMOVE] = g_signal_new (
+               "before-remove",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (ECacheClass, before_remove),
+               e_cache_signals_accumulator,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_BOOLEAN, 3,
+               G_TYPE_STRING,
+               G_TYPE_CANCELLABLE,
+               G_TYPE_POINTER);
+
+       signals[REVISION_CHANGED] = g_signal_new (
+               "revision-changed",
+               G_OBJECT_CLASS_TYPE (klass),
+               G_SIGNAL_RUN_LAST,
+               G_STRUCT_OFFSET (ECacheClass, revision_changed),
+               NULL,
+               NULL,
+               g_cclosure_marshal_generic,
+               G_TYPE_NONE, 0,
+               G_TYPE_NONE);
+
+       e_sqlite3_vfs_init ();
+}
+
+static void
+e_cache_init (ECache *cache)
+{
+       cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (cache, E_TYPE_CACHE, ECachePrivate);
+
+       cache->priv->filename = NULL;
+       cache->priv->db = NULL;
+       cache->priv->cancellable = NULL;
+       cache->priv->in_transaction = 0;
+       cache->priv->revision_change_frozen = 0;
+       cache->priv->revision_counter = 0;
+       cache->priv->last_revision_time = 0;
+       cache->priv->needs_revision_change = FALSE;
+
+       g_rec_mutex_init (&cache->priv->lock);
+}
diff --git a/src/libebackend/e-cache.h b/src/libebackend/e-cache.h
new file mode 100644
index 0000000..18b3ed7
--- /dev/null
+++ b/src/libebackend/e-cache.h
@@ -0,0 +1,524 @@
+/* -*- 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 (__LIBEBACKEND_H_INSIDE__) && !defined (LIBEBACKEND_COMPILATION)
+#error "Only <libebackend/libebackend.h> should be included directly."
+#endif
+
+#ifndef E_CACHE_H
+#define E_CACHE_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <libebackend/e-backend-enums.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CACHE \
+       (e_cache_get_type ())
+#define E_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CACHE, ECache))
+#define E_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_CACHE, ECacheClass))
+#define E_IS_CACHE(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CACHE))
+#define E_IS_CACHE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_CACHE))
+#define E_CACHE_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_CACHE, ECacheClass))
+
+G_BEGIN_DECLS
+
+#define E_CACHE_TABLE_OBJECTS  "ECacheObjects"
+#define E_CACHE_TABLE_KEYS     "ECacheKeys"
+
+#define E_CACHE_COLUMN_UID     "ECacheUID"
+#define E_CACHE_COLUMN_REVISION        "ECacheREV"
+#define E_CACHE_COLUMN_OBJECT  "ECacheOBJ"
+#define E_CACHE_COLUMN_STATE   "ECacheState"
+
+/**
+ * E_CACHE_ERROR:
+ *
+ * Error domain for #ECache operations.
+ *
+ * Since: 3.26
+ **/
+#define E_CACHE_ERROR (e_cache_error_quark ())
+
+GQuark         e_cache_error_quark     (void);
+
+/**
+ * ECacheError:
+ * @E_CACHE_ERROR_ENGINE: An error was reported from the SQLite engine
+ * @E_CACHE_ERROR_CONSTRAINT: The error occurred due to an explicit constraint, like
+ *    when attempting to add two objects with the same UID.
+ * @E_CACHE_ERROR_NOT_FOUND: An object was not found by UID (this is
+ *    different from a query that returns no results, which is not an error).
+ * @E_CACHE_ERROR_INVALID_QUERY: A query was invalid.
+ * @E_CACHE_ERROR_UNSUPPORTED_FIELD: A field requested for inclusion in summary is not supported.
+ * @E_CACHE_ERROR_UNSUPPORTED_QUERY: A query was not supported.
+ * @E_CACHE_ERROR_END_OF_LIST: An attempt was made to fetch results past the end of a the list.
+ * @E_CACHE_ERROR_LOAD: An error occured while loading or creating the database.
+ *
+ * Defines the types of possible errors reported by the #ECache
+ *
+ * Since: 3.26
+ */
+typedef enum {
+       E_CACHE_ERROR_ENGINE,
+       E_CACHE_ERROR_CONSTRAINT,
+       E_CACHE_ERROR_NOT_FOUND,
+       E_CACHE_ERROR_INVALID_QUERY,
+       E_CACHE_ERROR_UNSUPPORTED_FIELD,
+       E_CACHE_ERROR_UNSUPPORTED_QUERY,
+       E_CACHE_ERROR_END_OF_LIST,
+       E_CACHE_ERROR_LOAD
+} ECacheError;
+
+typedef struct _ECacheColumnValues ECacheColumnValues;
+
+#define E_TYPE_CACHE_COLUMN_VALUES (e_cache_column_values_get_type ())
+GType          e_cache_column_values_get_type  (void) G_GNUC_CONST;
+ECacheColumnValues *
+               e_cache_column_values_new       (void);
+ECacheColumnValues *
+               e_cache_column_values_copy      (ECacheColumnValues *other_columns);
+void           e_cache_column_values_free      (ECacheColumnValues *other_columns);
+void           e_cache_column_values_put       (ECacheColumnValues *other_columns,
+                                                const gchar *name,
+                                                const gchar *value);
+void           e_cache_column_values_take_value(ECacheColumnValues *other_columns,
+                                                const gchar *name,
+                                                gchar *value);
+void           e_cache_column_values_take      (ECacheColumnValues *other_columns,
+                                                gchar *name,
+                                                gchar *value);
+gboolean       e_cache_column_values_contains  (ECacheColumnValues *other_columns,
+                                                const gchar *name);
+gboolean       e_cache_column_values_remove    (ECacheColumnValues *other_columns,
+                                                const gchar *name);
+void           e_cache_column_values_remove_all(ECacheColumnValues *other_columns);
+const gchar *  e_cache_column_values_lookup    (ECacheColumnValues *other_columns,
+                                                const gchar *name);
+guint          e_cache_column_values_get_size  (ECacheColumnValues *other_columns);
+void           e_cache_column_values_init_iter (ECacheColumnValues *other_columns,
+                                                GHashTableIter *iter);
+
+/**
+ * ECacheOfflineChange:
+ * @uid: UID of the object
+ * @revision: stored revision of the object
+ * @object: the object itself
+ * @state: an #EOfflineState of the object
+ *
+ * Holds the information about offline change for one object.
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+       gchar *uid;
+       gchar *revision;
+       gchar *object;
+       EOfflineState state;
+} ECacheOfflineChange;
+
+#define E_TYPE_CACHE_OFFLINE_CHANGE (e_cache_offline_change_get_type ())
+
+GType          e_cache_offline_change_get_type (void) G_GNUC_CONST;
+ECacheOfflineChange *
+               e_cache_offline_change_new      (const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                EOfflineState state);
+ECacheOfflineChange *
+               e_cache_offline_change_copy     (const ECacheOfflineChange *change);
+void           e_cache_offline_change_free     (/* ECacheOfflineChange */ gpointer change);
+
+typedef struct {
+       gchar *name;
+       gchar *type;
+       gchar *index_name;
+} ECacheColumnInfo;
+
+#define E_TYPE_CACHE_COLUMN_INFO (e_cache_column_info_get_type ())
+GType          e_cache_column_info_get_type    (void) G_GNUC_CONST;
+ECacheColumnInfo *
+               e_cache_column_info_new         (const gchar *name,
+                                                const gchar *type,
+                                                const gchar *index_name);
+ECacheColumnInfo *
+               e_cache_column_info_copy        (const ECacheColumnInfo *info);
+void           e_cache_column_info_free        (/* ECacheColumnInfo */ gpointer info);
+
+/**
+ * ECacheLockType:
+ * @E_CACHE_LOCK_READ: Obtain a lock for reading.
+ * @E_CACHE_LOCK_WRITE: Obtain a lock for writing. This also starts a transaction.
+ *
+ * Indicates the type of lock requested in e_cache_lock().
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_CACHE_LOCK_READ,
+       E_CACHE_LOCK_WRITE
+} ECacheLockType;
+
+/**
+ * ECacheUnlockAction:
+ * @E_CACHE_UNLOCK_NONE: Just unlock, this is appropriate for locks which were obtained with 
%E_CACHE_LOCK_READ.
+ * @E_CACHE_UNLOCK_COMMIT: Commit any modifications which were made while the lock was held.
+ * @E_CACHE_UNLOCK_ROLLBACK: Rollback any modifications which were made while the lock was held.
+ *
+ * Indicates what type of action to take while unlocking the cache with e_cache_unlock().
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_CACHE_UNLOCK_NONE,
+       E_CACHE_UNLOCK_COMMIT,
+       E_CACHE_UNLOCK_ROLLBACK
+} ECacheUnlockAction;
+
+/**
+ * ECacheDeletedFlag:
+ * @E_CACHE_EXCLUDE_DELETED: Do not include locally deleted objects
+ * @E_CACHE_INCLUDE_DELETED: Include locally deleted objects
+ *
+ * Declares whether to exclude or include locally deleted objects.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_CACHE_EXCLUDE_DELETED = 0,
+       E_CACHE_INCLUDE_DELETED
+} ECacheDeletedFlag;
+
+/**
+ * ECacheOfflineFlag:
+ * @E_CACHE_OFFLINE_UNKNOWN: Do not know current online/offline state
+ * @E_CACHE_IS_ONLINE: The operation is done in online
+ * @E_CACHE_IS_OFFLINE: The operation is done in offline
+ *
+ * Declares whether the operation is done in online or offline.
+ * This influences the offline state of the related objects.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+       E_CACHE_OFFLINE_UNKNOWN = -1,
+       E_CACHE_IS_ONLINE = 0,
+       E_CACHE_IS_OFFLINE
+} ECacheOfflineFlag;
+
+typedef struct _ECache ECache;
+typedef struct _ECacheClass ECacheClass;
+typedef struct _ECachePrivate ECachePrivate;
+
+/**
+ * ECacheForeachFunc:
+ * @cache: an #ECache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @user_data: user data, as used in e_cache_foreach()
+ *
+ * A callback called for each object row when using e_cache_foreach() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheForeachFunc) (ECache *cache,
+                                        const gchar *uid,
+                                        const gchar *revision,
+                                        const gchar *object,
+                                        EOfflineState offline_state,
+                                        gint ncols,
+                                        const gchar *column_names[],
+                                        const gchar *column_values[],
+                                        gpointer user_data);
+
+/**
+ * ECacheUpdateFunc:
+ * @cache: an #ECache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @out_revision: (out): the new object revision to set; keep it untouched to not change
+ * @out_object: (out): the new object to set; keep it untouched to not change
+ * @out_offline_state: (out): the offline state to set; the default is the same as @offline_state
+ * @out_other_columns: (out) (transfer full): an #ECacheColumnValues with other columns to set; keep it 
untouched to not change any
+ * @user_data: user data, as used in e_cache_foreach_update()
+ *
+ * A callback called for each object row when using e_cache_foreach_update() function.
+ * When all out parameters are left untouched, then the row is not changed.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheUpdateFunc)  (ECache *cache,
+                                        const gchar *uid,
+                                        const gchar *revision,
+                                        const gchar *object,
+                                        EOfflineState offline_state,
+                                        gint ncols,
+                                        const gchar *column_names[],
+                                        const gchar *column_values[],
+                                        gchar **out_revision,
+                                        gchar **out_object,
+                                        EOfflineState *out_offline_state,
+                                        ECacheColumnValues **out_other_columns,
+                                        gpointer user_data);
+
+/**
+ * ECacheSelectFunc:
+ * @cache: an #ECache
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @user_data: user data, as used in e_cache_sqlite_select()
+ *
+ * A callback called for each row of a SELECT statement executed
+ * with e_cache_sqlite_select() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheSelectFunc)  (ECache *cache,
+                                        gint ncols,
+                                        const gchar *column_names[],
+                                        const gchar *column_values[],
+                                        gpointer user_data);
+
+/**
+ * ECache:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ECache {
+       /*< private >*/
+       GObject parent;
+       ECachePrivate *priv;
+};
+
+struct _ECacheClass {
+       GObjectClass parent_class;
+
+       /* Virtual methods */
+       gboolean        (* put_locked)          (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                ECacheColumnValues *other_columns,
+                                                EOfflineState offline_state,
+                                                gboolean is_replace,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* remove_locked)       (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* remove_all_locked)   (ECache *cache,
+                                                const GSList *uids, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* clear_offline_changes_locked)
+                                               (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       void            (* erase)               (ECache *cache);
+
+       /* Signals */
+       gboolean        (* before_put)          (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                ECacheColumnValues *other_columns,
+                                                gboolean is_replace,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       gboolean        (* before_remove)       (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       void            (* revision_changed)    (ECache *cache);
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_cache_get_type                (void) G_GNUC_CONST;
+
+gboolean       e_cache_initialize_sync         (ECache *cache,
+                                                const gchar *filename,
+                                                const GSList *other_columns, /* ECacheColumnInfo * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+const gchar *  e_cache_get_filename            (ECache *cache);
+gint           e_cache_get_version             (ECache *cache);
+void           e_cache_set_version             (ECache *cache,
+                                                gint version);
+gchar *                e_cache_dup_revision            (ECache *cache);
+void           e_cache_set_revision            (ECache *cache,
+                                                const gchar *revision);
+void           e_cache_change_revision         (ECache *cache);
+void           e_cache_freeze_revision_change  (ECache *cache);
+void           e_cache_thaw_revision_change    (ECache *cache);
+gboolean       e_cache_is_revision_change_frozen
+                                               (ECache *cache);
+void           e_cache_erase                   (ECache *cache);
+gboolean       e_cache_contains                (ECache *cache,
+                                                const gchar *uid,
+                                                ECacheDeletedFlag deleted_flag);
+gchar *                e_cache_get                     (ECache *cache,
+                                                const gchar *uid,
+                                                gchar **out_revision,
+                                                ECacheColumnValues **out_other_columns,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_put                     (ECache *cache,
+                                                const gchar *uid,
+                                                const gchar *revision,
+                                                const gchar *object,
+                                                ECacheColumnValues *other_columns,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_remove                  (ECache *cache,
+                                                const gchar *uid,
+                                                ECacheOfflineFlag offline_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_remove_all              (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+guint          e_cache_get_count               (ECache *cache,
+                                                ECacheDeletedFlag deleted_flag,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_get_uids                (ECache *cache,
+                                                ECacheDeletedFlag deleted_flag,
+                                                GSList **out_uids, /* gchar * */
+                                                GSList **out_revisions, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_get_objects             (ECache *cache,
+                                                ECacheDeletedFlag deleted_flag,
+                                                GSList **out_objects, /* gchar * */
+                                                GSList **out_revisions, /* gchar * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_foreach                 (ECache *cache,
+                                                ECacheDeletedFlag deleted_flag,
+                                                const gchar *where_clause,
+                                                ECacheForeachFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_foreach_update          (ECache *cache,
+                                                ECacheDeletedFlag deleted_flag,
+                                                const gchar *where_clause,
+                                                ECacheUpdateFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+/* Offline support */
+EOfflineState  e_cache_get_offline_state       (ECache *cache,
+                                                const gchar *uid,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_set_offline_state       (ECache *cache,
+                                                const gchar *uid,
+                                                EOfflineState state,
+                                                GCancellable *cancellable,
+                                                GError **error);
+GSList *       e_cache_get_offline_changes     (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_clear_offline_changes   (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+/* Custom keys */
+gboolean       e_cache_set_key                 (ECache *cache,
+                                                const gchar *key,
+                                                const gchar *value,
+                                                GError **error);
+gchar *                e_cache_dup_key                 (ECache *cache,
+                                                const gchar *key,
+                                                GError **error);
+gboolean       e_cache_set_key_int             (ECache *cache,
+                                                const gchar *key,
+                                                gint value,
+                                                GError **error);
+gint           e_cache_get_key_int             (ECache *cache,
+                                                const gchar *key,
+                                                GError **error);
+
+/* Locking */
+void           e_cache_lock                    (ECache *cache,
+                                                ECacheLockType lock_type);
+void           e_cache_unlock                  (ECache *cache,
+                                                ECacheUnlockAction action);
+
+/* Low-level SQLite functions */
+gpointer       e_cache_get_sqlitedb            (ECache *cache);
+gboolean       e_cache_sqlite_exec             (ECache *cache,
+                                                const gchar *sql_stmt,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_sqlite_select           (ECache *cache,
+                                                const gchar *sql_stmt,
+                                                ECacheSelectFunc func,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_cache_sqlite_maybe_vacuum     (ECache *cache,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+void           e_cache_sqlite_stmt_append_printf
+                                               (GString *stmt,
+                                                const gchar *format,
+                                                ...);
+gchar *                e_cache_sqlite_stmt_printf      (const gchar *format,
+                                                ...);
+void           e_cache_sqlite_stmt_free        (gchar *stmt);
+
+G_END_DECLS
+
+#endif /* E_CACHE_H */
diff --git a/src/libebackend/libebackend.h b/src/libebackend/libebackend.h
index c286f18..d1559fd 100644
--- a/src/libebackend/libebackend.h
+++ b/src/libebackend/libebackend.h
@@ -26,6 +26,7 @@
 #include <libebackend/e-backend-enumtypes.h>
 #include <libebackend/e-backend-factory.h>
 #include <libebackend/e-backend.h>
+#include <libebackend/e-cache.h>
 #include <libebackend/e-cache-reaper.h>
 #include <libebackend/e-collection-backend-factory.h>
 #include <libebackend/e-collection-backend.h>
diff --git a/src/libedataserver/CMakeLists.txt b/src/libedataserver/CMakeLists.txt
index 8e26b83..fc523ea 100644
--- a/src/libedataserver/CMakeLists.txt
+++ b/src/libedataserver/CMakeLists.txt
@@ -69,6 +69,7 @@ set(SOURCES
        e-secret-store.c
        e-sexp.c
        e-soup-auth-bearer.c
+       e-soup-session.c
        e-soup-ssl-trust.c
        e-source.c
        e-source-extension.c
@@ -117,9 +118,11 @@ set(SOURCES
        e-uid.c
        e-url.c
        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
@@ -148,6 +151,7 @@ set(HEADERS
        e-secret-store.h
        e-sexp.h
        e-soup-auth-bearer.h
+       e-soup-session.h
        e-soup-ssl-trust.h
        e-source.h
        e-source-address-book.h
@@ -196,9 +200,11 @@ set(HEADERS
        e-uid.h
        e-url.h
        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-data-server-util.c b/src/libedataserver/e-data-server-util.c
index b68c0d1..7cdae3e 100644
--- a/src/libedataserver/e-data-server-util.c
+++ b/src/libedataserver/e-data-server-util.c
@@ -505,6 +505,52 @@ e_util_utf8_remove_accents (const gchar *str)
 }
 
 /**
+ * e_util_utf8_decompose:
+ * @text: a UTF-8 string
+ *
+ * Converts the @text into a decomposed variant and strips it, which
+ * allows also cheap case insensitive comparision afterwards. This
+ * produces an output as being used in e_util_utf8_strstrcasedecomp().
+ *
+ * Returns: (transfer full): A newly allocated string, a decomposed
+ *    variant of the @text. Free with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_util_utf8_decompose (const gchar *text)
+{
+       gunichar unival;
+       const gchar *p;
+       gchar utf8[12];
+       GString *decomp;
+
+       if (!text)
+               return NULL;
+
+       decomp = g_string_sized_new (strlen (text) + 1);
+
+       for (p = e_util_unicode_get_utf8 (text, &unival);
+            p && unival;
+            p = e_util_unicode_get_utf8 (p, &unival)) {
+               gunichar sc;
+               sc = stripped_char (unival);
+               if (sc) {
+                       gint ulen = g_unichar_to_utf8 (sc, utf8);
+                       g_string_append_len (decomp, utf8, ulen);
+               }
+       }
+
+       /* NULL means there was illegal utf-8 sequence */
+       if (!p || !decomp->len) {
+               g_string_free (decomp, TRUE);
+               return NULL;
+       }
+
+       return g_string_free (decomp, FALSE);
+}
+
+/**
  * e_util_utf8_make_valid:
  * @str: a UTF-8 string
  *
@@ -2922,3 +2968,47 @@ e_util_get_source_oauth2_access_token_sync (ESource *source,
 
        return success;
 }
+
+static gpointer
+unref_object_in_thread (gpointer ptr)
+{
+       GObject *object = ptr;
+
+       g_return_val_if_fail (object != NULL, NULL);
+
+       g_object_unref (object);
+
+       return NULL;
+}
+
+/**
+ * e_util_unref_in_thread:
+ * @object: a #GObject
+ *
+ * Unrefs the given @object in a dedicated thread. This is useful when unreffing
+ * object deep in call stack when the caller might still use the object and
+ * this being the last reference to it.
+ *
+ * Since: 3.26
+ **/
+void
+e_util_unref_in_thread (gpointer object)
+{
+       GThread *thread;
+       GError *error = NULL;
+
+       if (!object)
+               return;
+
+       g_return_if_fail (G_IS_OBJECT (object));
+
+       thread = g_thread_try_new (NULL, unref_object_in_thread, object, &error);
+       if (thread) {
+               g_thread_unref (thread);
+       } else {
+               g_warning ("%s: Failed to run thread: %s", G_STRFUNC, error ? error->message : "Unknown 
error");
+               g_object_unref (object);
+       }
+
+       g_clear_error (&error);
+}
diff --git a/src/libedataserver/e-data-server-util.h b/src/libedataserver/e-data-server-util.h
index 6b26f12..9dfb801 100644
--- a/src/libedataserver/e-data-server-util.h
+++ b/src/libedataserver/e-data-server-util.h
@@ -55,6 +55,7 @@ const gchar * e_util_utf8_strstrcasedecomp    (const gchar *haystack,
 gint           e_util_utf8_strcasecmp          (const gchar *s1,
                                                 const gchar *s2);
 gchar *                e_util_utf8_remove_accents      (const gchar *str);
+gchar *                e_util_utf8_decompose           (const gchar *text);
 gchar *                e_util_utf8_make_valid          (const gchar *str);
 gchar *                e_util_utf8_data_make_valid     (const gchar *data,
                                                 gsize data_bytes);
@@ -280,6 +281,9 @@ gboolean    e_util_get_source_oauth2_access_token_sync
                                                 gint *out_expires_in_seconds,
                                                 GCancellable *cancellable,
                                                 GError **error);
+
+void           e_util_unref_in_thread          (gpointer object);
+
 G_END_DECLS
 
 #endif /* E_DATA_SERVER_UTIL_H */
diff --git a/src/libedataserver/e-soup-session.c b/src/libedataserver/e-soup-session.c
new file mode 100644
index 0000000..437a941
--- /dev/null
+++ b/src/libedataserver/e-soup-session.c
@@ -0,0 +1,1019 @@
+/* -*- 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-soup-session
+ * @include: libedataserver/libedataserver.h
+ * @short_description: A SoupSession descendant
+ *
+ * The #ESoupSession is a #SoupSession descendant, which hides common
+ * tasks related to the way evolution-data-server works.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <stdio.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-soup-auth-bearer.h"
+#include "e-soup-ssl-trust.h"
+#include "e-source-authentication.h"
+#include "e-source-webdav.h"
+
+#include "e-soup-session.h"
+
+#define BUFFER_SIZE 16384
+
+struct _ESoupSessionPrivate {
+       GMutex property_lock;
+       ESource *source;
+       ENamedParameters *credentials;
+
+       gboolean ssl_info_set;
+       gchar *ssl_certificate_pem;
+       GTlsCertificateFlags ssl_certificate_errors;
+
+       SoupLoggerLogLevel log_level;
+
+       GError *bearer_auth_error;
+       ESoupAuthBearer *using_bearer_auth;
+};
+
+enum {
+       PROP_0,
+       PROP_SOURCE,
+       PROP_CREDENTIALS
+};
+
+G_DEFINE_TYPE (ESoupSession, e_soup_session, SOUP_TYPE_SESSION)
+
+static void
+e_soup_session_ensure_bearer_auth_usage (ESoupSession *session,
+                                        ESoupAuthBearer *bearer)
+{
+       SoupSessionFeature *feature;
+       SoupURI *soup_uri;
+       ESourceWebdav *extension;
+       ESource *source;
+
+       g_return_if_fail (E_IS_SOUP_SESSION (session));
+
+       source = e_soup_session_get_source (session);
+
+       /* Preload the SoupAuthManager with a valid "Bearer" token
+        * when using OAuth 2.0. This avoids an extra unauthorized
+        * HTTP round-trip, which apparently Google doesn't like. */
+
+       feature = soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_AUTH_MANAGER);
+
+       if (!soup_session_feature_has_feature (feature, E_TYPE_SOUP_AUTH_BEARER)) {
+               /* Add the "Bearer" auth type to support OAuth 2.0. */
+               soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
+       }
+
+       extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+       soup_uri = e_source_webdav_dup_soup_uri (extension);
+
+       soup_auth_manager_use_auth (
+               SOUP_AUTH_MANAGER (feature),
+               soup_uri, SOUP_AUTH (bearer));
+
+       soup_uri_free (soup_uri);
+}
+
+static gboolean
+e_soup_session_setup_bearer_auth (ESoupSession *session,
+                                 gboolean is_in_authenticate_handler,
+                                 ESoupAuthBearer *bearer,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       ENamedParameters *credentials;
+       ESource *source;
+       gchar *access_token = NULL;
+       gint expires_in_seconds = -1;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+       g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
+
+       source = e_soup_session_get_source (session);
+       credentials = e_soup_session_dup_credentials (session);
+
+       if (!credentials || !e_named_parameters_count (credentials)) {
+               e_named_parameters_free (credentials);
+               g_set_error_literal (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED, _("Credentials 
required"));
+               return FALSE;
+       }
+
+       success = e_util_get_source_oauth2_access_token_sync (source, credentials,
+               &access_token, &expires_in_seconds, cancellable, error);
+
+       if (success) {
+               e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
+
+               if (!is_in_authenticate_handler)
+                       e_soup_session_ensure_bearer_auth_usage (session, bearer);
+       }
+
+       e_named_parameters_free (credentials);
+       g_free (access_token);
+
+       return success;
+}
+
+static gboolean
+e_soup_session_maybe_prepare_bearer_auth (ESoupSession *session,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       ESource *source;
+       gchar *auth_method = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+
+       source = e_soup_session_get_source (session);
+
+       if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *extension;
+
+               extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+               auth_method = e_source_authentication_dup_method (extension);
+       } else {
+               return TRUE;
+       }
+
+       if (g_strcmp0 (auth_method, "OAuth2") != 0 && g_strcmp0 (auth_method, "Google") != 0) {
+               g_free (auth_method);
+               return TRUE;
+       }
+
+       g_free (auth_method);
+
+       g_mutex_lock (&session->priv->property_lock);
+       if (session->priv->using_bearer_auth) {
+               ESoupAuthBearer *using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
+
+               g_mutex_unlock (&session->priv->property_lock);
+
+               success = e_soup_session_setup_bearer_auth (session, FALSE, using_bearer_auth, cancellable, 
error);
+
+               g_clear_object (&using_bearer_auth);
+       } else {
+               ESourceWebdav *extension;
+               SoupAuth *soup_auth;
+               SoupURI *soup_uri;
+
+               g_mutex_unlock (&session->priv->property_lock);
+
+               extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+               soup_uri = e_source_webdav_dup_soup_uri (extension);
+
+               soup_auth = g_object_new (
+                       E_TYPE_SOUP_AUTH_BEARER,
+                       SOUP_AUTH_HOST, soup_uri->host, NULL);
+
+               success = e_soup_session_setup_bearer_auth (session, FALSE, E_SOUP_AUTH_BEARER (soup_auth), 
cancellable, error);
+               if (success) {
+                       g_mutex_lock (&session->priv->property_lock);
+                       g_clear_object (&session->priv->using_bearer_auth);
+                       session->priv->using_bearer_auth = g_object_ref (soup_auth);
+                       g_mutex_unlock (&session->priv->property_lock);
+               }
+
+               g_object_unref (soup_auth);
+               soup_uri_free (soup_uri);
+       }
+
+       return success;
+}
+
+static void
+e_soup_session_authenticate_cb (SoupSession *soup_session,
+                               SoupMessage *message,
+                               SoupAuth *auth,
+                               gboolean retrying,
+                               gpointer user_data)
+{
+       ESoupSession *session;
+       const gchar *username;
+       ENamedParameters *credentials;
+       gchar *auth_user = NULL;
+
+       g_return_if_fail (E_IS_SOUP_SESSION (soup_session));
+
+       session = E_SOUP_SESSION (soup_session);
+
+       if (E_IS_SOUP_AUTH_BEARER (auth)) {
+               g_object_ref (auth);
+               g_warn_if_fail ((gpointer) session->priv->using_bearer_auth == (gpointer) auth);
+               g_clear_object (&session->priv->using_bearer_auth);
+               session->priv->using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
+       }
+
+       if (retrying)
+               return;
+
+       if (session->priv->using_bearer_auth) {
+               GError *local_error = NULL;
+
+               e_soup_session_setup_bearer_auth (session, TRUE, E_SOUP_AUTH_BEARER (auth), NULL, 
&local_error);
+
+               if (local_error) {
+                       g_mutex_lock (&session->priv->property_lock);
+
+                       /* Warn about an unclaimed error before we clear it.
+                        * This is just to verify the errors we set here are
+                        * actually making it back to the user. */
+                       g_warn_if_fail (session->priv->bearer_auth_error == NULL);
+                       g_clear_error (&session->priv->bearer_auth_error);
+
+                       g_propagate_error (&session->priv->bearer_auth_error, local_error);
+
+                       g_mutex_unlock (&session->priv->property_lock);
+               }
+
+               return;
+       }
+
+       credentials = e_soup_session_dup_credentials (session);
+
+       username = credentials ? e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME) : NULL;
+       if ((!username || !*username) &&
+           e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+               ESourceAuthentication *auth_extension;
+
+               auth_extension = e_source_get_extension (session->priv->source, 
E_SOURCE_EXTENSION_AUTHENTICATION);
+               auth_user = e_source_authentication_dup_user (auth_extension);
+
+               username = auth_user;
+       }
+
+       if (!username || !*username || !credentials ||
+           !e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+               soup_message_set_status (message, SOUP_STATUS_UNAUTHORIZED);
+       else
+               soup_auth_authenticate (auth, username, e_named_parameters_get (credentials, 
E_SOURCE_CREDENTIAL_PASSWORD));
+
+       e_named_parameters_free (credentials);
+       g_free (auth_user);
+}
+
+static void
+e_soup_session_set_source (ESoupSession *session,
+                          ESource *source)
+{
+       g_return_if_fail (E_IS_SOUP_SESSION (session));
+       g_return_if_fail (E_IS_SOURCE (source));
+       g_return_if_fail (!session->priv->source);
+
+       session->priv->source = g_object_ref (source);
+}
+
+static void
+e_soup_session_set_property (GObject *object,
+                            guint property_id,
+                            const GValue *value,
+                            GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_SOURCE:
+                       e_soup_session_set_source (
+                               E_SOUP_SESSION (object),
+                               g_value_get_object (value));
+                       return;
+
+               case PROP_CREDENTIALS:
+                       e_soup_session_set_credentials (
+                               E_SOUP_SESSION (object),
+                               g_value_get_boxed (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_soup_session_get_property (GObject *object,
+                            guint property_id,
+                            GValue *value,
+                            GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_SOURCE:
+                       g_value_set_object (
+                               value,
+                               e_soup_session_get_source (
+                               E_SOUP_SESSION (object)));
+                       return;
+
+               case PROP_CREDENTIALS:
+                       g_value_take_boxed (
+                               value,
+                               e_soup_session_dup_credentials (
+                               E_SOUP_SESSION (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_soup_session_finalize (GObject *object)
+{
+       ESoupSession *session = E_SOUP_SESSION (object);
+
+       g_clear_error (&session->priv->bearer_auth_error);
+       g_clear_object (&session->priv->source);
+       g_clear_object (&session->priv->using_bearer_auth);
+       g_clear_pointer (&session->priv->credentials, e_named_parameters_free);
+       g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
+
+       g_mutex_clear (&session->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_soup_session_parent_class)->finalize (object);
+}
+
+static void
+e_soup_session_class_init (ESoupSessionClass *klass)
+{
+       GObjectClass *object_class;
+
+       g_type_class_add_private (klass, sizeof (ESoupSessionPrivate));
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->set_property = e_soup_session_set_property;
+       object_class->get_property = e_soup_session_get_property;
+       object_class->finalize = e_soup_session_finalize;
+
+       /**
+        * ESoupSession:source:
+        *
+        * The #ESource being used for this soup session.
+        *
+        * Since: 3.26
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_SOURCE,
+               g_param_spec_object (
+                       "source",
+                       "Source",
+                       NULL,
+                       E_TYPE_SOURCE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY |
+                       G_PARAM_STATIC_STRINGS));
+
+       /**
+        * ESoupSession:credentials:
+        *
+        * The #ENamedParameters containing login credentials.
+        *
+        * Since: 3.26
+        **/
+       g_object_class_install_property (
+               object_class,
+               PROP_CREDENTIALS,
+               g_param_spec_boxed (
+                       "credentials",
+                       "Credentials",
+                       NULL,
+                       E_TYPE_NAMED_PARAMETERS,
+                       G_PARAM_READWRITE |
+                       G_PARAM_STATIC_STRINGS));
+}
+
+static void
+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);
+
+       g_object_set (
+               G_OBJECT (session),
+               SOUP_SESSION_TIMEOUT, 90,
+               SOUP_SESSION_SSL_STRICT, TRUE,
+               SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
+               SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
+               NULL);
+
+       g_signal_connect (session, "authenticate",
+               G_CALLBACK (e_soup_session_authenticate_cb), NULL);
+}
+
+/**
+ * e_soup_session_new:
+ * @source: an #ESource
+ *
+ * Creates a new #ESoupSession associated with given @source.
+ * The @source can be used to store and read SSL trust settings, but only if
+ * it already contains an #ESourceWebdav extension. Otherwise the SSL trust
+ * settings are ignored.
+ *
+ * Returns: (transfer full): a new #ESoupSession; free it with g_object_unref(),
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ESoupSession *
+e_soup_session_new (ESource *source)
+{
+       g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+
+       return g_object_new (E_TYPE_SOUP_SESSION,
+               "source", source,
+               NULL);
+}
+
+/**
+ * e_soup_session_setup_logging:
+ * @session: an #ESoupSession
+ * @logging_level: (nullable): logging level to setup, or %NULL
+ *
+ * Setups logging for the @session. The @logging_level can be one of:
+ * "all" - log whole raw communication;
+ * "body" - the same as "all";
+ * "headers" - log the headers only;
+ * "min" - minimal logging;
+ * "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
+e_soup_session_setup_logging (ESoupSession *session,
+                             const gchar *logging_level)
+{
+       SoupLogger *logger;
+
+       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;
+
+       if (g_ascii_strcasecmp (logging_level, "all") == 0 ||
+           g_ascii_strcasecmp (logging_level, "body") == 0 ||
+           g_ascii_strcasecmp (logging_level, "1") == 0)
+               session->priv->log_level = SOUP_LOGGER_LOG_BODY;
+       else if (g_ascii_strcasecmp (logging_level, "headers") == 0)
+               session->priv->log_level = SOUP_LOGGER_LOG_HEADERS;
+       else if (g_ascii_strcasecmp (logging_level, "min") == 0)
+               session->priv->log_level = SOUP_LOGGER_LOG_MINIMAL;
+       else
+               return;
+
+       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
+ *
+ * Returns: (transfer none): Associated #ESource with the @session.
+ *
+ * Since: 3.26
+ **/
+ESource *
+e_soup_session_get_source (ESoupSession *session)
+{
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+       return session->priv->source;
+}
+
+/**
+ * e_soup_session_set_credentials:
+ * @session: an #ESoupSession
+ * @credentials: (nullable): an #ENamedParameters with credentials to use, or %NULL
+ *
+ * Sets credentials to use for connection. Using %NULL for @credentials
+ * unsets previous value.
+ *
+ * Since: 3.26
+ **/
+void
+e_soup_session_set_credentials (ESoupSession *session,
+                               const ENamedParameters *credentials)
+{
+       g_return_if_fail (E_IS_SOUP_SESSION (session));
+
+       g_mutex_lock (&session->priv->property_lock);
+
+       if (credentials == session->priv->credentials) {
+               g_mutex_unlock (&session->priv->property_lock);
+               return;
+       }
+
+       e_named_parameters_free (session->priv->credentials);
+       if (credentials)
+               session->priv->credentials = e_named_parameters_new_clone (credentials);
+       else
+               session->priv->credentials = NULL;
+
+       g_mutex_unlock (&session->priv->property_lock);
+
+       g_object_notify (G_OBJECT (session), "credentials");
+}
+
+/**
+ * e_soup_session_dup_credentials:
+ * @session: an #ESoupSession
+ *
+ * Returns: (nullable) (transfer full): A copy of the credentials being
+ *    previously set with e_soup_session_set_credentials(), or %NULL when
+ *    none are set. Free the returned pointer with e_named_parameters_free(),
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ENamedParameters *
+e_soup_session_dup_credentials (ESoupSession *session)
+{
+       ENamedParameters *credentials;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+       g_mutex_lock (&session->priv->property_lock);
+
+       if (session->priv->credentials)
+               credentials = e_named_parameters_new_clone (session->priv->credentials);
+       else
+               credentials = NULL;
+
+       g_mutex_unlock (&session->priv->property_lock);
+
+       return credentials;
+}
+
+/**
+ * e_soup_session_get_ssl_error_details:
+ * @session: an #ESoupSession
+ * @out_certificate_pem: (out): return location for a server TLS/SSL certificate
+ *   in PEM format, when the last operation failed with a TLS/SSL error
+ * @out_certificate_errors: (out): return location for a #GTlsCertificateFlags,
+ *   with certificate error flags when the the operation failed with a TLS/SSL error
+ *
+ * Populates @out_certificate_pem and @out_certificate_errors with the last values
+ * returned on #SOUP_STATUS_SSL_FAILED error.
+ *
+ * Returns: Whether the information was available and set to the out parameters.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_soup_session_get_ssl_error_details (ESoupSession *session,
+                                     gchar **out_certificate_pem,
+                                     GTlsCertificateFlags *out_certificate_errors)
+{
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+       g_return_val_if_fail (out_certificate_pem != NULL, FALSE);
+       g_return_val_if_fail (out_certificate_errors != NULL, FALSE);
+
+       g_mutex_lock (&session->priv->property_lock);
+       if (!session->priv->ssl_info_set) {
+               g_mutex_unlock (&session->priv->property_lock);
+               return FALSE;
+       }
+
+       *out_certificate_pem = g_strdup (session->priv->ssl_certificate_pem);
+       *out_certificate_errors = session->priv->ssl_certificate_errors;
+
+       g_mutex_unlock (&session->priv->property_lock);
+
+       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;
+}
+
+static void
+e_soup_session_extract_ssl_data (ESoupSession *session,
+                                SoupMessage *message)
+{
+       GTlsCertificate *certificate = NULL;
+
+       g_return_if_fail (E_IS_SOUP_SESSION (session));
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+
+       g_mutex_lock (&session->priv->property_lock);
+
+       g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
+       session->priv->ssl_info_set = FALSE;
+
+       g_object_get (G_OBJECT (message),
+               "tls-certificate", &certificate,
+               "tls-errors", &session->priv->ssl_certificate_errors,
+               NULL);
+
+       if (certificate) {
+               g_object_get (certificate, "certificate-pem", &session->priv->ssl_certificate_pem, NULL);
+               session->priv->ssl_info_set = TRUE;
+
+               g_object_unref (certificate);
+       }
+
+       g_mutex_unlock (&session->priv->property_lock);
+}
+
+/**
+ * e_soup_session_check_result:
+ * @session: an #ESoupSession
+ * @request: a #SoupRequestHTTP
+ * @read_bytes: (nullable): optional bytes which had been read from the stream, or %NULL
+ * @bytes_length: how many bytes had been read; ignored when @read_bytes is %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Checks result of the @request and sets the @error if it failed.
+ * When it failed and the @read_bytes is provided, then these are
+ * set to @request's message response_body, thus it can be used
+ * later.
+ *
+ * Returns: Whether succeeded, aka %TRUE, when no error recognized
+ *    and %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_soup_session_check_result (ESoupSession *session,
+                            SoupRequestHTTP *request,
+                            gconstpointer read_bytes,
+                            gsize bytes_length,
+                            GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+       g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), FALSE);
+
+       message = soup_request_http_get_message (request);
+       g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
+
+       success = SOUP_STATUS_IS_SUCCESSFUL (message->status_code);
+       if (!success) {
+               if (message->status_code == SOUP_STATUS_CANCELLED) {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Operation was 
cancelled"));
+               } else {
+                       g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+                               _("Failed with HTTP error %d: %s"), message->status_code,
+                               e_soup_session_util_status_to_string (message->status_code, 
message->reason_phrase));
+               }
+
+               if (message->status_code == SOUP_STATUS_SSL_FAILED)
+                       e_soup_session_extract_ssl_data (session, message);
+
+               if (read_bytes && bytes_length > 0) {
+                       SoupBuffer *buffer;
+
+                       soup_message_body_append (message->response_body, SOUP_MEMORY_COPY, read_bytes, 
bytes_length);
+
+                       /* This writes data to message->response_body->data */
+                       buffer = soup_message_body_flatten (message->response_body);
+                       if (buffer)
+                               soup_buffer_free (buffer);
+               }
+       }
+
+       g_object_unref (message);
+
+       return success;
+}
+
+/**
+ * e_soup_session_send_request_sync:
+ * @session: an #ESoupSession
+ * @request: a #SoupRequestHTTP to send
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Synchronously sends prepared request and returns #GInputStream
+ * that can be used to read its contents.
+ *
+ * This calls soup_request_send() internally, but it also setups
+ * the request according to #ESoupSession:source authentication
+ * settings. It also extracts information about used certificate,
+ * in case of SOUP_STATUS_SSL_FAILED error and keeps it for later use
+ * by e_soup_session_get_ssl_error_details().
+ *
+ * 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.
+ *
+ * Note the @request is fully filled only after there is anything
+ * read from the resulting #GInputStream, thus use
+ * e_soup_session_check_result() to verify that the receive had
+ * been finished properly.
+ *
+ * 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.
+ *
+ * Since: 3.26
+ **/
+GInputStream *
+e_soup_session_send_request_sync (ESoupSession *session,
+                                 SoupRequestHTTP *request,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       ESoupAuthBearer *using_bearer_auth = NULL;
+       GInputStream *input_stream;
+       SoupMessage *message;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+       g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
+
+       if (!e_soup_session_maybe_prepare_bearer_auth (session, cancellable, error))
+               return NULL;
+
+       g_mutex_lock (&session->priv->property_lock);
+       g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
+       session->priv->ssl_certificate_errors = 0;
+       session->priv->ssl_info_set = FALSE;
+       if (session->priv->using_bearer_auth)
+               using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
+       g_mutex_unlock (&session->priv->property_lock);
+
+       if (session->priv->source &&
+           e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+               message = soup_request_http_get_message (request);
+
+               e_soup_ssl_trust_connect (message, session->priv->source);
+
+               g_clear_object (&message);
+       }
+
+       if (using_bearer_auth &&
+           e_soup_auth_bearer_is_expired (using_bearer_auth)) {
+               if (!e_soup_session_setup_bearer_auth (session, FALSE, using_bearer_auth, cancellable, 
&local_error)) {
+                       message = soup_request_http_get_message (request);
+
+                       if (local_error) {
+                               soup_message_set_status_full (message, SOUP_STATUS_BAD_REQUEST, 
local_error->message);
+                               g_propagate_error (error, local_error);
+                       } else {
+                               soup_message_set_status (message, SOUP_STATUS_BAD_REQUEST);
+                       }
+
+                       g_object_unref (using_bearer_auth);
+                       g_clear_object (&message);
+
+                       return NULL;
+               }
+       }
+
+       g_clear_object (&using_bearer_auth);
+
+       input_stream = soup_request_send (SOUP_REQUEST (request), cancellable, &local_error);
+       if (input_stream)
+               return input_stream;
+
+       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
+               message = soup_request_http_get_message (request);
+
+               e_soup_session_extract_ssl_data (session, message);
+
+               g_clear_object (&message);
+       }
+
+       if (local_error)
+               g_propagate_error (error, local_error);
+
+       return NULL;
+}
+
+/**
+ * e_soup_session_send_request_simple_sync:
+ * @session: an #ESoupSession
+ * @request: a #SoupRequestHTTP to send
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_soup_session_send_request_sync(), except it reads
+ * whole response content into memory and returns it as a #GByteArray.
+ * 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.
+ *
+ * Since: 3.26
+ **/
+GByteArray *
+e_soup_session_send_request_simple_sync (ESoupSession *session,
+                                        SoupRequestHTTP *request,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       GInputStream *input_stream;
+       GByteArray *bytes;
+       gint expected_length;
+       gpointer buffer;
+       gsize nread = 0;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_SOUP_SESSION (session), 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 (SOUP_REQUEST (request));
+       if (expected_length > 0)
+               bytes = g_byte_array_sized_new (expected_length);
+       else
+               bytes = g_byte_array_new ();
+
+       buffer = g_malloc (BUFFER_SIZE);
+
+       while (success = g_input_stream_read_all (input_stream, buffer, BUFFER_SIZE, &nread, cancellable, 
error),
+              success && nread > 0) {
+               g_byte_array_append (bytes, buffer, nread);
+       }
+
+       g_free (buffer);
+       g_object_unref (input_stream);
+
+       if (bytes->len > 0 && e_soup_session_get_log_level (session) == SOUP_LOGGER_LOG_BODY) {
+               fwrite (bytes->data, 1, bytes->len, stdout);
+               fprintf (stdout, "\n");
+               fflush (stdout);
+       }
+
+       if (success)
+               success = e_soup_session_check_result (session, request, bytes->data, bytes->len, error);
+
+       if (!success) {
+               g_byte_array_free (bytes, TRUE);
+               bytes = NULL;
+       }
+
+       return bytes;
+}
+
+/**
+ * e_soup_session_util_status_to_string:
+ * @status_code: an HTTP status code
+ * @reason_phrase: (nullable): preferred string to use for the message, or %NULL
+ *
+ * Returns the @reason_phrase, if it's non-%NULL and non-empty, a static string
+ * corresponding to @status_code. In case neither that can be found a localized
+ * "Unknown error" message is returned.
+ *
+ * Returns: (transfer none): Error text based on given arguments. The returned
+ *    value is valid as long as @reason_phrase is not freed.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_soup_session_util_status_to_string (guint status_code,
+                                     const gchar *reason_phrase)
+{
+       if (!reason_phrase || !*reason_phrase)
+               reason_phrase = soup_status_get_phrase (status_code);
+
+       if (reason_phrase && *reason_phrase)
+               return reason_phrase;
+
+       return _("Unknown error");
+}
diff --git a/src/libedataserver/e-soup-session.h b/src/libedataserver/e-soup-session.h
new file mode 100644
index 0000000..c877829
--- /dev/null
+++ b/src/libedataserver/e-soup-session.h
@@ -0,0 +1,120 @@
+/* -*- 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_SOUP_SESSION_H
+#define E_SOUP_SESSION_H
+
+#include <glib.h>
+#include <libsoup/soup.h>
+
+#include <libedataserver/e-data-server-util.h>
+#include <libedataserver/e-source.h>
+
+/* Standard GObject macros */
+#define E_TYPE_SOUP_SESSION \
+       (e_soup_session_get_type ())
+#define E_SOUP_SESSION(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_SOUP_SESSION, ESoupSession))
+#define E_SOUP_SESSION_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_SOUP_SESSION, ESoupSessionClass))
+#define E_IS_SOUP_SESSION(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_SOUP_SESSION))
+#define E_IS_SOUP_SESSION_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_SOUP_SESSION))
+#define E_SOUP_SESSION_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_SOUP_SESSION, ESoupSessionClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ESoupSession ESoupSession;
+typedef struct _ESoupSessionClass ESoupSessionClass;
+typedef struct _ESoupSessionPrivate ESoupSessionPrivate;
+
+/**
+ * ESoupSession:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ESoupSession {
+       /*< private >*/
+       SoupSession parent;
+       ESoupSessionPrivate *priv;
+};
+
+struct _ESoupSessionClass {
+       SoupSessionClass parent_class;
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+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);
+ENamedParameters *
+               e_soup_session_dup_credentials          (ESoupSession *session);
+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);
+gboolean       e_soup_session_check_result             (ESoupSession *session,
+                                                        SoupRequestHTTP *request,
+                                                        gconstpointer read_bytes,
+                                                        gsize bytes_length,
+                                                        GError **error);
+GInputStream * e_soup_session_send_request_sync        (ESoupSession *session,
+                                                        SoupRequestHTTP *request,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+GByteArray *   e_soup_session_send_request_simple_sync (ESoupSession *session,
+                                                        SoupRequestHTTP *request,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+const gchar *  e_soup_session_util_status_to_string    (guint status_code,
+                                                        const gchar *reason_phrase);
+
+G_END_DECLS
+
+#endif /* E_SOUP_SESSION_H */
diff --git a/src/libedataserver/e-source-credentials-provider-impl-google.c 
b/src/libedataserver/e-source-credentials-provider-impl-google.c
index 80146f2..9867df4 100644
--- a/src/libedataserver/e-source-credentials-provider-impl-google.c
+++ b/src/libedataserver/e-source-credentials-provider-impl-google.c
@@ -522,6 +522,7 @@ e_source_credentials_google_get_access_token_sync (ESource *source,
                                                   GError **error)
 {
        ENamedParameters *tmp_credentials = NULL;
+       gboolean success;
 
        g_return_val_if_fail (credentials != NULL, FALSE);
 
@@ -546,10 +547,13 @@ e_source_credentials_google_get_access_token_sync (ESource *source,
                return TRUE;
        }
 
+       /* Try to refresh the token */
+       success = e_source_credentials_google_refresh_token_sync (source, tmp_credentials ? tmp_credentials : 
credentials,
+               out_access_token, out_expires_in_seconds, cancellable, error);
+
        e_named_parameters_free (tmp_credentials);
 
-       /* Try to refresh the token */
-       return e_source_credentials_google_refresh_token_sync (source, credentials, out_access_token, 
out_expires_in_seconds, cancellable, error);
+       return success;
 }
 
 gboolean
diff --git a/src/libedataserver/e-source-enums.h b/src/libedataserver/e-source-enums.h
index f241c9f..2c37f52 100644
--- a/src/libedataserver/e-source-enums.h
+++ b/src/libedataserver/e-source-enums.h
@@ -67,6 +67,8 @@ typedef enum {
 
 /**
  * ESourceAuthenticationResult:
+ * @E_SOURCE_AUTHENTICATION_UNKNOWN:
+ *   Unknown error occurred while authenticating. Since: 3.26
  * @E_SOURCE_AUTHENTICATION_ERROR:
  *   An error occurred while authenticating.
  * @E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
@@ -83,6 +85,7 @@ typedef enum {
  * Since: 3.6
  **/
 typedef enum {
+       E_SOURCE_AUTHENTICATION_UNKNOWN = -1,
        E_SOURCE_AUTHENTICATION_ERROR,
        E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED,
        E_SOURCE_AUTHENTICATION_ACCEPTED,
diff --git a/src/libedataserver/e-webdav-discover.c b/src/libedataserver/e-webdav-discover.c
index b25577a..43eaeff 100644
--- a/src/libedataserver/e-webdav-discover.c
+++ b/src/libedataserver/e-webdav-discover.c
@@ -21,1582 +21,329 @@
 
 #include <libsoup/soup.h>
 
-#include <libxml/tree.h>
-#include <libxml/xpath.h>
-#include <libxml/xpathInternals.h>
-
-#include "e-soup-auth-bearer.h"
-#include "e-soup-ssl-trust.h"
-#include "e-source-authentication.h"
-#include "e-source-credentials-provider-impl-google.h"
 #include "e-source-webdav.h"
-#include "e-webdav-discover.h"
-
-#define XC(string) ((xmlChar *) string)
-
-/* Standard Namespaces */
-#define NS_WEBDAV  "DAV:"
-#define NS_CALDAV  "urn:ietf:params:xml:ns:caldav"
-#define NS_CARDDAV "urn:ietf:params:xml:ns:carddav"
-
-/* Application-Specific Namespaces */
-#define NS_ICAL    "http://apple.com/ns/ical/";
+#include "e-webdav-session.h"
+#include "e-xml-utils.h"
 
-/* Mainly for readability. */
-enum {
-       DEPTH_0 = 0,
-       DEPTH_1 = 1
-};
+#include "e-webdav-discover.h"
 
-typedef struct _EWebDAVDiscoverContext {
-       ESource *source;
-       gchar *url_use_path;
+typedef struct _WebDAVDiscoverData {
+       GHashTable *covered_hrefs;
+       GSList *addressbooks;
+       GSList *calendars;
        guint32 only_supports;
-       ENamedParameters *credentials;
-       gchar *out_certificate_pem;
-       GTlsCertificateFlags out_certificate_errors;
-       GSList *out_discovered_sources;
-       GSList *out_calendar_user_addresses;
-} EWebDAVDiscoverContext;
-
-static EWebDAVDiscoverContext *
-e_webdav_discover_context_new (ESource *source,
-                              const gchar *url_use_path,
-                              guint32 only_supports,
-                              const ENamedParameters *credentials)
-{
-       EWebDAVDiscoverContext *context;
-
-       context = g_new0 (EWebDAVDiscoverContext, 1);
-       context->source = g_object_ref (source);
-       context->url_use_path = g_strdup (url_use_path);
-       context->only_supports = only_supports;
-       context->credentials = e_named_parameters_new_clone (credentials);
-       context->out_certificate_pem = NULL;
-       context->out_certificate_errors = 0;
-       context->out_discovered_sources = NULL;
-       context->out_calendar_user_addresses = NULL;
-
-       return context;
-}
+       GSList **out_calendar_user_addresses;
+       GCancellable *cancellable;
+       GError **error;
+} WebDAVDiscoverData;
 
 static void
-e_webdav_discover_context_free (gpointer ptr)
-{
-       EWebDAVDiscoverContext *context = ptr;
-
-       if (!context)
-               return;
-
-       g_clear_object (&context->source);
-       g_free (context->url_use_path);
-       e_named_parameters_free (context->credentials);
-       g_free (context->out_certificate_pem);
-       e_webdav_discover_free_discovered_sources (context->out_discovered_sources);
-       g_slist_free_full (context->out_calendar_user_addresses, g_free);
-       g_free (context);
-}
-
-static gchar *
-e_webdav_discover_make_href_full_uri (SoupURI *base_uri,
-                                     const gchar *href)
+e_webdav_discover_split_resources (WebDAVDiscoverData *wdd,
+                                  const GSList *resources)
 {
-       SoupURI *soup_uri;
-       gchar *full_uri;
-
-       if (!base_uri || !href)
-               return g_strdup (href);
-
-       if (strstr (href, "://"))
-               return g_strdup (href);
-
-       soup_uri = soup_uri_copy (base_uri);
-       soup_uri_set_path (soup_uri, href);
-       soup_uri_set_user (soup_uri, NULL);
-       soup_uri_set_password (soup_uri, NULL);
-
-       full_uri = soup_uri_to_string (soup_uri, FALSE);
-
-       soup_uri_free (soup_uri);
-
-       return full_uri;
-}
-
-static void
-e_webdav_discover_redirect (SoupMessage *message,
-                           SoupSession *session)
-{
-       SoupURI *soup_uri;
-       const gchar *location;
-
-       if (!SOUP_STATUS_IS_REDIRECTION (message->status_code))
-               return;
-
-       location = soup_message_headers_get_list (message->response_headers, "Location");
-
-       if (location == NULL)
-               return;
-
-       soup_uri = soup_uri_new_with_base (soup_message_get_uri (message), location);
-
-       if (soup_uri == NULL) {
-               soup_message_set_status_full (
-                       message, SOUP_STATUS_MALFORMED,
-                       _("Invalid Redirect URL"));
-               return;
-       }
-
-       soup_message_set_uri (message, soup_uri);
-       soup_session_requeue_message (session, message);
-
-       soup_uri_free (soup_uri);
-}
-
-static gconstpointer
-compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
-                                         gsize *out_len)
-{
-#ifdef LIBXML2_NEW_BUFFER
-       *out_len = xmlOutputBufferGetSize (buf);
-       return xmlOutputBufferGetContent (buf);
-#else
-       *out_len = buf->buffer->use;
-       return buf->buffer->content;
-#endif
-}
-
-static G_GNUC_NULL_TERMINATED SoupMessage *
-e_webdav_discover_new_propfind (SoupSession *session,
-                               SoupURI *soup_uri,
-                               gint depth,
-                               ...)
-{
-       GHashTable *namespaces;
-       SoupMessage *message;
-       xmlDocPtr doc;
-       xmlNodePtr root;
-       xmlNodePtr node;
-       xmlNsPtr ns;
-       xmlOutputBufferPtr output;
-       gconstpointer content;
-       gsize length;
-       gpointer key;
-       va_list va;
-
-       /* Construct the XML content. */
-
-       doc = xmlNewDoc (XC ("1.0"));
-       node = xmlNewDocNode (doc, NULL, XC ("propfind"), NULL);
-
-       /* Build a hash table of namespace URIs to xmlNs structs. */
-       namespaces = g_hash_table_new (NULL, NULL);
-
-       ns = xmlNewNs (node, XC (NS_CALDAV), XC ("C"));
-       g_hash_table_insert (namespaces, (gpointer) NS_CALDAV, ns);
-
-       ns = xmlNewNs (node, XC (NS_CARDDAV), XC ("A"));
-       g_hash_table_insert (namespaces, (gpointer) NS_CARDDAV, ns);
-
-       ns = xmlNewNs (node, XC (NS_ICAL), XC ("IC"));
-       g_hash_table_insert (namespaces, (gpointer) NS_ICAL, ns);
-
-       /* Add WebDAV last since we use it below. */
-       ns = xmlNewNs (node, XC (NS_WEBDAV), XC ("D"));
-       g_hash_table_insert (namespaces, (gpointer) NS_WEBDAV, ns);
-
-       xmlSetNs (node, ns);
-       xmlDocSetRootElement (doc, node);
-
-       node = xmlNewTextChild (node, ns, XC ("prop"), NULL);
-
-       va_start (va, depth);
-       while ((key = va_arg (va, gpointer)) != NULL) {
-               xmlChar *name;
-
-               ns = g_hash_table_lookup (namespaces, key);
-               name = va_arg (va, xmlChar *);
-
-               if (ns != NULL && name != NULL)
-                       xmlNewTextChild (node, ns, name, NULL);
-               else
-                       g_warn_if_reached ();
-       }
-       va_end (va);
-
-       g_hash_table_destroy (namespaces);
-
-       /* Construct the SoupMessage. */
-
-       message = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, soup_uri);
-
-       soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
-
-       soup_message_headers_append (
-               message->request_headers,
-               "User-Agent", "Evolution/" VERSION);
-
-       soup_message_headers_append (
-               message->request_headers,
-               "Connection", "close");
-
-       soup_message_headers_append (
-               message->request_headers,
-               "Depth", (depth == 0) ? "0" : "1");
-
-       output = xmlAllocOutputBuffer (NULL);
-
-       root = xmlDocGetRootElement (doc);
-       xmlNodeDumpOutput (output, doc, root, 0, 1, NULL);
-       xmlOutputBufferFlush (output);
-
-       content = compat_libxml_output_buffer_get_content (output, &length);
-
-       soup_message_set_request (
-               message, "application/xml", SOUP_MEMORY_COPY,
-               content, length);
-
-       xmlOutputBufferClose (output);
-       xmlFreeDoc (doc);
-
-       soup_message_add_header_handler (
-               message, "got-body", "Location",
-               G_CALLBACK (e_webdav_discover_redirect), session);
-
-       return message;
-}
-
-static xmlXPathObjectPtr
-e_webdav_discover_get_xpath (xmlXPathContextPtr xp_ctx,
-                            const gchar *path_format,
-                            ...)
-{
-       xmlXPathObjectPtr xp_obj;
-       va_list va;
-       gchar *path;
-
-       va_start (va, path_format);
-       path = g_strdup_vprintf (path_format, va);
-       va_end (va);
-
-       xp_obj = xmlXPathEvalExpression (XC (path), xp_ctx);
-
-       g_free (path);
-
-       if (xp_obj == NULL)
-               return NULL;
-
-       if (xp_obj->type != XPATH_NODESET) {
-               xmlXPathFreeObject (xp_obj);
-               return NULL;
-       }
-
-       if (xmlXPathNodeSetGetLength (xp_obj->nodesetval) == 0) {
-               xmlXPathFreeObject (xp_obj);
-               return NULL;
-       }
-
-       return xp_obj;
-}
-
-static gchar *
-e_webdav_discover_get_xpath_string (xmlXPathContextPtr xp_ctx,
-                                   const gchar *path_format,
-                                   ...)
-{
-       xmlXPathObjectPtr xp_obj;
-       va_list va;
-       gchar *path;
-       gchar *expression;
-       gchar *string = NULL;
-
-       va_start (va, path_format);
-       path = g_strdup_vprintf (path_format, va);
-       va_end (va);
-
-       expression = g_strdup_printf ("string(%s)", path);
-       xp_obj = xmlXPathEvalExpression (XC (expression), xp_ctx);
-       g_free (expression);
-
-       g_free (path);
-
-       if (xp_obj == NULL)
-               return NULL;
-
-       if (xp_obj->type == XPATH_STRING)
-               string = g_strdup ((gchar *) xp_obj->stringval);
-
-       /* If the string is empty, return NULL. */
-       if (string != NULL && *string == '\0') {
-               g_free (string);
-               string = NULL;
+       const GSList *link;
+
+       g_return_if_fail (wdd != NULL);
+
+       for (link = resources; link; link = g_slist_next (link)) {
+               const EWebDAVResource *resource = link->data;
+
+               if (resource && (
+                   resource->kind == E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK ||
+                   resource->kind == E_WEBDAV_RESOURCE_KIND_CALENDAR)) {
+                       EWebDAVDiscoveredSource *discovered;
+
+                       if (resource->kind == E_WEBDAV_RESOURCE_KIND_CALENDAR &&
+                           wdd->only_supports != E_WEBDAV_DISCOVER_SUPPORTS_NONE &&
+                           (resource->supports & wdd->only_supports) == 0)
+                               continue;
+
+                       discovered = g_new0 (EWebDAVDiscoveredSource, 1);
+                       discovered->href = g_strdup (resource->href);
+                       discovered->supports = resource->supports;
+                       discovered->display_name = g_strdup (resource->display_name);
+                       discovered->description = g_strdup (resource->description);
+                       discovered->color = g_strdup (resource->color);
+
+                       if (resource->kind == E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK) {
+                               wdd->addressbooks = g_slist_prepend (wdd->addressbooks, discovered);
+                       } else {
+                               wdd->calendars = g_slist_prepend (wdd->calendars, discovered);
+                       }
+               }
        }
-
-       xmlXPathFreeObject (xp_obj);
-
-       return string;
 }
 
 static gboolean
-e_webdav_discover_setup_bearer_auth (ESource *source,
-                                    const ENamedParameters *credentials,
-                                    ESoupAuthBearer *bearer,
-                                    GCancellable *cancellable,
-                                    GError **error)
-{
-       gchar *access_token = NULL;
-       gint expires_in_seconds = -1;
-       gboolean success = FALSE;
-
-       g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
-       g_return_val_if_fail (credentials != NULL, FALSE);
-
-       success = e_util_get_source_oauth2_access_token_sync (source, credentials,
-               &access_token, &expires_in_seconds, cancellable, error);
-
-       if (success)
-               e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
-
-       g_free (access_token);
-
-       return success;
-}
-
-typedef struct _AuthenticateData {
-       ESource *source;
-       const ENamedParameters *credentials;
-} AuthenticateData;
+e_webdav_discover_propfind_uri_sync (EWebDAVSession *webdav,
+                                    WebDAVDiscoverData *wdd,
+                                    const gchar *uri,
+                                    gboolean only_sets);
 
-static void
-e_webdav_discover_authenticate_cb (SoupSession *session,
-                                  SoupMessage *msg,
-                                  SoupAuth *auth,
-                                  gboolean retrying,
-                                  gpointer user_data)
+static gboolean
+e_webdav_discover_traverse_propfind_response_cb (EWebDAVSession *webdav,
+                                                xmlXPathContextPtr xpath_ctx,
+                                                const gchar *xpath_prop_prefix,
+                                                const SoupURI *request_uri,
+                                                const gchar *href,
+                                                guint status_code,
+                                                gpointer user_data)
 {
-       AuthenticateData *auth_data = user_data;
-
-       g_return_if_fail (auth_data != NULL);
-
-       if (retrying)
-               return;
-
-       if (E_IS_SOUP_AUTH_BEARER (auth)) {
-               GError *local_error = NULL;
-
-               e_webdav_discover_setup_bearer_auth (auth_data->source, auth_data->credentials,
-                       E_SOUP_AUTH_BEARER (auth), NULL, &local_error);
-
-               if (local_error != NULL) {
-                       soup_message_set_status_full (msg, SOUP_STATUS_FORBIDDEN, local_error->message);
-
-                       g_error_free (local_error);
-               }
-       } else {
-               gchar *auth_user = NULL;
+       WebDAVDiscoverData *wdd = user_data;
 
-               if (e_named_parameters_get (auth_data->credentials, E_SOURCE_CREDENTIAL_USERNAME))
-                       auth_user = g_strdup (e_named_parameters_get (auth_data->credentials, 
E_SOURCE_CREDENTIAL_USERNAME));
-
-               if (auth_user && !*auth_user) {
-                       g_free (auth_user);
-                       auth_user = NULL;
-               }
+       g_return_val_if_fail (wdd != NULL, FALSE);
 
-               if (!auth_user) {
-                       ESourceAuthentication *auth_extension;
-
-                       auth_extension = e_source_get_extension (auth_data->source, 
E_SOURCE_EXTENSION_AUTHENTICATION);
-                       auth_user = e_source_authentication_dup_user (auth_extension);
-               }
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx,
+                       "C", E_WEBDAV_NS_CALDAV,
+                       "A", E_WEBDAV_NS_CARDDAV,
+                       NULL);
+       } else if (status_code == SOUP_STATUS_OK) {
+               xmlXPathObjectPtr xpath_obj;
+               gchar *principal_href, *full_href;
 
-               if (!auth_user || !*auth_user || !auth_data->credentials || !e_named_parameters_get 
(auth_data->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
-                       soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
-               else
-                       soup_auth_authenticate (auth, auth_user, e_named_parameters_get 
(auth_data->credentials, E_SOURCE_CREDENTIAL_PASSWORD));
+               xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/A:addressbook-home-set", xpath_prop_prefix);
+               if (xpath_obj) {
+                       gint ii, length;
 
-               g_free (auth_user);
-       }
-}
+                       length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
 
-static gboolean
-e_webdav_discover_check_successful (SoupMessage *message,
-                                   gchar **out_certificate_pem,
-                                   GTlsCertificateFlags *out_certificate_errors,
-                                   GError **error)
-{
-       GIOErrorEnum error_code;
+                       for (ii = 0; ii < length; ii++) {
+                               gchar *home_set_href;
 
-       g_return_val_if_fail (message != NULL, FALSE);
+                               full_href = NULL;
 
-       /* Loosely copied from the GVFS DAV backend. */
+                               home_set_href = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/A:addressbook-home-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+                               if (home_set_href && *home_set_href) {
+                                       GSList *resources = NULL;
 
-       if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code))
-               return TRUE;
+                                       full_href = e_webdav_session_ensure_full_uri (webdav, request_uri, 
home_set_href);
+                                       if (full_href && *full_href && !g_hash_table_contains 
(wdd->covered_hrefs, full_href) &&
+                                           e_webdav_session_list_sync (webdav, full_href, 
E_WEBDAV_DEPTH_THIS_AND_CHILDREN,
+                                               E_WEBDAV_LIST_SUPPORTS | E_WEBDAV_LIST_DISPLAY_NAME | 
E_WEBDAV_LIST_DESCRIPTION | E_WEBDAV_LIST_COLOR,
+                                               &resources, wdd->cancellable, wdd->error)) {
+                                               e_webdav_discover_split_resources (wdd, resources);
+                                               g_slist_free_full (resources, e_webdav_resource_free);
+                                       }
 
-       switch (message->status_code) {
-               case SOUP_STATUS_CANCELLED:
-                       error_code = G_IO_ERROR_CANCELLED;
-                       break;
-               case SOUP_STATUS_NOT_FOUND:
-                       error_code = G_IO_ERROR_NOT_FOUND;
-                       break;
-               case SOUP_STATUS_UNAUTHORIZED:
-               case SOUP_STATUS_FORBIDDEN:
-                       g_set_error (
-                               error, SOUP_HTTP_ERROR, message->status_code,
-                               _("HTTP Error: %s"), message->reason_phrase);
-                       return FALSE;
-               case SOUP_STATUS_PAYMENT_REQUIRED:
-                       error_code = G_IO_ERROR_PERMISSION_DENIED;
-                       break;
-               case SOUP_STATUS_REQUEST_TIMEOUT:
-                       error_code = G_IO_ERROR_TIMED_OUT;
-                       break;
-               case SOUP_STATUS_CANT_RESOLVE:
-                       error_code = G_IO_ERROR_HOST_NOT_FOUND;
-                       break;
-               case SOUP_STATUS_NOT_IMPLEMENTED:
-                       error_code = G_IO_ERROR_NOT_SUPPORTED;
-                       break;
-               case SOUP_STATUS_INSUFFICIENT_STORAGE:
-                       error_code = G_IO_ERROR_NO_SPACE;
-                       break;
-               case SOUP_STATUS_SSL_FAILED:
-                       if (out_certificate_pem) {
-                               GTlsCertificate *certificate = NULL;
-
-                               g_free (*out_certificate_pem);
-                               *out_certificate_pem = NULL;
-
-                               g_object_get (G_OBJECT (message), "tls-certificate", &certificate, NULL);
-
-                               if (certificate) {
-                                       g_object_get (certificate, "certificate-pem", out_certificate_pem, 
NULL);
-                                       g_object_unref (certificate);
+                                       if (full_href && *full_href)
+                                               g_hash_table_insert (wdd->covered_hrefs, g_strdup 
(full_href), GINT_TO_POINTER (1));
                                }
-                       }
 
-                       if (out_certificate_errors) {
-                               *out_certificate_errors = 0;
-                               g_object_get (G_OBJECT (message), "tls-errors", out_certificate_errors, NULL);
+                               g_free (home_set_href);
+                               g_free (full_href);
                        }
 
-                       g_set_error (
-                               error, SOUP_HTTP_ERROR, message->status_code,
-                               _("HTTP Error: %s"), message->reason_phrase);
-                       return FALSE;
-               default:
-                       error_code = G_IO_ERROR_FAILED;
-                       break;
-       }
-
-       g_set_error (
-               error, G_IO_ERROR, error_code,
-               _("HTTP Error: %s"), message->reason_phrase);
-
-       return FALSE;
-}
-
-static xmlDocPtr
-e_webdav_discover_parse_xml (SoupMessage *message,
-                            const gchar *expected_name,
-                            gchar **out_certificate_pem,
-                            GTlsCertificateFlags *out_certificate_errors,
-                            GError **error)
-{
-       xmlDocPtr doc;
-       xmlNodePtr root;
-
-       if (!e_webdav_discover_check_successful (message, out_certificate_pem, out_certificate_errors, error))
-               return NULL;
-
-       doc = xmlReadMemory (
-               message->response_body->data,
-               message->response_body->length,
-               "response.xml", NULL,
-               XML_PARSE_NONET |
-               XML_PARSE_NOWARNING |
-               XML_PARSE_NOCDATA |
-               XML_PARSE_COMPACT);
-
-       if (doc == NULL) {
-               g_set_error_literal (
-                       error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                       _("Could not parse response"));
-               return NULL;
-       }
-
-       root = xmlDocGetRootElement (doc);
-
-       if (root == NULL || root->children == NULL) {
-               g_set_error_literal (
-                       error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                       _("Empty response"));
-               xmlFreeDoc (doc);
-               return NULL;
-       }
-
-       if (g_strcmp0 ((gchar *) root->name, expected_name) != 0) {
-               g_set_error_literal (
-                       error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                       _("Unexpected reply from server"));
-               xmlFreeDoc (doc);
-               return NULL;
-       }
-
-       return doc;
-}
-
-static void
-e_webdav_discover_process_user_address_set (xmlXPathContextPtr xp_ctx,
-                                           GSList **out_calendar_user_addresses)
-{
-       xmlXPathObjectPtr xp_obj;
-       gint ii, length;
-
-       if (!out_calendar_user_addresses)
-               return;
-
-       xp_obj = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/C:calendar-user-address-set");
-
-       if (xp_obj == NULL)
-               return;
-
-       length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
-
-       for (ii = 0; ii < length; ii++) {
-               GSList *duplicate;
-               const gchar *address;
-               gchar *href;
-
-               href = e_webdav_discover_get_xpath_string (
-                       xp_ctx,
-                       "/D:multistatus"
-                       "/D:response"
-                       "/D:propstat"
-                       "/D:prop"
-                       "/C:calendar-user-address-set"
-                       "/D:href[%d]", ii + 1);
-
-               if (href == NULL)
-                       continue;
-
-               if (!g_str_has_prefix (href, "mailto:";)) {
-                       g_free (href);
-                       continue;
-               }
-
-               /* strlen("mailto:";) == 7 */
-               address = href + 7;
-
-               /* Avoid duplicates. */
-               duplicate = g_slist_find_custom (
-                       *out_calendar_user_addresses,
-                       address, (GCompareFunc) g_ascii_strcasecmp);
-
-               if (duplicate != NULL) {
-                       g_free (href);
-                       continue;
+                       xmlXPathFreeObject (xpath_obj);
                }
 
-               *out_calendar_user_addresses = g_slist_prepend (
-                       *out_calendar_user_addresses, g_strdup (address));
-
-               g_free (href);
-       }
-
-       xmlXPathFreeObject (xp_obj);
-}
-
-static guint32
-e_webdav_discover_get_supported_component_set (xmlXPathContextPtr xp_ctx,
-                                              gint response_index,
-                                              gint propstat_index)
-{
-       xmlXPathObjectPtr xp_obj;
-       guint32 set = 0;
-       gint ii, length;
-
-       xp_obj = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/C:supported-calendar-component-set"
-               "/C:comp",
-               response_index,
-               propstat_index);
-
-       /* If the property is not present, assume all component
-        * types are supported.  (RFC 4791, Section 5.2.3) */
-       if (xp_obj == NULL)
-               return E_WEBDAV_DISCOVER_SUPPORTS_EVENTS |
-                      E_WEBDAV_DISCOVER_SUPPORTS_MEMOS |
-                      E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
-
-       length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
-
-       for (ii = 0; ii < length; ii++) {
-               gchar *name;
-
-               name = e_webdav_discover_get_xpath_string (
-                       xp_ctx,
-                       "/D:multistatus"
-                       "/D:response[%d]"
-                       "/D:propstat[%d]"
-                       "/D:prop"
-                       "/C:supported-calendar-component-set"
-                       "/C:comp[%d]"
-                       "/@name",
-                       response_index,
-                       propstat_index,
-                       ii + 1);
-
-               if (name == NULL)
-                       continue;
-
-               if (g_ascii_strcasecmp (name, "VEVENT") == 0)
-                       set |= E_WEBDAV_DISCOVER_SUPPORTS_EVENTS;
-               else if (g_ascii_strcasecmp (name, "VJOURNAL") == 0)
-                       set |= E_WEBDAV_DISCOVER_SUPPORTS_MEMOS;
-               else if (g_ascii_strcasecmp (name, "VTODO") == 0)
-                       set |= E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
-
-               g_free (name);
-       }
-
-       xmlXPathFreeObject (xp_obj);
-
-       return set;
-}
-
-static void
-e_webdav_discover_process_calendar_response_propstat (SoupMessage *message,
-                                                     xmlXPathContextPtr xp_ctx,
-                                                     gint response_index,
-                                                     gint propstat_index,
-                                                     GSList **out_discovered_sources)
-{
-       xmlXPathObjectPtr xp_obj;
-       guint32 comp_set;
-       gchar *color_spec;
-       gchar *display_name;
-       gchar *description;
-       gchar *href_encoded;
-       gchar *status_line;
-       guint status;
-       gboolean success;
-       EWebDAVDiscoveredSource *discovered_source;
-
-       if (!out_discovered_sources)
-               return;
-
-       status_line = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:status",
-               response_index,
-               propstat_index);
-
-       if (status_line == NULL)
-               return;
-
-       success = soup_headers_parse_status_line (
-               status_line, NULL, &status, NULL);
-
-       g_free (status_line);
+               xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/C:calendar-home-set", xpath_prop_prefix);
+               if (xpath_obj) {
+                       gint ii, length;
 
-       if (!success || status != SOUP_STATUS_OK)
-               return;
+                       length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
 
-       comp_set = e_webdav_discover_get_supported_component_set (xp_ctx, response_index, propstat_index);
-       if (comp_set == E_WEBDAV_DISCOVER_SUPPORTS_NONE)
-               return;
+                       for (ii = 0; ii < length; ii++) {
+                               gchar *home_set_href, *full_href = NULL;
 
-       href_encoded = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:href",
-               response_index);
+                               home_set_href = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/C:calendar-home-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+                               if (home_set_href && *home_set_href) {
+                                       GSList *resources = NULL;
 
-       if (href_encoded == NULL)
-               return;
-
-       /* Make sure the resource is a calendar. */
-
-       xp_obj = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/D:resourcetype"
-               "/C:calendar",
-               response_index,
-               propstat_index);
-
-       if (xp_obj == NULL) {
-               g_free (href_encoded);
-               return;
-       }
+                                       full_href = e_webdav_session_ensure_full_uri (webdav, request_uri, 
home_set_href);
+                                       if (full_href && *full_href && !g_hash_table_contains 
(wdd->covered_hrefs, full_href) &&
+                                           e_webdav_session_list_sync (webdav, full_href, 
E_WEBDAV_DEPTH_THIS_AND_CHILDREN,
+                                               E_WEBDAV_LIST_SUPPORTS | E_WEBDAV_LIST_DISPLAY_NAME | 
E_WEBDAV_LIST_DESCRIPTION | E_WEBDAV_LIST_COLOR,
+                                               &resources, wdd->cancellable, wdd->error)) {
+                                               e_webdav_discover_split_resources (wdd, resources);
+                                               g_slist_free_full (resources, e_webdav_resource_free);
+                                       }
 
-       xmlXPathFreeObject (xp_obj);
-
-       /* Get the display name or fall back to the href. */
-
-       display_name = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/D:displayname",
-               response_index,
-               propstat_index);
-
-       if (display_name == NULL) {
-               gchar *href_decoded = soup_uri_decode (href_encoded);
-
-               if (href_decoded) {
-                       gchar *cp;
-
-                       /* Use the last non-empty path segment. */
-                       while ((cp = strrchr (href_decoded, '/')) != NULL) {
-                               if (*(cp + 1) == '\0')
-                                       *cp = '\0';
-                               else {
-                                       display_name = g_strdup (cp + 1);
-                                       break;
+                                       if (full_href && *full_href)
+                                               g_hash_table_insert (wdd->covered_hrefs, g_strdup 
(full_href), GINT_TO_POINTER (1));
                                }
-                       }
-               }
-
-               g_free (href_decoded);
-       }
-
-       description = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/C:calendar-description",
-               response_index,
-               propstat_index);
-
-       /* Get the color specification string. */
-
-       color_spec = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/IC:calendar-color",
-               response_index,
-               propstat_index);
-
-       discovered_source = g_new0 (EWebDAVDiscoveredSource, 1);
-       discovered_source->href = e_webdav_discover_make_href_full_uri (soup_message_get_uri (message), 
href_encoded);
-       discovered_source->supports = comp_set;
-       discovered_source->display_name = g_strdup (display_name);
-       discovered_source->description = g_strdup (description);
-       discovered_source->color = g_strdup (color_spec);
-
-       *out_discovered_sources = g_slist_prepend (*out_discovered_sources, discovered_source);
-
-       g_free (href_encoded);
-       g_free (display_name);
-       g_free (description);
-       g_free (color_spec);
-}
 
-static void
-e_webdav_discover_traverse_responses (SoupMessage *message,
-                                     xmlXPathContextPtr xp_ctx,
-                                     GSList **out_discovered_sources,
-                                     void (* func) (
-                                               SoupMessage *message,
-                                               xmlXPathContextPtr xp_ctx,
-                                               gint response_index,
-                                               gint propstat_index,
-                                               GSList **out_discovered_sources))
-{
-       xmlXPathObjectPtr xp_obj_response;
-
-       xp_obj_response = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response");
-
-       if (xp_obj_response != NULL) {
-               gint response_index, response_length;
+                               g_free (home_set_href);
+                               g_free (full_href);
+                       }
 
-               response_length = xmlXPathNodeSetGetLength (xp_obj_response->nodesetval);
+                       xmlXPathFreeObject (xpath_obj);
+               }
 
-               for (response_index = 0; response_index < response_length; response_index++) {
-                       xmlXPathObjectPtr xp_obj_propstat;
+               xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/C:calendar-user-address-set", xpath_prop_prefix);
+               if (xpath_obj) {
+                       gint ii, length;
 
-                       xp_obj_propstat = e_webdav_discover_get_xpath (
-                               xp_ctx,
-                               "/D:multistatus"
-                               "/D:response[%d]"
-                               "/D:propstat",
-                               response_index + 1);
+                       length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
 
-                       if (xp_obj_propstat != NULL) {
-                               gint propstat_index, propstat_length;
+                       for (ii = 0; ii < length; ii++) {
+                               gchar *address_href;
 
-                               propstat_length = xmlXPathNodeSetGetLength (xp_obj_propstat->nodesetval);
+                               address_href = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/C:calendar-user-address-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+                               if (address_href && g_ascii_strncasecmp (address_href, "mailto:";, 7) == 0) {
+                                       /* Skip the "mailto:"; prefix */
+                                       const gchar *address = address_href + 7;
 
-                               for (propstat_index = 0; propstat_index < propstat_length; propstat_index++) {
-                                       func (message, xp_ctx, response_index + 1, propstat_index + 1, 
out_discovered_sources);
+                                       /* Avoid duplicates and empty values */
+                                       if (*address &&
+                                           !g_slist_find_custom (*wdd->out_calendar_user_addresses, address, 
(GCompareFunc) g_ascii_strcasecmp)) {
+                                               *wdd->out_calendar_user_addresses = g_slist_prepend (
+                                                       *wdd->out_calendar_user_addresses, g_strdup 
(address));
+                                       }
                                }
 
-                               xmlXPathFreeObject (xp_obj_propstat);
+                               g_free (address_href);
                        }
-               }
 
-               xmlXPathFreeObject (xp_obj_response);
-       }
-}
-
-static gboolean
-e_webdav_discover_get_calendar_collection_details (SoupSession *session,
-                                                  SoupMessage *message,
-                                                  const gchar *path_or_uri,
-                                                  ESource *source,
-                                                  gchar **out_certificate_pem,
-                                                  GTlsCertificateFlags *out_certificate_errors,
-                                                  GSList **out_discovered_sources,
-                                                  GCancellable *cancellable,
-                                                  GError **error)
-{
-       xmlDocPtr doc;
-       xmlXPathContextPtr xp_ctx;
-       SoupURI *soup_uri;
-       GError *local_error = NULL;
-
-       if (g_cancellable_is_cancelled (cancellable))
-               return FALSE;
-
-       soup_uri = soup_uri_new (path_or_uri);
-       if (!soup_uri ||
-           !soup_uri_get_scheme (soup_uri) ||
-           !soup_uri_get_host (soup_uri) ||
-           !soup_uri_get_path (soup_uri) ||
-           !*soup_uri_get_scheme (soup_uri) ||
-           !*soup_uri_get_host (soup_uri) ||
-           !*soup_uri_get_path (soup_uri)) {
-               /* it's a path only, not full uri */
-               if (soup_uri)
-                       soup_uri_free (soup_uri);
-               soup_uri = soup_uri_copy (soup_message_get_uri (message));
-               soup_uri_set_path (soup_uri, path_or_uri);
-       }
+                       xmlXPathFreeObject (xpath_obj);
+               }
 
-       message = e_webdav_discover_new_propfind (
-               session, soup_uri, DEPTH_1,
-               NS_WEBDAV, XC ("displayname"),
-               NS_WEBDAV, XC ("resourcetype"),
-               NS_CALDAV, XC ("calendar-description"),
-               NS_CALDAV, XC ("supported-calendar-component-set"),
-               NS_CALDAV, XC ("calendar-user-address-set"),
-               NS_ICAL,   XC ("calendar-color"),
-               NULL);
-
-       e_soup_ssl_trust_connect (message, source);
-
-       /* This takes ownership of the message. */
-       soup_session_send_message (session, message);
-
-       if (message->status_code == SOUP_STATUS_BAD_REQUEST) {
-               g_clear_object (&message);
-
-               message = e_webdav_discover_new_propfind (
-                       session, soup_uri, DEPTH_0,
-                       NS_WEBDAV, XC ("displayname"),
-                       NS_WEBDAV, XC ("resourcetype"),
-                       NS_CALDAV, XC ("calendar-description"),
-                       NS_CALDAV, XC ("supported-calendar-component-set"),
-                       NS_CALDAV, XC ("calendar-user-address-set"),
-                       NS_ICAL,   XC ("calendar-color"),
-                       NULL);
+               principal_href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:current-user-principal/D:href", 
xpath_prop_prefix);
+               if (principal_href && *principal_href) {
+                       full_href = e_webdav_session_ensure_full_uri (webdav, request_uri, principal_href);
 
-               e_soup_ssl_trust_connect (message, source);
-               soup_session_send_message (session, message);
-       }
+                       if (full_href && *full_href)
+                               e_webdav_discover_propfind_uri_sync (webdav, wdd, full_href, TRUE);
 
-       soup_uri_free (soup_uri);
-
-       doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, 
out_certificate_errors, &local_error);
-       if (!doc) {
-               g_clear_object (&message);
+                       g_free (full_href);
+                       g_free (principal_href);
 
-               if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
-                   g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
-                       /* Ignore these errors, but still propagate them. */
-                       g_propagate_error (error, local_error);
                        return TRUE;
-               } else if (local_error) {
-                       g_propagate_error (error, local_error);
                }
 
-               return FALSE;
-       }
-
-       xp_ctx = xmlXPathNewContext (doc);
-       xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
-
-       e_webdav_discover_traverse_responses (message, xp_ctx, out_discovered_sources,
-               e_webdav_discover_process_calendar_response_propstat);
-
-       xmlXPathFreeContext (xp_ctx);
-       xmlFreeDoc (doc);
-
-       g_clear_object (&message);
-
-       return TRUE;
-}
-
-static gboolean
-e_webdav_discover_process_calendar_home_set (SoupSession *session,
-                                            SoupMessage *message,
-                                            ESource *source,
-                                            gchar **out_certificate_pem,
-                                            GTlsCertificateFlags *out_certificate_errors,
-                                            GSList **out_discovered_sources,
-                                            GSList **out_calendar_user_addresses,
-                                            GCancellable *cancellable,
-                                            GError **error)
-{
-       SoupURI *soup_uri;
-       xmlDocPtr doc;
-       xmlXPathContextPtr xp_ctx;
-       xmlXPathObjectPtr xp_obj;
-       gchar *calendar_home_set;
-       GError *local_error = NULL;
-       gboolean success;
+               g_free (principal_href);
 
-       g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
-       g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
-       g_return_val_if_fail (out_discovered_sources != NULL, FALSE);
-       g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+               principal_href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:principal-URL/D:href", 
xpath_prop_prefix);
+               if (principal_href && *principal_href) {
+                       full_href = e_webdav_session_ensure_full_uri (webdav, request_uri, principal_href);
 
-       if (g_cancellable_is_cancelled (cancellable))
-               return FALSE;
+                       if (full_href && *full_href)
+                               e_webdav_discover_propfind_uri_sync (webdav, wdd, full_href, TRUE);
 
-       doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, 
out_certificate_errors, &local_error);
+                       g_free (full_href);
+                       g_free (principal_href);
 
-       if (!doc) {
-               if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
-                   g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
-                       /* Ignore these errors, but still propagate them. */
-                       g_propagate_error (error, local_error);
                        return TRUE;
-               } else if (local_error) {
-                       g_propagate_error (error, local_error);
                }
 
-               return FALSE;
-       }
+               g_free (principal_href);
 
-       xp_ctx = xmlXPathNewContext (doc);
-       xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
-
-       /* Record any "C:calendar-user-address-set" properties. */
-       e_webdav_discover_process_user_address_set (xp_ctx, out_calendar_user_addresses);
-
-       /* Try to find the calendar home URL using the
-        * following properties in order of preference:
-        *
-        *   "C:calendar-home-set"
-        *   "D:current-user-principal"
-        *   "D:principal-URL"
-        *
-        * If the second or third URL preference is used, rerun
-        * the PROPFIND method on that URL at Depth=1 in hopes
-        * of getting a proper "C:calendar-home-set" property.
-        */
-
-       /* FIXME There can be multiple "D:href" elements for a
-        *       "C:calendar-home-set".  We're only processing
-        *       the first one.  Need to iterate over them. */
-
-       calendar_home_set = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/C:calendar-home-set"
-               "/D:href");
-
-       if (calendar_home_set != NULL)
-               goto get_collection_details;
-
-       g_free (calendar_home_set);
-
-       calendar_home_set = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/D:current-user-principal"
-               "/D:href");
-
-       if (calendar_home_set != NULL)
-               goto retry_propfind;
-
-       g_free (calendar_home_set);
-
-       calendar_home_set = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/D:principal-URL"
-               "/D:href");
-
-       if (calendar_home_set != NULL)
-               goto retry_propfind;
-
-       g_free (calendar_home_set);
-       calendar_home_set = NULL;
-
-       /* None of the aforementioned properties are present.  If the
-        * user-supplied CalDAV URL is a calendar resource, use that. */
-
-       xp_obj = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/D:resourcetype"
-               "/C:calendar");
-
-       if (xp_obj != NULL) {
-               soup_uri = soup_message_get_uri (message);
-
-               if (soup_uri->path != NULL && *soup_uri->path != '\0') {
-                       gchar *slash;
-
-                       soup_uri = soup_uri_copy (soup_uri);
-
-                       slash = strrchr (soup_uri->path, '/');
-                       while (slash != NULL && slash != soup_uri->path) {
-
-                               if (slash[1] != '\0') {
-                                       slash[1] = '\0';
-                                       calendar_home_set =
-                                               g_strdup (soup_uri->path);
-                                       break;
-                               }
+               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/C:calendar", xpath_prop_prefix) ||
+                   e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/A:addressbook", 
xpath_prop_prefix)) {
+                       GSList *resources = NULL;
 
-                               slash[0] = '\0';
-                               slash = strrchr (soup_uri->path, '/');
+                       if (!g_hash_table_contains (wdd->covered_hrefs, href) &&
+                           e_webdav_session_list_sync (webdav, href, E_WEBDAV_DEPTH_THIS,
+                               E_WEBDAV_LIST_SUPPORTS | E_WEBDAV_LIST_DISPLAY_NAME | 
E_WEBDAV_LIST_DESCRIPTION | E_WEBDAV_LIST_COLOR,
+                               &resources, wdd->cancellable, wdd->error)) {
+                               e_webdav_discover_split_resources (wdd, resources);
+                               g_slist_free_full (resources, e_webdav_resource_free);
                        }
 
-                       soup_uri_free (soup_uri);
+                       g_hash_table_insert (wdd->covered_hrefs, g_strdup (href), GINT_TO_POINTER (1));
                }
-
-               xmlXPathFreeObject (xp_obj);
        }
 
-       if (calendar_home_set == NULL || *calendar_home_set == '\0') {
-               g_free (calendar_home_set);
-               xmlXPathFreeContext (xp_ctx);
-               xmlFreeDoc (doc);
-               return TRUE;
-       }
-
- get_collection_details:
-
-       xmlXPathFreeContext (xp_ctx);
-       xmlFreeDoc (doc);
-
-       if (!e_webdav_discover_get_calendar_collection_details (
-               session, message, calendar_home_set, source,
-               out_certificate_pem, out_certificate_errors, out_discovered_sources,
-               cancellable, error)) {
-               g_free (calendar_home_set);
-               return FALSE;
-       }
-
-       g_free (calendar_home_set);
-
        return TRUE;
-
- retry_propfind:
-
-       xmlXPathFreeContext (xp_ctx);
-       xmlFreeDoc (doc);
-
-       soup_uri = soup_uri_copy (soup_message_get_uri (message));
-       soup_uri_set_path (soup_uri, calendar_home_set);
-
-       /* Note that we omit "D:resourcetype", "D:current-user-principal"
-        * and "D:principal-URL" in order to short-circuit the recursion. */
-       message = e_webdav_discover_new_propfind (
-               session, soup_uri, DEPTH_1,
-               NS_CALDAV, XC ("calendar-home-set"),
-               NS_CALDAV, XC ("calendar-user-address-set"),
-               NULL);
-
-       e_soup_ssl_trust_connect (message, source);
-
-       /* This takes ownership of the message. */
-       soup_session_send_message (session, message);
-
-       if (message->status_code == SOUP_STATUS_BAD_REQUEST) {
-               g_clear_object (&message);
-
-               message = e_webdav_discover_new_propfind (
-                       session, soup_uri, DEPTH_0,
-                       NS_CALDAV, XC ("calendar-home-set"),
-                       NS_CALDAV, XC ("calendar-user-address-set"),
-                       NULL);
-
-               e_soup_ssl_trust_connect (message, source);
-               soup_session_send_message (session, message);
-       }
-
-       soup_uri_free (soup_uri);
-
-       g_free (calendar_home_set);
-
-       success = e_webdav_discover_process_calendar_home_set (session, message, source,
-               out_certificate_pem, out_certificate_errors, out_discovered_sources, 
out_calendar_user_addresses,
-               cancellable, error);
-
-       g_object_unref (message);
-
-       return success;
 }
 
-static void
-e_webdav_discover_process_addressbook_response_propstat (SoupMessage *message,
-                                                        xmlXPathContextPtr xp_ctx,
-                                                        gint response_index,
-                                                        gint propstat_index,
-                                                        GSList **out_discovered_sources)
+static gboolean
+e_webdav_discover_propfind_uri_sync (EWebDAVSession *webdav,
+                                    WebDAVDiscoverData *wdd,
+                                    const gchar *uri,
+                                    gboolean only_sets)
 {
-       xmlXPathObjectPtr xp_obj;
-       gchar *display_name;
-       gchar *description;
-       gchar *href_encoded;
-       gchar *status_line;
-       guint status;
+       EXmlDocument *xml;
        gboolean success;
-       EWebDAVDiscoveredSource *discovered_source;
-
-       if (!out_discovered_sources)
-               return;
-
-       status_line = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:status",
-               response_index,
-               propstat_index);
-
-       if (status_line == NULL)
-               return;
 
-       success = soup_headers_parse_status_line (
-               status_line, NULL, &status, NULL);
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (wdd != NULL, FALSE);
+       g_return_val_if_fail (uri && *uri, FALSE);
 
-       g_free (status_line);
+       if (g_hash_table_contains (wdd->covered_hrefs, uri))
+               return TRUE;
 
-       if (!success || status != SOUP_STATUS_OK)
-               return;
+       g_hash_table_insert (wdd->covered_hrefs, g_strdup (uri), GINT_TO_POINTER (1));
 
-       href_encoded = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:href",
-               response_index);
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       g_return_val_if_fail (xml != NULL, FALSE);
 
-       if (href_encoded == NULL)
-               return;
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
 
-       /* Make sure the resource is an addressbook. */
-
-       xp_obj = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/D:resourcetype"
-               "/A:addressbook",
-               response_index,
-               propstat_index);
-
-       if (xp_obj == NULL) {
-               g_free (href_encoded);
-               return;
+       if (!only_sets) {
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "resourcetype");
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "current-user-principal");
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "principal-URL");
        }
 
-       xmlXPathFreeObject (xp_obj);
-
-       /* Get the display name or fall back to the href. */
-
-       display_name = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/D:displayname",
-               response_index,
-               propstat_index);
-
-       if (display_name == NULL) {
-               gchar *href_decoded = soup_uri_decode (href_encoded);
-
-               if (href_decoded) {
-                       gchar *cp;
-
-                       /* Use the last non-empty path segment. */
-                       while ((cp = strrchr (href_decoded, '/')) != NULL) {
-                               if (*(cp + 1) == '\0')
-                                       *cp = '\0';
-                               else {
-                                       display_name = g_strdup (cp + 1);
-                                       break;
-                               }
-                       }
-               }
-
-               g_free (href_decoded);
+       if ((wdd->only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+           (wdd->only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | 
E_WEBDAV_DISCOVER_SUPPORTS_TASKS)) != 0)) {
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-home-set");
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-user-address-set");
        }
 
-       description = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response[%d]"
-               "/D:propstat[%d]"
-               "/D:prop"
-               "/A:addressbook-description",
-               response_index,
-               propstat_index);
-
-       discovered_source = g_new0 (EWebDAVDiscoveredSource, 1);
-       discovered_source->href = e_webdav_discover_make_href_full_uri (soup_message_get_uri (message), 
href_encoded);
-       discovered_source->supports = E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS;
-       discovered_source->display_name = g_strdup (display_name);
-       discovered_source->description = g_strdup (description);
-       discovered_source->color = NULL;
-
-       *out_discovered_sources = g_slist_prepend (*out_discovered_sources, discovered_source);
-
-       g_free (href_encoded);
-       g_free (display_name);
-       g_free (description);
-}
-
-static gboolean
-e_webdav_discover_get_addressbook_collection_details (SoupSession *session,
-                                                     SoupMessage *message,
-                                                     const gchar *path_or_uri,
-                                                     ESource *source,
-                                                     gchar **out_certificate_pem,
-                                                     GTlsCertificateFlags *out_certificate_errors,
-                                                     GSList **out_discovered_sources,
-                                                     GCancellable *cancellable,
-                                                     GError **error)
-{
-       xmlDocPtr doc;
-       xmlXPathContextPtr xp_ctx;
-       SoupURI *soup_uri;
-       GError *local_error = NULL;
-
-       if (g_cancellable_is_cancelled (cancellable))
-               return FALSE;
-
-       soup_uri = soup_uri_new (path_or_uri);
-       if (!soup_uri ||
-           !soup_uri_get_scheme (soup_uri) ||
-           !soup_uri_get_host (soup_uri) ||
-           !soup_uri_get_path (soup_uri) ||
-           !*soup_uri_get_scheme (soup_uri) ||
-           !*soup_uri_get_host (soup_uri) ||
-           !*soup_uri_get_path (soup_uri)) {
-               /* it's a path only, not full uri */
-               if (soup_uri)
-                       soup_uri_free (soup_uri);
-               soup_uri = soup_uri_copy (soup_message_get_uri (message));
-               soup_uri_set_path (soup_uri, path_or_uri);
+       if ((wdd->only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+           (wdd->only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0)) {
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-home-set");
        }
 
-       message = e_webdav_discover_new_propfind (
-               session, soup_uri, DEPTH_1,
-               NS_WEBDAV, XC ("displayname"),
-               NS_WEBDAV, XC ("resourcetype"),
-               NS_CARDDAV, XC ("addressbook-description"),
-               NULL);
-
-       e_soup_ssl_trust_connect (message, source);
+       e_xml_document_end_element (xml); /* prop */
 
-       /* This takes ownership of the message. */
-       soup_session_send_message (session, message);
-
-       soup_uri_free (soup_uri);
-
-       doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, 
out_certificate_errors, &local_error);
-       if (!doc) {
-               g_clear_object (&message);
-
-               if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
-                   g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
-                       /* Ignore these errors, but still propagate them. */
-                       g_propagate_error (error, local_error);
-                       return TRUE;
-               } else if (local_error) {
-                       g_propagate_error (error, local_error);
-               }
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_discover_traverse_propfind_response_cb, wdd, wdd->cancellable, wdd->error);
 
-               return FALSE;
-       }
+       g_clear_object (&xml);
 
-       xp_ctx = xmlXPathNewContext (doc);
-       xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
+       return success;
+}
 
-       e_webdav_discover_traverse_responses (message, xp_ctx, out_discovered_sources,
-               e_webdav_discover_process_addressbook_response_propstat);
+typedef struct _EWebDAVDiscoverContext {
+       ESource *source;
+       gchar *url_use_path;
+       guint32 only_supports;
+       ENamedParameters *credentials;
+       gchar *out_certificate_pem;
+       GTlsCertificateFlags out_certificate_errors;
+       GSList *out_discovered_sources;
+       GSList *out_calendar_user_addresses;
+} EWebDAVDiscoverContext;
 
-       xmlXPathFreeContext (xp_ctx);
-       xmlFreeDoc (doc);
+static EWebDAVDiscoverContext *
+e_webdav_discover_context_new (ESource *source,
+                              const gchar *url_use_path,
+                              guint32 only_supports,
+                              const ENamedParameters *credentials)
+{
+       EWebDAVDiscoverContext *context;
 
-       g_clear_object (&message);
+       context = g_new0 (EWebDAVDiscoverContext, 1);
+       context->source = g_object_ref (source);
+       context->url_use_path = g_strdup (url_use_path);
+       context->only_supports = only_supports;
+       context->credentials = e_named_parameters_new_clone (credentials);
+       context->out_certificate_pem = NULL;
+       context->out_certificate_errors = 0;
+       context->out_discovered_sources = NULL;
+       context->out_calendar_user_addresses = NULL;
 
-       return TRUE;
+       return context;
 }
 
-static gboolean
-e_webdav_discover_process_addressbook_home_set (SoupSession *session,
-                                               SoupMessage *message,
-                                               ESource *source,
-                                               gchar **out_certificate_pem,
-                                               GTlsCertificateFlags *out_certificate_errors,
-                                               GSList **out_discovered_sources,
-                                               GCancellable *cancellable,
-                                               GError **error)
+static void
+e_webdav_discover_context_free (gpointer ptr)
 {
-       SoupURI *soup_uri;
-       xmlDocPtr doc;
-       xmlXPathContextPtr xp_ctx;
-       xmlXPathObjectPtr xp_obj;
-       gchar *addressbook_home_set;
-       GError *local_error = NULL;
-       gboolean success;
-
-       g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
-       g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
-       g_return_val_if_fail (out_discovered_sources != NULL, FALSE);
-       g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
-
-       if (g_cancellable_is_cancelled (cancellable))
-               return FALSE;
-
-       doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem, 
out_certificate_errors, &local_error);
-       if (!doc) {
-               if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
-                   g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
-                       /* Ignore these errors, but still propagate them. */
-                       g_propagate_error (error, local_error);
-                       return TRUE;
-               } else if (local_error) {
-                       g_propagate_error (error, local_error);
-               }
-
-               return FALSE;
-       }
-
-       xp_ctx = xmlXPathNewContext (doc);
-       xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
-       xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
-
-       /* Try to find the addressbook home URL using the
-        * following properties in order of preference:
-        *
-        *   "A:addressbook-home-set"
-        *   "D:current-user-principal"
-        *   "D:principal-URL"
-        *
-        * If the second or third URL preference is used, rerun
-        * the PROPFIND method on that URL at Depth=1 in hopes
-        * of getting a proper "A:addressbook-home-set" property.
-        */
-
-       /* FIXME There can be multiple "D:href" elements for a
-        *       "A:addressbook-home-set".  We're only processing
-        *       the first one.  Need to iterate over them. */
-
-       addressbook_home_set = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/A:addressbook-home-set"
-               "/D:href");
-
-       if (addressbook_home_set != NULL)
-               goto get_collection_details;
-
-       g_free (addressbook_home_set);
-
-       addressbook_home_set = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/D:current-user-principal"
-               "/D:href");
-
-       if (addressbook_home_set != NULL)
-               goto retry_propfind;
-
-       g_free (addressbook_home_set);
-
-       addressbook_home_set = e_webdav_discover_get_xpath_string (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/D:principal-URL"
-               "/D:href");
-
-       if (addressbook_home_set != NULL)
-               goto retry_propfind;
-
-       g_free (addressbook_home_set);
-       addressbook_home_set = NULL;
-
-       /* None of the aforementioned properties are present.  If the
-        * user-supplied CardDAV URL is an addressbook resource, use that. */
-
-       xp_obj = e_webdav_discover_get_xpath (
-               xp_ctx,
-               "/D:multistatus"
-               "/D:response"
-               "/D:propstat"
-               "/D:prop"
-               "/D:resourcetype"
-               "/A:addressbook");
-
-       if (xp_obj != NULL) {
-               soup_uri = soup_message_get_uri (message);
-
-               if (soup_uri->path != NULL && *soup_uri->path != '\0') {
-                       gchar *slash;
-
-                       soup_uri = soup_uri_copy (soup_uri);
-
-                       slash = strrchr (soup_uri->path, '/');
-                       while (slash != NULL && slash != soup_uri->path) {
-
-                               if (slash[1] != '\0') {
-                                       slash[1] = '\0';
-                                       addressbook_home_set =
-                                               g_strdup (soup_uri->path);
-                                       break;
-                               }
-
-                               slash[0] = '\0';
-                               slash = strrchr (soup_uri->path, '/');
-                       }
-
-                       soup_uri_free (soup_uri);
-               }
-
-               xmlXPathFreeObject (xp_obj);
-       }
-
-       if (addressbook_home_set == NULL || *addressbook_home_set == '\0') {
-               g_free (addressbook_home_set);
-               xmlXPathFreeContext (xp_ctx);
-               xmlFreeDoc (doc);
-               return TRUE;
-       }
-
- get_collection_details:
-
-       xmlXPathFreeContext (xp_ctx);
-       xmlFreeDoc (doc);
-
-       if (!e_webdav_discover_get_addressbook_collection_details (
-               session, message, addressbook_home_set, source,
-               out_certificate_pem, out_certificate_errors, out_discovered_sources,
-               cancellable, error)) {
-               g_free (addressbook_home_set);
-               return FALSE;
-       }
-
-       g_free (addressbook_home_set);
-
-       return TRUE;
-
- retry_propfind:
-
-       xmlXPathFreeContext (xp_ctx);
-       xmlFreeDoc (doc);
-
-       soup_uri = soup_uri_copy (soup_message_get_uri (message));
-       soup_uri_set_path (soup_uri, addressbook_home_set);
-
-       /* Note that we omit "D:resourcetype", "D:current-user-principal"
-        * and "D:principal-URL" in order to short-circuit the recursion. */
-       message = e_webdav_discover_new_propfind (
-               session, soup_uri, DEPTH_1,
-               NS_CARDDAV, XC ("addressbook-home-set"),
-               NULL);
-
-       e_soup_ssl_trust_connect (message, source);
-
-       /* This takes ownership of the message. */
-       soup_session_send_message (session, message);
-
-       soup_uri_free (soup_uri);
-
-       g_free (addressbook_home_set);
-
-       success = e_webdav_discover_process_addressbook_home_set (session, message, source,
-               out_certificate_pem, out_certificate_errors, out_discovered_sources,
-               cancellable, error);
+       EWebDAVDiscoverContext *context = ptr;
 
-       g_object_unref (message);
+       if (!context)
+               return;
 
-       return success;
+       g_clear_object (&context->source);
+       g_free (context->url_use_path);
+       e_named_parameters_free (context->credentials);
+       g_free (context->out_certificate_pem);
+       e_webdav_discover_free_discovered_sources (context->out_discovered_sources);
+       g_slist_free_full (context->out_calendar_user_addresses, g_free);
+       g_free (context);
 }
 
 static void
@@ -1779,13 +526,6 @@ e_webdav_discover_sources_finish (ESource *source,
        return g_task_propagate_boolean (G_TASK (result), error);
 }
 
-static void
-e_webdav_discover_cancelled_cb (GCancellable *cancellable,
-                               SoupSession *session)
-{
-       soup_session_abort (session);
-}
-
 /**
  * e_webdav_discover_sources_sync:
  * @source: an #ESource from which to take connection details
@@ -1843,11 +583,8 @@ e_webdav_discover_sources_sync (ESource *source,
                                GError **error)
 {
        ESourceWebdav *webdav_extension;
-       AuthenticateData auth_data;
-       SoupSession *session;
-       SoupMessage *message;
+       EWebDAVSession *webdav;
        SoupURI *soup_uri;
-       gulong cancelled_handler_id = 0, authenticate_handler_id;
        gboolean success;
 
        g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
@@ -1889,166 +626,76 @@ e_webdav_discover_sources_sync (ESource *source,
                g_string_free (new_path, TRUE);
        }
 
-       session = soup_session_new ();
-       g_object_set (
-               session,
-               SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
-               NULL);
-
-       message = e_webdav_discover_new_propfind (
-               session, soup_uri, DEPTH_0,
-               NS_WEBDAV, XC ("resourcetype"),
-               NS_WEBDAV, XC ("current-user-principal"),
-               NS_WEBDAV, XC ("principal-URL"),
-               NS_CALDAV, XC ("calendar-home-set"),
-               NS_CALDAV, XC ("calendar-user-address-set"),
-               NS_CARDDAV, XC ("addressbook-home-set"),
-               NS_CARDDAV, XC ("principal-address"),
-               NULL);
-
-       if (!message) {
-               soup_uri_free (soup_uri);
-               g_object_unref (session);
-               return FALSE;
-       }
+       webdav = e_webdav_session_new (source);
+       e_soup_session_setup_logging (E_SOUP_SESSION (webdav), g_getenv ("WEBDAV_DEBUG"));
+       e_soup_session_set_credentials (E_SOUP_SESSION (webdav), credentials);
 
-       if (g_getenv ("WEBDAV_DEBUG") != NULL) {
-               SoupLogger *logger;
+       if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               WebDAVDiscoverData wdd;
+               gchar *uri;
+               GError *local_error = NULL;
 
-               logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, 100 * 1024 * 1024);
-               soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
-               g_object_unref (logger);
-       }
+               wdd.covered_hrefs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+               wdd.addressbooks = NULL;
+               wdd.calendars = NULL;
+               wdd.only_supports = only_supports;
+               wdd.out_calendar_user_addresses = out_calendar_user_addresses;
+               wdd.cancellable = cancellable;
+               wdd.error = &local_error;
 
-       if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
-               SoupSessionFeature *feature;
-               ESourceAuthentication *auth_extension;
-               gchar *auth_method;
+               uri = soup_uri_to_string (soup_uri, FALSE);
 
-               feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER);
+               success = uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri, FALSE);
 
-               success = TRUE;
+               g_free (uri);
 
-               auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
-               auth_method = e_source_authentication_dup_method (auth_extension);
+               if (success && !wdd.calendars && (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+                  (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | 
E_WEBDAV_DISCOVER_SUPPORTS_TASKS)) != 0) &&
+                  (!soup_uri_get_path (soup_uri) || !strstr (soup_uri_get_path (soup_uri), 
"/.well-known/"))) {
+                       gchar *saved_path;
 
-               if (g_strcmp0 (auth_method, "OAuth2") == 0 || g_strcmp0 (auth_method, "Google") == 0) {
-                       SoupAuth *soup_auth;
+                       saved_path = g_strdup (soup_uri_get_path (soup_uri));
 
-                       soup_auth = g_object_new (E_TYPE_SOUP_AUTH_BEARER, SOUP_AUTH_HOST, soup_uri->host, 
NULL);
+                       soup_uri_set_path (soup_uri, "/.well-known/caldav");
 
-                       success = e_webdav_discover_setup_bearer_auth (source, credentials,
-                               E_SOUP_AUTH_BEARER (soup_auth), cancellable, error);
+                       uri = soup_uri_to_string (soup_uri, FALSE);
 
-                       if (success) {
-                               soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
-                               soup_auth_manager_use_auth (
-                                       SOUP_AUTH_MANAGER (feature),
-                                       soup_uri, soup_auth);
-                       }
+                       /* Ignore errors here */
+                       wdd.error = NULL;
+                       wdd.only_supports = E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | 
E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
 
-                       g_object_unref (soup_auth);
-               }
+                       success = uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri, 
FALSE);
 
-               g_free (auth_method);
+                       g_free (uri);
 
-               if (!success) {
-                       soup_uri_free (soup_uri);
-                       g_object_unref (message);
-                       g_object_unref (session);
-                       return FALSE;
+                       soup_uri_set_path (soup_uri, saved_path);
+                       g_free (saved_path);
                }
-       }
-
-       auth_data.source = source;
-       auth_data.credentials = credentials;
-
-       authenticate_handler_id = g_signal_connect (session, "authenticate",
-               G_CALLBACK (e_webdav_discover_authenticate_cb), &auth_data);
-
-       if (cancellable)
-               cancelled_handler_id = g_cancellable_connect (cancellable, G_CALLBACK 
(e_webdav_discover_cancelled_cb), session, NULL);
 
-       if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
-               GSList *calendars = NULL, *addressbooks = NULL;
-               GError *local_error = NULL;
-
-               e_soup_ssl_trust_connect (message, source);
-               soup_session_send_message (session, message);
-
-               success = TRUE;
-
-               if (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
-                  (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | 
E_WEBDAV_DISCOVER_SUPPORTS_TASKS)) != 0) {
-                       success = e_webdav_discover_process_calendar_home_set (session, message, source, 
out_certificate_pem,
-                               out_certificate_errors, &calendars, out_calendar_user_addresses, cancellable, 
&local_error);
-
-                       if (!calendars && !g_cancellable_is_cancelled (cancellable) && (!soup_uri_get_path 
(soup_uri) ||
-                           !strstr (soup_uri_get_path (soup_uri), "/.well-known/"))) {
-                               SoupMessage *well_known_message;
-                               gchar *saved_path;
-
-                               saved_path = g_strdup (soup_uri_get_path (soup_uri));
+               if (success && !wdd.addressbooks && (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+                   (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0) &&
+                   (!soup_uri_get_path (soup_uri) || !strstr (soup_uri_get_path (soup_uri), 
"/.well-known/"))) {
+                       gchar *saved_path;
 
-                               soup_uri_set_path (soup_uri, "/.well-known/caldav");
+                       saved_path = g_strdup (soup_uri_get_path (soup_uri));
 
-                               well_known_message = e_webdav_discover_new_propfind (
-                                       session, soup_uri, DEPTH_0,
-                                       NS_WEBDAV, XC ("resourcetype"),
-                                       NS_WEBDAV, XC ("current-user-principal"),
-                                       NS_WEBDAV, XC ("principal-URL"),
-                                       NS_CALDAV, XC ("calendar-home-set"),
-                                       NS_CALDAV, XC ("calendar-user-address-set"),
-                                       NULL);
+                       soup_uri_set_path (soup_uri, "/.well-known/carddav");
 
-                               soup_uri_set_path (soup_uri, saved_path);
-                               g_free (saved_path);
+                       uri = soup_uri_to_string (soup_uri, FALSE);
 
-                               if (well_known_message) {
-                                       e_soup_ssl_trust_connect (well_known_message, source);
-                                       soup_session_send_message (session, well_known_message);
+                       /* Ignore errors here */
+                       wdd.error = NULL;
+                       wdd.only_supports = E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS;
 
-                                       /* Ignore errors here */
-                                       e_webdav_discover_process_calendar_home_set (session, 
well_known_message, source, out_certificate_pem,
-                                               out_certificate_errors, &calendars, 
out_calendar_user_addresses, cancellable, NULL);
+                       success = uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri, 
FALSE);
 
-                                       g_clear_object (&well_known_message);
-                               }
-                       }
-               }
+                       g_free (uri);
 
-               if (success && (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
-                   (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0)) {
-                       success = e_webdav_discover_process_addressbook_home_set (session, message, source, 
out_certificate_pem,
-                               out_certificate_errors, &addressbooks, cancellable, local_error ? NULL : 
&local_error);
-
-                       if (!addressbooks && !g_cancellable_is_cancelled (cancellable) && (!soup_uri_get_path 
(soup_uri) ||
-                           !strstr (soup_uri_get_path (soup_uri), "/.well-known/"))) {
-                               g_clear_object (&message);
-
-                               soup_uri_set_path (soup_uri, "/.well-known/carddav");
-
-                               message = e_webdav_discover_new_propfind (
-                                       session, soup_uri, DEPTH_0,
-                                       NS_WEBDAV, XC ("resourcetype"),
-                                       NS_WEBDAV, XC ("current-user-principal"),
-                                       NS_WEBDAV, XC ("principal-URL"),
-                                       NS_CARDDAV, XC ("addressbook-home-set"),
-                                       NS_CARDDAV, XC ("principal-address"),
-                                       NULL);
-
-                               if (message) {
-                                       e_soup_ssl_trust_connect (message, source);
-                                       soup_session_send_message (session, message);
-
-                                       /* Ignore errors here */
-                                       e_webdav_discover_process_addressbook_home_set (session, message, 
source, out_certificate_pem,
-                                               out_certificate_errors, &addressbooks, cancellable, NULL);
-                               }
-                       }
+                       soup_uri_set_path (soup_uri, saved_path);
+                       g_free (saved_path);
                }
 
-               if (calendars || addressbooks) {
+               if (wdd.calendars || wdd.addressbooks) {
                        success = TRUE;
                        g_clear_error (&local_error);
                } else if (local_error) {
@@ -2056,13 +703,13 @@ e_webdav_discover_sources_sync (ESource *source,
                }
 
                if (out_discovered_sources) {
-                       if (calendars)
-                               *out_discovered_sources = g_slist_concat (*out_discovered_sources, calendars);
-                       if (addressbooks)
-                               *out_discovered_sources = g_slist_concat (*out_discovered_sources, 
addressbooks);
+                       if (wdd.calendars)
+                               *out_discovered_sources = g_slist_concat (*out_discovered_sources, 
wdd.calendars);
+                       if (wdd.addressbooks)
+                               *out_discovered_sources = g_slist_concat (*out_discovered_sources, 
wdd.addressbooks);
                } else {
-                       e_webdav_discover_free_discovered_sources (calendars);
-                       e_webdav_discover_free_discovered_sources (addressbooks);
+                       e_webdav_discover_free_discovered_sources (wdd.calendars);
+                       e_webdav_discover_free_discovered_sources (wdd.addressbooks);
                }
 
                if (out_calendar_user_addresses && *out_calendar_user_addresses)
@@ -2070,19 +717,17 @@ e_webdav_discover_sources_sync (ESource *source,
 
                if (out_discovered_sources && *out_discovered_sources)
                        *out_discovered_sources = g_slist_reverse (*out_discovered_sources);
+
+               g_hash_table_destroy (wdd.covered_hrefs);
        } else {
                success = FALSE;
        }
 
-       if (cancellable && cancelled_handler_id)
-               g_cancellable_disconnect (cancellable, cancelled_handler_id);
-
-       if (authenticate_handler_id)
-               g_signal_handler_disconnect (session, authenticate_handler_id);
+       if (!success)
+               e_soup_session_get_ssl_error_details (E_SOUP_SESSION (webdav), out_certificate_pem, 
out_certificate_errors);
 
        soup_uri_free (soup_uri);
-       g_clear_object (&message);
-       g_object_unref (session);
+       g_object_unref (webdav);
 
        return success;
 }
diff --git a/src/libedataserver/e-webdav-session.c b/src/libedataserver/e-webdav-session.c
new file mode 100644
index 0000000..65762ca
--- /dev/null
+++ b/src/libedataserver/e-webdav-session.c
@@ -0,0 +1,4983 @@
+/* -*- 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-webdav-session
+ * @include: libedataserver/libedataserver.h
+ * @short_description: A WebDAV, CalDAV and CardDAV session
+ *
+ * The #EWebDAVSession is a class to work with WebDAV (RFC 4918),
+ * CalDAV (RFC 4791) or CardDAV (RFC 6352) servers, providing API
+ * for common requests/responses, on top of an #ESoupSession. It
+ * supports also Access Control Protocol (RFC 3744).
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <stdio.h>
+#include <glib/gi18n-lib.h>
+
+#include "camel/camel.h"
+
+#include "e-source-authentication.h"
+#include "e-source-webdav.h"
+#include "e-xml-utils.h"
+
+#include "e-webdav-session.h"
+
+#define BUFFER_SIZE 16384
+
+struct _EWebDAVSessionPrivate {
+       gboolean dummy;
+};
+
+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)
+G_DEFINE_BOXED_TYPE (EWebDAVPropertyChange, e_webdav_property_change, e_webdav_property_change_copy, 
e_webdav_property_change_free)
+G_DEFINE_BOXED_TYPE (EWebDAVPrivilege, e_webdav_privilege, e_webdav_privilege_copy, e_webdav_privilege_free)
+G_DEFINE_BOXED_TYPE (EWebDAVAccessControlEntry, e_webdav_access_control_entry, 
e_webdav_access_control_entry_copy, e_webdav_access_control_entry_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:
+ * @src: (nullable): an #EWebDAVResource to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVResource prefilled with
+ *    the same values as @src, or %NULL, when @src is %NULL.
+ *    Free it with e_webdav_resource_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVResource *
+e_webdav_resource_copy (const EWebDAVResource *src)
+{
+       if (!src)
+               return NULL;
+
+       return e_webdav_resource_new (src->kind,
+               src->supports,
+               src->href,
+               src->etag,
+               src->display_name,
+               src->content_type,
+               src->content_length,
+               src->creation_date,
+               src->last_modified,
+               src->description,
+               src->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 nothing, 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 EWebDAVPropertyChange *
+e_webdav_property_change_new (EWebDAVPropertyChangeKind kind,
+                             const gchar *ns_uri,
+                             const gchar *name,
+                             const gchar *value)
+{
+       EWebDAVPropertyChange *change;
+
+       change = g_new0 (EWebDAVPropertyChange, 1);
+       change->kind = kind;
+       change->ns_uri = g_strdup (ns_uri);
+       change->name = g_strdup (name);
+       change->value = g_strdup (value);
+
+       return change;
+}
+
+/**
+ * e_webdav_property_change_new_set:
+ * @ns_uri: namespace URI of the property
+ * @name: name of the property
+ * @value: (nullable): value of the property, or %NULL for empty value
+ *
+ * Creates a new #EWebDAVPropertyChange of kind %E_WEBDAV_PROPERTY_SET,
+ * which is used to modify or set the property value. The @value is a string
+ * representation of the value to store. It can be %NULL, but it means
+ * an empty value, not to remove it. To remove property use
+ * e_webdav_property_change_new_remove() instead.
+ *
+ * Returns: (transfer full): A new #EWebDAVPropertyChange. Free it with
+ *    e_webdav_property_change_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPropertyChange *
+e_webdav_property_change_new_set (const gchar *ns_uri,
+                                 const gchar *name,
+                                 const gchar *value)
+{
+       g_return_val_if_fail (ns_uri != NULL, NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       return e_webdav_property_change_new (E_WEBDAV_PROPERTY_SET, ns_uri, name, value);
+}
+
+/**
+ * e_webdav_property_change_new_remove:
+ * @ns_uri: namespace URI of the property
+ * @name: name of the property
+ *
+ * Creates a new #EWebDAVPropertyChange of kind %E_WEBDAV_PROPERTY_REMOVE,
+ * which is used to remove the given property. To change property value
+ * use e_webdav_property_change_new_set() instead.
+ *
+ * Returns: (transfer full): A new #EWebDAVPropertyChange. Free it with
+ *    e_webdav_property_change_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPropertyChange *
+e_webdav_property_change_new_remove (const gchar *ns_uri,
+                                    const gchar *name)
+{
+       g_return_val_if_fail (ns_uri != NULL, NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       return e_webdav_property_change_new (E_WEBDAV_PROPERTY_REMOVE, ns_uri, name, NULL);
+}
+
+/**
+ * e_webdav_property_change_copy:
+ * @src: (nullable): an #EWebDAVPropertyChange to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVPropertyChange prefilled with
+ *    the same values as @src, or %NULL, when @src is %NULL.
+ *    Free it with e_webdav_property_change_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPropertyChange *
+e_webdav_property_change_copy (const EWebDAVPropertyChange *src)
+{
+       if (!src)
+               return NULL;
+
+       return e_webdav_property_change_new (
+               src->kind,
+               src->ns_uri,
+               src->name,
+               src->value);
+}
+
+/**
+ * e_webdav_property_change_free:
+ * @ptr: (nullable): an #EWebDAVPropertyChange
+ *
+ * Frees an #EWebDAVPropertyChange previously created with e_webdav_property_change_new_set(),
+ * e_webdav_property_change_new_remove() or or e_webdav_property_change_copy().
+ * The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_property_change_free (gpointer ptr)
+{
+       EWebDAVPropertyChange *change = ptr;
+
+       if (change) {
+               g_free (change->ns_uri);
+               g_free (change->name);
+               g_free (change->value);
+               g_free (change);
+       }
+}
+
+/**
+ * e_webdav_privilege_new:
+ * @ns_uri: (nullable): a namespace URI
+ * @name: (nullable): element name
+ * @description: (nullable): human read-able description, or %NULL
+ * @kind: an #EWebDAVPrivilegeKind
+ * @hint: an #EWebDAVPrivilegeHint
+ *
+ * Describes one privilege entry. The @hint can be %E_WEBDAV_PRIVILEGE_HINT_UNKNOWN
+ * for privileges which are not known to the #EWebDAVSession. It's possible, because
+ * the servers can define their own privileges. The hint is also tried to pair with
+ * known hnts when it's %E_WEBDAV_PRIVILEGE_HINT_UNKNOWN.
+ *
+ * The @ns_uri and @name can be %NULL only if the @hint is one of the known
+ * privileges. Otherwise it's an error to pass either of the two as %NULL.
+ *
+ * Returns: (transfer full): A newly created #EWebDAVPrivilege, prefilled with
+ *    given values. Free it with e_webdav_privilege_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPrivilege *
+e_webdav_privilege_new (const gchar *ns_uri,
+                       const gchar *name,
+                       const gchar *description,
+                       EWebDAVPrivilegeKind kind,
+                       EWebDAVPrivilegeHint hint)
+{
+       EWebDAVPrivilege *privilege;
+
+       if ((!ns_uri || !name) && hint != E_WEBDAV_PRIVILEGE_HINT_UNKNOWN) {
+               const gchar *use_ns_uri = NULL, *use_name = NULL;
+
+               switch (hint) {
+               case E_WEBDAV_PRIVILEGE_HINT_UNKNOWN:
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_READ:
+                       use_name = "read";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_WRITE:
+                       use_name = "write";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_WRITE_PROPERTIES:
+                       use_name = "write-properties";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT:
+                       use_name = "write-content";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_UNLOCK:
+                       use_name = "unlock";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_READ_ACL:
+                       use_name = "read-acl";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_WRITE_ACL:
+                       use_name = "write-acl";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_READ_CURRENT_USER_PRIVILEGE_SET:
+                       use_name = "read-current-user-privilege-set";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_BIND:
+                       use_name = "bind";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_UNBIND:
+                       use_name = "unbind";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_ALL:
+                       use_name = "all";
+                       break;
+               case E_WEBDAV_PRIVILEGE_HINT_CALDAV_READ_FREE_BUSY:
+                       use_ns_uri = E_WEBDAV_NS_CALDAV;
+                       use_name = "read-free-busy";
+                       break;
+               }
+
+               if (use_name) {
+                       ns_uri = use_ns_uri ? use_ns_uri : E_WEBDAV_NS_DAV;
+                       name = use_name;
+               }
+       }
+
+       g_return_val_if_fail (ns_uri != NULL, NULL);
+       g_return_val_if_fail (name != NULL, NULL);
+
+       if (hint == E_WEBDAV_PRIVILEGE_HINT_UNKNOWN) {
+               if (g_str_equal (ns_uri, E_WEBDAV_NS_DAV)) {
+                       if (g_str_equal (name, "read"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_READ;
+                       else if (g_str_equal (name, "write"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_WRITE;
+                       else if (g_str_equal (name, "write-properties"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_WRITE_PROPERTIES;
+                       else if (g_str_equal (name, "write-content"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT;
+                       else if (g_str_equal (name, "unlock"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_UNLOCK;
+                       else if (g_str_equal (name, "read-acl"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_READ_ACL;
+                       else if (g_str_equal (name, "write-acl"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_WRITE_ACL;
+                       else if (g_str_equal (name, "read-current-user-privilege-set"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_READ_CURRENT_USER_PRIVILEGE_SET;
+                       else if (g_str_equal (name, "bind"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_BIND;
+                       else if (g_str_equal (name, "unbind"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_UNBIND;
+                       else if (g_str_equal (name, "all"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_ALL;
+               } else if (g_str_equal (ns_uri, E_WEBDAV_NS_CALDAV)) {
+                       if (g_str_equal (name, "read-free-busy"))
+                               hint = E_WEBDAV_PRIVILEGE_HINT_CALDAV_READ_FREE_BUSY;
+               }
+       }
+
+       privilege = g_new (EWebDAVPrivilege, 1);
+       privilege->ns_uri = g_strdup (ns_uri);
+       privilege->name = g_strdup (name);
+       privilege->description = g_strdup (description);
+       privilege->kind = kind;
+       privilege->hint = hint;
+
+       return privilege;
+}
+
+/**
+ * e_webdav_privilege_copy:
+ * @src: (nullable): an #EWebDAVPrivilege to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVPrivilege prefilled with
+ *    the same values as @src, or %NULL, when @src is %NULL.
+ *    Free it with e_webdav_privilege_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPrivilege *
+e_webdav_privilege_copy (const EWebDAVPrivilege *src)
+{
+       if (!src)
+               return NULL;
+
+       return e_webdav_privilege_new (
+               src->ns_uri,
+               src->name,
+               src->description,
+               src->kind,
+               src->hint);
+}
+
+/**
+ * e_webdav_privilege_free:
+ * @ptr: (nullable): an #EWebDAVPrivilege
+ *
+ * Frees an #EWebDAVPrivilege previously created with e_webdav_privilege_new()
+ * or e_webdav_privilege_copy(). The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_privilege_free (gpointer ptr)
+{
+       EWebDAVPrivilege *privilege = ptr;
+
+       if (privilege) {
+               g_free (privilege->ns_uri);
+               g_free (privilege->name);
+               g_free (privilege->description);
+               g_free (privilege);
+       }
+}
+
+/**
+ * e_webdav_access_control_entry_new:
+ * @principal_kind: an #EWebDAVACEPrincipalKind
+ * @principal_href: (nullable): principal href; should be set only if @principal_kind is 
@E_WEBDAV_ACE_PRINCIPAL_HREF
+ * @flags: bit-or of #EWebDAVACEFlag values
+ * @inherited_href: (nullable): href of the resource from which inherits; should be set only if @flags 
contain E_WEBDAV_ACE_FLAG_INHERITED
+ *
+ * Describes one Access Control Entry (ACE).
+ *
+ * The @flags should always contain either %E_WEBDAV_ACE_FLAG_GRANT or
+ * %E_WEBDAV_ACE_FLAG_DENY value.
+ *
+ * Use e_webdav_access_control_entry_append_privilege() to add respective
+ * privileges to the entry.
+ *
+ * Returns: (transfer full): A newly created #EWebDAVAccessControlEntry, prefilled with
+ *    given values. Free it with e_webdav_access_control_entry_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVAccessControlEntry *
+e_webdav_access_control_entry_new (EWebDAVACEPrincipalKind principal_kind,
+                                  const gchar *principal_href,
+                                  guint32 flags,
+                                  const gchar *inherited_href)
+{
+       EWebDAVAccessControlEntry *ace;
+
+       if (principal_kind == E_WEBDAV_ACE_PRINCIPAL_HREF)
+               g_return_val_if_fail (principal_href != NULL, NULL);
+       else
+               g_return_val_if_fail (principal_href == NULL, NULL);
+
+       if ((flags & E_WEBDAV_ACE_FLAG_INHERITED) != 0)
+               g_return_val_if_fail (inherited_href != NULL, NULL);
+       else
+               g_return_val_if_fail (inherited_href == NULL, NULL);
+
+       ace = g_new0 (EWebDAVAccessControlEntry, 1);
+       ace->principal_kind = principal_kind;
+       ace->principal_href = g_strdup (principal_href);
+       ace->flags = flags;
+       ace->inherited_href = g_strdup (inherited_href);
+       ace->privileges = NULL;
+
+       return ace;
+}
+
+/**
+ * e_webdav_access_control_entry_copy:
+ * @src: (nullable): an #EWebDAVAccessControlEntry to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVAccessControlEntry prefilled with
+ *    the same values as @src, or %NULL, when @src is %NULL.
+ *    Free it with e_webdav_access_control_entry_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVAccessControlEntry *
+e_webdav_access_control_entry_copy (const EWebDAVAccessControlEntry *src)
+{
+       EWebDAVAccessControlEntry *ace;
+       GSList *link;
+
+       if (!src)
+               return NULL;
+
+       ace = e_webdav_access_control_entry_new (
+               src->principal_kind,
+               src->principal_href,
+               src->flags,
+               src->inherited_href);
+       if (!ace)
+               return NULL;
+
+       for (link = src->privileges; link; link = g_slist_next (link)) {
+               EWebDAVPrivilege *privilege = link->data;
+
+               if (privilege)
+                       ace->privileges = g_slist_prepend (ace->privileges, e_webdav_privilege_copy 
(privilege));
+       }
+
+       ace->privileges = g_slist_reverse (ace->privileges);
+
+       return ace;
+}
+
+/**
+ * e_webdav_access_control_entry_free:
+ * @ptr: (nullable): an #EWebDAVAccessControlEntry
+ *
+ * Frees an #EWebDAVAccessControlEntry previously created with e_webdav_access_control_entry_new()
+ * or e_webdav_access_control_entry_copy(). The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_access_control_entry_free (gpointer ptr)
+{
+       EWebDAVAccessControlEntry *ace = ptr;
+
+       if (ace) {
+               g_free (ace->principal_href);
+               g_free (ace->inherited_href);
+               g_slist_free_full (ace->privileges, e_webdav_privilege_free);
+               g_free (ace);
+       }
+}
+
+/**
+ * e_webdav_access_control_entry_append_privilege:
+ * @ace: an #EWebDAVAccessControlEntry
+ * @privilege: (transfer full): an #EWebDAVPrivilege
+ *
+ * Appends a new @privilege to the list of privileges for the @ace.
+ * The function assumes ownership of the @privilege, which is freed
+ * together with the @ace.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_access_control_entry_append_privilege (EWebDAVAccessControlEntry *ace,
+                                               EWebDAVPrivilege *privilege)
+{
+       g_return_if_fail (ace != NULL);
+       g_return_if_fail (privilege != NULL);
+
+       ace->privileges = g_slist_append (ace->privileges, privilege);
+}
+
+/**
+ * e_webdav_access_control_entry_get_privileges:
+ * @ace: an #EWebDAVAccessControlEntry
+ *
+ * Returns: (element-type EWebDAVPrivilege) (transfer none): A #GSList of #EWebDAVPrivilege
+ *    with the list of privileges for the @ace. The reurned #GSList, together with its data
+ *    is owned by the @ace.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_webdav_access_control_entry_get_privileges (EWebDAVAccessControlEntry *ace)
+{
+       g_return_val_if_fail (ace != NULL, NULL);
+
+       return ace->privileges;
+}
+
+static void
+e_webdav_session_class_init (EWebDAVSessionClass *klass)
+{
+       g_type_class_add_private (klass, sizeof (EWebDAVSessionPrivate));
+}
+
+static void
+e_webdav_session_init (EWebDAVSession *webdav)
+{
+       webdav->priv = G_TYPE_INSTANCE_GET_PRIVATE (webdav, E_TYPE_WEBDAV_SESSION, EWebDAVSessionPrivate);
+}
+
+/**
+ * e_webdav_session_new:
+ * @source: an #ESource
+ *
+ * Creates a new #EWebDAVSession associated with given @source. It's
+ * a user's error to try to create the #EWebDAVSession for a source
+ * which doesn't have #ESourceWebdav extension properly defined.
+ *
+ * Returns: (transfer full): a new #EWebDAVSession; free it with g_object_unref(),
+ *    when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVSession *
+e_webdav_session_new (ESource *source)
+{
+       g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+       g_return_val_if_fail (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND), NULL);
+
+       return g_object_new (E_TYPE_WEBDAV_SESSION,
+               "source", source,
+               NULL);
+}
+
+/**
+ * e_webdav_session_new_request:
+ * @webdav: an #EWebDAVSession
+ * @method: an HTTP method
+ * @uri: (nullable): URI to create the request for, or %NULL to read from #ESource
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: (transfer full): A new #SoupRequestHTTP for the given @uri, or, when %NULL,
+ *    for the URI stored in the associated #ESource. Free the returned structure
+ *    with g_object_unref(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+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 gboolean
+e_webdav_session_extract_propstat_error_cb (EWebDAVSession *webdav,
+                                           xmlXPathContextPtr xpath_ctx,
+                                           const gchar *xpath_prop_prefix,
+                                           const SoupURI *request_uri,
+                                           const gchar *href,
+                                           guint status_code,
+                                           gpointer user_data)
+{
+       GError **error = user_data;
+
+       g_return_val_if_fail (error != NULL, FALSE);
+
+       if (!xpath_prop_prefix)
+               return TRUE;
+
+       if (status_code != SOUP_STATUS_OK && (
+           status_code != SOUP_STATUS_FAILED_DEPENDENCY ||
+           !*error)) {
+               gchar *description;
+
+               description = e_xml_xpath_eval_as_string (xpath_ctx, "%s/../D:responsedescription", 
xpath_prop_prefix);
+               if (!description || !*description) {
+                       g_free (description);
+
+                       description = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/../../D:responsedescription", xpath_prop_prefix);
+               }
+
+               g_clear_error (error);
+               g_set_error_literal (error, SOUP_HTTP_ERROR, status_code,
+                       e_soup_session_util_status_to_string (status_code, description));
+
+               g_free (description);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_webdav_session_extract_dav_error (EWebDAVSession *webdav,
+                                   xmlXPathContextPtr xpath_ctx,
+                                   const gchar *xpath_prefix,
+                                   gchar **out_detail_text)
+{
+       xmlXPathObjectPtr xpath_obj;
+       gchar *detail_text;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (xpath_prefix != NULL, FALSE);
+       g_return_val_if_fail (out_detail_text != NULL, FALSE);
+
+       if (!e_xml_xpath_eval_exists (xpath_ctx, "%s/D:error", xpath_prefix))
+               return FALSE;
+
+       detail_text = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:error", xpath_prefix);
+
+       xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:error", xpath_prefix);
+       if (xpath_obj) {
+               if (xpath_obj->type == XPATH_NODESET &&
+                   xpath_obj->nodesetval &&
+                   xpath_obj->nodesetval->nodeNr == 1 &&
+                   xpath_obj->nodesetval->nodeTab &&
+                   xpath_obj->nodesetval->nodeTab[0] &&
+                   xpath_obj->nodesetval->nodeTab[0]->children) {
+                       GString *text = g_string_new ("");
+                       xmlNodePtr node;
+
+                       for (node = xpath_obj->nodesetval->nodeTab[0]->children; node; node = node->next) {
+                               if (node->type == XML_ELEMENT_NODE &&
+                                   node->name && *(node->name))
+                                       g_string_append_printf (text, "[%s]", (const gchar *) node->name);
+                       }
+
+                       if (text->len > 0) {
+                               if (detail_text) {
+                                       g_strstrip (detail_text);
+                                       if (*detail_text)
+                                               g_string_prepend (text, detail_text);
+                                       g_free (detail_text);
+                               }
+
+                               detail_text = g_string_free (text, FALSE);
+                       } else {
+                               g_string_free (text, TRUE);
+                       }
+               }
+
+               xmlXPathFreeObject (xpath_obj);
+       }
+
+       *out_detail_text = detail_text;
+
+       return detail_text != NULL;
+}
+
+/**
+ * e_webdav_session_replace_with_detailed_error:
+ * @webdav: an #EWebDAVSession
+ * @request: a #SoupRequestHTTP
+ * @response_data: (nullable): received response data, or %NULL
+ * @ignore_multistatus: whether to ignore multistatus responses
+ * @prefix: (nullable): error message prefix, used when replacing, or %NULL
+ * @inout_error: (inout) (nullable) (transfer full): a #GError variable to replace content to, or %NULL
+ *
+ * Tries to read detailed error information from @response_data,
+ * if not provided, then from @request's response_body. If the detailed
+ * error cannot be found, then does nothing, otherwise frees the content
+ * of @inout_error, if any, and then populates it with an error message
+ * prefixed with @prefix.
+ *
+ * The @prefix might be of form "Failed to something", because the resulting
+ * error message will be:
+ * "Failed to something: HTTP error code XXX (reason_phrase): detailed_error".
+ * When @prefix is %NULL, the error message will be:
+ * "Failed with HTTP error code XXX (reason phrase): detailed_error".
+ *
+ * As the caller might not be interested in errors, also the @inout_error
+ * can be %NULL, in which case the function does nothing.
+ *
+ * Returns: Whether any detailed error had been recognized.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_replace_with_detailed_error (EWebDAVSession *webdav,
+                                             SoupRequestHTTP *request,
+                                             const GByteArray *response_data,
+                                             gboolean ignore_multistatus,
+                                             const gchar *prefix,
+                                             GError **inout_error)
+{
+       SoupMessage *message;
+       GByteArray byte_array = { 0 };
+       const gchar *content_type, *reason_phrase;
+       gchar *detail_text = NULL;
+       gchar *reason_phrase_copy = NULL;
+       gboolean error_set = FALSE;
+       guint status_code;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), FALSE);
+
+       message = soup_request_http_get_message (request);
+       if (!message)
+               return FALSE;
+
+       status_code = message->status_code;
+       reason_phrase = message->reason_phrase;
+       byte_array.data = NULL;
+       byte_array.len = 0;
+
+       if (response_data && response_data->len) {
+               byte_array.data = (gpointer) response_data->data;
+               byte_array.len = response_data->len;
+       } else if (message->response_body && message->response_body->length) {
+               byte_array.data = (gpointer) message->response_body->data;
+               byte_array.len = message->response_body->length;
+       }
+
+       if (!byte_array.data || !byte_array.len)
+               goto out;
+
+       if (status_code == SOUP_STATUS_MULTI_STATUS &&
+           !ignore_multistatus &&
+           !e_webdav_session_traverse_multistatus_response (webdav, message, &byte_array,
+               e_webdav_session_extract_propstat_error_cb, &local_error, NULL)) {
+               g_clear_error (&local_error);
+       }
+
+       if (local_error) {
+               if (prefix)
+                       g_prefix_error (&local_error, "%s: ", prefix);
+               g_propagate_error (inout_error, local_error);
+
+               g_object_unref (message);
+
+               return TRUE;
+       }
+
+       content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+       if (content_type && (
+           (g_ascii_strcasecmp (content_type, "application/xml") == 0 ||
+            g_ascii_strcasecmp (content_type, "text/xml") == 0))) {
+               xmlDocPtr doc;
+
+               if (status_code == SOUP_STATUS_MULTI_STATUS && ignore_multistatus)
+                       doc = NULL;
+               else
+                       doc = e_xml_parse_data (byte_array.data, byte_array.len);
+
+               if (doc) {
+                       xmlXPathContextPtr xpath_ctx;
+
+                       xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+                               "D", E_WEBDAV_NS_DAV,
+                               "C", E_WEBDAV_NS_CALDAV,
+                               "A", E_WEBDAV_NS_CARDDAV,
+                               NULL);
+
+                       if (xpath_ctx &&
+                           e_webdav_session_extract_dav_error (webdav, xpath_ctx, "", &detail_text)) {
+                               /* do nothing, detail_text is set */
+                       } else if (xpath_ctx) {
+                               const gchar *path_prefix = NULL;
+
+                               if (e_xml_xpath_eval_exists (xpath_ctx, "/D:multistatus/D:response/D:status"))
+                                       path_prefix = "/D:multistatus/D:response";
+                               else if (e_xml_xpath_eval_exists (xpath_ctx, 
"/C:mkcalendar-response/D:status"))
+                                       path_prefix = "/C:mkcalendar-response";
+                               else if (e_xml_xpath_eval_exists (xpath_ctx, "/D:mkcol-response/D:status"))
+                                       path_prefix = "/D:mkcol-response";
+
+                               if (path_prefix) {
+                                       guint parsed_status = 0;
+                                       gchar *status;
+
+                                       status = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:status", 
path_prefix);
+                                       if (status && soup_headers_parse_status_line (status, NULL, 
&parsed_status, &reason_phrase_copy) &&
+                                           !SOUP_STATUS_IS_SUCCESSFUL (parsed_status)) {
+                                               status_code = parsed_status;
+                                               reason_phrase = reason_phrase_copy;
+                                               detail_text = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:responsedescription", path_prefix);
+
+                                               if (!detail_text)
+                                                       e_webdav_session_extract_dav_error (webdav, 
xpath_ctx, path_prefix, &detail_text);
+                                       } else {
+                                               e_webdav_session_extract_dav_error (webdav, xpath_ctx, 
path_prefix, &detail_text);
+                                       }
+
+                                       g_free (status);
+                               }
+                       }
+
+                       if (xpath_ctx)
+                               xmlXPathFreeContext (xpath_ctx);
+                       xmlFreeDoc (doc);
+               }
+       } else if (content_type &&
+            g_ascii_strcasecmp (content_type, "text/plain") == 0) {
+               detail_text = g_strndup ((const gchar *) byte_array.data, byte_array.len);
+       } else if (content_type &&
+            g_ascii_strcasecmp (content_type, "text/html") == 0) {
+               SoupURI *soup_uri;
+               gchar *uri = NULL;
+
+               soup_uri = soup_message_get_uri (message);
+               if (soup_uri) {
+                       soup_uri = soup_uri_copy (soup_uri);
+                       soup_uri_set_password (soup_uri, NULL);
+
+                       uri = soup_uri_to_string (soup_uri, FALSE);
+
+                       soup_uri_free (soup_uri);
+               }
+
+               if (uri && *uri)
+                       detail_text = g_strdup_printf (_("The server responded with an HTML page, which can 
mean there's an error on the server or with the client request. The used URI was: %s"), uri);
+               else
+                       detail_text = g_strdup_printf (_("The server responded with an HTML page, which can 
mean there's an error on the server or with the client request."));
+
+               g_free (uri);
+       }
+
+ out:
+       if (detail_text)
+               g_strstrip (detail_text);
+
+       if (detail_text && *detail_text) {
+               error_set = TRUE;
+
+               g_clear_error (inout_error);
+
+               if (prefix) {
+                       g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+                               /* Translators: The first '%s' is replaced with error prefix, as provided
+                                  by the caller, which can be in a form: "Failed with something".
+                                  The '%d' is replaced with actual HTTP status code.
+                                  The second '%s' is replaced with a reason phrase of the error (user 
readable text).
+                                  The last '%s' is replaced with detailed error text, as returned by the 
server. */
+                               _("%s: HTTP error code %d (%s): %s"), prefix, status_code,
+                               e_soup_session_util_status_to_string (status_code, reason_phrase),
+                               detail_text);
+               } else {
+                       g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+                               /* Translators: The '%d' is replaced with actual HTTP status code.
+                                  The '%s' is replaced with a reason phrase of the error (user readable 
text).
+                                  The last '%s' is replaced with detailed error text, as returned by the 
server. */
+                               _("Failed with HTTP error code %d (%s): %s"), status_code,
+                               e_soup_session_util_status_to_string (status_code, reason_phrase),
+                               detail_text);
+               }
+       } else if (status_code && !SOUP_STATUS_IS_SUCCESSFUL (status_code)) {
+               error_set = TRUE;
+
+               g_clear_error (inout_error);
+
+               if (prefix) {
+                       g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+                               /* Translators: The first '%s' is replaced with error prefix, as provided
+                                  by the caller, which can be in a form: "Failed with something".
+                                  The '%d' is replaced with actual HTTP status code.
+                                  The second '%s' is replaced with a reason phrase of the error (user 
readable text). */
+                               _("%s: HTTP error code %d (%s)"), prefix, status_code,
+                               e_soup_session_util_status_to_string (status_code, reason_phrase));
+               } else {
+                       g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+                               /* Translators: The '%d' is replaced with actual HTTP status code.
+                                  The '%s' is replaced with a reason phrase of the error (user readable 
text). */
+                               _("Failed with HTTP error code %d (%s)"), status_code,
+                               e_soup_session_util_status_to_string (status_code, reason_phrase));
+               }
+       }
+
+       g_object_unref (message);
+       g_free (reason_phrase_copy);
+       g_free (detail_text);
+
+       return error_set;
+}
+
+/**
+ * e_webdav_session_ensure_full_uri:
+ * @webdav: an #EWebDAVSession
+ * @request_uri: (nullable): a #SoupURI to which the @href belongs, or %NULL
+ * @href: a possibly path-only href
+ *
+ * Converts possibly path-only @href into a full URI under the @request_uri.
+ * When the @request_uri is %NULL, the URI defined in associated #ESource is
+ * used instead.
+ *
+ * Free the returned pointer with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): The @href as a full URI
+ *
+ * Since: 3.24
+ **/
+gchar *
+e_webdav_session_ensure_full_uri (EWebDAVSession *webdav,
+                                 const SoupURI *request_uri,
+                                 const gchar *href)
+{
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), NULL);
+       g_return_val_if_fail (href != NULL, NULL);
+
+       if (*href == '/' || !strstr (href, "://")) {
+               SoupURI *soup_uri;
+               gchar *full_uri;
+
+               if (request_uri) {
+                       soup_uri = soup_uri_copy ((SoupURI *) request_uri);
+               } else {
+                       ESource *source;
+                       ESourceWebdav *webdav_extension;
+
+                       source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+                       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);
+
+               soup_uri_set_path (soup_uri, href);
+               soup_uri_set_user (soup_uri, NULL);
+               soup_uri_set_password (soup_uri, NULL);
+
+               full_uri = soup_uri_to_string (soup_uri, FALSE);
+
+               soup_uri_free (soup_uri);
+
+               return full_uri;
+       }
+
+       return g_strdup (href);
+}
+
+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 %SOUP_METHOD_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_post_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @data: data to post to the server
+ * @data_length: length of @data, or -1, when @data is NUL-terminated
+ * @out_content_type: (nullable) (transfer full): return location for response Content-Type, or %NULL
+ * @out_content: (nullable) (transfer full): return location for response content, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues POST request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * The optional @out_content_type can be used to get content type of the response.
+ * Free it with g_free(), when no longer needed.
+ *
+ * The optional @out_content can be used to get actual result content. Free it
+ * with g_byte_array_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_post_sync (EWebDAVSession *webdav,
+                           const gchar *uri,
+                           const gchar *data,
+                           gsize data_length,
+                           gchar **out_content_type,
+                           GByteArray **out_content,
+                           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 (data != NULL, FALSE);
+
+       if (out_content_type)
+               *out_content_type = NULL;
+
+       if (out_content)
+               *out_content = NULL;
+
+       if (data_length == (gsize) -1)
+               data_length = strlen (data);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_POST, 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_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+               SOUP_MEMORY_COPY, data, data_length);
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to 
post data"), error) &&
+               bytes != NULL;
+
+       if (success) {
+               if (out_content_type) {
+                       *out_content_type = g_strdup (soup_message_headers_get_content_type 
(message->response_headers, NULL));
+               }
+
+               if (out_content) {
+                       *out_content = bytes;
+                       bytes = NULL;
+               }
+       }
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * 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_THIS, %E_WEBDAV_DEPTH_THIS_AND_CHILDREN or 
%E_WEBDAV_DEPTH_INFINITY
+ * @xml: (nullable): the request itself, as an #EXmlDocument, the root element should be DAV:propfind, or 
%NULL
+ * @func: an #EWebDAVPropstatTraverseFunc 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,
+                               EWebDAVPropstatTraverseFunc 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_replace (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);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to 
get properties"), error) &&
+               bytes != NULL;
+
+       if (success)
+               success = e_webdav_session_traverse_multistatus_response (webdav, message, bytes, func, 
func_user_data, error);
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_proppatch_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @xml: an #EXmlDocument with request changes, its root element should be DAV:propertyupdate
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues PROPPATCH request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource, with the @changes. The order of requested changes
+ * inside @xml is significant, unlike on other places.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_proppatch_sync (EWebDAVSession *webdav,
+                                const gchar *uri,
+                                const EXmlDocument *xml,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gchar *content;
+       gsize content_length;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_PROPPATCH, 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;
+       }
+
+       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);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
update properties"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_report_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @depth: (nullable): requested depth, can be %NULL, then no Depth header is sent
+ * @xml: the request itself, as an #EXmlDocument
+ * @func: (nullable): an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the 
multistatus response, or %NULL
+ * @func_user_data: user data passed to @func
+ * @out_content_type: (nullable) (transfer full): return location for response Content-Type, or %NULL
+ * @out_content: (nullable) (transfer full): return location for response content, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues REPORT 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 report can result in a multistatus response, but also to raw data. In case
+ * the @func is provided and the result is a multistatus response, then it is traversed
+ * using this @func. 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 optional @out_content_type can be used to get content type of the response.
+ * Free it with g_free(), when no longer needed.
+ *
+ * The optional @out_content can be used to get actual result content. Free it
+ * with g_byte_array_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_report_sync (EWebDAVSession *webdav,
+                             const gchar *uri,
+                             const gchar *depth,
+                             const EXmlDocument *xml,
+                             EWebDAVPropstatTraverseFunc func,
+                             gpointer func_user_data,
+                             gchar **out_content_type,
+                             GByteArray **out_content,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gchar *content;
+       gsize content_length;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+
+       if (out_content_type)
+               *out_content_type = NULL;
+
+       if (out_content)
+               *out_content = NULL;
+
+       request = e_webdav_session_new_request (webdav, "REPORT", 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;
+       }
+
+       if (depth)
+               soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+       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);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to 
issue REPORT"), error) &&
+               bytes != NULL;
+
+       if (success && func && message->status_code == SOUP_STATUS_MULTI_STATUS)
+               success = e_webdav_session_traverse_multistatus_response (webdav, message, bytes, func, 
func_user_data, error);
+
+       if (success) {
+               if (out_content_type) {
+                       *out_content_type = g_strdup (soup_message_headers_get_content_type 
(message->response_headers, NULL));
+               }
+
+               if (out_content) {
+                       *out_content = bytes;
+                       bytes = NULL;
+               }
+       }
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_mkcol_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the collection to create
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new generic collection identified by @uri on the server.
+ * To create specific collections use e_webdav_session_mkcalendar_sync()
+ * or e_webdav_session_mkcol_addressbook_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_mkcol_sync (EWebDAVSession *webdav,
+                            const gchar *uri,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       SoupRequestHTTP *request;
+       GByteArray *bytes;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_MKCOL, uri, error);
+       if (!request)
+               return FALSE;
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
create collection"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_mkcol_addressbook_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the collection to create
+ * @display_name: (nullable): a human-readable display name to set, or %NULL
+ * @description: (nullable): a human-readable description of the address book, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new address book collection identified by @uri on the server.
+ *
+ * Note that CardDAV RFC 6352 Section 5.2 forbids to create address book
+ * resources under other address book resources (no nested address books
+ * are allowed).
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_mkcol_addressbook_sync (EWebDAVSession *webdav,
+                                        const gchar *uri,
+                                        const gchar *display_name,
+                                        const gchar *description,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       EXmlDocument *xml;
+       gchar *content;
+       gsize content_length;
+       GByteArray *bytes;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_MKCOL, 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;
+       }
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "mkcol");
+       e_xml_document_add_namespaces (xml, "A", E_WEBDAV_NS_CARDDAV, NULL);
+
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "set");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+       e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "resourcetype");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "collection");
+       e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook");
+       e_xml_document_end_element (xml); /* resourcetype */
+
+       if (display_name && *display_name) {
+               e_xml_document_start_text_element (xml, E_WEBDAV_NS_DAV, "displayname");
+               e_xml_document_write_string (xml, display_name);
+               e_xml_document_end_element (xml);
+       }
+
+       if (description && *description) {
+               e_xml_document_start_text_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-description");
+               e_xml_document_write_string (xml, description);
+               e_xml_document_end_element (xml);
+       }
+
+       e_xml_document_end_element (xml); /* prop */
+       e_xml_document_end_element (xml); /* set */
+
+       content = e_xml_document_get_content (xml, &content_length);
+       if (!content) {
+               g_object_unref (message);
+               g_object_unref (request);
+               g_object_unref (xml);
+
+               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get XML 
request content"));
+
+               return FALSE;
+       }
+
+       soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+               SOUP_MEMORY_TAKE, content, content_length);
+
+       g_object_unref (xml);
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
create address book"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_mkcalendar_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the collection to create
+ * @display_name: (nullable): a human-readable display name to set, or %NULL
+ * @description: (nullable): a human-readable description of the calendar, or %NULL
+ * @color: (nullable): a color to set, in format "#RRGGBB", or %NULL
+ * @supports: a bit-or of EWebDAVResourceSupports values
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new calendar collection identified by @uri on the server.
+ * The @supports defines what component types can be stored into
+ * the created calendar collection. Only %E_WEBDAV_RESOURCE_SUPPORTS_NONE
+ * and values related to iCalendar content can be used here.
+ * Using %E_WEBDAV_RESOURCE_SUPPORTS_NONE means that everything is supported.
+ *
+ * Note that CalDAV RFC 4791 Section 4.2 forbids to create calendar
+ * resources under other calendar resources (no nested calendars
+ * are allowed).
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_mkcalendar_sync (EWebDAVSession *webdav,
+                                 const gchar *uri,
+                                 const gchar *display_name,
+                                 const gchar *description,
+                                 const gchar *color,
+                                 guint32 supports,
+                                 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 (uri != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, "MKCALENDAR", 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;
+       }
+
+       supports = supports & (
+               E_WEBDAV_RESOURCE_SUPPORTS_EVENTS |
+               E_WEBDAV_RESOURCE_SUPPORTS_MEMOS |
+               E_WEBDAV_RESOURCE_SUPPORTS_TASKS |
+               E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY |
+               E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE);
+
+       if ((display_name && *display_name) ||
+           (description && *description) ||
+           (color && *color) ||
+           (supports != 0)) {
+               EXmlDocument *xml;
+               gchar *content;
+               gsize content_length;
+
+               xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "mkcalendar");
+               e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
+
+               e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "set");
+               e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+
+               if (display_name && *display_name) {
+                       e_xml_document_start_text_element (xml, E_WEBDAV_NS_DAV, "displayname");
+                       e_xml_document_write_string (xml, display_name);
+                       e_xml_document_end_element (xml);
+               }
+
+               if (description && *description) {
+                       e_xml_document_start_text_element (xml, E_WEBDAV_NS_CALDAV, "calendar-description");
+                       e_xml_document_write_string (xml, description);
+                       e_xml_document_end_element (xml);
+               }
+
+               if (color && *color) {
+                       e_xml_document_add_namespaces (xml, "IC", E_WEBDAV_NS_ICAL, NULL);
+
+                       e_xml_document_start_text_element (xml, E_WEBDAV_NS_ICAL, "calendar-color");
+                       e_xml_document_write_string (xml, color);
+                       e_xml_document_end_element (xml);
+               }
+
+               if (supports != 0) {
+                       struct SupportValues {
+                               guint32 mask;
+                               const gchar *value;
+                       } values[] = {
+                               { E_WEBDAV_RESOURCE_SUPPORTS_EVENTS, "VEVENT" },
+                               { E_WEBDAV_RESOURCE_SUPPORTS_MEMOS, "VJOURNAL" },
+                               { E_WEBDAV_RESOURCE_SUPPORTS_TASKS, "VTODO" },
+                               { E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY, "VFREEBUSY" },
+                               { E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE, "TIMEZONE" }
+                       };
+                       gint ii;
+
+                       e_xml_document_start_text_element (xml, E_WEBDAV_NS_CALDAV, 
"supported-calendar-component-set");
+
+                       for (ii = 0; ii < G_N_ELEMENTS (values); ii++) {
+                               if ((supports & values[ii].mask) != 0) {
+                                       e_xml_document_start_text_element (xml, E_WEBDAV_NS_CALDAV, "comp");
+                                       e_xml_document_add_attribute (xml, NULL, "name", values[ii].value);
+                                       e_xml_document_end_element (xml); /* comp */
+                               }
+                       }
+
+                       e_xml_document_end_element (xml); /* supported-calendar-component-set */
+               }
+
+               e_xml_document_end_element (xml); /* prop */
+               e_xml_document_end_element (xml); /* set */
+
+               content = e_xml_document_get_content (xml, &content_length);
+               if (!content) {
+                       g_object_unref (message);
+                       g_object_unref (request);
+                       g_object_unref (xml);
+
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get 
XML request content"));
+
+                       return FALSE;
+               }
+
+               soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+                       SOUP_MEMORY_TAKE, content, content_length);
+
+               g_object_unref (xml);
+       }
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
create calendar"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+static void
+e_webdav_session_extract_href_and_etag (SoupMessage *message,
+                                       gchar **out_href,
+                                       gchar **out_etag)
+{
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+
+       if (out_href) {
+               const gchar *header;
+
+               *out_href = NULL;
+
+               header = soup_message_headers_get_list (message->response_headers, "Location");
+               if (header) {
+                       gchar *file = strrchr (header, '/');
+
+                       if (file) {
+                               gchar *decoded;
+
+                               decoded = soup_uri_decode (file + 1);
+                               *out_href = soup_uri_encode (decoded ? decoded : (file + 1), NULL);
+
+                               g_free (decoded);
+                       }
+               }
+
+               if (!*out_href)
+                       *out_href = soup_uri_to_string (soup_message_get_uri (message), FALSE);
+       }
+
+       if (out_etag) {
+               const gchar *header;
+
+               *out_etag = NULL;
+
+               header = soup_message_headers_get_list (message->response_headers, "ETag");
+               if (header)
+                       *out_etag = e_webdav_session_util_maybe_dequote (g_strdup (header));
+       }
+}
+
+/**
+ * e_webdav_session_get_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to read
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @out_stream: (out) (caller-allocates): a #GOutputStream to write data to
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads a resource identified by @uri from the server and writes it
+ * to the @stream. The URI cannot reference a collection.
+ *
+ * Free returned pointer of @out_href and @out_etag, if not %NULL, with g_free(),
+ * when no longer needed.
+ *
+ * The e_webdav_session_get_data_sync() can be used to read the resource data
+ * directly to memory.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_sync (EWebDAVSession *webdav,
+                          const gchar *uri,
+                          gchar **out_href,
+                          gchar **out_etag,
+                          GOutputStream *out_stream,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GInputStream *input_stream;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+       g_return_val_if_fail (G_IS_OUTPUT_STREAM (out_stream), FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_GET, 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;
+       }
+
+       input_stream = e_soup_session_send_request_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = input_stream != NULL;
+
+       if (success) {
+               SoupLoggerLogLevel log_level = e_soup_session_get_log_level (E_SOUP_SESSION (webdav));
+               gpointer buffer;
+               gsize nread = 0, nwritten;
+               gboolean first_chunk = TRUE;
+
+               buffer = g_malloc (BUFFER_SIZE);
+
+               while (success = g_input_stream_read_all (input_stream, buffer, BUFFER_SIZE, &nread, 
cancellable, error),
+                      success && nread > 0) {
+                       if (log_level == SOUP_LOGGER_LOG_BODY) {
+                               fwrite (buffer, 1, nread, stdout);
+                               fflush (stdout);
+                       }
+
+                       if (first_chunk) {
+                               GByteArray tmp_bytes;
+
+                               first_chunk = FALSE;
+
+                               tmp_bytes.data = buffer;
+                               tmp_bytes.len = nread;
+
+                               success = !e_webdav_session_replace_with_detailed_error (webdav, request, 
&tmp_bytes, FALSE, _("Failed to read resource"), error);
+                               if (!success)
+                                       break;
+                       }
+
+                       success = g_output_stream_write_all (out_stream, buffer, nread, &nwritten, 
cancellable, error);
+                       if (!success)
+                               break;
+               }
+
+               if (success && first_chunk) {
+                       success = !e_webdav_session_replace_with_detailed_error (webdav, request, NULL, 
FALSE, _("Failed to read resource"), error);
+               } else if (success && !first_chunk && log_level == SOUP_LOGGER_LOG_BODY) {
+                       fprintf (stdout, "\n");
+                       fflush (stdout);
+               }
+
+               g_free (buffer);
+       }
+
+       if (success)
+               e_webdav_session_extract_href_and_etag (message, out_href, out_etag);
+
+       g_clear_object (&input_stream);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_get_data_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to read
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @out_bytes: (out) (transfer full): return location for bytes being read
+ * @out_length: (out) (nullable): option return location for length of bytes being read, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads a resource identified by @uri from the server. The URI cannot
+ * reference a collection.
+ *
+ * The @out_bytes is filled by actual data being read. If not %NULL, @out_length
+ * is populated with how many bytes had been read. The @out_bytes is always
+ * NUL-terminated, while this termination byte is not part of @out_length.
+ * Free the @out_bytes with g_free(), when no longer needed.
+ *
+ * Free returned pointer of @out_href and @out_etag, if not %NULL, with g_free(),
+ * when no longer needed.
+ *
+ * To read large data use e_webdav_session_get_sync() instead.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_data_sync (EWebDAVSession *webdav,
+                               const gchar *uri,
+                               gchar **out_href,
+                               gchar **out_etag,
+                               gchar **out_bytes,
+                               gsize *out_length,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       GOutputStream *output_stream;
+       gsize bytes_written = 0;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+       g_return_val_if_fail (out_bytes != NULL, FALSE);
+
+       *out_bytes = NULL;
+       if (out_length)
+               *out_length = 0;
+
+       output_stream = g_memory_output_stream_new_resizable ();
+
+       success = e_webdav_session_get_sync (webdav, uri, out_href, out_etag, output_stream, cancellable, 
error) &&
+               g_output_stream_write_all (output_stream, "", 1, &bytes_written, cancellable, error) &&
+               g_output_stream_close (output_stream, cancellable, error);
+
+       if (success) {
+               if (out_length)
+                       *out_length = g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM 
(output_stream)) - 1;
+
+               *out_bytes = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (output_stream));
+       }
+
+       g_object_unref (output_stream);
+
+       return success;
+}
+
+typedef struct _ChunkWriteData {
+       SoupSession *session;
+       SoupLoggerLogLevel log_level;
+       GInputStream *stream;
+       goffset read_from;
+       gboolean wrote_any;
+       gsize buffer_size;
+       gpointer buffer;
+       GCancellable *cancellable;
+       GError *error;
+} ChunkWriteData;
+
+static void
+e_webdav_session_write_next_chunk (SoupMessage *message,
+                                  gpointer user_data)
+{
+       ChunkWriteData *cwd = user_data;
+       gsize nread;
+
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+       g_return_if_fail (cwd != NULL);
+
+       if (!g_input_stream_read_all (cwd->stream, cwd->buffer, cwd->buffer_size, &nread, cwd->cancellable, 
&cwd->error)) {
+               soup_session_cancel_message (cwd->session, message, SOUP_STATUS_CANCELLED);
+               return;
+       }
+
+       if (nread == 0) {
+               soup_message_body_complete (message->request_body);
+       } else {
+               cwd->wrote_any = TRUE;
+               soup_message_body_append (message->request_body, SOUP_MEMORY_TEMPORARY, cwd->buffer, nread);
+
+               if (cwd->log_level == SOUP_LOGGER_LOG_BODY) {
+                       fwrite (cwd->buffer, 1, nread, stdout);
+                       fflush (stdout);
+               }
+       }
+}
+
+static void
+e_webdav_session_write_restarted (SoupMessage *message,
+                                 gpointer user_data)
+{
+       ChunkWriteData *cwd = user_data;
+
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+       g_return_if_fail (cwd != NULL);
+
+       /* The 302 redirect will turn it into a GET request and
+        * reset the body encoding back to "NONE". Fix that.
+        */
+       soup_message_headers_set_encoding (message->request_headers, SOUP_ENCODING_CHUNKED);
+       message->method = SOUP_METHOD_PUT;
+
+       if (cwd->wrote_any) {
+               cwd->wrote_any = FALSE;
+
+               if (!G_IS_SEEKABLE (cwd->stream) || !g_seekable_can_seek (G_SEEKABLE (cwd->stream)) ||
+                   !g_seekable_seek (G_SEEKABLE (cwd->stream), cwd->read_from, G_SEEK_SET, cwd->cancellable, 
&cwd->error)) {
+                       if (!cwd->error)
+                               g_set_error_literal (&cwd->error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT,
+                                       _("Cannot rewind input stream: Not supported"));
+
+                       soup_session_cancel_message (cwd->session, message, SOUP_STATUS_CANCELLED);
+               }
+       }
+}
+
+/**
+ * e_webdav_session_put_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to write
+ * @etag: (nullable): an ETag of the resource, if it's an existing resource, or %NULL
+ * @content_type: Content-Type of the @bytes to be written
+ * @stream: a #GInputStream with data to be written
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Writes data from @stream to a resource identified by @uri to the server.
+ * The URI cannot reference a collection.
+ *
+ * The @etag argument is used to avoid clashes when overwriting existing
+ * resources. It can contain three values:
+ *  - %NULL - to write completely new resource
+ *  - empty string - write new resource or overwrite any existing, regardless changes on the server
+ *  - valid ETag - overwrite existing resource only if it wasn't changed on the server.
+ *
+ * Note that the actual behaviour is also influenced by #ESourceWebdav:avoid-ifmatch
+ * property of the associated #ESource.
+ *
+ * The @out_href, if provided, is filled with the resulting URI
+ * of the written resource. It can be different from the @uri when the server
+ * redirected to a different location.
+ *
+ * The @out_etag contains ETag of the resource after it had been saved.
+ *
+ * The @stream should support also #GSeekable interface, because the data
+ * send can require restart of the send due to redirect or other reasons.
+ *
+ * The e_webdav_session_put_data_sync() can be used to write data stored in memory.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_put_sync (EWebDAVSession *webdav,
+                          const gchar *uri,
+                          const gchar *etag,
+                          const gchar *content_type,
+                          GInputStream *stream,
+                          gchar **out_href,
+                          gchar **out_etag,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       ChunkWriteData cwd;
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gulong restarted_id, wrote_headers_id, wrote_chunk_id;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+       g_return_val_if_fail (content_type != NULL, FALSE);
+       g_return_val_if_fail (G_IS_INPUT_STREAM (stream), FALSE);
+
+       if (out_href)
+               *out_href = NULL;
+       if (out_etag)
+               *out_etag = NULL;
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_PUT, 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;
+       }
+
+       if (!etag || *etag) {
+               ESource *source;
+               gboolean avoid_ifmatch = FALSE;
+
+               source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+               if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+                       ESourceWebdav *webdav_extension;
+
+                       webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+                       avoid_ifmatch = e_source_webdav_get_avoid_ifmatch (webdav_extension);
+               }
+
+               if (!avoid_ifmatch) {
+                       if (etag) {
+                               gint len = strlen (etag);
+
+                               if (*etag == '\"' && len > 2 && etag[len - 1] == '\"') {
+                                       soup_message_headers_replace (message->request_headers, "If-Match", 
etag);
+                               } else {
+                                       gchar *quoted;
+
+                                       quoted = g_strconcat ("\"", etag, "\"", NULL);
+                                       soup_message_headers_replace (message->request_headers, "If-Match", 
quoted);
+                                       g_free (quoted);
+                               }
+                       } else {
+                               soup_message_headers_replace (message->request_headers, "If-None-Match", "*");
+                       }
+               }
+       }
+
+       cwd.session = SOUP_SESSION (webdav);
+       cwd.log_level = e_soup_session_get_log_level (E_SOUP_SESSION (webdav));
+       cwd.stream = stream;
+       cwd.read_from = 0;
+       cwd.wrote_any = FALSE;
+       cwd.buffer_size = BUFFER_SIZE;
+       cwd.buffer = g_malloc (cwd.buffer_size);
+       cwd.cancellable = cancellable;
+       cwd.error = NULL;
+
+       if (G_IS_SEEKABLE (stream) && g_seekable_can_seek (G_SEEKABLE (stream)))
+               cwd.read_from = g_seekable_tell (G_SEEKABLE (stream));
+
+       if (content_type && *content_type)
+               soup_message_headers_replace (message->request_headers, "Content-Type", content_type);
+
+       soup_message_headers_set_encoding (message->request_headers, SOUP_ENCODING_CHUNKED);
+       soup_message_body_set_accumulate (message->request_body, FALSE);
+       soup_message_set_flags (message, SOUP_MESSAGE_CAN_REBUILD);
+
+       restarted_id = g_signal_connect (message, "restarted", G_CALLBACK (e_webdav_session_write_restarted), 
&cwd);
+       wrote_headers_id = g_signal_connect (message, "wrote-headers", G_CALLBACK 
(e_webdav_session_write_next_chunk), &cwd);
+       wrote_chunk_id = g_signal_connect (message, "wrote-chunk", G_CALLBACK 
(e_webdav_session_write_next_chunk), &cwd);
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       g_signal_handler_disconnect (message, restarted_id);
+       g_signal_handler_disconnect (message, wrote_headers_id);
+       g_signal_handler_disconnect (message, wrote_chunk_id);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
put data"), error) &&
+               bytes != NULL;
+
+       if (cwd.wrote_any && cwd.log_level == SOUP_LOGGER_LOG_BODY) {
+               fprintf (stdout, "\n");
+               fflush (stdout);
+       }
+
+       if (cwd.error) {
+               g_clear_error (error);
+               g_propagate_error (error, cwd.error);
+               success = FALSE;
+       }
+
+       if (success) {
+               if (success && !SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
+                       success = FALSE;
+
+                       g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+                               _("Failed to put data to server, error code %d (%s)"), message->status_code,
+                               e_soup_session_util_status_to_string (message->status_code, 
message->reason_phrase));
+               }
+       }
+
+       if (success)
+               e_webdav_session_extract_href_and_etag (message, out_href, out_etag);
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+       g_free (cwd.buffer);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_put_data_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to write
+ * @etag: (nullable): an ETag of the resource, if it's an existing resource, or %NULL
+ * @content_type: Content-Type of the @bytes to be written
+ * @bytes: actual bytes to be written
+ * @length: how many bytes to write, or -1, when the @bytes is NUL-terminated
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Writes data to a resource identified by @uri to the server. The URI cannot
+ * reference a collection.
+ *
+ * The @etag argument is used to avoid clashes when overwriting existing
+ * resources. It can contain three values:
+ *  - %NULL - to write completely new resource
+ *  - empty string - write new resource or overwrite any existing, regardless changes on the server
+ *  - valid ETag - overwrite existing resource only if it wasn't changed on the server.
+ *
+ * Note that the actual usage of @etag is also influenced by #ESourceWebdav:avoid-ifmatch
+ * property of the associated #ESource.
+ *
+ * The @out_href, if provided, is filled with the resulting URI
+ * of the written resource. It can be different from the @uri when the server
+ * redirected to a different location.
+ *
+ * The @out_etag contains ETag of the resource after it had been saved.
+ *
+ * To read large data use e_webdav_session_put_sync() instead.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_put_data_sync (EWebDAVSession *webdav,
+                               const gchar *uri,
+                               const gchar *etag,
+                               const gchar *content_type,
+                               const gchar *bytes,
+                               gsize length,
+                               gchar **out_href,
+                               gchar **out_etag,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       GInputStream *input_stream;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+       g_return_val_if_fail (content_type != NULL, FALSE);
+       g_return_val_if_fail (bytes != NULL, FALSE);
+
+       if (length == (gsize) -1)
+               length = strlen (bytes);
+
+       input_stream = g_memory_input_stream_new_from_data (bytes, length, NULL);
+
+       success = e_webdav_session_put_sync (webdav, uri, etag, content_type,
+               input_stream, out_href, out_etag, cancellable, error);
+
+       g_object_unref (input_stream);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_delete_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to delete
+ * @depth: (nullable): optional requested depth, can be one of %E_WEBDAV_DEPTH_THIS or 
%E_WEBDAV_DEPTH_INFINITY, or %NULL
+ * @etag: (nullable): an optional ETag of the resource, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Deletes a resource identified by @uri on the server. The URI can
+ * reference a collection, in which case @depth should be %E_WEBDAV_DEPTH_INFINITY.
+ * Use @depth %E_WEBDAV_DEPTH_THIS when deleting a regular resource, or %NULL,
+ * to let the server use default Depth.
+ *
+ * The @etag argument is used to avoid clashes when overwriting existing resources.
+ * Use %NULL @etag when deleting collection resources or to force the deletion,
+ * otherwise provide a valid ETag of a non-collection resource to verify that
+ * the version requested to delete is the same as on the server.
+ *
+ * Note that the actual usage of @etag is also influenced by #ESourceWebdav:avoid-ifmatch
+ * property of the associated #ESource.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_delete_sync (EWebDAVSession *webdav,
+                             const gchar *uri,
+                             const gchar *depth,
+                             const gchar *etag,
+                             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 (uri != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_DELETE, 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;
+       }
+
+       if (etag) {
+               ESource *source;
+               gboolean avoid_ifmatch = FALSE;
+
+               source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+               if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+                       ESourceWebdav *webdav_extension;
+
+                       webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+                       avoid_ifmatch = e_source_webdav_get_avoid_ifmatch (webdav_extension);
+               }
+
+               if (!avoid_ifmatch) {
+                       gint len = strlen (etag);
+
+                       if (*etag == '\"' && len > 2 && etag[len - 1] == '\"') {
+                               soup_message_headers_replace (message->request_headers, "If-Match", etag);
+                       } else {
+                               gchar *quoted;
+
+                               quoted = g_strconcat ("\"", etag, "\"", NULL);
+                               soup_message_headers_replace (message->request_headers, "If-Match", quoted);
+                               g_free (quoted);
+                       }
+               }
+       }
+
+       if (depth)
+               soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
delete resource"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_copy_sync:
+ * @webdav: an #EWebDAVSession
+ * @source_uri: URI of the resource or collection to copy
+ * @destination_uri: URI of the destination
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS or %E_WEBDAV_DEPTH_INFINITY
+ * @can_overwrite: whether can overwrite @destination_uri, when it exists
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Copies a resource identified by @source_uri to @destination_uri on the server.
+ * The @source_uri can reference also collections, in which case the @depth influences
+ * whether only the collection itself is copied (%E_WEBDAV_DEPTH_THIS) or whether
+ * the collection with all its children is copied (%E_WEBDAV_DEPTH_INFINITY).
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+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)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (source_uri != NULL, FALSE);
+       g_return_val_if_fail (destination_uri != NULL, FALSE);
+       g_return_val_if_fail (depth != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_COPY, source_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_replace (message->request_headers, "Depth", depth);
+       soup_message_headers_replace (message->request_headers, "Destination", destination_uri);
+       soup_message_headers_replace (message->request_headers, "Overwrite", can_overwrite ? "T" : "F");
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
copy resource"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_move_sync:
+ * @webdav: an #EWebDAVSession
+ * @source_uri: URI of the resource or collection to copy
+ * @destination_uri: URI of the destination
+ * @can_overwrite: whether can overwrite @destination_uri, when it exists
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Moves a resource identified by @source_uri to @destination_uri on the server.
+ * The @source_uri can reference also collections.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_move_sync (EWebDAVSession *webdav,
+                           const gchar *source_uri,
+                           const gchar *destination_uri,
+                           gboolean can_overwrite,
+                           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 (source_uri != NULL, FALSE);
+       g_return_val_if_fail (destination_uri != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_MOVE, source_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_replace (message->request_headers, "Depth", E_WEBDAV_DEPTH_INFINITY);
+       soup_message_headers_replace (message->request_headers, "Destination", destination_uri);
+       soup_message_headers_replace (message->request_headers, "Overwrite", can_overwrite ? "T" : "F");
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
move resource"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_lock_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS or %E_WEBDAV_DEPTH_INFINITY
+ * @lock_timeout: timeout for the lock, in seconds, on 0 to infinity
+ * @xml: an XML describing the lock request, with DAV:lockinfo root element
+ * @out_lock_token: (out) (transfer full): return location of the obtained or refreshed lock token
+ * @out_xml_response: (out) (nullable) (transfer full): optional return location for the server response as 
#xmlDocPtr
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Locks a resource identified by @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * The @out_lock_token can be refreshed with e_webdav_session_refresh_lock_sync().
+ * Release the lock with e_webdav_session_unlock_sync().
+ * Free the returned @out_lock_token with g_free(), when no longer needed.
+ *
+ * If provided, free the returned @out_xml_response with xmlFreeDoc(),
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_lock_sync (EWebDAVSession *webdav,
+                           const gchar *uri,
+                           const gchar *depth,
+                           gint32 lock_timeout,
+                           const EXmlDocument *xml,
+                           gchar **out_lock_token,
+                           xmlDocPtr *out_xml_response,
+                           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);
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+       g_return_val_if_fail (out_lock_token != NULL, FALSE);
+
+       *out_lock_token = NULL;
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_LOCK, 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;
+       }
+
+       if (depth)
+               soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+       if (lock_timeout) {
+               gchar *value;
+
+               value = g_strdup_printf ("Second-%d", lock_timeout);
+               soup_message_headers_replace (message->request_headers, "Timeout", value);
+               g_free (value);
+       } else {
+               soup_message_headers_replace (message->request_headers, "Timeout", "Infinite");
+       }
+
+       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);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
lock resource"), error) &&
+               bytes != NULL;
+
+       if (success && out_xml_response) {
+               const gchar *content_type;
+
+               *out_xml_response = NULL;
+
+               content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+               if (!content_type ||
+                   (g_ascii_strcasecmp (content_type, "application/xml") != 0 &&
+                    g_ascii_strcasecmp (content_type, "text/xml") != 0)) {
+                       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);
+                       }
+
+                       success = FALSE;
+               }
+
+               if (success) {
+                       xmlDocPtr doc;
+
+                       doc = e_xml_parse_data (bytes->data, bytes->len);
+                       if (!doc) {
+                               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                                       _("Failed to parse XML data"));
+
+                               success = FALSE;
+                       } else {
+                               *out_xml_response = doc;
+                       }
+               }
+       }
+
+       if (success)
+               *out_lock_token = g_strdup (soup_message_headers_get_list (message->response_headers, 
"Lock-Token"));
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_refresh_lock_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @lock_token: token of an existing lock
+ * @lock_timeout: timeout for the lock, in seconds, on 0 to infinity
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Refreshes existing lock @lock_token for a resource identified by @uri,
+ * or, in case it's %NULL, on the URI defined in associated #ESource.
+ * The @lock_token is returned from e_webdav_session_lock_sync() and
+ * the @uri should be the same as that used with e_webdav_session_lock_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_refresh_lock_sync (EWebDAVSession *webdav,
+                                   const gchar *uri,
+                                   const gchar *lock_token,
+                                   gint32 lock_timeout,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gchar *value;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (lock_token != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_LOCK, 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;
+       }
+
+       if (lock_timeout) {
+               value = g_strdup_printf ("Second-%d", lock_timeout);
+               soup_message_headers_replace (message->request_headers, "Timeout", value);
+               g_free (value);
+       } else {
+               soup_message_headers_replace (message->request_headers, "Timeout", "Infinite");
+       }
+
+       soup_message_headers_replace (message->request_headers, "Lock-Token", lock_token);
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
refresh lock"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_unlock_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @lock_token: token of an existing lock
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Releases (unlocks) existing lock @lock_token for a resource identified by @uri,
+ * or, in case it's %NULL, on the URI defined in associated #ESource.
+ * The @lock_token is returned from e_webdav_session_lock_sync() and
+ * the @uri should be the same as that used with e_webdav_session_lock_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_unlock_sync (EWebDAVSession *webdav,
+                             const gchar *uri,
+                             const gchar *lock_token,
+                             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 (lock_token != NULL, FALSE);
+
+       request = e_webdav_session_new_request (webdav, SOUP_METHOD_UNLOCK, 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_replace (message->request_headers, "Lock-Token", lock_token);
+
+       bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to 
unlock"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+static gboolean
+e_webdav_session_traverse_propstat_response (EWebDAVSession *webdav,
+                                            const SoupMessage *message,
+                                            const GByteArray *xml_data,
+                                            gboolean require_multistatus,
+                                            const gchar *additional_ns_prefix,
+                                            const gchar *additional_ns,
+                                            const gchar *propstat_path_prefix,
+                                            EWebDAVPropstatTraverseFunc func,
+                                            gpointer func_user_data,
+                                            GError **error)
+{
+       SoupURI *request_uri = NULL;
+       xmlDocPtr doc;
+       xmlXPathContextPtr xpath_ctx;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (xml_data != NULL, FALSE);
+       g_return_val_if_fail (propstat_path_prefix != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       if (message) {
+               const gchar *content_type;
+
+               if (require_multistatus && message->status_code != SOUP_STATUS_MULTI_STATUS) {
+                       g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+                               _("Expected multistatus response, but %d returned (%s)"), 
message->status_code,
+                               e_soup_session_util_status_to_string (message->status_code, 
message->reason_phrase));
+
+                       return FALSE;
+               }
+
+               content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+               if (!content_type ||
+                   (g_ascii_strcasecmp (content_type, "application/xml") != 0 &&
+                    g_ascii_strcasecmp (content_type, "text/xml") != 0)) {
+                       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);
+                       }
+
+                       return FALSE;
+               }
+
+               request_uri = soup_message_get_uri ((SoupMessage *) message);
+       }
+
+       doc = e_xml_parse_data (xml_data->data, xml_data->len);
+
+       if (!doc) {
+               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                       _("Failed to parse XML data"));
+
+               return FALSE;
+       }
+
+       xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+               "D", E_WEBDAV_NS_DAV,
+               additional_ns_prefix, additional_ns,
+               NULL);
+
+       if (xpath_ctx &&
+           func (webdav, xpath_ctx, NULL, request_uri, NULL, SOUP_STATUS_NONE, func_user_data)) {
+               xmlXPathObjectPtr xpath_obj_response;
+
+               xpath_obj_response = e_xml_xpath_eval (xpath_ctx, "%s", propstat_path_prefix);
+
+               if (xpath_obj_response) {
+                       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,
+                                       "%s[%d]/D:propstat",
+                                       propstat_path_prefix, response_index + 1);
+
+                               if (xpath_obj_propstat) {
+                                       gchar *href;
+                                       gint propstat_index, propstat_length;
+
+                                       href = e_xml_xpath_eval_as_string (xpath_ctx, "%s[%d]/D:href", 
propstat_path_prefix, response_index + 1);
+                                       if (href) {
+                                               gchar *full_uri;
+
+                                               full_uri = e_webdav_session_ensure_full_uri (webdav, 
request_uri, href);
+                                               if (full_uri) {
+                                                       g_free (href);
+                                                       href = full_uri;
+                                               }
+                                       }
+
+                                       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 
("%s[%d]/D:propstat[%d]/D:prop",
+                                                       propstat_path_prefix, 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, href, status_code, func_user_data);
+
+                                               g_free (propstat_prefix);
+                                       }
+
+                                       xmlXPathFreeObject (xpath_obj_propstat);
+                                       g_free (href);
+                               }
+                       }
+
+                       xmlXPathFreeObject (xpath_obj_response);
+               }
+       }
+
+       if (xpath_ctx)
+               xmlXPathFreeContext (xpath_ctx);
+       xmlFreeDoc (doc);
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_traverse_multistatus_response:
+ * @webdav: an #EWebDAVSession
+ * @message: (nullable): an optional #SoupMessage corresponding to the response, or %NULL
+ * @xml_data: a #GByteArray containing DAV:multistatus response
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the multistatus response
+ * @func_user_data: user data passed to @func
+ * @error: return location for a #GError, or %NULL
+ *
+ * Traverses a DAV:multistatus response and 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 @message, if provided, is used to verify that the response is a multi-status
+ * and that the Content-Type is properly set. It's used to get a request URI as well.
+ *
+ * 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.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_traverse_multistatus_response (EWebDAVSession *webdav,
+                                               const SoupMessage *message,
+                                               const GByteArray *xml_data,
+                                               EWebDAVPropstatTraverseFunc func,
+                                               gpointer func_user_data,
+                                               GError **error)
+{
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (xml_data != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       return e_webdav_session_traverse_propstat_response (webdav, message, xml_data, TRUE,
+               NULL, NULL, "/D:multistatus/D:response",
+               func, func_user_data, error);
+}
+
+/**
+ * e_webdav_session_traverse_mkcol_response:
+ * @webdav: an #EWebDAVSession
+ * @message: (nullable): an optional #SoupMessage corresponding to the response, or %NULL
+ * @xml_data: a #GByteArray containing DAV:mkcol-response response
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the response
+ * @func_user_data: user data passed to @func
+ * @error: return location for a #GError, or %NULL
+ *
+ * Traverses a DAV:mkcol-response response and 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 @message, if provided, is used to verify that the response is an XML Content-Type.
+ * It's used to get the request URI as well.
+ *
+ * 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.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_traverse_mkcol_response (EWebDAVSession *webdav,
+                                         const SoupMessage *message,
+                                         const GByteArray *xml_data,
+                                         EWebDAVPropstatTraverseFunc func,
+                                         gpointer func_user_data,
+                                         GError **error)
+{
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (xml_data != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       return e_webdav_session_traverse_propstat_response (webdav, message, xml_data, FALSE,
+               NULL, NULL, "/D:mkcol-response",
+               func, func_user_data, error);
+}
+
+/**
+ * e_webdav_session_traverse_mkcalendar_response:
+ * @webdav: an #EWebDAVSession
+ * @message: (nullable): an optional #SoupMessage corresponding to the response, or %NULL
+ * @xml_data: a #GByteArray containing CALDAV:mkcalendar-response response
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the response
+ * @func_user_data: user data passed to @func
+ * @error: return location for a #GError, or %NULL
+ *
+ * Traverses a CALDAV:mkcalendar-response response and calls @func for each returned DAV:propstat.
+ * The provided XPath context has registered %E_WEBDAV_NS_DAV namespace with prefix "D" and
+ * %E_WEBDAV_NS_CALDAV namespace with prefix "C". It doesn't have any other namespace registered.
+ *
+ * The @message, if provided, is used to verify that the response is an XML Content-Type.
+ * It's used to get the request URI as well.
+ *
+ * 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.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_traverse_mkcalendar_response (EWebDAVSession *webdav,
+                                              const SoupMessage *message,
+                                              const GByteArray *xml_data,
+                                              EWebDAVPropstatTraverseFunc func,
+                                              gpointer func_user_data,
+                                              GError **error)
+{
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (xml_data != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       return e_webdav_session_traverse_propstat_response (webdav, message, xml_data, FALSE,
+               "C", E_WEBDAV_NS_CALDAV, "/C:mkcalendar-response",
+               func, func_user_data, error);
+}
+
+static gboolean
+e_webdav_session_getctag_cb (EWebDAVSession *webdav,
+                            xmlXPathContextPtr xpath_ctx,
+                            const gchar *xpath_prop_prefix,
+                            const SoupURI *request_uri,
+                            const gchar *href,
+                            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_util_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,
+ * in case it's %NULL, on the URI defined in associated #ESource. 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_add_empty_element (xml, E_WEBDAV_NS_CALENDARSERVER, "getctag");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, 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;
+                               else if (g_ascii_strcasecmp (name, "VFREEBUSY") == 0)
+                                       supports |= E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY;
+                               else if (g_ascii_strcasecmp (name, "VTIMEZONE") == 0)
+                                       supports |= E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE;
+
+                               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 |
+                               E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY |
+                               E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE;
+               }
+       }
+
+       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_util_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, 0);
+       g_return_val_if_fail (xpath_prop_prefix != NULL, 0);
+
+       value = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "D:getcontentlength", NULL);
+       if (!value)
+               return 0;
+
+       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,
+                         const gchar *href,
+                         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 *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;
+
+               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,
+                       href,
+                       NULL, /* etag */
+                       NULL, /* display_name */
+                       NULL, /* content_type */
+                       content_length,
+                       creation_date,
+                       last_modified,
+                       NULL, /* description */
+                       NULL); /* color */
+               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
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS, %E_WEBDAV_DEPTH_THIS_AND_CHILDREN or 
%E_WEBDAV_DEPTH_INFINITY
+ * @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, in case it's %NULL, of the URI defined
+ * in 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,
+                           const gchar *depth,
+                           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_add_empty_element (xml, NULL, "resourcetype");
+
+       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_add_empty_element (xml, E_WEBDAV_NS_CALDAV, 
"supported-calendar-component-set");
+       }
+
+       if ((flags & E_WEBDAV_LIST_DISPLAY_NAME) != 0) {
+               e_xml_document_add_empty_element (xml, NULL, "displayname");
+       }
+
+       if ((flags & E_WEBDAV_LIST_ETAG) != 0) {
+               e_xml_document_add_empty_element (xml, NULL, "getetag");
+
+               e_xml_document_add_namespaces (xml, "CS", E_WEBDAV_NS_CALENDARSERVER, NULL);
+
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALENDARSERVER, "getctag");
+       }
+
+       if ((flags & E_WEBDAV_LIST_CONTENT_TYPE) != 0) {
+               e_xml_document_add_empty_element (xml, NULL, "getcontenttype");
+       }
+
+       if ((flags & E_WEBDAV_LIST_CONTENT_LENGTH) != 0) {
+               e_xml_document_add_empty_element (xml, NULL, "getcontentlength");
+       }
+
+       if ((flags & E_WEBDAV_LIST_CREATION_DATE) != 0) {
+               e_xml_document_add_empty_element (xml, NULL, "creationdate");
+       }
+
+       if ((flags & E_WEBDAV_LIST_LAST_MODIFIED) != 0) {
+               e_xml_document_add_empty_element (xml, NULL, "getlastmodified");
+       }
+
+       if ((flags & E_WEBDAV_LIST_DESCRIPTION) != 0) {
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-description");
+
+               e_xml_document_add_namespaces (xml, "A", E_WEBDAV_NS_CARDDAV, NULL);
+
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-description");
+       }
+
+       if ((flags & E_WEBDAV_LIST_COLOR) != 0) {
+               e_xml_document_add_namespaces (xml, "IC", E_WEBDAV_NS_ICAL, NULL);
+
+               e_xml_document_add_empty_element (xml, E_WEBDAV_NS_ICAL, "calendar-color");
+       }
+
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, depth, 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;
+}
+
+/**
+ * e_webdav_session_update_properties_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @changes: (element-type EWebDAVResource): a #GSList with request changes
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Updates properties (set/remove) on the provided @uri, or, in case it's %NULL,
+ * on the URI defined in associated #ESource, with the @changes. The order
+ * of @changes is significant, unlike on other places.
+ *
+ * This function supports only flat properties, those not under other element.
+ * To support more complex property tries use e_webdav_session_proppatch_sync()
+ * directly.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_update_properties_sync (EWebDAVSession *webdav,
+                                        const gchar *uri,
+                                        const GSList *changes,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       EXmlDocument *xml;
+       GSList *link;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (changes != NULL, FALSE);
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propertyupdate");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       for (link = (GSList *) changes; link; link = g_slist_next (link)) {
+               EWebDAVPropertyChange *change = link->data;
+
+               if (!change)
+                       continue;
+
+               switch (change->kind) {
+               case E_WEBDAV_PROPERTY_SET:
+                       e_xml_document_start_element (xml, NULL, "set");
+                       e_xml_document_start_element (xml, NULL, "prop");
+                       e_xml_document_start_text_element (xml, change->ns_uri, change->name);
+                       if (change->value) {
+                               e_xml_document_write_string (xml, change->value);
+                       }
+                       e_xml_document_end_element (xml); /* change->name */
+                       e_xml_document_end_element (xml); /* prop */
+                       e_xml_document_end_element (xml); /* set */
+                       break;
+               case E_WEBDAV_PROPERTY_REMOVE:
+                       e_xml_document_start_element (xml, NULL, "remove");
+                       e_xml_document_start_element (xml, NULL, "prop");
+                       e_xml_document_add_empty_element (xml, change->ns_uri, change->name);
+                       e_xml_document_end_element (xml); /* prop */
+                       e_xml_document_end_element (xml); /* remove */
+                       break;
+               }
+       }
+
+       success = e_webdav_session_proppatch_sync (webdav, uri, xml, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_lock_resource_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @lock_scope: an #EWebDAVLockScope to define the scope of the lock
+ * @lock_timeout: timeout for the lock, in seconds, on 0 to infinity
+ * @owner: (nullable): optional identificator of the owner of the lock, or %NULL
+ * @out_lock_token: (out) (transfer full): return location of the obtained or refreshed lock token
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Locks a resource identified by @uri, or, in case it's %NULL, by the URI defined
+ * in associated #ESource. It obtains a write lock with the given @lock_scope.
+ *
+ * The @owner is used to identify the lock owner. When it's an http:// or https://,
+ * then it's referenced as DAV:href, otherwise the value is treated as plain text.
+ * If it's %NULL, then the user name from the associated #ESource is used.
+ *
+ * The @out_lock_token can be refreshed with e_webdav_session_refresh_lock_sync().
+ * Release the lock with e_webdav_session_unlock_sync().
+ * Free the returned @out_lock_token with g_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_lock_resource_sync (EWebDAVSession *webdav,
+                                    const gchar *uri,
+                                    EWebDAVLockScope lock_scope,
+                                    gint32 lock_timeout,
+                                    const gchar *owner,
+                                    gchar **out_lock_token,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       EXmlDocument *xml;
+       gchar *owner_ref;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (out_lock_token != NULL, FALSE);
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "lockinfo");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       e_xml_document_start_element (xml, NULL, "lockscope");
+       switch (lock_scope) {
+       case E_WEBDAV_LOCK_EXCLUSIVE:
+               e_xml_document_add_empty_element (xml, NULL, "exclusive");
+               break;
+       case E_WEBDAV_LOCK_SHARED:
+               e_xml_document_add_empty_element (xml, NULL, "shared");
+               break;
+       }
+       e_xml_document_end_element (xml); /* lockscope */
+
+       e_xml_document_start_element (xml, NULL, "locktype");
+       e_xml_document_add_empty_element (xml, NULL, "write");
+       e_xml_document_end_element (xml); /* locktype */
+
+       e_xml_document_start_text_element (xml, NULL, "owner");
+       if (owner) {
+               owner_ref = g_strdup (owner);
+       } else {
+               ESource *source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+
+               owner_ref = NULL;
+
+               if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+                       owner_ref = e_source_authentication_dup_user (
+                               e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION));
+
+                       if (owner_ref && !*owner_ref)
+                               g_clear_pointer (&owner_ref, g_free);
+               }
+
+               if (!owner_ref && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+                       owner_ref = e_source_webdav_dup_email_address (
+                               e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND));
+
+                       if (owner_ref && !*owner_ref)
+                               g_clear_pointer (&owner_ref, g_free);
+               }
+       }
+
+       if (!owner_ref)
+               owner_ref = g_strconcat (g_get_host_name (), " / ", g_get_user_name (), NULL);
+
+       if (owner_ref) {
+               if (g_str_has_prefix (owner_ref, "http://";) ||
+                   g_str_has_prefix (owner_ref, "https://";)) {
+                       e_xml_document_start_element (xml, NULL, "href");
+                       e_xml_document_write_string (xml, owner_ref);
+                       e_xml_document_end_element (xml); /* href */
+               } else {
+                       e_xml_document_write_string (xml, owner_ref);
+               }
+       }
+
+       g_free (owner_ref);
+       e_xml_document_end_element (xml); /* owner */
+
+       success = e_webdav_session_lock_sync (webdav, uri, E_WEBDAV_DEPTH_INFINITY, lock_timeout, xml,
+               out_lock_token, NULL, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success;
+}
+
+static void
+e_webdav_session_traverse_privilege_level (xmlXPathContextPtr xpath_ctx,
+                                          const gchar *xpath_prefix,
+                                          GNode *parent)
+{
+       xmlXPathObjectPtr xpath_obj;
+
+       g_return_if_fail (xpath_ctx != NULL);
+       g_return_if_fail (xpath_prefix != NULL);
+       g_return_if_fail (parent != NULL);
+
+       xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:supported-privilege", xpath_prefix);
+
+       if (xpath_obj) {
+               gint ii, length;
+
+               length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+               for (ii = 0; ii < length; ii++) {
+                       xmlXPathObjectPtr xpath_obj_privilege;
+                       gchar *prefix;
+
+                       prefix = g_strdup_printf ("%s/D:supported-privilege[%d]", xpath_prefix, ii + 1);
+                       xpath_obj_privilege = e_xml_xpath_eval (xpath_ctx, "%s/D:privilege", prefix);
+
+                       if (xpath_obj_privilege &&
+                           xpath_obj_privilege->type == XPATH_NODESET &&
+                           xpath_obj_privilege->nodesetval &&
+                           xpath_obj_privilege->nodesetval->nodeNr == 1 &&
+                           xpath_obj_privilege->nodesetval->nodeTab &&
+                           xpath_obj_privilege->nodesetval->nodeTab[0] &&
+                           xpath_obj_privilege->nodesetval->nodeTab[0]->children) {
+                               xmlNodePtr node;
+
+                               for (node = xpath_obj_privilege->nodesetval->nodeTab[0]->children; node; node 
= node->next) {
+                                       if (node->type == XML_ELEMENT_NODE &&
+                                           node->name && *(node->name) &&
+                                           node->ns && node->ns->href && *(node->ns->href)) {
+                                               break;
+                                       }
+                               }
+
+                               if (node) {
+                                       GNode *child;
+                                       gchar *description;
+                                       EWebDAVPrivilegeKind kind = E_WEBDAV_PRIVILEGE_KIND_COMMON;
+                                       EWebDAVPrivilegeHint hint = E_WEBDAV_PRIVILEGE_HINT_UNKNOWN;
+                                       EWebDAVPrivilege *privilege;
+
+                                       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:abstract", prefix))
+                                               kind = E_WEBDAV_PRIVILEGE_KIND_ABSTRACT;
+                                       else if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:aggregate", 
prefix))
+                                               kind = E_WEBDAV_PRIVILEGE_KIND_AGGREGATE;
+
+                                       description = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:description", prefix);
+                                       privilege = e_webdav_privilege_new ((const gchar *) node->ns->href, 
(const gchar *) node->name, description, kind, hint);
+                                       child = g_node_new (privilege);
+                                       g_node_append (parent, child);
+
+                                       g_free (description);
+
+                                       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:supported-privilege", 
prefix))
+                                               e_webdav_session_traverse_privilege_level (xpath_ctx, prefix, 
child);
+                               }
+                       }
+
+                       if (xpath_obj_privilege)
+                               xmlXPathFreeObject (xpath_obj_privilege);
+
+                       g_free (prefix);
+               }
+
+               xmlXPathFreeObject (xpath_obj);
+       }
+}
+
+static gboolean
+e_webdav_session_supported_privilege_set_cb (EWebDAVSession *webdav,
+                                            xmlXPathContextPtr xpath_ctx,
+                                            const gchar *xpath_prop_prefix,
+                                            const SoupURI *request_uri,
+                                            const gchar *href,
+                                            guint status_code,
+                                            gpointer user_data)
+{
+       GNode **out_privileges = user_data;
+
+       g_return_val_if_fail (out_privileges != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx,
+                       "C", E_WEBDAV_NS_CALDAV,
+                       NULL);
+       } else if (status_code == SOUP_STATUS_OK &&
+                  e_xml_xpath_eval_exists (xpath_ctx, "%s/D:supported-privilege-set/D:supported-privilege", 
xpath_prop_prefix)) {
+               GNode *root;
+               gchar *prefix;
+
+               prefix = g_strconcat (xpath_prop_prefix, "/D:supported-privilege-set", NULL);
+               root = g_node_new (NULL);
+
+               e_webdav_session_traverse_privilege_level (xpath_ctx, prefix, root);
+
+               *out_privileges = root;
+
+               g_free (prefix);
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_acl_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @xml:the request itself, as an #EXmlDocument, the root element should be DAV:acl
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues ACL request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_acl_sync (EWebDAVSession *webdav,
+                          const gchar *uri,
+                          const EXmlDocument *xml,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       SoupRequestHTTP *request;
+       SoupMessage *message;
+       GByteArray *bytes;
+       gchar *content;
+       gsize content_length;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+
+       request = e_webdav_session_new_request (webdav, "ACL", 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;
+       }
+
+       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);
+
+       success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to 
get access control list"), error) &&
+               bytes != NULL;
+
+       if (bytes)
+               g_byte_array_free (bytes, TRUE);
+       g_object_unref (message);
+       g_object_unref (request);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_get_supported_privilege_set_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_privileges: (out) (transfer full) (element-type EWebDAVPrivilege): return location for the tree of 
supported privileges
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets supported privileges for the @uri, or, in case it's %NULL, for the URI
+ * defined in associated #ESource.
+ *
+ * The root node of @out_privileges has always %NULL data.
+ *
+ * Free the returned @out_privileges with e_webdav_session_util_free_privileges()
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_supported_privilege_set_sync (EWebDAVSession *webdav,
+                                                  const gchar *uri,
+                                                  GNode **out_privileges,
+                                                  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_privileges != NULL, FALSE);
+
+       *out_privileges = 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_add_empty_element (xml, NULL, "supported-privilege-set");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_session_supported_privilege_set_cb, out_privileges, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success;
+}
+
+static EWebDAVPrivilege *
+e_webdav_session_extract_privilege_simple (xmlXPathObjectPtr xpath_obj_privilege)
+{
+       EWebDAVPrivilege *privilege = NULL;
+
+       if (xpath_obj_privilege &&
+           xpath_obj_privilege->type == XPATH_NODESET &&
+           xpath_obj_privilege->nodesetval &&
+           xpath_obj_privilege->nodesetval->nodeNr == 1 &&
+           xpath_obj_privilege->nodesetval->nodeTab &&
+           xpath_obj_privilege->nodesetval->nodeTab[0] &&
+           xpath_obj_privilege->nodesetval->nodeTab[0]->children) {
+               xmlNodePtr node;
+
+               for (node = xpath_obj_privilege->nodesetval->nodeTab[0]->children; node; node = node->next) {
+                       if (node->type == XML_ELEMENT_NODE &&
+                           node->name && *(node->name) &&
+                           node->ns && node->ns->href && *(node->ns->href)) {
+                               break;
+                       }
+               }
+
+               if (node) {
+                       privilege = e_webdav_privilege_new ((const gchar *) node->ns->href, (const gchar *) 
node->name,
+                               NULL, E_WEBDAV_PRIVILEGE_KIND_COMMON, E_WEBDAV_PRIVILEGE_HINT_UNKNOWN);
+               }
+       }
+
+       return privilege;
+}
+
+static gboolean
+e_webdav_session_current_user_privilege_set_cb (EWebDAVSession *webdav,
+                                               xmlXPathContextPtr xpath_ctx,
+                                               const gchar *xpath_prop_prefix,
+                                               const SoupURI *request_uri,
+                                               const gchar *href,
+                                               guint status_code,
+                                               gpointer user_data)
+{
+       GSList **out_privileges = user_data;
+
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (out_privileges != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+               e_xml_xpath_context_register_namespaces (xpath_ctx,
+                       "C", E_WEBDAV_NS_CALDAV,
+                       NULL);
+       } else if (status_code == SOUP_STATUS_OK &&
+                  e_xml_xpath_eval_exists (xpath_ctx, "%s/D:current-user-privilege-set/D:privilege", 
xpath_prop_prefix)) {
+               xmlXPathObjectPtr xpath_obj;
+
+               xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:current-user-privilege-set/D:privilege", 
xpath_prop_prefix);
+
+               if (xpath_obj) {
+                       gint ii, length;
+
+                       length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+                       for (ii = 0; ii < length; ii++) {
+                               xmlXPathObjectPtr xpath_obj_privilege;
+
+                               xpath_obj_privilege = e_xml_xpath_eval (xpath_ctx, 
"%s/D:current-user-privilege-set/D:privilege[%d]", xpath_prop_prefix, ii + 1);
+
+                               if (xpath_obj_privilege) {
+                                       EWebDAVPrivilege *privilege;
+
+                                       privilege = e_webdav_session_extract_privilege_simple 
(xpath_obj_privilege);
+                                       if (privilege)
+                                               *out_privileges = g_slist_prepend (*out_privileges, 
privilege);
+
+                                       xmlXPathFreeObject (xpath_obj_privilege);
+                               }
+                       }
+
+                       xmlXPathFreeObject (xpath_obj);
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_get_current_user_privilege_set_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_privileges: (out) (transfer full) (element-type EWebDAVPrivilege): return location for a %GSList of 
#EWebDAVPrivilege
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets current user privileges for the @uri, or, in case it's %NULL, for the URI
+ * defined in associated #ESource.
+ *
+ * Free the returned @out_privileges with
+ * g_slist_free_full (privileges, e_webdav_privilege_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_current_user_privilege_set_sync (EWebDAVSession *webdav,
+                                                     const gchar *uri,
+                                                     GSList **out_privileges,
+                                                     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_privileges != NULL, FALSE);
+
+       *out_privileges = 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_add_empty_element (xml, NULL, "current-user-privilege-set");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_session_current_user_privilege_set_cb, out_privileges, cancellable, error);
+
+       g_object_unref (xml);
+
+       if (success)
+               *out_privileges = g_slist_reverse (*out_privileges);
+
+       return success;
+}
+
+static EWebDAVACEPrincipalKind
+e_webdav_session_extract_acl_principal (xmlXPathContextPtr xpath_ctx,
+                                       const gchar *principal_prefix,
+                                       gchar **out_principal_href,
+                                       GSList **out_principal_hrefs)
+{
+       g_return_val_if_fail (xpath_ctx != NULL, E_WEBDAV_ACE_PRINCIPAL_UNKNOWN);
+       g_return_val_if_fail (principal_prefix != NULL, E_WEBDAV_ACE_PRINCIPAL_UNKNOWN);
+       g_return_val_if_fail (out_principal_href != NULL || out_principal_hrefs != NULL, 
E_WEBDAV_ACE_PRINCIPAL_UNKNOWN);
+
+       *out_principal_href = NULL;
+
+       if (!e_xml_xpath_eval_exists (xpath_ctx, "%s", principal_prefix))
+               return E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:href", principal_prefix)) {
+               if (out_principal_href) {
+                       *out_principal_href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:href", 
principal_prefix);
+               } else {
+                       xmlXPathObjectPtr xpath_obj;
+
+                       *out_principal_hrefs = NULL;
+
+                       xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:href", principal_prefix);
+
+                       if (xpath_obj) {
+                               gint ii, length;
+
+                               length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+                               for (ii = 0; ii < length; ii++) {
+                                       gchar *href;
+
+                                       href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:href[%d]", 
principal_prefix, ii + 1);
+                                       if (href)
+                                               *out_principal_hrefs = g_slist_prepend (*out_principal_hrefs, 
href);
+                               }
+                       }
+
+                       *out_principal_hrefs = g_slist_reverse (*out_principal_hrefs);
+               }
+
+               return E_WEBDAV_ACE_PRINCIPAL_HREF;
+       }
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:all", principal_prefix))
+               return E_WEBDAV_ACE_PRINCIPAL_ALL;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:authenticated", principal_prefix))
+               return E_WEBDAV_ACE_PRINCIPAL_AUTHENTICATED;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:unauthenticated", principal_prefix))
+               return E_WEBDAV_ACE_PRINCIPAL_UNAUTHENTICATED;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:self", principal_prefix))
+               return E_WEBDAV_ACE_PRINCIPAL_SELF;
+
+       if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:property", principal_prefix)) {
+               /* No details read about what properties */
+               EWebDAVACEPrincipalKind kind = E_WEBDAV_ACE_PRINCIPAL_PROPERTY;
+
+               /* Special-case owner */
+               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:property/D:owner", principal_prefix)) {
+                       xmlXPathObjectPtr xpath_obj_property;
+
+                       xpath_obj_property = e_xml_xpath_eval (xpath_ctx, "%s/D:property", principal_prefix);
+
+                       /* DAV:owner is the only child and there is only one DAV:property child of the 
DAV:principal */
+                       if (xpath_obj_property &&
+                           xpath_obj_property->type == XPATH_NODESET &&
+                           xmlXPathNodeSetGetLength (xpath_obj_property->nodesetval) == 1 &&
+                           xpath_obj_property->nodesetval &&
+                           xpath_obj_property->nodesetval->nodeNr == 1 &&
+                           xpath_obj_property->nodesetval->nodeTab &&
+                           xpath_obj_property->nodesetval->nodeTab[0] &&
+                           xpath_obj_property->nodesetval->nodeTab[0]->children) {
+                               xmlNodePtr node;
+                               gint subelements = 0;
+
+                               for (node = xpath_obj_property->nodesetval->nodeTab[0]->children; node && 
subelements <= 1; node = node->next) {
+                                       if (node->type == XML_ELEMENT_NODE)
+                                               subelements++;
+                               }
+
+                               if (subelements == 1)
+                                       kind = E_WEBDAV_ACE_PRINCIPAL_OWNER;
+                       }
+
+                       if (xpath_obj_property)
+                               xmlXPathFreeObject (xpath_obj_property);
+               }
+
+               return kind;
+       }
+
+       return E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+}
+
+static gboolean
+e_webdav_session_acl_cb (EWebDAVSession *webdav,
+                        xmlXPathContextPtr xpath_ctx,
+                        const gchar *xpath_prop_prefix,
+                        const SoupURI *request_uri,
+                        const gchar *href,
+                        guint status_code,
+                        gpointer user_data)
+{
+       GSList **out_entries = user_data;
+
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (out_entries != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+       } else if (status_code == SOUP_STATUS_OK &&
+                  e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl/D:ace", xpath_prop_prefix)) {
+               xmlXPathObjectPtr xpath_obj_ace;
+
+               xpath_obj_ace = e_xml_xpath_eval (xpath_ctx, "%s/D:acl/D:ace", xpath_prop_prefix);
+
+               if (xpath_obj_ace) {
+                       gint ii, length;
+
+                       length = xmlXPathNodeSetGetLength (xpath_obj_ace->nodesetval);
+
+                       for (ii = 0; ii < length; ii++) {
+                               EWebDAVACEPrincipalKind principal_kind = E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+                               xmlXPathObjectPtr xpath_obj = NULL;
+                               gchar *principal_href = NULL;
+                               guint32 flags = E_WEBDAV_ACE_FLAG_UNKNOWN;
+                               gchar *inherited_href = NULL;
+                               gchar *privilege_prefix = NULL;
+                               gchar *ace_prefix;
+
+                               ace_prefix = g_strdup_printf ("%s/D:acl/D:ace[%d]", xpath_prop_prefix, ii + 
1);
+
+                               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:invert", ace_prefix)) {
+                                       gchar *prefix;
+
+                                       flags |= E_WEBDAV_ACE_FLAG_INVERT;
+
+                                       prefix = g_strdup_printf ("%s/D:invert/D:principal", ace_prefix);
+                                       principal_kind = e_webdav_session_extract_acl_principal (xpath_ctx, 
prefix, &principal_href, NULL);
+                                       g_free (prefix);
+                               } else {
+                                       gchar *prefix;
+
+                                       prefix = g_strdup_printf ("%s/D:principal", ace_prefix);
+                                       principal_kind = e_webdav_session_extract_acl_principal (xpath_ctx, 
prefix, &principal_href, NULL);
+                                       g_free (prefix);
+                               }
+
+                               if (principal_kind == E_WEBDAV_ACE_PRINCIPAL_UNKNOWN) {
+                                       g_free (ace_prefix);
+                                       continue;
+                               }
+
+                               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:protected", ace_prefix))
+                                       flags |= E_WEBDAV_ACE_FLAG_PROTECTED;
+
+                               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:inherited/D:href", ace_prefix)) 
{
+                                       flags |= E_WEBDAV_ACE_FLAG_INHERITED;
+                                       inherited_href = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:inherited/D:href", ace_prefix);
+                               }
+
+                               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:grant", ace_prefix)) {
+                                       privilege_prefix = g_strdup_printf ("%s/D:grant/D:privilege", 
ace_prefix);
+                                       flags |= E_WEBDAV_ACE_FLAG_GRANT;
+                               } else if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:deny", ace_prefix)) {
+                                       privilege_prefix = g_strdup_printf ("%s/D:deny/D:privilege", 
ace_prefix);
+                                       flags |= E_WEBDAV_ACE_FLAG_DENY;
+                               }
+
+                               if (privilege_prefix)
+                                       xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s", privilege_prefix);
+
+                               if (xpath_obj) {
+                                       EWebDAVAccessControlEntry *ace;
+                                       gint ii, length;
+
+                                       ace = e_webdav_access_control_entry_new (principal_kind, 
principal_href, flags, inherited_href);
+                                       if (ace) {
+                                               length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+                                               for (ii = 0; ii < length; ii++) {
+                                                       xmlXPathObjectPtr xpath_obj_privilege;
+
+                                                       xpath_obj_privilege = e_xml_xpath_eval (xpath_ctx, 
"%s[%d]", privilege_prefix, ii + 1);
+
+                                                       if (xpath_obj_privilege) {
+                                                               EWebDAVPrivilege *privilege;
+
+                                                               privilege = 
e_webdav_session_extract_privilege_simple (xpath_obj_privilege);
+                                                               if (privilege)
+                                                                       ace->privileges = g_slist_prepend 
(ace->privileges, privilege);
+
+                                                               xmlXPathFreeObject (xpath_obj_privilege);
+                                                       }
+                                               }
+
+                                               ace->privileges = g_slist_reverse (ace->privileges);
+
+                                               *out_entries = g_slist_prepend (*out_entries, ace);
+                                       }
+
+                                       xmlXPathFreeObject (xpath_obj);
+                               }
+
+                               g_free (principal_href);
+                               g_free (inherited_href);
+                               g_free (privilege_prefix);
+                               g_free (ace_prefix);
+                       }
+
+                       xmlXPathFreeObject (xpath_obj_ace);
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_get_acl_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_entries: (out) (transfer full) (element-type EWebDAVAccessControlEntry): return location for a 
#GSList of #EWebDAVAccessControlEntry
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets Access Control List (ACL) for the @uri, or, in case it's %NULL, for the URI
+ * defined in associated #ESource.
+ *
+ * This function doesn't read general #E_WEBDAV_ACE_PRINCIPAL_PROPERTY.
+ *
+ * Free the returned @out_entries with
+ * g_slist_free_full (entries, e_webdav_access_control_entry_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_acl_sync (EWebDAVSession *webdav,
+                              const gchar *uri,
+                              GSList **out_entries,
+                              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_entries != NULL, FALSE);
+
+       *out_entries = 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_add_empty_element (xml, NULL, "acl");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_session_acl_cb, out_entries, cancellable, error);
+
+       g_object_unref (xml);
+
+       if (success)
+               *out_entries = g_slist_reverse (*out_entries);
+
+       return success;
+}
+
+typedef struct _ACLRestrictionsData {
+       guint32 *out_restrictions;
+       EWebDAVACEPrincipalKind *out_principal_kind;
+       GSList **out_principal_hrefs;
+} ACLRestrictionsData;
+
+static gboolean
+e_webdav_session_acl_restrictions_cb (EWebDAVSession *webdav,
+                                     xmlXPathContextPtr xpath_ctx,
+                                     const gchar *xpath_prop_prefix,
+                                     const SoupURI *request_uri,
+                                     const gchar *href,
+                                     guint status_code,
+                                     gpointer user_data)
+{
+       ACLRestrictionsData *ard = user_data;
+
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (ard != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+       } else if (status_code == SOUP_STATUS_OK &&
+                  e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions", xpath_prop_prefix)) {
+               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:grant-only", 
xpath_prop_prefix))
+                       *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_GRANT_ONLY;
+
+               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:no-invert", 
xpath_prop_prefix))
+                       *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_NO_INVERT;
+
+               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:deny-before-grant", 
xpath_prop_prefix))
+                       *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_DENY_BEFORE_GRANT;
+
+               if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:required-principal", 
xpath_prop_prefix)) {
+                       gchar *prefix;
+
+                       *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_REQUIRED_PRINCIPAL;
+
+                       prefix = g_strdup_printf ("%s/D:acl-restrictions/D:required-principal", 
xpath_prop_prefix);
+                       *ard->out_principal_kind = e_webdav_session_extract_acl_principal (xpath_ctx, prefix, 
NULL, ard->out_principal_hrefs);
+                       g_free (prefix);
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_get_acl_restrictions_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_restrictions: (out): return location for bit-or of #EWebDAVACLRestrictions
+ * @out_principal_kind: (out): return location for principal kind
+ * @out_principal_hrefs: (out) (transfer full) (element-type utf8): return location for a #GSList of 
principal href-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets Access Control List (ACL) restrictions for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource. The @out_principal_kind is valid only
+ * if the @out_restrictions contains #E_WEBDAV_ACL_RESTRICTION_REQUIRED_PRINCIPAL.
+ * The @out_principal_hrefs is valid only if the @out_principal_kind is valid and when
+ * it is #E_WEBDAV_ACE_PRINCIPAL_HREF.
+ *
+ * Free the returned @out_principal_hrefs with
+ * g_slist_free_full (entries, g_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_acl_restrictions_sync (EWebDAVSession *webdav,
+                                           const gchar *uri,
+                                           guint32 *out_restrictions,
+                                           EWebDAVACEPrincipalKind *out_principal_kind,
+                                           GSList **out_principal_hrefs,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       ACLRestrictionsData ard;
+       EXmlDocument *xml;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (out_restrictions != NULL, FALSE);
+       g_return_val_if_fail (out_principal_kind != NULL, FALSE);
+       g_return_val_if_fail (out_principal_hrefs != NULL, FALSE);
+
+       *out_restrictions = E_WEBDAV_ACL_RESTRICTION_NONE;
+       *out_principal_kind = E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+       *out_principal_hrefs = 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_add_empty_element (xml, NULL, "acl-restrictions");
+       e_xml_document_end_element (xml); /* prop */
+
+       ard.out_restrictions = out_restrictions;
+       ard.out_principal_kind = out_principal_kind;
+       ard.out_principal_hrefs = out_principal_hrefs;
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_session_acl_restrictions_cb, &ard, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success;
+}
+
+static gboolean
+e_webdav_session_principal_collection_set_cb (EWebDAVSession *webdav,
+                                             xmlXPathContextPtr xpath_ctx,
+                                             const gchar *xpath_prop_prefix,
+                                             const SoupURI *request_uri,
+                                             const gchar *href,
+                                             guint status_code,
+                                             gpointer user_data)
+{
+       GSList **out_principal_hrefs = user_data;
+
+       g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+       g_return_val_if_fail (out_principal_hrefs != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+       } else if (status_code == SOUP_STATUS_OK &&
+                  e_xml_xpath_eval_exists (xpath_ctx, "%s/D:principal-collection-set", xpath_prop_prefix)) {
+               xmlXPathObjectPtr xpath_obj;
+
+               xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:principal-collection-set/D:href", 
xpath_prop_prefix);
+
+               if (xpath_obj) {
+                       gint ii, length;
+
+                       length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+                       for (ii = 0; ii < length; ii++) {
+                               gchar *href;
+
+                               href = e_xml_xpath_eval_as_string (xpath_ctx, 
"%s/D:principal-collection-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+                               if (href)
+                                       *out_principal_hrefs = g_slist_prepend (*out_principal_hrefs, href);
+                       }
+
+                       xmlXPathFreeObject (xpath_obj);
+               }
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_get_principal_collection_set_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_principal_hrefs: (out) (transfer full) (element-type utf8): return location for a #GSList of 
principal href-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets list of principal collection href for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource. The @out_principal_hrefs are root
+ * collections that contain the principals that are available on the server that
+ * implements this resource.
+ *
+ * Free the returned @out_principal_hrefs with
+ * g_slist_free_full (entries, g_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_principal_collection_set_sync (EWebDAVSession *webdav,
+                                                   const gchar *uri,
+                                                   GSList **out_principal_hrefs, /* gchar * */
+                                                   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_principal_hrefs != NULL, FALSE);
+
+       *out_principal_hrefs = 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_add_empty_element (xml, NULL, "principal-collection-set");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_session_principal_collection_set_cb, out_principal_hrefs, cancellable, error);
+
+       g_object_unref (xml);
+
+       if (success)
+               *out_principal_hrefs = g_slist_reverse (*out_principal_hrefs);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_set_acl_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @entries: (element-type EWebDAVAccessControlEntry): entries to write
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes Access Control List (ACL) for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource.
+ *
+ * Make sure that the @entries satisfy ACL restrictions, as returned
+ * by e_webdav_session_get_acl_restrictions_sync(). The order in the @entries
+ * is preserved. It cannot contain any %E_WEBDAV_ACE_FLAG_PROTECTED,
+ * nor @E_WEBDAV_ACE_FLAG_INHERITED, items.
+ *
+ * Use e_webdav_session_get_acl_sync() to read currently known ACL entries,
+ * remove from the list those protected and inherited, and then modify
+ * the rest with the required changed.
+ *
+ * Note this function doesn't support general %E_WEBDAV_ACE_PRINCIPAL_PROPERTY and
+ * returns %G_IO_ERROR_NOT_SUPPORTED error when any such is tried to be written.
+ *
+ * In case the returned entries contain any %E_WEBDAV_ACE_PRINCIPAL_PROPERTY,
+ * or there's a need to write such Access Control Entry, then do not use
+ * e_webdav_session_get_acl_sync(), neither e_webdav_session_set_acl_sync(),
+ * and write more generic implementation.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_set_acl_sync (EWebDAVSession *webdav,
+                              const gchar *uri,
+                              const GSList *entries,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       EXmlDocument *xml;
+       GSList *link, *plink;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (entries != NULL, FALSE);
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "acl");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       for (link = (GSList *) entries; link; link = g_slist_next (link)) {
+               EWebDAVAccessControlEntry *ace = link->data;
+
+               if (!ace) {
+                       g_warn_if_fail (ace != NULL);
+                       g_object_unref (xml);
+                       return FALSE;
+               }
+
+               if ((ace->flags & E_WEBDAV_ACE_FLAG_PROTECTED) != 0 ||
+                   (ace->flags & E_WEBDAV_ACE_FLAG_INHERITED) != 0) {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                               _("Cannot store protected nor inherited Access Control Entry."));
+                       g_object_unref (xml);
+                       return FALSE;
+               }
+
+               if (ace->principal_kind == E_WEBDAV_ACE_PRINCIPAL_UNKNOWN) {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                               _("Provided invalid principal kind for Access Control Entry."));
+                       g_object_unref (xml);
+                       return FALSE;
+               }
+
+               if (ace->principal_kind == E_WEBDAV_ACE_PRINCIPAL_PROPERTY) {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                               _("Cannot store property-based Access Control Entry."));
+                       g_object_unref (xml);
+                       return FALSE;
+               }
+
+               if ((ace->flags & (E_WEBDAV_ACE_FLAG_GRANT | E_WEBDAV_ACE_FLAG_DENY)) == 0) {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                               _("Access Control Entry can be only to Grant or Deny, but not None."));
+                       g_object_unref (xml);
+                       return FALSE;
+               }
+
+               if ((ace->flags & E_WEBDAV_ACE_FLAG_GRANT) != 0 &&
+                   (ace->flags & E_WEBDAV_ACE_FLAG_DENY) != 0) {
+                       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                               _("Access Control Entry can be only to Grant or Deny, but not both."));
+                       g_object_unref (xml);
+                       return FALSE;
+               }
+
+               e_xml_document_start_element (xml, NULL, "ace");
+
+               if ((ace->flags & E_WEBDAV_ACE_FLAG_INVERT) != 0)
+                       e_xml_document_start_element (xml, NULL, "invert");
+
+               e_xml_document_start_element (xml, NULL, "principal");
+               switch (ace->principal_kind) {
+               case E_WEBDAV_ACE_PRINCIPAL_UNKNOWN:
+                       g_warn_if_reached ();
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_HREF:
+                       e_xml_document_start_text_element (xml, NULL, "href");
+                       e_xml_document_write_string (xml, ace->principal_href);
+                       e_xml_document_end_element (xml);
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_ALL:
+                       e_xml_document_add_empty_element (xml, NULL, "all");
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_AUTHENTICATED:
+                       e_xml_document_add_empty_element (xml, NULL, "authenticated");
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_UNAUTHENTICATED:
+                       e_xml_document_add_empty_element (xml, NULL, "unauthenticated");
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_PROPERTY:
+                       g_warn_if_reached ();
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_SELF:
+                       e_xml_document_add_empty_element (xml, NULL, "self");
+                       break;
+               case E_WEBDAV_ACE_PRINCIPAL_OWNER:
+                       e_xml_document_start_element (xml, NULL, "property");
+                       e_xml_document_add_empty_element (xml, NULL, "owner");
+                       e_xml_document_end_element (xml);
+                       break;
+
+               }
+               e_xml_document_end_element (xml); /* principal */
+
+               if ((ace->flags & E_WEBDAV_ACE_FLAG_INVERT) != 0)
+                       e_xml_document_end_element (xml); /* invert */
+
+               if ((ace->flags & E_WEBDAV_ACE_FLAG_GRANT) != 0)
+                       e_xml_document_start_element (xml, NULL, "grant");
+               else if ((ace->flags & E_WEBDAV_ACE_FLAG_DENY) != 0)
+                       e_xml_document_start_element (xml, NULL, "deny");
+               else
+                       g_warn_if_reached ();
+
+               for (plink = ace->privileges; plink; plink = g_slist_next (plink)) {
+                       EWebDAVPrivilege *privilege = plink->data;
+
+                       if (!privilege) {
+                               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                                       _("Access Control Entry privilege cannot be NULL."));
+                               g_object_unref (xml);
+                               return FALSE;
+                       }
+
+                       e_xml_document_start_element (xml, NULL, "privilege");
+                       e_xml_document_add_empty_element (xml, privilege->ns_uri, privilege->name);
+                       e_xml_document_end_element (xml); /* privilege */
+               }
+
+               e_xml_document_end_element (xml); /* grant or deny */
+
+               e_xml_document_end_element (xml); /* ace */
+       }
+
+       success = e_webdav_session_acl_sync (webdav, uri, xml, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success;
+}
+
+static gboolean
+e_webdav_session_principal_property_search_cb (EWebDAVSession *webdav,
+                                              xmlXPathContextPtr xpath_ctx,
+                                              const gchar *xpath_prop_prefix,
+                                              const SoupURI *request_uri,
+                                              const gchar *href,
+                                              guint status_code,
+                                              gpointer user_data)
+{
+       GSList **out_principals = user_data;
+
+       g_return_val_if_fail (out_principals != NULL, FALSE);
+
+       if (!xpath_prop_prefix) {
+       } else if (status_code == SOUP_STATUS_OK) {
+               EWebDAVResource *resource;
+               gchar *display_name;
+
+               display_name = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, 
"D:displayname", NULL);
+
+               resource = e_webdav_resource_new (
+                       E_WEBDAV_RESOURCE_KIND_PRINCIPAL,
+                       0, /* supports */
+                       href,
+                       NULL, /* etag */
+                       NULL, /* display_name */
+                       NULL, /* content_type */
+                       0, /* content_length */
+                       0, /* creation_date */
+                       0, /* last_modified */
+                       NULL, /* description */
+                       NULL); /* color */
+               resource->display_name = display_name;
+
+               *out_principals = g_slist_prepend (*out_principals, resource);
+       }
+
+       return TRUE;
+}
+
+/**
+ * e_webdav_session_principal_property_search_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @apply_to_principal_collection_set: whether to apply to principal-collection-set
+ * @match_ns_uri: (nullable): namespace URI of the property to search in, or %NULL for %E_WEBDAV_NS_DAV
+ * @match_property: name of the property to search in
+ * @match_value: a string value to search for
+ * @out_principals: (out) (transfer full) (element-type EWebDAVResource): return location for matching 
principals
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues a DAV:principal-property-search for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource. The DAV:principal-property-search
+ * performs a search for all principals whose properties contain character data
+ * that matches the search criteria @match_value in @match_property property
+ * of namespace @match_ns_uri.
+ *
+ * By default, the function searches all members (at any depth) of the collection
+ * identified by the @uri. If @apply_to_principal_collection_set is set to %TRUE,
+ * the search is applied instead to each collection returned by
+ * e_webdav_session_get_principal_collection_set_sync() for the @uri.
+ *
+ * The @out_principals is a #GSList of #EWebDAVResource, where the kind
+ * is set to %E_WEBDAV_RESOURCE_KIND_PRINCIPAL and only href with displayname
+ * are filled. All other members of #EWebDAVResource are not set.
+ *
+ * Free the returned @out_principals with
+ * g_slist_free_full (principals, e_webdav_resource_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded. Note it can report success also when no matching
+ *    principal had been found.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_principal_property_search_sync (EWebDAVSession *webdav,
+                                                const gchar *uri,
+                                                gboolean apply_to_principal_collection_set,
+                                                const gchar *match_ns_uri,
+                                                const gchar *match_property,
+                                                const gchar *match_value,
+                                                GSList **out_principals,
+                                                GCancellable *cancellable,
+                                                GError **error)
+{
+       EXmlDocument *xml;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (match_property != NULL, FALSE);
+       g_return_val_if_fail (match_value != NULL, FALSE);
+       g_return_val_if_fail (out_principals != NULL, FALSE);
+
+       *out_principals = NULL;
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "principal-property-search");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       if (apply_to_principal_collection_set) {
+               e_xml_document_add_empty_element (xml, NULL, "apply-to-principal-collection-set");
+       }
+
+       e_xml_document_start_element (xml, NULL, "property-search");
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_add_empty_element (xml, match_ns_uri, match_property);
+       e_xml_document_end_element (xml); /* prop */
+       e_xml_document_start_text_element (xml, NULL, "match");
+       e_xml_document_write_string (xml, match_value);
+       e_xml_document_end_element (xml); /* match */
+       e_xml_document_end_element (xml); /* property-search */
+
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_add_empty_element (xml, NULL, "displayname");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_report_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               e_webdav_session_principal_property_search_cb, out_principals, NULL, NULL, cancellable, 
error);
+
+       g_object_unref (xml);
+
+       if (success)
+               *out_principals = g_slist_reverse (*out_principals);
+
+       return success;
+}
+
+/**
+ * e_webdav_session_util_maybe_dequote:
+ * @text: (inout) (nullable): text to dequote
+ *
+ * Dequotes @text, if it's enclosed in double-quotes. The function
+ * changes @text, it doesn't allocate new string. The function does
+ * nothing when the @text is not enclosed in double-quotes.
+ *
+ * Returns: possibly dequoted @text
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_webdav_session_util_maybe_dequote (gchar *text)
+{
+       gint len;
+
+       if (!text || *text != '\"')
+               return text;
+
+       len = strlen (text);
+
+       if (len < 2 || text[len - 1] != '\"')
+               return text;
+
+       memmove (text, text + 1, len - 2);
+       text[len - 2] = '\0';
+
+       return text;
+}
+
+static gboolean
+e_webdav_session_free_in_traverse_cb (GNode *node,
+                                     gpointer user_data)
+{
+       if (node) {
+               e_webdav_privilege_free (node->data);
+               node->data = NULL;
+       }
+
+       return FALSE;
+}
+
+/**
+ * e_webdav_session_util_free_privileges:
+ * @privileges: (nullable): a tree of #EWebDAVPrivilege structures
+ *
+ * Frees @privileges returned by e_webdav_session_get_supported_privilege_set_sync().
+ * The function does nothing, if @privileges is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_session_util_free_privileges (GNode *privileges)
+{
+       if (!privileges)
+               return;
+
+       g_node_traverse (privileges, G_PRE_ORDER, G_TRAVERSE_ALL, -1, e_webdav_session_free_in_traverse_cb, 
NULL);
+       g_node_destroy (privileges);
+}
diff --git a/src/libedataserver/e-webdav-session.h b/src/libedataserver/e-webdav-session.h
new file mode 100644
index 0000000..8d15080
--- /dev/null
+++ b/src/libedataserver/e-webdav-session.h
@@ -0,0 +1,582 @@
+/* -*- 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_WEBDAV_SESSION_H
+#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 \
+       (e_webdav_session_get_type ())
+#define E_WEBDAV_SESSION(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_WEBDAV_SESSION, EWebDAVSession))
+#define E_WEBDAV_SESSION_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_WEBDAV_SESSION, EWebDAVSessionClass))
+#define E_IS_WEBDAV_SESSION(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_WEBDAV_SESSION))
+#define E_IS_WEBDAV_SESSION_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_WEBDAV_SESSION))
+#define E_WEBDAV_SESSION_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_WEBDAV_SESSION, EWebDAVSessionClass))
+
+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_DEPTH_THIS                    "0"
+#define E_WEBDAV_DEPTH_THIS_AND_CHILDREN       "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 struct _EWebDAVSession EWebDAVSession;
+typedef struct _EWebDAVSessionClass EWebDAVSessionClass;
+typedef struct _EWebDAVSessionPrivate EWebDAVSessionPrivate;
+
+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,
+       E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY     = 1 << 4,
+       E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE     = 1 << 5
+} 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 *src);
+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;
+
+/**
+ * EWebDAVPropstatTraverseFunc:
+ * @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
+ * @href: (nullable): a full URI to which the property belongs, or %NULL, when not found
+ * @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(),
+ * e_webdav_session_report_sync() and other XML response with DAV:propstat
+ * elements traversal functions.
+ *
+ * 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 response, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* EWebDAVPropstatTraverseFunc)       (EWebDAVSession *webdav,
+                                                        xmlXPathContextPtr xpath_ctx,
+                                                        const gchar *xpath_prop_prefix,
+                                                        const SoupURI *request_uri,
+                                                        const gchar *href,
+                                                        guint status_code,
+                                                        gpointer user_data);
+
+typedef enum {
+       E_WEBDAV_PROPERTY_SET,
+       E_WEBDAV_PROPERTY_REMOVE
+} EWebDAVPropertyChangeKind;
+
+typedef struct _EWebDAVPropertyChange {
+       EWebDAVPropertyChangeKind kind;
+       gchar *ns_uri;
+       gchar *name;
+       gchar *value;
+} EWebDAVPropertyChange;
+
+GType          e_webdav_property_change_get_type       (void) G_GNUC_CONST;
+EWebDAVPropertyChange *
+               e_webdav_property_change_new_set        (const gchar *ns_uri,
+                                                        const gchar *name,
+                                                        const gchar *value);
+EWebDAVPropertyChange *
+               e_webdav_property_change_new_remove     (const gchar *ns_uri,
+                                                        const gchar *name);
+EWebDAVPropertyChange *
+               e_webdav_property_change_copy           (const EWebDAVPropertyChange *src);
+void           e_webdav_property_change_free           (gpointer ptr); /* EWebDAVPropertyChange * */
+
+typedef enum {
+       E_WEBDAV_LOCK_EXCLUSIVE,
+       E_WEBDAV_LOCK_SHARED
+} EWebDAVLockScope;
+
+#define E_WEBDAV_COLLATION_ASCII_NUMERIC_SUFFIX "ascii-numeric"
+#define E_WEBDAV_COLLATION_ASCII_NUMERIC "i;" E_WEBDAV_COLLATION_ASCII_NUMERIC_SUFFIX
+
+#define E_WEBDAV_COLLATION_ASCII_CASEMAP_SUFFIX "ascii-casemap"
+#define E_WEBDAV_COLLATION_ASCII_CASEMAP "i;" E_WEBDAV_COLLATION_ASCII_CASEMAP_SUFFIX
+
+#define E_WEBDAV_COLLATION_OCTET_SUFFIX "octet"
+#define E_WEBDAV_COLLATION_OCTET "i;" E_WEBDAV_COLLATION_OCTET_SUFFIX
+
+#define E_WEBDAV_COLLATION_UNICODE_CASEMAP_SUFFIX "unicode-casemap"
+#define E_WEBDAV_COLLATION_UNICODE_CASEMAP "i;" E_WEBDAV_COLLATION_UNICODE_CASEMAP_SUFFIX
+
+typedef enum {
+       E_WEBDAV_PRIVILEGE_KIND_UNKNOWN = 0,
+       E_WEBDAV_PRIVILEGE_KIND_ABSTRACT,
+       E_WEBDAV_PRIVILEGE_KIND_AGGREGATE,
+       E_WEBDAV_PRIVILEGE_KIND_COMMON
+} EWebDAVPrivilegeKind;
+
+typedef enum {
+       E_WEBDAV_PRIVILEGE_HINT_UNKNOWN = 0,
+       E_WEBDAV_PRIVILEGE_HINT_READ,
+       E_WEBDAV_PRIVILEGE_HINT_WRITE,
+       E_WEBDAV_PRIVILEGE_HINT_WRITE_PROPERTIES,
+       E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT,
+       E_WEBDAV_PRIVILEGE_HINT_UNLOCK,
+       E_WEBDAV_PRIVILEGE_HINT_READ_ACL,
+       E_WEBDAV_PRIVILEGE_HINT_WRITE_ACL,
+       E_WEBDAV_PRIVILEGE_HINT_READ_CURRENT_USER_PRIVILEGE_SET,
+       E_WEBDAV_PRIVILEGE_HINT_BIND,
+       E_WEBDAV_PRIVILEGE_HINT_UNBIND,
+       E_WEBDAV_PRIVILEGE_HINT_ALL,
+       E_WEBDAV_PRIVILEGE_HINT_CALDAV_READ_FREE_BUSY
+} EWebDAVPrivilegeHint;
+
+typedef struct _EWebDAVPrivilege {
+       gchar *ns_uri;
+       gchar *name;
+       gchar *description;
+       EWebDAVPrivilegeKind kind;
+       EWebDAVPrivilegeHint hint;
+} EWebDAVPrivilege;
+
+GType          e_webdav_privilege_get_type             (void) G_GNUC_CONST;
+EWebDAVPrivilege *
+               e_webdav_privilege_new                  (const gchar *ns_uri,
+                                                        const gchar *name,
+                                                        const gchar *description,
+                                                        EWebDAVPrivilegeKind kind,
+                                                        EWebDAVPrivilegeHint hint);
+EWebDAVPrivilege *
+               e_webdav_privilege_copy                 (const EWebDAVPrivilege *src);
+void           e_webdav_privilege_free                 (gpointer ptr); /* EWebDAVPrivilege * */
+
+typedef enum {
+       E_WEBDAV_ACE_PRINCIPAL_UNKNOWN = 0,
+       E_WEBDAV_ACE_PRINCIPAL_HREF,
+       E_WEBDAV_ACE_PRINCIPAL_ALL,
+       E_WEBDAV_ACE_PRINCIPAL_AUTHENTICATED,
+       E_WEBDAV_ACE_PRINCIPAL_UNAUTHENTICATED,
+       E_WEBDAV_ACE_PRINCIPAL_PROPERTY,
+       E_WEBDAV_ACE_PRINCIPAL_SELF,
+       E_WEBDAV_ACE_PRINCIPAL_OWNER /* special-case, 'property' with only 'DAV:owner' child */
+} EWebDAVACEPrincipalKind;
+
+typedef enum {
+       E_WEBDAV_ACE_FLAG_UNKNOWN       = 0,
+       E_WEBDAV_ACE_FLAG_GRANT         = 1 << 0,
+       E_WEBDAV_ACE_FLAG_DENY          = 1 << 1,
+       E_WEBDAV_ACE_FLAG_INVERT        = 1 << 2,
+       E_WEBDAV_ACE_FLAG_PROTECTED     = 1 << 3,
+       E_WEBDAV_ACE_FLAG_INHERITED     = 1 << 4
+} EWebDAVACEFlag;
+
+typedef struct _EWebDAVAccessControlEntry {
+       EWebDAVACEPrincipalKind principal_kind;
+       gchar *principal_href; /* valid onyl if principal_kind is E_WEBDAV_ACE_PRINCIPAL_HREF */
+       guint32 flags; /* bit-or of EWebDAVACEFlag */
+       gchar *inherited_href; /* valid only if flags contain E_WEBDAV_ACE_INHERITED */
+       GSList *privileges; /* EWebDAVPrivilege * */
+} EWebDAVAccessControlEntry;
+
+GType          e_webdav_access_control_entry_get_type  (void) G_GNUC_CONST;
+EWebDAVAccessControlEntry *
+               e_webdav_access_control_entry_new       (EWebDAVACEPrincipalKind principal_kind,
+                                                        const gchar *principal_href,
+                                                        guint32 flags, /* bit-or of EWebDAVACEFlag */
+                                                        const gchar *inherited_href);
+EWebDAVAccessControlEntry *
+               e_webdav_access_control_entry_copy      (const EWebDAVAccessControlEntry *src);
+void           e_webdav_access_control_entry_free      (gpointer ptr); /* EWebDAVAccessControlEntry * */
+void           e_webdav_access_control_entry_append_privilege
+                                                       (EWebDAVAccessControlEntry *ace,
+                                                        EWebDAVPrivilege *privilege);
+GSList *       e_webdav_access_control_entry_get_privileges
+                                                       (EWebDAVAccessControlEntry *ace); /* EWebDAVPrivilege 
* */
+
+typedef enum {
+       E_WEBDAV_ACL_RESTRICTION_NONE                   = 0,
+       E_WEBDAV_ACL_RESTRICTION_GRANT_ONLY             = 1 << 0,
+       E_WEBDAV_ACL_RESTRICTION_NO_INVERT              = 1 << 1,
+       E_WEBDAV_ACL_RESTRICTION_DENY_BEFORE_GRANT      = 1 << 2,
+       E_WEBDAV_ACL_RESTRICTION_REQUIRED_PRINCIPAL     = 1 << 3
+} EWebDAVACLRestrictions;
+
+/**
+ * EWebDAVSession:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EWebDAVSession {
+       /*< private >*/
+       ESoupSession parent;
+       EWebDAVSessionPrivate *priv;
+};
+
+struct _EWebDAVSessionClass {
+       ESoupSessionClass parent_class;
+
+       /* Padding for future expansion */
+       gpointer reserved[10];
+};
+
+GType          e_webdav_session_get_type               (void) G_GNUC_CONST;
+
+EWebDAVSession *e_webdav_session_new                   (ESource *source);
+SoupRequestHTTP *
+               e_webdav_session_new_request            (EWebDAVSession *webdav,
+                                                        const gchar *method,
+                                                        const gchar *uri,
+                                                        GError **error);
+gboolean       e_webdav_session_replace_with_detailed_error
+                                                       (EWebDAVSession *webdav,
+                                                        SoupRequestHTTP *request,
+                                                        const GByteArray *response_data,
+                                                        gboolean ignore_multistatus,
+                                                        const gchar *prefix,
+                                                        GError **inout_error);
+gchar *                e_webdav_session_ensure_full_uri        (EWebDAVSession *webdav,
+                                                        const SoupURI *request_uri,
+                                                        const gchar *href);
+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_post_sync              (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *data,
+                                                        gsize data_length,
+                                                        gchar **out_content_type,
+                                                        GByteArray **out_content,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_propfind_sync          (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *depth,
+                                                        const EXmlDocument *xml,
+                                                        EWebDAVPropstatTraverseFunc 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_report_sync            (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *depth,
+                                                        const EXmlDocument *xml,
+                                                        EWebDAVPropstatTraverseFunc func,
+                                                        gpointer func_user_data,
+                                                        gchar **out_content_type,
+                                                        GByteArray **out_content,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_mkcol_sync             (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_mkcol_addressbook_sync (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *display_name,
+                                                        const gchar *description,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_mkcalendar_sync        (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *display_name,
+                                                        const gchar *description,
+                                                        const gchar *color,
+                                                        guint32 supports, /* bit-or of 
EWebDAVResourceSupports */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_sync               (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        gchar **out_href,
+                                                        gchar **out_etag,
+                                                        GOutputStream *out_stream,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_data_sync          (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        gchar **out_href,
+                                                        gchar **out_etag,
+                                                        gchar **out_bytes,
+                                                        gsize *out_length,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_put_sync               (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *etag,
+                                                        const gchar *content_type,
+                                                        GInputStream *stream,
+                                                        gchar **out_href,
+                                                        gchar **out_etag,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_put_data_sync          (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *etag,
+                                                        const gchar *content_type,
+                                                        const gchar *bytes,
+                                                        gsize length,
+                                                        gchar **out_href,
+                                                        gchar **out_etag,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_delete_sync            (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *depth,
+                                                        const gchar *etag,
+                                                        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,
+                                                        gint32 lock_timeout,
+                                                        const EXmlDocument *xml,
+                                                        gchar **out_lock_token,
+                                                        xmlDocPtr *out_xml_response,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_refresh_lock_sync      (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const gchar *lock_token,
+                                                        gint32 lock_timeout,
+                                                        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_traverse_multistatus_response
+                                                       (EWebDAVSession *webdav,
+                                                        const SoupMessage *message,
+                                                        const GByteArray *xml_data,
+                                                        EWebDAVPropstatTraverseFunc func,
+                                                        gpointer func_user_data,
+                                                        GError **error);
+gboolean       e_webdav_session_traverse_mkcol_response
+                                                       (EWebDAVSession *webdav,
+                                                        const SoupMessage *message,
+                                                        const GByteArray *xml_data,
+                                                        EWebDAVPropstatTraverseFunc func,
+                                                        gpointer func_user_data,
+                                                        GError **error);
+gboolean       e_webdav_session_traverse_mkcalendar_response
+                                                       (EWebDAVSession *webdav,
+                                                        const SoupMessage *message,
+                                                        const GByteArray *xml_data,
+                                                        EWebDAVPropstatTraverseFunc func,
+                                                        gpointer func_user_data,
+                                                        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,
+                                                        const gchar *depth,
+                                                        guint32 flags, /* bit-or of EWebDAVListFlags */
+                                                        GSList **out_resources, /* EWebDAVResource * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_update_properties_sync (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const GSList *changes, /* EWebDAVPropertyChange * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_lock_resource_sync     (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        EWebDAVLockScope lock_scope,
+                                                        gint32 lock_timeout,
+                                                        const gchar *owner,
+                                                        gchar **out_lock_token,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_acl_sync               (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const EXmlDocument *xml,
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_supported_privilege_set_sync
+                                                       (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GNode **out_privileges, /* EWebDAVPrivilege * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_current_user_privilege_set_sync
+                                                       (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GSList **out_privileges, /* EWebDAVPrivilege * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_acl_sync           (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GSList **out_entries, /* EWebDAVAccessControlEntry * 
*/
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_acl_restrictions_sync
+                                                       (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        guint32 *out_restrictions, /* bit-or of 
EWebDAVACLRestrictions */
+                                                        EWebDAVACEPrincipalKind *out_principal_kind,
+                                                        GSList **out_principal_hrefs, /* gchar * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_get_principal_collection_set_sync
+                                                       (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        GSList **out_principal_hrefs, /* gchar * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_set_acl_sync           (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        const GSList *entries, /* EWebDAVAccessControlEntry 
* */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gboolean       e_webdav_session_principal_property_search_sync
+                                                       (EWebDAVSession *webdav,
+                                                        const gchar *uri,
+                                                        gboolean apply_to_principal_collection_set,
+                                                        const gchar *match_ns_uri,
+                                                        const gchar *match_property,
+                                                        const gchar *match_value,
+                                                        GSList **out_principals, /* EWebDAVResource * */
+                                                        GCancellable *cancellable,
+                                                        GError **error);
+gchar *                e_webdav_session_util_maybe_dequote     (gchar *text);
+void           e_webdav_session_util_free_privileges   (GNode *privileges); /* EWebDAVPrivilege * */
+
+G_END_DECLS
+
+#endif /* E_WEBDAV_SESSION_H */
diff --git a/src/libedataserver/e-xml-document.c b/src/libedataserver/e-xml-document.c
new file mode 100644
index 0000000..c195699
--- /dev/null
+++ b/src/libedataserver/e-xml-document.c
@@ -0,0 +1,727 @@
+/* -*- 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_empty_element:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new element, or %NULL
+ * @name: name of the new element
+ *
+ * Adds an empty element, which is an element with no attribute and no value.
+ *
+ * It's the same as calling e_xml_document_start_element() immediately
+ * followed by e_xml_document_end_element().
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_empty_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);
+
+       e_xml_document_start_element (xml, ns_href, name);
+       e_xml_document_end_element (xml);
+}
+
+/**
+ * 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_add_attribute_int:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: integer value of the attribute
+ *
+ * Adds a new attribute with an integer value 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_int (EXmlDocument *xml,
+                                 const gchar *ns_href,
+                                 const gchar *name,
+                                 gint64 value)
+{
+       gchar *strvalue;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (name != NULL);
+
+       strvalue = g_strdup_printf ("%" G_GINT64_FORMAT, value);
+       e_xml_document_add_attribute (xml, ns_href, name, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * e_xml_document_add_attribute_double:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: double value of the attribute
+ *
+ * Adds a new attribute with a double value 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_double (EXmlDocument *xml,
+                                    const gchar *ns_href,
+                                    const gchar *name,
+                                    gdouble value)
+{
+       gchar *strvalue;
+
+       g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+       g_return_if_fail (xml->priv->current_element != NULL);
+       g_return_if_fail (name != NULL);
+
+       strvalue = g_strdup_printf ("%f", value);
+       e_xml_document_add_attribute (xml, ns_href, name, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * e_xml_document_add_attribute_time:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: time_t value of the attribute
+ *
+ * Adds a new attribute with a time_t value in ISO 8601 format to the current element.
+ * The format is "YYYY-MM-DDTHH:MM:SSZ".
+ * 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_time (EXmlDocument *xml,
+                                  const gchar *ns_href,
+                                  const gchar *name,
+                                  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);
+       g_return_if_fail (name != NULL);
+
+       tv.tv_usec = 0;
+       tv.tv_sec = value;
+
+       strvalue = g_time_val_to_iso8601 (&tv);
+       e_xml_document_add_attribute (xml, ns_href, name, strvalue);
+       g_free (strvalue);
+}
+
+/**
+ * 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.
+ * The format is "YYYY-MM-DDTHH:MM:SSZ".
+ *
+ * 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..e1fcf94
--- /dev/null
+++ b/src/libedataserver/e-xml-document.h
@@ -0,0 +1,134 @@
+/* -*- 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_empty_element
+                                               (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name);
+void           e_xml_document_add_attribute    (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name,
+                                                const gchar *value);
+void           e_xml_document_add_attribute_int
+                                               (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name,
+                                                gint64 value);
+void           e_xml_document_add_attribute_double
+                                               (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name,
+                                                gdouble value);
+void           e_xml_document_add_attribute_time
+                                               (EXmlDocument *xml,
+                                                const gchar *ns_href,
+                                                const gchar *name,
+                                                time_t 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..4b7a267 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 (gconstpointer 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..5222792 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                (gconstpointer 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 47b6304..b11f8f8 100644
--- a/src/libedataserver/libedataserver.h
+++ b/src/libedataserver/libedataserver.h
@@ -43,6 +43,7 @@
 #include <libedataserver/e-secret-store.h>
 #include <libedataserver/e-sexp.h>
 #include <libedataserver/e-soup-auth-bearer.h>
+#include <libedataserver/e-soup-session.h>
 #include <libedataserver/e-soup-ssl-trust.h>
 #include <libedataserver/e-source-address-book.h>
 #include <libedataserver/e-source-alarms.h>
@@ -91,6 +92,8 @@
 #include <libedataserver/e-uid.h>
 #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>
diff --git a/tests/libebook/client/test-book-client-view-operations.c 
b/tests/libebook/client/test-book-client-view-operations.c
index e126750..7ad3db4 100644
--- a/tests/libebook/client/test-book-client-view-operations.c
+++ b/tests/libebook/client/test-book-client-view-operations.c
@@ -31,6 +31,7 @@ static ETestServerClosure book_closure_direct_async = { E_TEST_SERVER_DIRECT_ADD
 #define N_CONTACTS 5
 
 typedef struct {
+       ESourceRegistry *registry;
        ETestServerClosure *closure;
        GThread         *thread;
        const gchar     *book_uid;
@@ -179,7 +180,6 @@ static gpointer
 test_view_thread_async (ThreadData *data)
 {
        GMainContext    *context;
-       ESourceRegistry *registry;
        ESource         *source;
        GError          *error = NULL;
 
@@ -188,18 +188,14 @@ test_view_thread_async (ThreadData *data)
        g_main_context_push_thread_default (context);
 
        /* Open the test book client in this thread */
-       registry = e_source_registry_new_sync (NULL, &error);
-       if (!registry)
-               g_error ("Unable to create the registry: %s", error->message);
-
-       source = e_source_registry_ref_source (registry, data->book_uid);
+       source = e_source_registry_ref_source (data->registry, data->book_uid);
        if (!source)
                g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
 
        if (data->closure->type == E_TEST_SERVER_DIRECT_ADDRESS_BOOK) {
                /* There is no Async API to open a direct book for now, let's stick with the sync API
                 */
-               data->client = (EBookClient *) e_book_client_connect_direct_sync (registry, source, (guint32) 
-1, NULL, &error);
+               data->client = (EBookClient *) e_book_client_connect_direct_sync (data->registry, source, 
(guint32) -1, NULL, &error);
 
                if (!data->client)
                        g_error ("Unable to create EBookClient for uid '%s': %s", data->book_uid, 
error->message);
@@ -215,7 +211,6 @@ test_view_thread_async (ThreadData *data)
        g_main_loop_run (data->loop);
 
        g_object_unref (source);
-       g_object_unref (registry);
 
        g_object_unref (data->client);
        g_main_context_pop_thread_default (context);
@@ -263,7 +258,6 @@ static gpointer
 test_view_thread_sync (ThreadData *data)
 {
        GMainContext    *context;
-       ESourceRegistry *registry;
        ESource         *source;
        GError          *error = NULL;
 
@@ -272,16 +266,12 @@ test_view_thread_sync (ThreadData *data)
        g_main_context_push_thread_default (context);
 
        /* Open the test book client in this thread */
-       registry = e_source_registry_new_sync (NULL, &error);
-       if (!registry)
-               g_error ("Unable to create the registry: %s", error->message);
-
-       source = e_source_registry_ref_source (registry, data->book_uid);
+       source = e_source_registry_ref_source (data->registry, data->book_uid);
        if (!source)
                g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
 
        if (data->closure->type == E_TEST_SERVER_DIRECT_ADDRESS_BOOK)
-               data->client = (EBookClient *) e_book_client_connect_direct_sync (registry, source, (guint32) 
-1, NULL, &error);
+               data->client = (EBookClient *) e_book_client_connect_direct_sync (data->registry, source, 
(guint32) -1, NULL, &error);
        else
                data->client = (EBookClient *) e_book_client_connect_sync (source, (guint32) -1, NULL, 
&error);
 
@@ -293,7 +283,6 @@ test_view_thread_sync (ThreadData *data)
        g_main_loop_run (data->loop);
 
        g_object_unref (source);
-       g_object_unref (registry);
 
        g_object_unref (data->client);
        g_main_context_pop_thread_default (context);
@@ -305,12 +294,16 @@ test_view_thread_sync (ThreadData *data)
 
 static ThreadData *
 create_test_thread (const gchar *book_uid,
+                   ESourceRegistry *registry,
                     gconstpointer user_data,
                     gboolean sync)
 {
        ThreadData  *data = g_slice_new0 (ThreadData);
 
+       g_assert_nonnull (registry);
+
        data->book_uid = book_uid;
+       data->registry = registry;
        data->closure = (ETestServerClosure *) user_data;
 
        g_mutex_init (&data->complete_mutex);
@@ -352,7 +345,7 @@ test_concurrent_views (ETestServerFixture *fixture,
        /* Create all concurrent threads accessing the same addressbook */
        tests = g_new0 (ThreadData *, N_THREADS);
        for (i = 0; i < N_THREADS; i++)
-               tests[i] = create_test_thread (book_uid, user_data, sync);
+               tests[i] = create_test_thread (book_uid, fixture->registry, user_data, sync);
 
        /* Wait for all threads to receive the complete signal */
        for (i = 0; i < N_THREADS; i++) {
diff --git a/tests/libebook/client/test-book-client-write-write.c 
b/tests/libebook/client/test-book-client-write-write.c
index ee888e3..00e6cae 100644
--- a/tests/libebook/client/test-book-client-write-write.c
+++ b/tests/libebook/client/test-book-client-write-write.c
@@ -45,6 +45,7 @@ typedef struct {
 } TestData;
 
 typedef struct {
+       ESourceRegistry *registry;
        GThread       *thread;
        const gchar   *book_uid;
        const gchar   *contact_uid;
@@ -182,7 +183,6 @@ static gpointer
 test_write_thread (ThreadData *data)
 {
        GMainContext    *context;
-       ESourceRegistry *registry;
        GSource         *gsource;
        ESource         *source;
        GError          *error = NULL;
@@ -192,11 +192,7 @@ test_write_thread (ThreadData *data)
        g_main_context_push_thread_default (context);
 
        /* Open the test book client in this thread */
-       registry = e_source_registry_new_sync (NULL, &error);
-       if (!registry)
-               g_error ("Unable to create the registry: %s", error->message);
-
-       source = e_source_registry_ref_source (registry, data->book_uid);
+       source = e_source_registry_ref_source (data->registry, data->book_uid);
        if (!source)
                g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
 
@@ -212,7 +208,6 @@ test_write_thread (ThreadData *data)
        g_main_loop_run (data->loop);
 
        g_object_unref (source);
-       g_object_unref (registry);
 
        g_object_unref (data->client);
        g_main_context_pop_thread_default (context);
@@ -223,7 +218,8 @@ test_write_thread (ThreadData *data)
 }
 
 static ThreadData *
-create_test_thread (const gchar *book_uid,
+create_test_thread (ESourceRegistry *registry,
+                   const gchar *book_uid,
                     const gchar *contact_uid,
                     EContactField field,
                     const gchar *value)
@@ -231,6 +227,9 @@ create_test_thread (const gchar *book_uid,
        ThreadData  *data = g_slice_new0 (ThreadData);
        const gchar *name = e_contact_field_name (field);
 
+       g_assert_nonnull (registry);
+
+       data->registry = registry;
        data->book_uid = book_uid;
        data->contact_uid = contact_uid;
        data->field = field;
@@ -276,6 +275,7 @@ test_concurrent_writes (ETestServerFixture *fixture,
        tests = g_new0 (ThreadData *, G_N_ELEMENTS (field_tests));
        for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
                tests[i] = create_test_thread (
+                       fixture->registry,
                        book_uid, contact_uid,
                        field_tests[i].field,
                        field_tests[i].value);
diff --git a/tests/libebook/data/vcards/.gitattributes b/tests/libebook/data/vcards/.gitattributes
new file mode 100644
index 0000000..e668d08
--- /dev/null
+++ b/tests/libebook/data/vcards/.gitattributes
@@ -0,0 +1 @@
+*.vcf eol=crlf
diff --git a/tests/libebook/data/vcards/custom-1.vcf b/tests/libebook/data/vcards/custom-1.vcf
index ae8dcfe..ea3aaef 100644
--- a/tests/libebook/data/vcards/custom-1.vcf
+++ b/tests/libebook/data/vcards/custom-1.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-1
+REV:0
 FN:Micheal Jackson
 TEL;HOME:+1-221-5423789
 EMAIL;TYPE=home,work:micheal jackson com
diff --git a/tests/libebook/data/vcards/custom-2.vcf b/tests/libebook/data/vcards/custom-2.vcf
index c7a1f50..50b208b 100644
--- a/tests/libebook/data/vcards/custom-2.vcf
+++ b/tests/libebook/data/vcards/custom-2.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-2
+REV:0
 FN:Janet Jackson
 N:Janet
 TEL;HOME:7654321
diff --git a/tests/libebook/data/vcards/custom-3.vcf b/tests/libebook/data/vcards/custom-3.vcf
index c7fb251..cd8a26d 100644
--- a/tests/libebook/data/vcards/custom-3.vcf
+++ b/tests/libebook/data/vcards/custom-3.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-3
+REV:0
 FN:Bobby Brown
 TEL;HOME:+9999999
 EMAIL;TYPE=work:bobby brown org
diff --git a/tests/libebook/data/vcards/custom-4.vcf b/tests/libebook/data/vcards/custom-4.vcf
index 77e2990..3f1b3b4 100644
--- a/tests/libebook/data/vcards/custom-4.vcf
+++ b/tests/libebook/data/vcards/custom-4.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:Custom-4
+REV:0
 FN:Big Bobby Brown
 TEL;TYPE=work,pref:+9999999
 EMAIL:big bobby brown org
diff --git a/tests/libebook/data/vcards/custom-5.vcf b/tests/libebook/data/vcards/custom-5.vcf
index 9ade0a4..c28bae9 100644
--- a/tests/libebook/data/vcards/custom-5.vcf
+++ b/tests/libebook/data/vcards/custom-5.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-5
+REV:0
 FN:James Brown
 TEL;HOME:+6666666
 EMAIL;TYPE=home,work:james brown com
diff --git a/tests/libebook/data/vcards/custom-6.vcf b/tests/libebook/data/vcards/custom-6.vcf
index a43da5e..cba585d 100644
--- a/tests/libebook/data/vcards/custom-6.vcf
+++ b/tests/libebook/data/vcards/custom-6.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-6
+REV:0
 TEL;HOME:ask Jenny for Lisa's number
 FN:%Stran_ge Name
 END:VCARD
diff --git a/tests/libebook/data/vcards/custom-7.vcf b/tests/libebook/data/vcards/custom-7.vcf
index dae210a..66ff9cb 100644
--- a/tests/libebook/data/vcards/custom-7.vcf
+++ b/tests/libebook/data/vcards/custom-7.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-7
+REV:0
 FN:Purple Goose
 TEL;HOME:+49-89-7888 99
 KEY;ENCODING=b;TYPE=X509:AA==
diff --git a/tests/libebook/data/vcards/custom-8.vcf b/tests/libebook/data/vcards/custom-8.vcf
index 5306ad0..462a3b8 100644
--- a/tests/libebook/data/vcards/custom-8.vcf
+++ b/tests/libebook/data/vcards/custom-8.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-8
+REV:0
 FN:Purple Pony
 TEL;HOME:+31-221-5423789
 EMAIL;TYPE=home,work:purple pony com
diff --git a/tests/libebook/data/vcards/custom-9.vcf b/tests/libebook/data/vcards/custom-9.vcf
index c7a57dd..f07e9f3 100644
--- a/tests/libebook/data/vcards/custom-9.vcf
+++ b/tests/libebook/data/vcards/custom-9.vcf
@@ -1,5 +1,6 @@
 BEGIN:VCARD
 UID:custom-9
+REV:0
 FN:Pink Pony
 TEL;HOME:514-845-8436
 EMAIL;TYPE=home,work:pink pony com
diff --git a/tests/libebook/data/vcards/logo-1.vcf b/tests/libebook/data/vcards/logo-1.vcf
new file mode 100644
index 0000000..ad1de8c
--- /dev/null
+++ b/tests/libebook/data/vcards/logo-1.vcf
@@ -0,0 +1,21 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:logo-1
+FN:logo
+N:;logo;;;
+LOGO;TYPE="X-EVOLUTION-UNKNOWN";ENCODING=b:/9j/4AAQSkZJRgABAQEASABIAAD/2wB
+ DABYPEBMQDhYTEhMYFxYaIDYjIB4eIEIvMic2TkVSUU1FTEpWYXxpVlx1XUpMbJNtdYCEi4yLV
+ GiZo5eHonyIi4b/2wBDARcYGCAcID8jIz+GWUxZhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa
+ GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhob/wgARCABAAEADAREAAhEBAxEB/8QAFgABAQEAAAAAA
+ AAAAAAAAAAAAgEE/8QAFwEBAQEBAAAAAAAAAAAAAAAAAAECBP/aAAwDAQACEAMQAAABXP2RDRD
+ ZAhs2Y2UlENkCGzZjZSUQ2QNFNmNlJRDZA0U2Y2UlENkDRTZjZSUQ2QNFNmNlJRDZA0U2Y2UlE
+ NkDRT//xAAUEAEAAAAAAAAAAAAAAAAAAABg/9oACAEBAAEFAgH/xAAUEQEAAAAAAAAAAAAAAAA
+ AAABg/9oACAEDAQE/AQH/xAAUEQEAAAAAAAAAAAAAAAAAAABg/9oACAECAQE/AQH/xAAUEAEAA
+ AAAAAAAAAAAAAAAAABg/9oACAEBAAY/AgH/xAAWEAADAAAAAAAAAAAAAAAAAAABIFD/2gAIAQE
+ AAT8hQ3P/2gAMAwEAAgADAAAAEIzqtZzqlZzqhZzqhZzqhZyqhZyqhZyqhf/EABQRAQAAAAAAA
+ AAAAAAAAAAAAGD/2gAIAQMBAT8QAf/EABQRAQAAAAAAAAAAAAAAAAAAAGD/2gAIAQIBAT8QAf/
+ EABYQAQEBAAAAAAAAAAAAAAAAAAEAEP/aAAgBAQABPxBmZwzMzMzMzMzMzMzMzMzMzMzMzMzMz
+ MzMzMzMzMzMzf/Z
+REV:2017-05-10T14:36:01Z(2)
+EMAIL;TYPE=WORK:logo@no.where
+END:VCARD
\ No newline at end of file
diff --git a/tests/libebook/data/vcards/photo-1.vcf b/tests/libebook/data/vcards/photo-1.vcf
new file mode 100644
index 0000000..4bf5bce
--- /dev/null
+++ b/tests/libebook/data/vcards/photo-1.vcf
@@ -0,0 +1,59 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:photo-1
+FN:photo
+N:;photo;;;
+PHOTO;TYPE="X-EVOLUTION-UNKNOWN";ENCODING=b:iVBORw0KGgoAAAANSUhEUgAAAEAAAAB
+ ACAIAAAAlC+aJAAAKo0lEQVRo3n1aW7LcuA4j6OzkLujufxOzgu5gPsQHIHdNqpKc03bbEh8gC
+ Ar//98/D74Pvonvk98nvw++mZ8Hf5/8PPk38XnwffLz4Jv5/ZN+Jz5P/n3wOR/q1cSnn/b9U8/
+ /PPlNfP9gb8j8ngU8+X3wyX7yfPd8+Of83DdnnlV9E0QQgQBi/oD9ExlxLiACCJCBiGAE++Pg+
+ TbqQl/a2xgR0Esg55HngYhAUBbB4DxmV7BPqwVn/Xb+zteBehUC8bdWBkYQCNbLIoI4//XiSMz
+ r58GYGyLifCH6+STQ1xjYrR+TEmGbYOz+z/6yHj7WYahtQ/fXNgXLGNRnIxgEOMvDOI+13vJWk
+ DHb6LftdiLo4YAIQmwwtg1E5ImLCJLol8i3O5DYNu+nHStD9kHUM6McgbaSuvdcIDo+y0VAhQh
+ PWHECkDxvqd3vV84KElznjmM3D6AxSIlsRuDYGxUVx3mMDmpaUJZLNLdCdkFW/EGiAbXQE6Zio
+ Fj/5URZXay9mmfbYBIx0L2KiWfFJ91IC7EKwVnZiRAy2CjSWwc8OCsMy7PcKM9KAEhoiek3lSG
+ JUTdhgoUWnpi4t0RiG4nrTfSdKAeIkzdj9clUXzKQtVeu1UKTNxpkCBSMKUYqbtCRsvOv4avAg
+ /rwY0oQDWaGZAh1IGaFjbwRAaZEz946KdObsMisxQJmc4a+b5If5gkAZ59sNEOA4IKSoCfVuI1
+ bgrokiBNCtpTGo4bLdYpiPDf2OqwNwvp1kigdRrLNBRDuvQx0kBiQMMqD8/AIJg54U6orFMIOW
+ LIdsxmDwXsEScYFWmO3jo628zG7xFi/nuI8qWDs2HS2UKmXt4Fm24dewFLP0ny5AjfQOkrDDSz
+ lg78WGcCWZAQpZAZbCaUEHITDoRKahUAHnsDT/AAtZwPVqNAk+33siregPw6rbayblo0gJt2x1
+ QNV5SvPuM5DRELsKbQmoOyBoNC6QGjBONawWC9KAa2ZBR77lCFn62OKgTUwFnhPvnFsilxmVyZ
+ WzgH2N6r0yOtjI4K1UsBf3TuD4L2gOql4Wp63r2vJlXji8A4yN+6JYLHlqXVotkPeUSt5j96Mw
+ igHdKlMQpGi6PkG1rhsaQPGV02XsLcdMidkdO6afK8bMZykw3FyYQy4HIQoWhlGnSFQHFJr4ra
+ 5ErCmEVQmJnmVtCiE1fKtu8PP1uvQzIzoZoSNkrxiWVhjvIHIAmfgunF/YWgCvCMugd4er36q6
+ xU9OwdFsdbjWKjQiHfxa8oq7UaRCzh+nMQocgHrCzQLhs/mMlvxHadBxMu/y6UWm9FbBSU8YSX
+ NP+bAsBpXgp4n9ZbKFxOFxSQjt4/Q9o3UfWnKWruLWgCNuQvIH2syMDi7YCjZ14lGy6piSuvKN
+ 4QgUjj49iXUMiCdlPZ6Bxl46uYw2WMJCGGvoIM1gnMHA1Z0ebcP2ntSuXDZKOubVqwHx+70gnU
+ 55eY1GCQEpeQrQa5yxZEigryaDRBT+PhqmSYS65n5RoZu3GkMY2QHWWcRXqh08ILFlV60EQhrD
+ KraFNO2vgD0duXud9KqjkQjHMW3/G0asOGyQIyWHW0iLwihzfysDp1PNJpqtHf7Jyom5A8HhLH
+ BFXesTbQi24HKqzYoK2Ros3koGOFGfjXaNLubKlB/U00P0SJwdfGjbGydOBx/u7u7366s53TUX
+ KJOCvcxCr4AdYL3LnHjobP7hCwmRFnb5rHKUmlLcEBSXY3anE0nwnBUG4AeelW/S2ZVUwbr3Kb
+ TlW0zkotaV9gsXGN8866mu6XJHhohP2DOoYdQ4MRymLiUvuG5LrJyfEsNIayZtlrhWEFVtFvnC
+ NWi4NR9qAyr0GFQdchnV0N4Q7HqmCmlWFF2gD5/cGMhTwo40KYcyypK6Vy9NUQsgxJQYDwi+Wa
+ CU8ksy3qmXeFUOk6nVjnQHAUTs1bDsOwi0JoFl4xOCDHurjuCKkzwJZuNeIANO4Eyo5cSUlilP
+ 5s4FisQ0JRkxEruA9kuPmy5RThv1458Uobq3rvvAqRGmEVQ2FHePmRuOxd6y6BUHl2Et4/a5u0
+ 8VbQA0djDSAQCP7pG9BtMvyMsFlbBHIlnyJwBMJ35wHg9hffuQokXaRFMkCIPEdRxF/qfastMS
+ jgyYb+7fs9f44Alq1aV9AX4pYEobYTmB0e1QrQ3J8cpxevYDBRTUgcDryb/eMCk4kt2iztkgZW
+ 2MLOYu2SKVhDaWc0HvwjfyXcKf+OoghfBmGVn4cpC74olnE5XpIELJOLdDR0h29qJQX5lKAwHf
+ 2tZVkwAhBh0970J3pXYRH2ZMYBcnApihljKPV99J0ftLNnWG9EefsKnRZQKKsMA5b3Cqcriicu
+ fq8qUFId7HAeFhbu/UDOw40JHDlt3DqptRkHUEZo1A9QhkY0GM+It5ZUg9xLjAF1HiM5milsnC
+ uwapFXoT8iLA8DZ+egiWMn5RBa6O8sLQd513QrhzTDm5WouazxbMgJFeNRyj5Wk2Vr4KIi4h2a
+ kRBuDkZgefhJFtLjl1+Vu/hCiAOoEk+9umss9IGIJJfZWOotp0wd1RLaCCD/TUqKQUHYZFKELW
+ 2tAG0bpFGPaAe5wcuunyBDOeVXnUuLHHmBX7fCMn/jI1+GCka+HBbb3Xx3fTQlc599WD/dkxbp
+ lyECO+6oTU7iUHRqv3SQmtf0cOcAHXJAKrrPSe3iN1eurMNHliLGIHm5gUVcOt+iB3rBuLt0ea
+ MlRly65wyBo5yry76uiSpJeYIBr2jRr6rI+LI+mstkAITrHuSINItWkwrRIF3HoLWmFBX523DJ
+ lHLpnBxWoOu01s2Go3GqaLm69vuR1xiuhwmVnrfFqBgqPW/Ecry1ZuPFMhne4DA5mai1v2UF5n
+ 6UdIcIWlJtx4QYhKh9WVD8kS8W/kempieRzfOzsddpF1kANPu6B9/TTCslhghMlaSm6skgXb53
+ Zj0hRsIsjLeI109IJcLxOUdySk0u/0HBVKRf8QWYQCZftBBD2sNMUkq79LTEi5KSR0kLuzLUBe
+ gVp29WxvjGpq6dB/BClx1RJ13tsPC0jX8xpC6JZ+ujnEPUesOGGcMOA5YhOFLz7xMzq7FAPtJ0
+ aVEj8x0xzM0mEUVxa4NieQlaOaansw0d8XZlvsk/pfHdM3+xYhYmK4QwpHMQ1IQPxRia8lFbYS
+ /V0AR02pKbInA2yGZ3LD/N9nYLZtnxOLV76qs0y5iiZTSzVlMD7gIRqUg2UOqPZFOB1tuoamM6
+ sWrjVQkcOBYdwPRklz0k8oUt2uM5Yj2iw0FnLSDNKoWDHdLRnJnAPXnWsGyILJKhjXZsSj+YLu
+ GANutANHxxiaixsbKnijoHVKINdS0b7NqFmRaEYnTSSkNr+FmT2iOT2KlCzN12b2QGwhzy53by
+ UAVxngsPneTwKt9kSdLKxM5OcirRMkxSpcQ/sanTDz8n5kUoYntGl2vuEoLahnWT06qi8jLe2m
+ gXrLVRMFzBdFuQMMW08JU4F34QdLa+QK0zjNYuZ7pl21nKAcPusPmRndDdBGwnBWS/u4wzYhME
+ ew9N6W3PGHcICYvwdQjbvoh2a3Z/QdZQ6zBy6hOkH5oSWjuOMM9mhSkpI86cUB2IVzAg5ZGVCE
+ F5nVDCI3L0ktGde0ZgyjP0XqSHx2GyNb9QAAAAASUVORK5CYII=
+EMAIL;TYPE=WORK:photo@no.where
+REV:2017-05-10T14:34:50Z(0)
+END:VCARD
diff --git a/tests/libebook/vcard/.gitattributes b/tests/libebook/vcard/.gitattributes
new file mode 100644
index 0000000..e668d08
--- /dev/null
+++ b/tests/libebook/vcard/.gitattributes
@@ -0,0 +1 @@
+*.vcf eol=crlf
diff --git a/tests/libebook/vcard/11.vcf b/tests/libebook/vcard/11.vcf
index e56c60d..d385d99 100644
--- a/tests/libebook/vcard/11.vcf
+++ b/tests/libebook/vcard/11.vcf
@@ -1,9 +1,9 @@
-BEGIN:VCARD
-VERSION:3.0
-X-EVOLUTION-FILE-AS:AttributeParam\, Invalid
-FN:Invalid AttributeParam
-N:AttributeParam;Invalid;;;
-NOPARAM;:pas-id-4094434A00000001
-TAIL;PARAM=value;:pas-id-4094434A00000001
-MIDDLE;PARAM=value;;PARAM2=value2:pas-id-4094434A00000001
+BEGIN:VCARD
+VERSION:3.0
+X-EVOLUTION-FILE-AS:AttributeParam\, Invalid
+FN:Invalid AttributeParam
+N:AttributeParam;Invalid;;;
+NOPARAM;:pas-id-4094434A00000001
+TAIL;PARAM=value;:pas-id-4094434A00000001
+MIDDLE;PARAM=value;;PARAM2=value2:pas-id-4094434A00000001
 END:VCARD
\ No newline at end of file
diff --git a/tests/libebook/vcard/12.vcf b/tests/libebook/vcard/12.vcf
index d1c0598..7f1e6ec 100644
--- a/tests/libebook/vcard/12.vcf
+++ b/tests/libebook/vcard/12.vcf
@@ -1,23 +1,23 @@
-BEGIN:VCARD
-VERSION:2.1
-N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-Br=C3=BCning;=
-Michael
-FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-Michael Br=C3=BCning
-TEL;CELL;WORK:+491622433834
-TEL;CELL;HOME:017623384942
-TEL;WORK:+4973117546691
-EMAIL;WORK:ext-Michael Bruning nokia com
-ADR;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-;=
-;=
-M=C3=BCnsterplatz 21;=
-Ulm;=
-Baden-W=C3=BCrttemberg;=
-89073;=
-
-LABEL;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-M=C3=BCnsterplatz 21=0D=0A89073 Ulm, Baden-W=C3=BCrttemberg
-BDAY:19781229
-END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+Br=C3=BCning;=
+Michael
+FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+Michael Br=C3=BCning
+TEL;CELL;WORK:+491622433834
+TEL;CELL;HOME:017623384942
+TEL;WORK:+4973117546691
+EMAIL;WORK:ext-Michael Bruning nokia com
+ADR;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+;=
+;=
+M=C3=BCnsterplatz 21;=
+Ulm;=
+Baden-W=C3=BCrttemberg;=
+89073;=
+
+LABEL;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+M=C3=BCnsterplatz 21=0D=0A89073 Ulm, Baden-W=C3=BCrttemberg
+BDAY:19781229
+END:VCARD
diff --git a/tests/libebook/vcard/3.vcf b/tests/libebook/vcard/3.vcf
index 0428d6a..3058716 100644
--- a/tests/libebook/vcard/3.vcf
+++ b/tests/libebook/vcard/3.vcf
@@ -1,13 +1,13 @@
-BEGIN:VCARD
-VERSION:2.1
-X-EVOLUTION-FILE-AS:Friedman, Nat
-FN:Nat
-N:Friedman;Nat;D;Mr.
-ADR;POSTAL;WORK:P.O. Box 101;;;Any Town;CA;91921-1234
-TEL;WORK:617 679 1984
-TEL;CELL:123 456 7890
-EMAIL;INTERNET:nat nat org
-EMAIL;INTERNET:nat ximian com
-BDAY:1977-08-06
-UID:pas-id-3E65886900000002
-END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+X-EVOLUTION-FILE-AS:Friedman, Nat
+FN:Nat
+N:Friedman;Nat;D;Mr.
+ADR;POSTAL;WORK:P.O. Box 101;;;Any Town;CA;91921-1234
+TEL;WORK:617 679 1984
+TEL;CELL:123 456 7890
+EMAIL;INTERNET:nat nat org
+EMAIL;INTERNET:nat ximian com
+BDAY:1977-08-06
+UID:pas-id-3E65886900000002
+END:VCARD
diff --git a/tests/libebook/vcard/4.vcf b/tests/libebook/vcard/4.vcf
index 305088a..46878f7 100644
--- a/tests/libebook/vcard/4.vcf
+++ b/tests/libebook/vcard/4.vcf
@@ -1,11 +1,11 @@
-BEGIN:VCARD
-VERSION:2.1
-X-EVOLUTION-FILE-AS:address, canada
-FN:canada address
-N:address;canada
-ADR;WORK:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
-ADR;HOME;PREF:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
-LABEL;QUOTED-PRINTABLE;WORK:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
-LABEL;QUOTED-PRINTABLE;HOME;PREF:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
-UID:pas-id-3E84C16E00000001
-END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+X-EVOLUTION-FILE-AS:address, canada
+FN:canada address
+N:address;canada
+ADR;WORK:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
+ADR;HOME;PREF:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
+LABEL;QUOTED-PRINTABLE;WORK:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
+LABEL;QUOTED-PRINTABLE;HOME;PREF:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
+UID:pas-id-3E84C16E00000001
+END:VCARD
diff --git a/tests/libebook/vcard/5.vcf b/tests/libebook/vcard/5.vcf
index f424330..51bbe20 100644
--- a/tests/libebook/vcard/5.vcf
+++ b/tests/libebook/vcard/5.vcf
@@ -1,32 +1,32 @@
-BEGIN:VCARD
-VERSION:2.1
-X-EVOLUTION-FILE-AS;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-FN;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-N;CHARSET=UTF-8:;十城目管理大型知座
-ADR;WORK;PREF;QUOTED-PRINTABLE;CHARSET=UTF-8:;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
-=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
-=9E=8B=E7=9F=A5=E5=BA=A7;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-LABEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;PREF:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
-=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
-=9E=8B=E7=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=
-=A4=A7=E5=9E=8B=E7=9F=A5=E5=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;VOICE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;FAX:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;HOME:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;CELL:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-EMAIL;INTERNET:weird weird com
-ORG;CHARSET=UTF-8:十城目管理大型知座
-TITLE;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-UID:pas-id-3E52FE2E00000000
-END:VCARD
-
+BEGIN:VCARD
+VERSION:2.1
+X-EVOLUTION-FILE-AS;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+FN;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+N;CHARSET=UTF-8:;十城目管理大型知座
+ADR;WORK;PREF;QUOTED-PRINTABLE;CHARSET=UTF-8:;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
+=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
+=9E=8B=E7=9F=A5=E5=BA=A7;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+LABEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;PREF:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
+=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
+=9E=8B=E7=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=
+=A4=A7=E5=9E=8B=E7=9F=A5=E5=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;VOICE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;FAX:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;HOME:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;CELL:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+EMAIL;INTERNET:weird weird com
+ORG;CHARSET=UTF-8:十城目管理大型知座
+TITLE;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+UID:pas-id-3E52FE2E00000000
+END:VCARD
+
diff --git a/tests/libedata-book/CMakeLists.txt b/tests/libedata-book/CMakeLists.txt
index 347196d..8bcc6c1 100644
--- a/tests/libedata-book/CMakeLists.txt
+++ b/tests/libedata-book/CMakeLists.txt
@@ -37,29 +37,31 @@ set(extra_ldflags
 set(SOURCES
        data-test-utils.c
        data-test-utils.h
+       test-book-cache-utils.c
+       test-book-cache-utils.h
 )
 
-add_library(data-test-utils STATIC
+add_library(data-book-test-utils STATIC
        ${SOURCES}
 )
 
-add_dependencies(data-test-utils
+add_dependencies(data-book-test-utils
        edataserver
        ${extra_deps}
 )
 
-target_compile_definitions(data-test-utils PRIVATE
-       -DG_LOG_DOMAIN=\"data-test-utils\"
+target_compile_definitions(data-book-test-utils PRIVATE
+       -DG_LOG_DOMAIN=\"data-book-test-utils\"
        ${extra_defines}
 )
 
-target_compile_options(data-test-utils PUBLIC
+target_compile_options(data-book-test-utils PUBLIC
        ${BACKEND_CFLAGS}
        ${DATA_SERVER_CFLAGS}
        ${extra_cflags}
 )
 
-target_include_directories(data-test-utils PUBLIC
+target_include_directories(data-book-test-utils PUBLIC
        ${CMAKE_BINARY_DIR}
        ${CMAKE_BINARY_DIR}/src
        ${CMAKE_SOURCE_DIR}/src
@@ -68,7 +70,7 @@ target_include_directories(data-test-utils PUBLIC
        ${extra_incdirs}
 )
 
-target_link_libraries(data-test-utils
+target_link_libraries(data-book-test-utils
        edataserver
        ${extra_deps}
        ${BACKEND_LDFLAGS}
@@ -79,7 +81,7 @@ target_link_libraries(data-test-utils
 set(extra_deps
        ebook
        ebook-contacts
-       data-test-utils
+       data-book-test-utils
 )
 
 set(extra_defines)
@@ -94,6 +96,18 @@ set(extra_defines)
 # This is because each migrated test changes the
 # locale and reloads the same addressbook of the previous test.
 set(TESTS
+       test-book-cache-get-contact
+       test-book-cache-create-cursor
+       test-book-cache-cursor-move-by-posix
+       test-book-cache-cursor-move-by-en-US
+       test-book-cache-cursor-move-by-fr-CA
+       test-book-cache-cursor-move-by-de-DE
+       test-book-cache-cursor-set-target
+       test-book-cache-cursor-calculate
+       test-book-cache-cursor-set-sexp
+       test-book-cache-cursor-change-locale
+       test-book-cache-offline
+       test-book-meta-backend
        test-sqlite-get-contact
        test-sqlite-create-cursor
        test-sqlite-cursor-move-by-posix
diff --git a/tests/libedata-book/data-test-utils.h b/tests/libedata-book/data-test-utils.h
index b72822e..e415101 100644
--- a/tests/libedata-book/data-test-utils.h
+++ b/tests/libedata-book/data-test-utils.h
@@ -22,6 +22,8 @@
 
 #include <libedata-book/libedata-book.h>
 
+G_BEGIN_DECLS
+
 /* This legend shows the add order, and various sort order of the sorted
  * vcards. The UIDs of these contacts are formed as 'sorted-1', 'sorted-2' etc
  * and the numbering of the contacts is according to the 'N' column in the
@@ -174,4 +176,6 @@ StepData *step_test_new_full               (const gchar         *test_prefix,
 void      step_test_add                    (StepData    *data,
                                            gboolean     filtered);
 
+G_END_DECLS
+
 #endif /* DATA_TEST_UTILS_H */
diff --git a/tests/libedata-book/test-book-cache-create-cursor.c 
b/tests/libedata-book/test-book-cache-create-cursor.c
new file mode 100644
index 0000000..c4cc6e1
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-create-cursor.c
@@ -0,0 +1,125 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static TCUClosure closure = { NULL };
+
+static void
+test_create_cursor_empty_query (TCUFixture *fixture,
+                               gconstpointer user_data)
+{
+       EBookCacheCursor *cursor;
+       EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+       EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING, E_BOOK_CURSOR_SORT_ASCENDING };
+       GError *error = NULL;
+
+       cursor = e_book_cache_cursor_new (
+               fixture->book_cache, NULL,
+               sort_fields, sort_types, 2, &error);
+
+       g_assert (cursor != NULL);
+       e_book_cache_cursor_free (fixture->book_cache, cursor);
+}
+
+static void
+test_create_cursor_valid_query (TCUFixture *fixture,
+                               gconstpointer user_data)
+{
+       EBookCacheCursor *cursor;
+       EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+       EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING, E_BOOK_CURSOR_SORT_ASCENDING };
+       EBookQuery *query;
+       gchar *sexp;
+       GError *error = NULL;
+
+       query = e_book_query_field_test (E_CONTACT_FULL_NAME, E_BOOK_QUERY_IS, "James Brown");
+       sexp = e_book_query_to_string (query);
+
+       cursor = e_book_cache_cursor_new (
+               fixture->book_cache, sexp,
+               sort_fields, sort_types, 2, &error);
+
+       g_assert (cursor != NULL);
+       e_book_cache_cursor_free (fixture->book_cache, cursor);
+       g_free (sexp);
+       e_book_query_unref (query);
+}
+
+static void
+test_create_cursor_invalid_sort (TCUFixture *fixture,
+                                gconstpointer user_data)
+{
+       EBookCacheCursor *cursor;
+       EContactField sort_fields[] = { E_CONTACT_TEL };
+       EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING };
+       GError *error = NULL;
+
+       cursor = e_book_cache_cursor_new (
+               fixture->book_cache, NULL,
+               sort_fields, sort_types, 1, &error);
+
+       g_assert (cursor == NULL);
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+       g_clear_error (&error);
+}
+
+static void
+test_create_cursor_missing_sort (TCUFixture *fixture,
+                                gconstpointer user_data)
+{
+       EBookCacheCursor *cursor;
+       GError *error = NULL;
+
+       cursor = e_book_cache_cursor_new (fixture->book_cache, NULL, NULL, NULL, 0, &error);
+
+       g_assert (cursor == NULL);
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+       g_clear_error (&error);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       g_test_add (
+               "/EBookCacheCursor/Create/EmptyQuery", TCUFixture, &closure,
+               tcu_fixture_setup, test_create_cursor_empty_query, tcu_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Create/ValidQuery", TCUFixture, &closure,
+               tcu_fixture_setup, test_create_cursor_valid_query, tcu_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Create/InvalidSort", TCUFixture, &closure,
+               tcu_fixture_setup, test_create_cursor_invalid_sort, tcu_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Create/MissingSort", TCUFixture, &closure,
+               tcu_fixture_setup, test_create_cursor_missing_sort, tcu_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-calculate.c 
b/tests/libedata-book/test-book-cache-cursor-calculate.c
new file mode 100644
index 0000000..522ca6f
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-calculate.c
@@ -0,0 +1,695 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static TCUCursorClosure ascending_closure = {
+       { NULL },
+       NULL, E_BOOK_CURSOR_SORT_ASCENDING
+};
+
+static TCUCursorClosure descending_closure = {
+       { NULL },
+       NULL, E_BOOK_CURSOR_SORT_DESCENDING
+};
+
+static void
+test_cursor_calculate_initial (TCUCursorFixture *fixture,
+                              gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+           g_error ("Error calculating cursor: %s", error->message);
+
+       g_assert_cmpint (position, ==, 0);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_move_forward (TCUCursorFixture *fixture,
+                                   gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      5,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Assert the first 5 contacts in en_US order */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-11",
+               "sorted-1",
+               "sorted-2",
+               "sorted-5",
+               "sorted-6",
+               NULL);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 0 + 5 = position 5, result index 4 (results[0, 1, 2, 3, 4]) */
+       g_assert_cmpint (position, ==, 5);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_move_backwards (TCUCursorFixture *fixture,
+                                     gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_END,
+                                      -5,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Assert the last 5 contacts in en_US order */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-20",
+               "sorted-19",
+               "sorted-9",
+               "sorted-13",
+               "sorted-12",
+               NULL);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+           g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 20 - 5 = position 16 result index 15 (results[20, 19, 18, 17, 16]) */
+       g_assert_cmpint (position, ==, 16);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_back_and_forth (TCUCursorFixture *fixture,
+                                     gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                      7,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       g_assert_cmpint (g_slist_length (results), ==, 7);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 0 + 7 = position 7 result index 6 (results[0, 1, 2, 3, 4, 5, 6]) */
+       g_assert_cmpint (position, ==, 7);
+       g_assert_cmpint (total, ==, 20);
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      -4,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       g_assert_cmpint (g_slist_length (results), ==, 4);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 7 - 4 = position 3 result index 2 (results[5, 4, 3, 2]) */
+       g_assert_cmpint (position, ==, 3);
+       g_assert_cmpint (total, ==, 20);
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      5,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 3 + 5 = position 8 result index 7 (results[3, 4, 5, 6, 7]) */
+       g_assert_cmpint (position, ==, 8);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_partial_target (TCUCursorFixture *fixture,
+                                     gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+       ECollator *collator;
+       gint n_labels;
+       const gchar *const *labels;
+
+       /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+       collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+       labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+       g_assert_cmpstr (labels[3], ==, "C");
+       e_collator_unref (collator);
+
+       /* Set the cursor at the start of family names beginning with 'C' */
+       e_book_cache_cursor_set_target_alphabetic_index (
+               ((TCUFixture *) fixture)->book_cache,
+               fixture->cursor, 3);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* Position is 13, there are 13 contacts before the letter 'C' in en_US locale */
+       g_assert_cmpint (position, ==, 13);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_after_modification (TCUCursorFixture *fixture,
+                                         gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Set the cursor to point exactly 'blackbird' (which is the 12th contact) */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      12, NULL, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* blackbird is at position 12 in en_US locale */
+       g_assert_cmpint (position, ==, 12);
+       g_assert_cmpint (total, ==, 20);
+
+       /* Rename Muffler -> Jacob Appelbaum */
+       e_contact_set (fixture->contacts[19 - 1], E_CONTACT_FAMILY_NAME, "Appelbaum");
+       e_contact_set (fixture->contacts[19 - 1], E_CONTACT_GIVEN_NAME, "Jacob");
+       if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+                                       fixture->contacts[19 - 1],
+                                       e_contact_get_const (fixture->contacts[19 - 1], E_CONTACT_UID),
+                                       E_CACHE_IS_ONLINE, NULL, &error))
+               g_error ("Failed to modify contact: %s", error->message);
+
+       /* Rename Müller -> Sade Adu */
+       e_contact_set (fixture->contacts[20 - 1], E_CONTACT_FAMILY_NAME, "Adu");
+       e_contact_set (fixture->contacts[20 - 1], E_CONTACT_GIVEN_NAME, "Sade");
+       if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+                                       fixture->contacts[20 - 1],
+                                       e_contact_get_const (fixture->contacts[20 - 1], E_CONTACT_UID),
+                                       E_CACHE_IS_ONLINE, NULL, &error))
+               g_error ("Failed to modify contact: %s", error->message);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* blackbird is now at position 14 after moving 2 later contacts to begin with 'A' */
+       g_assert_cmpint (position, ==, 14);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_filtered_initial (TCUCursorFixture *fixture,
+                                       gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+           g_error ("Error calculating cursor: %s", error->message);
+
+       g_assert_cmpint (position, ==, 0);
+       g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_move_forward (TCUCursorFixture *fixture,
+                                            gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      5, &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+           g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 0 + 5 = position 5, result index 4 (results[0, 1, 2, 3, 4]) */
+       g_assert_cmpint (position, ==, 5);
+       g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_move_backwards (TCUCursorFixture *fixture,
+                                              gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_END,
+                                      -5,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 13 - 5 = position 9 (results[13, 12, 11, 10, 9]) */
+       g_assert_cmpint (position, ==, 9);
+       g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_partial_target (TCUCursorFixture *fixture,
+                                              gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+       ECollator *collator;
+       gint n_labels;
+       const gchar *const *labels;
+
+       /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+       collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+       labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+       g_assert_cmpstr (labels[3], ==, "C");
+       e_collator_unref (collator);
+
+       /* Set the cursor at the start of family names beginning with 'C' */
+       e_book_cache_cursor_set_target_alphabetic_index (
+               ((TCUFixture *) fixture)->book_cache,
+               fixture->cursor, 3);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* There are 9 contacts before the letter 'C' in the en_US locale */
+       g_assert_cmpint (position, ==, 9);
+       g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_after_modification (TCUCursorFixture *fixture,
+                                                  gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Set the cursor to point exactly 'blackbird' (which is the 8th contact when filtered) */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                      8, NULL, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* 'blackbirds' -> Jacob Appelbaum */
+       e_contact_set (fixture->contacts[18 - 1], E_CONTACT_FAMILY_NAME, "Appelbaum");
+       e_contact_set (fixture->contacts[18 - 1], E_CONTACT_GIVEN_NAME, "Jacob");
+       if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+                                       fixture->contacts[18 - 1],
+                                       e_contact_get_const (fixture->contacts[18 - 1], E_CONTACT_UID),
+                                       E_CACHE_IS_ONLINE, NULL, &error))
+               g_error ("Failed to modify contact: %s", error->message);
+
+       /* 'black-birds' -> Sade Adu */
+       e_contact_set (fixture->contacts[17 - 1], E_CONTACT_FAMILY_NAME, "Adu");
+       e_contact_set (fixture->contacts[17 - 1], E_CONTACT_GIVEN_NAME, "Sade");
+       if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+                                       fixture->contacts[17 - 1],
+                                       e_contact_get_const (fixture->contacts[17 - 1], E_CONTACT_UID),
+                                       E_CACHE_IS_ONLINE, NULL, &error))
+               g_error ("Failed to modify contact: %s", error->message);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* blackbird is now at position 11 after moving 2 later contacts to begin with 'A' */
+       g_assert_cmpint (position, ==, 9);
+       g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_descending_move_forward (TCUCursorFixture *fixture,
+                                               gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                      5,
+                                      &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Assert the first 5 contacts in en_US order */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-20",
+               "sorted-19",
+               "sorted-9",
+               "sorted-13",
+               "sorted-12",
+               NULL);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+           g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 0 + 5 = position 5, result index 4 (results[0, 1, 2, 3, 4]) */
+       g_assert_cmpint (position, ==, 5);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_descending_move_backwards (TCUCursorFixture *fixture,
+                                                gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Move cursor */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_END,
+                                      -5, &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Assert the last 5 contacts in en_US order */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-11",
+               "sorted-1",
+               "sorted-2",
+               "sorted-5",
+               "sorted-6",
+               NULL);
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+           g_error ("Error calculating cursor: %s", error->message);
+
+       /* results 20 - 5 = position 16 result index 15 (results[20, 19, 18, 17, 16]) */
+       g_assert_cmpint (position, ==, 16);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_descending_partial_target (TCUCursorFixture *fixture,
+                                                gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+       ECollator *collator;
+       gint n_labels;
+       const gchar *const *labels;
+
+       /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+       collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+       labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+       g_assert_cmpstr (labels[3], ==, "C");
+       e_collator_unref (collator);
+
+       /* Set the cursor at the start of family names beginning with 'C' */
+       e_book_cache_cursor_set_target_alphabetic_index (
+               ((TCUFixture *) fixture)->book_cache,
+               fixture->cursor, 3);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* Position is 7, there are 7 contacts leading up to the last 'C' in en_US locale
+        * (when sorting in descending order) */
+       g_assert_cmpint (position, ==, 7);
+       g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_descending_after_modification (TCUCursorFixture *fixture,
+                                                    gconstpointer user_data)
+{
+       GError *error = NULL;
+       gint position = 0, total = 0;
+
+       /* Set the cursor to point exactly 'Bät' (which is the 12th contact in descending order) */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                      12, NULL, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* 'Bät' is at position 12 in en_US locale (descending order) */
+       g_assert_cmpint (position, ==, 12);
+       g_assert_cmpint (total, ==, 20);
+
+       /* Rename Muffler -> Jacob Appelbaum */
+       e_contact_set (fixture->contacts[19 - 1], E_CONTACT_FAMILY_NAME, "Appelbaum");
+       e_contact_set (fixture->contacts[19 - 1], E_CONTACT_GIVEN_NAME, "Jacob");
+       if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+                                       fixture->contacts[19 - 1],
+                                       e_contact_get_const (fixture->contacts[19 - 1], E_CONTACT_UID),
+                                       E_CACHE_IS_ONLINE, NULL, &error))
+               g_error ("Failed to modify contact: %s", error->message);
+
+       /* Rename Müller -> Sade Adu */
+       e_contact_set (fixture->contacts[20 - 1], E_CONTACT_FAMILY_NAME, "Adu");
+       e_contact_set (fixture->contacts[20 - 1], E_CONTACT_GIVEN_NAME, "Sade");
+       if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+                                       fixture->contacts[20 - 1],
+                                       e_contact_get_const (fixture->contacts[20 - 1], E_CONTACT_UID),
+                                       E_CACHE_IS_ONLINE, NULL, &error))
+               g_error ("Failed to modify contact: %s", error->message);
+
+       /* Check new position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* 'Bät' is now at position 10 in descending order after moving 2 contacts to begin with 'A' */
+       g_assert_cmpint (position, ==, 10);
+       g_assert_cmpint (total, ==, 20);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Initial", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_initial,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/MoveForward", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_move_forward,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/MoveBackwards", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_move_backwards,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/BackAndForth", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_back_and_forth,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/AlphabeticTarget", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_partial_target,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/AfterModification", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_after_modification,
+               tcu_cursor_fixture_teardown);
+
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Filtered/Initial", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_filtered_setup,
+               test_cursor_calculate_filtered_initial,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Filtered/MoveForward", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_filtered_setup,
+               test_cursor_calculate_filtered_move_forward,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Filtered/MoveBackwards", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_filtered_setup,
+               test_cursor_calculate_filtered_move_backwards,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Filtered/AlphabeticTarget", TCUCursorFixture, &ascending_closure,
+               tcu_cursor_fixture_filtered_setup,
+               test_cursor_calculate_filtered_partial_target,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Filtered/AfterModification", TCUCursorFixture, 
&ascending_closure,
+               tcu_cursor_fixture_filtered_setup,
+               test_cursor_calculate_filtered_after_modification,
+               tcu_cursor_fixture_teardown);
+
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Descending/Initial", TCUCursorFixture, &descending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_initial,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Descending/MoveForward", TCUCursorFixture, &descending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_descending_move_forward,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Descending/MoveBackwards", TCUCursorFixture, &descending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_descending_move_backwards,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Descending/BackAndForth", TCUCursorFixture, &descending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_back_and_forth,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Descending/AlphabeticTarget", TCUCursorFixture, 
&descending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_descending_partial_target,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/Calculate/Descending/AfterModification", TCUCursorFixture, 
&descending_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_calculate_descending_after_modification,
+               tcu_cursor_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-change-locale.c 
b/tests/libedata-book/test-book-cache-cursor-change-locale.c
new file mode 100644
index 0000000..f95a901
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-change-locale.c
@@ -0,0 +1,102 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+       gboolean empty_book;
+       const gchar *path;
+} params[] = {
+       { FALSE, "/EBookCacheCursor/DefaultSummary" },
+       { TRUE,  "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUStepData *data;
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/ChangeLocale/POSIX/en_US", "POSIX",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 2,  6,  3,  8);
+               tcu_step_test_add_assertion (data, 5, 1,  5,  4,  7,  15);
+               tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+               tcu_step_test_add_assertion (data, 5, 12, 13, 9,  19, 20);
+
+               tcu_step_test_change_locale (data, "en_US.UTF-8", 0);
+               tcu_step_test_add_assertion (data, 5, 11, 1,  2,  5,  6);
+               tcu_step_test_add_assertion (data, 5, 4,  3,  7,  8,  15);
+               tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+               tcu_step_test_add_assertion (data, 5, 12, 13, 9,  19, 20);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/ChangeLocale/en_US/fr_CA", "en_US.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1,  2,  5,  6);
+               tcu_step_test_add_assertion (data, 5, 4,  3,  7,  8,  15);
+               tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+               tcu_step_test_add_assertion (data, 5, 12, 13, 9,  19, 20);
+
+               tcu_step_test_change_locale (data, "fr_CA.UTF-8", 0);
+               tcu_step_test_add_assertion (data, 5, 11, 1,  2,  5,  6);
+               tcu_step_test_add_assertion (data, 5, 4,  3,  7,  8,  15);
+               tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+               tcu_step_test_add_assertion (data, 5, 13, 12, 9,  19, 20);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/ChangeLocale/fr_CA/de_DE", "fr_CA.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1,  2,  5,  6);
+               tcu_step_test_add_assertion (data, 5, 4,  3,  7,  8,  15);
+               tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+               tcu_step_test_add_assertion (data, 5, 13, 12, 9,  19, 20);
+
+               /* When changing from fr_CA to de_DE, two numbers change:
+                *
+                * sorted-5:
+                *    049-2459-4393 is now parsed with the national number as 4924594393
+                *
+                * sorted-4:
+                *    12 245999 is now parsed with national number 12245999 instead of 2245999
+                *
+                */
+               tcu_step_test_change_locale (data, "de_DE.UTF-8", 2);
+               tcu_step_test_add_assertion (data, 5, 11, 1,  2,  5,  6);
+               tcu_step_test_add_assertion (data, 5, 7,  8,  4,  3,  15);
+               tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+               tcu_step_test_add_assertion (data, 5, 12, 13, 9,  20, 19);
+               tcu_step_test_add (data, FALSE);
+       }
+
+       /* On this case, we want to delete the work directory and start fresh */
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-de-DE.c 
b/tests/libedata-book/test-book-cache-cursor-move-by-de-DE.c
new file mode 100644
index 0000000..8432f03
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-de-DE.c
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+       gboolean empty_book;
+       const gchar *path;
+} params[] = {
+       { FALSE, "/EBookCacheCursor/DefaultSummary" },
+       { TRUE,  "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUStepData *data;
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/de_DE/Move/Forward", "de_DE.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+               tcu_step_test_add_assertion (data, 6, 7, 8, 4, 3, 15, 17);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/de_DE/Move/ForwardOnNameless", "de_DE.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 1, 11);
+               tcu_step_test_add_assertion (data, 3, 1, 2, 5);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/de_DE/Move/Backwards", "de_DE.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 19, 20, 9, 13, 12);
+               tcu_step_test_add_assertion (data, -8, 14, 10, 18, 16, 17, 15, 3, 4);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/de_DE/Filtered/Move/Forward", "de_DE.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 8);
+               tcu_step_test_add_assertion (data, 8, 3, 17, 16, 18, 10, 14, 12, 9);
+               tcu_step_test_add (data, TRUE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/de_DE/Filtered/Move/Backwards", "de_DE.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+               tcu_step_test_add_assertion (data, -8, 16, 17, 3, 8, 5, 2, 1, 11);
+               tcu_step_test_add (data, TRUE);
+       }
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-en-US.c 
b/tests/libedata-book/test-book-cache-cursor-move-by-en-US.c
new file mode 100644
index 0000000..94955d5
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-en-US.c
@@ -0,0 +1,100 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+       gboolean empty_book;
+       const gchar *path;
+} params[] = {
+       { FALSE, "/EBookCacheCursor/DefaultSummary" },
+       { TRUE,  "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUStepData *data;
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/en_US/Move/Forward", "en_US.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+               tcu_step_test_add_assertion (data, 6, 4, 3, 7, 8, 15, 17);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/en_US/Move/ForwardOnNameless", "en_US.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 1, 11);
+               tcu_step_test_add_assertion (data, 3, 1, 2, 5);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/en_US/Move/Backwards", "en_US.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 20, 19, 9, 13, 12);
+               tcu_step_test_add_assertion (data, -8, 14, 10, 18, 16, 17, 15, 8, 7);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/en_US/Filtered/Move/Forward", "en_US.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 3);
+               tcu_step_test_add_assertion (data, 8, 8, 17, 16, 18, 10, 14, 12, 9);
+               tcu_step_test_add (data, TRUE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/en_US/Filtered/Move/Backwards", "en_US.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+               tcu_step_test_add_assertion (data, -8, 16, 17, 8, 3, 5, 2, 1, 11);
+               tcu_step_test_add (data, TRUE);
+
+               data = tcu_step_test_new_full (
+                       params[ii].path, "/en_US/Move/Descending/Forward", "en_US.UTF-8",
+                       params[ii].empty_book,
+                       E_BOOK_CURSOR_SORT_DESCENDING);
+               tcu_step_test_add_assertion (data, 5, 20, 19, 9,  13, 12);
+               tcu_step_test_add_assertion (data, 5, 14, 10, 18, 16, 17);
+               tcu_step_test_add_assertion (data, 5, 15, 8,  7,  3,  4);
+               tcu_step_test_add_assertion (data, 5, 6,  5,  2,  1,  11);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new_full (
+                       params[ii].path, "/en_US/Move/Descending/Backwards", "en_US.UTF-8",
+                       params[ii].empty_book,
+                       E_BOOK_CURSOR_SORT_DESCENDING);
+               tcu_step_test_add_assertion (data, -10, 11, 1,  2,  5,  6,  4,  3,  7,  8, 15);
+               tcu_step_test_add_assertion (data, -10, 17, 16, 18, 10, 14, 12, 13, 9, 19, 20);
+               tcu_step_test_add (data, FALSE);
+       }
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-fr-CA.c 
b/tests/libedata-book/test-book-cache-cursor-move-by-fr-CA.c
new file mode 100644
index 0000000..8d71e27
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-fr-CA.c
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+       gboolean empty_book;
+       const gchar *path;
+} params[] = {
+       { FALSE, "/EBookCacheCursor/DefaultSummary" },
+       { TRUE,  "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUStepData *data;
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/fr_CA/Move/Forward", "fr_CA.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+               tcu_step_test_add_assertion (data, 6, 4, 3, 7, 8, 15, 17);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/fr_CA/Move/ForwardOnNameless", "fr_CA.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 1, 11);
+               tcu_step_test_add_assertion (data, 3, 1, 2, 5);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/fr_CA/Move/Backwards", "fr_CA.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 20, 19, 9, 12, 13);
+               tcu_step_test_add_assertion (data, -8, 14, 10, 18, 16, 17, 15, 8, 7);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/fr_CA/Filtered/Move/Forward", "fr_CA.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 3);
+               tcu_step_test_add_assertion (data, 8, 8, 17, 16, 18, 10, 14, 12, 9);
+               tcu_step_test_add (data, TRUE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/fr_CA/Filtered/Move/Backwards", "fr_CA.UTF-8",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+               tcu_step_test_add_assertion (data, -8, 16, 17, 8, 3, 5, 2, 1, 11);
+               tcu_step_test_add (data, TRUE);
+       }
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-posix.c 
b/tests/libedata-book/test-book-cache-cursor-move-by-posix.c
new file mode 100644
index 0000000..110cd6c
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-posix.c
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+       gboolean empty_book;
+       const gchar *path;
+} params[] = {
+       { FALSE, "/EBookCacheCursor/DefaultSummary" },
+       { TRUE,  "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUStepData *data;
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/POSIX/Move/Forward", "POSIX",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 2, 6, 3, 8);
+               tcu_step_test_add_assertion (data, 6, 1,  5,  4,  7,  15, 17);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/POSIX/Move/ForwardOnNameless", "POSIX",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 1, 11);
+               tcu_step_test_add_assertion (data, 3, 2, 6, 3);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/POSIX/Move/Backwards", "POSIX",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 20, 19, 9, 13, 12);
+               tcu_step_test_add_assertion (data, -12, 14, 10, 18, 16, 17, 15, 7, 4, 5, 1, 8, 3);
+               tcu_step_test_add (data, FALSE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/POSIX/Filtered/Move/Forward", "POSIX",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, 5, 11, 2, 3, 8, 1);
+               tcu_step_test_add_assertion (data, 8, 5, 17, 16, 18, 10, 14, 12, 9);
+               tcu_step_test_add (data, TRUE);
+
+               data = tcu_step_test_new (
+                       params[ii].path, "/POSIX/Filtered/Move/Backwards", "POSIX",
+                       params[ii].empty_book);
+               tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+               tcu_step_test_add_assertion (data, -8, 16, 17, 5, 1, 8, 3, 2, 11);
+               tcu_step_test_add (data, TRUE);
+       }
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-set-sexp.c 
b/tests/libedata-book/test-book-cache-cursor-set-sexp.c
new file mode 100644
index 0000000..88da7de
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-set-sexp.c
@@ -0,0 +1,155 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static TCUCursorClosure book_closure = { { NULL }, NULL, E_BOOK_CURSOR_SORT_ASCENDING };
+
+static void
+test_cursor_sexp_calculate_position (TCUCursorFixture *fixture,
+                                    gconstpointer user_data)
+{
+       GError *error = NULL;
+       EBookQuery *query;
+       gint    position = 0, total = 0;
+       gchar *sexp = NULL;
+       GSList *results = NULL, *node;
+       EBookCacheSearchData *data;
+
+       /* Set the cursor to point exactly to 'blackbirds', which is the 12th contact in en_US */
+       if (!e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                       fixture->cursor,
+                                       E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                       E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                       12, &results, NULL, &error))
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Ensure we moved to the right contact */
+       node = g_slist_last (results);
+       g_assert (node);
+       data = node->data;
+       g_assert_cmpstr (data->uid, ==, "sorted-16");
+       g_slist_free_full (results, e_book_cache_search_data_free);
+
+       /* Check position */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* blackbird is at position 12 in an unfiltered en_US locale */
+       g_assert_cmpint (position, ==, 12);
+       g_assert_cmpint (total, ==, 20);
+
+       /* Set new sexp, only contacts with .com email addresses */
+       query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_ENDS_WITH, ".com");
+       sexp = e_book_query_to_string (query);
+       e_book_query_unref (query);
+
+       if (!e_book_cache_cursor_set_sexp (((TCUFixture *) fixture)->book_cache,
+                                           fixture->cursor, sexp, &error))
+               g_error ("Failed to set sexp: %s", error->message);
+
+       /* Check new position after modified sexp */
+       if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+                                            fixture->cursor, &total, &position, NULL, &error))
+               g_error ("Error calculating cursor: %s", error->message);
+
+       /* 'blackbird' is now at position 8 out of 13, with a filtered set of contacts in en_US locale */
+       g_assert_cmpint (position, ==, 8);
+       g_assert_cmpint (total, ==, 13);
+
+       g_free (sexp);
+}
+
+static void
+test_cursor_sexp_and_step (TCUCursorFixture *fixture,
+                          gconstpointer user_data)
+{
+       GError *error = NULL;
+       EBookQuery *query;
+       gchar *sexp = NULL;
+       GSList *results = NULL, *node;
+       EBookCacheSearchData *data;
+
+       /* Set new sexp, only contacts with .com email addresses */
+       query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_ENDS_WITH, ".com");
+       sexp = e_book_query_to_string (query);
+       e_book_query_unref (query);
+
+       if (!e_book_cache_cursor_set_sexp (((TCUFixture *) fixture)->book_cache,
+                                           fixture->cursor, sexp, &error))
+               g_error ("Failed to set sexp: %s", error->message);
+
+       /* Step 6 results from the beginning of the filtered list, gets up to contact 'sorted-8' */
+       if (!e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                       fixture->cursor,
+                                       E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                       E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                       6, &results, NULL, &error))
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Ensure we moved to the right contact */
+       node = g_slist_last (results);
+       g_assert (node);
+       data = node->data;
+       g_assert_cmpstr (data->uid, ==, "sorted-8");
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Step 6 results more, gets up to contact 'sorted-12' */
+       if (!e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                       fixture->cursor,
+                                       E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                       E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                       6, &results, NULL, &error))
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       /* Ensure we moved to the right contact */
+       node = g_slist_last (results);
+       g_assert (node);
+       data = node->data;
+       g_assert_cmpstr (data->uid, ==, "sorted-12");
+       g_slist_free_full (results, e_book_cache_search_data_free);
+
+       g_free (sexp);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       g_test_add (
+               "/EBookCacheCursor/SetSexp/CalculatePosition", TCUCursorFixture, &book_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_sexp_calculate_position,
+               tcu_cursor_fixture_teardown);
+       g_test_add (
+               "/EBookCacheCursor/SetSexp/Step", TCUCursorFixture, &book_closure,
+               tcu_cursor_fixture_setup,
+               test_cursor_sexp_and_step,
+               tcu_cursor_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-set-target.c 
b/tests/libedata-book/test-book-cache-cursor-set-target.c
new file mode 100644
index 0000000..f230634
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-set-target.c
@@ -0,0 +1,225 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+/*****************************************************
+ *          Expect the same results twice            *
+ *****************************************************/
+static void
+test_cursor_set_target_reset_cursor (TCUCursorFixture *fixture,
+                                    gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+
+       /* First batch */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                      5, &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       tcu_print_results (results);
+
+       /* Assert the first 5 contacts in en_US order */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-11",
+               "sorted-1",
+               "sorted-2",
+               "sorted-5",
+               "sorted-6",
+               NULL);
+
+       g_slist_free_full (results, e_book_cache_search_data_free);
+       results = NULL;
+
+       /* Second batch reset (same results) */
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+                                      5, &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       tcu_print_results (results);
+
+       /* Assert the first 5 contacts in en_US order again */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-11",
+               "sorted-1",
+               "sorted-2",
+               "sorted-5",
+               "sorted-6",
+               NULL);
+
+       g_slist_free_full (results, e_book_cache_search_data_free);
+}
+
+/*****************************************************
+ * Expect results with family name starting with 'C' *
+ *****************************************************/
+static void
+test_cursor_set_target_c_next_results (TCUCursorFixture *fixture,
+                                      gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       ECollator *collator;
+       gint n_labels;
+       const gchar *const *labels;
+
+       /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+       collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+       labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+       g_assert_cmpstr (labels[3], ==, "C");
+       e_collator_unref (collator);
+
+       /* Set the cursor at the start of family names beginning with 'C' */
+       e_book_cache_cursor_set_target_alphabetic_index (
+               ((TCUFixture *) fixture)->book_cache,
+               fixture->cursor, 3);
+
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      5, &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       tcu_print_results (results);
+
+       /* Assert that we got the results starting at C */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-10",
+               "sorted-14",
+               "sorted-12",
+               "sorted-13",
+               "sorted-9",
+               NULL);
+
+       g_slist_free_full (results, e_book_cache_search_data_free);
+}
+
+/*****************************************************
+ *       Expect results before the letter 'C'        *
+ *****************************************************/
+static void
+test_cursor_set_target_c_prev_results (TCUCursorFixture *fixture,
+                                       gconstpointer user_data)
+{
+       GSList *results = NULL;
+       GError *error = NULL;
+       ECollator *collator;
+       gint n_labels;
+       const gchar *const *labels;
+
+       /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+       collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+       labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+       g_assert_cmpstr (labels[3], ==, "C");
+       e_collator_unref (collator);
+
+       /* Set the cursor at the start of family names beginning with 'C' */
+       e_book_cache_cursor_set_target_alphabetic_index (
+               ((TCUFixture *) fixture)->book_cache,
+               fixture->cursor, 3);
+
+       if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+                                      fixture->cursor,
+                                      E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                                      E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+                                      -5, &results, NULL, &error) < 0)
+               g_error ("Error fetching cursor results: %s", error->message);
+
+       tcu_print_results (results);
+
+       /* Assert that we got the results before C */
+       g_assert_cmpint (g_slist_length (results), ==, 5);
+       tcu_assert_contacts_order (
+               results,
+               "sorted-18",
+               "sorted-16",
+               "sorted-17",
+               "sorted-15",
+               "sorted-8",
+               NULL);
+
+       g_slist_free_full (results, e_book_cache_search_data_free);
+}
+
+static TCUCursorClosure closures[] = {
+       { { NULL }, NULL, E_BOOK_CURSOR_SORT_ASCENDING },
+       { { tcu_setup_empty_book }, NULL, E_BOOK_CURSOR_SORT_ASCENDING }
+};
+
+static const gchar *prefixes[] = {
+       "/EBookCache/DefaultSummary",
+       "/EBookCache/EmptySummary"
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       for (ii = 0; ii < G_N_ELEMENTS (closures); ii++) {
+               gchar *path;
+
+               path = g_strconcat (prefixes[ii], "/SetTarget/ResetCursor", NULL);
+               g_test_add (
+                       path, TCUCursorFixture, &closures[ii],
+                       tcu_cursor_fixture_setup,
+                       test_cursor_set_target_reset_cursor,
+                       tcu_cursor_fixture_teardown);
+               g_free (path);
+
+               path = g_strconcat (prefixes[ii], "/SetTarget/Alphabetic/C/NextResults", NULL);
+               g_test_add (
+                       path, TCUCursorFixture, &closures[ii],
+                       tcu_cursor_fixture_setup,
+                       test_cursor_set_target_c_next_results,
+                       tcu_cursor_fixture_teardown);
+               g_free (path);
+
+               path = g_strconcat (prefixes[ii], "/SetTarget/Alphabetic/C/PreviousResults", NULL);
+               g_test_add (
+                       path, TCUCursorFixture, &closures[ii],
+                       tcu_cursor_fixture_setup,
+                       test_cursor_set_target_c_prev_results,
+                       tcu_cursor_fixture_teardown);
+               g_free (path);
+       }
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-get-contact.c 
b/tests/libedata-book/test-book-cache-get-contact.c
new file mode 100644
index 0000000..c214d4f
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-get-contact.c
@@ -0,0 +1,78 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static void
+test_get_contact (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       EContact *other = NULL;
+       GError *error = NULL;
+
+       tcu_add_contact_from_test_case (fixture, "simple-1", &contact);
+
+       if (!e_book_cache_get_contact (fixture->book_cache,
+               (const gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+               FALSE, &other, NULL, &error)) {
+               g_error (
+                       "Failed to get contact with uid '%s': %s",
+                       (const gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+                       error->message);
+       }
+
+       g_object_unref (contact);
+       g_object_unref (other);
+}
+
+static TCUClosure closures[] = {
+       { NULL },
+       { tcu_setup_empty_book }
+};
+
+static const gchar *paths[] = {
+       "/EBookCache/DefaultSummary/GetContact",
+       "/EBookCache/EmptySummary/GetContact",
+};
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       for (ii = 0; ii < G_N_ELEMENTS (closures); ii++) {
+               g_test_add (
+                       paths[ii], TCUFixture, &closures[ii],
+                       tcu_fixture_setup, test_get_contact, tcu_fixture_teardown);
+       }
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-offline.c b/tests/libedata-book/test-book-cache-offline.c
new file mode 100644
index 0000000..b6d4f1e
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-offline.c
@@ -0,0 +1,1138 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static void
+test_fill_cache (TCUFixture *fixture,
+                EContact **out_contact)
+{
+       tcu_add_contact_from_test_case (fixture, "custom-1", out_contact);
+       tcu_add_contact_from_test_case (fixture, "custom-3", NULL);
+       tcu_add_contact_from_test_case (fixture, "custom-9", NULL);
+}
+
+enum {
+       EXPECT_DEFAULT          = (0),
+       EXPECT_CUSTOM_1         = (1 << 0),
+       EXPECT_CUSTOM_9         = (1 << 1),
+       EXPECT_SIMPLE_1         = (1 << 2),
+       EXPECT_SIMPLE_2         = (1 << 3),
+       HAS_SEARCH_DATA         = (1 << 4),
+       HAS_META_CONTACTS       = (1 << 5),
+       SKIP_CONTACT_PUT        = (1 << 6)
+};
+
+static void
+test_check_search_result (const GSList *list,
+                         guint32 flags)
+{
+       gboolean expect_custom_1 = (flags & EXPECT_CUSTOM_1) != 0;
+       gboolean expect_custom_9 = (flags & EXPECT_CUSTOM_9) != 0;
+       gboolean expect_simple_1 = (flags & EXPECT_SIMPLE_1) != 0;
+       gboolean expect_simple_2 = (flags & EXPECT_SIMPLE_2) != 0;
+       gboolean has_search_data = (flags & HAS_SEARCH_DATA) != 0;
+       gboolean has_meta_contacts = (flags & HAS_META_CONTACTS) != 0;
+       gboolean have_custom_1 = FALSE;
+       gboolean have_custom_3 = FALSE;
+       gboolean have_custom_9 = FALSE;
+       gboolean have_simple_1 = FALSE;
+       gboolean have_simple_2 = FALSE;
+       const GSList *link;
+
+       for (link = list; link; link = g_slist_next (link)) {
+               const gchar *uid;
+
+               if (has_search_data) {
+                       EBookCacheSearchData *sd = link->data;
+                       EContact *contact;
+
+                       g_assert (sd != NULL);
+                       g_assert (sd->uid != NULL);
+                       g_assert (sd->vcard != NULL);
+
+                       uid = sd->uid;
+
+                       contact = e_contact_new_from_vcard (sd->vcard);
+                       g_assert (E_IS_CONTACT (contact));
+                       g_assert_cmpstr (uid, ==, e_contact_get_const (contact, E_CONTACT_UID));
+
+                       if (has_meta_contacts) {
+                               g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+                               g_assert_null (e_contact_get_const (contact, E_CONTACT_EMAIL_1));
+                       } else {
+                               g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_EMAIL_1));
+                       }
+
+                       g_clear_object (&contact);
+               } else {
+                       uid = link->data;
+               }
+
+               g_assert_nonnull (uid);
+
+               if (g_str_equal (uid, "custom-1")) {
+                       g_assert (expect_custom_1);
+                       g_assert (!have_custom_1);
+                       have_custom_1 = TRUE;
+               } else if (g_str_equal (uid, "custom-3")) {
+                       g_assert (!have_custom_3);
+                       have_custom_3 = TRUE;
+               } else if (g_str_equal (uid, "custom-9")) {
+                       g_assert (expect_custom_9);
+                       g_assert (!have_custom_9);
+                       have_custom_9 = TRUE;
+               } else if (g_str_equal (uid, "simple-1")) {
+                       g_assert (expect_simple_1);
+                       g_assert (!have_simple_1);
+                       have_simple_1 = TRUE;
+               } else if (g_str_equal (uid, "simple-2")) {
+                       g_assert (expect_simple_2);
+                       g_assert (!have_simple_2);
+                       have_simple_2 = TRUE;
+               } else {
+                       /* It's not supposed to be NULL, but it will print the value of 'uid' */
+                       g_assert_cmpstr (uid, ==, NULL);
+               }
+       }
+
+       g_assert ((expect_custom_1 && have_custom_1) || (!expect_custom_1 && !have_custom_1));
+       g_assert ((expect_custom_9 && have_custom_9) || (!expect_custom_9 && !have_custom_9));
+       g_assert ((expect_simple_1 && have_simple_1) || (!expect_simple_1 && !have_simple_1));
+       g_assert ((expect_simple_2 && have_simple_2) || (!expect_simple_2 && !have_simple_2));
+       g_assert (have_custom_3);
+}
+
+static void
+test_basic_cursor (TCUFixture *fixture,
+                  guint32 flags,
+                  const gchar *sexp)
+{
+       EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+       EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING, E_BOOK_CURSOR_SORT_ASCENDING };
+       EBookCacheCursor *cursor;
+       gint total = -1, position = -1, expect_total;
+       GSList *list;
+       GError *error = NULL;
+
+       expect_total = 1 +
+               (((flags & EXPECT_CUSTOM_1) != 0) ? 1 : 0) +
+               (((flags & EXPECT_CUSTOM_9) != 0) ? 1 : 0) +
+               (((flags & EXPECT_SIMPLE_1) != 0) ? 1 : 0) +
+               (((flags & EXPECT_SIMPLE_2) != 0) ? 1 : 0);
+
+       cursor = e_book_cache_cursor_new (fixture->book_cache, sexp, sort_fields, sort_types, 2, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (cursor);
+
+       g_assert (e_book_cache_cursor_calculate (fixture->book_cache, cursor, &total, &position, NULL, 
&error));
+       g_assert_no_error (error);
+       g_assert_cmpint (total, ==, expect_total);
+       g_assert_cmpint (position, ==, 0);
+
+       g_assert_cmpint (e_book_cache_cursor_step (fixture->book_cache, cursor, 
E_BOOK_CACHE_CURSOR_STEP_FETCH,
+               E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT, total, &list, NULL, &error), ==, total);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, total);
+
+       test_check_search_result (list, flags | HAS_SEARCH_DATA);
+
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       e_book_cache_cursor_free (fixture->book_cache, cursor);
+}
+
+static void
+test_basic_search (TCUFixture *fixture,
+                  guint32 flags)
+{
+       EBookQuery *query;
+       GSList *list = NULL;
+       gchar *sexp;
+       gint expect_total;
+       GError *error = NULL;
+
+       expect_total = 2 +
+               ((flags & EXPECT_CUSTOM_1) != 0 ? 1 : 0) +
+               ((flags & EXPECT_SIMPLE_1) != 0 ? 1 : 0) +
+               ((flags & EXPECT_SIMPLE_2) != 0 ? 1 : 0);
+
+       /* All contacts first */
+       g_assert (e_book_cache_search (fixture->book_cache, NULL, FALSE, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, expect_total);
+       test_check_search_result (list, flags | EXPECT_CUSTOM_9 | HAS_SEARCH_DATA);
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_book_cache_search (fixture->book_cache, NULL, TRUE, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, expect_total);
+       test_check_search_result (list, flags | EXPECT_CUSTOM_9 | HAS_SEARCH_DATA | HAS_META_CONTACTS);
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_book_cache_search_uids (fixture->book_cache, NULL, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, expect_total);
+       test_check_search_result (list, flags | EXPECT_CUSTOM_9);
+       g_slist_free_full (list, g_free);
+       list = NULL;
+
+       test_basic_cursor (fixture, flags | EXPECT_CUSTOM_9, NULL);
+
+       /* Only Brown, aka custom-3, as an autocomplete query */
+       query = e_book_query_field_test (E_CONTACT_FULL_NAME, E_BOOK_QUERY_CONTAINS, "Brown");
+       sexp = e_book_query_to_string (query);
+       e_book_query_unref (query);
+
+       g_assert (e_book_cache_search (fixture->book_cache, sexp, FALSE, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, HAS_SEARCH_DATA);
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_book_cache_search (fixture->book_cache, sexp, TRUE, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, HAS_SEARCH_DATA | HAS_META_CONTACTS);
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_book_cache_search_uids (fixture->book_cache, sexp, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, EXPECT_DEFAULT);
+       g_slist_free_full (list, g_free);
+       list = NULL;
+
+       test_basic_cursor (fixture, EXPECT_DEFAULT, sexp);
+
+       g_free (sexp);
+
+       /* Only Brown, aka custom-3, as a regular query */
+       query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_CONTAINS, "brown");
+       sexp = e_book_query_to_string (query);
+       e_book_query_unref (query);
+
+       g_assert (e_book_cache_search (fixture->book_cache, sexp, FALSE, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, HAS_SEARCH_DATA);
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_book_cache_search (fixture->book_cache, sexp, TRUE, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, HAS_SEARCH_DATA | HAS_META_CONTACTS);
+       g_slist_free_full (list, e_book_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_book_cache_search_uids (fixture->book_cache, sexp, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, EXPECT_DEFAULT);
+       g_slist_free_full (list, g_free);
+       list = NULL;
+
+       test_basic_cursor (fixture, EXPECT_DEFAULT, sexp);
+
+       g_free (sexp);
+
+       /* Invalid expression */
+       g_assert (!e_book_cache_search (fixture->book_cache, "invalid expression here", TRUE, &list, NULL, 
&error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+       g_assert_null (list);
+       g_clear_error (&error);
+
+       g_assert (!e_book_cache_search_uids (fixture->book_cache, "invalid expression here", &list, NULL, 
&error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+       g_assert_null (list);
+       g_clear_error (&error);
+}
+
+/* Expects pairs of UID (gchar *) and EOfflineState (gint), terminated by NULL */
+static void
+test_check_offline_changes (TCUFixture *fixture,
+                           ...) G_GNUC_NULL_TERMINATED;
+
+static void
+test_check_offline_changes (TCUFixture *fixture,
+                           ...)
+{
+       GSList *changes, *link;
+       va_list args;
+       GHashTable *expects;
+       const gchar *uid;
+       GError *error = NULL;
+
+       changes = e_cache_get_offline_changes (E_CACHE (fixture->book_cache), NULL, &error);
+
+       g_assert_no_error (error);
+
+       expects = g_hash_table_new (g_str_hash, g_str_equal);
+
+       va_start (args, fixture);
+       uid = va_arg (args, const gchar *);
+       while (uid) {
+               gint state = va_arg (args, gint);
+
+               g_hash_table_insert (expects, (gpointer) uid, GINT_TO_POINTER (state));
+               uid = va_arg (args, const gchar *);
+       }
+       va_end (args);
+
+       g_assert_cmpint (g_slist_length (changes), ==, g_hash_table_size (expects));
+
+       for (link = changes; link; link = g_slist_next (link)) {
+               ECacheOfflineChange *change = link->data;
+               gint expect_state;
+
+               g_assert_nonnull (change);
+               g_assert (g_hash_table_contains (expects, change->uid));
+
+               expect_state = GPOINTER_TO_INT (g_hash_table_lookup (expects, change->uid));
+               g_assert_cmpint (expect_state, ==, change->state);
+       }
+
+       g_slist_free_full (changes, e_cache_offline_change_free);
+       g_hash_table_destroy (expects);
+}
+
+static EOfflineState
+test_check_offline_state (TCUFixture *fixture,
+                         const gchar *uid,
+                         EOfflineState expect_offline_state)
+{
+       EOfflineState offline_state;
+       GError *error = NULL;
+
+       offline_state = e_cache_get_offline_state (E_CACHE (fixture->book_cache), uid, NULL, &error);
+       g_assert_cmpint (offline_state, ==, expect_offline_state);
+
+       if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_clear_error (&error);
+       } else {
+               g_assert_no_error (error);
+       }
+
+       return offline_state;
+}
+
+static void
+test_check_edit_saved (TCUFixture *fixture,
+                      const gchar *uid,
+                      const gchar *rev_value)
+{
+       EContact *contact = NULL;
+       GError *error = NULL;
+
+       g_assert (e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_nonnull (contact);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_REV), ==, rev_value);
+
+       g_clear_object (&contact);
+
+       g_assert (e_book_cache_get_contact (fixture->book_cache, uid, TRUE, &contact, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_nonnull (contact);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_REV), ==, rev_value);
+
+       g_clear_object (&contact);
+}
+
+static void
+test_verify_storage (TCUFixture *fixture,
+                    const gchar *uid,
+                    const gchar *expect_rev,
+                    const gchar *expect_extra,
+                    EOfflineState expect_offline_state)
+{
+       EContact *contact = NULL;
+       EOfflineState offline_state;
+       gchar *vcard, *saved_rev = NULL, *saved_extra = NULL;
+       GError *error = NULL;
+
+       if (expect_offline_state == E_OFFLINE_STATE_LOCALLY_DELETED ||
+           expect_offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               g_assert (!e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, 
&error));
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_assert_null (contact);
+
+               g_clear_error (&error);
+       } else {
+               g_assert (e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, &error));
+               g_assert_no_error (error);
+               g_assert_nonnull (contact);
+       }
+
+       offline_state = test_check_offline_state (fixture, uid, expect_offline_state);
+
+       if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               g_assert (!e_cache_contains (E_CACHE (fixture->book_cache), uid, E_CACHE_EXCLUDE_DELETED));
+               g_assert (!e_cache_contains (E_CACHE (fixture->book_cache), uid, E_CACHE_INCLUDE_DELETED));
+               test_check_offline_changes (fixture, NULL);
+               return;
+       }
+
+       g_assert (e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpstr (saved_extra, ==, expect_extra);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_REV), ==, expect_rev);
+
+       g_clear_object (&contact);
+
+       vcard = e_cache_get (E_CACHE (fixture->book_cache), uid, &saved_rev, NULL, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (vcard);
+       g_assert_nonnull (saved_rev);
+
+       g_assert_cmpstr (saved_rev, ==, expect_rev);
+
+       g_free (vcard);
+       g_free (saved_rev);
+       g_free (saved_extra);
+
+       if (expect_offline_state == E_OFFLINE_STATE_SYNCED)
+               test_check_offline_changes (fixture, NULL);
+       else
+               test_check_offline_changes (fixture, uid, expect_offline_state, NULL);
+}
+
+static void
+test_offline_basics (TCUFixture *fixture,
+                    gconstpointer user_data)
+{
+       EOfflineState states[] = {
+               E_OFFLINE_STATE_LOCALLY_CREATED,
+               E_OFFLINE_STATE_LOCALLY_MODIFIED,
+               E_OFFLINE_STATE_LOCALLY_DELETED,
+               E_OFFLINE_STATE_SYNCED
+       };
+       EContact *contact = NULL;
+       gint ii;
+       const gchar *uid;
+       gchar *saved_extra = NULL, *tmp;
+       GError *error = NULL;
+
+       /* Basic ECache stuff */
+       e_cache_set_version (E_CACHE (fixture->book_cache), 123);
+       g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->book_cache)), ==, 123);
+
+       e_cache_set_revision (E_CACHE (fixture->book_cache), "rev-321");
+       tmp = e_cache_dup_revision (E_CACHE (fixture->book_cache));
+       g_assert_cmpstr ("rev-321", ==, tmp);
+       g_free (tmp);
+
+       g_assert (e_cache_set_key (E_CACHE (fixture->book_cache), "my-key-str", "key-str-value", &error));
+       g_assert_no_error (error);
+
+       tmp = e_cache_dup_key (E_CACHE (fixture->book_cache), "my-key-str", &error);
+       g_assert_no_error (error);
+       g_assert_cmpstr ("key-str-value", ==, tmp);
+       g_free (tmp);
+
+       g_assert (e_cache_set_key_int (E_CACHE (fixture->book_cache), "version", 567, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_key_int (E_CACHE (fixture->book_cache), "version", &error), ==, 567);
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->book_cache)), ==, 123);
+
+       /* Add in online */
+       test_fill_cache (fixture, &contact);
+       g_assert_nonnull (contact);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       g_assert (e_book_cache_set_contact_extra (fixture->book_cache, uid, "extra-0", NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert (e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpstr (saved_extra, ==, "extra-0");
+
+       g_free (saved_extra);
+       saved_extra = NULL;
+
+       e_contact_set (contact, E_CONTACT_REV, "rev-0");
+
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Try change status */
+       for (ii = 0; ii < G_N_ELEMENTS (states); ii++) {
+               g_assert (e_cache_set_offline_state (E_CACHE (fixture->book_cache), uid, states[ii], NULL, 
&error));
+               g_assert_no_error (error);
+
+               test_check_offline_state (fixture, uid, states[ii]);
+
+               if (states[ii] != E_OFFLINE_STATE_SYNCED)
+                       test_check_offline_changes (fixture, uid, states[ii], NULL);
+
+               if (states[ii] == E_OFFLINE_STATE_LOCALLY_DELETED) {
+                       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), 
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 2);
+                       g_assert_no_error (error);
+
+                       g_assert (!e_cache_contains (E_CACHE (fixture->book_cache), uid, 
E_CACHE_EXCLUDE_DELETED));
+
+                       g_assert (e_book_cache_set_contact_extra (fixture->book_cache, uid, "extra-1", NULL, 
&error));
+                       g_assert_no_error (error);
+
+                       g_assert (e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, 
NULL, &error));
+                       g_assert_no_error (error);
+                       g_assert_cmpstr (saved_extra, ==, "extra-1");
+
+                       g_free (saved_extra);
+                       saved_extra = NULL;
+
+                       /* Search when locally deleted */
+                       test_basic_search (fixture, EXPECT_DEFAULT);
+               } else {
+                       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), 
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 3);
+                       g_assert_no_error (error);
+
+                       g_assert (e_cache_contains (E_CACHE (fixture->book_cache), uid, 
E_CACHE_EXCLUDE_DELETED));
+
+                       /* Search when locally available */
+                       test_basic_search (fixture, EXPECT_CUSTOM_1);
+               }
+
+               g_assert (e_cache_contains (E_CACHE (fixture->book_cache), uid, E_CACHE_INCLUDE_DELETED));
+
+               g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, 
NULL, &error), ==, 3);
+               g_assert_no_error (error);
+       }
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Edit in online */
+       e_contact_set (contact, E_CONTACT_REV, "rev-1");
+
+       g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_ONLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_verify_storage (fixture, uid, "rev-1", NULL, E_OFFLINE_STATE_SYNCED);
+       test_check_offline_changes (fixture, NULL);
+
+       e_contact_set (contact, E_CONTACT_REV, "rev-2");
+
+       g_assert (e_book_cache_put_contact (fixture->book_cache, contact, "extra-2", E_CACHE_IS_ONLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_verify_storage (fixture, uid, "rev-2", "extra-2", E_OFFLINE_STATE_SYNCED);
+       test_check_offline_changes (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       /* Search before delete */
+       test_basic_search (fixture, EXPECT_CUSTOM_1);
+
+       /* Delete in online */
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_ONLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert (!e_cache_set_offline_state (E_CACHE (fixture->book_cache), uid, 
E_OFFLINE_STATE_LOCALLY_MODIFIED, NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_clear_error (&error);
+
+       test_verify_storage (fixture, uid, NULL, NULL, E_OFFLINE_STATE_UNKNOWN);
+       test_check_offline_changes (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_no_error (error);
+
+       g_assert (!e_book_cache_set_contact_extra (fixture->book_cache, uid, "extra-3", NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_clear_error (&error);
+
+       g_assert (!e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_assert_null (saved_extra);
+       g_clear_error (&error);
+
+       g_clear_object (&contact);
+
+       /* Search after delete */
+       test_basic_search (fixture, EXPECT_DEFAULT);
+}
+
+static void
+test_offline_add_one (TCUFixture *fixture,
+                     const gchar *case_name,
+                     gint expect_total,
+                     guint32 flags,
+                     EContact **out_contact)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       if (!(flags & SKIP_CONTACT_PUT)) {
+               contact = tcu_new_contact_from_test_case (case_name);
+               g_assert_nonnull (contact);
+
+               uid = e_contact_get_const (contact, E_CONTACT_UID);
+               g_assert_nonnull (uid);
+
+               test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+               /* Add a contact in offline */
+               g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, 
NULL, &error));
+               g_assert_no_error (error);
+       } else {
+               uid = case_name;
+       }
+
+       if ((flags & EXPECT_SIMPLE_1) != 0) {
+               test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_CREATED);
+       } else {
+               test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+       }
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, expect_total);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, flags);
+
+       if (out_contact)
+               *out_contact = contact;
+       else
+               g_clear_object (&contact);
+}
+
+static void
+test_offline_add (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add the first in offline */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       /* Add the second in offline */
+       test_offline_add_one (fixture, "simple-2", 5, EXPECT_SIMPLE_1 | EXPECT_SIMPLE_2 | EXPECT_CUSTOM_1, 
NULL);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               "simple-2", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+}
+
+static void
+test_offline_add_edit (TCUFixture *fixture,
+                      gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, &contact);
+       g_assert_nonnull (contact);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       /* Modify added in offline */
+       e_contact_set (contact, E_CONTACT_REV, "rev-2");
+
+       g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1 | SKIP_CONTACT_PUT, 
NULL);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       test_check_edit_saved (fixture, "simple-1", "rev-2");
+
+       g_clear_object (&contact);
+}
+
+static void
+test_offline_add_delete (TCUFixture *fixture,
+                        gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, &contact);
+       g_assert_nonnull (contact);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       /* Delete added in offline */
+
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       test_offline_add_one (fixture, "simple-1", 3, EXPECT_CUSTOM_1 | SKIP_CONTACT_PUT, NULL);
+
+       test_check_offline_changes (fixture, NULL);
+
+       g_clear_object (&contact);
+}
+
+static void
+test_offline_add_delete_add (TCUFixture *fixture,
+                            gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, &contact);
+       g_assert_nonnull (contact);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       /* Delete added in offline */
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       test_offline_add_one (fixture, "simple-1", 3, EXPECT_CUSTOM_1 | SKIP_CONTACT_PUT, NULL);
+
+       test_check_offline_changes (fixture, NULL);
+
+       g_clear_object (&contact);
+
+       /* Add in offline again */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+}
+
+static void
+test_offline_add_resync (TCUFixture *fixture,
+                        gconstpointer user_data)
+{
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       /* Resync all offline changes */
+       g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->book_cache), NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 4);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_SYNCED);
+}
+
+static void
+test_offline_edit_common (TCUFixture *fixture,
+                         gchar **out_uid)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &contact);
+       g_assert_nonnull (contact);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Modify in offline */
+       e_contact_set (contact, E_CONTACT_REV, "rev-2");
+
+       g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_edit_saved (fixture, uid, "rev-2");
+
+       test_basic_search (fixture, EXPECT_CUSTOM_1);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+
+       if (out_uid)
+               *out_uid = g_strdup (uid);
+
+       g_clear_object (&contact);
+}
+
+static void
+test_offline_edit (TCUFixture *fixture,
+                  gconstpointer user_data)
+{
+       test_offline_edit_common (fixture, NULL);
+}
+
+static void
+test_offline_edit_delete (TCUFixture *fixture,
+                         gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       gchar *uid = NULL;
+       GError *error = NULL;
+
+       test_offline_edit_common (fixture, &uid);
+
+       /* Delete the modified contact in offline */
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       g_assert (!e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_assert_null (contact);
+
+       g_clear_error (&error);
+       g_free (uid);
+}
+
+static void
+test_offline_edit_resync (TCUFixture *fixture,
+                         gconstpointer user_data)
+{
+       gchar *uid = NULL;
+       GError *error = NULL;
+
+       test_offline_edit_common (fixture, &uid);
+
+       /* Resync all offline changes */
+       g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->book_cache), NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_CUSTOM_1);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       g_free (uid);
+}
+
+static void
+test_offline_delete (TCUFixture *fixture,
+                    gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &contact);
+       g_assert_nonnull (contact);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Delete in offline */
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       g_clear_object (&contact);
+}
+
+static void
+test_offline_delete_add (TCUFixture *fixture,
+                        gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &contact);
+       g_assert_nonnull (contact);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Delete locally created in offline */
+       test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, "simple-1", E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_CUSTOM_1);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+       test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_UNKNOWN);
+
+       /* Delete synced in offline */
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       /* Add one in offline */
+       test_offline_add_one (fixture, "simple-1", 3, EXPECT_SIMPLE_1, NULL);
+
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+       test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+       /* Modify the previous contact and add it again */
+       e_contact_set (contact, E_CONTACT_REV, "rev-3");
+
+       g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 4);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 4);
+       g_assert_no_error (error);
+
+       test_check_edit_saved (fixture, uid, "rev-3");
+
+       test_basic_search (fixture, EXPECT_CUSTOM_1 | EXPECT_SIMPLE_1);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+               "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+       test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+       g_clear_object (&contact);
+}
+
+static void
+test_offline_delete_resync (TCUFixture *fixture,
+                           gconstpointer user_data)
+{
+       EContact *contact = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &contact);
+       g_assert_nonnull (contact);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Delete in offline */
+       g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       /* Resync all offline changes */
+       e_cache_clear_offline_changes (E_CACHE (fixture->book_cache), NULL, &error);
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+       g_clear_object (&contact);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUClosure closure = { NULL };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       g_test_add ("/EBookCache/Offline/Basics", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_basics, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/Add", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/AddEdit", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_edit, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/AddDelete", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_delete, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/AddDeleteAdd", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_delete_add, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/AddResync", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_resync, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/Edit", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_edit, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/EditDelete", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_edit_delete, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/EditResync", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_edit_resync, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/Delete", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_delete, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/DeleteAdd", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_delete_add, tcu_fixture_teardown);
+       g_test_add ("/EBookCache/Offline/DeleteResync", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_delete_resync, tcu_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-utils.c b/tests/libedata-book/test-book-cache-utils.c
new file mode 100644
index 0000000..5db1684
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-utils.c
@@ -0,0 +1,695 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013, Openismus GmbH
+ *
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#include "evolution-data-server-config.h"
+
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include "test-book-cache-utils.h"
+
+gchar *
+tcu_new_vcard_from_test_case (const gchar *case_name)
+{
+       gchar *filename;
+       gchar *case_filename;
+       GFile * file;
+       GError *error = NULL;
+       gchar *vcard;
+
+       case_filename = g_strdup_printf ("%s.vcf", case_name);
+
+       /* In the case of installed tests, they run in ${pkglibexecdir}/installed-tests
+        * and the vcards are installed in ${pkglibexecdir}/installed-tests/vcards
+        */
+       if (g_getenv ("TEST_INSTALLED_SERVICES") != NULL)
+               filename = g_build_filename (INSTALLED_TEST_DIR, "vcards", case_filename, NULL);
+       else
+               filename = g_build_filename (SRCDIR, "..", "libebook", "data", "vcards", case_filename, NULL);
+
+       file = g_file_new_for_path (filename);
+       if (!g_file_load_contents (file, NULL, &vcard, NULL, NULL, &error))
+               g_error (
+                       "failed to read test contact file '%s': %s",
+                       filename, error->message);
+
+       g_free (case_filename);
+       g_free (filename);
+       g_object_unref (file);
+
+       return vcard;
+}
+
+EContact *
+tcu_new_contact_from_test_case (const gchar *case_name)
+{
+       gchar *vcard;
+       EContact *contact = NULL;
+
+       vcard = tcu_new_vcard_from_test_case (case_name);
+       if (vcard)
+               contact = e_contact_new_from_vcard (vcard);
+       g_free (vcard);
+
+       if (!contact)
+               g_error (
+                       "failed to construct contact from test case '%s'",
+                       case_name);
+
+       return contact;
+}
+
+void
+tcu_add_contact_from_test_case (TCUFixture *fixture,
+                               const gchar *case_name,
+                               EContact **ret_contact)
+{
+       EContact *contact;
+       GError *error = NULL;
+
+       contact = tcu_new_contact_from_test_case (case_name);
+
+       if (!e_book_cache_put_contact (fixture->book_cache, contact, case_name, E_CACHE_IS_ONLINE, NULL, 
&error))
+               g_error ("Failed to add contact: %s", error->message);
+
+       if (ret_contact)
+               *ret_contact = g_object_ref (contact);
+
+       g_clear_object (&contact);
+}
+
+static void
+delete_work_directory (const gchar *filename)
+{
+       /* XXX Instead of complex error checking here, we should ideally use
+        * a recursive GDir / g_unlink() function.
+        *
+        * We cannot use GFile and the recursive delete function without
+        * corrupting our contained D-Bus environment with service files
+        * from the OS.
+        */
+       const gchar *argv[] = { "/bin/rm", "-rf", filename, NULL };
+       gboolean spawn_succeeded;
+       gint exit_status;
+
+       spawn_succeeded = g_spawn_sync (
+               NULL, (gchar **) argv, NULL, 0, NULL, NULL,
+                                       NULL, NULL, &exit_status, NULL);
+
+       g_assert (spawn_succeeded);
+       #ifndef G_OS_WIN32
+       g_assert (WIFEXITED (exit_status));
+       g_assert_cmpint (WEXITSTATUS (exit_status), ==, 0);
+       #else
+       g_assert_cmpint (exit_status, ==, 0);
+       #endif
+}
+
+ESourceBackendSummarySetup *
+tcu_setup_empty_book (void)
+{
+       ESourceBackendSummarySetup *setup;
+       ESource *scratch;
+       GError *error = NULL;
+
+       scratch = e_source_new_with_uid ("test-source", NULL, &error);
+       if (!scratch)
+               g_error ("Error creating scratch source: %s", error ? error->message : "Unknown error");
+
+       /* This is a bit of a cheat */
+       setup = g_object_new (E_TYPE_SOURCE_BACKEND_SUMMARY_SETUP, "source", scratch, NULL);
+       e_source_backend_summary_setup_set_summary_fields (
+               setup,
+               /* We don't use this field in our tests anyway */
+               E_CONTACT_FILE_AS,
+               0);
+
+       g_object_unref (scratch);
+
+       return setup;
+}
+
+static void
+e164_changed_cb (EBookCache *book_cache,
+                EContact *contact,
+                gboolean is_replace,
+                gpointer user_data)
+{
+       TCUFixture *fixture = user_data;
+
+       if (is_replace)
+               fixture->n_locale_changes++;
+       else
+               fixture->n_add_changes++;
+}
+
+void
+tcu_fixture_setup (TCUFixture *fixture,
+                  gconstpointer user_data)
+{
+       TCUClosure *closure = (TCUClosure *) user_data;
+       ESourceBackendSummarySetup *setup = NULL;
+       gchar *filename, *directory;
+       GError *error = NULL;
+
+       if (!g_file_test (CAMEL_PROVIDERDIR, G_FILE_TEST_IS_DIR | G_FILE_TEST_EXISTS)) {
+               if (g_mkdir_with_parents (CAMEL_PROVIDERDIR, 0700) == -1)
+                       g_warning ("%s: Failed to create folder '%s': %s\n", G_STRFUNC, CAMEL_PROVIDERDIR, 
g_strerror (errno));
+       }
+
+       /* Cleanup from last test */
+       directory = g_build_filename (g_get_tmp_dir (), "test-book-cache", NULL);
+       delete_work_directory (directory);
+       g_free (directory);
+       filename = g_build_filename (g_get_tmp_dir (), "test-book-cache", "cache.db", NULL);
+
+       if (closure->setup_summary)
+               setup = closure->setup_summary ();
+
+       fixture->book_cache = e_book_cache_new_full (filename, NULL, setup, NULL, &error);
+
+       g_clear_object (&setup);
+
+       if (!fixture->book_cache)
+               g_error ("Failed to create the EBookCache: %s", error->message);
+
+       g_free (filename);
+
+       g_signal_connect (fixture->book_cache, "e164-changed",
+               G_CALLBACK (e164_changed_cb), fixture);
+}
+
+void
+tcu_fixture_teardown (TCUFixture *fixture,
+                     gconstpointer user_data)
+{
+       g_object_unref (fixture->book_cache);
+}
+
+void
+tcu_cursor_fixture_setup (TCUCursorFixture *fixture,
+                         gconstpointer user_data)
+{
+       TCUFixture *base_fixture = (TCUFixture   *) fixture;
+       TCUCursorClosure *data = (TCUCursorClosure *) user_data;
+       EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+       EBookCursorSortType sort_types[] = { data->sort_type, data->sort_type };
+       GSList *contacts = NULL;
+       GSList *extra_list = NULL;
+       GError *error = NULL;
+       gint ii;
+       gchar *sexp = NULL;
+
+       tcu_fixture_setup (base_fixture, user_data);
+
+       if (data->locale)
+               tcu_cursor_fixture_set_locale (fixture, data->locale);
+       else
+               tcu_cursor_fixture_set_locale (fixture, "en_US.UTF-8");
+
+       for (ii = 0; ii < N_SORTED_CONTACTS; ii++) {
+               gchar *case_name = g_strdup_printf ("sorted-%d", ii + 1);
+               gchar *vcard;
+               EContact *contact;
+
+               vcard = tcu_new_vcard_from_test_case (case_name);
+               contact = e_contact_new_from_vcard (vcard);
+               contacts = g_slist_prepend (contacts, contact);
+               extra_list = g_slist_prepend (extra_list, case_name);
+
+               g_free (vcard);
+
+               fixture->contacts[ii] = g_object_ref (contact);
+       }
+
+       if (!e_book_cache_put_contacts (base_fixture->book_cache, contacts, extra_list, E_CACHE_IS_ONLINE, 
NULL, &error)) {
+               /* Dont complain here, we re-use the same addressbook for multiple tests
+                * and we can't add the same contacts twice
+                */
+               if (g_error_matches (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT))
+                       g_clear_error (&error);
+               else
+                       g_error ("Failed to add test contacts: %s", error->message);
+       }
+
+       g_slist_free_full (contacts, g_object_unref);
+       g_slist_free_full (extra_list, g_free);
+
+       /* Allow a surrounding fixture setup to add a query here */
+       if (fixture->query) {
+               sexp = e_book_query_to_string (fixture->query);
+               e_book_query_unref (fixture->query);
+               fixture->query = NULL;
+       }
+
+       fixture->cursor = e_book_cache_cursor_new (
+               base_fixture->book_cache, sexp,
+               sort_fields, sort_types, 2, &error);
+
+       if (!fixture->cursor)
+               g_error ("Failed to create cursor: %s\n", error->message);
+
+       g_free (sexp);
+}
+
+void
+tcu_cursor_fixture_filtered_setup (TCUCursorFixture *fixture,
+                                  gconstpointer user_data)
+{
+       fixture->query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_ENDS_WITH, ".com");
+
+       tcu_cursor_fixture_setup (fixture, user_data);
+}
+
+void
+tcu_cursor_fixture_teardown (TCUCursorFixture *fixture,
+                            gconstpointer user_data)
+{
+       TCUFixture *base_fixture = (TCUFixture   *) fixture;
+       gint ii;
+
+       for (ii = 0; ii < N_SORTED_CONTACTS; ii++) {
+               if (fixture->contacts[ii])
+                       g_object_unref (fixture->contacts[ii]);
+       }
+
+       e_book_cache_cursor_free (base_fixture->book_cache, fixture->cursor);
+       tcu_fixture_teardown (base_fixture, user_data);
+}
+
+void
+tcu_cursor_fixture_set_locale (TCUCursorFixture *fixture,
+                              const gchar *locale)
+{
+       TCUFixture *base_fixture = (TCUFixture   *) fixture;
+       GError *error = NULL;
+
+       if (!e_book_cache_set_locale (base_fixture->book_cache, locale, NULL, &error))
+               g_error ("Failed to set locale: %s", error->message);
+}
+
+static gint
+find_contact_data (EBookCacheSearchData *data,
+                   const gchar *uid)
+{
+       return g_strcmp0 (data->uid, uid);
+}
+
+void
+tcu_assert_contacts_order_slist (GSList *results,
+                                GSList *uids)
+{
+       gint position = -1;
+       GSList *link, *l;
+
+       /* Assert that all passed UIDs are found in the
+        * results, and that those UIDs are in the
+        * specified order.
+        */
+       for (l = uids; l; l = l->next) {
+               const gchar *uid = l->data;
+               gint new_position;
+
+               link = g_slist_find_custom (results, uid, (GCompareFunc) find_contact_data);
+               if (!link)
+                       g_error ("Specified uid '%s' was not found in results", uid);
+
+               new_position = g_slist_position (results, link);
+               g_assert_cmpint (new_position, >, position);
+               position = new_position;
+       }
+}
+
+void
+tcu_assert_contacts_order (GSList *results,
+                          const gchar *first_uid,
+                          ...)
+{
+       GSList *uids = NULL;
+       gchar *uid;
+       va_list args;
+
+       g_assert (first_uid);
+
+       uids = g_slist_append (uids, (gpointer) first_uid);
+
+       va_start (args, first_uid);
+       uid = va_arg (args, gchar *);
+       while (uid) {
+               uids = g_slist_append (uids, uid);
+               uid = va_arg (args, gchar *);
+       }
+       va_end (args);
+
+       tcu_assert_contacts_order_slist (results, uids);
+       g_slist_free (uids);
+}
+
+void
+tcu_print_results (const GSList *results)
+{
+       const GSList *link;
+
+       if (g_getenv ("TEST_DEBUG") == NULL)
+               return;
+
+       g_print ("\nPRINTING RESULTS:\n");
+
+       for (link = results; link; link = link->next) {
+               EBookCacheSearchData *data = link->data;
+
+               g_print ("\n%s\n", data->vcard);
+       }
+
+       g_print ("\nRESULT LIST_FINISHED\n");
+}
+
+/********************************************
+ *           Move By Test Helpers
+ ********************************************/
+#define DEBUG_FIXTURE        0
+
+static TCUStepData *
+step_test_new_internal (const gchar *test_path,
+                        const gchar *locale,
+                        gboolean empty_book)
+{
+       TCUStepData *data;
+
+       data = g_slice_new0 (TCUStepData);
+
+       data->parent.locale = g_strdup (locale);
+       data->parent.sort_type = E_BOOK_CURSOR_SORT_ASCENDING;
+
+       if (empty_book)
+               data->parent.parent.setup_summary = tcu_setup_empty_book;
+
+       data->path = g_strdup (test_path);
+
+       return data;
+}
+
+static void
+step_test_free (TCUStepData *data)
+{
+       GList *l;
+
+       g_free (data->path);
+       g_free ((gchar *) data->parent.locale);
+
+       for (l = data->assertions; l; l = l->next) {
+               TCUStepAssertion *assertion = l->data;
+
+               g_free (assertion->locale);
+               g_slice_free (TCUStepAssertion, assertion);
+       }
+
+       g_list_free (data->assertions);
+
+       g_slice_free (TCUStepData, data);
+}
+
+TCUStepData *
+tcu_step_test_new (const gchar *test_prefix,
+                  const gchar *test_path,
+                  const gchar *locale,
+                  gboolean empty_book)
+{
+       TCUStepData *data;
+       gchar *path;
+
+       path = g_strconcat (test_prefix, test_path, NULL);
+       data = step_test_new_internal (path, locale, empty_book);
+       g_free (path);
+
+       return data;
+}
+
+TCUStepData *
+tcu_step_test_new_full (const gchar *test_prefix,
+                       const gchar *test_path,
+                       const gchar *locale,
+                       gboolean empty_book,
+                       EBookCursorSortType sort_type)
+{
+       TCUStepData *data;
+       gchar *path;
+
+       path = g_strconcat (test_prefix, test_path, NULL);
+       data = step_test_new_internal (path, locale, empty_book);
+       data->parent.sort_type = sort_type;
+       g_free (path);
+
+       return data;
+}
+
+static void
+test_cursor_move_teardown (TCUCursorFixture *fixture,
+                          gconstpointer user_data)
+{
+       TCUStepData *data = (TCUStepData *) user_data;
+
+       tcu_cursor_fixture_teardown (fixture, user_data);
+       step_test_free (data);
+}
+
+static void
+assert_step (TCUCursorFixture *fixture,
+            TCUStepData *data,
+            TCUStepAssertion *assertion,
+            GSList *results,
+            gint n_results,
+            gboolean expect_results)
+{
+       GSList *uids = NULL;
+       gint ii, expected = 0;
+
+       /* Count the number of really expected results */
+       for (ii = 0; ii < ABS (assertion->count); ii++) {
+               gint index = assertion->expected[ii];
+
+               if (index < 0)
+                       break;
+
+               expected++;
+       }
+
+       g_assert_cmpint (n_results, ==, expected);
+       if (!expect_results) {
+               g_assert_cmpint (g_slist_length (results), ==, 0);
+               return;
+       }
+
+       /* Assert the exact amount of requested results */
+       g_assert_cmpint (g_slist_length (results), ==, expected);
+
+#if DEBUG_FIXTURE
+       g_print (
+               "%s: Constructing expected result list for a fetch of %d: ",
+               data->path, assertion->count);
+#endif
+       for (ii = 0; ii < ABS (assertion->count); ii++) {
+               gint index = assertion->expected[ii];
+               gchar *uid;
+
+               if (index < 0)
+                       break;
+
+               uid = (gchar *) e_contact_get_const (fixture->contacts[index], E_CONTACT_UID);
+               uids = g_slist_append (uids, uid);
+
+#if DEBUG_FIXTURE
+               g_print ("%s ", uid);
+#endif
+
+       }
+#if DEBUG_FIXTURE
+       g_print ("\n");
+#endif
+
+       tcu_assert_contacts_order_slist (results, uids);
+       g_slist_free (uids);
+}
+
+static void
+test_step (TCUCursorFixture *fixture,
+          gconstpointer user_data)
+{
+       TCUFixture *base_fixture = (TCUFixture   *) fixture;
+       TCUStepData *data = (TCUStepData *) user_data;
+       GSList *results = NULL;
+       GError *error = NULL;
+       gint n_results;
+       EBookCacheCursorOrigin origin;
+       GList *l;
+       gboolean reset = TRUE;
+
+       for (l = data->assertions; l; l = l->next) {
+               TCUStepAssertion *assertion = l->data;
+
+               if (assertion->locale) {
+                       gint n_locale_changes = base_fixture->n_locale_changes;
+
+                       if (!e_book_cache_set_locale (base_fixture->book_cache, assertion->locale, NULL, 
&error))
+                               g_error ("Failed to set locale: %s", error->message);
+
+                       n_locale_changes = (base_fixture->n_locale_changes - n_locale_changes);
+
+                       /* Only check for contact changes is phone numbers are supported,
+                        * contact changes only happen because of e164 number interpretations.
+                        */
+                       if (e_phone_number_is_supported () &&
+                           assertion->count != n_locale_changes)
+                               g_error ("Expected %d e164 numbers to change, %d actually changed.",
+                                       assertion->count, n_locale_changes);
+
+                       reset = TRUE;
+                       continue;
+               }
+
+               /* For the first call to e_book_cache_cursor_step(),
+               * or the first reset after locale change, set the origin accordingly.
+                */
+              if (reset) {
+                      if (assertion->count < 0)
+                              origin = E_BOOK_CACHE_CURSOR_ORIGIN_END;
+                      else
+                              origin = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+
+                      reset = FALSE;
+              } else {
+                      origin = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+              }
+
+               /* Try only fetching the contacts but not moving the cursor */
+               n_results = e_book_cache_cursor_step (
+                       base_fixture->book_cache,
+                       fixture->cursor,
+                       E_BOOK_CACHE_CURSOR_STEP_FETCH,
+                       origin,
+                       assertion->count,
+                       &results,
+                       NULL, &error);
+               if (n_results < 0)
+                       g_error ("Error fetching cursor results: %s", error->message);
+
+               tcu_print_results (results);
+               assert_step (fixture, data, assertion, results, n_results, TRUE);
+               g_slist_free_full (results, e_book_cache_search_data_free);
+               results = NULL;
+
+               /* Do it again, this time only moving the cursor */
+               n_results = e_book_cache_cursor_step (
+                       base_fixture->book_cache,
+                       fixture->cursor,
+                       E_BOOK_CACHE_CURSOR_STEP_MOVE,
+                       origin,
+                       assertion->count,
+                       &results,
+                       NULL, &error);
+               if (n_results < 0)
+                       g_error ("Error fetching cursor results: %s", error->message);
+
+               tcu_print_results (results);
+               assert_step (fixture, data, assertion, results, n_results, FALSE);
+               g_slist_free_full (results, e_book_cache_search_data_free);
+               results = NULL;
+       }
+}
+
+static void
+step_test_add_assertion_va_list (TCUStepData *data,
+                                gint count,
+                                va_list args)
+{
+       TCUStepAssertion *assertion = g_slice_new0 (TCUStepAssertion);
+       gint expected, ii = 0;
+
+       assertion->count = count;
+
+#if DEBUG_FIXTURE
+       g_print ("Adding assertion to test %d: %s\n", ii + 1, data->path);
+       g_print ("  Test will move by %d and expect: ", count);
+#endif
+       for (ii = 0; ii < ABS (count); ii++) {
+               expected = va_arg (args, gint);
+
+#if DEBUG_FIXTURE
+               g_print ("%d ", expected);
+#endif
+               assertion->expected[ii] = expected - 1;
+       }
+#if DEBUG_FIXTURE
+       g_print ("\n");
+#endif
+
+       data->assertions = g_list_append (data->assertions, assertion);
+}
+
+/* A positive of negative 'count' value
+ * followed by ABS (count) UID indexes.
+ *
+ * The indexes start at 1 so that they
+ * are easier to match up with the chart
+ * in data-test-utils.h
+ */
+void
+tcu_step_test_add_assertion (TCUStepData *data,
+                            gint count,
+                            ...)
+{
+       va_list args;
+
+       va_start (args, count);
+       step_test_add_assertion_va_list (data, count, args);
+       va_end (args);
+}
+
+void
+tcu_step_test_change_locale (TCUStepData *data,
+                            const gchar *locale,
+                            gint expected_changes)
+{
+       TCUStepAssertion *assertion = g_slice_new0 (TCUStepAssertion);
+
+       assertion->locale = g_strdup (locale);
+       assertion->count = expected_changes;
+       data->assertions = g_list_append (data->assertions, assertion);
+}
+
+void
+tcu_step_test_add (TCUStepData *data,
+                  gboolean filtered)
+{
+       data->filtered = filtered;
+
+       g_test_add (
+               data->path, TCUCursorFixture, data,
+               filtered ?
+               tcu_cursor_fixture_filtered_setup :
+               tcu_cursor_fixture_setup,
+               test_step,
+               test_cursor_move_teardown);
+}
diff --git a/tests/libedata-book/test-book-cache-utils.h b/tests/libedata-book/test-book-cache-utils.h
new file mode 100644
index 0000000..4718665
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-utils.h
@@ -0,0 +1,178 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013, Openismus GmbH
+ *
+ * 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/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#ifndef TEST_BOOK_CACHE_UTILS_H
+#define TEST_BOOK_CACHE_UTILS_H
+
+#include <libedata-book/libedata-book.h>
+
+G_BEGIN_DECLS
+
+/* This legend shows the add order, and various sort order of the sorted
+ * vcards. The UIDs of these contacts are formed as 'sorted-1', 'sorted-2' etc
+ * and the numbering of the contacts is according to the 'N' column in the
+ * following legend.
+ *
+ * The Email column indicates whether the contact has a .com email address
+ * (in order to test filtered cursor results) and corresponds to the natural
+ * order in the 'N' column.
+ *
+ * +-----------------------------------------------------------------------------------------------+
+ * | N   | Email | Last Name   | en_US_POSIX    | en_US / de_DE  | fr_CA          | de_DE          |
+ * |     |       |             |                |                |                | (phonebook)    |
+ * +-----------------------------------------------------------------------------------------------+
+ * | 1   | Yes   | bad         |             11 |             11 |             11 |             11 |
+ * | 2   | Yes   | Bad         | Bad         2  | bad         1  | bad         1  | bad         1  |
+ * | 3   | Yes   | Bat         | Bäd         6  | Bad         2  | Bad         2  | Bad         2  |
+ * | 4   | No    | bat         | Bat         3  | bäd         5  | bäd         5  | bäd         5  |
+ * | 5   | Yes   | bäd         | Bät         8  | Bäd         6  | Bäd         6  | Bäd         6  |
+ * | 6   | No    | Bäd         | bad         1  | bat         4  | bat         4  | bät         7  |
+ * | 7   | No    | bät         | bäd         5  | Bat         3  | Bat         3  | Bät         8  |
+ * | 8   | Yes   | Bät         | bat         4  | bät         7  | bät         7  | bat         4  |
+ * | 9   | Yes   | côté        | bät         7  | Bät         8  | Bät         8  | Bat         3  |
+ * | 10  | Yes   | C           | black-bird  15 | black-bird  15 | black-bird  15 | black-bird  15 |
+ * | 11  | Yes   |             | black-birds 17 | black-birds 17 | black-birds 17 | black-birds 17 |
+ * | 12  | Yes   | coté        | blackbird   16 | blackbird   16 | blackbird   16 | blackbird   16 |
+ * | 13  | No    | côte        | blackbirds  18 | blackbirds  18 | blackbirds  18 | blackbirds  18 |
+ * | 14  | Yes   | cote        | C           10 | C           10 | C           10 | C           10 |
+ * | 15  | No    | black-bird  | cote        14 | cote        14 | cote        14 | cote        14 |
+ * | 16  | Yes   | blackbird   | coté        12 | coté        12 | côte        13 | coté        12 |
+ * | 17  | Yes   | black-birds | côte        13 | côte        13 | coté        12 | côte        13 |
+ * | 18  | Yes   | blackbirds  | côté        9  | côté        9  | côté        9  | côté        9  |
+ * | 19  | No    | Muffler     | Muffler     19 | Muffler     19 | Muffler     19 | Müller      20 |
+ * | 20  | No    | Müller      | Müller      20 | Müller      20 | Müller      20 | Muffler     19 |
+ * +-----------------------------------------------------------------------------------------------+
+ *
+ * See this ICU demo to check additional sort ordering by ICU in various locales:
+ *     http://demo.icu-project.org/icu-bin/locexp?_=en_US&d_=en&x=col
+ */
+
+/* 13 contacts in the test data have an email address ending with ".com" */
+#define N_FILTERED_CONTACTS  13
+#define N_SORTED_CONTACTS    20
+
+typedef ESourceBackendSummarySetup * (* TCUSetupSummaryFunc) (void);
+
+typedef struct {
+       EBookCache *book_cache;
+
+       gint n_add_changes;
+       gint n_locale_changes;
+} TCUFixture;
+
+typedef struct {
+       TCUSetupSummaryFunc setup_summary;
+} TCUClosure;
+
+typedef struct {
+       TCUFixture parent_fixture;
+
+       EBookCacheCursor *cursor;
+       EContact *contacts[N_SORTED_CONTACTS];
+       EBookQuery *query;
+
+       guint own_id;
+} TCUCursorFixture;
+
+typedef struct {
+       TCUClosure parent;
+
+       const gchar *locale;
+       EBookCursorSortType sort_type;
+} TCUCursorClosure;
+
+typedef struct {
+       /* A locale change */
+       gchar *locale;
+
+       /* count argument for move */
+       gint count;
+
+       /* An array of 'ABS (counts[i])' expected contacts */
+       gint expected[N_SORTED_CONTACTS];
+} TCUStepAssertion;
+
+typedef struct {
+       TCUCursorClosure parent;
+       gchar *path;
+
+       GList *assertions;
+
+       /* Whether this is a filtered test */
+       gboolean filtered;
+} TCUStepData;
+
+/* Base fixture */
+void           tcu_fixture_setup                       (TCUFixture *fixture,
+                                                        gconstpointer user_data);
+void           tcu_fixture_teardown                    (TCUFixture *fixture,
+                                                        gconstpointer user_data);
+ESourceBackendSummarySetup *
+               tcu_setup_empty_book                    (void);
+
+/* Cursor fixture */
+void           tcu_cursor_fixture_setup                (TCUCursorFixture *fixture,
+                                                        gconstpointer user_data);
+void           tcu_cursor_fixture_teardown             (TCUCursorFixture *fixture,
+                                                        gconstpointer user_data);
+void           tcu_cursor_fixture_set_locale           (TCUCursorFixture *fixture,
+                                                        const gchar *locale);
+
+/* Filters contacts with E_CONTACT_EMAIL ending with '.com' */
+void           tcu_cursor_fixture_filtered_setup       (TCUCursorFixture *fixture,
+                                                        gconstpointer user_data);
+
+gchar *                tcu_new_vcard_from_test_case            (const gchar *case_name);
+EContact *     tcu_new_contact_from_test_case          (const gchar *case_name);
+
+void           tcu_add_contact_from_test_case          (TCUFixture *fixture,
+                                                        const gchar *case_name,
+                                                        EContact **ret_contact);
+void           tcu_assert_contacts_order_slist         (GSList *results,
+                                                        GSList *uids);
+void           tcu_assert_contacts_order               (GSList *results,
+                                                        const gchar *first_uid,
+                                                        ...) G_GNUC_NULL_TERMINATED;
+
+void           tcu_print_results                       (const GSList *results);
+
+/*  Step test helpers */
+void           tcu_step_test_add_assertion             (TCUStepData *data,
+                                                        gint count,
+                                                        ...);
+void           tcu_step_test_change_locale             (TCUStepData *data,
+                                                        const gchar *locale,
+                                                        gint expected_changes);
+
+TCUStepData *  tcu_step_test_new                       (const gchar *test_prefix,
+                                                        const gchar *test_path,
+                                                        const gchar *locale,
+                                                        gboolean empty_book);
+TCUStepData *  tcu_step_test_new_full                  (const gchar *test_prefix,
+                                                        const gchar *test_path,
+                                                        const gchar *locale,
+                                                        gboolean empty_book,
+                                                        EBookCursorSortType sort_type);
+
+void           tcu_step_test_add                       (TCUStepData *data,
+                                                        gboolean filtered);
+
+G_END_DECLS
+
+#endif /* TEST_BOOK_CACHE_UTILS_H */
diff --git a/tests/libedata-book/test-book-meta-backend.c b/tests/libedata-book/test-book-meta-backend.c
new file mode 100644
index 0000000..331032c
--- /dev/null
+++ b/tests/libedata-book/test-book-meta-backend.c
@@ -0,0 +1,1718 @@
+/* -*- 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/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include "libebook-contacts/libebook-contacts.h"
+
+#include "e-test-server-utils.h"
+#include "test-book-cache-utils.h"
+
+#define REMOTE_URL     "https://www.gnome.org/wp-content/themes/gnome-grass/images/gnome-logo.svg";
+#define MODIFIED_FN_STR        "Modified FN"
+
+typedef struct _EBookMetaBackendTest {
+       EBookMetaBackend parent;
+
+       GHashTable *contacts;
+
+       gint sync_tag_index;
+       gboolean can_connect;
+       gboolean is_connected;
+       gint connect_count;
+       gint list_count;
+       gint save_count;
+       gint load_count;
+       gint remove_count;
+} EBookMetaBackendTest;
+
+typedef struct _EBookMetaBackendTestClass {
+       EBookMetaBackendClass parent_class;
+} EBookMetaBackendTestClass;
+
+#define E_TYPE_BOOK_META_BACKEND_TEST (e_book_meta_backend_test_get_type ())
+#define E_BOOK_META_BACKEND_TEST(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_BOOK_META_BACKEND_TEST, EBookMetaBackendTest))
+#define E_IS_BOOK_META_BACKEND_TEST(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_BOOK_META_BACKEND_TEST))
+
+GType e_book_meta_backend_test_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (EBookMetaBackendTest, e_book_meta_backend_test, E_TYPE_BOOK_META_BACKEND)
+
+static void
+ebmb_test_add_test_case (EBookMetaBackendTest *test_backend,
+                        const gchar *case_name)
+{
+       EContact *contact;
+
+       g_assert_nonnull (test_backend);
+       g_assert_nonnull (case_name);
+
+       contact = tcu_new_contact_from_test_case (case_name);
+       g_assert_nonnull (contact);
+
+       g_hash_table_insert (test_backend->contacts, e_contact_get (contact, E_CONTACT_UID), contact);
+}
+
+static void
+ebmb_test_remove_component (EBookMetaBackendTest *test_backend,
+                           const gchar *uid)
+{
+       g_assert_nonnull (test_backend);
+       g_assert_nonnull (uid);
+
+       g_hash_table_remove (test_backend->contacts, uid);
+}
+
+static GHashTable * /* gchar * ~> NULL */
+ebmb_test_gather_uids (va_list args)
+{
+       GHashTable *expects;
+       const gchar *uid;
+
+       expects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+       uid = va_arg (args, const gchar *);
+       while (uid) {
+               g_hash_table_insert (expects, g_strdup (uid), NULL);
+               uid = va_arg (args, const gchar *);
+       }
+
+       return expects;
+}
+
+static void
+ebmb_test_hash_contains (GHashTable *contacts, /* gchar *uid ~> EContact * */
+                        gboolean negate,
+                        gboolean exact,
+                        ...) /* uid-s, ended with NULL */
+{
+       va_list args;
+       GHashTable *expects;
+       GHashTableIter iter;
+       gpointer uid;
+       guint ntotal;
+
+       g_return_if_fail (contacts != NULL);
+
+       va_start (args, exact);
+       expects = ebmb_test_gather_uids (args);
+       va_end (args);
+
+       ntotal = g_hash_table_size (expects);
+
+       g_hash_table_iter_init (&iter, contacts);
+       while (g_hash_table_iter_next (&iter, &uid, NULL)) {
+               if (exact) {
+                       if (negate)
+                               g_assert (!g_hash_table_remove (expects, uid));
+                       else
+                               g_assert (g_hash_table_remove (expects, uid));
+               } else {
+                       g_hash_table_remove (expects, uid);
+               }
+       }
+
+       if (negate)
+               g_assert_cmpint (g_hash_table_size (expects), ==, ntotal);
+       else
+               g_assert_cmpint (g_hash_table_size (expects), ==, 0);
+
+       g_hash_table_destroy (expects);
+}
+
+static void
+ebmb_test_cache_contains (EBookCache *book_cache,
+                         gboolean negate,
+                         gboolean exact,
+                         ...) /* uid-s, ended with NULL */
+{
+       va_list args;
+       GHashTable *expects;
+       GHashTableIter iter;
+       ECache *cache;
+       gpointer key;
+       gint found = 0;
+
+       g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+
+       va_start (args, exact);
+       expects = ebmb_test_gather_uids (args);
+       va_end (args);
+
+       cache = E_CACHE (book_cache);
+
+       g_hash_table_iter_init (&iter, expects);
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               const gchar *uid = key;
+
+               g_assert_nonnull (uid);
+
+               if (e_cache_contains (cache, uid, E_CACHE_EXCLUDE_DELETED))
+                       found++;
+       }
+
+       if (negate)
+               g_assert_cmpint (0, ==, found);
+       else
+               g_assert_cmpint (g_hash_table_size (expects), ==, found);
+
+       g_hash_table_destroy (expects);
+
+       if (exact && !negate)
+               g_assert_cmpint (e_cache_get_count (cache, E_CACHE_EXCLUDE_DELETED, NULL, NULL), ==, found);
+}
+
+static void
+ebmb_test_cache_and_server_equal (EBookCache *book_cache,
+                                 GHashTable *contacts,
+                                 ECacheDeletedFlag deleted_flag)
+{
+       ECache *cache;
+       GHashTableIter iter;
+       gpointer uid;
+
+       g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+       g_return_if_fail (contacts != NULL);
+
+       cache = E_CACHE (book_cache);
+
+       g_assert_cmpint (e_cache_get_count (cache, deleted_flag, NULL, NULL), ==,
+               g_hash_table_size (contacts));
+
+       g_hash_table_iter_init (&iter, contacts);
+       while (g_hash_table_iter_next (&iter, &uid, NULL)) {
+               g_assert (e_cache_contains (cache, uid, deleted_flag));
+       }
+}
+
+static gchar *
+e_book_meta_backend_test_get_backend_property (EBookBackend *book_backend,
+                                              const gchar *prop_name)
+{
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (book_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strjoin (",",
+                       e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+                       "local",
+                       "contact-lists",
+                       NULL);
+       }
+
+       /* Chain up to parent's method. */
+       return E_BOOK_BACKEND_CLASS (e_book_meta_backend_test_parent_class)->get_backend_property 
(book_backend, prop_name);
+}
+
+static gboolean
+e_book_meta_backend_test_connect_sync (EBookMetaBackend *meta_backend,
+                                      const ENamedParameters *credentials,
+                                      ESourceAuthenticationResult *out_auth_result,
+                                      gchar **out_certificate_pem,
+                                      GTlsCertificateFlags *out_certificate_errors,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+
+       if (test_backend->is_connected)
+               return TRUE;
+
+       test_backend->connect_count++;
+
+       if (test_backend->can_connect) {
+               test_backend->is_connected = TRUE;
+               return TRUE;
+       }
+
+       g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+               e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+       return FALSE;
+}
+
+static gboolean
+e_book_meta_backend_test_disconnect_sync (EBookMetaBackend *meta_backend,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       test_backend->is_connected = FALSE;
+
+       return TRUE;
+}
+
+static gboolean
+e_book_meta_backend_test_get_changes_sync (EBookMetaBackend *meta_backend,
+                                          const gchar *last_sync_tag,
+                                          gboolean is_repeat,
+                                          gchar **out_new_sync_tag,
+                                          gboolean *out_repeat,
+                                          GSList **out_created_objects,
+                                          GSList **out_modified_objects,
+                                          GSList **out_removed_objects,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_repeat != NULL, FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+
+       if (!test_backend->sync_tag_index) {
+               g_assert_null (last_sync_tag);
+       } else {
+               g_assert_nonnull (last_sync_tag);
+               g_assert_cmpint (atoi (last_sync_tag), ==, test_backend->sync_tag_index);
+
+               test_backend->sync_tag_index++;
+               *out_new_sync_tag = g_strdup_printf ("%d", test_backend->sync_tag_index);
+
+               if (test_backend->sync_tag_index == 2)
+                       *out_repeat = TRUE;
+               else if (test_backend->sync_tag_index == 3)
+                       return TRUE;
+       }
+
+       /* Nothing to do here at the moment, left the work to the parent class,
+          which calls list_existing_sync() internally. */
+       return E_BOOK_META_BACKEND_CLASS (e_book_meta_backend_test_parent_class)->get_changes_sync 
(meta_backend,
+               last_sync_tag, is_repeat, out_new_sync_tag, out_repeat, out_created_objects,
+               out_modified_objects, out_removed_objects, cancellable, error);
+}
+
+static gboolean
+e_book_meta_backend_test_list_existing_sync (EBookMetaBackend *meta_backend,
+                                            gchar **out_new_sync_tag,
+                                            GSList **out_existing_objects,
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+       GHashTableIter iter;
+       gpointer key, value;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag, FALSE);
+       g_return_val_if_fail (out_existing_objects, FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       test_backend->list_count++;
+
+       g_assert (test_backend->is_connected);
+
+       *out_existing_objects = NULL;
+
+       g_hash_table_iter_init (&iter, test_backend->contacts);
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               const gchar *uid;
+               gchar *revision;
+               EBookMetaBackendInfo *nfo;
+
+               uid = key;
+               revision = e_contact_get (value, E_CONTACT_REV);
+
+               nfo = e_book_meta_backend_info_new (uid, revision, NULL, NULL);
+               *out_existing_objects = g_slist_prepend (*out_existing_objects, nfo);
+
+               g_free (revision);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_book_meta_backend_test_save_contact_sync (EBookMetaBackend *meta_backend,
+                                           gboolean overwrite_existing,
+                                           EConflictResolution conflict_resolution,
+                                           EContact *contact,
+                                           const gchar *extra,
+                                           gchar **out_new_uid,
+                                           gchar **out_new_extra,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+       const gchar *uid;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       test_backend->save_count++;
+
+       g_assert (test_backend->is_connected);
+
+       uid = e_contact_get_const (contact, E_CONTACT_UID);
+       g_assert_nonnull (uid);
+
+       if (g_hash_table_contains (test_backend->contacts, uid)) {
+               if (!overwrite_existing) {
+                       g_propagate_error (error, e_data_book_create_error 
(E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS, NULL));
+                       return FALSE;
+               }
+
+               g_hash_table_remove (test_backend->contacts, uid);
+       }
+
+       /* Intentionally do not add a referenced 'contact', thus any later changes
+          on it are not "propagated" into the test_backend's content. */
+       g_hash_table_insert (test_backend->contacts, g_strdup (uid), e_contact_duplicate (contact));
+
+       *out_new_uid = g_strdup (uid);
+
+       return TRUE;
+}
+
+static gboolean
+e_book_meta_backend_test_load_contact_sync (EBookMetaBackend *meta_backend,
+                                           const gchar *uid,
+                                           const gchar *extra,
+                                           EContact **out_contact,
+                                           gchar **out_extra,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_contact != NULL, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       test_backend->load_count++;
+
+       g_assert (test_backend->is_connected);
+
+       *out_contact = g_hash_table_lookup (test_backend->contacts, uid);
+
+       if (*out_contact) {
+               *out_contact = e_contact_duplicate (*out_contact);
+               *out_extra = g_strconcat ("extra for ", uid, NULL);
+               return TRUE;
+       } else {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, 
NULL));
+       }
+
+       return FALSE;
+}
+
+static gboolean
+e_book_meta_backend_test_remove_contact_sync (EBookMetaBackend *meta_backend,
+                                             EConflictResolution conflict_resolution,
+                                             const gchar *uid,
+                                             const gchar *extra,
+                                             const gchar *object,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       EBookMetaBackendTest *test_backend;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (extra != NULL, FALSE);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       test_backend->remove_count++;
+
+       g_assert (test_backend->is_connected);
+
+       success = g_hash_table_remove (test_backend->contacts, uid);
+       if (success) {
+               gchar *expected_extra;
+
+               expected_extra = g_strconcat ("extra for ", uid, NULL);
+               g_assert_cmpstr (expected_extra, ==, extra);
+               g_free (expected_extra);
+       } else {
+               g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, 
NULL));
+       }
+
+       return success;
+}
+
+static void
+e_book_meta_backend_test_reset_counters (EBookMetaBackendTest *test_backend)
+{
+       g_return_if_fail (E_IS_BOOK_META_BACKEND_TEST (test_backend));
+
+       test_backend->connect_count = 0;
+       test_backend->list_count = 0;
+       test_backend->save_count = 0;
+       test_backend->load_count = 0;
+       test_backend->remove_count = 0;
+}
+
+static EBookCache *glob_use_cache = NULL;
+
+static void
+e_book_meta_backend_test_constructed (GObject *object)
+{
+       EBookMetaBackendTest *test_backend = E_BOOK_META_BACKEND_TEST (object);
+
+       g_assert_nonnull (glob_use_cache);
+
+       /* Set it before EBookMetaBackend::constucted() creates its own cache */
+       e_book_meta_backend_set_cache (E_BOOK_META_BACKEND (test_backend), glob_use_cache);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_meta_backend_test_parent_class)->constructed (object);
+}
+
+static void
+e_book_meta_backend_test_finalize (GObject *object)
+{
+       EBookMetaBackendTest *test_backend = E_BOOK_META_BACKEND_TEST (object);
+
+       g_assert_nonnull (test_backend->contacts);
+
+       g_hash_table_destroy (test_backend->contacts);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_meta_backend_test_parent_class)->finalize (object);
+}
+
+static void
+e_book_meta_backend_test_class_init (EBookMetaBackendTestClass *klass)
+{
+       EBookMetaBackendClass *book_meta_backend_class;
+       EBookBackendClass *book_backend_class;
+       GObjectClass *object_class;
+
+       book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+       book_meta_backend_class->connect_sync = e_book_meta_backend_test_connect_sync;
+       book_meta_backend_class->disconnect_sync = e_book_meta_backend_test_disconnect_sync;
+       book_meta_backend_class->get_changes_sync = e_book_meta_backend_test_get_changes_sync;
+       book_meta_backend_class->list_existing_sync = e_book_meta_backend_test_list_existing_sync;
+       book_meta_backend_class->save_contact_sync = e_book_meta_backend_test_save_contact_sync;
+       book_meta_backend_class->load_contact_sync = e_book_meta_backend_test_load_contact_sync;
+       book_meta_backend_class->remove_contact_sync = e_book_meta_backend_test_remove_contact_sync;
+
+       book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+       book_backend_class->get_backend_property = e_book_meta_backend_test_get_backend_property;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_book_meta_backend_test_constructed;
+       object_class->finalize = e_book_meta_backend_test_finalize;
+}
+
+static void
+e_book_meta_backend_test_init (EBookMetaBackendTest *test_backend)
+{
+       test_backend->sync_tag_index = 0;
+       test_backend->is_connected = FALSE;
+       test_backend->can_connect = TRUE;
+       test_backend->contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+       ebmb_test_add_test_case (test_backend, "custom-1");
+       ebmb_test_add_test_case (test_backend, "custom-2");
+       ebmb_test_add_test_case (test_backend, "custom-3");
+       ebmb_test_add_test_case (test_backend, "custom-5");
+       ebmb_test_add_test_case (test_backend, "custom-6");
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       e_backend_set_online (E_BACKEND (test_backend), TRUE);
+       e_book_backend_set_writable (E_BOOK_BACKEND (test_backend), TRUE);
+}
+
+static ESourceRegistry *glob_registry = NULL;
+
+static EBookMetaBackend *
+e_book_meta_backend_test_new (EBookCache *cache)
+{
+       EBookMetaBackend *meta_backend;
+       GHashTableIter iter;
+       ESource *scratch;
+       gpointer contact;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert (E_IS_BOOK_CACHE (cache));
+
+       g_assert_nonnull (glob_registry);
+       g_assert_null (glob_use_cache);
+
+       glob_use_cache = cache;
+
+       scratch = e_source_new_with_uid ("test-source", NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (scratch);
+
+       meta_backend = g_object_new (E_TYPE_BOOK_META_BACKEND_TEST,
+               "source", scratch,
+               "registry", glob_registry,
+               NULL);
+       g_assert_nonnull (meta_backend);
+
+       g_assert (glob_use_cache == cache);
+       glob_use_cache = NULL;
+
+       g_object_unref (scratch);
+
+       g_hash_table_iter_init (&iter, E_BOOK_META_BACKEND_TEST (meta_backend)->contacts);
+       while (g_hash_table_iter_next (&iter, NULL, &contact)) {
+               gchar *extra;
+
+               extra = g_strconcat ("extra for ", e_contact_get_const (contact, E_CONTACT_UID), NULL);
+               success = e_book_cache_put_contact (cache, contact, extra, E_CACHE_IS_ONLINE, NULL, &error);
+               g_free (extra);
+
+               g_assert_no_error (error);
+               g_assert (success);
+       }
+
+       return meta_backend;
+}
+
+static void
+e_book_meta_backend_test_change_online (EBookMetaBackend *meta_backend,
+                                       gboolean is_online)
+{
+       EFlag *flag;
+       gulong handler_id;
+
+       if (!is_online) {
+               e_backend_set_online (E_BACKEND (meta_backend), FALSE);
+               return;
+       }
+
+       if (e_backend_get_online (E_BACKEND (meta_backend)))
+               return;
+
+       flag = e_flag_new ();
+
+       handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+               G_CALLBACK (e_flag_set), flag);
+
+       /* Going online triggers refresh, thus wait for it */
+       e_backend_set_online (E_BACKEND (meta_backend), TRUE);
+
+       e_flag_wait (flag);
+       e_flag_free (flag);
+
+       g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+e_book_meta_backend_test_call_refresh (EBookMetaBackend *meta_backend)
+{
+       EFlag *flag;
+       gulong handler_id;
+       gboolean success;
+       GError *error = NULL;
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend)))
+               return;
+
+       flag = e_flag_new ();
+
+       handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+               G_CALLBACK (e_flag_set), flag);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->refresh_sync (E_BOOK_BACKEND (meta_backend), NULL, 
&error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       e_flag_wait (flag);
+       e_flag_free (flag);
+
+       g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+test_one_photo (EBookMetaBackend *meta_backend,
+               const gchar *test_case,
+               EContactField field)
+{
+       EContact *contact;
+       EContactPhoto *photo;
+       guchar *orig_content;
+       gchar *new_content = NULL;
+       gsize orig_len = 0, new_len = 0;
+       gchar *filename;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert (E_IS_BOOK_META_BACKEND (meta_backend));
+       g_assert_nonnull (test_case);
+       g_assert (field == E_CONTACT_PHOTO || field == E_CONTACT_LOGO);
+
+       contact = tcu_new_contact_from_test_case (test_case);
+       g_assert_nonnull (contact);
+
+       photo = e_contact_get (contact, field);
+       g_assert_nonnull (photo);
+       g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_INLINED);
+
+       orig_content = (guchar *) e_contact_photo_get_inlined (photo, &orig_len);
+       g_assert_nonnull (orig_content);
+       g_assert_cmpint (orig_len, >, 0);
+
+       orig_content = g_memdup (orig_content, (guint) orig_len);
+
+       e_contact_photo_free (photo);
+
+       success = e_book_meta_backend_store_inline_photos_sync (meta_backend, contact, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       photo = e_contact_get (contact, field);
+       g_assert_nonnull (photo);
+       g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+       g_assert_nonnull (e_contact_photo_get_uri (photo));
+
+       filename = g_filename_from_uri (e_contact_photo_get_uri (photo), NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (filename);
+
+       success = g_file_get_contents (filename, &new_content, &new_len, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (new_content);
+       g_assert_cmpmem (orig_content, orig_len, new_content, new_len);
+
+       g_free (new_content);
+       g_free (filename);
+
+       e_contact_photo_free (photo);
+
+       success = e_book_meta_backend_inline_local_photos_sync (meta_backend, contact, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       photo = e_contact_get (contact, field);
+       g_assert_nonnull (photo);
+       g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_INLINED);
+
+       new_content = (gchar *) e_contact_photo_get_inlined (photo, &new_len);
+       g_assert_nonnull (new_content);
+       g_assert_cmpmem (orig_content, orig_len, new_content, new_len);
+
+       e_contact_photo_free (photo);
+       g_free (orig_content);
+
+       /* Also try with remote URI, which should be left as is */
+       photo = e_contact_photo_new ();
+       g_assert_nonnull (photo);
+
+       photo->type = E_CONTACT_PHOTO_TYPE_URI;
+       e_contact_photo_set_uri (photo, REMOTE_URL);
+       e_contact_set (contact, field, photo);
+       e_contact_photo_free (photo);
+
+       photo = e_contact_get (contact, field);
+       g_assert_nonnull (photo);
+       g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+       g_assert_cmpstr (e_contact_photo_get_uri (photo), ==, REMOTE_URL);
+       e_contact_photo_free (photo);
+
+       success = e_book_meta_backend_store_inline_photos_sync (meta_backend, contact, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       photo = e_contact_get (contact, field);
+       g_assert_nonnull (photo);
+       g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+       g_assert_cmpstr (e_contact_photo_get_uri (photo), ==, REMOTE_URL);
+       e_contact_photo_free (photo);
+
+       success = e_book_meta_backend_inline_local_photos_sync (meta_backend, contact, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       photo = e_contact_get (contact, field);
+       g_assert_nonnull (photo);
+       g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+       g_assert_cmpstr (e_contact_photo_get_uri (photo), ==, REMOTE_URL);
+       e_contact_photo_free (photo);
+
+       g_object_unref (contact);
+}
+
+static void
+test_photos (TCUFixture *fixture,
+            gconstpointer user_data)
+{
+       EBookMetaBackend *meta_backend;
+
+       meta_backend = e_book_meta_backend_test_new (fixture->book_cache);
+       g_assert_nonnull (meta_backend);
+
+       test_one_photo (meta_backend, "photo-1", E_CONTACT_PHOTO);
+       test_one_photo (meta_backend, "logo-1", E_CONTACT_LOGO);
+
+       g_object_unref (meta_backend);
+}
+
+static void
+test_empty_cache (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       EBookMetaBackend *meta_backend;
+       EBookMetaBackendTest *test_backend;
+       GSList *uids;
+       gboolean success;
+       GError *error = NULL;
+
+       meta_backend = e_book_meta_backend_test_new (fixture->book_cache);
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       g_assert_nonnull (test_backend);
+
+       uids = NULL;
+       success = e_book_cache_search_uids (fixture->book_cache, NULL, &uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_slist_length (uids), >, 0);
+       g_slist_free_full (uids, g_free);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), >, 0);
+       g_assert_no_error (error);
+
+       /* Empty the cache */
+       success = e_book_meta_backend_empty_cache_sync (meta_backend, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       /* Verify the cache is truly empty */
+       uids = NULL;
+       success = e_book_cache_search_uids (fixture->book_cache, NULL, &uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_slist_length (uids), ==, 0);
+       g_slist_free_full (uids, g_free);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 0);
+       g_assert_no_error (error);
+
+       g_object_unref (meta_backend);
+}
+
+static void
+test_create_contacts (EBookMetaBackend *meta_backend)
+{
+       EBookMetaBackendTest *test_backend;
+       EBookCache *book_cache;
+       GSList *offline_changes;
+       gchar *vcards[2] = { NULL, NULL }, *tmp;
+       GQueue new_contacts = G_QUEUE_INIT;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (book_cache);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       /* Try to add existing contact, it should fail */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-1");
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS);
+       g_assert (!success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 0);
+       g_clear_error (&error);
+       g_free (vcards[0]);
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       /* Try to add new contact */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-7");
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+       g_assert_cmpstr (e_contact_get_const (g_queue_peek_head (&new_contacts), E_CONTACT_UID), ==, 
"custom-7");
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       g_assert_cmpint (test_backend->list_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&new_contacts);
+       g_free (vcards[0]);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       /* Going offline */
+       e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       /* Try to add existing contact, it should fail */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-7");
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS);
+       g_assert (!success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 0);
+       g_clear_error (&error);
+       g_free (vcards[0]);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+
+       /* Try to add new contact */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-8");
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+       g_assert_cmpstr (e_contact_get_const (g_queue_peek_head (&new_contacts), E_CONTACT_UID), ==, 
"custom-8");
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+
+       g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&new_contacts);
+       g_free (vcards[0]);
+
+       ebmb_test_hash_contains (test_backend->contacts, TRUE, FALSE,
+               "custom-8", NULL, NULL);
+       ebmb_test_cache_contains (book_cache, FALSE, FALSE,
+               "custom-8", NULL, NULL);
+
+       /* Going online */
+       e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       offline_changes = e_cache_get_offline_changes (E_CACHE (book_cache), NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+       /* Add contact without UID */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-9");
+       g_assert_nonnull (vcards[0]);
+       tmp = strstr (vcards[0], "UID:custom-9\r\n");
+       g_assert_nonnull (tmp);
+       strncpy (tmp, "X-TEST:*007*", 12);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+       g_assert_cmpstr (e_contact_get_const (g_queue_peek_head (&new_contacts), E_CONTACT_UID), !=, 
"custom-9");
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       g_assert_cmpint (test_backend->list_count, ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 2);
+
+       tmp = e_vcard_to_string (E_VCARD (g_queue_peek_head (&new_contacts)), EVC_FORMAT_VCARD_30);
+       g_assert_nonnull (tmp);
+       g_assert_nonnull (strstr (tmp, "X-TEST:*007*\r\n"));
+       g_assert_nonnull (strstr (tmp, e_contact_get_const (g_queue_peek_head (&new_contacts), 
E_CONTACT_UID)));
+       g_free (tmp);
+
+       g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&new_contacts);
+       g_free (vcards[0]);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       g_object_unref (book_cache);
+}
+
+static gchar *
+ebmb_test_modify_case (const gchar *case_name)
+{
+       gchar *vcard, *tmp;
+       const gchar *rev;
+       EContact *contact;
+
+       g_assert_nonnull (case_name);
+
+       contact = tcu_new_contact_from_test_case (case_name);
+       g_assert_nonnull (contact);
+
+       e_contact_set (contact, E_CONTACT_FULL_NAME, MODIFIED_FN_STR);
+
+       rev = e_contact_get_const (contact, E_CONTACT_REV);
+       if (!rev)
+               tmp = g_strdup ("0");
+       else
+               tmp = g_strdup_printf ("%d", atoi (rev) + 1);
+       e_contact_set (contact, E_CONTACT_REV, tmp);
+       g_free (tmp);
+
+       vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+       g_object_unref (contact);
+
+       return vcard;
+}
+
+static void
+test_modify_contacts (EBookMetaBackend *meta_backend)
+{
+       EBookMetaBackendTest *test_backend;
+       EBookCache *book_cache;
+       EContact *contact;
+       GSList *offline_changes;
+       gchar *vcards[2] = { NULL, NULL }, *tmp;
+       GQueue new_contacts = G_QUEUE_INIT;
+       gint old_rev, new_rev;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (book_cache);
+
+       /* Modify non-existing contact */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-1");
+       g_assert_nonnull (vcards[0]);
+       tmp = strstr (vcards[0], "UID:custom-1");
+       g_assert_nonnull (tmp);
+       strncpy (tmp + 4, "unknown", 7);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+       g_assert (!success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 0);
+       g_clear_error (&error);
+       g_free (vcards[0]);
+
+       /* Modify existing contact */
+       vcards[0] = ebmb_test_modify_case ("custom-1");
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       contact = tcu_new_contact_from_test_case ("custom-1");
+       g_assert_nonnull (contact);
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+       old_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), !=, MODIFIED_FN_STR);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-1");
+
+       g_object_unref (contact);
+
+       contact = g_queue_peek_head (&new_contacts);
+       g_assert_nonnull (contact);
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+       new_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_cmpint (old_rev + 1, ==, new_rev);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), ==, MODIFIED_FN_STR);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-1");
+
+       g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&new_contacts);
+       g_free (vcards[0]);
+
+       /* Going offline */
+       e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       /* Modify custom-2 */
+       vcards[0] = ebmb_test_modify_case ("custom-2");
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &new_contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+
+       contact = tcu_new_contact_from_test_case ("custom-2");
+       g_assert_nonnull (contact);
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+       old_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), !=, MODIFIED_FN_STR);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-2");
+
+       g_object_unref (contact);
+
+       contact = g_queue_peek_head (&new_contacts);
+       g_assert_nonnull (contact);
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+       new_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+       g_assert_cmpint (old_rev + 1, ==, new_rev);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), ==, MODIFIED_FN_STR);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-2");
+
+       g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&new_contacts);
+       g_free (vcards[0]);
+
+       /* Going online */
+       e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       offline_changes = e_cache_get_offline_changes (E_CACHE (book_cache), NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+       g_object_unref (book_cache);
+}
+
+static void
+test_remove_contacts (EBookMetaBackend *meta_backend)
+{
+       EBookMetaBackendTest *test_backend;
+       EBookCache *book_cache;
+       const gchar *uids[2] = { NULL, NULL };
+       GSList *offline_changes;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (book_cache);
+
+       /* Remove non-existing contact */
+       uids[0] = "unknown-contact";
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) uids, NULL, &error);
+       g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+       g_assert (!success);
+       g_clear_error (&error);
+
+       /* Remove existing contact */
+       uids[0] = "custom-1";
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       ebmb_test_hash_contains (test_backend->contacts, TRUE, FALSE,
+               "custom-1", NULL,
+               NULL);
+
+       /* Going offline */
+       e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       /* Remove existing contact */
+       uids[0] = "custom-3";
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       ebmb_test_hash_contains (test_backend->contacts, FALSE, FALSE,
+               "custom-3", NULL,
+               NULL);
+       ebmb_test_cache_contains (book_cache, TRUE, FALSE,
+               "custom-3", NULL,
+               NULL);
+
+       /* Going online */
+       e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       ebmb_test_hash_contains (test_backend->contacts, TRUE, FALSE,
+               "custom-3", NULL,
+               NULL);
+       ebmb_test_cache_contains (book_cache, TRUE, FALSE,
+               "custom-3", NULL,
+               NULL);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       offline_changes = e_cache_get_offline_changes (E_CACHE (book_cache), NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+       g_object_unref (book_cache);
+}
+
+static void
+test_get_contact (EBookMetaBackend *meta_backend)
+{
+       EBookMetaBackendTest *test_backend;
+       EBookCache *book_cache;
+       EContact *contact;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (book_cache);
+
+       e_book_cache_remove_contact (book_cache, "custom-5", E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+       e_book_cache_remove_contact (book_cache, "custom-6", E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+
+       /* Non-existing */
+       contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+               "unknown-contact", NULL, &error);
+       g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+       g_assert_null (contact);
+       g_clear_error (&error);
+
+       /* Existing */
+       contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+               "custom-1", NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (contact);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-1");
+       g_object_unref (contact);
+
+       /* Going offline */
+       e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+       g_assert (!e_cache_contains (E_CACHE (book_cache), "custom-5", E_CACHE_EXCLUDE_DELETED));
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+               "custom-5", NULL, &error);
+       g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+       g_assert_null (contact);
+       g_clear_error (&error);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+       g_assert_cmpint (test_backend->list_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+
+       /* Going online */
+       e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert (e_cache_contains (E_CACHE (book_cache), "custom-5", E_CACHE_EXCLUDE_DELETED));
+
+       /* Remove it from the cache, thus it's loaded from the "server" on demand */
+       e_book_cache_remove_contact (book_cache, "custom-5", E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       e_book_meta_backend_test_reset_counters (test_backend);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+
+       contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+               "custom-5", NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (contact);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+       g_assert_cmpint (test_backend->list_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-5");
+       g_object_unref (contact);
+
+       g_assert (e_cache_contains (E_CACHE (book_cache), "custom-5", E_CACHE_EXCLUDE_DELETED));
+
+       g_object_unref (book_cache);
+}
+
+static void
+test_get_contact_list (EBookMetaBackend *meta_backend)
+{
+       GQueue contacts = G_QUEUE_INIT;
+       EContact *contact;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_sync (E_BOOK_BACKEND 
(meta_backend),
+               "(is \"uid\" \"unknown-contact\")", &contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&contacts), ==, 0);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_sync (E_BOOK_BACKEND 
(meta_backend),
+               "(is \"uid\" \"custom-3\")", &contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_queue_get_length (&contacts), ==, 1);
+       contact = g_queue_peek_head (&contacts);
+       g_assert_nonnull (contact);
+       g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-3");
+       g_queue_foreach (&contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&contacts);
+}
+
+static void
+test_get_contact_list_uids (EBookMetaBackend *meta_backend)
+{
+       GQueue uids = G_QUEUE_INIT;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_uids_sync (E_BOOK_BACKEND 
(meta_backend),
+               "(is \"uid\" \"unknown-contact\")", &uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&uids), ==, 0);
+
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_uids_sync (E_BOOK_BACKEND 
(meta_backend),
+               "(is \"uid\" \"custom-3\")", &uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_queue_get_length (&uids), ==, 1);
+       g_assert_nonnull (g_queue_peek_head (&uids));
+       g_assert_cmpstr (g_queue_peek_head (&uids), ==, "custom-3");
+       g_queue_foreach (&uids, (GFunc) g_free, NULL);
+       g_queue_clear (&uids);
+}
+
+static void
+test_refresh (EBookMetaBackend *meta_backend)
+{
+       EBookMetaBackendTest *test_backend;
+       EBookCache *book_cache;
+       ECache *cache;
+       guint count;
+       EContact *contact;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (book_cache);
+
+       cache = E_CACHE (book_cache);
+
+       /* Empty local cache */
+       e_cache_remove_all (cache, NULL, &error);
+       g_assert_no_error (error);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 0);
+
+       e_book_meta_backend_test_reset_counters (test_backend);
+
+       ebmb_test_remove_component (test_backend, "custom-5");
+       ebmb_test_remove_component (test_backend, "custom-6");
+
+       /* Sync with server content */
+       e_book_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 3);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 3);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       /* Add new contact */
+       ebmb_test_add_test_case (test_backend, "custom-5");
+
+       /* Sync with server content */
+       e_book_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 4);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 4);
+
+       ebmb_test_hash_contains (test_backend->contacts, FALSE, TRUE,
+               "custom-1",
+               "custom-2",
+               "custom-3",
+               "custom-5",
+               NULL);
+
+       ebmb_test_cache_contains (book_cache, FALSE, TRUE,
+               "custom-1",
+               "custom-2",
+               "custom-3",
+               "custom-5",
+               NULL);
+
+       /* Sync with server content */
+       e_book_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 3);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 4);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 4);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       /* Add some more contacts */
+       ebmb_test_add_test_case (test_backend, "custom-6");
+       ebmb_test_add_test_case (test_backend, "custom-7");
+
+       /* Sync with server content */
+       e_book_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 4);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 6);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 6);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       /* Remove two contacts */
+       ebmb_test_remove_component (test_backend, "custom-2");
+       ebmb_test_remove_component (test_backend, "custom-5");
+
+       /* Sync with server content */
+       e_book_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 5);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 6);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 4);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       /* Mix add/remove/modify */
+       ebmb_test_add_test_case (test_backend, "custom-8");
+
+       ebmb_test_remove_component (test_backend, "custom-3");
+       ebmb_test_remove_component (test_backend, "custom-6");
+
+       contact = g_hash_table_lookup (test_backend->contacts, "custom-1");
+       g_assert_nonnull (contact);
+       e_contact_set (contact, E_CONTACT_REV, "changed");
+
+       contact = g_hash_table_lookup (test_backend->contacts, "custom-7");
+       g_assert_nonnull (contact);
+       e_contact_set (contact, E_CONTACT_REV, "changed");
+
+       /* Sync with server content */
+       e_book_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 6);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 9);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 3);
+
+       ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+       g_object_unref (book_cache);
+}
+
+static void
+test_cursor (EBookMetaBackend *meta_backend)
+{
+       EDataBookCursor *cursor;
+       EContactField sort_fields[] = { E_CONTACT_FULL_NAME };
+       EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING };
+       GQueue contacts = G_QUEUE_INIT;
+       gchar *vcards[2] = { NULL, NULL };
+       const gchar *uids[2] = { NULL, NULL };
+       gint traversed;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       /* Create the cursor */
+       cursor = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_cursor (E_BOOK_BACKEND (meta_backend),
+               sort_fields, sort_types, 1, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (cursor);
+       g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 5);
+       g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 0);
+
+       traversed = e_data_book_cursor_step (cursor, NULL, E_BOOK_CURSOR_STEP_MOVE, 
E_BOOK_CURSOR_ORIGIN_CURRENT, 3, NULL, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (traversed, ==, 3);
+       g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 5);
+       g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 3);
+
+       /* Create */
+       vcards[0] = tcu_new_vcard_from_test_case ("custom-7");
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&contacts), ==, 1);
+       g_queue_foreach (&contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&contacts);
+       g_free (vcards[0]);
+
+       g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 6);
+       g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 3);
+
+       /* Modify */
+       vcards[0] = ebmb_test_modify_case ("custom-2");
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) vcards, &contacts, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_queue_get_length (&contacts), ==, 1);
+       g_queue_foreach (&contacts, (GFunc) g_object_unref, NULL);
+       g_queue_clear (&contacts);
+       g_free (vcards[0]);
+
+       g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 6);
+       g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 3);
+
+       /* Remove */
+       uids[0] = "custom-3";
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND 
(meta_backend),
+               (const gchar * const *) uids, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 5);
+       g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 2);
+
+       /* Free the cursor */
+       success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->delete_cursor (E_BOOK_BACKEND (meta_backend), 
cursor, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+}
+
+typedef void (* TestWithMainLoopFunc) (EBookMetaBackend *meta_backend);
+
+typedef struct _MainLoopThreadData {
+       TestWithMainLoopFunc func;
+       EBookMetaBackend *meta_backend;
+       GMainLoop *main_loop;
+} MainLoopThreadData;
+
+static gpointer
+test_with_main_loop_thread (gpointer user_data)
+{
+       MainLoopThreadData *mlt = user_data;
+
+       g_assert_nonnull (mlt);
+       g_assert_nonnull (mlt->func);
+       g_assert_nonnull (mlt->meta_backend);
+
+       mlt->func (mlt->meta_backend);
+
+       g_main_loop_quit (mlt->main_loop);
+
+       return NULL;
+}
+
+static gboolean
+quit_test_with_mainloop_cb (gpointer user_data)
+{
+       GMainLoop *main_loop = user_data;
+
+       g_assert_nonnull (main_loop);
+
+       g_main_loop_quit (main_loop);
+
+       g_assert_not_reached ();
+
+       return FALSE;
+}
+
+static gboolean
+test_with_mainloop_run_thread_idle (gpointer user_data)
+{
+       GThread *thread;
+
+       g_assert_nonnull (user_data);
+
+       thread = g_thread_new (NULL, test_with_main_loop_thread, user_data);
+       g_thread_unref (thread);
+
+       return FALSE;
+}
+
+static void
+test_with_main_loop (EBookCache *book_cache,
+                    TestWithMainLoopFunc func)
+{
+       MainLoopThreadData mlt;
+       EBookMetaBackend *meta_backend;
+       guint timeout_id;
+
+       g_assert_nonnull (book_cache);
+       g_assert_nonnull (func);
+
+       meta_backend = e_book_meta_backend_test_new (book_cache);
+       g_assert_nonnull (meta_backend);
+
+       mlt.func = func;
+       mlt.meta_backend = meta_backend;
+       mlt.main_loop = g_main_loop_new (NULL, FALSE);
+
+       g_idle_add (test_with_mainloop_run_thread_idle, &mlt);
+       timeout_id = g_timeout_add_seconds (10, quit_test_with_mainloop_cb, mlt.main_loop);
+
+       g_main_loop_run (mlt.main_loop);
+
+       g_source_remove (timeout_id);
+       g_main_loop_unref (mlt.main_loop);
+       g_clear_object (&mlt.meta_backend);
+}
+
+#define main_loop_wrapper(_func) \
+static void \
+_func ## _tcu (TCUFixture *fixture, \
+              gconstpointer user_data) \
+{ \
+       test_with_main_loop (fixture->book_cache, _func); \
+}
+
+main_loop_wrapper (test_create_contacts)
+main_loop_wrapper (test_modify_contacts)
+main_loop_wrapper (test_remove_contacts)
+main_loop_wrapper (test_get_contact)
+main_loop_wrapper (test_get_contact_list)
+main_loop_wrapper (test_get_contact_list_uids)
+main_loop_wrapper (test_refresh)
+main_loop_wrapper (test_cursor)
+
+#undef main_loop_wrapper
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       ETestServerClosure tsclosure = {
+               E_TEST_SERVER_NONE,
+               NULL, /* Source customization function */
+               0,    /* Calendar Type */
+               TRUE, /* Keep the working sandbox after the test, don't remove it */
+               NULL, /* Destroy Notify function */
+       };
+       ETestServerFixture tsfixture = { 0 };
+       TCUClosure closure = { 0 };
+       gint res;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       e_test_server_utils_setup (&tsfixture, &tsclosure);
+
+       glob_registry = tsfixture.registry;
+       g_assert_nonnull (glob_registry);
+
+       g_test_add ("/EBookMetaBackend/Photos", TCUFixture, &closure,
+               tcu_fixture_setup, test_photos, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/EmptyCache", TCUFixture, &closure,
+               tcu_fixture_setup, test_empty_cache, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/CreateContacts", TCUFixture, &closure,
+               tcu_fixture_setup, test_create_contacts_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/ModifyContacts", TCUFixture, &closure,
+               tcu_fixture_setup, test_modify_contacts_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/RemoveContacts", TCUFixture, &closure,
+               tcu_fixture_setup, test_remove_contacts_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/GetContact", TCUFixture, &closure,
+               tcu_fixture_setup, test_get_contact_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/GetContactList", TCUFixture, &closure,
+               tcu_fixture_setup, test_get_contact_list_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/GetContactListUids", TCUFixture, &closure,
+               tcu_fixture_setup, test_get_contact_list_uids_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/Refresh", TCUFixture, &closure,
+               tcu_fixture_setup, test_refresh_tcu, tcu_fixture_teardown);
+       g_test_add ("/EBookMetaBackend/Cursor", TCUFixture, &closure,
+               tcu_fixture_setup, test_cursor_tcu, tcu_fixture_teardown);
+
+       res = g_test_run ();
+
+       e_test_server_utils_teardown (&tsfixture, &tsclosure);
+
+       return res;
+}
diff --git a/tests/libedata-book/test-sqlite-create-cursor.c b/tests/libedata-book/test-sqlite-create-cursor.c
index 0357d47..8ad9012 100644
--- a/tests/libedata-book/test-sqlite-create-cursor.c
+++ b/tests/libedata-book/test-sqlite-create-cursor.c
@@ -77,8 +77,8 @@ test_create_cursor_invalid_sort (EbSqlFixture *fixture,
                sort_fields, sort_types, 1, &error);
 
        g_assert (cursor == NULL);
-       g_assert (error);
-       g_assert (g_error_matches (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY));
+       g_assert_error (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY);
+       g_clear_error (&error);
 }
 
 static void
@@ -91,8 +91,8 @@ test_create_cursor_missing_sort (EbSqlFixture *fixture,
        cursor = e_book_sqlite_cursor_new (fixture->ebsql, NULL, NULL, NULL, 0, &error);
 
        g_assert (cursor == NULL);
-       g_assert (error);
-       g_assert (g_error_matches (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY));
+       g_assert_error (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY);
+       g_clear_error (&error);
 }
 
 gint
diff --git a/tests/libedata-cal/CMakeLists.txt b/tests/libedata-cal/CMakeLists.txt
index d42a64b..d5f6145 100644
--- a/tests/libedata-cal/CMakeLists.txt
+++ b/tests/libedata-cal/CMakeLists.txt
@@ -4,7 +4,14 @@ set(extra_deps
        edata-cal
 )
 
-set(extra_defines)
+set(extra_defines
+       -DSRCDIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"
+       -DINSTALLED_TEST_DIR=\"${INSTALLED_TESTS_EXEC_DIR}\"
+       -DBACKENDDIR=\"${ecal_backenddir}\"
+       -DDATADIR=\"${SHARE_INSTALL_PREFIX}\"
+       -DBUILDDIR=\"${CAMKE_BINARY_DIR}\"
+       -DCAMEL_PROVIDERDIR=\"${camel_providerdir}\"
+)
 
 set(extra_cflags
        ${CALENDAR_CFLAGS}
@@ -18,10 +25,64 @@ set(extra_ldflags
        ${CALENDAR_LDFLAGS}
 )
 
+set(SOURCES
+       test-cal-cache-utils.c
+       test-cal-cache-utils.h
+)
+
+add_library(data-cal-test-utils STATIC
+       ${SOURCES}
+)
+
+add_dependencies(data-cal-test-utils
+       edataserver
+       ${extra_deps}
+)
+target_compile_definitions(data-cal-test-utils PRIVATE
+       -DG_LOG_DOMAIN=\"data-cal-test-utils\"
+       ${extra_defines}
+)
+
+target_compile_options(data-cal-test-utils PUBLIC
+       ${BACKEND_CFLAGS}
+       ${DATA_SERVER_CFLAGS}
+       ${extra_cflags}
+)
+
+target_include_directories(data-cal-test-utils PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_BINARY_DIR}/src
+       ${CMAKE_SOURCE_DIR}/src
+       ${BACKEND_INCLUDE_DIRS}
+       ${DATA_SERVER_INCLUDE_DIRS}
+       ${extra_incdirs}
+)
+
+target_link_libraries(data-cal-test-utils
+       edataserver
+       ${extra_deps}
+       ${BACKEND_LDFLAGS}
+       ${DATA_SERVER_LDFLAGS}
+       ${extra_ldflags}
+)
+
+set(extra_deps
+       ecal
+       edata-cal
+       data-cal-test-utils
+)
+
+set(extra_defines)
+
 # Should be kept ordered approximately from least to most difficult/complex
 set(TESTS
        test-cal-backend-sexp
        test-intervaltree
+       test-cal-cache-getters
+       test-cal-cache-intervals
+       test-cal-cache-offline
+       test-cal-cache-search
+       test-cal-meta-backend
 )
 
 foreach(_test ${TESTS})
@@ -38,3 +99,11 @@ foreach(_test ${TESTS})
                "TEST_INSTALLED_SERVICES=1"
        )
 endforeach(_test)
+
+if(ENABLE_INSTALLED_TESTS)
+       file(GLOB COMPONENTS ${CMAKE_CURRENT_SOURCE_DIR}/components/*.ics)
+
+       install(FILES ${COMPONENTS}
+               DESTINATION ${INSTALLED_TESTS_EXEC_DIR}/components
+       )
+endif(ENABLE_INSTALLED_TESTS)
diff --git a/tests/libedata-cal/components/.gitattributes b/tests/libedata-cal/components/.gitattributes
new file mode 100644
index 0000000..11bcb63
--- /dev/null
+++ b/tests/libedata-cal/components/.gitattributes
@@ -0,0 +1 @@
+*.ics eol=crlf
diff --git a/tests/libedata-cal/components/event-1.ics b/tests/libedata-cal/components/event-1.ics
new file mode 100644
index 0000000..39eaa58
--- /dev/null
+++ b/tests/libedata-cal/components/event-1.ics
@@ -0,0 +1,18 @@
+BEGIN:VEVENT
+UID:event-1
+DTSTAMP:20170130T000000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:1
+DTSTART:20170209T013000Z
+DTEND:20170209T030000Z
+SUMMARY:Alarmed
+DESCRIPTION:Event with alarm
+CLASS:PUBLIC
+TRANSP:OPACHE
+BEGIN:VALARM
+TRIGGER:-PT30M
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-2.ics b/tests/libedata-cal/components/event-2.ics
new file mode 100644
index 0000000..ce980be
--- /dev/null
+++ b/tests/libedata-cal/components/event-2.ics
@@ -0,0 +1,14 @@
+BEGIN:VEVENT
+UID:event-2
+DTSTAMP:20170103T070000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:1
+DTSTART:20170103T080000Z
+DTEND:20170103T083000Z
+SUMMARY:First working day
+DESCRIPTION:Multiline\n
+ description text
+CLASS:CONFIDENTIAL
+CATEGORIES:Work,Hard
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-3.ics b/tests/libedata-cal/components/event-3.ics
new file mode 100644
index 0000000..4fcf5bf
--- /dev/null
+++ b/tests/libedata-cal/components/event-3.ics
@@ -0,0 +1,12 @@
+BEGIN:VEVENT
+UID:event-3
+DTSTAMP:20170103T070000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:3
+DTSTART:20170104T100000Z
+DTEND:20170110T120000Z
+SUMMARY:Lunch prepare
+LOCATION:Kitchen
+CLASS:PRIVATE
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-4.ics b/tests/libedata-cal/components/event-4.ics
new file mode 100644
index 0000000..2fde425
--- /dev/null
+++ b/tests/libedata-cal/components/event-4.ics
@@ -0,0 +1,12 @@
+BEGIN:VEVENT
+UID:event-4
+DTSTAMP:20170102T000000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:3
+DTSTART:20170102T100000Z
+DTEND:20170102T180000Z
+SUMMARY:After-party clean up
+LOCATION:All around
+CLASS:PUBLIC
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-5.ics b/tests/libedata-cal/components/event-5.ics
new file mode 100644
index 0000000..a12a488
--- /dev/null
+++ b/tests/libedata-cal/components/event-5.ics
@@ -0,0 +1,18 @@
+BEGIN:VEVENT
+UID:event-5
+DTSTAMP:20101231T000000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:1
+DTSTART:20091231T000000Z
+DTEND:20100101T235959Z
+SUMMARY:New Year Party
+LOCATION:All around
+CATEGORIES:Holiday,International
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:-P1D
+DESCRIPTION:New Year Party
+END:VALARM
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-6-a.ics b/tests/libedata-cal/components/event-6-a.ics
new file mode 100644
index 0000000..404b51e
--- /dev/null
+++ b/tests/libedata-cal/components/event-6-a.ics
@@ -0,0 +1,13 @@
+BEGIN:VEVENT
+UID:event-6
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=America/New_York:20170225T150000
+DTEND;TZID=America/New_York:20170225T160000
+SEQUENCE:2
+SUMMARY:Recurring with detached instance (3rd instance)
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170221T125024Z
+LAST-MODIFIED:20170221T125341Z
+RECURRENCE-ID;TZID=America/New_York:20170225T134900
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-6.ics b/tests/libedata-cal/components/event-6.ics
new file mode 100644
index 0000000..ee85a01
--- /dev/null
+++ b/tests/libedata-cal/components/event-6.ics
@@ -0,0 +1,12 @@
+BEGIN:VEVENT
+UID:event-6
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=America/New_York:20170221T134900
+DTEND;TZID=America/New_York:20170221T144900
+SEQUENCE:1
+SUMMARY:Recurring with detached instance
+RRULE:FREQ=DAILY;COUNT=5;INTERVAL=2
+CLASS:PUBLIC
+CREATED:20170221T125024Z
+LAST-MODIFIED:20170221T125024Z
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-7.ics b/tests/libedata-cal/components/event-7.ics
new file mode 100644
index 0000000..b08d392
--- /dev/null
+++ b/tests/libedata-cal/components/event-7.ics
@@ -0,0 +1,14 @@
+BEGIN:VEVENT
+UID:event-7
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=/freeassociation.sourceforge.net/America/New_York:20170221T135000
+DTEND;TZID=/freeassociation.sourceforge.net/America/New_York:20170221T145000
+SEQUENCE:1
+SUMMARY:With attachment
+TRANSP:OPAQUE
+ATTACH:file:///usr/share/icons/hicolor/48x48/apps/evolution.png
+CLASS:PUBLIC
+CREATED:20170221T125054Z
+LAST-MODIFIED:20170221T125054Z
+CATEGORIES:Holiday,Work
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-8.ics b/tests/libedata-cal/components/event-8.ics
new file mode 100644
index 0000000..35887ad
--- /dev/null
+++ b/tests/libedata-cal/components/event-8.ics
@@ -0,0 +1,16 @@
+BEGIN:VEVENT
+UID:event-8
+DTSTAMP:20170221T121736Z
+DTSTART:20170225T160000
+DTEND:20170225T160500
+SEQUENCE:2
+ORGANIZER;CN=Bob:MAILTO:bob@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;LANGUAGE=en:MAILTO:alice@no.where
+SUMMARY:Meet Alice (Floating time)
+COMMENT:User commentary text
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170221T131322Z
+LAST-MODIFIED:20170221T131322Z
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-9.ics b/tests/libedata-cal/components/event-9.ics
new file mode 100644
index 0000000..f0edf3c
--- /dev/null
+++ b/tests/libedata-cal/components/event-9.ics
@@ -0,0 +1,17 @@
+BEGIN:VEVENT
+UID:event-9
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=America/New_York:20170225T160000
+DTEND;TZID=America/New_York:20170225T170000
+SEQUENCE:2
+ORGANIZER;CN=Alice:MAILTO:alice@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;CN=Bob;LANGUAGE=en:MAILTO:bob@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;CN=Charlie;LANGUAGE=en:MAILTO:charlie@no.where
+SUMMARY:2-on-1
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170221T131421Z
+LAST-MODIFIED:20170221T131421Z
+END:VEVENT
diff --git a/tests/libedata-cal/components/invite-1.ics b/tests/libedata-cal/components/invite-1.ics
new file mode 100644
index 0000000..32267b6
--- /dev/null
+++ b/tests/libedata-cal/components/invite-1.ics
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T175957Z
+DTSTART:20170321T120000Z
+DTEND:20170321T130000Z
+SEQUENCE:1
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;CN=User;LANGUAGE=en:MAILTO:user@no.where
+SUMMARY:Invite
+TRANSP:OPAQUE
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/invite-2.ics b/tests/libedata-cal/components/invite-2.ics
new file mode 100644
index 0000000..bf8423e
--- /dev/null
+++ b/tests/libedata-cal/components/invite-2.ics
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T181958Z
+DTSTART:20170321T120000Z
+DTEND:20170321T130000Z
+SEQUENCE:1
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=ACCEPTED:MAILTO:user@no.where
+SUMMARY:Invite
+TRANSP:OPAQUE
+CLASS:PUBLIC
+COMMENT:See you there
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/invite-3.ics b/tests/libedata-cal/components/invite-3.ics
new file mode 100644
index 0000000..80b65d2
--- /dev/null
+++ b/tests/libedata-cal/components/invite-3.ics
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T180058Z
+DTSTART:20170321T130000Z
+DTEND:20170321T140000Z
+SEQUENCE:2
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;
+ RSVP=TRUE;CN=User;LANGUAGE=en:MAILTO:user@no.where
+SUMMARY:Invite (modified)
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170308T180033Z
+LAST-MODIFIED:20170308T180033Z
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/invite-4.ics b/tests/libedata-cal/components/invite-4.ics
new file mode 100644
index 0000000..ab1092b
--- /dev/null
+++ b/tests/libedata-cal/components/invite-4.ics
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T180113Z
+DTSTART:20170321T130000Z
+DTEND:20170321T140000Z
+SEQUENCE:2
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;
+ RSVP=TRUE;CN=User;LANGUAGE=en:MAILTO:user@no.where
+SUMMARY:Invite (modified)
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170308T180033Z
+LAST-MODIFIED:20170308T180058Z
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/task-1.ics b/tests/libedata-cal/components/task-1.ics
new file mode 100644
index 0000000..f64663c
--- /dev/null
+++ b/tests/libedata-cal/components/task-1.ics
@@ -0,0 +1,9 @@
+BEGIN:VTODO
+UID:task-1
+DTSTAMP:20170221T130109Z
+SUMMARY:Simple task
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130123Z
+LAST-MODIFIED:20170221T130123Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-2.ics b/tests/libedata-cal/components/task-2.ics
new file mode 100644
index 0000000..64b83ab
--- /dev/null
+++ b/tests/libedata-cal/components/task-2.ics
@@ -0,0 +1,11 @@
+BEGIN:VTODO
+UID:task-2
+DTSTAMP:20170221T130109Z
+SUMMARY:With Due date
+DUE;TZID=America/New_York:20170313T000000
+PERCENT-COMPLETE:0
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130208Z
+LAST-MODIFIED:20170221T130208Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-3.ics b/tests/libedata-cal/components/task-3.ics
new file mode 100644
index 0000000..e54e4fc
--- /dev/null
+++ b/tests/libedata-cal/components/task-3.ics
@@ -0,0 +1,13 @@
+BEGIN:VTODO
+UID:task-3
+DTSTAMP:20170221T130109Z
+SUMMARY:With completed
+STATUS:COMPLETED
+COMPLETED;America/New_York:20170221T000000
+PERCENT-COMPLETE:100
+CLASS:PUBLIC
+DESCRIPTION:Task having _with_ in description
+SEQUENCE:1
+CREATED:20170221T130319Z
+LAST-MODIFIED:20170221T130319Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-4.ics b/tests/libedata-cal/components/task-4.ics
new file mode 100644
index 0000000..82c36aa
--- /dev/null
+++ b/tests/libedata-cal/components/task-4.ics
@@ -0,0 +1,13 @@
+BEGIN:VTODO
+UID:task-4
+DTSTAMP:20170221T130109Z
+SUMMARY:With completed (2nd) and Due
+STATUS:COMPLETED
+DUE;TZID=America/New_York:20170301T000000
+COMPLETED;TZID=America/New_York:20170221T000000
+PERCENT-COMPLETE:100
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130339Z
+LAST-MODIFIED:20170221T130339Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-5.ics b/tests/libedata-cal/components/task-5.ics
new file mode 100644
index 0000000..b813194
--- /dev/null
+++ b/tests/libedata-cal/components/task-5.ics
@@ -0,0 +1,13 @@
+BEGIN:VTODO
+UID:task-5
+DTSTAMP:20170221T130109Z
+SUMMARY:20% complete
+STATUS:IN-PROCESS
+PERCENT-COMPLETE:20
+CLASS:PUBLIC
+SEQUENCE:1
+PRIORITY:7
+CREATED:20170221T130411Z
+LAST-MODIFIED:20170221T130411Z
+LOCATION:Kitchen
+END:VTODO
diff --git a/tests/libedata-cal/components/task-6.ics b/tests/libedata-cal/components/task-6.ics
new file mode 100644
index 0000000..1ea7777
--- /dev/null
+++ b/tests/libedata-cal/components/task-6.ics
@@ -0,0 +1,14 @@
+BEGIN:VTODO
+UID:task-6
+DTSTAMP:20170221T130109Z
+SUMMARY:90% complete (Confidential)
+STATUS:IN-PROCESS
+PRIORITY:5
+PERCENT-COMPLETE:90
+CLASS:CONFIDENTIAL
+COMMENT:User commentary text
+SEQUENCE:2
+DTSTART;TZID=America/New_York:20131213T131313
+CREATED:20170221T130512Z
+LAST-MODIFIED:20170221T130610Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-7.ics b/tests/libedata-cal/components/task-7.ics
new file mode 100644
index 0000000..bd01312
--- /dev/null
+++ b/tests/libedata-cal/components/task-7.ics
@@ -0,0 +1,17 @@
+BEGIN:VTODO
+UID:task-7
+DTSTAMP:20170221T130109Z
+SUMMARY:55% complete (Private)
+STATUS:IN-PROCESS
+PERCENT-COMPLETE:55
+CLASS:PRIVATE
+SEQUENCE:3
+PRIORITY:2
+CREATED:20170221T130447Z
+LAST-MODIFIED:20170221T130618Z
+BEGIN:VALARM
+TRIGGER:-PT30M
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VTODO
diff --git a/tests/libedata-cal/components/task-8.ics b/tests/libedata-cal/components/task-8.ics
new file mode 100644
index 0000000..f3ca458
--- /dev/null
+++ b/tests/libedata-cal/components/task-8.ics
@@ -0,0 +1,11 @@
+BEGIN:VTODO
+UID:task-8
+DTSTAMP:20170221T130109Z
+SUMMARY:Status - Cancelled
+STATUS:CANCELLED
+PERCENT-COMPLETE:33
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130727Z
+LAST-MODIFIED:20170221T130727Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-9.ics b/tests/libedata-cal/components/task-9.ics
new file mode 100644
index 0000000..af66b26
--- /dev/null
+++ b/tests/libedata-cal/components/task-9.ics
@@ -0,0 +1,11 @@
+BEGIN:VTODO
+UID:task-9
+DTSTAMP:20170221T130109Z
+SUMMARY:With start date
+DTSTART;TZID=America/New_York:20170213T000000
+PERCENT-COMPLETE:0
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T132838Z
+LAST-MODIFIED:20170221T132838Z
+END:VTODO
diff --git a/tests/libedata-cal/test-cal-cache-getters.c b/tests/libedata-cal/test-cal-cache-getters.c
new file mode 100644
index 0000000..4c05413
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-getters.c
@@ -0,0 +1,247 @@
+/* -*- 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/>.
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+static ECalComponentId *
+extract_id_from_component (ECalComponent *component)
+{
+       ECalComponentId *id;
+
+       g_assert (component != NULL);
+
+       id = e_cal_component_get_id (component);
+       g_assert (id != NULL);
+       g_assert (id->uid != NULL);
+
+       return id;
+}
+
+static ECalComponentId *
+extract_id_from_string (const gchar *icalstring)
+{
+       ECalComponent *component;
+       ECalComponentId *id;
+
+       g_assert (icalstring != NULL);
+
+       component = e_cal_component_new_from_string (icalstring);
+       g_assert (component != NULL);
+
+       id = extract_id_from_component (component);
+
+       g_object_unref (component);
+
+       return id;
+}
+
+static void
+test_get_one (ECalCache *cal_cache,
+             const gchar *uid,
+             const gchar *rid,
+             gboolean expect_failure)
+{
+       ECalComponent *component = NULL;
+       ECalComponentId *id;
+       gchar *icalstring = NULL;
+       gboolean success;
+       GError *error = NULL;
+
+       success = e_cal_cache_get_component (cal_cache, uid, rid, &component, NULL, &error);
+       if (expect_failure) {
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_assert (!success);
+               g_assert (!component);
+
+               g_clear_error (&error);
+       } else {
+               g_assert_no_error (error);
+               g_assert (success);
+               g_assert_nonnull (component);
+
+               id = extract_id_from_component (component);
+
+               g_assert_cmpstr (id->uid, ==, uid);
+               g_assert_cmpstr (id->rid, ==, rid && *rid ? rid : NULL);
+
+               e_cal_component_free_id (id);
+               g_object_unref (component);
+       }
+
+       success = e_cal_cache_get_component_as_string (cal_cache, uid, rid, &icalstring, NULL, &error);
+       if (expect_failure) {
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_assert (!success);
+               g_assert (!icalstring);
+
+               g_clear_error (&error);
+       } else {
+               g_assert_no_error (error);
+               g_assert (success);
+               g_assert_nonnull (icalstring);
+
+               id = extract_id_from_string (icalstring);
+
+               g_assert_cmpstr (id->uid, ==, uid);
+               g_assert_cmpstr (id->rid, ==, rid && *rid ? rid : NULL);
+
+               e_cal_component_free_id (id);
+               g_free (icalstring);
+       }
+}
+
+static void
+test_getters_one (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       test_get_one (fixture->cal_cache, "unexistent-event", NULL, TRUE);
+       test_get_one (fixture->cal_cache, "unexistent-event", "", TRUE);
+       test_get_one (fixture->cal_cache, "event-2", NULL, FALSE);
+       test_get_one (fixture->cal_cache, "event-2", "", FALSE);
+       test_get_one (fixture->cal_cache, "event-5", NULL, FALSE);
+       test_get_one (fixture->cal_cache, "event-5", "", FALSE);
+       test_get_one (fixture->cal_cache, "event-5", "20131231T000000Z", TRUE);
+       test_get_one (fixture->cal_cache, "event-6", NULL, FALSE);
+       test_get_one (fixture->cal_cache, "event-6", "", FALSE);
+       test_get_one (fixture->cal_cache, "event-6", "20170225T134900", FALSE);
+}
+
+/* NULL-terminated list of pairs <uid, rid>, what to expect */
+static void
+test_get_all (ECalCache *cal_cache,
+             const gchar *uid,
+             ...)
+{
+       ECalComponentId *id;
+       GSList *items, *link;
+       va_list va;
+       const gchar *tmp;
+       GHashTable *expects;
+       gboolean success;
+       GError *error = NULL;
+
+       expects = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc) 
e_cal_component_id_equal,
+               (GDestroyNotify) e_cal_component_free_id, NULL);
+
+       va_start (va, uid);
+       tmp = va_arg (va, const gchar *);
+       while (tmp) {
+               const gchar *rid = va_arg (va, const gchar *);
+               id = e_cal_component_id_new (tmp, rid);
+
+               g_hash_table_insert (expects, id, NULL);
+
+               tmp = va_arg (va, const gchar *);
+       }
+       va_end (va);
+
+       items = NULL;
+
+       success = e_cal_cache_get_components_by_uid (cal_cache, uid, &items, NULL, &error);
+       if (!g_hash_table_size (expects)) {
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_assert (!success);
+               g_assert (!items);
+
+               g_clear_error (&error);
+       } else {
+               g_assert_no_error (error);
+               g_assert (success);
+               g_assert_nonnull (items);
+
+               g_assert_cmpint (g_hash_table_size (expects), ==, g_slist_length (items));
+
+               for (link = items; link; link = g_slist_next (link)) {
+                       id = extract_id_from_component (link->data);
+
+                       g_assert_cmpstr (id->uid, ==, uid);
+                       g_assert (g_hash_table_contains (expects, id));
+
+                       e_cal_component_free_id (id);
+               }
+
+               g_slist_free_full (items, g_object_unref);
+       }
+
+       items = NULL;
+
+       success = e_cal_cache_get_components_by_uid_as_string (cal_cache, uid, &items, NULL, &error);
+       if (!g_hash_table_size (expects)) {
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_assert (!success);
+               g_assert (!items);
+
+               g_clear_error (&error);
+       } else {
+               g_assert_no_error (error);
+               g_assert (success);
+               g_assert_nonnull (items);
+
+               g_assert_cmpint (g_hash_table_size (expects), ==, g_slist_length (items));
+
+               for (link = items; link; link = g_slist_next (link)) {
+                       id = extract_id_from_string (link->data);
+
+                       g_assert_cmpstr (id->uid, ==, uid);
+                       g_assert (g_hash_table_contains (expects, id));
+
+                       e_cal_component_free_id (id);
+               }
+
+               g_slist_free_full (items, g_free);
+       }
+
+       g_hash_table_destroy (expects);
+}
+
+static void
+test_getters_all (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       test_get_all (fixture->cal_cache, "unexistent-event", NULL);
+       test_get_all (fixture->cal_cache, "unexistent-event", NULL);
+       test_get_all (fixture->cal_cache, "event-2", "event-2", NULL, NULL);
+       test_get_all (fixture->cal_cache, "event-5", "event-5", NULL, NULL);
+       test_get_all (fixture->cal_cache, "event-6", "event-6", NULL, "event-6", "20170225T134900", NULL);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUClosure closure_events = { TCU_LOAD_COMPONENT_SET_EVENTS };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       g_test_add ("/ECalCache/Getters/One", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_getters_one, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Getters/All", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_getters_all, tcu_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-intervals.c b/tests/libedata-cal/test-cal-cache-intervals.c
new file mode 100644
index 0000000..7d32622
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-intervals.c
@@ -0,0 +1,344 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+#define NUM_INTERVALS_CLOSED   100
+#define NUM_INTERVALS_OPEN     100
+#define NUM_SEARCHES           500
+#define DELETE_PROBABILITY     0.3
+#define _TIME_MIN              ((time_t) 0)            /* Min valid time_t     */
+#define _TIME_MAX              ((time_t) INT_MAX)      /* Max valid time_t     */
+
+typedef struct _IntervalData {
+       gint start;
+       gint end;
+       ECalComponent * comp;
+} IntervalData;
+
+static void
+interval_data_free (gpointer ptr)
+{
+       IntervalData *id = ptr;
+
+       if (id) {
+               g_object_unref (id->comp);
+               g_free (id);
+       }
+}
+
+static gint
+compare_intervals (time_t x_start,
+                  time_t x_end,
+                  time_t y_start,
+                  time_t y_end)
+{
+       /* assumption: x_start <= x_end */
+       /* assumption: y_start <= y_end */
+
+       /* x is left of y */
+       if (x_end < y_start)
+               return -1;
+
+       /* x is right of y */
+       if (y_end < x_start)
+               return 1;
+
+       /* x and y overlap */
+       return 0;
+}
+
+static GHashTable *
+search_in_intervals (ETimezoneCache *zone_cache,
+                    GSList *intervals,
+                    time_t start,
+                    time_t end)
+{
+       ECalBackendSExp *sexp;
+       struct icaltimetype itt_start, itt_end;
+       gchar *expr;
+       GSList *link;
+       GHashTable *res;
+
+       itt_start = icaltime_from_timet_with_zone (start, FALSE, NULL);
+       itt_end = icaltime_from_timet_with_zone (end, FALSE, NULL);
+
+       expr = g_strdup_printf ("(occur-in-time-range? (make-time \"%04d%02d%02dT%02d%02d%02dZ\") (make-time 
\"%04d%02d%02dT%02d%02d%02dZ\"))",
+               itt_start.year, itt_start.month, itt_start.day, itt_start.hour, itt_start.minute, 
itt_start.second,
+               itt_end.year, itt_end.month, itt_end.day, itt_end.hour, itt_end.minute, itt_end.second);
+
+       sexp = e_cal_backend_sexp_new (expr);
+
+       g_free (expr);
+
+       g_assert_nonnull (sexp);
+
+       res = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc) 
e_cal_component_id_equal,
+               (GDestroyNotify) e_cal_component_free_id, g_object_unref);
+
+       for (link = intervals; link; link = g_slist_next (link)) {
+               IntervalData *data = link->data;
+
+               if (compare_intervals (start, end, data->start, data->end) == 0 &&
+                   e_cal_backend_sexp_match_comp (sexp, data->comp, zone_cache)) {
+                       ECalComponentId *id = NULL;
+
+                       id = e_cal_component_get_id (data->comp);
+                       g_assert_nonnull (id);
+
+                       g_hash_table_insert (res, id, g_object_ref (data->comp));
+               }
+       }
+
+       g_object_unref (sexp);
+
+       return res;
+}
+
+static void
+check_search_results (GSList *ecalcomps,
+                     GHashTable *from_intervals)
+{
+       GSList *link;
+
+       g_assert_cmpint (g_slist_length (ecalcomps), ==, g_hash_table_size (from_intervals));
+
+       for (link = ecalcomps; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+               ECalComponentId *id = NULL;
+
+               id = e_cal_component_get_id (comp);
+               g_assert_nonnull (id);
+
+               g_assert (g_hash_table_contains (from_intervals, id));
+
+               e_cal_component_free_id (id);
+       }
+}
+
+static ECalComponent *
+create_test_component (time_t start,
+                      time_t end)
+{
+       ECalComponent *comp;
+       ECalComponentText summary;
+       struct icaltimetype current, ittstart, ittend;
+
+       comp = e_cal_component_new ();
+
+       e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_EVENT);
+
+       ittstart = icaltime_from_timet_with_zone (start, 0, NULL);
+       ittend = icaltime_from_timet_with_zone (end, 0, NULL);
+
+       icalcomponent_set_dtstart (e_cal_component_get_icalcomponent (comp), ittstart);
+       if (end != _TIME_MAX)
+               icalcomponent_set_dtend (e_cal_component_get_icalcomponent (comp), ittend);
+
+       summary.value = g_strdup_printf ("%s - %s", icaltime_as_ical_string (ittstart), 
icaltime_as_ical_string (ittend));
+       summary.altrep = NULL;
+
+       e_cal_component_set_summary (comp, &summary);
+
+       g_free ((gchar *) summary.value);
+
+       current = icaltime_from_timet_with_zone (time (NULL), 0, NULL);
+       e_cal_component_set_created (comp, &current);
+       e_cal_component_set_last_modified (comp, &current);
+
+       e_cal_component_rescan (comp);
+
+       return comp;
+}
+
+static void
+test_intervals (TCUFixture *fixture,
+               gconstpointer user_data)
+{
+       /*
+        * outline:
+        * 1. create new tree and empty list of intervals
+        * 2. insert some intervals into tree and list
+        * 3. do various searches, compare results of both structures
+        * 4. delete some intervals
+        * 5. do various searches, compare results of both structures
+        * 6. free memory
+        */
+       GRand *myrand;
+       IntervalData *interval;
+       ECalComponent *comp;
+       ETimezoneCache *zone_cache;
+       GSList *l1, *intervals = NULL;
+       GHashTable *from_intervals;
+       gint num_deleted = 0;
+       gint ii, start, end;
+       gboolean success;
+       GError *error = NULL;
+
+       zone_cache = E_TIMEZONE_CACHE (fixture->cal_cache);
+
+       myrand = g_rand_new ();
+
+       for (ii = 0; ii < NUM_INTERVALS_CLOSED; ii++) {
+               start = g_rand_int_range (myrand, 0, 1000);
+               end = g_rand_int_range (myrand, start, 2000);
+               comp = create_test_component (start, end);
+               g_assert (comp != NULL);
+
+               interval = g_new (IntervalData, 1);
+               interval->start = start;
+               interval->end = end;
+               interval->comp = comp;
+
+               intervals = g_slist_prepend (intervals, interval);
+
+               success = e_cal_cache_put_component (fixture->cal_cache, comp, NULL, E_OFFLINE_STATE_SYNCED, 
NULL, &error);
+               g_assert_no_error (error);
+               g_assert (success);
+       }
+
+       end = _TIME_MAX;
+
+       /* insert open ended intervals */
+       for (ii = 0; ii < NUM_INTERVALS_OPEN; ii++) {
+               start = g_rand_int_range (myrand, 0, 1000);
+               comp = create_test_component (start, end);
+               g_assert (comp != NULL);
+
+               interval = g_new (IntervalData, 1);
+               interval->start = start;
+               interval->end = end;
+               interval->comp = comp;
+
+               intervals = g_slist_prepend (intervals, interval);
+
+               success = e_cal_cache_put_component (fixture->cal_cache, comp, NULL, E_OFFLINE_STATE_SYNCED, 
NULL, &error);
+               g_assert_no_error (error);
+               g_assert (success);
+       }
+
+       for (ii = 0; ii < NUM_SEARCHES; ii++) {
+               start = g_rand_int_range (myrand, 0, 1000);
+               end = g_rand_int_range (myrand, 2000, _TIME_MAX);
+
+               l1 = NULL;
+
+               success = e_cal_cache_get_components_in_range (fixture->cal_cache, start, end, &l1, NULL, 
&error);
+               g_assert_no_error (error);
+               g_assert (success);
+
+               from_intervals = search_in_intervals (zone_cache, intervals, start, end);
+
+               check_search_results (l1, from_intervals);
+
+               g_slist_free_full (l1, g_object_unref);
+               g_hash_table_destroy (from_intervals);
+       }
+
+       /* open-ended intervals */
+       for (ii = 0; ii < 20; ii++) {
+               start = g_rand_int_range (myrand, 0, 1000);
+               end = _TIME_MAX;
+
+               l1 = NULL;
+
+               success = e_cal_cache_get_components_in_range (fixture->cal_cache, start, end, &l1, NULL, 
&error);
+               g_assert_no_error (error);
+               g_assert (success);
+
+               from_intervals = search_in_intervals (zone_cache, intervals, start, end);
+
+               check_search_results (l1, from_intervals);
+
+               g_slist_free_full (l1, g_object_unref);
+               g_hash_table_destroy (from_intervals);
+       }
+
+       l1 = intervals;
+
+       while (l1) {
+               /* perhaps we will delete l1 */
+               GSList *next = l1->next;
+
+               if (g_rand_double (myrand) < DELETE_PROBABILITY) {
+                       ECalComponent *comp;
+                       ECalComponentId *id;
+
+                       interval = l1->data;
+                       comp = interval->comp;
+
+                       id = e_cal_component_get_id (comp);
+                       g_assert (id != NULL);
+
+                       success = e_cal_cache_remove_component (fixture->cal_cache, id->uid, id->rid, 
E_OFFLINE_STATE_SYNCED, NULL, &error);
+                       g_assert_no_error (error);
+                       g_assert (success);
+
+                       e_cal_component_free_id (id);
+
+                       interval_data_free (interval);
+                       intervals = g_slist_delete_link (intervals, l1);
+
+                       num_deleted++;
+               }
+
+               l1 = next;
+       }
+
+       for (ii = 0; ii < NUM_SEARCHES; ii++) {
+               start = g_rand_int_range (myrand, 0, 1000);
+               end = g_rand_int_range (myrand, start + 1, 2000);
+
+               l1 = NULL;
+
+               success = e_cal_cache_get_components_in_range (fixture->cal_cache, start, end, &l1, NULL, 
&error);
+               g_assert_no_error (error);
+               g_assert (success);
+
+               from_intervals = search_in_intervals (zone_cache, intervals, start, end);
+
+               check_search_results (l1, from_intervals);
+
+               g_slist_free_full (l1, g_object_unref);
+               g_hash_table_destroy (from_intervals);
+       }
+
+       g_slist_free_full (intervals, interval_data_free);
+       g_rand_free (myrand);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       g_test_add ("/ECalCache/Intervals", TCUFixture, NULL,
+               tcu_fixture_setup, test_intervals, tcu_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-offline.c b/tests/libedata-cal/test-cal-cache-offline.c
new file mode 100644
index 0000000..a831157
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-offline.c
@@ -0,0 +1,1043 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+static void
+test_fill_cache (TCUFixture *fixture,
+                ECalComponent **out_component)
+{
+       tcu_add_component_from_test_case (fixture, "event-1", out_component);
+       tcu_add_component_from_test_case (fixture, "event-2", NULL);
+       tcu_add_component_from_test_case (fixture, "event-5", NULL);
+}
+
+enum {
+       EXPECT_DEFAULT          = (0),
+       EXPECT_EVENT_1          = (1 << 0),
+       EXPECT_EVENT_2          = (1 << 1),
+       EXPECT_EVENT_3          = (1 << 2),
+       EXPECT_EVENT_4          = (1 << 3),
+       HAS_SEARCH_DATA         = (1 << 4),
+       SKIP_COMPONENT_PUT      = (1 << 5)
+};
+
+static void
+test_check_search_result (const GSList *list,
+                         guint32 flags)
+{
+       gboolean expect_event_1 = (flags & EXPECT_EVENT_1) != 0;
+       gboolean expect_event_2 = (flags & EXPECT_EVENT_2) != 0;
+       gboolean expect_event_3 = (flags & EXPECT_EVENT_3) != 0;
+       gboolean expect_event_4 = (flags & EXPECT_EVENT_4) != 0;
+       gboolean has_search_data = (flags & HAS_SEARCH_DATA) != 0;
+       gboolean have_event_1 = FALSE;
+       gboolean have_event_2 = FALSE;
+       gboolean have_event_3 = FALSE;
+       gboolean have_event_4 = FALSE;
+       gboolean have_event_5 = FALSE;
+       const GSList *link;
+
+       for (link = list; link; link = g_slist_next (link)) {
+               const gchar *uid;
+
+               if (has_search_data) {
+                       ECalCacheSearchData *sd = link->data;
+                       ECalComponent *component;
+
+                       g_assert (sd != NULL);
+                       g_assert (sd->uid != NULL);
+                       g_assert (sd->object != NULL);
+
+                       uid = sd->uid;
+
+                       component = e_cal_component_new_from_string (sd->object);
+                       g_assert (E_IS_CAL_COMPONENT (component));
+                       g_assert_cmpstr (uid, ==, icalcomponent_get_uid (e_cal_component_get_icalcomponent 
(component)));
+                       g_assert_nonnull (icalcomponent_get_summary (e_cal_component_get_icalcomponent 
(component)));
+
+                       g_clear_object (&component);
+               } else {
+                       const ECalComponentId *id = link->data;
+
+                       g_assert (id != NULL);
+                       g_assert (id->uid != NULL);
+
+                       uid = id->uid;
+               }
+
+               g_assert_nonnull (uid);
+
+               if (g_str_equal (uid, "event-1")) {
+                       g_assert (expect_event_1);
+                       g_assert (!have_event_1);
+                       have_event_1 = TRUE;
+               } else if (g_str_equal (uid, "event-2")) {
+                       g_assert (!have_event_2);
+                       have_event_2 = TRUE;
+               } else if (g_str_equal (uid, "event-3")) {
+                       g_assert (expect_event_3);
+                       g_assert (!have_event_3);
+                       have_event_3 = TRUE;
+               } else if (g_str_equal (uid, "event-4")) {
+                       g_assert (expect_event_4);
+                       g_assert (!have_event_4);
+                       have_event_4 = TRUE;
+               } else if (g_str_equal (uid, "event-5")) {
+                       g_assert (!have_event_5);
+                       have_event_5 = TRUE;
+               } else {
+                       /* It's not supposed to be NULL, but it will print the value of 'uid' */
+                       g_assert_cmpstr (uid, ==, NULL);
+               }
+       }
+
+       g_assert ((expect_event_1 && have_event_1) || (!expect_event_1 && !have_event_1));
+       g_assert ((expect_event_2 && have_event_2) || (!expect_event_2 && !have_event_2));
+       g_assert ((expect_event_3 && have_event_3) || (!expect_event_3 && !have_event_3));
+       g_assert ((expect_event_4 && have_event_4) || (!expect_event_4 && !have_event_4));
+       g_assert (have_event_5);
+}
+
+static void
+test_basic_search (TCUFixture *fixture,
+                  guint32 flags)
+{
+       GSList *list = NULL;
+       const gchar *sexp;
+       gint expect_total;
+       GError *error = NULL;
+
+       expect_total = 2 +
+               ((flags & EXPECT_EVENT_1) != 0 ? 1 : 0) +
+               ((flags & EXPECT_EVENT_3) != 0 ? 1 : 0) +
+               ((flags & EXPECT_EVENT_4) != 0 ? 1 : 0);
+
+       /* All components first */
+       g_assert (e_cal_cache_search (fixture->cal_cache, NULL, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, expect_total);
+       test_check_search_result (list, flags | EXPECT_EVENT_2 | HAS_SEARCH_DATA);
+       g_slist_free_full (list, e_cal_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_cal_cache_search_ids (fixture->cal_cache, NULL, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, expect_total);
+       test_check_search_result (list, flags | EXPECT_EVENT_2);
+       g_slist_free_full (list, (GDestroyNotify) e_cal_component_free_id);
+       list = NULL;
+
+       /* Only Party, aka event-5, as an in-summary query */
+       sexp = "(has-categories? \"Holiday\")";
+
+       g_assert (e_cal_cache_search (fixture->cal_cache, sexp, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, HAS_SEARCH_DATA);
+       g_slist_free_full (list, e_cal_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_cal_cache_search_ids (fixture->cal_cache, sexp, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, EXPECT_DEFAULT);
+       g_slist_free_full (list, (GDestroyNotify) e_cal_component_free_id);
+       list = NULL;
+
+       /* Only Party, aka event-5, as a non-summarised query */
+       sexp = "(has-alarms-in-range? (make-time \"20091229T230000Z\") (make-time \"20091231T010000Z\"))";
+
+       g_assert (e_cal_cache_search (fixture->cal_cache, sexp, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, HAS_SEARCH_DATA);
+       g_slist_free_full (list, e_cal_cache_search_data_free);
+       list = NULL;
+
+       g_assert (e_cal_cache_search_ids (fixture->cal_cache, sexp, &list, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (list), ==, 1);
+       test_check_search_result (list, EXPECT_DEFAULT);
+       g_slist_free_full (list, (GDestroyNotify) e_cal_component_free_id);
+       list = NULL;
+
+       /* Invalid expression */
+       g_assert (!e_cal_cache_search (fixture->cal_cache, "invalid expression here", &list, NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+       g_assert_null (list);
+       g_clear_error (&error);
+
+       g_assert (!e_cal_cache_search_ids (fixture->cal_cache, "invalid expression here", &list, NULL, 
&error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+       g_assert_null (list);
+       g_clear_error (&error);
+}
+
+/* Expects pairs of UID (gchar *) and EOfflineState (gint), terminated by NULL */
+static void
+test_check_offline_changes (TCUFixture *fixture,
+                           ...) G_GNUC_NULL_TERMINATED;
+
+static void
+test_check_offline_changes (TCUFixture *fixture,
+                           ...)
+{
+       GSList *changes, *link;
+       va_list args;
+       GHashTable *expects;
+       const gchar *uid;
+       GError *error = NULL;
+
+       changes = e_cache_get_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error);
+
+       g_assert_no_error (error);
+
+       expects = g_hash_table_new (g_str_hash, g_str_equal);
+
+       va_start (args, fixture);
+       uid = va_arg (args, const gchar *);
+       while (uid) {
+               gint state = va_arg (args, gint);
+
+               g_hash_table_insert (expects, (gpointer) uid, GINT_TO_POINTER (state));
+               uid = va_arg (args, const gchar *);
+       }
+       va_end (args);
+
+       g_assert_cmpint (g_slist_length (changes), ==, g_hash_table_size (expects));
+
+       for (link = changes; link; link = g_slist_next (link)) {
+               ECacheOfflineChange *change = link->data;
+               gint expect_state;
+
+               g_assert_nonnull (change);
+               g_assert (g_hash_table_contains (expects, change->uid));
+
+               expect_state = GPOINTER_TO_INT (g_hash_table_lookup (expects, change->uid));
+               g_assert_cmpint (expect_state, ==, change->state);
+       }
+
+       g_slist_free_full (changes, e_cache_offline_change_free);
+       g_hash_table_destroy (expects);
+}
+
+static EOfflineState
+test_check_offline_state (TCUFixture *fixture,
+                         const gchar *uid,
+                         EOfflineState expect_offline_state)
+{
+       EOfflineState offline_state;
+       GError *error = NULL;
+
+       offline_state = e_cache_get_offline_state (E_CACHE (fixture->cal_cache), uid, NULL, &error);
+       g_assert_cmpint (offline_state, ==, expect_offline_state);
+
+       if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_clear_error (&error);
+       } else {
+               g_assert_no_error (error);
+       }
+
+       return offline_state;
+}
+
+static void
+test_check_edit_saved (TCUFixture *fixture,
+                      const gchar *uid,
+                      const gchar *summ_value)
+{
+       ECalComponent *component = NULL;
+       GError *error = NULL;
+
+       g_assert (e_cal_cache_get_component (fixture->cal_cache, uid, NULL, &component, NULL, &error));
+       g_assert_no_error (error);
+       g_assert_nonnull (component);
+       g_assert_cmpstr (icalcomponent_get_summary (e_cal_component_get_icalcomponent (component)), ==, 
summ_value);
+
+       g_clear_object (&component);
+}
+
+static void
+test_verify_storage (TCUFixture *fixture,
+                    const gchar *uid,
+                    const gchar *expect_summ,
+                    const gchar *expect_extra,
+                    EOfflineState expect_offline_state)
+{
+       ECalComponent *component = NULL;
+       EOfflineState offline_state;
+       gchar *saved_extra = NULL;
+       GError *error = NULL;
+
+       if (expect_offline_state == E_OFFLINE_STATE_LOCALLY_DELETED ||
+           expect_offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               g_assert (!e_cal_cache_get_component (fixture->cal_cache, uid, NULL, &component, NULL, 
&error));
+               g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+               g_assert_null (component);
+
+               g_clear_error (&error);
+       } else {
+               g_assert (e_cal_cache_get_component (fixture->cal_cache, uid, NULL, &component, NULL, 
&error));
+               g_assert_no_error (error);
+               g_assert_nonnull (component);
+       }
+
+       offline_state = test_check_offline_state (fixture, uid, expect_offline_state);
+
+       if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+               g_assert (!e_cal_cache_contains (fixture->cal_cache, uid, NULL, E_CACHE_EXCLUDE_DELETED));
+               g_assert (!e_cal_cache_contains (fixture->cal_cache, uid, NULL, E_CACHE_INCLUDE_DELETED));
+               test_check_offline_changes (fixture, NULL);
+               return;
+       }
+
+       g_assert (e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, &saved_extra, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpstr (saved_extra, ==, expect_extra);
+       g_assert_cmpstr (icalcomponent_get_summary (e_cal_component_get_icalcomponent (component)), ==, 
expect_summ);
+
+       g_clear_object (&component);
+       g_free (saved_extra);
+
+       if (expect_offline_state == E_OFFLINE_STATE_SYNCED)
+               test_check_offline_changes (fixture, NULL);
+       else
+               test_check_offline_changes (fixture, uid, expect_offline_state, NULL);
+}
+
+static void
+test_offline_basics (TCUFixture *fixture,
+                    gconstpointer user_data)
+{
+       EOfflineState states[] = {
+               E_OFFLINE_STATE_LOCALLY_CREATED,
+               E_OFFLINE_STATE_LOCALLY_MODIFIED,
+               E_OFFLINE_STATE_LOCALLY_DELETED,
+               E_OFFLINE_STATE_SYNCED
+       };
+       ECalComponent *component = NULL;
+       gint ii;
+       const gchar *uid;
+       gchar *saved_extra = NULL, *tmp;
+       GError *error = NULL;
+
+       /* Basic ECache stuff */
+       e_cache_set_version (E_CACHE (fixture->cal_cache), 123);
+       g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->cal_cache)), ==, 123);
+
+       e_cache_set_revision (E_CACHE (fixture->cal_cache), "rev-321");
+       tmp = e_cache_dup_revision (E_CACHE (fixture->cal_cache));
+       g_assert_cmpstr ("rev-321", ==, tmp);
+       g_free (tmp);
+
+       g_assert (e_cache_set_key (E_CACHE (fixture->cal_cache), "my-key-str", "key-str-value", &error));
+       g_assert_no_error (error);
+
+       tmp = e_cache_dup_key (E_CACHE (fixture->cal_cache), "my-key-str", &error);
+       g_assert_no_error (error);
+       g_assert_cmpstr ("key-str-value", ==, tmp);
+       g_free (tmp);
+
+       g_assert (e_cache_set_key_int (E_CACHE (fixture->cal_cache), "version", 567, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_key_int (E_CACHE (fixture->cal_cache), "version", &error), ==, 567);
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->cal_cache)), ==, 123);
+
+       /* Add in online */
+       test_fill_cache (fixture, &component);
+       g_assert_nonnull (component);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       g_assert (e_cal_cache_set_component_extra (fixture->cal_cache, uid, NULL, "extra-0", NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert (e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, &saved_extra, NULL, 
&error));
+       g_assert_no_error (error);
+       g_assert_cmpstr (saved_extra, ==, "extra-0");
+
+       g_free (saved_extra);
+       saved_extra = NULL;
+
+       icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-0");
+
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Try change status */
+       for (ii = 0; ii < G_N_ELEMENTS (states); ii++) {
+               g_assert (e_cache_set_offline_state (E_CACHE (fixture->cal_cache), uid, states[ii], NULL, 
&error));
+               g_assert_no_error (error);
+
+               test_check_offline_state (fixture, uid, states[ii]);
+
+               if (states[ii] != E_OFFLINE_STATE_SYNCED)
+                       test_check_offline_changes (fixture, uid, states[ii], NULL);
+
+               if (states[ii] == E_OFFLINE_STATE_LOCALLY_DELETED) {
+                       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), 
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 2);
+                       g_assert_no_error (error);
+
+                       g_assert (!e_cal_cache_contains (fixture->cal_cache, uid, NULL, 
E_CACHE_EXCLUDE_DELETED));
+
+                       g_assert (e_cal_cache_set_component_extra (fixture->cal_cache, uid, NULL, "extra-1", 
NULL, &error));
+                       g_assert_no_error (error);
+
+                       g_assert (e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, 
&saved_extra, NULL, &error));
+                       g_assert_no_error (error);
+                       g_assert_cmpstr (saved_extra, ==, "extra-1");
+
+                       g_free (saved_extra);
+                       saved_extra = NULL;
+
+                       /* Search when locally deleted */
+                       test_basic_search (fixture, EXPECT_DEFAULT);
+               } else {
+                       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), 
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 3);
+                       g_assert_no_error (error);
+
+                       g_assert (e_cal_cache_contains (fixture->cal_cache, uid, NULL, 
E_CACHE_EXCLUDE_DELETED));
+
+                       /* Search when locally available */
+                       test_basic_search (fixture, EXPECT_EVENT_1);
+               }
+
+               g_assert (e_cal_cache_contains (fixture->cal_cache, uid, NULL, E_CACHE_INCLUDE_DELETED));
+
+               g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, 
NULL, &error), ==, 3);
+               g_assert_no_error (error);
+       }
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Edit in online */
+       icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-1");
+
+       g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_ONLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_verify_storage (fixture, uid, "summ-1", NULL, E_OFFLINE_STATE_SYNCED);
+       test_check_offline_changes (fixture, NULL);
+
+       icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-2");
+
+       g_assert (e_cal_cache_put_component (fixture->cal_cache, component, "extra-2", E_CACHE_IS_ONLINE, 
NULL, &error));
+       g_assert_no_error (error);
+
+       test_verify_storage (fixture, uid, "summ-2", "extra-2", E_OFFLINE_STATE_SYNCED);
+       test_check_offline_changes (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       /* Search before delete */
+       test_basic_search (fixture, EXPECT_EVENT_1);
+
+       /* Delete in online */
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_ONLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert (!e_cache_set_offline_state (E_CACHE (fixture->cal_cache), uid, 
E_OFFLINE_STATE_LOCALLY_MODIFIED, NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_clear_error (&error);
+
+       test_verify_storage (fixture, uid, NULL, NULL, E_OFFLINE_STATE_UNKNOWN);
+       test_check_offline_changes (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_no_error (error);
+
+       g_assert (!e_cal_cache_set_component_extra (fixture->cal_cache, uid, NULL, "extra-3", NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_clear_error (&error);
+
+       g_assert (!e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, &saved_extra, NULL, 
&error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_assert_null (saved_extra);
+       g_clear_error (&error);
+
+       g_clear_object (&component);
+
+       /* Search after delete */
+       test_basic_search (fixture, EXPECT_DEFAULT);
+}
+
+static void
+test_offline_add_one (TCUFixture *fixture,
+                     const gchar *case_name,
+                     gint expect_total,
+                     guint32 flags,
+                     ECalComponent **out_component)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       if (!(flags & SKIP_COMPONENT_PUT)) {
+               component = tcu_new_component_from_test_case (case_name);
+               g_assert_nonnull (component);
+
+               uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+               g_assert_nonnull (uid);
+
+               test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+               /* Add a component in offline */
+               g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, 
NULL, &error));
+               g_assert_no_error (error);
+       } else {
+               uid = case_name;
+       }
+
+       if ((flags & EXPECT_EVENT_3) != 0) {
+               test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_CREATED);
+       } else {
+               test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+       }
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, expect_total);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, flags);
+
+       if (out_component)
+               *out_component = component;
+       else
+               g_clear_object (&component);
+}
+
+static void
+test_offline_add (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add the first in offline */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       /* Add the second in offline */
+       test_offline_add_one (fixture, "event-4", 5, EXPECT_EVENT_3 | EXPECT_EVENT_4 | EXPECT_EVENT_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               "event-4", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+}
+
+static void
+test_offline_add_edit (TCUFixture *fixture,
+                      gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, &component);
+       g_assert_nonnull (component);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       /* Modify added in offline */
+       icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-2");
+
+       g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1 | SKIP_COMPONENT_PUT, 
NULL);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       test_check_edit_saved (fixture, "event-3", "summ-2");
+
+       g_clear_object (&component);
+}
+
+static void
+test_offline_add_delete (TCUFixture *fixture,
+                        gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, &component);
+       g_assert_nonnull (component);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       /* Delete added in offline */
+
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_offline_add_one (fixture, "event-3", 3, EXPECT_EVENT_1 | SKIP_COMPONENT_PUT, NULL);
+
+       test_check_offline_changes (fixture, NULL);
+
+       g_clear_object (&component);
+}
+
+static void
+test_offline_add_delete_add (TCUFixture *fixture,
+                            gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, &component);
+       g_assert_nonnull (component);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       /* Delete added in offline */
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       test_offline_add_one (fixture, "event-3", 3, EXPECT_EVENT_1 | SKIP_COMPONENT_PUT, NULL);
+
+       test_check_offline_changes (fixture, NULL);
+
+       g_clear_object (&component);
+
+       /* Add in offline again */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+}
+
+static void
+test_offline_add_resync (TCUFixture *fixture,
+                        gconstpointer user_data)
+{
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+
+       /* Add in offline */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+
+       test_check_offline_changes (fixture,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       /* Resync all offline changes */
+       g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 4);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_EVENT_3 | EXPECT_EVENT_1);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_SYNCED);
+}
+
+static void
+test_offline_edit_common (TCUFixture *fixture,
+                         gchar **out_uid)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &component);
+       g_assert_nonnull (component);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Modify in offline */
+       icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-2");
+
+       g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_edit_saved (fixture, uid, "summ-2");
+
+       test_basic_search (fixture, EXPECT_EVENT_1);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+
+       if (out_uid)
+               *out_uid = g_strdup (uid);
+
+       g_clear_object (&component);
+}
+
+static void
+test_offline_edit (TCUFixture *fixture,
+                  gconstpointer user_data)
+{
+       test_offline_edit_common (fixture, NULL);
+}
+
+static void
+test_offline_edit_delete (TCUFixture *fixture,
+                         gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       gchar *uid = NULL;
+       GError *error = NULL;
+
+       test_offline_edit_common (fixture, &uid);
+
+       /* Delete the modified component in offline */
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       g_assert (!e_cal_cache_get_component (fixture->cal_cache, uid, FALSE, &component, NULL, &error));
+       g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+       g_assert_null (component);
+
+       g_clear_error (&error);
+       g_free (uid);
+}
+
+static void
+test_offline_edit_resync (TCUFixture *fixture,
+                         gconstpointer user_data)
+{
+       gchar *uid = NULL;
+       GError *error = NULL;
+
+       test_offline_edit_common (fixture, &uid);
+
+       /* Resync all offline changes */
+       g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_EVENT_1);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       g_free (uid);
+}
+
+static void
+test_offline_delete (TCUFixture *fixture,
+                    gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &component);
+       g_assert_nonnull (component);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Delete in offline */
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       g_clear_object (&component);
+}
+
+static void
+test_offline_delete_add (TCUFixture *fixture,
+                        gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &component);
+       g_assert_nonnull (component);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Delete locally created in offline */
+       test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, "event-3", NULL, E_CACHE_IS_OFFLINE, 
NULL, &error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_EVENT_1);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+       test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_UNKNOWN);
+
+       /* Delete synced in offline */
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       /* Add one in offline */
+       test_offline_add_one (fixture, "event-3", 3, EXPECT_EVENT_3, NULL);
+
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+       test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+       /* Modify the previous component and add it again */
+       icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-3");
+
+       g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 4);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 4);
+       g_assert_no_error (error);
+
+       test_check_edit_saved (fixture, uid, "summ-3");
+
+       test_basic_search (fixture, EXPECT_EVENT_1 | EXPECT_EVENT_3);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+               "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+       test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+       g_clear_object (&component);
+}
+
+static void
+test_offline_delete_resync (TCUFixture *fixture,
+                           gconstpointer user_data)
+{
+       ECalComponent *component = NULL;
+       const gchar *uid;
+       GError *error = NULL;
+
+       /* Add in online */
+       test_fill_cache (fixture, &component);
+       g_assert_nonnull (component);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+       g_assert_nonnull (uid);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+       /* Delete in offline */
+       g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL, 
&error));
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 3);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture,
+               uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+               NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+       /* Resync all offline changes */
+       e_cache_clear_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error);
+       g_assert_no_error (error);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 2);
+       g_assert_no_error (error);
+
+       test_basic_search (fixture, EXPECT_DEFAULT);
+       test_check_offline_changes (fixture, NULL);
+       test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+       g_clear_object (&component);
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUClosure closure = { TCU_LOAD_COMPONENT_SET_NONE };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       g_test_add ("/ECalCache/Offline/Basics", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_basics, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/Add", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/AddEdit", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_edit, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/AddDelete", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_delete, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/AddDeleteAdd", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_delete_add, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/AddResync", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_add_resync, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/Edit", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_edit, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/EditDelete", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_edit_delete, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/EditResync", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_edit_resync, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/Delete", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_delete, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/DeleteAdd", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_delete_add, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Offline/DeleteResync", TCUFixture, &closure,
+               tcu_fixture_setup, test_offline_delete_resync, tcu_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-search.c b/tests/libedata-cal/test-cal-cache-search.c
new file mode 100644
index 0000000..ac662b7
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-search.c
@@ -0,0 +1,473 @@
+/* -*- 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/>.
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+#define dd(x)
+
+static GHashTable *
+test_search_manual (ECalCache *cal_cache,
+                   const gchar *expr)
+{
+       GSList *components = NULL, *link;
+       GHashTable *res;
+       ECalBackendSExp *sexp;
+       ETimezoneCache *zone_cache;
+       gboolean success;
+       GError *error = NULL;
+
+       res = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc) 
e_cal_component_id_equal,
+               (GDestroyNotify) e_cal_component_free_id, g_object_unref);
+
+       zone_cache = E_TIMEZONE_CACHE (cal_cache);
+
+       /* Get all the components stored in the summary. */
+       success = e_cal_cache_search_components (cal_cache, NULL, &components, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (components);
+
+       sexp = e_cal_backend_sexp_new (expr);
+       g_assert (sexp != NULL);
+
+       for (link = components; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+
+               if (e_cal_backend_sexp_match_comp (sexp, comp, zone_cache)) {
+                       ECalComponentId *id = NULL;
+
+                       id = e_cal_component_get_id (comp);
+                       g_assert_nonnull (id);
+
+                       g_hash_table_insert (res, id, g_object_ref (comp));
+               }
+       }
+
+       g_slist_free_full (components, g_object_unref);
+       g_object_unref (sexp);
+
+       return res;
+}
+
+#if dd(1)+0
+static void
+test_search_dump_results (GSList *search_data,
+                         GHashTable *should_be)
+{
+       GSList *link;
+       GHashTableIter iter;
+       gpointer key;
+       gint ii;
+
+       printf ("   Found %d in ECalCache:\n", g_slist_length (search_data));
+       for (ii = 0, link = search_data; link; link = g_slist_next (link), ii++) {
+               ECalCacheSearchData *sd = link->data;
+
+               printf ("      [%d]: %s%s%s\n", ii, sd->uid, sd->rid ? ", " : "", sd->rid ? sd->rid : "");
+       }
+
+       printf ("\n");
+       printf ("   Found %d in traverse:\n", g_hash_table_size (should_be));
+
+       ii = 0;
+       g_hash_table_iter_init (&iter, should_be);
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               ECalComponentId *id = key;
+
+               printf ("      [%d]: %s%s%s\n", ii, id->uid, id->rid ? ", " : "", id->rid ? id->rid : "");
+               ii++;
+       }
+
+       printf ("\n");
+}
+#endif
+
+static void
+test_search_result_equal (GSList *items,
+                         GHashTable *should_be,
+                         gboolean (* check_cb) (GHashTable *should_be, gpointer item_data))
+{
+       GSList *link;
+
+       g_assert_cmpint (g_slist_length (items), ==, g_hash_table_size (should_be));
+
+       for (link = items; link; link = g_slist_next (link)) {
+               g_assert (check_cb (should_be, link->data));
+       }
+}
+
+static gboolean
+search_data_check_cb (GHashTable *should_be,
+                     gpointer item_data)
+{
+       ECalCacheSearchData *sd = item_data;
+       ECalComponentId id;
+
+       g_assert (sd != NULL);
+       g_assert (sd->uid != NULL);
+
+       id.uid = sd->uid;
+       id.rid = sd->rid;
+
+       return g_hash_table_contains (should_be, &id);
+}
+
+static gboolean
+component_check_cb (GHashTable *should_be,
+                   gpointer item_data)
+{
+       ECalComponent *comp = item_data;
+       ECalComponentId *id;
+       gboolean contains;
+
+       g_assert (comp != NULL);
+
+       id = e_cal_component_get_id (comp);
+
+       g_assert (id != NULL);
+       g_assert (id->uid != NULL);
+
+       contains = g_hash_table_contains (should_be, id);
+
+       e_cal_component_free_id (id);
+
+       return contains;
+}
+
+static gboolean
+id_check_cb (GHashTable *should_be,
+            gpointer item_data)
+{
+       ECalComponentId *id = item_data;
+
+       g_assert (id != NULL);
+       g_assert (id->uid != NULL);
+
+       return g_hash_table_contains (should_be, id);
+}
+
+static void
+test_search_expr (TCUFixture *fixture,
+                 const gchar *expr,
+                 const gchar *expects)
+{
+       GSList *items = NULL;
+       GHashTable *should_be;
+       gboolean success;
+       GError *error = NULL;
+
+       should_be = test_search_manual (fixture->cal_cache, expr);
+       g_assert_nonnull (should_be);
+
+       success = e_cal_cache_search (fixture->cal_cache, expr, &items, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       dd (test_search_dump_results (items, should_be));
+
+       test_search_result_equal (items, should_be, search_data_check_cb);
+
+       g_slist_free_full (items, e_cal_cache_search_data_free);
+       items = NULL;
+
+       success = e_cal_cache_search_components (fixture->cal_cache, expr, &items, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       test_search_result_equal (items, should_be, component_check_cb);
+
+       g_slist_free_full (items, g_object_unref);
+       items = NULL;
+
+       success = e_cal_cache_search_ids (fixture->cal_cache, expr, &items, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       if (expects) {
+               GSList *link;
+               gboolean negate = *expects == '!';
+
+               if (negate)
+                       expects++;
+
+               for (link = items; link; link = g_slist_next (link)) {
+                       ECalComponentId *id = link->data;
+
+                       if (g_strcmp0 (id->uid, expects) == 0)
+                               break;
+               }
+
+               if (link && negate)
+                       g_error ("Found '%s' in result of '%s', though it should not be there", expects, 
expr);
+               else if (!link && !negate)
+                       g_error ("Not found '%s' in result of '%s', though it should be there", expects, 
expr);
+       }
+
+       test_search_result_equal (items, should_be, id_check_cb);
+
+       g_slist_free_full (items, (GDestroyNotify) e_cal_component_free_id);
+
+       g_hash_table_destroy (should_be);
+}
+
+static void
+test_search (TCUFixture *fixture,
+            const gchar *expr,
+            const gchar *expects)
+{
+       gchar *not_expr;
+
+       test_search_expr (fixture, expr, expects);
+
+       not_expr = g_strdup_printf ("(not (%s))", expr);
+       test_search_expr (fixture, not_expr, NULL);
+       g_free (not_expr);
+}
+
+static void
+test_search_uid (TCUFixture *fixture,
+                gconstpointer user_data)
+{
+       test_search (fixture, "(uid? \"event-3\")", "event-3");
+       test_search (fixture, "(uid? \"event-6\")", "event-6");
+       test_search (fixture, "(or (uid? \"event-3\") (uid? \"event-6\"))", "event-3");
+       test_search (fixture, "(and (uid? \"event-3\") (uid? \"event-6\"))", "!event-3");
+}
+
+static void
+test_search_occur_in_time_range (TCUFixture *fixture,
+                                gconstpointer user_data)
+{
+       test_search (fixture, "(occur-in-time-range? (make-time \"20010101T000000Z\") (make-time 
\"20010101T010000Z\"))", "!event-1");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170209T000000Z\") (make-time 
\"20170210T000000Z\"))", "event-1");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170209T020000Z\") (make-time 
\"20170209T023000Z\"))", "event-1");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20111231T000000Z\") (make-time 
\"20111231T595959Z\"))", "event-5");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170225T210100Z\") (make-time 
\"20170225T210200Z\") \"America/New_York\")", "event-8");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170225T150100Z\") (make-time 
\"20170225T150200Z\") \"Europe/Berlin\")", "event-8");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170225T160100Z\") (make-time 
\"20170225T160200Z\") \"UTC\")", "event-8");
+
+       /* event-6 */
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170221T180000Z\") (make-time 
\"20170221T190000Z\"))", "event-6");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170221T180000Z\") (make-time 
\"20170221T190000Z\") \"America/New_York\")", "event-6");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170221T200000Z\") (make-time 
\"20170221T210000Z\") \"Europe/Berlin\")", "!event-6");
+       test_search (fixture, "(occur-in-time-range? (make-time \"20170221T180000Z\") (make-time 
\"20170221T190000Z\") \"Europe/Berlin\")", "event-6");
+}
+
+static void
+test_search_due_in_time_range (TCUFixture *fixture,
+                              gconstpointer user_data)
+{
+       test_search (fixture, "(due-in-time-range? (make-time \"20170101T000000Z\") (make-time 
\"20170101T010000Z\"))", "!task-4");
+       test_search (fixture, "(due-in-time-range? (make-time \"20170228T000000Z\") (make-time 
\"20170302T000000Z\"))", "task-4");
+}
+
+static void
+test_search_contains (TCUFixture *fixture,
+                     gconstpointer user_data)
+{
+       const TCUClosure *closure = user_data;
+       gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+       test_search (fixture, "(contains? \"any\" \"party\")", searches_events ? "event-5" : NULL);
+       test_search (fixture, "(contains? \"comment\" \"mentar\")", searches_events ? "event-8" : "task-6");
+       test_search (fixture, "(contains? \"description\" \"with\")", searches_events ? "event-1" : "task-3");
+       test_search (fixture, "(contains? \"summary\" \"meet\")", searches_events ? "event-8" : NULL);
+       test_search (fixture, "(contains? \"location\" \"kitchen\")", searches_events ? "event-3" : "task-5");
+       test_search (fixture, "(contains? \"attendee\" \"CharLie\")", searches_events ? "event-9" : NULL);
+       test_search (fixture, "(contains? \"organizer\" \"bOb\")", searches_events ? "event-8" : NULL);
+       test_search (fixture, "(contains? \"classification\" \"Public\")", searches_events ? "event-4" : 
"task-4");
+       test_search (fixture, "(contains? \"classification\" \"Private\")", searches_events ? "event-3" : 
"task-7");
+       test_search (fixture, "(contains? \"classification\" \"Confidential\")", searches_events ? "event-2" 
: "task-6");
+       test_search (fixture, "(contains? \"status\" \"NOT STARTED\")", searches_events ? NULL : "task-1");
+       test_search (fixture, "(contains? \"status\" \"COMPLETED\")", searches_events ? NULL : "task-4");
+       test_search (fixture, "(contains? \"status\" \"CANCELLED\")", searches_events ? NULL : "task-8");
+       test_search (fixture, "(contains? \"status\" \"IN PROGRESS\")", searches_events ? NULL : "task-7");
+       test_search (fixture, "(contains? \"priority\" \"HIGH\")", searches_events ? NULL : "task-7");
+       test_search (fixture, "(contains? \"priority\" \"NORMAL\")", searches_events ? NULL : "task-6");
+       test_search (fixture, "(contains? \"priority\" \"LOW\")", searches_events ? NULL : "task-5");
+       test_search (fixture, "(contains? \"priority\" \"UNDEFINED\")", searches_events ? NULL : "task-1");
+}
+
+static void
+test_search_has_start (TCUFixture *fixture,
+                      gconstpointer user_data)
+{
+       const TCUClosure *closure = user_data;
+       gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+       test_search (fixture, "(has-start?)", searches_events ? "event-1" : "task-9");
+       test_search (fixture, "(has-start?)", searches_events ? "event-1" : "!task-8");
+       test_search (fixture, "(not (has-start?))", searches_events ? "!event-1" : "!task-9");
+       test_search (fixture, "(not (has-start?))", searches_events ? "!event-1" : "task-8");
+}
+
+static void
+test_search_has_alarms (TCUFixture *fixture,
+                       gconstpointer user_data)
+{
+       const TCUClosure *closure = user_data;
+       gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+       test_search (fixture, "(has-alarms?)", searches_events ? "event-1" : "task-7");
+       test_search (fixture, "(has-alarms?)", searches_events ? "event-1" : "!task-6");
+       test_search (fixture, "(not (has-alarms?))", searches_events ? "!event-1" : "!task-7");
+       test_search (fixture, "(not (has-alarms?))", searches_events ? "!event-1" : "task-6");
+}
+
+static void
+test_search_has_alarms_in_range (TCUFixture *fixture,
+                                gconstpointer user_data)
+{
+       const TCUClosure *closure = user_data;
+       gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+       test_search (fixture, "(has-alarms-in-range? (make-time \"20091229T230000Z\") (make-time 
\"20091231T010000Z\"))",
+               searches_events ? "event-5" : "!task-7");
+}
+
+static void
+test_search_has_recurrences (TCUFixture *fixture,
+                            gconstpointer user_data)
+{
+       test_search (fixture, "(has-recurrences?)", "event-6");
+       test_search (fixture, "(not (has-recurrences?))", "!event-6");
+}
+
+static void
+test_search_has_categories (TCUFixture *fixture,
+                           gconstpointer user_data)
+{
+       test_search (fixture, "(has-categories? #f)", "!event-2");
+       test_search (fixture, "(has-categories? \"Holiday\")", "event-7");
+       test_search (fixture, "(has-categories? \"Hard\" \"Work\")", "event-2");
+       test_search (fixture, "(has-categories? \"Hard\" \"Work\")", "!event-4");
+}
+
+static void
+test_search_is_completed (TCUFixture *fixture,
+                         gconstpointer user_data)
+{
+       test_search (fixture, "(is-completed?)", "task-4");
+       test_search (fixture, "(is-completed?)", "!task-5");
+       test_search (fixture, "(not (is-completed?))", "!task-4");
+}
+
+static void
+test_search_completed_before (TCUFixture *fixture,
+                             gconstpointer user_data)
+{
+       test_search (fixture, "(completed-before? (make-time \"20170221T000000Z\"))", "!task-4");
+       test_search (fixture, "(completed-before? (make-time \"20170222T000000Z\"))", "task-4");
+}
+
+static void
+test_search_has_attachments (TCUFixture *fixture,
+                            gconstpointer user_data)
+{
+       test_search (fixture, "(has-attachments?)", "event-7");
+       test_search (fixture, "(not (has-attachments?))", "!event-7");
+}
+
+static void
+test_search_percent_complete (TCUFixture *fixture,
+                             gconstpointer user_data)
+{
+       test_search (fixture, "(< (percent-complete?) 30)", "task-5");
+       test_search (fixture, "(< (percent-complete?) 30)", "!task-7");
+}
+
+static void
+test_search_occurrences_count (TCUFixture *fixture,
+                              gconstpointer user_data)
+{
+       test_search (fixture, "(and (= (occurrences-count?) 1) (occur-in-time-range? (make-time 
\"20170209T000000Z\") (make-time \"20170210T000000Z\")))", "event-1");
+       test_search (fixture, "(= (occurrences-count? (make-time \"20170209T000000Z\") (make-time 
\"20170210T000000Z\")) 1)", "event-1");
+}
+
+static void
+test_search_complex (TCUFixture *fixture,
+                    gconstpointer user_data)
+{
+       test_search (fixture,
+               "(or "
+                       "(and (not (is-completed?)) (has-start?) (not (has-alarms?)))"
+                       "(contains? \"summary\" \"-on-\")"
+                       "(has-attachments?)"
+               ")", "event-3");
+}
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       TCUClosure closure_events = { TCU_LOAD_COMPONENT_SET_EVENTS };
+       TCUClosure closure_tasks = { TCU_LOAD_COMPONENT_SET_TASKS };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+       g_test_add ("/ECalCache/Search/Uid", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_uid, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/OccurInTimeRange", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_occur_in_time_range, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/DueInTimeRange", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_due_in_time_range, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/Contains/Events", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_contains, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/Contains/Tasks", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_contains, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasStart/Events", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_has_start, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasStart/Tasks", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_has_start, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasAlarms/Events", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_has_alarms, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasAlarms/Tasks", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_has_alarms, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasAlarmsInRange/Events", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_has_alarms_in_range, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasAlarmsInRange/Tasks", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_has_alarms_in_range, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasRecurrences", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_has_recurrences, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasCategories", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_has_categories, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/IsCompleted", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_is_completed, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/CompletedBefore", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_completed_before, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/HasAttachments", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_has_attachments, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/PercentComplete", TCUFixture, &closure_tasks,
+               tcu_fixture_setup, test_search_percent_complete, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/OccurrencesCount", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_occurrences_count, tcu_fixture_teardown);
+       g_test_add ("/ECalCache/Search/Complex", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_search_complex, tcu_fixture_teardown);
+
+       return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-utils.c b/tests/libedata-cal/test-cal-cache-utils.c
new file mode 100644
index 0000000..c109466
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-utils.c
@@ -0,0 +1,180 @@
+/* -*- 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/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include "test-cal-cache-utils.h"
+
+static void
+delete_work_directory (const gchar *filename)
+{
+       /* XXX Instead of complex error checking here, we should ideally use
+        * a recursive GDir / g_unlink() function.
+        *
+        * We cannot use GFile and the recursive delete function without
+        * corrupting our contained D-Bus environment with service files
+        * from the OS.
+        */
+       const gchar *argv[] = { "/bin/rm", "-rf", filename, NULL };
+       gboolean spawn_succeeded;
+       gint exit_status;
+
+       spawn_succeeded = g_spawn_sync (
+               NULL, (gchar **) argv, NULL, 0, NULL, NULL,
+                                       NULL, NULL, &exit_status, NULL);
+
+       g_assert (spawn_succeeded);
+       #ifndef G_OS_WIN32
+       g_assert (WIFEXITED (exit_status));
+       g_assert_cmpint (WEXITSTATUS (exit_status), ==, 0);
+       #else
+       g_assert_cmpint (exit_status, ==, 0);
+       #endif
+}
+
+void
+tcu_fixture_setup (TCUFixture *fixture,
+                  gconstpointer user_data)
+{
+       const TCUClosure *closure = user_data;
+       gchar *filename, *directory;
+       GError *error = NULL;
+
+       if (!g_file_test (CAMEL_PROVIDERDIR, G_FILE_TEST_IS_DIR | G_FILE_TEST_EXISTS)) {
+               if (g_mkdir_with_parents (CAMEL_PROVIDERDIR, 0700) == -1)
+                       g_warning ("%s: Failed to create folder '%s': %s\n", G_STRFUNC, CAMEL_PROVIDERDIR, 
g_strerror (errno));
+       }
+
+       /* Cleanup from last test */
+       directory = g_build_filename (g_get_tmp_dir (), "test-cal-cache", NULL);
+       delete_work_directory (directory);
+       g_free (directory);
+       filename = g_build_filename (g_get_tmp_dir (), "test-cal-cache", "cache.db", NULL);
+
+       fixture->cal_cache = e_cal_cache_new (filename, NULL, &error);
+
+       if (!fixture->cal_cache)
+               g_error ("Failed to create the ECalCache: %s", error->message);
+
+       g_free (filename);
+
+       if (closure) {
+               if (closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS) {
+                       tcu_add_component_from_test_case (fixture, "event-1", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-2", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-3", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-4", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-5", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-6", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-6-a", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-7", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-8", NULL);
+                       tcu_add_component_from_test_case (fixture, "event-9", NULL);
+               } else if (closure->load_set == TCU_LOAD_COMPONENT_SET_TASKS) {
+                       tcu_add_component_from_test_case (fixture, "task-1", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-2", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-3", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-4", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-5", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-6", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-7", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-8", NULL);
+                       tcu_add_component_from_test_case (fixture, "task-9", NULL);
+               }
+       }
+}
+
+void
+tcu_fixture_teardown (TCUFixture *fixture,
+                     gconstpointer user_data)
+{
+       g_object_unref (fixture->cal_cache);
+}
+
+gchar *
+tcu_new_icalstring_from_test_case (const gchar *case_name)
+{
+       gchar *filename;
+       gchar *case_filename;
+       GFile * file;
+       GError *error = NULL;
+       gchar *icalstring = NULL;
+
+       case_filename = g_strdup_printf ("%s.ics", case_name);
+
+       /* In the case of installed tests, they run in ${pkglibexecdir}/installed-tests
+        * and the components are installed in ${pkglibexecdir}/installed-tests/components
+        */
+       if (g_getenv ("TEST_INSTALLED_SERVICES") != NULL)
+               filename = g_build_filename (INSTALLED_TEST_DIR, "components", case_filename, NULL);
+       else
+               filename = g_build_filename (SRCDIR, "..", "libedata-cal", "components", case_filename, NULL);
+
+       file = g_file_new_for_path (filename);
+       if (!g_file_load_contents (file, NULL, &icalstring, NULL, NULL, &error))
+               g_error (
+                       "Failed to read test iCal string file '%s': %s",
+                       filename, error->message);
+
+       g_free (case_filename);
+       g_free (filename);
+       g_object_unref (file);
+
+       return icalstring;
+}
+
+ECalComponent *
+tcu_new_component_from_test_case (const gchar *case_name)
+{
+       gchar *icalstring;
+       ECalComponent *component = NULL;
+
+       icalstring = tcu_new_icalstring_from_test_case (case_name);
+       if (icalstring)
+               component = e_cal_component_new_from_string (icalstring);
+       g_free (icalstring);
+
+       if (!component)
+               g_error (
+                       "Failed to construct component from test case '%s'",
+                       case_name);
+
+       return component;
+}
+
+void
+tcu_add_component_from_test_case (TCUFixture *fixture,
+                                 const gchar *case_name,
+                                 ECalComponent **out_component)
+{
+       ECalComponent *component;
+       GError *error = NULL;
+
+       component = tcu_new_component_from_test_case (case_name);
+
+       if (!e_cal_cache_put_component (fixture->cal_cache, component, case_name, E_CACHE_IS_ONLINE, NULL, 
&error))
+               g_error ("Failed to add component: %s", error->message);
+
+       if (out_component)
+               *out_component = g_object_ref (component);
+
+       g_clear_object (&component);
+}
diff --git a/tests/libedata-cal/test-cal-cache-utils.h b/tests/libedata-cal/test-cal-cache-utils.h
new file mode 100644
index 0000000..d3616fc
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-utils.h
@@ -0,0 +1,52 @@
+/* -*- 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/>.
+ */
+
+#ifndef TEST_CACHE_UTILS_H
+#define TEST_CACHE_UTILS_H
+
+#include <libedata-cal/libedata-cal.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+       TCU_LOAD_COMPONENT_SET_NONE,
+       TCU_LOAD_COMPONENT_SET_EVENTS,
+       TCU_LOAD_COMPONENT_SET_TASKS
+} TCULoadComponentSet;
+
+typedef struct {
+       ECalCache *cal_cache;
+} TCUFixture;
+
+typedef struct {
+       TCULoadComponentSet load_set;
+} TCUClosure;
+
+void           tcu_fixture_setup                       (TCUFixture *fixture,
+                                                        gconstpointer user_data);
+void           tcu_fixture_teardown                    (TCUFixture *fixture,
+                                                        gconstpointer user_data);
+
+gchar *                tcu_new_icalstring_from_test_case       (const gchar *case_name);
+ECalComponent *        tcu_new_component_from_test_case        (const gchar *case_name);
+void           tcu_add_component_from_test_case        (TCUFixture *fixture,
+                                                        const gchar *case_name,
+                                                        ECalComponent **out_component);
+
+G_END_DECLS
+
+#endif /* TEST_CACHE_UTILS_H */
diff --git a/tests/libedata-cal/test-cal-meta-backend.c b/tests/libedata-cal/test-cal-meta-backend.c
new file mode 100644
index 0000000..c931f01
--- /dev/null
+++ b/tests/libedata-cal/test-cal-meta-backend.c
@@ -0,0 +1,2723 @@
+/* -*- 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/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include "libecal/libecal.h"
+
+#include "e-test-server-utils.h"
+#include "test-cal-cache-utils.h"
+
+void _e_cal_cache_remove_loaded_timezones (ECalCache *cal_cache); /* e-cal-cache.c, private function */
+
+#define EXPECTED_TZID          "/freeassociation.sourceforge.net/America/New_York"
+#define EXPECTED_LOCATION      "America/New_York"
+#define REMOTE_URL             "https://www.gnome.org/wp-content/themes/gnome-grass/images/gnome-logo.svg";
+#define MODIFIED_SUMMARY_STR   "Modified summary"
+
+typedef struct _ECalMetaBackendTest {
+       ECalMetaBackend parent;
+
+       icalcomponent *vcalendar;
+
+       gint sync_tag_index;
+       gboolean can_connect;
+       gboolean is_connected;
+       gint connect_count;
+       gint list_count;
+       gint save_count;
+       gint load_count;
+       gint remove_count;
+} ECalMetaBackendTest;
+
+typedef struct _ECalMetaBackendTestClass {
+       ECalMetaBackendClass parent_class;
+} ECalMetaBackendTestClass;
+
+#define E_TYPE_CAL_META_BACKEND_TEST (e_cal_meta_backend_test_get_type ())
+#define E_CAL_META_BACKEND_TEST(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CAL_META_BACKEND_TEST, ECalMetaBackendTest))
+#define E_IS_CAL_META_BACKEND_TEST(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CAL_META_BACKEND_TEST))
+
+GType e_cal_meta_backend_test_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (ECalMetaBackendTest, e_cal_meta_backend_test, E_TYPE_CAL_META_BACKEND)
+
+static void
+ecmb_test_add_test_case (ECalMetaBackendTest *test_backend,
+                        const gchar *case_name)
+{
+       gchar *icalstr;
+       icalcomponent *icalcomp;
+
+       g_assert_nonnull (test_backend);
+       g_assert_nonnull (case_name);
+
+       icalstr = tcu_new_icalstring_from_test_case (case_name);
+       g_assert_nonnull (icalstr);
+
+       icalcomp = icalcomponent_new_from_string (icalstr);
+       g_assert_nonnull (icalcomp);
+       g_free (icalstr);
+
+       icalcomponent_add_component (test_backend->vcalendar, icalcomp);
+}
+
+static void
+ecmb_test_remove_component (ECalMetaBackendTest *test_backend,
+                           const gchar *uid,
+                           const gchar *rid)
+{
+       icalcomponent *icalcomp;
+
+       g_assert_nonnull (test_backend);
+       g_assert_nonnull (uid);
+
+       if (rid && !*rid)
+               rid = NULL;
+
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;) {
+               const gchar *server_uid;
+
+               server_uid = icalcomponent_get_uid (icalcomp);
+               g_assert_nonnull (server_uid);
+
+               if (g_str_equal (server_uid, uid) && (!rid || !*rid ||
+                   (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY) &&
+                   g_str_equal (rid, icaltime_as_ical_string (icalcomponent_get_recurrenceid (icalcomp)))))) 
{
+                       icalcomponent_remove_component (test_backend->vcalendar, icalcomp);
+                       icalcomponent_free (icalcomp);
+
+                       icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, 
ICAL_VEVENT_COMPONENT);
+               } else {
+                       icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, 
ICAL_VEVENT_COMPONENT);
+               }
+       }
+}
+
+static GHashTable * /* ECalComponentId * ~> NULL */
+ecmb_test_gather_ids (va_list args)
+{
+       GHashTable *expects;
+       const gchar *uid, *rid;
+
+       expects = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc) 
e_cal_component_id_equal,
+               (GDestroyNotify) e_cal_component_free_id, NULL);
+
+       uid = va_arg (args, const gchar *);
+       while (uid) {
+               rid = va_arg (args, const gchar *);
+
+               g_hash_table_insert (expects, e_cal_component_id_new (uid, rid), NULL);
+               uid = va_arg (args, const gchar *);
+       }
+
+       return expects;
+}
+
+static void
+ecmb_test_vcalendar_contains (icalcomponent *vcalendar,
+                             gboolean negate,
+                             gboolean exact,
+                             ...) /* <uid, rid> pairs, ended with NULL */
+{
+       va_list args;
+       GHashTable *expects;
+       icalcomponent *icalcomp;
+       guint ntotal;
+
+       g_return_if_fail (vcalendar != NULL);
+       g_return_if_fail (icalcomponent_isa (vcalendar) == ICAL_VCALENDAR_COMPONENT);
+
+       va_start (args, exact);
+       expects = ecmb_test_gather_ids (args);
+       va_end (args);
+
+       ntotal = g_hash_table_size (expects);
+
+       for (icalcomp = icalcomponent_get_first_component (vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;
+            icalcomp = icalcomponent_get_next_component (vcalendar, ICAL_VEVENT_COMPONENT)) {
+               ECalComponentId id;
+
+               id.uid = (gpointer) icalcomponent_get_uid (icalcomp);
+               if (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY))
+                       id.rid = (gpointer) icaltime_as_ical_string (icalcomponent_get_recurrenceid 
(icalcomp));
+               else
+                       id.rid = NULL;
+
+               if (exact) {
+                       if (negate)
+                               g_assert (!g_hash_table_remove (expects, &id));
+                       else
+                               g_assert (g_hash_table_remove (expects, &id));
+               } else {
+                       g_hash_table_remove (expects, &id);
+               }
+       }
+
+       if (negate)
+               g_assert_cmpint (g_hash_table_size (expects), ==, ntotal);
+       else
+               g_assert_cmpint (g_hash_table_size (expects), ==, 0);
+
+       g_hash_table_destroy (expects);
+}
+
+static void
+ecmb_test_cache_contains (ECalCache *cal_cache,
+                         gboolean negate,
+                         gboolean exact,
+                         ...) /* <uid, rid> pairs, ended with NULL */
+{
+       va_list args;
+       GHashTable *expects;
+       GHashTableIter iter;
+       gpointer key;
+       gint found = 0;
+
+       g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+
+       va_start (args, exact);
+       expects = ecmb_test_gather_ids (args);
+       va_end (args);
+
+       g_hash_table_iter_init (&iter, expects);
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               ECalComponentId *id = key;
+
+               g_assert_nonnull (id);
+
+               if (e_cal_cache_contains (cal_cache, id->uid, id->rid, E_CACHE_EXCLUDE_DELETED))
+                       found++;
+       }
+
+       if (negate)
+               g_assert_cmpint (0, ==, found);
+       else
+               g_assert_cmpint (g_hash_table_size (expects), ==, found);
+
+       g_hash_table_destroy (expects);
+
+       if (exact && !negate)
+               g_assert_cmpint (e_cache_get_count (E_CACHE (cal_cache), E_CACHE_EXCLUDE_DELETED, NULL, 
NULL), ==, found);
+}
+
+static void
+ecmb_test_cache_and_server_equal (ECalCache *cal_cache,
+                                 icalcomponent *vcalendar,
+                                 ECacheDeletedFlag deleted_flag)
+{
+       icalcomponent *icalcomp;
+
+       g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+       g_return_if_fail (vcalendar != NULL);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (cal_cache), deleted_flag, NULL, NULL), ==,
+               icalcomponent_count_components (vcalendar, ICAL_VEVENT_COMPONENT));
+
+       for (icalcomp = icalcomponent_get_first_component (vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;
+            icalcomp = icalcomponent_get_next_component (vcalendar, ICAL_VEVENT_COMPONENT)) {
+               const gchar *uid, *rid = NULL;
+
+               uid = icalcomponent_get_uid (icalcomp);
+               if (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY))
+                       rid = icaltime_as_ical_string (icalcomponent_get_recurrenceid (icalcomp));
+
+               g_assert (e_cal_cache_contains (cal_cache, uid, rid, deleted_flag));
+       }
+}
+
+static gchar *
+e_cal_meta_backend_test_get_backend_property (ECalBackend *cal_backend,
+                                             const gchar *prop_name)
+{
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (cal_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strjoin (",",
+                       e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cal_backend)),
+                       CAL_STATIC_CAPABILITY_ALARM_DESCRIPTION,
+                       NULL);
+       } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS)) {
+               return g_strdup ("user@no.where");
+       }
+
+       /* Chain up to parent's method. */
+       return E_CAL_BACKEND_CLASS (e_cal_meta_backend_test_parent_class)->get_backend_property (cal_backend, 
prop_name);
+}
+
+static gboolean
+e_cal_meta_backend_test_connect_sync (ECalMetaBackend *meta_backend,
+                                     const ENamedParameters *credentials,
+                                     ESourceAuthenticationResult *out_auth_result,
+                                     gchar **out_certificate_pem,
+                                     GTlsCertificateFlags *out_certificate_errors,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+
+       if (test_backend->is_connected)
+               return TRUE;
+
+       test_backend->connect_count++;
+
+       if (test_backend->can_connect) {
+               test_backend->is_connected = TRUE;
+               return TRUE;
+       }
+
+       g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+               e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+       return FALSE;
+}
+
+static gboolean
+e_cal_meta_backend_test_disconnect_sync (ECalMetaBackend *meta_backend,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       test_backend->is_connected = FALSE;
+
+       return TRUE;
+}
+
+static gboolean
+e_cal_meta_backend_test_get_changes_sync (ECalMetaBackend *meta_backend,
+                                         const gchar *last_sync_tag,
+                                         gboolean is_repeat,
+                                         gchar **out_new_sync_tag,
+                                         gboolean *out_repeat,
+                                         GSList **out_created_objects,
+                                         GSList **out_modified_objects,
+                                         GSList **out_removed_objects,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_repeat != NULL, FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+
+       if (!test_backend->sync_tag_index) {
+               g_assert_null (last_sync_tag);
+       } else {
+               g_assert_nonnull (last_sync_tag);
+               g_assert_cmpint (atoi (last_sync_tag), ==, test_backend->sync_tag_index);
+
+               test_backend->sync_tag_index++;
+               *out_new_sync_tag = g_strdup_printf ("%d", test_backend->sync_tag_index);
+
+               if (test_backend->sync_tag_index == 2)
+                       *out_repeat = TRUE;
+               else if (test_backend->sync_tag_index == 3)
+                       return TRUE;
+       }
+
+       /* Nothing to do here at the moment, left the work to the parent class,
+          which calls list_existing_sync() internally. */
+       return E_CAL_META_BACKEND_CLASS (e_cal_meta_backend_test_parent_class)->get_changes_sync 
(meta_backend,
+               last_sync_tag, is_repeat, out_new_sync_tag, out_repeat, out_created_objects,
+               out_modified_objects, out_removed_objects, cancellable, error);
+}
+
+static gboolean
+e_cal_meta_backend_test_list_existing_sync (ECalMetaBackend *meta_backend,
+                                           gchar **out_new_sync_tag,
+                                           GSList **out_existing_objects,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       icalcomponent *icalcomp;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag, FALSE);
+       g_return_val_if_fail (out_existing_objects, FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       test_backend->list_count++;
+
+       g_assert (test_backend->is_connected);
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       *out_existing_objects = NULL;
+
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;
+            icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+               const gchar *uid;
+               gchar *revision;
+               ECalMetaBackendInfo *nfo;
+
+               /* Detached instances are stored together with the master object */
+               if (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY))
+                       continue;
+
+               uid = icalcomponent_get_uid (icalcomp);
+               revision = e_cal_cache_dup_component_revision (cal_cache, icalcomp);
+
+               nfo = e_cal_meta_backend_info_new (uid, revision, NULL, NULL);
+               *out_existing_objects = g_slist_prepend (*out_existing_objects, nfo);
+
+               g_free (revision);
+       }
+
+       g_object_unref (cal_cache);
+
+       return TRUE;
+}
+
+static gboolean
+e_cal_meta_backend_test_save_component_sync (ECalMetaBackend *meta_backend,
+                                            gboolean overwrite_existing,
+                                            EConflictResolution conflict_resolution,
+                                            const GSList *instances,
+                                            const gchar *extra,
+                                            gchar **out_new_uid,
+                                            gchar **out_new_extra,
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+       icalcomponent *icalcomp;
+       const gchar *uid;
+       GSList *link;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (instances != NULL, FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       test_backend->save_count++;
+
+       g_assert (test_backend->is_connected);
+
+       uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (instances->data));
+       g_assert_nonnull (uid);
+
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;) {
+               const gchar *server_uid;
+
+               server_uid = icalcomponent_get_uid (icalcomp);
+               g_assert_nonnull (server_uid);
+
+               if (g_str_equal (server_uid, uid)) {
+                       if (!overwrite_existing) {
+                               g_propagate_error (error, e_data_cal_create_error (ObjectIdAlreadyExists, 
NULL));
+                               return FALSE;
+                       }
+
+                       icalcomponent_remove_component (test_backend->vcalendar, icalcomp);
+                       icalcomponent_free (icalcomp);
+
+                       icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, 
ICAL_VEVENT_COMPONENT);
+               } else {
+                       icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, 
ICAL_VEVENT_COMPONENT);
+               }
+       }
+
+       for (link = (GSList *) instances; link; link = g_slist_next (link)) {
+               ECalComponent *comp = link->data;
+               const gchar *comp_uid;
+
+               icalcomp = e_cal_component_get_icalcomponent (comp);
+               g_assert_nonnull (icalcomp);
+
+               comp_uid = icalcomponent_get_uid (icalcomp);
+               g_assert_cmpstr (uid, ==, comp_uid);
+
+               icalcomponent_add_component (test_backend->vcalendar, icalcomponent_new_clone (icalcomp));
+       }
+
+       *out_new_uid = g_strdup (uid);
+
+       return TRUE;
+}
+
+static gboolean
+e_cal_meta_backend_test_load_component_sync (ECalMetaBackend *meta_backend,
+                                            const gchar *uid,
+                                            const gchar *extra,
+                                            icalcomponent **out_instances,
+                                            gchar **out_extra,
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+       icalcomponent *icalcomp;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_instances != NULL, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       test_backend->load_count++;
+
+       g_assert (test_backend->is_connected);
+
+       *out_instances = NULL;
+
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;
+            icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+               const gchar *server_uid;
+
+               server_uid = icalcomponent_get_uid (icalcomp);
+               g_assert_nonnull (server_uid);
+
+               if (g_str_equal (server_uid, uid)) {
+                       if (!*out_instances)
+                               *out_instances = e_cal_util_new_top_level ();
+
+                       icalcomponent_add_component (*out_instances, icalcomponent_new_clone (icalcomp));
+               }
+       }
+
+       if (*out_instances) {
+               *out_extra = g_strconcat ("extra for ", uid, NULL);
+               return TRUE;
+       } else {
+               g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+       }
+
+       return FALSE;
+}
+
+static gboolean
+e_cal_meta_backend_test_remove_component_sync (ECalMetaBackend *meta_backend,
+                                              EConflictResolution conflict_resolution,
+                                              const gchar *uid,
+                                              const gchar *extra,
+                                              const gchar *object,
+                                              GCancellable *cancellable,
+                                              GError **error)
+{
+       ECalMetaBackendTest *test_backend;
+       icalcomponent *icalcomp;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (extra != NULL, FALSE);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       test_backend->remove_count++;
+
+       g_assert (test_backend->is_connected);
+
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;) {
+               const gchar *server_uid;
+
+               server_uid = icalcomponent_get_uid (icalcomp);
+               g_assert_nonnull (server_uid);
+
+               if (g_str_equal (server_uid, uid)) {
+                       if (!success) {
+                               gchar *expected_extra;
+
+                               expected_extra = g_strconcat ("extra for ", uid, NULL);
+                               g_assert_cmpstr (expected_extra, ==, extra);
+                               g_free (expected_extra);
+                       }
+
+                       success = TRUE;
+
+                       icalcomponent_remove_component (test_backend->vcalendar, icalcomp);
+                       icalcomponent_free (icalcomp);
+
+                       icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, 
ICAL_VEVENT_COMPONENT);
+               } else {
+                       icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, 
ICAL_VEVENT_COMPONENT);
+               }
+       }
+
+       if (!success)
+               g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+
+       return success;
+}
+
+static void
+e_cal_meta_backend_test_reset_counters (ECalMetaBackendTest *test_backend)
+{
+       g_return_if_fail (E_IS_CAL_META_BACKEND_TEST (test_backend));
+
+       test_backend->connect_count = 0;
+       test_backend->list_count = 0;
+       test_backend->save_count = 0;
+       test_backend->load_count = 0;
+       test_backend->remove_count = 0;
+}
+
+static ECalCache *glob_use_cache = NULL;
+
+static void
+e_cal_meta_backend_test_constructed (GObject *object)
+{
+       ECalMetaBackendTest *test_backend = E_CAL_META_BACKEND_TEST (object);
+
+       g_assert_nonnull (glob_use_cache);
+
+       /* Set it before ECalMetaBackend::constucted() creates its own cache */
+       e_cal_meta_backend_set_cache (E_CAL_META_BACKEND (test_backend), glob_use_cache);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_meta_backend_test_parent_class)->constructed (object);
+}
+
+static void
+e_cal_meta_backend_test_finalize (GObject *object)
+{
+       ECalMetaBackendTest *test_backend = E_CAL_META_BACKEND_TEST (object);
+
+       g_assert_nonnull (test_backend->vcalendar);
+
+       icalcomponent_free (test_backend->vcalendar);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_meta_backend_test_parent_class)->finalize (object);
+}
+
+static void
+e_cal_meta_backend_test_class_init (ECalMetaBackendTestClass *klass)
+{
+       ECalMetaBackendClass *cal_meta_backend_class;
+       ECalBackendClass *cal_backend_class;
+       GObjectClass *object_class;
+
+       cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+       cal_meta_backend_class->connect_sync = e_cal_meta_backend_test_connect_sync;
+       cal_meta_backend_class->disconnect_sync = e_cal_meta_backend_test_disconnect_sync;
+       cal_meta_backend_class->get_changes_sync = e_cal_meta_backend_test_get_changes_sync;
+       cal_meta_backend_class->list_existing_sync = e_cal_meta_backend_test_list_existing_sync;
+       cal_meta_backend_class->save_component_sync = e_cal_meta_backend_test_save_component_sync;
+       cal_meta_backend_class->load_component_sync = e_cal_meta_backend_test_load_component_sync;
+       cal_meta_backend_class->remove_component_sync = e_cal_meta_backend_test_remove_component_sync;
+
+       cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+       cal_backend_class->get_backend_property = e_cal_meta_backend_test_get_backend_property;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_cal_meta_backend_test_constructed;
+       object_class->finalize = e_cal_meta_backend_test_finalize;
+}
+
+static void
+e_cal_meta_backend_test_init (ECalMetaBackendTest *test_backend)
+{
+       test_backend->sync_tag_index = 0;
+       test_backend->is_connected = FALSE;
+       test_backend->can_connect = TRUE;
+       test_backend->vcalendar = e_cal_util_new_top_level ();
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       e_backend_set_online (E_BACKEND (test_backend), TRUE);
+       e_cal_backend_set_writable (E_CAL_BACKEND (test_backend), TRUE);
+
+       ecmb_test_add_test_case (test_backend, "event-1");
+       ecmb_test_add_test_case (test_backend, "event-2");
+       ecmb_test_add_test_case (test_backend, "event-3");
+       ecmb_test_add_test_case (test_backend, "event-4");
+       ecmb_test_add_test_case (test_backend, "event-5");
+       ecmb_test_add_test_case (test_backend, "event-6");
+       ecmb_test_add_test_case (test_backend, "event-6-a");
+       ecmb_test_add_test_case (test_backend, "event-7");
+       ecmb_test_add_test_case (test_backend, "event-8");
+       ecmb_test_add_test_case (test_backend, "event-9");
+}
+
+static ESourceRegistry *glob_registry = NULL;
+
+static ECalMetaBackend *
+e_cal_meta_backend_test_new (ECalCache *cache)
+{
+       ECalMetaBackend *meta_backend;
+       ESource *scratch;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert (E_IS_CAL_CACHE (cache));
+
+       g_assert_nonnull (glob_registry);
+       g_assert_null (glob_use_cache);
+
+       glob_use_cache = cache;
+
+       scratch = e_source_new_with_uid ("test-source", NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (scratch);
+
+       meta_backend = g_object_new (E_TYPE_CAL_META_BACKEND_TEST,
+               "source", scratch,
+               "registry", glob_registry,
+               "kind", ICAL_VEVENT_COMPONENT,
+               NULL);
+       g_assert_nonnull (meta_backend);
+
+       g_assert (glob_use_cache == cache);
+       glob_use_cache = NULL;
+
+       g_object_unref (scratch);
+
+       e_cal_meta_backend_set_cache (meta_backend, cache);
+
+       #define set_extra_data(_uid, _rid) \
+               success = e_cal_cache_set_component_extra (cache, _uid, _rid, "extra for " _uid, NULL, 
&error); \
+               g_assert_no_error (error); \
+               g_assert (success);
+
+       set_extra_data ("event-1", NULL);
+       set_extra_data ("event-2", NULL);
+       set_extra_data ("event-3", NULL);
+       set_extra_data ("event-4", NULL);
+       set_extra_data ("event-5", NULL);
+       set_extra_data ("event-6", NULL);
+       set_extra_data ("event-6", "20170225T134900");
+       set_extra_data ("event-7", NULL);
+       set_extra_data ("event-8", NULL);
+       set_extra_data ("event-9", NULL);
+
+       #undef set_extra_data
+
+       return meta_backend;
+}
+
+static void
+e_cal_meta_backend_test_change_online (ECalMetaBackend *meta_backend,
+                                      gboolean is_online)
+{
+       EFlag *flag;
+       gulong handler_id;
+
+       if (!is_online) {
+               e_backend_set_online (E_BACKEND (meta_backend), FALSE);
+               return;
+       }
+
+       if (e_backend_get_online (E_BACKEND (meta_backend)))
+               return;
+
+       flag = e_flag_new ();
+
+       handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+               G_CALLBACK (e_flag_set), flag);
+
+       /* Going online triggers refresh, thus wait for it */
+       e_backend_set_online (E_BACKEND (meta_backend), TRUE);
+
+       e_flag_wait (flag);
+       e_flag_free (flag);
+
+       g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+e_cal_meta_backend_test_call_refresh (ECalMetaBackend *meta_backend)
+{
+       EFlag *flag;
+       gulong handler_id;
+       GError *error = NULL;
+
+       if (!e_backend_get_online (E_BACKEND (meta_backend)))
+               return;
+
+       flag = e_flag_new ();
+
+       handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+               G_CALLBACK (e_flag_set), flag);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->refresh_sync (E_CAL_BACKEND_SYNC (meta_backend), NULL, 
NULL, &error);
+       g_assert_no_error (error);
+
+       e_flag_wait (flag);
+       e_flag_free (flag);
+
+       g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+assert_tzid_matches_cb (icalparameter *param,
+                       gpointer user_data)
+{
+       const gchar *expected_tzid = user_data;
+
+       g_assert_cmpstr (icalparameter_get_tzid (param), ==, expected_tzid);
+}
+
+static void
+test_merge_instances (TCUFixture *fixture,
+                     gconstpointer user_data)
+{
+       ECalMetaBackend *meta_backend;
+       GSList *instances = NULL;
+       icalcomponent *icalcomp, *subcomp;
+       icalproperty *prop;
+       gboolean success;
+       GError *error = NULL;
+
+       meta_backend = e_cal_meta_backend_test_new (fixture->cal_cache);
+       g_assert_nonnull (meta_backend);
+
+       /* event-1 has only UTC times, with no TZID */
+       success = e_cal_cache_get_components_by_uid (fixture->cal_cache, "event-1", &instances, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (instances);
+
+       /* TZID as is */
+       icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, FALSE);
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 1);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_ANY_COMPONENT);
+       g_assert_nonnull (subcomp);
+       g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VEVENT_COMPONENT);
+
+       icalcomponent_free (icalcomp);
+
+       /* TZID as location */
+       icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, TRUE);
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 1);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_ANY_COMPONENT);
+       g_assert_nonnull (subcomp);
+       g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VEVENT_COMPONENT);
+
+       icalcomponent_free (icalcomp);
+
+       g_slist_free_full (instances, g_object_unref);
+       instances = NULL;
+
+       /* event-7 has built-in TZID */
+       success = e_cal_cache_get_components_by_uid (fixture->cal_cache, "event-7", &instances, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (instances);
+
+       /* TZID as is */
+       icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, FALSE);
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 2);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 1);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+       g_assert_nonnull (subcomp);
+       g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+       prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_TZID);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+       g_assert_nonnull (subcomp);
+       icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+       icalcomponent_free (icalcomp);
+
+       /* TZID to location */
+       icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, TRUE);
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 2);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 1);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+       g_assert_nonnull (subcomp);
+       g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+       prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_LOCATION);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+       g_assert_nonnull (subcomp);
+       icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+       icalcomponent_free (icalcomp);
+       g_slist_free_full (instances, g_object_unref);
+       instances = NULL;
+
+       /* event-6 has TZID-s as locations already and a detached instance */
+       success = e_cal_cache_get_components_by_uid (fixture->cal_cache, "event-6", &instances, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (instances);
+
+       /* TZID as is */
+       icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, FALSE);
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 3);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 2);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+       g_assert_nonnull (subcomp);
+       g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+       prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_LOCATION);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+       g_assert_nonnull (subcomp);
+       icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+       subcomp = icalcomponent_get_next_component (icalcomp, ICAL_VEVENT_COMPONENT);
+       g_assert_nonnull (subcomp);
+       icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+       icalcomponent_free (icalcomp);
+
+       /* TZID to location */
+       icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, TRUE);
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 3);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+       g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 2);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+       g_assert_nonnull (subcomp);
+       g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+       prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_LOCATION);
+
+       subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+       g_assert_nonnull (subcomp);
+       icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+       subcomp = icalcomponent_get_next_component (icalcomp, ICAL_VEVENT_COMPONENT);
+       g_assert_nonnull (subcomp);
+       icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+       icalcomponent_free (icalcomp);
+       g_slist_free_full (instances, g_object_unref);
+
+       g_object_unref (meta_backend);
+}
+
+static void
+check_attachment_content (icalattach *attach,
+                         const gchar *expected_content,
+                         gsize expected_content_len)
+{
+       g_assert_nonnull (attach);
+       g_assert_nonnull (expected_content);
+       g_assert_cmpint (expected_content_len, >, 0);
+
+       if (icalattach_get_is_url (attach)) {
+               const gchar *url;
+               gboolean success;
+               gchar *filename;
+               gchar *content = NULL;
+               gsize content_len = -1;
+               GError *error = NULL;
+
+               url = icalattach_get_url (attach);
+               g_assert_nonnull (url);
+               g_assert (g_str_has_prefix (url, "file://"));
+
+               filename = g_filename_from_uri (icalattach_get_url (attach), NULL, &error);
+               g_assert_no_error (error);
+               g_assert_nonnull (filename);
+
+               success = g_file_get_contents (filename, &content, &content_len, &error);
+               g_assert_no_error (error);
+               g_assert (success);
+               g_assert_nonnull (content);
+               g_assert_cmpint (content_len, >, 0);
+
+               g_assert_cmpmem (content, content_len, expected_content, expected_content_len);
+
+               g_free (filename);
+               g_free (content);
+       } else {
+               guchar *base64;
+               gsize base64_len;
+
+               base64 = g_base64_decode ((const gchar *) icalattach_get_data (attach), &base64_len);
+               g_assert_nonnull (base64);
+               g_assert_cmpmem (base64, base64_len, expected_content, expected_content_len);
+
+               g_free (base64);
+       }
+}
+
+static void
+test_attachments (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       ECalMetaBackend *meta_backend;
+       gchar *content = NULL;
+       gsize content_len = 0;
+       ECalComponent *comp = NULL;
+       icalcomponent *icalcomp;
+       icalproperty *prop;
+       icalparameter *param;
+       icalattach *attach;
+       gchar *filename;
+       const gchar *basename;
+       gboolean success;
+       GError *error = NULL;
+
+       meta_backend = e_cal_meta_backend_test_new (fixture->cal_cache);
+       g_assert_nonnull (meta_backend);
+
+       /* It has a URL attachment */
+       success = e_cal_cache_get_component (fixture->cal_cache, "event-7", NULL, &comp, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (comp);
+
+       icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
+       g_assert_nonnull (icalcomp);
+       g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 1);
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (icalattach_get_is_url (attach));
+
+       filename = g_filename_from_uri (icalattach_get_url (attach), NULL, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (filename);
+
+       basename = strrchr (filename, '/');
+       g_assert_nonnull (basename);
+       basename++;
+
+       success = g_file_get_contents (filename, &content, &content_len, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_nonnull (content);
+       g_assert_cmpint (content_len, >, 0);
+
+       success = e_cal_meta_backend_inline_local_attachments_sync (meta_backend, icalcomp, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 1);
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+       param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+       g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (!icalattach_get_is_url (attach));
+
+       check_attachment_content (attach, content, content_len);
+
+       success = e_cal_meta_backend_store_inline_attachments_sync (meta_backend, icalcomp, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 1);
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+       param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+       g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (icalattach_get_is_url (attach));
+
+       check_attachment_content (attach, content, content_len);
+
+       /* Add a URL attachment which is not pointing to a local file */
+       attach = icalattach_new_from_url (REMOTE_URL);
+       prop = icalproperty_new_attach (attach);
+       icalattach_unref (attach);
+       icalcomponent_add_property (icalcomp, prop);
+
+       g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 2);
+
+       success = e_cal_meta_backend_inline_local_attachments_sync (meta_backend, icalcomp, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 2);
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+       param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+       g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (!icalattach_get_is_url (attach));
+
+       check_attachment_content (attach, content, content_len);
+
+       /* Verify the remote URL did not change */
+       prop = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (icalattach_get_is_url (attach));
+       g_assert_cmpstr (icalattach_get_url (attach), ==, REMOTE_URL);
+
+       success = e_cal_meta_backend_store_inline_attachments_sync (meta_backend, icalcomp, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 2);
+
+       prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+       param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+       g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (icalattach_get_is_url (attach));
+
+       check_attachment_content (attach, content, content_len);
+
+       /* Verify the remote URL did not change */
+       prop = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY);
+       g_assert_nonnull (prop);
+       g_assert_null (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+
+       attach = icalproperty_get_attach (prop);
+       g_assert_nonnull (attach);
+       g_assert (icalattach_get_is_url (attach));
+       g_assert_cmpstr (icalattach_get_url (attach), ==, REMOTE_URL);
+
+       icalcomponent_free (icalcomp);
+       g_object_unref (meta_backend);
+       g_object_unref (comp);
+       g_free (filename);
+       g_free (content);
+}
+
+static void
+test_empty_cache (TCUFixture *fixture,
+                 gconstpointer user_data)
+{
+       #define TZID "/meta/backend/test/timezone"
+       #define TZLOC "test/timezone"
+
+       const gchar *in_tzobj =
+               "BEGIN:VTIMEZONE\r\n"
+               "TZID:" TZID "\r\n"
+               "X-LIC-LOCATION:" TZLOC "\r\n"
+               "BEGIN:STANDARD\r\n"
+               "TZNAME:Test-ST\r\n"
+               "DTSTART:19701106T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+               "TZOFFSETFROM:-0400\r\n"
+               "TZOFFSETTO:-0500\r\n"
+               "END:STANDARD\r\n"
+               "BEGIN:DAYLIGHT\r\n"
+               "TZNAME:Test-DT\r\n"
+               "DTSTART:19700313T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+               "TZOFFSETFROM:-0500\r\n"
+               "TZOFFSETTO:-0400\r\n"
+               "END:DAYLIGHT\r\n"
+               "END:VTIMEZONE\r\n";
+       ECalMetaBackend *meta_backend;
+       GList *zones;
+       gboolean success;
+       GError *error = NULL;
+
+       meta_backend = e_cal_meta_backend_test_new (fixture->cal_cache);
+       g_assert_nonnull (meta_backend);
+
+       /* Add timezone to the cache */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->add_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, in_tzobj, &error);
+       g_assert_no_error (error);
+
+       zones = NULL;
+       success = e_cal_cache_list_timezones (fixture->cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 1);
+       g_list_free (zones);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), >, 0);
+       g_assert_no_error (error);
+
+       /* Empty the cache */
+       success = e_cal_meta_backend_empty_cache_sync (meta_backend, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       /* Verify the cache is truly empty */
+       zones = NULL;
+       success = e_cal_cache_list_timezones (fixture->cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 0);
+       g_list_free (zones);
+
+       g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL, 
&error), ==, 0);
+       g_assert_no_error (error);
+
+       g_object_unref (meta_backend);
+
+       #undef TZID
+       #undef TZLOC
+}
+
+static void
+test_send_objects (ECalMetaBackend *meta_backend)
+{
+       GSList *users = NULL;
+       const gchar *calobj = "fake-iCalendar-object";
+       gchar *modified_calobj = NULL;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->send_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, calobj, &users, &modified_calobj, &error);
+
+       g_assert_no_error (error);
+       g_assert_null (users);
+       g_assert_cmpstr (calobj, ==, modified_calobj);
+
+       g_free (modified_calobj);
+}
+
+static void
+test_get_attachment_uris (ECalMetaBackend *meta_backend)
+{
+       GSList *uris = NULL;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       /* non-existent event */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_attachment_uris_sync (E_CAL_BACKEND_SYNC 
(meta_backend),
+               NULL, NULL, "unknown-event", NULL, &uris, &error);
+
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (uris);
+
+       g_clear_error (&error);
+
+       /* existent event, but with no attachments */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_attachment_uris_sync (E_CAL_BACKEND_SYNC 
(meta_backend),
+               NULL, NULL, "event-1", NULL, &uris, &error);
+
+       g_assert_no_error (error);
+       g_assert_null (uris);
+
+       /* event with attachments */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_attachment_uris_sync (E_CAL_BACKEND_SYNC 
(meta_backend),
+               NULL, NULL, "event-7", NULL, &uris, &error);
+
+       g_assert_no_error (error);
+       g_assert_nonnull (uris);
+       g_assert_cmpint (g_slist_length (uris), ==, 1);
+       g_assert_cmpstr (uris->data, ==, "file:///usr/share/icons/hicolor/48x48/apps/evolution.png");
+
+       g_slist_free_full (uris, g_free);
+}
+
+static void
+test_discard_alarm (ECalMetaBackend *meta_backend)
+{
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       /* Not implemented */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->discard_alarm_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "unknown-event", NULL, NULL, &error);
+
+       g_assert_error (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED);
+
+       g_clear_error (&error);
+}
+
+static void
+test_timezones (ECalMetaBackend *meta_backend)
+{
+       #define TZID "/meta/backend/test/timezone"
+       #define TZLOC "test/timezone"
+
+       const gchar *in_tzobj =
+               "BEGIN:VTIMEZONE\r\n"
+               "TZID:" TZID "\r\n"
+               "X-LIC-LOCATION:" TZLOC "\r\n"
+               "BEGIN:STANDARD\r\n"
+               "TZNAME:Test-ST\r\n"
+               "DTSTART:19701106T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+               "TZOFFSETFROM:-0400\r\n"
+               "TZOFFSETTO:-0500\r\n"
+               "END:STANDARD\r\n"
+               "BEGIN:DAYLIGHT\r\n"
+               "TZNAME:Test-DT\r\n"
+               "DTSTART:19700313T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+               "TZOFFSETFROM:-0500\r\n"
+               "TZOFFSETTO:-0400\r\n"
+               "END:DAYLIGHT\r\n"
+               "END:VTIMEZONE\r\n";
+       ECalCache *cal_cache;
+       icalcomponent *vcalendar;
+       gchar *tzobj = NULL;
+       GList *zones;
+       gboolean success;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       /* Verify neither TZID, not LOCATION is in the timezone cache */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, TZID, &tzobj, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (tzobj);
+       g_clear_error (&error);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, TZLOC, &tzobj, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (tzobj);
+       g_clear_error (&error);
+
+       /* Add it to the cache */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->add_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, in_tzobj, &error);
+       g_assert_no_error (error);
+
+       /* Read it back */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, TZID, &tzobj, &error);
+       g_assert_no_error (error);
+       g_assert_cmpstr (tzobj, ==, in_tzobj);
+       g_free (tzobj);
+       tzobj = NULL;
+
+       /* As a non-built-in timezone it cannot be read with location, only with TZID */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, TZLOC, &tzobj, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (tzobj);
+       g_clear_error (&error);
+
+       /* Try also internal timezone, which will be renamed and added to the cache too */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "America/New_York", &tzobj, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (tzobj);
+       g_assert (strstr (tzobj, "America/New_York") != NULL);
+       g_free (tzobj);
+       tzobj = NULL;
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       vcalendar = icalcomponent_new_from_string (
+               "BEGIN:VCALENDAR\r\n"
+               "BEGIN:VTIMEZONE\r\n"
+               "TZID:tzid1\r\n"
+               "X-LIC-LOCATION:tzid/1\r\n"
+               "BEGIN:STANDARD\r\n"
+               "TZNAME:Test-ST\r\n"
+               "DTSTART:19701106T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+               "TZOFFSETFROM:-0400\r\n"
+               "TZOFFSETTO:-0500\r\n"
+               "END:STANDARD\r\n"
+               "BEGIN:DAYLIGHT\r\n"
+               "TZNAME:Test-DT\r\n"
+               "DTSTART:19700313T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+               "TZOFFSETFROM:-0500\r\n"
+               "TZOFFSETTO:-0400\r\n"
+               "END:DAYLIGHT\r\n"
+               "END:VTIMEZONE\r\n"
+               "BEGIN:VTIMEZONE\r\n"
+               "TZID:tzid2\r\n"
+               "X-LIC-LOCATION:tzid/2\r\n"
+               "BEGIN:STANDARD\r\n"
+               "TZNAME:Test-ST\r\n"
+               "DTSTART:19701106T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+               "TZOFFSETFROM:-0400\r\n"
+               "TZOFFSETTO:-0500\r\n"
+               "END:STANDARD\r\n"
+               "BEGIN:DAYLIGHT\r\n"
+               "TZNAME:Test-DT\r\n"
+               "DTSTART:19700313T020000\r\n"
+               "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+               "TZOFFSETFROM:-0500\r\n"
+               "TZOFFSETTO:-0400\r\n"
+               "END:DAYLIGHT\r\n"
+               "END:VTIMEZONE\r\n"
+               "BEGIN:VEVENT\r\n"
+               "UID:test-event\r\n"
+               "DTSTAMP:20170130T000000Z\r\n"
+               "CREATED:20170216T155507Z\r\n"
+               "LAST-MODIFIED:20170216T155543Z\r\n"
+               "SEQUENCE:1\r\n"
+               "DTSTART:20170209T013000Z\r\n"
+               "DTEND:20170209T030000Z\r\n"
+               "SUMMARY:Test Event\r\n"
+               "END:VEVENT\r\n"
+               "END:VCALENDAR\r\n");
+       g_assert_nonnull (vcalendar);
+
+       zones = NULL;
+       success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 2);
+       g_list_free (zones);
+
+       /* Merge with existing */
+       success = e_cal_meta_backend_gather_timezones_sync (meta_backend, vcalendar, FALSE, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       zones = NULL;
+       success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 4);
+       g_list_free (zones);
+
+       success = e_cal_cache_remove_timezones (cal_cache, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       _e_cal_cache_remove_loaded_timezones (cal_cache);
+
+       zones = NULL;
+       success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 0);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->add_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, in_tzobj, &error);
+       g_assert_no_error (error);
+
+       zones = NULL;
+       success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 1);
+       g_list_free (zones);
+
+       /* Remove existing and add the new */
+       success = e_cal_meta_backend_gather_timezones_sync (meta_backend, vcalendar, TRUE, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+
+       _e_cal_cache_remove_loaded_timezones (cal_cache);
+
+       zones = NULL;
+       success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+       g_assert_no_error (error);
+       g_assert (success);
+       g_assert_cmpint (g_list_length (zones), ==, 2);
+       g_list_free (zones);
+
+       icalcomponent_free (vcalendar);
+       g_object_unref (cal_cache);
+
+       #undef TZLOC
+       #undef TZID
+}
+
+static void
+test_get_free_busy (ECalMetaBackend *meta_backend)
+{
+       const gchar *expected_fbobj =
+               "BEGIN:VFREEBUSY\r\n"
+               "ORGANIZER:mailto:user@no.where\r\n";
+               "DTSTART:20170102T080000Z\r\n"
+               "DTEND:20170102T200000Z\r\n"
+               "FREEBUSY;FBTYPE=BUSY;X-SUMMARY=After-party clean up;X-LOCATION=All around:\r\n"
+               " 20170102T100000Z/20170102T180000Z\r\n"
+               "END:VFREEBUSY\r\n";
+       GSList *users, *objects = NULL;
+       time_t start, end;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       users = g_slist_prepend (NULL, (gpointer) "user@no.where");
+       users = g_slist_prepend (users, (gpointer) "unknown@no.where");
+
+       start = icaltime_as_timet (icaltime_from_string ("20170102T080000Z"));
+       end = icaltime_as_timet (icaltime_from_string ("20170102T200000Z"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_free_busy_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, users, start, end, &objects, &error);
+
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (objects), ==, 1);
+       g_assert_cmpstr (objects->data, ==, expected_fbobj);
+
+       g_slist_free_full (objects, g_free);
+       g_slist_free (users);
+}
+
+static void
+test_create_objects (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       GSList *objects, *uids = NULL, *new_components = NULL, *offline_changes;
+       gchar *calobj, *tmp;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       /* Prepare cache and server content */
+       e_cal_cache_remove_component (cal_cache, "event-7", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+       e_cal_cache_remove_component (cal_cache, "event-8", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+       e_cal_cache_remove_component (cal_cache, "event-9", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+
+       ecmb_test_remove_component (test_backend, "event-7", NULL);
+       ecmb_test_remove_component (test_backend, "event-8", NULL);
+       ecmb_test_remove_component (test_backend, "event-9", NULL);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Try to add existing event, it should fail */
+       objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-1"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, &uids, &new_components, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectIdAlreadyExists);
+       g_assert_null (uids);
+       g_assert_null (new_components);
+       g_clear_error (&error);
+       g_slist_free_full (objects, g_free);
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       /* Try to add new event */
+       objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-7"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, &uids, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (uids), ==, 1);
+       g_assert_cmpstr (uids->data, ==, "event-7");
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       g_assert_cmpint (test_backend->list_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       g_slist_free_full (uids, g_free);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       uids = NULL;
+       new_components = NULL;
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Going offline */
+       e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       /* Try to add existing event, it should fail */
+       objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-7"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, &uids, &new_components, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectIdAlreadyExists);
+       g_assert_null (uids);
+       g_assert_null (new_components);
+       g_clear_error (&error);
+       g_slist_free_full (objects, g_free);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+
+       /* Try to add new event */
+       objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-8"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, &uids, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (uids), ==, 1);
+       g_assert_cmpstr (uids->data, ==, "event-8");
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+
+       g_slist_free_full (uids, g_free);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       uids = NULL;
+       new_components = NULL;
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "event-8", NULL, NULL);
+       ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+               "event-8", NULL, NULL);
+
+       /* Going online */
+       e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       offline_changes = e_cal_cache_get_offline_changes (cal_cache, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+       /* Add event without UID */
+       calobj = tcu_new_icalstring_from_test_case ("event-9");
+       g_assert_nonnull (calobj);
+       tmp = strstr (calobj, "UID:event-9\r\n");
+       g_assert_nonnull (tmp);
+       strncpy (tmp, "X-TEST:*007", 11);
+
+       objects = g_slist_prepend (NULL, calobj);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, &uids, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (uids), ==, 1);
+       g_assert_cmpstr (uids->data, !=, "event-9");
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       g_assert_cmpint (test_backend->list_count, ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 2);
+
+       calobj = e_cal_component_get_as_string (new_components->data);
+       g_assert_nonnull (calobj);
+       g_assert_nonnull (strstr (calobj, "X-TEST:*007\r\n"));
+       g_assert_nonnull (strstr (calobj, uids->data));
+
+       g_slist_free_full (uids, g_free);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       g_free (calobj);
+       uids = NULL;
+       new_components = NULL;
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       g_object_unref (cal_cache);
+}
+
+static gchar *
+ecmb_test_modify_case (const gchar *case_name,
+                      const gchar *ridstr)
+{
+       gchar *calobj;
+       icalcomponent *icalcomp;
+
+       g_assert_nonnull (case_name);
+
+       calobj = tcu_new_icalstring_from_test_case (case_name);
+       g_assert_nonnull (calobj);
+       icalcomp = icalcomponent_new_from_string (calobj);
+       g_assert_nonnull (icalcomp);
+       g_free (calobj);
+
+       icalcomponent_set_summary (icalcomp, MODIFIED_SUMMARY_STR);
+       icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 1);
+
+       if (ridstr)
+               icalcomponent_set_recurrenceid (icalcomp, icaltime_from_string (ridstr));
+
+       calobj = icalcomponent_as_ical_string_r (icalcomp);
+       icalcomponent_free (icalcomp);
+
+       return calobj;
+}
+
+static void
+test_modify_objects (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       GSList *objects, *old_components = NULL, *new_components = NULL, *offline_changes;
+       gchar *calobj, *tmp;
+       icalcomponent *icalcomp;
+       gint old_sequence;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       /* Modify non-existing event */
+       calobj = tcu_new_icalstring_from_test_case ("event-1");
+       g_assert_nonnull (calobj);
+       tmp = strstr (calobj, "UID:event-1");
+       g_assert_nonnull (tmp);
+       strncpy (tmp + 4, "unknown", 7);
+
+       objects = g_slist_prepend (NULL, calobj);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (old_components);
+       g_assert_null (new_components);
+       g_clear_error (&error);
+       g_slist_free_full (objects, g_free);
+
+       /* Modify existing event */
+       objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-1", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+       old_sequence = icalcomponent_get_sequence (icalcomp);
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-1");
+
+       icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+       g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-1");
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Going offline */
+       e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       /* Modify event-2 */
+       objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-2", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+
+       icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+       old_sequence = icalcomponent_get_sequence (icalcomp);
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-2");
+
+       icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+       g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-2");
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Going online */
+       e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       offline_changes = e_cal_cache_get_offline_changes (cal_cache, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+       /* Modify non-recurring with THIS */
+       objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-4", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 2);
+
+       icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+       old_sequence = icalcomponent_get_sequence (icalcomp);
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-4");
+
+       icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+       g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-4");
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Modify non-detached recurring instance with ONLY_THIS */
+       objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-6", "20170227T134900"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (old_components);
+       g_assert_null (new_components);
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 2);
+       g_clear_error (&error);
+       g_slist_free_full (objects, g_free);
+
+       /* Modify detached recurring instance with ONLY_THIS */
+       objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-6-a", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, objects, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 3);
+       g_assert_cmpint (test_backend->save_count, ==, 3);
+
+       icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+       old_sequence = icalcomponent_get_sequence (icalcomp);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-6");
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+
+       icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+       g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-6");
+       g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+       g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (objects, g_free);
+       old_components = NULL;
+       new_components = NULL;
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       g_object_unref (cal_cache);
+}
+
+static void
+test_remove_objects (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       GSList *ids, *old_components = NULL, *new_components = NULL, *offline_changes;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       /* Remove non-existing event */
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("unknown-event", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (old_components);
+       g_assert_null (new_components);
+       g_clear_error (&error);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+
+       /* Remove existing event */
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-1", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_null (new_components->data);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "event-1", NULL,
+               NULL);
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free (new_components);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Remove existing detached instance */
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "event-6", "20170225T134900",
+               NULL);
+
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-6", "20170225T134900"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       /* Master object is there */
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "event-6", NULL,
+               NULL);
+       /* Just-removed detached instance is not there */
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "event-6", "20170225T134900",
+               NULL);
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Remove non-existing detached instance with ONLY_THIS - fails */
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "event-6", "20170227T134900",
+               NULL);
+
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-6", "20170227T134900"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (old_components);
+       g_assert_null (new_components);
+       g_clear_error (&error);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+
+       /* Remove non-existing detached instance with THIS - changes master object */
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-6", "20170227T134900"));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 2);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       /* Master object is there */
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "event-6", NULL,
+               NULL);
+       /* Just-removed detached instance is not there */
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "event-6", "20170227T134900",
+               NULL);
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free_full (new_components, g_object_unref);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Going offline */
+       e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       /* Remove existing event */
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-3", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_null (new_components->data);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "event-3", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+               "event-3", NULL,
+               NULL);
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free (new_components);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+       old_components = NULL;
+       new_components = NULL;
+
+       /* Going online */
+       e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "event-3", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+               "event-3", NULL,
+               NULL);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       offline_changes = e_cal_cache_get_offline_changes (cal_cache, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+       g_object_unref (cal_cache);
+}
+
+static void
+test_receive_objects (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       gchar *calobj;
+       icalcomponent *icalcomp;
+       GSList *ids, *old_components = NULL, *new_components = NULL;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       /* Organizer side - receives reply from an attendee */
+       calobj = tcu_new_icalstring_from_test_case ("invite-1");
+       g_assert_nonnull (calobj);
+
+       icalcomp = icalcomponent_new_from_string (calobj);
+       g_assert_nonnull (icalcomp);
+       g_assert_nonnull (icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT));
+       g_free (calobj);
+
+       icalcomponent_add_component (test_backend->vcalendar, icalcomponent_new_clone 
(icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT)));
+
+       icalcomponent_free (icalcomp);
+
+       /* To get the 'invite' component into local cache */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "invite", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+               "invite", NULL,
+               NULL);
+
+       calobj = NULL;
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "invite", NULL, &calobj, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert_nonnull (strstr (calobj, "PARTSTAT=NEEDS-ACTION"));
+       g_assert_null (strstr (calobj, "PARTSTAT=ACCEPTED"));
+       g_free (calobj);
+
+       calobj = tcu_new_icalstring_from_test_case ("invite-2");
+       g_assert_nonnull (calobj);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, calobj, &error);
+       g_assert_no_error (error);
+       g_free (calobj);
+
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       calobj = NULL;
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "invite", NULL, &calobj, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert_null (strstr (calobj, "PARTSTAT=NEEDS-ACTION"));
+       g_assert_nonnull (strstr (calobj, "PARTSTAT=ACCEPTED"));
+       g_free (calobj);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Remove the 'invite' component, to test also user side */
+       ids = g_slist_prepend (NULL, e_cal_component_id_new ("invite", NULL));
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, ids, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (g_slist_length (old_components), ==, 1);
+       g_assert_cmpint (g_slist_length (new_components), ==, 1);
+       g_assert_null (new_components->data);
+       g_assert_cmpint (test_backend->load_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 1);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       g_slist_free_full (old_components, g_object_unref);
+       g_slist_free (new_components);
+       g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+       old_components = NULL;
+       new_components = NULL;
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "invite", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+               "invite", NULL,
+               NULL);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* User side - receives invitation */
+       calobj = tcu_new_icalstring_from_test_case ("invite-1");
+       g_assert_nonnull (calobj);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, calobj, &error);
+       g_assert_no_error (error);
+       g_free (calobj);
+
+       g_assert_cmpint (test_backend->load_count, ==, 3);
+       g_assert_cmpint (test_backend->save_count, ==, 2);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "invite", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+               "invite", NULL,
+               NULL);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       calobj = NULL;
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "invite", NULL, &calobj, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert_nonnull (strstr (calobj, "SUMMARY:Invite\r\n"));
+       g_assert_null (strstr (calobj, "SUMMARY:Invite (modified)\r\n"));
+       g_free (calobj);
+
+       /* Receives update from organizer */
+       calobj = tcu_new_icalstring_from_test_case ("invite-3");
+       g_assert_nonnull (calobj);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, calobj, &error);
+       g_assert_no_error (error);
+       g_free (calobj);
+
+       g_assert_cmpint (test_backend->load_count, ==, 4);
+       g_assert_cmpint (test_backend->save_count, ==, 3);
+       g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+               "invite", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+               "invite", NULL,
+               NULL);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       calobj = NULL;
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "invite", NULL, &calobj, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert_null (strstr (calobj, "SUMMARY:Invite\r\n"));
+       g_assert_nonnull (strstr (calobj, "SUMMARY:Invite (modified)\r\n"));
+       g_free (calobj);
+
+       /* Receives cancellation from organizer */
+       calobj = tcu_new_icalstring_from_test_case ("invite-4");
+       g_assert_nonnull (calobj);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, calobj, &error);
+       g_assert_no_error (error);
+       g_free (calobj);
+
+       g_assert_cmpint (test_backend->load_count, ==, 4);
+       g_assert_cmpint (test_backend->save_count, ==, 3);
+       g_assert_cmpint (test_backend->remove_count, ==, 2);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+               "invite", NULL,
+               NULL);
+       ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+               "invite", NULL,
+               NULL);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       g_object_unref (cal_cache);
+}
+
+static void
+test_get_object (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       gchar *calobj = NULL;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       e_cal_cache_remove_component (cal_cache, "event-7", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+       e_cal_cache_remove_component (cal_cache, "event-8", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+       e_cal_cache_remove_component (cal_cache, "event-9", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+
+       /* Master object */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "event-6", NULL, &calobj, &error);
+
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert (strstr (calobj, "UID:event-6"));
+       g_assert (!strstr (calobj, "RECURRENCE-ID;TZID=America/New_York:20170225T134900"));
+       g_free (calobj);
+       calobj = NULL;
+
+       /* Detached instance */
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "event-6", "20170225T134900", &calobj, &error);
+
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert (strstr (calobj, "UID:event-6"));
+       g_assert (strstr (calobj, "RECURRENCE-ID;TZID=America/New_York:20170225T134900"));
+       g_free (calobj);
+       calobj = NULL;
+
+       /* Going offline */
+       e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+       g_assert (!e_cal_cache_contains (cal_cache, "event-7", NULL, E_CACHE_EXCLUDE_DELETED));
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "event-7", NULL, &calobj, &error);
+       g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+       g_assert_null (calobj);
+       g_clear_error (&error);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+       g_assert_cmpint (test_backend->list_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 0);
+
+       /* Going online */
+       e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+       g_assert (e_cal_cache_contains (cal_cache, "event-7", NULL, E_CACHE_EXCLUDE_DELETED));
+
+       /* Remove it from the cache, thus it's loaded from the "server" on demand */
+       e_cal_cache_remove_component (cal_cache, "event-7", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+       g_assert_no_error (error);
+
+       g_assert_cmpint (test_backend->connect_count, ==, 1);
+       e_cal_meta_backend_test_reset_counters (test_backend);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "event-7", NULL, &calobj, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobj);
+       g_assert_cmpint (test_backend->connect_count, ==, 0);
+       g_assert_cmpint (test_backend->list_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 1);
+       g_assert_nonnull (strstr (calobj, "UID:event-7"));
+       g_free (calobj);
+
+       g_assert (e_cal_cache_contains (cal_cache, "event-7", NULL, E_CACHE_EXCLUDE_DELETED));
+
+       g_object_unref (cal_cache);
+}
+
+static void
+test_get_object_list (ECalMetaBackend *meta_backend)
+{
+       GSList *calobjs = NULL;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_list_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "(uid? \"unknown-event\")", &calobjs, &error);
+       g_assert_no_error (error);
+       g_assert_null (calobjs);
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_list_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "(uid? \"event-3\")", &calobjs, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobjs);
+       g_assert_cmpint (g_slist_length (calobjs), ==, 1);
+       g_assert (strstr (calobjs->data, "UID:event-3"));
+       g_slist_free_full (calobjs, g_free);
+       calobjs = NULL;
+
+       E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_list_sync (E_CAL_BACKEND_SYNC (meta_backend),
+               NULL, NULL, "(uid? \"event-6\")", &calobjs, &error);
+       g_assert_no_error (error);
+       g_assert_nonnull (calobjs);
+       g_assert_cmpint (g_slist_length (calobjs), ==, 2);
+       g_assert (strstr (calobjs->data, "UID:event-6"));
+       g_assert (strstr (calobjs->next->data, "UID:event-6"));
+       g_assert_cmpstr (calobjs->data, !=, calobjs->next->data);
+       g_slist_free_full (calobjs, g_free);
+}
+
+static void
+test_refresh (ECalMetaBackend *meta_backend)
+{
+       ECalMetaBackendTest *test_backend;
+       ECalCache *cal_cache;
+       ECache *cache;
+       guint count;
+       icalcomponent *icalcomp;
+       GError *error = NULL;
+
+       g_assert_nonnull (meta_backend);
+
+       test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_assert_nonnull (cal_cache);
+
+       cache = E_CACHE (cal_cache);
+
+       /* Empty local cache */
+       e_cache_remove_all (cache, NULL, &error);
+       g_assert_no_error (error);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 0);
+
+       e_cal_meta_backend_test_reset_counters (test_backend);
+
+       ecmb_test_remove_component (test_backend, "event-6", "20170225T134900");
+       ecmb_test_remove_component (test_backend, "event-7", NULL);
+       ecmb_test_remove_component (test_backend, "event-8", NULL);
+       ecmb_test_remove_component (test_backend, "event-9", NULL);
+
+       /* Sync with server content */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 1);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 6);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 6);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Add detached instance, but do not modify the master object, thus it looks like unchanged */
+       ecmb_test_add_test_case (test_backend, "event-6-a");
+
+       /* Sync with server content */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 2);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 6);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 6);
+
+       ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, TRUE,
+               "event-1", NULL,
+               "event-2", NULL,
+               "event-3", NULL,
+               "event-4", NULL,
+               "event-5", NULL,
+               "event-6", NULL,
+               "event-6", "20170225T134900",
+               NULL);
+
+       ecmb_test_cache_contains (cal_cache, FALSE, TRUE,
+               "event-1", NULL,
+               "event-2", NULL,
+               "event-3", NULL,
+               "event-4", NULL,
+               "event-5", NULL,
+               "event-6", NULL,
+               NULL);
+
+       /* Modify the master object, thus the detached instance will be recognized */
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;
+            icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+               if (g_strcmp0 ("event-6", icalcomponent_get_uid (icalcomp)) == 0) {
+                       icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 1);
+               }
+       }
+
+       /* Sync with server content */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 3);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 7);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 7);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Add some more events */
+       ecmb_test_add_test_case (test_backend, "event-7");
+       ecmb_test_add_test_case (test_backend, "event-9");
+
+       /* Sync with server content */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 4);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 9);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 9);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Remove two events */
+       ecmb_test_remove_component (test_backend, "event-2", NULL);
+       ecmb_test_remove_component (test_backend, "event-4", NULL);
+
+       /* Sync with server content */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 5);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 9);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 7);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       /* Mix add/remove/modify */
+       ecmb_test_add_test_case (test_backend, "event-8");
+
+       ecmb_test_remove_component (test_backend, "event-3", NULL);
+       ecmb_test_remove_component (test_backend, "event-6", NULL);
+       ecmb_test_remove_component (test_backend, "event-6", "20170225T134900");
+
+       for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+            icalcomp;
+            icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+               if (g_strcmp0 ("event-5", icalcomponent_get_uid (icalcomp)) == 0 ||
+                   g_strcmp0 ("event-9", icalcomponent_get_uid (icalcomp)) == 0) {
+                       icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 1);
+               }
+       }
+
+       /* Sync with server content */
+       e_cal_meta_backend_test_call_refresh (meta_backend);
+
+       g_assert_cmpint (test_backend->list_count, ==, 6);
+       g_assert_cmpint (test_backend->save_count, ==, 0);
+       g_assert_cmpint (test_backend->load_count, ==, 12);
+       g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+       count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+       g_assert_no_error (error);
+       g_assert_cmpint (count, ==, 5);
+
+       ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+       g_object_unref (cal_cache);
+}
+
+typedef void (* TestWithMainLoopFunc) (ECalMetaBackend *meta_backend);
+
+typedef struct _MainLoopThreadData {
+       TestWithMainLoopFunc func;
+       ECalMetaBackend *meta_backend;
+       GMainLoop *main_loop;
+} MainLoopThreadData;
+
+static gpointer
+test_with_main_loop_thread (gpointer user_data)
+{
+       MainLoopThreadData *mlt = user_data;
+
+       g_assert_nonnull (mlt);
+       g_assert_nonnull (mlt->func);
+       g_assert_nonnull (mlt->meta_backend);
+
+       mlt->func (mlt->meta_backend);
+
+       g_main_loop_quit (mlt->main_loop);
+
+       return NULL;
+}
+
+static gboolean
+quit_test_with_mainloop_cb (gpointer user_data)
+{
+       GMainLoop *main_loop = user_data;
+
+       g_assert_nonnull (main_loop);
+
+       g_main_loop_quit (main_loop);
+
+       g_assert_not_reached ();
+
+       return FALSE;
+}
+
+static gboolean
+test_with_mainloop_run_thread_idle (gpointer user_data)
+{
+       GThread *thread;
+
+       g_assert_nonnull (user_data);
+
+       thread = g_thread_new (NULL, test_with_main_loop_thread, user_data);
+       g_thread_unref (thread);
+
+       return FALSE;
+}
+
+static void
+test_with_main_loop (ECalCache *cal_cache,
+                    TestWithMainLoopFunc func)
+{
+       MainLoopThreadData mlt;
+       ECalMetaBackend *meta_backend;
+       guint timeout_id;
+
+       g_assert_nonnull (cal_cache);
+       g_assert_nonnull (func);
+
+       meta_backend = e_cal_meta_backend_test_new (cal_cache);
+       g_assert_nonnull (meta_backend);
+
+       mlt.func = func;
+       mlt.meta_backend = meta_backend;
+       mlt.main_loop = g_main_loop_new (NULL, FALSE);
+
+       g_idle_add (test_with_mainloop_run_thread_idle, &mlt);
+       timeout_id = g_timeout_add_seconds (10, quit_test_with_mainloop_cb, mlt.main_loop);
+
+       g_main_loop_run (mlt.main_loop);
+
+       g_source_remove (timeout_id);
+       g_main_loop_unref (mlt.main_loop);
+       g_clear_object (&mlt.meta_backend);
+}
+
+#define main_loop_wrapper(_func) \
+static void \
+_func ## _tcu (TCUFixture *fixture, \
+              gconstpointer user_data) \
+{ \
+       test_with_main_loop (fixture->cal_cache, _func); \
+}
+
+main_loop_wrapper (test_send_objects)
+main_loop_wrapper (test_get_attachment_uris)
+main_loop_wrapper (test_discard_alarm)
+main_loop_wrapper (test_timezones)
+main_loop_wrapper (test_get_free_busy)
+main_loop_wrapper (test_create_objects)
+main_loop_wrapper (test_modify_objects)
+main_loop_wrapper (test_remove_objects)
+main_loop_wrapper (test_receive_objects)
+main_loop_wrapper (test_get_object)
+main_loop_wrapper (test_get_object_list)
+main_loop_wrapper (test_refresh)
+
+#undef main_loop_wrapper
+
+gint
+main (gint argc,
+      gchar **argv)
+{
+       ETestServerClosure tsclosure = {
+               E_TEST_SERVER_NONE,
+               NULL, /* Source customization function */
+               0,    /* Calendar Type */
+               TRUE, /* Keep the working sandbox after the test, don't remove it */
+               NULL, /* Destroy Notify function */
+       };
+       ETestServerFixture tsfixture = { 0 };
+       TCUClosure closure_events = { TCU_LOAD_COMPONENT_SET_EVENTS };
+       gint res;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+       g_type_init ();
+#endif
+       g_test_init (&argc, &argv, NULL);
+
+       /* Ensure that the client and server get the same locale */
+       g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+       setlocale (LC_ALL, "");
+
+#ifdef HAVE_ICALTZUTIL_SET_EXACT_VTIMEZONES_SUPPORT
+       icaltzutil_set_exact_vtimezones_support (0);
+#endif
+
+       e_test_server_utils_setup (&tsfixture, &tsclosure);
+
+       glob_registry = tsfixture.registry;
+       g_assert_nonnull (glob_registry);
+
+       g_test_add ("/ECalMetaBackend/MergeInstances", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_merge_instances, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/Attachments", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_attachments, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/EmptyCache", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_empty_cache, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/SendObjects", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_send_objects_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/GetAttachmentUris", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_get_attachment_uris_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/DiscardAlarm", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_discard_alarm_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/Timezones", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_timezones_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/GetFreeBusy", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_get_free_busy_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/CreateObjects", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_create_objects_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/ModifyObjects", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_modify_objects_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/RemoveObjects", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_remove_objects_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/ReceiveObjects", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_receive_objects_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/GetObject", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_get_object_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/GetObjectList", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_get_object_list_tcu, tcu_fixture_teardown);
+       g_test_add ("/ECalMetaBackend/Refresh", TCUFixture, &closure_events,
+               tcu_fixture_setup, test_refresh_tcu, tcu_fixture_teardown);
+
+       res = g_test_run ();
+
+       e_test_server_utils_teardown (&tsfixture, &tsclosure);
+
+       return res;
+}



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