[evolution-data-server] I#165 - Add backend to access Nextcloud Notes



commit ae8c55886bc2dfda2bae3c4324d922276d087be7
Author: Milan Crha <mcrha redhat com>
Date:   Fri Mar 6 09:07:37 2020 +0100

    I#165 - Add backend to access Nextcloud Notes
    
    Closes https://gitlab.gnome.org/GNOME/evolution-data-server/issues/165

 po/POTFILES.in                                     |    2 +
 src/calendar/backends/CMakeLists.txt               |    1 +
 src/calendar/backends/webdav-notes/CMakeLists.txt  |   47 +
 .../e-cal-backend-webdav-notes-factory.c           |   71 +
 .../webdav-notes/e-cal-backend-webdav-notes.c      | 1432 ++++++++++++++++++++
 .../webdav-notes/e-cal-backend-webdav-notes.h      |   60 +
 src/calendar/libecal/e-cal-util.h                  |   11 +
 src/libebackend/e-webdav-collection-backend.c      |   11 +-
 src/libedataserver/e-webdav-discover.c             |   56 +-
 src/libedataserver/e-webdav-discover.h             |    1 +
 src/libedataserver/e-webdav-session.c              |   47 +-
 src/libedataserver/e-webdav-session.h              |    6 +-
 src/libedataserverui/e-webdav-discover-widget.c    |    2 +-
 .../evolution-source-registry/CMakeLists.txt       |    1 +
 .../builtin/webdav-notes-stub.source.in            |    5 +
 .../evolution-source-registry-resource.xml         |    1 +
 16 files changed, 1725 insertions(+), 29 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 98382fb63..d69bd9931 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -31,6 +31,7 @@ src/calendar/backends/contacts/e-cal-backend-contacts.c
 src/calendar/backends/file/e-cal-backend-file.c
 src/calendar/backends/http/e-cal-backend-http.c
 src/calendar/backends/weather/e-cal-backend-weather.c
+src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.c
 src/calendar/libecal/e-cal-client.c
 src/calendar/libecal/e-cal-client-view.c
 src/calendar/libecal/e-cal-component.c
@@ -241,6 +242,7 @@ src/services/evolution-calendar-factory/evolution-calendar-factory.c
 [type: gettext/ini]src/services/evolution-source-registry/builtin/vfolder.source.in
 [type: gettext/ini]src/services/evolution-source-registry/builtin/weather-stub.source.in
 [type: gettext/ini]src/services/evolution-source-registry/builtin/webcal-stub.source.in
+[type: gettext/ini]src/services/evolution-source-registry/builtin/webdav-notes-stub.source.in
 src/services/evolution-source-registry/evolution-source-registry.c
 src/services/evolution-user-prompter/evolution-user-prompter.c
 src/services/evolution-user-prompter/prompt-user-gtk.c
diff --git a/src/calendar/backends/CMakeLists.txt b/src/calendar/backends/CMakeLists.txt
index 56b8c3836..0de3a6d18 100644
--- a/src/calendar/backends/CMakeLists.txt
+++ b/src/calendar/backends/CMakeLists.txt
@@ -2,6 +2,7 @@ add_subdirectory(caldav)
 add_subdirectory(contacts)
 add_subdirectory(file)
 add_subdirectory(http)
+add_subdirectory(webdav-notes)
 
 if(HAVE_LIBGDATA)
        add_subdirectory(gtasks)
diff --git a/src/calendar/backends/webdav-notes/CMakeLists.txt 
b/src/calendar/backends/webdav-notes/CMakeLists.txt
new file mode 100644
index 000000000..4a1ae855b
--- /dev/null
+++ b/src/calendar/backends/webdav-notes/CMakeLists.txt
@@ -0,0 +1,47 @@
+set(DEPENDENCIES
+       ebackend
+       ecal
+       edataserver
+       edata-cal
+)
+
+set(SOURCES
+       e-cal-backend-webdav-notes-factory.c
+       e-cal-backend-webdav-notes.c
+       e-cal-backend-webdav-notes.h
+)
+
+add_library(ecalbackendwebdavnotes MODULE
+       ${SOURCES}
+)
+
+add_dependencies(ecalbackendwebdavnotes
+       ${DEPENDENCIES}
+)
+
+target_compile_definitions(ecalbackendwebdavnotes PRIVATE
+       -DG_LOG_DOMAIN=\"e-cal-backend-webdav-notes\"
+       -DBACKENDDIR=\"${ecal_backenddir}\"
+)
+
+target_compile_options(ecalbackendwebdavnotes PUBLIC
+       ${CALENDAR_CFLAGS}
+)
+
+target_include_directories(ecalbackendwebdavnotes PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_BINARY_DIR}/src
+       ${CMAKE_SOURCE_DIR}/src
+       ${CMAKE_BINARY_DIR}/src/calendar
+       ${CMAKE_SOURCE_DIR}/src/calendar
+       ${CALENDAR_INCLUDE_DIRS}
+)
+
+target_link_libraries(ecalbackendwebdavnotes
+       ${DEPENDENCIES}
+       ${CALENDAR_LDFLAGS}
+)
+
+install(TARGETS ecalbackendwebdavnotes
+       DESTINATION ${ecal_backenddir}
+)
diff --git a/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes-factory.c 
b/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes-factory.c
new file mode 100644
index 000000000..b38f0c85a
--- /dev/null
+++ b/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes-factory.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 Red Hat (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include "e-cal-backend-webdav-notes.h"
+
+typedef ECalBackendFactory ECalBackendWebDAVNotesFactory;
+typedef ECalBackendFactoryClass ECalBackendWebDAVNotesFactoryClass;
+
+static EModule *e_module;
+
+/* Module Entry Points */
+void e_module_load (GTypeModule *type_module);
+void e_module_unload (GTypeModule *type_module);
+
+/* Forward Declarations */
+GType e_cal_backend_webdav_notes_factory_get_type (void);
+
+G_DEFINE_DYNAMIC_TYPE (ECalBackendWebDAVNotesFactory, e_cal_backend_webdav_notes_factory, 
E_TYPE_CAL_BACKEND_FACTORY)
+
+static void
+e_cal_backend_webdav_notes_factory_class_init (ECalBackendFactoryClass *class)
+{
+       EBackendFactoryClass *backend_factory_class;
+
+       backend_factory_class = E_BACKEND_FACTORY_CLASS (class);
+       backend_factory_class->e_module = e_module;
+       backend_factory_class->share_subprocess = TRUE;
+
+       class->factory_name = "webdav-notes";
+       class->component_kind = I_CAL_VJOURNAL_COMPONENT;
+       class->backend_type = E_TYPE_CAL_BACKEND_WEBDAV_NOTES;
+}
+
+static void
+e_cal_backend_webdav_notes_factory_class_finalize (ECalBackendFactoryClass *class)
+{
+}
+
+static void
+e_cal_backend_webdav_notes_factory_init (ECalBackendFactory *factory)
+{
+}
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+       e_module = E_MODULE (type_module);
+
+       e_cal_backend_webdav_notes_factory_register_type (type_module);
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+       e_module = NULL;
+}
diff --git a/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.c 
b/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.c
new file mode 100644
index 000000000..a4d2bd01d
--- /dev/null
+++ b/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.c
@@ -0,0 +1,1432 @@
+/*
+ * Copyright (C) 2020 Red Hat (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+#include <glib/gi18n-lib.h>
+
+#include <libedataserver/libedataserver.h>
+
+#include "e-cal-backend-webdav-notes.h"
+
+#define E_WEBDAV_NOTES_X_ETAG "X-EVOLUTION-WEBDAV-NOTES-ETAG"
+
+#define ECC_ERROR(_code) e_cal_client_error_create (_code, NULL)
+#define ECC_ERROR_EX(_code, _msg) e_cal_client_error_create (_code, _msg)
+
+struct _ECalBackendWebDAVNotesPrivate {
+       /* The main WebDAV session  */
+       EWebDAVSession *webdav;
+       GMutex webdav_lock;
+
+       gboolean etag_supported;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (ECalBackendWebDAVNotes, e_cal_backend_webdav_notes, E_TYPE_CAL_META_BACKEND)
+
+static EWebDAVSession *
+ecb_webdav_notes_ref_session (ECalBackendWebDAVNotes *cbnotes)
+{
+       EWebDAVSession *webdav;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (cbnotes), NULL);
+
+       g_mutex_lock (&cbnotes->priv->webdav_lock);
+       if (cbnotes->priv->webdav)
+               webdav = g_object_ref (cbnotes->priv->webdav);
+       else
+               webdav = NULL;
+       g_mutex_unlock (&cbnotes->priv->webdav_lock);
+
+       return webdav;
+}
+
+static gboolean
+ecb_webdav_notes_connect_sync (ECalMetaBackend *meta_backend,
+                              const ENamedParameters *credentials,
+                              ESourceAuthenticationResult *out_auth_result,
+                              gchar **out_certificate_pem,
+                              GTlsCertificateFlags *out_certificate_errors,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       GHashTable *capabilities = NULL, *allows = NULL;
+       ESource *source;
+       gboolean success, is_writable = FALSE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+
+       g_mutex_lock (&cbnotes->priv->webdav_lock);
+       if (cbnotes->priv->webdav) {
+               g_mutex_unlock (&cbnotes->priv->webdav_lock);
+               return TRUE;
+       }
+       g_mutex_unlock (&cbnotes->priv->webdav_lock);
+
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+
+       webdav = e_webdav_session_new (source);
+
+       e_soup_session_setup_logging (E_SOUP_SESSION (webdav), g_getenv ("WEBDAV_NOTES_DEBUG"));
+
+       e_binding_bind_property (
+               cbnotes, "proxy-resolver",
+               webdav, "proxy-resolver",
+               G_BINDING_SYNC_CREATE);
+
+       /* This is just to make it similar to the CalDAV backend. */
+       cbnotes->priv->etag_supported = TRUE;
+
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+
+       e_soup_session_set_credentials (E_SOUP_SESSION (webdav), credentials);
+
+       success = e_webdav_session_options_sync (webdav, NULL,
+               &capabilities, &allows, cancellable, &local_error);
+
+       if (success && !g_cancellable_is_cancelled (cancellable)) {
+               GSList *privileges = NULL, *link;
+
+               /* Ignore any errors here */
+               if (e_webdav_session_get_current_user_privilege_set_sync (webdav, NULL, &privileges, 
cancellable, NULL)) {
+                       for (link = privileges; link && !is_writable; link = g_slist_next (link)) {
+                               EWebDAVPrivilege *privilege = link->data;
+
+                               if (privilege) {
+                                       is_writable =
+                                               privilege->hint == E_WEBDAV_PRIVILEGE_HINT_WRITE ||
+                                               privilege->hint == E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT ||
+                                               privilege->hint == E_WEBDAV_PRIVILEGE_HINT_ALL;
+                               }
+                       }
+
+                       g_slist_free_full (privileges, e_webdav_privilege_free);
+               } else {
+                       is_writable = allows && (
+                               g_hash_table_contains (allows, SOUP_METHOD_PUT) ||
+                               g_hash_table_contains (allows, SOUP_METHOD_POST) ||
+                               g_hash_table_contains (allows, SOUP_METHOD_DELETE));
+               }
+       }
+
+       if (success) {
+               e_cal_backend_set_writable (E_CAL_BACKEND (cbnotes), is_writable);
+               e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
+       }
+
+       if (success) {
+               gchar *ctag = NULL;
+
+               /* Some servers, notably Google, allow OPTIONS when not
+                  authorized (aka without credentials), thus try something
+                  more aggressive, just in case.
+
+                  The 'getctag' extension is not required, thuch check
+                  for unauthorized error only. */
+               if (!e_webdav_session_getctag_sync (webdav, NULL, &ctag, cancellable, &local_error) &&
+                   g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       success = FALSE;
+               } else {
+                       g_clear_error (&local_error);
+               }
+
+               g_free (ctag);
+       }
+
+       if (!success) {
+               gboolean credentials_empty;
+               gboolean is_ssl_error;
+
+               credentials_empty = (!credentials || !e_named_parameters_count (credentials)) &&
+                       e_soup_session_get_authentication_requires_credentials (E_SOUP_SESSION (webdav));
+               is_ssl_error = g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED);
+
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+
+               /* because evolution knows only G_IO_ERROR_CANCELLED */
+               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_CANCELLED)) {
+                       local_error->domain = G_IO_ERROR;
+                       local_error->code = G_IO_ERROR_CANCELLED;
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) && 
credentials_empty) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       if (credentials_empty)
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       else
+                               *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+               } else if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) ||
+                          (!e_soup_session_get_authentication_requires_credentials (E_SOUP_SESSION (webdav)) 
&&
+                          g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+               } else if (!local_error) {
+                       g_set_error_literal (&local_error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               _("Unknown error"));
+               }
+
+               if (local_error) {
+                       g_propagate_error (error, local_error);
+                       local_error = NULL;
+               }
+
+               if (is_ssl_error) {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
+
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+                       e_soup_session_get_ssl_error_details (E_SOUP_SESSION (webdav), out_certificate_pem, 
out_certificate_errors);
+               } else {
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+               }
+       }
+
+       if (capabilities)
+               g_hash_table_destroy (capabilities);
+       if (allows)
+               g_hash_table_destroy (allows);
+
+       if (success && !g_cancellable_set_error_if_cancelled (cancellable, error)) {
+               g_mutex_lock (&cbnotes->priv->webdav_lock);
+               cbnotes->priv->webdav = webdav;
+               g_mutex_unlock (&cbnotes->priv->webdav_lock);
+       } else {
+               if (success) {
+                       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+                       success = FALSE;
+               }
+
+               g_clear_object (&webdav);
+       }
+
+       return success;
+}
+
+static gboolean
+ecb_webdav_notes_disconnect_sync (ECalMetaBackend *meta_backend,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       ESource *source;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+
+       g_mutex_lock (&cbnotes->priv->webdav_lock);
+       if (cbnotes->priv->webdav)
+               soup_session_abort (SOUP_SESSION (cbnotes->priv->webdav));
+
+       g_clear_object (&cbnotes->priv->webdav);
+       g_mutex_unlock (&cbnotes->priv->webdav_lock);
+
+       source = e_backend_get_source (E_BACKEND (meta_backend));
+       e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+
+       return TRUE;
+}
+
+typedef struct _WebDAVNotesChangesData {
+       GSList **out_modified_objects;
+       GSList **out_removed_objects;
+       GHashTable *known_items; /* gchar *href ~> ECalMetaBackendInfo * */
+} WebDAVNotesChangesData;
+
+static gboolean
+ecb_webdav_notes_search_changes_cb (ECalCache *cal_cache,
+                                   const gchar *uid,
+                                   const gchar *rid,
+                                   const gchar *revision,
+                                   const gchar *object,
+                                   const gchar *extra,
+                                   guint32 custom_flags,
+                                   EOfflineState offline_state,
+                                   gpointer user_data)
+{
+       WebDAVNotesChangesData *ccd = user_data;
+
+       g_return_val_if_fail (ccd != NULL, FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       /* The 'extra' can be NULL for added components in offline mode */
+       if (((extra && *extra) || offline_state != E_OFFLINE_STATE_LOCALLY_CREATED) && (!rid || !*rid)) {
+               ECalMetaBackendInfo *nfo;
+
+               nfo = (extra && *extra) ? g_hash_table_lookup (ccd->known_items, extra) : NULL;
+               if (nfo) {
+                       if (g_strcmp0 (revision, nfo->revision) == 0) {
+                               g_hash_table_remove (ccd->known_items, extra);
+                       } else {
+                               if (!nfo->uid || !*(nfo->uid)) {
+                                       g_free (nfo->uid);
+                                       nfo->uid = g_strdup (uid);
+                               }
+
+                               *(ccd->out_modified_objects) = g_slist_prepend (*(ccd->out_modified_objects),
+                                       e_cal_meta_backend_info_copy (nfo));
+
+                               g_hash_table_remove (ccd->known_items, extra);
+                       }
+               } else {
+                       *(ccd->out_removed_objects) = g_slist_prepend (*(ccd->out_removed_objects),
+                               e_cal_meta_backend_info_new (uid, revision, object, extra));
+               }
+       }
+
+       return TRUE;
+}
+
+static void
+ecb_webdav_notes_check_credentials_error (ECalBackendWebDAVNotes *cbnotes,
+                                         EWebDAVSession *webdav,
+                                         GError *op_error)
+{
+       g_return_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (cbnotes));
+
+       if (g_error_matches (op_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED) && webdav) {
+               op_error->domain = E_CLIENT_ERROR;
+               op_error->code = E_CLIENT_ERROR_TLS_NOT_AVAILABLE;
+       } else if (g_error_matches (op_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
+                  g_error_matches (op_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
+               gboolean was_forbidden = g_error_matches (op_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN);
+
+               op_error->domain = E_CLIENT_ERROR;
+               op_error->code = E_CLIENT_ERROR_AUTHENTICATION_REQUIRED;
+
+               if (webdav) {
+                       ENamedParameters *credentials;
+                       gboolean empty_credentials;
+
+                       credentials = e_soup_session_dup_credentials (E_SOUP_SESSION (webdav));
+                       empty_credentials = !credentials || !e_named_parameters_count (credentials);
+                       e_named_parameters_free (credentials);
+
+                       if (!empty_credentials) {
+                               if (was_forbidden) {
+                                       if (e_webdav_session_get_last_dav_error_is_permission (webdav)) {
+                                               op_error->code = E_CLIENT_ERROR_PERMISSION_DENIED;
+                                               g_free (op_error->message);
+                                               op_error->message = g_strdup (e_client_error_to_string 
(op_error->code));
+                                       } else {
+                                               /* To avoid credentials prompt */
+                                               op_error->code = E_CLIENT_ERROR_OTHER_ERROR;
+                                       }
+                               } else {
+                                       op_error->code = E_CLIENT_ERROR_AUTHENTICATION_FAILED;
+                               }
+                       }
+               }
+       }
+}
+
+static gboolean
+ecb_webdav_notes_getetag_cb (EWebDAVSession *webdav,
+                            xmlXPathContextPtr xpath_ctx,
+                            const gchar *xpath_prop_prefix,
+                            const SoupURI *request_uri,
+                            const gchar *href,
+                            guint status_code,
+                            gpointer user_data)
+{
+       if (!xpath_prop_prefix)
+               return TRUE;
+
+       if (status_code == SOUP_STATUS_OK) {
+               gchar **out_etag = user_data;
+               gchar *etag;
+
+               g_return_val_if_fail (out_etag != NULL, FALSE);
+
+               etag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:getetag", xpath_prop_prefix);
+
+               if (etag && *etag) {
+                       *out_etag = e_webdav_session_util_maybe_dequote (etag);
+               } else {
+                       g_free (etag);
+               }
+       }
+
+       return FALSE;
+}
+
+static gboolean
+ecb_webdav_notes_getetag_sync (EWebDAVSession *webdav,
+                              const gchar *uri,
+                              gchar **out_etag,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       EXmlDocument *xml;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+       g_return_val_if_fail (out_etag != NULL, FALSE);
+
+       *out_etag = NULL;
+
+       xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       e_xml_document_start_element (xml, NULL, "prop");
+       e_xml_document_add_empty_element (xml, NULL, "getetag");
+       e_xml_document_end_element (xml); /* prop */
+
+       success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+               ecb_webdav_notes_getetag_cb, out_etag, cancellable, error);
+
+       g_object_unref (xml);
+
+       return success && *out_etag != NULL;
+}
+
+static ICalComponent *
+ecb_webdav_notes_new_icomp (glong creation_date,
+                           glong last_modified,
+                           const gchar *uid,
+                           const gchar *revision,
+                           const gchar *summary,
+                           const gchar *description)
+{
+       ICalComponent *icomp;
+       ICalTime *itt;
+       glong tt;
+
+       icomp = i_cal_component_new_vjournal ();
+
+       if (creation_date > 0)
+               tt = creation_date;
+       else if (last_modified > 0)
+               tt = last_modified;
+       else
+               tt = (time_t) time (NULL);
+
+       itt = i_cal_time_new_from_timet_with_zone ((time_t) tt, FALSE, i_cal_timezone_get_utc_timezone ());
+       i_cal_component_take_property (icomp, i_cal_property_new_created (itt));
+       g_object_unref (itt);
+
+       if (last_modified > 0)
+               tt = last_modified;
+       else
+               tt = (time_t) time (NULL);
+
+       itt = i_cal_time_new_from_timet_with_zone ((time_t) tt, FALSE, i_cal_timezone_get_utc_timezone ());
+       i_cal_component_take_property (icomp, i_cal_property_new_lastmodified (itt));
+       g_object_unref (itt);
+
+       i_cal_component_set_uid (icomp, uid);
+
+       if (summary && g_str_has_suffix (summary, ".txt")) {
+               gchar *tmp;
+
+               tmp = g_strndup (summary, strlen (summary) - 4);
+               i_cal_component_set_summary (icomp, tmp);
+               g_free (tmp);
+       } else if (summary && *summary) {
+               i_cal_component_set_summary (icomp, summary);
+       }
+
+       if (description)
+               i_cal_component_set_description (icomp, description);
+
+       e_cal_util_component_set_x_property (icomp, E_WEBDAV_NOTES_X_ETAG, revision);
+
+       return icomp;
+}
+
+static gboolean
+ecb_webdav_notes_get_objects_sync (EWebDAVSession *webdav,
+                                  GHashTable *resources_hash, /* gchar *href ~> EWebDAVResource * */
+                                  GSList *infos, /* ECalMetaBackendInfo * */
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       GSList *link;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+
+       for (link = infos; success && link; link = g_slist_next (link)) {
+               ECalMetaBackendInfo *nfo = link->data;
+               gchar *etag = NULL, *bytes = NULL;
+
+               if (!nfo)
+                       continue;
+
+               success = e_webdav_session_get_data_sync (webdav, nfo->extra, NULL, &etag, &bytes, NULL, 
cancellable, error);
+
+               if (success) {
+                       EWebDAVResource *resource;
+
+                       resource = g_hash_table_lookup (resources_hash, nfo->extra);
+
+                       if (resource) {
+                               ICalComponent *icomp;
+
+                               if (g_strcmp0 (nfo->revision, etag) != 0) {
+                                       g_free (nfo->revision);
+                                       nfo->revision = etag;
+                                       etag = NULL;
+                               }
+
+                               icomp = ecb_webdav_notes_new_icomp (resource->creation_date,
+                                       resource->last_modified,
+                                       nfo->uid,
+                                       nfo->revision,
+                                       resource->display_name,
+                                       bytes);
+
+                               g_warn_if_fail (nfo->object == NULL);
+
+                               nfo->object = i_cal_component_as_ical_string (icomp);
+
+                               g_object_unref (icomp);
+                       } else { /* !resource */
+                               g_warn_if_reached ();
+                       }
+               }
+
+               g_free (etag);
+               g_free (bytes);
+       }
+
+       return success;
+}
+
+static gchar *
+ecb_webdav_notes_href_to_uid (const gchar *href)
+{
+       const gchar *filename;
+
+       if (!href || !*href)
+               return NULL;
+
+       filename = strrchr (href, '/');
+
+       if (filename && filename[1])
+               return g_uri_unescape_string (filename + 1, NULL);
+
+       return g_uri_unescape_string (href, NULL);
+}
+
+static gboolean
+ecb_webdav_notes_get_changes_sync (ECalMetaBackend *meta_backend,
+                                  const gchar *last_sync_tag,
+                                  gboolean is_repeat,
+                                  gchar **out_new_sync_tag,
+                                  gboolean *out_repeat,
+                                  GSList **out_created_objects,
+                                  GSList **out_modified_objects,
+                                  GSList **out_removed_objects,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       GHashTable *known_items; /* gchar *href ~> ECalMetaBackendInfo * */
+       GHashTable *resources_hash; /* gchar *href ~> EWebDAVResource *, both borrowed from 'resources' 
GSList */
+       GHashTableIter iter;
+       GSList *resources = NULL, *link;
+       gpointer key = NULL, value = NULL;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag, FALSE);
+       g_return_val_if_fail (out_created_objects, FALSE);
+       g_return_val_if_fail (out_modified_objects, FALSE);
+       g_return_val_if_fail (out_removed_objects, FALSE);
+
+       *out_new_sync_tag = NULL;
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+       webdav = ecb_webdav_notes_ref_session (cbnotes);
+
+       if (cbnotes->priv->etag_supported) {
+               gchar *new_sync_tag = NULL;
+
+               success = ecb_webdav_notes_getetag_sync (webdav, NULL, &new_sync_tag, cancellable, NULL);
+               if (!success) {
+                       cbnotes->priv->etag_supported = g_cancellable_set_error_if_cancelled (cancellable, 
error);
+                       if (cbnotes->priv->etag_supported || !webdav) {
+                               g_clear_object (&webdav);
+                               return FALSE;
+                       }
+               } else if (new_sync_tag && last_sync_tag && g_strcmp0 (last_sync_tag, new_sync_tag) == 0) {
+                       *out_new_sync_tag = new_sync_tag;
+                       g_clear_object (&webdav);
+                       return TRUE;
+               }
+
+               *out_new_sync_tag = new_sync_tag;
+       }
+
+       known_items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_cal_meta_backend_info_free);
+       resources_hash = g_hash_table_new (g_str_hash, g_str_equal);
+
+       success = e_webdav_session_list_sync (webdav, NULL, E_WEBDAV_DEPTH_THIS_AND_CHILDREN,
+               E_WEBDAV_LIST_ETAG | E_WEBDAV_LIST_DISPLAY_NAME | E_WEBDAV_LIST_CREATION_DATE | 
E_WEBDAV_LIST_LAST_MODIFIED,
+               &resources, cancellable, &local_error);
+
+       if (success) {
+               ECalCache *cal_cache;
+               WebDAVNotesChangesData ccd;
+
+               for (link = resources; link; link = g_slist_next (link)) {
+                       EWebDAVResource *resource = link->data;
+
+                       if (resource && resource->kind == E_WEBDAV_RESOURCE_KIND_RESOURCE && resource->href 
&& g_str_has_suffix (resource->href, ".txt")) {
+                               gchar *filename = ecb_webdav_notes_href_to_uid (resource->href);
+
+                               g_hash_table_insert (known_items, g_strdup (resource->href),
+                                       e_cal_meta_backend_info_new (filename, resource->etag, NULL, 
resource->href));
+
+                               g_hash_table_insert (resources_hash, resource->href, resource);
+
+                               g_free (filename);
+                       }
+               }
+
+               ccd.out_modified_objects = out_modified_objects;
+               ccd.out_removed_objects = out_removed_objects;
+               ccd.known_items = known_items;
+
+               cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+
+               success = e_cal_cache_search_with_callback (cal_cache, NULL, 
ecb_webdav_notes_search_changes_cb, &ccd, cancellable, &local_error);
+
+               g_clear_object (&cal_cache);
+       }
+
+       if (success) {
+               g_hash_table_iter_init (&iter, known_items);
+               while (g_hash_table_iter_next (&iter, &key, &value)) {
+                       *out_created_objects = g_slist_prepend (*out_created_objects, 
e_cal_meta_backend_info_copy (value));
+               }
+       }
+
+       g_hash_table_destroy (known_items);
+
+       if (success && (*out_created_objects || *out_modified_objects)) {
+               success = ecb_webdav_notes_get_objects_sync (webdav, resources_hash, *out_created_objects, 
cancellable, &local_error);
+               success = success && ecb_webdav_notes_get_objects_sync (webdav, resources_hash, 
*out_modified_objects, cancellable, &local_error);
+       }
+
+       if (local_error) {
+               ecb_webdav_notes_check_credentials_error (cbnotes, webdav, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       g_slist_free_full (resources, e_webdav_resource_free);
+       g_hash_table_destroy (resources_hash);
+       g_clear_object (&webdav);
+
+       return success;
+}
+
+static gboolean
+ecb_webdav_notes_list_existing_sync (ECalMetaBackend *meta_backend,
+                                    gchar **out_new_sync_tag,
+                                    GSList **out_existing_objects,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       GSList *resources = NULL;
+       GError *local_error = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+       g_return_val_if_fail (out_existing_objects != NULL, FALSE);
+
+       *out_existing_objects = NULL;
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+       webdav = ecb_webdav_notes_ref_session (cbnotes);
+
+       success = e_webdav_session_list_sync (webdav, NULL, E_WEBDAV_DEPTH_THIS_AND_CHILDREN, 
E_WEBDAV_LIST_ETAG, &resources, cancellable, &local_error);
+
+       if (success) {
+               GSList *link;
+
+               for (link = resources; link; link = g_slist_next (link)) {
+                       EWebDAVResource *resource = link->data;
+
+                       if (resource && resource->kind == E_WEBDAV_RESOURCE_KIND_RESOURCE && resource->href 
&& g_str_has_suffix (resource->href, ".txt")) {
+                               gchar *filename = ecb_webdav_notes_href_to_uid (resource->href);
+
+                               *out_existing_objects = g_slist_prepend (*out_existing_objects,
+                                       e_cal_meta_backend_info_new (filename, resource->etag, NULL, 
resource->href));
+
+                               g_free (filename);
+                       }
+               }
+
+               *out_existing_objects = g_slist_reverse (*out_existing_objects);
+       }
+
+       if (local_error) {
+               ecb_webdav_notes_check_credentials_error (cbnotes, webdav, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       g_slist_free_full (resources, e_webdav_resource_free);
+       g_clear_object (&webdav);
+
+       return success;
+}
+
+static gchar *
+ecb_webdav_notes_uid_to_uri (ECalBackendWebDAVNotes *cbnotes,
+                            const gchar *uid)
+{
+       ESourceWebdav *webdav_extension;
+       SoupURI *soup_uri;
+       gchar *uri, *tmp, *filename, *uid_hash = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (cbnotes), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       webdav_extension = e_source_get_extension (e_backend_get_source (E_BACKEND (cbnotes)), 
E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+       soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+       g_return_val_if_fail (soup_uri != NULL, NULL);
+
+       /* UIDs with forward slashes can cause trouble, because the destination server can
+          consider them as a path delimiter. Double-encode the URL doesn't always work,
+          thus rather cause a mismatch between stored UID and its href on the server. */
+       if (strchr (uid, '/')) {
+               uid_hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, uid, -1);
+
+               if (uid_hash)
+                       uid = uid_hash;
+       }
+
+       filename = soup_uri_encode (uid, NULL);
+
+       if (soup_uri->path) {
+               gchar *slash = strrchr (soup_uri->path, '/');
+
+               if (slash && !slash[1])
+                       *slash = '\0';
+       }
+
+       soup_uri_set_user (soup_uri, NULL);
+       soup_uri_set_password (soup_uri, NULL);
+
+       tmp = g_strconcat (soup_uri->path && *soup_uri->path ? soup_uri->path : "", "/", filename, NULL);
+       soup_uri_set_path (soup_uri, tmp);
+       g_free (tmp);
+
+       uri = soup_uri_to_string (soup_uri, FALSE);
+
+       soup_uri_free (soup_uri);
+       g_free (filename);
+       g_free (uid_hash);
+
+       return uri;
+}
+
+static void
+ecb_webdav_notes_store_component_etag (ICalComponent *icomp,
+                                      const gchar *etag)
+{
+       ICalComponent *subcomp;
+
+       g_return_if_fail (icomp != NULL);
+       g_return_if_fail (etag != NULL);
+
+       e_cal_util_component_set_x_property (icomp, E_WEBDAV_NOTES_X_ETAG, etag);
+
+       for (subcomp = i_cal_component_get_first_component (icomp, I_CAL_ANY_COMPONENT);
+            subcomp;
+            g_object_unref (subcomp), subcomp = i_cal_component_get_next_component (icomp, 
I_CAL_ANY_COMPONENT)) {
+               ICalComponentKind kind = i_cal_component_isa (subcomp);
+
+               if (kind == I_CAL_VJOURNAL_COMPONENT) {
+                       e_cal_util_component_set_x_property (subcomp, E_WEBDAV_NOTES_X_ETAG, etag);
+               }
+       }
+}
+
+static gboolean
+ecb_webdav_notes_load_component_sync (ECalMetaBackend *meta_backend,
+                                     const gchar *uid,
+                                     const gchar *extra,
+                                     ICalComponent **out_component,
+                                     gchar **out_extra,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       EWebDAVResource *use_resource = NULL;
+       gchar *uri = NULL, *href = NULL, *etag = NULL, *bytes = NULL;
+       gsize length = -1;
+       gboolean success = FALSE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_component != NULL, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+
+       /* When called immediately after save and the server didn't change the component,
+          then the 'extra' contains "href" + "\n" + "vCalendar", to avoid unneeded GET
+          from the server. */
+       if (extra && *extra) {
+               const gchar *newline;
+
+               newline = strchr (extra, '\n');
+               if (newline && newline[1] && newline != extra) {
+                       ICalComponent *vcalendar;
+
+                       vcalendar = i_cal_component_new_from_string (newline + 1);
+                       if (vcalendar) {
+                               *out_extra = g_strndup (extra, newline - extra);
+                               *out_component = vcalendar;
+
+                               return TRUE;
+                       }
+               }
+       }
+
+       webdav = ecb_webdav_notes_ref_session (cbnotes);
+
+       if (extra && *extra) {
+               uri = g_strdup (extra);
+
+               success = e_webdav_session_get_data_sync (webdav, uri, &href, &etag, &bytes, &length, 
cancellable, &local_error);
+
+               if (!success) {
+                       g_free (uri);
+                       uri = NULL;
+               }
+       }
+
+       if (!success && cbnotes->priv->etag_supported) {
+               gchar *new_sync_tag = NULL;
+
+               if (ecb_webdav_notes_getetag_sync (webdav, NULL, &new_sync_tag, cancellable, NULL) && 
new_sync_tag) {
+                       gchar *last_sync_tag;
+
+                       last_sync_tag = e_cal_meta_backend_dup_sync_tag (meta_backend);
+
+                       /* The calendar didn't change, thus the component cannot be there */
+                       if (g_strcmp0 (last_sync_tag, new_sync_tag) == 0) {
+                               g_clear_error (&local_error);
+                               g_clear_object (&webdav);
+                               g_free (last_sync_tag);
+                               g_free (new_sync_tag);
+
+                               g_propagate_error (error, ECC_ERROR (E_CAL_CLIENT_ERROR_OBJECT_NOT_FOUND));
+
+                               return FALSE;
+                       }
+
+                       g_free (last_sync_tag);
+               }
+
+               g_free (new_sync_tag);
+       }
+
+       if (!success) {
+               uri = ecb_webdav_notes_uid_to_uri (cbnotes, uid);
+               g_return_val_if_fail (uri != NULL, FALSE);
+
+               g_clear_error (&local_error);
+
+               success = e_webdav_session_get_data_sync (webdav, uri, &href, &etag, &bytes, &length, 
cancellable, &local_error);
+       }
+
+       if (success) {
+               GSList *resources = NULL;
+
+               success = e_webdav_session_list_sync (webdav, href, E_WEBDAV_DEPTH_THIS,
+                       E_WEBDAV_LIST_DISPLAY_NAME | E_WEBDAV_LIST_CREATION_DATE | 
E_WEBDAV_LIST_LAST_MODIFIED,
+                       &resources, cancellable, &local_error);
+
+               if (success) {
+                       GSList *link;
+
+                       for (link = resources; link; link = g_slist_next (link)) {
+                               EWebDAVResource *resource = link->data;
+
+                               if (resource && resource->kind == E_WEBDAV_RESOURCE_KIND_RESOURCE && 
g_strcmp0 (resource->href, href) == 0) {
+                                       use_resource = resource;
+                                       link->data = NULL;
+                                       break;
+                               }
+                       }
+
+                       g_slist_free_full (resources, e_webdav_resource_free);
+
+                       success = use_resource != NULL;
+
+                       if (!success)
+                               local_error = ECC_ERROR (E_CAL_CLIENT_ERROR_OBJECT_NOT_FOUND);
+               }
+       }
+
+       if (success) {
+               *out_component = NULL;
+
+               if (href && etag && length != ((gsize) -1)) {
+                       ICalComponent *icomp;
+                       gchar *filename = ecb_webdav_notes_href_to_uid (use_resource->href);
+
+                       icomp = ecb_webdav_notes_new_icomp (use_resource->creation_date,
+                               use_resource->last_modified,
+                               filename,
+                               etag,
+                               use_resource->display_name,
+                               bytes);
+
+                       g_free (filename);
+
+                       if (icomp) {
+                               ecb_webdav_notes_store_component_etag (icomp, etag);
+
+                               *out_component = icomp;
+                       }
+               }
+
+               if (!*out_component) {
+                       success = FALSE;
+
+                       if (!href)
+                               g_propagate_error (&local_error, ECC_ERROR_EX 
(E_CAL_CLIENT_ERROR_INVALID_OBJECT, _("Server didn’t return object’s href")));
+                       else if (!etag)
+                               g_propagate_error (&local_error, ECC_ERROR_EX 
(E_CAL_CLIENT_ERROR_INVALID_OBJECT, _("Server didn’t return object’s ETag")));
+                       else
+                               g_propagate_error (&local_error, ECC_ERROR 
(E_CAL_CLIENT_ERROR_INVALID_OBJECT));
+               } else if (out_extra) {
+                       *out_extra = g_strdup (href);
+               }
+       }
+
+       e_webdav_resource_free (use_resource);
+       g_free (uri);
+       g_free (href);
+       g_free (etag);
+       g_free (bytes);
+
+       if (local_error) {
+               ecb_webdav_notes_check_credentials_error (cbnotes, webdav, local_error);
+
+               if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+                       local_error->domain = E_CAL_CLIENT_ERROR;
+                       local_error->code = E_CAL_CLIENT_ERROR_OBJECT_NOT_FOUND;
+               }
+
+               g_propagate_error (error, local_error);
+       }
+
+       g_clear_object (&webdav);
+
+       return success;
+}
+
+/* A rough code to mimic what Nextcloud does, it's not accurate, but it's close
+   enough to work similarly. It skips leading whitespaces and uses up to the first
+   100 letters of the first non-empty line as the base file name. */
+static gchar *
+ecb_webdav_notes_construct_base_filename (const gchar *description)
+{
+       GString *base_filename;
+       gunichar uchr;
+       gboolean add_space = FALSE;
+
+       if (!description || !*description || !g_utf8_validate (description, -1, NULL))
+               return g_strdup (_("New note"));
+
+       base_filename = g_string_sized_new (102);
+
+       while (uchr = g_utf8_get_char (description), g_unichar_isspace (uchr))
+               description = g_utf8_next_char (description);
+
+       while (uchr = g_utf8_get_char (description), uchr && uchr != '\r' && uchr != '\n') {
+               if (g_unichar_isspace (uchr)) {
+                       add_space = TRUE;
+               } else if ((uchr >> 8) != 0 || !strchr ("\"/\\?:*|", (uchr & 0xFF))) {
+                       if (base_filename->len >= 98)
+                               break;
+
+                       if (add_space) {
+                               g_string_append_c (base_filename, ' ');
+                               add_space = FALSE;
+                       }
+
+                       g_string_append_unichar (base_filename, uchr);
+
+                       if (base_filename->len >= 100)
+                               break;
+               }
+
+               description = g_utf8_next_char (description);
+       }
+
+       if (!base_filename->len)
+               g_string_append (base_filename, _("New note"));
+
+       return g_string_free (base_filename, FALSE);
+}
+
+static gboolean
+ecb_webdav_notes_save_component_sync (ECalMetaBackend *meta_backend,
+                                     gboolean overwrite_existing,
+                                     EConflictResolution conflict_resolution,
+                                     const GSList *instances,
+                                     const gchar *extra,
+                                     guint32 opflags,
+                                     gchar **out_new_uid,
+                                     gchar **out_new_extra,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       ICalComponent *icomp;
+       gchar *href = NULL, *etag = NULL;
+       const gchar *description = NULL, *uid = NULL;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+       g_return_val_if_fail (instances != NULL, FALSE);
+       g_return_val_if_fail (instances->data != NULL, FALSE);
+       g_return_val_if_fail (instances->next == NULL, FALSE);
+       g_return_val_if_fail (out_new_uid, FALSE);
+       g_return_val_if_fail (out_new_extra, FALSE);
+
+       icomp = e_cal_component_get_icalcomponent (instances->data);
+       g_return_val_if_fail (i_cal_component_isa (icomp) == I_CAL_VJOURNAL_COMPONENT, FALSE);
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+
+       description = i_cal_component_get_description (icomp);
+       etag = e_cal_util_component_dup_x_property (icomp, E_WEBDAV_NOTES_X_ETAG);
+       uid = i_cal_component_get_uid (icomp);
+
+       webdav = ecb_webdav_notes_ref_session (cbnotes);
+
+       if (uid && (!overwrite_existing || (extra && *extra))) {
+               gchar *new_extra = NULL, *new_etag = NULL;
+               gchar *base_filename, *expected_filename;
+               gboolean force_write = FALSE, new_filename = FALSE;
+               guint counter = 1;
+
+               base_filename = ecb_webdav_notes_construct_base_filename (description);
+               expected_filename = g_strconcat (base_filename, ".txt", NULL);
+
+               if (overwrite_existing) {
+                       switch (conflict_resolution) {
+                       case E_CONFLICT_RESOLUTION_FAIL:
+                       case E_CONFLICT_RESOLUTION_USE_NEWER:
+                       case E_CONFLICT_RESOLUTION_KEEP_SERVER:
+                       case E_CONFLICT_RESOLUTION_WRITE_COPY:
+                               break;
+                       case E_CONFLICT_RESOLUTION_KEEP_LOCAL:
+                               force_write = TRUE;
+                               break;
+                       }
+               }
+
+               new_filename = g_strcmp0 (expected_filename, uid) != 0;
+
+               /* Checkw whether the saved file on the server is already with the "(nnn)" suffix */
+               if (new_filename && expected_filename && uid &&
+                   g_str_has_suffix (uid, ").txt") &&
+                   g_ascii_strncasecmp (uid, expected_filename, strlen (expected_filename) - 4 /* strlen 
(".txt") */) == 0) {
+                       gint ii = strlen (expected_filename) - 4;
+
+                       if (uid[ii] == ' ' && uid[ii + 1] == '(') {
+                               ii += 2;
+
+                               while (uid[ii] >= '0' && uid[ii] <= '9')
+                                       ii++;
+
+                               if (g_strcmp0 (uid + ii, ").txt") == 0)
+                                       new_filename = FALSE;
+                       }
+               }
+
+               if (!extra || !*extra || new_filename) {
+                       href = ecb_webdav_notes_uid_to_uri (cbnotes, expected_filename);
+                       uid = expected_filename;
+                       force_write = FALSE;
+               }
+
+               do {
+                       if (!counter)
+                               break;
+
+                       g_clear_error (&local_error);
+
+                       if (counter > 1) {
+                               g_free (expected_filename);
+                               expected_filename = g_strdup_printf ("%s (%u).txt", base_filename, counter);
+
+                               g_free (href);
+                               href = ecb_webdav_notes_uid_to_uri (cbnotes, expected_filename);
+
+                               uid = expected_filename;
+                       }
+
+                       success = e_webdav_session_put_data_sync (webdav, (!new_filename && extra && *extra) 
? extra : href,
+                               force_write ? "" : (overwrite_existing && !new_filename) ? etag : NULL, 
"text/plain; charset=\"utf-8\"",
+                               description ? description : "", -1, &new_extra, &new_etag, cancellable, 
&local_error);
+
+                       counter++;
+               } while (!success && new_filename && !g_cancellable_is_cancelled (cancellable) &&
+                        g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_PRECONDITION_FAILED));
+
+               if (success && new_filename && extra && *extra) {
+                       /* The name on the server changed, remove the old file */
+                       e_webdav_session_delete_sync (webdav, extra, E_WEBDAV_DEPTH_THIS, etag, cancellable, 
NULL);
+               }
+
+               if (success) {
+                       /* Only if both are returned and it's not a weak ETag */
+                       if (new_extra && *new_extra && new_etag && *new_etag &&
+                           g_ascii_strncasecmp (new_etag, "W/", 2) != 0) {
+                               ICalComponent *icomp_copy;
+                               gchar *tmp = NULL, *ical_string;
+                               glong now = (glong) time (NULL);
+
+                               if (g_str_has_suffix (uid, ".txt"))
+                                       tmp = g_strndup (uid, strlen (uid) - 4);
+
+                               icomp_copy = ecb_webdav_notes_new_icomp (now, now, uid, new_etag,
+                                       tmp ? tmp : uid, description);
+
+                               g_free (tmp);
+
+                               ical_string = i_cal_component_as_ical_string (icomp_copy);
+
+                               /* Encodes the href and the component into one string, which
+                                  will be decoded in the load function */
+                               tmp = g_strconcat (new_extra, "\n", ical_string, NULL);
+                               g_free (new_extra);
+                               new_extra = tmp;
+
+                               g_object_unref (icomp_copy);
+                               g_free (ical_string);
+                       }
+
+                       /* To read the component back, either from the new_extra
+                          or from the server, because the server could change it */
+                       *out_new_uid = g_strdup (uid);
+
+                       if (out_new_extra)
+                               *out_new_extra = new_extra;
+                       else
+                               g_free (new_extra);
+               }
+
+               g_free (base_filename);
+               g_free (expected_filename);
+               g_free (new_etag);
+       } else if (uid) {
+               success = FALSE;
+               g_propagate_error (error, ECC_ERROR_EX (E_CAL_CLIENT_ERROR_INVALID_OBJECT, _("Missing 
information about component URL, local cache is possibly incomplete or broken. Remove it, please.")));
+       } else {
+               success = FALSE;
+               g_propagate_error (error, ECC_ERROR (E_CAL_CLIENT_ERROR_INVALID_OBJECT));
+       }
+
+       g_free (href);
+       g_free (etag);
+
+       if (local_error) {
+               ecb_webdav_notes_check_credentials_error (cbnotes, webdav, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       g_clear_object (&webdav);
+
+       return success;
+}
+
+static gboolean
+ecb_webdav_notes_remove_component_sync (ECalMetaBackend *meta_backend,
+                                       EConflictResolution conflict_resolution,
+                                       const gchar *uid,
+                                       const gchar *extra,
+                                       const gchar *object,
+                                       guint32 opflags,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       ICalComponent *icomp;
+       gchar *etag = NULL;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+
+       if (!extra || !*extra) {
+               g_propagate_error (error, ECC_ERROR (E_CAL_CLIENT_ERROR_INVALID_OBJECT));
+               return FALSE;
+       }
+
+       icomp = i_cal_component_new_from_string (object);
+       if (!icomp) {
+               g_propagate_error (error, ECC_ERROR (E_CAL_CLIENT_ERROR_INVALID_OBJECT));
+               return FALSE;
+       }
+
+       if (conflict_resolution == E_CONFLICT_RESOLUTION_FAIL)
+               etag = e_cal_util_component_dup_x_property (icomp, E_WEBDAV_NOTES_X_ETAG);
+
+       webdav = ecb_webdav_notes_ref_session (cbnotes);
+
+       success = e_webdav_session_delete_sync (webdav, extra,
+               NULL, etag, cancellable, &local_error);
+
+       g_object_unref (icomp);
+       g_free (etag);
+
+       /* Ignore not found errors, this was a delete and the resource is gone.
+          It can be that it had been deleted on the server by other application. */
+       if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+               g_clear_error (&local_error);
+               success = TRUE;
+       }
+
+       if (local_error) {
+               ecb_webdav_notes_check_credentials_error (cbnotes, webdav, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       g_clear_object (&webdav);
+
+       return success;
+}
+
+static gboolean
+ecb_webdav_notes_get_ssl_error_details (ECalMetaBackend *meta_backend,
+                                       gchar **out_certificate_pem,
+                                       GTlsCertificateFlags *out_certificate_errors)
+{
+       ECalBackendWebDAVNotes *cbnotes;
+       EWebDAVSession *webdav;
+       gboolean res;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (meta_backend), FALSE);
+
+       cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (meta_backend);
+       webdav = ecb_webdav_notes_ref_session (cbnotes);
+
+       if (!webdav)
+               return FALSE;
+
+       res = e_soup_session_get_ssl_error_details (E_SOUP_SESSION (webdav), out_certificate_pem, 
out_certificate_errors);
+
+       g_clear_object (&webdav);
+
+       return res;
+}
+
+static gchar *
+ecb_webdav_notes_get_usermail (ECalBackendWebDAVNotes *cbnotes)
+{
+       ESource *source;
+       ESourceAuthentication *auth_extension;
+       ESourceWebdav *webdav_extension;
+       const gchar *extension_name;
+       gchar *usermail;
+       gchar *username;
+       gchar *res = NULL;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (cbnotes), NULL);
+
+       source = e_backend_get_source (E_BACKEND (cbnotes));
+
+       extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+       webdav_extension = e_source_get_extension (source, extension_name);
+
+       /* This will never return an empty string. */
+       usermail = e_source_webdav_dup_email_address (webdav_extension);
+
+       if (usermail)
+               return usermail;
+
+       extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
+       auth_extension = e_source_get_extension (source, extension_name);
+       username = e_source_authentication_dup_user (auth_extension);
+
+       if (username && strchr (username, '@') && strrchr (username, '.') > strchr (username, '@')) {
+               res = username;
+               username = NULL;
+       }
+
+       g_free (username);
+
+       return res;
+}
+
+static gchar *
+ecb_webdav_notes_get_backend_property (ECalBackend *backend,
+                                      const gchar *prop_name)
+{
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strjoin (",",
+                       e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (backend)),
+                       E_CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED,
+                       E_CAL_STATIC_CAPABILITY_SIMPLE_MEMO,
+                       NULL);
+       } else if (g_str_equal (prop_name, E_CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
+                  g_str_equal (prop_name, E_CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
+               return ecb_webdav_notes_get_usermail (E_CAL_BACKEND_WEBDAV_NOTES (backend));
+       }
+
+       /* Chain up to parent's method. */
+       return E_CAL_BACKEND_CLASS (e_cal_backend_webdav_notes_parent_class)->impl_get_backend_property 
(backend, prop_name);
+}
+
+static void
+ecb_webdav_notes_notify_property_changed_cb (GObject *object,
+                                            GParamSpec *param,
+                                            gpointer user_data)
+{
+       ECalBackendWebDAVNotes *cbnotes = user_data;
+       ECalBackend *cal_backend;
+       gboolean email_address_changed;
+       gchar *value;
+
+       g_return_if_fail (E_IS_CAL_BACKEND_WEBDAV_NOTES (cbnotes));
+
+       cal_backend = E_CAL_BACKEND (cbnotes);
+
+       email_address_changed = param && g_strcmp0 (param->name, "email-address") == 0;
+
+       if (email_address_changed) {
+               value = ecb_webdav_notes_get_backend_property (cal_backend, 
E_CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS);
+               e_cal_backend_notify_property_changed (cal_backend, E_CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS, 
value);
+               e_cal_backend_notify_property_changed (cal_backend, 
E_CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS, value);
+               g_free (value);
+       }
+}
+
+static gchar *
+ecb_webdav_notes_dup_component_revision_cb (ECalCache *cal_cache,
+                                           ICalComponent *icomp)
+{
+       g_return_val_if_fail (icomp != NULL, NULL);
+
+       return e_cal_util_component_dup_x_property (icomp, E_WEBDAV_NOTES_X_ETAG);
+}
+
+static void
+e_cal_backend_webdav_notes_constructed (GObject *object)
+{
+       ECalBackendWebDAVNotes *cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (object);
+       ECalCache *cal_cache;
+       ESource *source;
+       ESourceWebdav *webdav_extension;
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_webdav_notes_parent_class)->constructed (object);
+
+       cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbnotes));
+
+       g_signal_connect (cal_cache, "dup-component-revision",
+               G_CALLBACK (ecb_webdav_notes_dup_component_revision_cb), NULL);
+
+       g_clear_object (&cal_cache);
+
+       source = e_backend_get_source (E_BACKEND (cbnotes));
+       webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+
+       g_signal_connect_object (webdav_extension, "notify::email-address",
+               G_CALLBACK (ecb_webdav_notes_notify_property_changed_cb), cbnotes, 0);
+}
+
+static void
+e_cal_backend_webdav_notes_dispose (GObject *object)
+{
+       ECalBackendWebDAVNotes *cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (object);
+
+       g_mutex_lock (&cbnotes->priv->webdav_lock);
+       g_clear_object (&cbnotes->priv->webdav);
+       g_mutex_unlock (&cbnotes->priv->webdav_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_webdav_notes_parent_class)->dispose (object);
+}
+
+static void
+e_cal_backend_webdav_notes_finalize (GObject *object)
+{
+       ECalBackendWebDAVNotes *cbnotes = E_CAL_BACKEND_WEBDAV_NOTES (object);
+
+       g_mutex_clear (&cbnotes->priv->webdav_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_webdav_notes_parent_class)->finalize (object);
+}
+
+static void
+e_cal_backend_webdav_notes_init (ECalBackendWebDAVNotes *cbnotes)
+{
+       cbnotes->priv = e_cal_backend_webdav_notes_get_instance_private (cbnotes);
+
+       g_mutex_init (&cbnotes->priv->webdav_lock);
+}
+
+static void
+e_cal_backend_webdav_notes_class_init (ECalBackendWebDAVNotesClass *klass)
+{
+       GObjectClass *object_class;
+       ECalBackendClass *cal_backend_class;
+       ECalMetaBackendClass *cal_meta_backend_class;
+
+       cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+       cal_meta_backend_class->connect_sync = ecb_webdav_notes_connect_sync;
+       cal_meta_backend_class->disconnect_sync = ecb_webdav_notes_disconnect_sync;
+       cal_meta_backend_class->get_changes_sync = ecb_webdav_notes_get_changes_sync;
+       cal_meta_backend_class->list_existing_sync = ecb_webdav_notes_list_existing_sync;
+       cal_meta_backend_class->load_component_sync = ecb_webdav_notes_load_component_sync;
+       cal_meta_backend_class->save_component_sync = ecb_webdav_notes_save_component_sync;
+       cal_meta_backend_class->remove_component_sync = ecb_webdav_notes_remove_component_sync;
+       cal_meta_backend_class->get_ssl_error_details = ecb_webdav_notes_get_ssl_error_details;
+
+       cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+       cal_backend_class->impl_get_backend_property = ecb_webdav_notes_get_backend_property;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_cal_backend_webdav_notes_constructed;
+       object_class->dispose = e_cal_backend_webdav_notes_dispose;
+       object_class->finalize = e_cal_backend_webdav_notes_finalize;
+}
diff --git a/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.h 
b/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.h
new file mode 100644
index 000000000..814acb281
--- /dev/null
+++ b/src/calendar/backends/webdav-notes/e-cal-backend-webdav-notes.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 Red Hat (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef E_CAL_BACKEND_WEBDAV_NOTES_H
+#define E_CAL_BACKEND_WEBDAV_NOTES_H
+
+#include <libedata-cal/libedata-cal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CAL_BACKEND_WEBDAV_NOTES \
+       (e_cal_backend_webdav_notes_get_type ())
+#define E_CAL_BACKEND_WEBDAV_NOTES(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CAL_BACKEND_WEBDAV_NOTES, ECalBackendWebDAVNotes))
+#define E_CAL_BACKEND_WEBDAV_NOTES_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_CAL_BACKEND_WEBDAV_NOTES, ECalBackendWebDAVNotesClass))
+#define E_IS_CAL_BACKEND_WEBDAV_NOTES(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CAL_BACKEND_WEBDAV_NOTES))
+#define E_IS_CAL_BACKEND_WEBDAV_NOTES_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_CAL_BACKEND_WEBDAV_NOTES))
+#define E_CAL_BACKEND_WEBDAV_NOTES_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_CAL_BACKEND_WEBDAV_NOTES, ECalBackendWebDAVNotesClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ECalBackendWebDAVNotes ECalBackendWebDAVNotes;
+typedef struct _ECalBackendWebDAVNotesClass ECalBackendWebDAVNotesClass;
+typedef struct _ECalBackendWebDAVNotesPrivate ECalBackendWebDAVNotesPrivate;
+
+struct _ECalBackendWebDAVNotes {
+       ECalMetaBackend parent;
+       ECalBackendWebDAVNotesPrivate *priv;
+};
+
+struct _ECalBackendWebDAVNotesClass {
+       ECalMetaBackendClass parent_class;
+};
+
+GType          e_cal_backend_webdav_notes_get_type     (void);
+
+G_END_DECLS
+
+#endif /* E_CAL_BACKEND_WEBDAV_NOTES_H */
diff --git a/src/calendar/libecal/e-cal-util.h b/src/calendar/libecal/e-cal-util.h
index e9f66f1d2..6fcb2172c 100644
--- a/src/calendar/libecal/e-cal-util.h
+++ b/src/calendar/libecal/e-cal-util.h
@@ -199,6 +199,17 @@ G_BEGIN_DECLS
  **/
 #define E_CAL_STATIC_CAPABILITY_TASK_HANDLE_RECUR      "task-handle-recur"
 
+/**
+ * E_CAL_STATIC_CAPABILITY_SIMPLE_MEMO:
+ *
+ * When the capability is set, the backend handles only simple memos,
+ * which means it stores only memo description. The summary can be changed
+ * by the backend, if needed.
+ *
+ * Since: 3.38
+ **/
+#define E_CAL_STATIC_CAPABILITY_SIMPLE_MEMO            "simple-memo"
+
 struct _ECalClient;
 
 ICalComponent *        e_cal_util_new_top_level        (void);
diff --git a/src/libebackend/e-webdav-collection-backend.c b/src/libebackend/e-webdav-collection-backend.c
index 19bfc03e1..9fad3b253 100644
--- a/src/libebackend/e-webdav-collection-backend.c
+++ b/src/libebackend/e-webdav-collection-backend.c
@@ -129,6 +129,11 @@ webdav_collection_add_found_source (ECollectionBackend *collection,
                provider = "caldav";
                identity_prefix = "tasks";
                break;
+       case E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES:
+               backend_name = E_SOURCE_EXTENSION_MEMO_LIST;
+               provider = "webdav-notes";
+               identity_prefix = "notes";
+               break;
        default:
                g_warn_if_reached ();
                return;
@@ -547,13 +552,15 @@ e_webdav_collection_backend_discover_sync (EWebDAVCollectionBackend *webdav_back
        if (e_source_collection_get_calendar_enabled (collection_extension) && calendar_url &&
            !g_cancellable_is_cancelled (cancellable) &&
            e_webdav_discover_sources_full_sync (source, calendar_url,
-               E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | 
E_WEBDAV_DISCOVER_SUPPORTS_TASKS | E_WEBDAV_DISCOVER_SUPPORTS_CALENDAR_AUTO_SCHEDULE,
+               E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | 
E_WEBDAV_DISCOVER_SUPPORTS_TASKS |
+               E_WEBDAV_DISCOVER_SUPPORTS_CALENDAR_AUTO_SCHEDULE | E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES,
                credentials, (EWebDAVDiscoverRefSourceFunc) e_source_registry_server_ref_source, server,
                out_certificate_pem, out_certificate_errors, &discovered_sources, NULL, cancellable, 
&local_error)) {
                EWebDAVDiscoverSupports source_types[] = {
                        E_WEBDAV_DISCOVER_SUPPORTS_EVENTS,
                        E_WEBDAV_DISCOVER_SUPPORTS_MEMOS,
-                       E_WEBDAV_DISCOVER_SUPPORTS_TASKS
+                       E_WEBDAV_DISCOVER_SUPPORTS_TASKS,
+                       E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES
                };
 
                webdav_collection_process_discovered_sources (collection, discovered_sources, known_sources, 
source_types, G_N_ELEMENTS (source_types));
diff --git a/src/libedataserver/e-webdav-discover.c b/src/libedataserver/e-webdav-discover.c
index e6d1faaa9..204923f3a 100644
--- a/src/libedataserver/e-webdav-discover.c
+++ b/src/libedataserver/e-webdav-discover.c
@@ -71,7 +71,8 @@ e_webdav_discover_split_resources (WebDAVDiscoverData *wdd,
                if (resource && (
                    resource->kind == E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK ||
                    resource->kind == E_WEBDAV_RESOURCE_KIND_CALENDAR ||
-                   resource->kind == E_WEBDAV_RESOURCE_KIND_SUBSCRIBED_ICALENDAR)) {
+                   resource->kind == E_WEBDAV_RESOURCE_KIND_SUBSCRIBED_ICALENDAR ||
+                   resource->kind == E_WEBDAV_RESOURCE_KIND_WEBDAV_NOTES)) {
                        EWebDAVDiscoveredSource *discovered;
 
                        if ((wdd->only_supports & (~CUSTOM_SUPPORTS_FLAGS)) != 
E_WEBDAV_DISCOVER_SUPPORTS_NONE &&
@@ -297,6 +298,27 @@ e_webdav_discover_traverse_propfind_response_cb (EWebDAVSession *webdav,
                        else
                                g_clear_error (&local_error);
                }
+
+               if (((wdd->only_supports & (~CUSTOM_SUPPORTS_FLAGS)) == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+                   (wdd->only_supports & E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES) != 0) &&
+                   (g_str_has_suffix (href, "/Notes") ||
+                   g_str_has_suffix (href, "/Notes/")) &&
+                   !e_webdav_discovery_already_discovered (href, wdd->calendars) &&
+                   e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/D:collection", xpath_prop_prefix)) 
{
+                       GSList *resources = NULL;
+
+                       resources = g_slist_prepend (NULL,
+                               e_webdav_resource_new (E_WEBDAV_RESOURCE_KIND_WEBDAV_NOTES,
+                                       E_WEBDAV_RESOURCE_SUPPORTS_WEBDAV_NOTES, href, NULL,
+                                       _("Notes"),
+                                       NULL, 0, 0, 0, NULL, NULL));
+
+                       e_webdav_discover_split_resources (wdd, resources);
+
+                       g_slist_free_full (resources, e_webdav_resource_free);
+
+                       g_hash_table_insert (wdd->covered_hrefs, g_strdup (href), GINT_TO_POINTER (2));
+               }
        }
 
        return TRUE;
@@ -919,6 +941,38 @@ e_webdav_discover_sources_full_sync (ESource *source,
                        wdd.error = NULL;
                }
 
+               if (!g_cancellable_is_cancelled (cancellable) &&
+                   ((only_supports & (~CUSTOM_SUPPORTS_FLAGS)) == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+                   (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES)) != 0) &&
+                   (!soup_uri_get_path (soup_uri) || !strstr (soup_uri_get_path (soup_uri), 
"/.well-known/"))) {
+                       gchar *saved_path;
+                       GError *local_error_2nd = NULL;
+
+                       saved_path = g_strdup (soup_uri_get_path (soup_uri));
+
+                       soup_uri_set_path (soup_uri, "/.well-known/webdav/Notes/");
+
+                       uri = soup_uri_to_string (soup_uri, FALSE);
+
+                       wdd.error = &local_error_2nd;
+                       wdd.only_supports = E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES;
+                       g_hash_table_remove_all (wdd.covered_hrefs);
+
+                       success = (uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri, 
FALSE)) || success;
+
+                       g_free (uri);
+
+                       soup_uri_set_path (soup_uri, saved_path);
+                       g_free (saved_path);
+
+                       if (e_webdav_discover_maybe_replace_auth_error (&local_error, &local_error_2nd))
+                               success = FALSE;
+
+                       g_clear_error (&local_error_2nd);
+
+                       wdd.error = NULL;
+               }
+
                if (!g_cancellable_is_cancelled (cancellable) && !wdd.addressbooks &&
                    ((only_supports & (~CUSTOM_SUPPORTS_FLAGS)) == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
                    (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0) &&
diff --git a/src/libedataserver/e-webdav-discover.h b/src/libedataserver/e-webdav-discover.h
index 623df891b..a7d4cb22b 100644
--- a/src/libedataserver/e-webdav-discover.h
+++ b/src/libedataserver/e-webdav-discover.h
@@ -35,6 +35,7 @@ typedef enum {
        E_WEBDAV_DISCOVER_SUPPORTS_EVENTS                 = E_WEBDAV_RESOURCE_SUPPORTS_EVENTS,
        E_WEBDAV_DISCOVER_SUPPORTS_MEMOS                  = E_WEBDAV_RESOURCE_SUPPORTS_MEMOS,
        E_WEBDAV_DISCOVER_SUPPORTS_TASKS                  = E_WEBDAV_RESOURCE_SUPPORTS_TASKS,
+       E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES           = E_WEBDAV_RESOURCE_SUPPORTS_WEBDAV_NOTES,
        E_WEBDAV_DISCOVER_SUPPORTS_CALENDAR_AUTO_SCHEDULE = E_WEBDAV_RESOURCE_SUPPORTS_LAST << 1,
        E_WEBDAV_DISCOVER_SUPPORTS_SUBSCRIBED_ICALENDAR   = E_WEBDAV_DISCOVER_SUPPORTS_CALENDAR_AUTO_SCHEDULE 
<< 1
 } EWebDAVDiscoverSupports;
diff --git a/src/libedataserver/e-webdav-session.c b/src/libedataserver/e-webdav-session.c
index 1117e451c..d71f6ccc8 100644
--- a/src/libedataserver/e-webdav-session.c
+++ b/src/libedataserver/e-webdav-session.c
@@ -860,7 +860,8 @@ e_webdav_session_replace_with_detailed_error_internal (EWebDAVSession *webdav,
                                                       gboolean ignore_multistatus,
                                                       const gchar *prefix,
                                                       GError **inout_error,
-                                                      gboolean can_change_last_dav_error_code)
+                                                      gboolean can_change_last_dav_error_code,
+                                                      gboolean skip_text_on_success)
 {
        SoupMessage *message;
        GByteArray byte_array = { 0 };
@@ -912,7 +913,7 @@ e_webdav_session_replace_with_detailed_error_internal (EWebDAVSession *webdav,
        }
 
        content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
-       if (content_type && (
+       if (content_type && (!skip_text_on_success || (status_code && !SOUP_STATUS_IS_SUCCESSFUL 
(status_code))) && (
            (g_ascii_strcasecmp (content_type, "application/xml") == 0 ||
             g_ascii_strcasecmp (content_type, "text/xml") == 0))) {
                xmlDocPtr doc;
@@ -969,10 +970,10 @@ e_webdav_session_replace_with_detailed_error_internal (EWebDAVSession *webdav,
                                xmlXPathFreeContext (xpath_ctx);
                        xmlFreeDoc (doc);
                }
-       } else if (content_type &&
+       } else if (content_type && (!skip_text_on_success || (status_code && !SOUP_STATUS_IS_SUCCESSFUL 
(status_code))) &&
             g_ascii_strcasecmp (content_type, "text/plain") == 0) {
                detail_text = g_strndup ((const gchar *) byte_array.data, byte_array.len);
-       } else if (content_type &&
+       } else if (content_type && (!skip_text_on_success || (status_code && !SOUP_STATUS_IS_SUCCESSFUL 
(status_code))) &&
             g_ascii_strcasecmp (content_type, "text/html") == 0) {
                SoupURI *soup_uri;
                gchar *uri = NULL;
@@ -1088,7 +1089,7 @@ e_webdav_session_replace_with_detailed_error (EWebDAVSession *webdav,
                                              const gchar *prefix,
                                              GError **inout_error)
 {
-       return e_webdav_session_replace_with_detailed_error_internal (webdav, request, response_data, 
ignore_multistatus, prefix, inout_error, FALSE);
+       return e_webdav_session_replace_with_detailed_error_internal (webdav, request, response_data, 
ignore_multistatus, prefix, inout_error, FALSE, FALSE);
 }
 
 /**
@@ -1335,7 +1336,7 @@ e_webdav_session_post_with_content_type_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to post data"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to post data"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (success) {
@@ -1483,7 +1484,7 @@ e_webdav_session_propfind_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to get properties"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to get properties"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (success)
@@ -1559,7 +1560,7 @@ e_webdav_session_proppatch_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to update properties"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to update properties"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -1665,7 +1666,7 @@ e_webdav_session_report_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to issue REPORT"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to issue REPORT"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (success && func && message->status_code == SOUP_STATUS_MULTI_STATUS)
@@ -1726,7 +1727,7 @@ e_webdav_session_mkcol_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to create collection"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to create collection"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -1831,7 +1832,7 @@ e_webdav_session_mkcol_addressbook_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to create address book"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to create address book"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -1993,7 +1994,7 @@ e_webdav_session_mkcalendar_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to create calendar"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to create calendar"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -2127,7 +2128,7 @@ e_webdav_session_get_sync (EWebDAVSession *webdav,
                                tmp_bytes.data = buffer;
                                tmp_bytes.len = nread;
 
-                               success = !e_webdav_session_replace_with_detailed_error_internal (webdav, 
request, &tmp_bytes, FALSE, _("Failed to read resource"), error, TRUE);
+                               success = !e_webdav_session_replace_with_detailed_error_internal (webdav, 
request, &tmp_bytes, FALSE, _("Failed to read resource"), error, TRUE, TRUE);
                                if (!success)
                                        break;
                        }
@@ -2138,7 +2139,7 @@ e_webdav_session_get_sync (EWebDAVSession *webdav,
                }
 
                if (success && first_chunk) {
-                       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, 
NULL, FALSE, _("Failed to read resource"), error, TRUE);
+                       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, 
NULL, FALSE, _("Failed to read resource"), error, TRUE, TRUE);
                } else if (success && !first_chunk && log_level == SOUP_LOGGER_LOG_BODY) {
                        fprintf (stdout, "\n");
                        fflush (stdout);
@@ -2452,7 +2453,7 @@ e_webdav_session_put_sync (EWebDAVSession *webdav,
        g_signal_handler_disconnect (message, wrote_headers_id);
        g_signal_handler_disconnect (message, wrote_chunk_id);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to put data"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to put data"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (cwd.wrote_any && cwd.log_level == SOUP_LOGGER_LOG_BODY) {
@@ -2598,7 +2599,7 @@ e_webdav_session_put_data_sync (EWebDAVSession *webdav,
 
        ret_bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, ret_bytes, FALSE, 
_("Failed to put data"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, ret_bytes, FALSE, 
_("Failed to put data"), error, TRUE, FALSE) &&
                ret_bytes != NULL;
 
        if (success) {
@@ -2700,7 +2701,7 @@ e_webdav_session_delete_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to delete resource"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to delete resource"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -2769,7 +2770,7 @@ e_webdav_session_copy_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to copy resource"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to copy resource"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -2833,7 +2834,7 @@ e_webdav_session_move_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to move resource"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to move resource"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -2940,7 +2941,7 @@ e_webdav_session_lock_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to lock resource"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to lock resource"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (success && out_xml_response) {
@@ -3050,7 +3051,7 @@ e_webdav_session_refresh_lock_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to refresh lock"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to refresh lock"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -3111,7 +3112,7 @@ e_webdav_session_unlock_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to unlock"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, FALSE, 
_("Failed to unlock"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
@@ -4227,7 +4228,7 @@ e_webdav_session_acl_sync (EWebDAVSession *webdav,
 
        bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable, 
error);
 
-       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to get access control list"), error, TRUE) &&
+       success = !e_webdav_session_replace_with_detailed_error_internal (webdav, request, bytes, TRUE, 
_("Failed to get access control list"), error, TRUE, FALSE) &&
                bytes != NULL;
 
        if (bytes)
diff --git a/src/libedataserver/e-webdav-session.h b/src/libedataserver/e-webdav-session.h
index 0345e437e..418d30c0c 100644
--- a/src/libedataserver/e-webdav-session.h
+++ b/src/libedataserver/e-webdav-session.h
@@ -88,7 +88,8 @@ typedef enum {
        E_WEBDAV_RESOURCE_KIND_PRINCIPAL,
        E_WEBDAV_RESOURCE_KIND_COLLECTION,
        E_WEBDAV_RESOURCE_KIND_RESOURCE,
-       E_WEBDAV_RESOURCE_KIND_SUBSCRIBED_ICALENDAR
+       E_WEBDAV_RESOURCE_KIND_SUBSCRIBED_ICALENDAR,
+       E_WEBDAV_RESOURCE_KIND_WEBDAV_NOTES
 } EWebDAVResourceKind;
 
 typedef enum {
@@ -99,7 +100,8 @@ typedef enum {
        E_WEBDAV_RESOURCE_SUPPORTS_TASKS        = 1 << 3,
        E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY     = 1 << 4,
        E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE     = 1 << 5,
-       E_WEBDAV_RESOURCE_SUPPORTS_LAST         = E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE
+       E_WEBDAV_RESOURCE_SUPPORTS_WEBDAV_NOTES = 1 << 6,
+       E_WEBDAV_RESOURCE_SUPPORTS_LAST         = E_WEBDAV_RESOURCE_SUPPORTS_WEBDAV_NOTES
 } EWebDAVResourceSupports;
 
 typedef struct _EWebDAVResource {
diff --git a/src/libedataserverui/e-webdav-discover-widget.c b/src/libedataserverui/e-webdav-discover-widget.c
index 916cd6d68..fbe2f6260 100644
--- a/src/libedataserverui/e-webdav-discover-widget.c
+++ b/src/libedataserverui/e-webdav-discover-widget.c
@@ -523,7 +523,7 @@ e_webdav_discover_content_fill_discovered_sources (GtkTreeView *tree_view,
 
                addbit (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS, C_("WebDAVDiscover", "Contacts"));
                addbit (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS, C_("WebDAVDiscover", "Events"));
-               addbit (E_WEBDAV_DISCOVER_SUPPORTS_MEMOS, C_("WebDAVDiscover", "Memos"));
+               addbit (E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | E_WEBDAV_DISCOVER_SUPPORTS_WEBDAV_NOTES, 
C_("WebDAVDiscover", "Memos"));
                addbit (E_WEBDAV_DISCOVER_SUPPORTS_TASKS, C_("WebDAVDiscover", "Tasks"));
 
                #undef addbit
diff --git a/src/services/evolution-source-registry/CMakeLists.txt 
b/src/services/evolution-source-registry/CMakeLists.txt
index a6462a4d0..1dfd24c50 100644
--- a/src/services/evolution-source-registry/CMakeLists.txt
+++ b/src/services/evolution-source-registry/CMakeLists.txt
@@ -30,6 +30,7 @@ set(builtin_sources_files
        local-stub.source
        weather-stub.source
        webcal-stub.source
+       webdav-notes-stub.source
        birthdays.source
        local.source
        sendmail.source
diff --git a/src/services/evolution-source-registry/builtin/webdav-notes-stub.source.in 
b/src/services/evolution-source-registry/builtin/webdav-notes-stub.source.in
new file mode 100644
index 000000000..01a3b0135
--- /dev/null
+++ b/src/services/evolution-source-registry/builtin/webdav-notes-stub.source.in
@@ -0,0 +1,5 @@
+
+[Data Source]
+_DisplayName=WebDAV Notes
+Enabled=true
+Parent=
diff --git a/src/services/evolution-source-registry/evolution-source-registry-resource.xml 
b/src/services/evolution-source-registry/evolution-source-registry-resource.xml
index 828dd2a64..409aa7062 100644
--- a/src/services/evolution-source-registry/evolution-source-registry-resource.xml
+++ b/src/services/evolution-source-registry/evolution-source-registry-resource.xml
@@ -9,6 +9,7 @@
     <file>local-stub.source</file>
     <file>weather-stub.source</file>
     <file>webcal-stub.source</file>
+    <file>webdav-notes-stub.source</file>
   </gresource>
   <gresource prefix="/org/gnome/evolution-data-server/rw-sources">
     <file>birthdays.source</file>


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