[evolution-data-server/wip/offline-cache] EBookMetaBackend prototype



commit 0d3a76508b5478890fe4b741c7c2be39fe47e49b
Author: Milan Crha <mcrha redhat com>
Date:   Wed May 10 11:02:47 2017 +0200

    EBookMetaBackend prototype

 po/POTFILES.in                                     |    2 +
 src/addressbook/libedata-book/CMakeLists.txt       |    6 +-
 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       |    2 +-
 src/addressbook/libedata-book/e-book-cache.h       |    8 +-
 .../libedata-book/e-book-meta-backend.c            | 3673 ++++++++++++++++++++
 .../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      |    2 +
 src/calendar/libedata-cal/e-cal-backend.c          |   16 +-
 src/calendar/libedata-cal/e-cal-backend.h          |    2 +-
 src/calendar/libedata-cal/e-cal-meta-backend.c     |   19 +-
 14 files changed, 4620 insertions(+), 30 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 38c86b7..2a2d003 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -19,9 +19,11 @@ 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
diff --git a/src/addressbook/libedata-book/CMakeLists.txt b/src/addressbook/libedata-book/CMakeLists.txt
index 8935769..2bac3a9 100644
--- a/src/addressbook/libedata-book/CMakeLists.txt
+++ b/src/addressbook/libedata-book/CMakeLists.txt
@@ -17,9 +17,11 @@ set(SOURCES
        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
@@ -35,15 +37,17 @@ set(HEADERS
        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
index 0fbdaff..49f809c 100644
--- a/src/addressbook/libedata-book/e-book-cache.c
+++ b/src/addressbook/libedata-book/e-book-cache.c
@@ -3320,7 +3320,7 @@ ebc_search_select_cb (ECache *cache,
        }
 
        if (sd->user_func) {
-               return sd->user_func (cache, column_values[sd->uid_index], column_values[sd->revision_index],
+               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);
        }
 
diff --git a/src/addressbook/libedata-book/e-book-cache.h b/src/addressbook/libedata-book/e-book-cache.h
index 7f0aa08..e36b86c 100644
--- a/src/addressbook/libedata-book/e-book-cache.h
+++ b/src/addressbook/libedata-book/e-book-cache.h
@@ -90,7 +90,7 @@ void          e_book_cache_search_data_free   (/* EBookCacheSearchData * */ gpointer data)
 
 /**
  * EBookCacheSearchFunc:
- * @cache: an #ECache
+ * @book_cache: an #EBookCache
  * @uid: a unique object identifier
  * @revision: the object revision
  * @object: the object itself
@@ -105,7 +105,7 @@ void                e_book_cache_search_data_free   (/* EBookCacheSearchData * */ 
gpointer data)
  *
  * Since: 3.26
  **/
-typedef gboolean (* EBookCacheSearchFunc)      (ECache *cache,
+typedef gboolean (* EBookCacheSearchFunc)      (EBookCache *book_cache,
                                                 const gchar *uid,
                                                 const gchar *revision,
                                                 const gchar *object,
@@ -258,12 +258,12 @@ gboolean  e_book_cache_get_contact_extra  (EBookCache *book_cache,
 gboolean       e_book_cache_search             (EBookCache *book_cache,
                                                 const gchar *sexp,
                                                 gboolean meta_contacts,
-                                                GSList **out_list,
+                                                GSList **out_list, /* EBookCacheSearchData * */
                                                 GCancellable *cancellable,
                                                 GError **error);
 gboolean       e_book_cache_search_uids        (EBookCache *book_cache,
                                                 const gchar *sexp,
-                                                GSList **out_list,
+                                                GSList **out_list, /* gchar * */
                                                 GCancellable *cancellable,
                                                 GError **error);
 gboolean       e_book_cache_search_with_callback
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..bac3b4a
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-meta-backend.c
@@ -0,0 +1,3673 @@
+/* -*- 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 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;
+       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,
+                                               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);
+
+       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);
+               e_named_parameters_free (credentials);
+
+               return TRUE;
+       }
+
+       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_KEEP_LOCAL, 
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 (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, 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;
+                               }
+
+                               success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, nfo->uid, 
nfo->object, nfo->extra, 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) &&
+           e_book_meta_backend_disconnect_sync (meta_backend, cancellable, error)) {
+               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);
+
+       e_book_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+
+       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,
+                               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);
+
+       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 ((EContact *) 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) {
+               success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, new_uid, NULL,
+                       new_extra ? new_extra : extra, cancellable, error);
+
+               if (success && g_strcmp0 (new_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 = new_uid;
+               else
+                       g_free (new_uid);
+
+               if (out_requires_put)
+                       *out_requires_put = FALSE;
+       }
+
+       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)));
+       }
+
+       /* 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))
+                       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 = NULL;
+                       } 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_KEEP_LOCAL;
+       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_KEEP_LOCAL;
+       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_KEEP_LOCAL;
+       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 != NULL, 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)) {
+               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, cancellable, 
NULL)) {
+                       found = e_book_cache_get_contact (book_cache, uid, FALSE, &contact, cancellable, 
NULL);
+               }
+
+               if (!found)
+                       g_propagate_error (error, e_data_book_create_error 
(E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL));
+       } 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);
+       }
+
+       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;
+       }
+
+       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_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;
+       }
+
+       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->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->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;
+}
+
+/**
+ * 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);
+
+       g_clear_object (&meta_backend->priv->cache);
+       meta_backend->priv->cache = g_object_ref (cache);
+
+       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) {
+                       const 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);
+                       }
+               }
+       }
+
+       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;
+
+       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;
+
+       local_filename = g_build_filename (cache_path, prefix, extension ? "." : NULL, extension, NULL);
+
+       g_object_unref (book_cache);
+       g_free (cache_path);
+       g_free (checksum);
+       g_free (prefix);
+       g_free (extension);
+
+       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..f21f5e8
--- /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 af797cb..50ee041 100644
--- a/src/addressbook/libedata-book/libedata-book.h
+++ b/src/addressbook/libedata-book/libedata-book.h
@@ -30,8 +30,10 @@
 #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/libedata-cal/e-cal-backend.c b/src/calendar/libedata-cal/e-cal-backend.c
index f4e3651..4f1934c 100644
--- a/src/calendar/libedata-cal/e-cal-backend.c
+++ b/src/calendar/libedata-cal/e-cal-backend.c
@@ -4617,7 +4617,7 @@ e_cal_backend_prepare_for_completion (ECalBackend *backend,
 
 /**
  * e_cal_backend_schedule_custom_operation:
- * @backend: an #ECalBackend
+ * @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
@@ -4635,7 +4635,7 @@ e_cal_backend_prepare_for_completion (ECalBackend *backend,
  * Since: 3.26
  **/
 void
-e_cal_backend_schedule_custom_operation (ECalBackend *backend,
+e_cal_backend_schedule_custom_operation (ECalBackend *cal_backend,
                                         GCancellable *use_cancellable,
                                         ECalBackendCustomOpFunc func,
                                         gpointer user_data,
@@ -4643,14 +4643,14 @@ e_cal_backend_schedule_custom_operation (ECalBackend *backend,
 {
        DispatchNode *node;
 
-       g_return_if_fail (E_IS_CAL_BACKEND (backend));
+       g_return_if_fail (E_IS_CAL_BACKEND (cal_backend));
        g_return_if_fail (func != NULL);
 
-       g_mutex_lock (&backend->priv->operation_lock);
+       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 (backend);
+       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;
@@ -4658,9 +4658,9 @@ e_cal_backend_schedule_custom_operation (ECalBackend *backend,
        if (G_IS_CANCELLABLE (use_cancellable))
                node->cancellable = g_object_ref (use_cancellable);
 
-       g_queue_push_tail (&backend->priv->pending_operations, node);
+       g_queue_push_tail (&cal_backend->priv->pending_operations, node);
 
-       g_mutex_unlock (&backend->priv->operation_lock);
+       g_mutex_unlock (&cal_backend->priv->operation_lock);
 
-       cal_backend_dispatch_next_operation (backend);
+       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 84e5532..91d52c7 100644
--- a/src/calendar/libedata-cal/e-cal-backend.h
+++ b/src/calendar/libedata-cal/e-cal-backend.h
@@ -547,7 +547,7 @@ typedef void        (* ECalBackendCustomOpFunc)     (ECalBackend *cal_backend,
                                                 GError **error);
 
 void           e_cal_backend_schedule_custom_operation
-                                               (ECalBackend *backend,
+                                               (ECalBackend *cal_backend,
                                                 GCancellable *use_cancellable,
                                                 ECalBackendCustomOpFunc func,
                                                 gpointer user_data,
diff --git a/src/calendar/libedata-cal/e-cal-meta-backend.c b/src/calendar/libedata-cal/e-cal-meta-backend.c
index c6d032b..620f0bd 100644
--- a/src/calendar/libedata-cal/e-cal-meta-backend.c
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.c
@@ -119,7 +119,7 @@ static gboolean ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
                                                  GError **error);
 
 /**
- * e_cal_cache_search_data_new:
+ * 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
@@ -1384,7 +1384,6 @@ ecmb_refresh_sync (ECalBackendSync *sync_backend,
                   EDataCal *cal,
                   GCancellable *cancellable,
                   GError **error)
-
 {
        ECalMetaBackend *meta_backend;
 
@@ -3959,11 +3958,11 @@ e_cal_meta_backend_empty_cache_sync (ECalMetaBackend *meta_backend,
  * @cancellable: optional #GCancellable object, or %NULL
  * @error: return location for a #GError, or %NULL
  *
- * This is called always before any operations which requires a connection
+ * 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 not online.
+ * 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
@@ -3974,7 +3973,7 @@ e_cal_meta_backend_empty_cache_sync (ECalMetaBackend *meta_backend,
  * 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 reasons.
+ * 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
@@ -3991,7 +3990,7 @@ e_cal_meta_backend_empty_cache_sync (ECalMetaBackend *meta_backend,
  * Since: 3.26
  **/
 gboolean
-e_cal_meta_backend_connect_sync        (ECalMetaBackend *meta_backend,
+e_cal_meta_backend_connect_sync (ECalMetaBackend *meta_backend,
                                 const ENamedParameters *credentials,
                                 ESourceAuthenticationResult *out_auth_result,
                                 gchar **out_certificate_pem,
@@ -4365,10 +4364,10 @@ e_cal_meta_backend_remove_component_sync (ECalMetaBackend *meta_backend,
  * @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.
+ * 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
+ * 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.
@@ -4408,10 +4407,10 @@ e_cal_meta_backend_search_sync (ECalMetaBackend *meta_backend,
  * @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.
+ * 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
+ * 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.


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