[evolution-ews] Add (disabled by default) Microsoft 365 (Graph API) connector



commit 73f722fffe7c932a040ca053c4299bc572018d4c
Author: Milan Crha <mcrha redhat com>
Date:   Wed Aug 5 08:44:31 2020 +0200

    Add (disabled by default) Microsoft 365 (Graph API) connector
    
    To enable the connector users need to export ENABLE_M365=1 environment
    variable in a way that it's known both to the Evolution itself and
    to the evolution-data-server background services. One such place can
    be in /etc/environment.
    
    It's disabled by default, because the Microsoft Graph API is currently
    far from being feature complete with the EWS API, thus it's here only
    as kind of a preview.

 CMakeLists.txt                                     |   25 +
 config.h.in                                        |   12 +
 po/POTFILES.in                                     |   75 +-
 src/CMakeLists.txt                                 |   66 +-
 src/EWS/CMakeLists.txt                             |   64 +
 src/{ => EWS}/addressbook/CMakeLists.txt           |    4 +-
 .../addressbook/e-book-backend-ews-factory.c       |    4 +-
 src/{ => EWS}/addressbook/e-book-backend-ews.c     |   14 +-
 src/{ => EWS}/addressbook/e-book-backend-ews.h     |    0
 src/{ => EWS}/addressbook/ews-oab-decoder.c        |    0
 src/{ => EWS}/addressbook/ews-oab-decoder.h        |    0
 src/{ => EWS}/addressbook/ews-oab-decompress.c     |    0
 src/{ => EWS}/addressbook/ews-oab-decompress.h     |    0
 src/{ => EWS}/addressbook/ews-oab-props.h          |    0
 .../addressbook/gal-lzx-decompress-test.c          |    0
 src/{ => EWS}/addressbook/mspack/README.mspack     |    0
 src/{ => EWS}/addressbook/mspack/lzx.h             |    0
 src/{ => EWS}/addressbook/mspack/lzxd.c            |    0
 src/{ => EWS}/addressbook/mspack/oab-decompress.c  |    0
 src/{ => EWS}/addressbook/mspack/readbits.h        |    0
 src/{ => EWS}/addressbook/mspack/readhuff.h        |    0
 src/{ => EWS}/addressbook/oab-decode-test.c        |    0
 src/{ => EWS}/calendar/CMakeLists.txt              |    4 +-
 src/{ => EWS}/calendar/e-cal-backend-ews-factory.c |    4 +-
 src/{ => EWS}/calendar/e-cal-backend-ews-utils.c   |    8 +-
 src/{ => EWS}/calendar/e-cal-backend-ews-utils.h   |    4 +-
 src/{ => EWS}/calendar/e-cal-backend-ews.c         |    8 +-
 src/{ => EWS}/calendar/e-cal-backend-ews.h         |    2 +-
 src/{ => EWS}/calendar/windowsZones.xml            |    0
 src/{ => EWS}/camel/CMakeLists.txt                 |    8 +-
 src/{ => EWS}/camel/camel-ews-enums.h              |    0
 src/{ => EWS}/camel/camel-ews-folder.c             |   10 +-
 src/{ => EWS}/camel/camel-ews-folder.h             |    0
 src/{ => EWS}/camel/camel-ews-message-info.c       |    0
 src/{ => EWS}/camel/camel-ews-message-info.h       |    0
 src/{ => EWS}/camel/camel-ews-private.h            |    0
 src/{ => EWS}/camel/camel-ews-provider.c           |    2 +-
 src/{ => EWS}/camel/camel-ews-search.c             |    2 +-
 src/{ => EWS}/camel/camel-ews-search.h             |    0
 src/{ => EWS}/camel/camel-ews-store-summary.c      |    2 +-
 src/{ => EWS}/camel/camel-ews-store-summary.h      |    2 +-
 src/{ => EWS}/camel/camel-ews-store.c              |    8 +-
 src/{ => EWS}/camel/camel-ews-store.h              |    2 +-
 src/{ => EWS}/camel/camel-ews-summary.c            |    0
 src/{ => EWS}/camel/camel-ews-summary.h            |    0
 src/{ => EWS}/camel/camel-ews-transport.c          |    4 +-
 src/{ => EWS}/camel/camel-ews-transport.h          |    0
 src/{ => EWS}/camel/camel-ews-utils.c              |    8 +-
 src/{ => EWS}/camel/camel-ews-utils.h              |    2 +-
 src/{ => EWS}/camel/libcamelews.urls               |    0
 src/{server => EWS/common}/CMakeLists.txt          |    4 +-
 src/{server => EWS/common}/camel-ews-settings.c    |    0
 src/{server => EWS/common}/camel-ews-settings.h    |    0
 .../common}/camel-sasl-xoauth2-office365.c         |    0
 .../common}/camel-sasl-xoauth2-office365.h         |    0
 src/{server => EWS/common}/e-ews-calendar-utils.c  |    0
 src/{server => EWS/common}/e-ews-calendar-utils.h  |    4 +-
 src/{server => EWS/common}/e-ews-camel-common.c    |    4 +-
 src/{server => EWS/common}/e-ews-camel-common.h    |    4 +-
 .../common}/e-ews-connection-utils.c               |    0
 .../common}/e-ews-connection-utils.h               |    2 +-
 src/{server => EWS/common}/e-ews-connection.c      |    0
 src/{server => EWS/common}/e-ews-connection.h      |    0
 src/{server => EWS/common}/e-ews-debug.c           |    0
 src/{server => EWS/common}/e-ews-debug.h           |    2 +-
 src/{server => EWS/common}/e-ews-enums.h           |    0
 src/{server => EWS/common}/e-ews-folder.c          |    0
 src/{server => EWS/common}/e-ews-folder.h          |    0
 src/{server => EWS/common}/e-ews-item-change.c     |    0
 src/{server => EWS/common}/e-ews-item-change.h     |    0
 src/{server => EWS/common}/e-ews-item.c            |    0
 src/{server => EWS/common}/e-ews-item.h            |    0
 src/{server => EWS/common}/e-ews-message.c         |    0
 src/{server => EWS/common}/e-ews-message.h         |    0
 src/{server => EWS/common}/e-ews-notification.c    |    0
 src/{server => EWS/common}/e-ews-notification.h    |    0
 src/{server => EWS/common}/e-ews-oof-settings.c    |    0
 src/{server => EWS/common}/e-ews-oof-settings.h    |    4 +-
 .../common}/e-ews-query-to-restriction.c           |    2 +-
 .../common}/e-ews-query-to-restriction.h           |    4 +-
 .../common}/e-oauth2-service-office365.c           |    2 +-
 .../common}/e-oauth2-service-office365.h           |    0
 src/{server => EWS/common}/e-soap-message.c        |    0
 src/{server => EWS/common}/e-soap-message.h        |    0
 src/{server => EWS/common}/e-soap-response.c       |    0
 src/{server => EWS/common}/e-soap-response.h       |    0
 src/{server => EWS/common}/e-soup-auth-negotiate.c |    0
 src/{server => EWS/common}/e-soup-auth-negotiate.h |    0
 src/{server => EWS/common}/e-source-ews-folder.c   |    0
 src/{server => EWS/common}/e-source-ews-folder.h   |    2 +-
 src/{server => EWS/common}/ews-errors.c            |    0
 src/{server => EWS/common}/ews-errors.h            |    2 +-
 .../evolution}/CMakeLists.txt                      |    0
 .../evolution}/e-book-config-ews.c                 |    4 +-
 .../evolution}/e-book-config-ews.h                 |    0
 .../evolution}/e-cal-config-ews.c                  |    0
 .../evolution}/e-cal-config-ews.h                  |    0
 .../evolution}/e-ews-config-lookup.c               |    4 +-
 .../evolution}/e-ews-config-lookup.h               |    0
 .../evolution}/e-ews-config-ui-extension.c         |    0
 .../evolution}/e-ews-config-ui-extension.h         |    0
 .../evolution}/e-ews-config-utils.c                |    6 +-
 .../evolution}/e-ews-config-utils.h                |    2 +-
 .../evolution}/e-ews-edit-folder-permissions.c     |    0
 .../evolution}/e-ews-edit-folder-permissions.h     |    6 +-
 .../evolution}/e-ews-ooo-notificator.c             |    2 +-
 .../evolution}/e-ews-ooo-notificator.h             |    0
 .../evolution}/e-ews-photo-source.c                |    4 +-
 .../evolution}/e-ews-photo-source.h                |    0
 .../evolution}/e-ews-search-user.c                 |    0
 .../evolution}/e-ews-search-user.h                 |    4 +-
 .../evolution}/e-ews-subscribe-foreign-folder.c    |    2 +-
 .../evolution}/e-ews-subscribe-foreign-folder.h    |    0
 .../evolution}/e-mail-config-ews-autodiscover.c    |    4 +-
 .../evolution}/e-mail-config-ews-autodiscover.h    |    0
 .../evolution}/e-mail-config-ews-backend.c         |    2 +-
 .../evolution}/e-mail-config-ews-backend.h         |    0
 .../evolution}/e-mail-config-ews-delegates-page.c  |    8 +-
 .../evolution}/e-mail-config-ews-delegates-page.h  |    0
 .../e-mail-config-ews-folder-sizes-page.c          |    0
 .../e-mail-config-ews-folder-sizes-page.h          |    0
 .../evolution}/e-mail-config-ews-gal.c             |    2 +-
 .../evolution}/e-mail-config-ews-gal.h             |    0
 .../evolution}/e-mail-config-ews-notebook.c        |    0
 .../evolution}/e-mail-config-ews-notebook.h        |    0
 .../evolution}/e-mail-config-ews-oal-combo-box.c   |    4 +-
 .../evolution}/e-mail-config-ews-oal-combo-box.h   |    0
 .../evolution}/e-mail-config-ews-offline-options.c |    0
 .../evolution}/e-mail-config-ews-offline-options.h |    0
 .../evolution}/e-mail-config-ews-ooo-page.c        |    8 +-
 .../evolution}/e-mail-config-ews-ooo-page.h        |    0
 .../evolution}/module-ews-configuration.c          |    6 +-
 .../evolution}/module-ews-configuration.error.xml  |    0
 src/{collection => EWS/registry}/CMakeLists.txt    |    0
 .../registry}/e-ews-backend-factory.c              |    0
 .../registry}/e-ews-backend-factory.h              |    0
 src/{collection => EWS/registry}/e-ews-backend.c   |    4 +-
 src/{collection => EWS/registry}/e-ews-backend.h   |    2 +-
 .../registry}/module-ews-backend.c                 |    4 +-
 src/Microsoft365/CMakeLists.txt                    |   64 +
 src/Microsoft365/addressbook/CMakeLists.txt        |   65 +
 .../addressbook/e-book-backend-m365-factory.c      |   74 +
 src/Microsoft365/addressbook/e-book-backend-m365.c | 1933 +++++++
 src/Microsoft365/addressbook/e-book-backend-m365.h |   32 +
 src/Microsoft365/calendar/CMakeLists.txt           |   61 +
 .../calendar/e-cal-backend-m365-factory.c          |  139 +
 src/Microsoft365/calendar/e-cal-backend-m365.c     | 3710 ++++++++++++
 src/Microsoft365/calendar/e-cal-backend-m365.h     |   37 +
 src/Microsoft365/camel/CMakeLists.txt              |   75 +
 src/Microsoft365/camel/camel-m365-folder-summary.c |  306 +
 src/Microsoft365/camel/camel-m365-folder-summary.h |   72 +
 src/Microsoft365/camel/camel-m365-folder.c         | 1847 ++++++
 src/Microsoft365/camel/camel-m365-folder.h         |   60 +
 src/Microsoft365/camel/camel-m365-message-info.c   |  409 ++
 src/Microsoft365/camel/camel-m365-message-info.h   |   70 +
 src/Microsoft365/camel/camel-m365-provider.c       |  122 +
 src/Microsoft365/camel/camel-m365-store-summary.c  | 1439 +++++
 src/Microsoft365/camel/camel-m365-store-summary.h  |  189 +
 src/Microsoft365/camel/camel-m365-store.c          | 1831 ++++++
 src/Microsoft365/camel/camel-m365-store.h          |   70 +
 src/Microsoft365/camel/camel-m365-transport.c      |  400 ++
 src/Microsoft365/camel/camel-m365-transport.h      |   50 +
 src/Microsoft365/camel/camel-m365-utils.c          | 1004 ++++
 src/Microsoft365/camel/camel-m365-utils.h          |   48 +
 src/Microsoft365/camel/libcamelmicrosoft365.urls   |    1 +
 src/Microsoft365/common/CMakeLists.txt             |   72 +
 src/Microsoft365/common/camel-m365-settings.c      |  938 +++
 src/Microsoft365/common/camel-m365-settings.h      |  126 +
 .../common/camel-sasl-xoauth2-microsoft365.c       |   45 +
 .../common/camel-sasl-xoauth2-microsoft365.h       |   54 +
 src/Microsoft365/common/e-m365-connection.c        | 6007 ++++++++++++++++++++
 src/Microsoft365/common/e-m365-connection.h        |  758 +++
 src/Microsoft365/common/e-m365-enums.h             |   26 +
 src/Microsoft365/common/e-m365-json-utils.c        | 3889 +++++++++++++
 src/Microsoft365/common/e-m365-json-utils.h        | 1056 ++++
 src/Microsoft365/common/e-m365-tz-utils.c          |  184 +
 src/Microsoft365/common/e-m365-tz-utils.h          |   21 +
 .../common/e-oauth2-service-microsoft365.c         |  374 ++
 .../common/e-oauth2-service-microsoft365.h         |   54 +
 src/Microsoft365/common/e-source-m365-folder.c     |  287 +
 src/Microsoft365/common/e-source-m365-folder.h     |   70 +
 src/Microsoft365/evolution/CMakeLists.txt          |   39 +
 src/Microsoft365/evolution/e-book-config-m365.c    |   66 +
 src/Microsoft365/evolution/e-book-config-m365.h    |   51 +
 src/Microsoft365/evolution/e-cal-config-m365.c     |   84 +
 src/Microsoft365/evolution/e-cal-config-m365.h     |   51 +
 .../evolution/e-mail-config-m365-backend.c         |  492 ++
 .../evolution/e-mail-config-m365-backend.h         |   53 +
 .../evolution/module-m365-configuration.c          |   41 +
 src/Microsoft365/registry/CMakeLists.txt           |   24 +
 src/Microsoft365/registry/e-m365-backend-factory.c |   78 +
 src/Microsoft365/registry/e-m365-backend-factory.h |   52 +
 src/Microsoft365/registry/e-m365-backend.c         |  955 ++++
 src/Microsoft365/registry/e-m365-backend.h         |   51 +
 src/Microsoft365/registry/e-source-m365-deltas.c   |  155 +
 src/Microsoft365/registry/e-source-m365-deltas.h   |   61 +
 src/Microsoft365/registry/module-m365-backend.c    |   41 +
 tests/CMakeLists.txt                               |    4 +-
 tests/ews-test-camel.c                             |    6 +-
 tests/ews-test-common.h                            |    4 +-
 tests/ews-test-timezones.c                         |    4 +-
 201 files changed, 30626 insertions(+), 213 deletions(-)
---
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fc2571a7..2791a9c8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -46,6 +46,7 @@ set(eds_minimum_version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJ
 set(evo_minimum_version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH})
 set(mspack_minimum_version 0.4)
 set(libical_glib_minimum_version 3.0.5)
+set(json_glib_minimum_version 1.0.4)
 
 # Load modules from the source tree
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
@@ -142,6 +143,8 @@ pkg_check_modules(SOUP REQUIRED libsoup-2.4>=${soup_minimum_version})
 
 pkg_check_modules(LIBICAL_GLIB REQUIRED libical-glib>=${libical_glib_minimum_version})
 
+pkg_check_modules(JSON_GLIB REQUIRED json-glib-1.0>=${json_glib_minimum_version})
+
 # ******************************
 # libmspack with OAB support
 # ******************************
@@ -197,6 +200,28 @@ if(WITH_OFFICE365_REDIRECT_URI STREQUAL "")
        set(WITH_OFFICE365_REDIRECT_URI "https://login.microsoftonline.com/common/oauth2/nativeclient";)
 endif(WITH_OFFICE365_REDIRECT_URI STREQUAL "")
 
+# ******************************
+# Microsoft365 OAuth2 support (v2.0)
+# ******************************
+
+add_printable_variable(WITH_MICROSOFT365_TENANT "Microsoft 365 OAuth 2.0 tenant" "")
+
+if(WITH_MICROSOFT365_TENANT STREQUAL "")
+       set(WITH_MICROSOFT365_TENANT "common")
+endif(WITH_MICROSOFT365_TENANT STREQUAL "")
+
+add_printable_variable(WITH_MICROSOFT365_CLIENT_ID "Microsoft 365 OAuth 2.0 client ID" "")
+
+if(WITH_MICROSOFT365_CLIENT_ID STREQUAL "")
+       set(WITH_MICROSOFT365_CLIENT_ID "20460e5d-ce91-49af-a3a5-70b6be7486d1")
+endif(WITH_MICROSOFT365_CLIENT_ID STREQUAL "")
+
+add_printable_variable(WITH_MICROSOFT365_REDIRECT_URI "Microsoft 365 OAuth 2.0 redirect URI" "")
+
+if(WITH_MICROSOFT365_REDIRECT_URI STREQUAL "")
+       set(WITH_MICROSOFT365_REDIRECT_URI "https://login.microsoftonline.com/common/oauth2/nativeclient";)
+endif(WITH_MICROSOFT365_REDIRECT_URI STREQUAL "")
+
 # ******************************
 # Special directories
 # ******************************
diff --git a/config.h.in b/config.h.in
index ad3f60c7..ae8ae20e 100644
--- a/config.h.in
+++ b/config.h.in
@@ -12,6 +12,9 @@
 /* Package name for gettext */
 #define GETTEXT_PACKAGE "@GETTEXT_PACKAGE@"
 
+/* Configured with enabled maintainer mode */
+#cmakedefine ENABLE_MAINTAINER_MODE 1
+
 /* Define to add support for low level tests */
 #cmakedefine ENABLE_TESTS 1
 
@@ -29,3 +32,12 @@
 
 /* Define Office365 OAuth 2.0 default Redirect URI to use */
 #define OFFICE365_REDIRECT_URI "@WITH_OFFICE365_REDIRECT_URI@"
+
+/* Define Microsoft 365 OAuth 2.0 default Tenant to use */
+#define MICROSOFT365_TENANT "@WITH_MICROSOFT365_TENANT@"
+
+/* Define Microsoft 365 OAuth 2.0 default Client ID to use */
+#define MICROSOFT365_CLIENT_ID "@WITH_MICROSOFT365_CLIENT_ID@"
+
+/* Define Microsoft 365 OAuth 2.0 default Redirect URI to use */
+#define MICROSOFT365_REDIRECT_URI "@WITH_MICROSOFT365_REDIRECT_URI@"
diff --git a/po/POTFILES.in b/po/POTFILES.in
index fd153dd1..2a89548f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -2,35 +2,46 @@
 # Please keep this list in alphabetic order.
 [encoding: UTF-8]
 org.gnome.Evolution-ews.metainfo.xml.in
-src/addressbook/e-book-backend-ews.c
-src/calendar/e-cal-backend-ews.c
-src/calendar/e-cal-backend-ews-utils.c
-src/camel/camel-ews-folder.c
-src/camel/camel-ews-provider.c
-src/camel/camel-ews-store.c
-src/camel/camel-ews-store.h
-src/camel/camel-ews-transport.c
-src/camel/camel-ews-utils.c
-src/collection/e-ews-backend.c
-src/collection/module-ews-backend.c
-src/configuration/e-book-config-ews.c
-src/configuration/e-ews-config-lookup.c
-src/configuration/e-ews-config-utils.c
-src/configuration/e-ews-edit-folder-permissions.c
-src/configuration/e-ews-ooo-notificator.c
-src/configuration/e-ews-search-user.c
-src/configuration/e-ews-subscribe-foreign-folder.c
-src/configuration/e-mail-config-ews-autodiscover.c
-src/configuration/e-mail-config-ews-backend.c
-src/configuration/e-mail-config-ews-delegates-page.c
-src/configuration/e-mail-config-ews-folder-sizes-page.c
-src/configuration/e-mail-config-ews-gal.c
-src/configuration/e-mail-config-ews-ooo-page.c
-src/configuration/module-ews-configuration.error.xml
-src/server/camel-sasl-xoauth2-office365.c
-src/server/e-ews-calendar-utils.c
-src/server/e-ews-camel-common.c
-src/server/e-ews-connection.c
-src/server/e-ews-connection-utils.c
-src/server/e-ews-folder.c
-src/server/e-oauth2-service-office365.c
+src/EWS/addressbook/e-book-backend-ews.c
+src/EWS/calendar/e-cal-backend-ews.c
+src/EWS/calendar/e-cal-backend-ews-utils.c
+src/EWS/camel/camel-ews-folder.c
+src/EWS/camel/camel-ews-provider.c
+src/EWS/camel/camel-ews-store.c
+src/EWS/camel/camel-ews-store.h
+src/EWS/camel/camel-ews-transport.c
+src/EWS/camel/camel-ews-utils.c
+src/EWS/common/camel-sasl-xoauth2-office365.c
+src/EWS/common/e-ews-calendar-utils.c
+src/EWS/common/e-ews-camel-common.c
+src/EWS/common/e-ews-connection.c
+src/EWS/common/e-ews-connection-utils.c
+src/EWS/common/e-ews-folder.c
+src/EWS/common/e-oauth2-service-office365.c
+src/EWS/evolution/e-book-config-ews.c
+src/EWS/evolution/e-ews-config-lookup.c
+src/EWS/evolution/e-ews-config-utils.c
+src/EWS/evolution/e-ews-edit-folder-permissions.c
+src/EWS/evolution/e-ews-ooo-notificator.c
+src/EWS/evolution/e-ews-search-user.c
+src/EWS/evolution/e-ews-subscribe-foreign-folder.c
+src/EWS/evolution/e-mail-config-ews-autodiscover.c
+src/EWS/evolution/e-mail-config-ews-backend.c
+src/EWS/evolution/e-mail-config-ews-delegates-page.c
+src/EWS/evolution/e-mail-config-ews-folder-sizes-page.c
+src/EWS/evolution/e-mail-config-ews-gal.c
+src/EWS/evolution/e-mail-config-ews-ooo-page.c
+src/EWS/evolution/module-ews-configuration.error.xml
+src/EWS/registry/e-ews-backend.c
+src/EWS/registry/module-ews-backend.c
+src/Microsoft365/addressbook/e-book-backend-m365.c
+src/Microsoft365/calendar/e-cal-backend-m365.c
+src/Microsoft365/camel/camel-m365-folder.c
+src/Microsoft365/camel/camel-m365-provider.c
+src/Microsoft365/camel/camel-m365-store.c
+src/Microsoft365/camel/camel-m365-transport.c
+src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.c
+src/Microsoft365/common/e-oauth2-service-microsoft365.c
+src/Microsoft365/common/e-m365-connection.c
+src/Microsoft365/evolution/e-mail-config-m365-backend.c
+src/Microsoft365/registry/e-m365-backend.c
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5b514ee3..32df2977 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,64 +1,2 @@
-macro(add_simple_module _name _sourcesvar _depsvar _defsvar _cflagsvar _incdirsvar _ldflagsvar _destination)
-       set(DEPENDENCIES
-               evolution-ews
-       )
-
-       set(SOURCES
-               ${${_sourcesvar}}
-       )
-
-       add_library(${_name} MODULE
-               ${SOURCES}
-       )
-
-       set_target_properties(${_name} PROPERTIES
-               PREFIX ""
-       )
-
-       add_dependencies(${_name}
-               ${DEPENDENCIES}
-               ${${_depsvar}}
-       )
-
-       target_compile_definitions(${_name} PRIVATE
-               -DG_LOG_DOMAIN=\"${_name}\"
-               -DEXCHANGE_EWS_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
-               ${${_defsvar}}
-       )
-
-       target_compile_options(${_name} PUBLIC
-               ${LIBEBACKEND_CFLAGS}
-               ${LIBEDATASERVER_CFLAGS}
-               ${SOUP_CFLAGS}
-               ${${_cflagsvar}}
-       )
-
-       target_include_directories(${_name} PUBLIC
-               ${CMAKE_BINARY_DIR}
-               ${CMAKE_SOURCE_DIR}
-               ${LIBEBACKEND_INCLUDE_DIRS}
-               ${LIBEDATASERVER_INCLUDE_DIRS}
-               ${SOUP_INCLUDE_DIRS}
-               ${${_incdirsvar}}
-       )
-
-       target_link_libraries(${_name}
-               ${DEPENDENCIES}
-               ${${_depsvar}}
-               ${LIBEBACKEND_LDFLAGS}
-               ${LIBEDATASERVER_LDFLAGS}
-               ${SOUP_LDFLAGS}
-               ${${_ldflagsvar}}
-       )
-
-       install(TARGETS ${_name}
-               DESTINATION ${_destination}
-       )
-endmacro(add_simple_module)
-
-add_subdirectory(addressbook)
-add_subdirectory(calendar)
-add_subdirectory(camel)
-add_subdirectory(collection)
-add_subdirectory(configuration)
-add_subdirectory(server)
+add_subdirectory(EWS)
+add_subdirectory(Microsoft365)
diff --git a/src/EWS/CMakeLists.txt b/src/EWS/CMakeLists.txt
new file mode 100644
index 00000000..a2b3b2b1
--- /dev/null
+++ b/src/EWS/CMakeLists.txt
@@ -0,0 +1,64 @@
+macro(add_simple_module _name _sourcesvar _depsvar _defsvar _cflagsvar _incdirsvar _ldflagsvar _destination)
+       set(DEPENDENCIES
+               evolution-ews
+       )
+
+       set(SOURCES
+               ${${_sourcesvar}}
+       )
+
+       add_library(${_name} MODULE
+               ${SOURCES}
+       )
+
+       set_target_properties(${_name} PROPERTIES
+               PREFIX ""
+       )
+
+       add_dependencies(${_name}
+               ${DEPENDENCIES}
+               ${${_depsvar}}
+       )
+
+       target_compile_definitions(${_name} PRIVATE
+               -DG_LOG_DOMAIN=\"${_name}\"
+               -DEXCHANGE_EWS_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+               ${${_defsvar}}
+       )
+
+       target_compile_options(${_name} PUBLIC
+               ${LIBEBACKEND_CFLAGS}
+               ${LIBEDATASERVER_CFLAGS}
+               ${SOUP_CFLAGS}
+               ${${_cflagsvar}}
+       )
+
+       target_include_directories(${_name} PUBLIC
+               ${CMAKE_BINARY_DIR}
+               ${CMAKE_SOURCE_DIR}
+               ${LIBEBACKEND_INCLUDE_DIRS}
+               ${LIBEDATASERVER_INCLUDE_DIRS}
+               ${SOUP_INCLUDE_DIRS}
+               ${${_incdirsvar}}
+       )
+
+       target_link_libraries(${_name}
+               ${DEPENDENCIES}
+               ${${_depsvar}}
+               ${LIBEBACKEND_LDFLAGS}
+               ${LIBEDATASERVER_LDFLAGS}
+               ${SOUP_LDFLAGS}
+               ${${_ldflagsvar}}
+       )
+
+       install(TARGETS ${_name}
+               DESTINATION ${_destination}
+       )
+endmacro(add_simple_module)
+
+add_subdirectory(addressbook)
+add_subdirectory(calendar)
+add_subdirectory(camel)
+add_subdirectory(common)
+add_subdirectory(evolution)
+add_subdirectory(registry)
diff --git a/src/addressbook/CMakeLists.txt b/src/EWS/addressbook/CMakeLists.txt
similarity index 98%
rename from src/addressbook/CMakeLists.txt
rename to src/EWS/addressbook/CMakeLists.txt
index f2b40098..7b5e5298 100644
--- a/src/addressbook/CMakeLists.txt
+++ b/src/EWS/addressbook/CMakeLists.txt
@@ -53,8 +53,8 @@ target_compile_options(ebookbackendews PUBLIC
 target_include_directories(ebookbackendews PUBLIC
        ${CMAKE_BINARY_DIR}
        ${CMAKE_SOURCE_DIR}
-       ${CMAKE_BINARY_DIR}/src
-       ${CMAKE_SOURCE_DIR}/src
+       ${CMAKE_BINARY_DIR}/src/EWS
+       ${CMAKE_SOURCE_DIR}/src/EWS
        ${CMAKE_CURRENT_BINARY_DIR}
        ${CMAKE_CURRENT_SOURCE_DIR}
        ${LIBEBACKEND_INCLUDE_DIRS}
diff --git a/src/addressbook/e-book-backend-ews-factory.c b/src/EWS/addressbook/e-book-backend-ews-factory.c
similarity index 95%
rename from src/addressbook/e-book-backend-ews-factory.c
rename to src/EWS/addressbook/e-book-backend-ews-factory.c
index 28197533..7b0a4d1d 100644
--- a/src/addressbook/e-book-backend-ews-factory.c
+++ b/src/EWS/addressbook/e-book-backend-ews-factory.c
@@ -11,8 +11,8 @@
 
 #include <libedata-book/libedata-book.h>
 
-#include "server/e-oauth2-service-office365.h"
-#include "server/e-source-ews-folder.h"
+#include "common/e-oauth2-service-office365.h"
+#include "common/e-source-ews-folder.h"
 
 #include "e-book-backend-ews.h"
 
diff --git a/src/addressbook/e-book-backend-ews.c b/src/EWS/addressbook/e-book-backend-ews.c
similarity index 99%
rename from src/addressbook/e-book-backend-ews.c
rename to src/EWS/addressbook/e-book-backend-ews.c
index 893461dc..1f9f2fb0 100644
--- a/src/addressbook/e-book-backend-ews.c
+++ b/src/EWS/addressbook/e-book-backend-ews.c
@@ -27,13 +27,13 @@
 
 #include <libedata-book/libedata-book.h>
 
-#include "server/e-ews-item-change.h"
-#include "server/e-ews-message.h"
-#include "server/e-ews-connection.h"
-#include "server/e-ews-connection-utils.h"
-#include "server/e-ews-item.h"
-#include "server/e-ews-query-to-restriction.h"
-#include "server/e-source-ews-folder.h"
+#include "common/e-ews-item-change.h"
+#include "common/e-ews-message.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-connection-utils.h"
+#include "common/e-ews-item.h"
+#include "common/e-ews-query-to-restriction.h"
+#include "common/e-source-ews-folder.h"
 
 #include "e-book-backend-ews.h"
 #include "ews-oab-decoder.h"
diff --git a/src/addressbook/e-book-backend-ews.h b/src/EWS/addressbook/e-book-backend-ews.h
similarity index 100%
rename from src/addressbook/e-book-backend-ews.h
rename to src/EWS/addressbook/e-book-backend-ews.h
diff --git a/src/addressbook/ews-oab-decoder.c b/src/EWS/addressbook/ews-oab-decoder.c
similarity index 100%
rename from src/addressbook/ews-oab-decoder.c
rename to src/EWS/addressbook/ews-oab-decoder.c
diff --git a/src/addressbook/ews-oab-decoder.h b/src/EWS/addressbook/ews-oab-decoder.h
similarity index 100%
rename from src/addressbook/ews-oab-decoder.h
rename to src/EWS/addressbook/ews-oab-decoder.h
diff --git a/src/addressbook/ews-oab-decompress.c b/src/EWS/addressbook/ews-oab-decompress.c
similarity index 100%
rename from src/addressbook/ews-oab-decompress.c
rename to src/EWS/addressbook/ews-oab-decompress.c
diff --git a/src/addressbook/ews-oab-decompress.h b/src/EWS/addressbook/ews-oab-decompress.h
similarity index 100%
rename from src/addressbook/ews-oab-decompress.h
rename to src/EWS/addressbook/ews-oab-decompress.h
diff --git a/src/addressbook/ews-oab-props.h b/src/EWS/addressbook/ews-oab-props.h
similarity index 100%
rename from src/addressbook/ews-oab-props.h
rename to src/EWS/addressbook/ews-oab-props.h
diff --git a/src/addressbook/gal-lzx-decompress-test.c b/src/EWS/addressbook/gal-lzx-decompress-test.c
similarity index 100%
rename from src/addressbook/gal-lzx-decompress-test.c
rename to src/EWS/addressbook/gal-lzx-decompress-test.c
diff --git a/src/addressbook/mspack/README.mspack b/src/EWS/addressbook/mspack/README.mspack
similarity index 100%
rename from src/addressbook/mspack/README.mspack
rename to src/EWS/addressbook/mspack/README.mspack
diff --git a/src/addressbook/mspack/lzx.h b/src/EWS/addressbook/mspack/lzx.h
similarity index 100%
rename from src/addressbook/mspack/lzx.h
rename to src/EWS/addressbook/mspack/lzx.h
diff --git a/src/addressbook/mspack/lzxd.c b/src/EWS/addressbook/mspack/lzxd.c
similarity index 100%
rename from src/addressbook/mspack/lzxd.c
rename to src/EWS/addressbook/mspack/lzxd.c
diff --git a/src/addressbook/mspack/oab-decompress.c b/src/EWS/addressbook/mspack/oab-decompress.c
similarity index 100%
rename from src/addressbook/mspack/oab-decompress.c
rename to src/EWS/addressbook/mspack/oab-decompress.c
diff --git a/src/addressbook/mspack/readbits.h b/src/EWS/addressbook/mspack/readbits.h
similarity index 100%
rename from src/addressbook/mspack/readbits.h
rename to src/EWS/addressbook/mspack/readbits.h
diff --git a/src/addressbook/mspack/readhuff.h b/src/EWS/addressbook/mspack/readhuff.h
similarity index 100%
rename from src/addressbook/mspack/readhuff.h
rename to src/EWS/addressbook/mspack/readhuff.h
diff --git a/src/addressbook/oab-decode-test.c b/src/EWS/addressbook/oab-decode-test.c
similarity index 100%
rename from src/addressbook/oab-decode-test.c
rename to src/EWS/addressbook/oab-decode-test.c
diff --git a/src/calendar/CMakeLists.txt b/src/EWS/calendar/CMakeLists.txt
similarity index 95%
rename from src/calendar/CMakeLists.txt
rename to src/EWS/calendar/CMakeLists.txt
index 4492b211..3a6ce029 100644
--- a/src/calendar/CMakeLists.txt
+++ b/src/EWS/calendar/CMakeLists.txt
@@ -41,8 +41,8 @@ target_compile_options(ecalbackendews PUBLIC
 target_include_directories(ecalbackendews PUBLIC
        ${CMAKE_BINARY_DIR}
        ${CMAKE_SOURCE_DIR}
-       ${CMAKE_BINARY_DIR}/src
-       ${CMAKE_SOURCE_DIR}/src
+       ${CMAKE_BINARY_DIR}/src/EWS
+       ${CMAKE_SOURCE_DIR}/src/EWS
        ${CMAKE_CURRENT_BINARY_DIR}
        ${CAMEL_INCLUDE_DIRS}
        ${EVOLUTION_CALENDAR_INCLUDE_DIRS}
diff --git a/src/calendar/e-cal-backend-ews-factory.c b/src/EWS/calendar/e-cal-backend-ews-factory.c
similarity index 97%
rename from src/calendar/e-cal-backend-ews-factory.c
rename to src/EWS/calendar/e-cal-backend-ews-factory.c
index bd489806..820eaf1d 100644
--- a/src/calendar/e-cal-backend-ews-factory.c
+++ b/src/EWS/calendar/e-cal-backend-ews-factory.c
@@ -11,8 +11,8 @@
 
 #include <libedata-cal/libedata-cal.h>
 
-#include "server/e-oauth2-service-office365.h"
-#include "server/e-source-ews-folder.h"
+#include "common/e-oauth2-service-office365.h"
+#include "common/e-source-ews-folder.h"
 
 #include "e-cal-backend-ews.h"
 
diff --git a/src/calendar/e-cal-backend-ews-utils.c b/src/EWS/calendar/e-cal-backend-ews-utils.c
similarity index 99%
rename from src/calendar/e-cal-backend-ews-utils.c
rename to src/EWS/calendar/e-cal-backend-ews-utils.c
index 017f2caf..10cc4f11 100644
--- a/src/calendar/e-cal-backend-ews-utils.c
+++ b/src/EWS/calendar/e-cal-backend-ews-utils.c
@@ -29,10 +29,10 @@
 #include <libecal/libecal.h>
 #include <libsoup/soup-misc.h>
 
-#include "server/e-ews-calendar-utils.h"
-#include "server/e-ews-connection.h"
-#include "server/e-ews-message.h"
-#include "server/e-ews-item-change.h"
+#include "common/e-ews-calendar-utils.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-message.h"
+#include "common/e-ews-item-change.h"
 
 #include "e-cal-backend-ews-utils.h"
 
diff --git a/src/calendar/e-cal-backend-ews-utils.h b/src/EWS/calendar/e-cal-backend-ews-utils.h
similarity index 97%
rename from src/calendar/e-cal-backend-ews-utils.h
rename to src/EWS/calendar/e-cal-backend-ews-utils.h
index c491f258..2f0da704 100644
--- a/src/calendar/e-cal-backend-ews-utils.h
+++ b/src/EWS/calendar/e-cal-backend-ews-utils.h
@@ -9,8 +9,8 @@
 
 #include <libecal/libecal.h>
 
-#include "server/e-ews-connection.h"
-#include "server/e-ews-item-change.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-item-change.h"
 
 #include "e-cal-backend-ews.h"
 
diff --git a/src/calendar/e-cal-backend-ews.c b/src/EWS/calendar/e-cal-backend-ews.c
similarity index 99%
rename from src/calendar/e-cal-backend-ews.c
rename to src/EWS/calendar/e-cal-backend-ews.c
index 2de76fb0..dd7fb647 100644
--- a/src/calendar/e-cal-backend-ews.c
+++ b/src/EWS/calendar/e-cal-backend-ews.c
@@ -24,10 +24,10 @@
 #include <calendar/gui/calendar-config.h>
 #include <calendar/gui/itip-utils.h>
 
-#include "server/e-source-ews-folder.h"
-#include "server/e-ews-calendar-utils.h"
-#include "server/e-ews-connection-utils.h"
-#include "server/e-ews-camel-common.h"
+#include "common/e-source-ews-folder.h"
+#include "common/e-ews-calendar-utils.h"
+#include "common/e-ews-connection-utils.h"
+#include "common/e-ews-camel-common.h"
 
 #include "e-cal-backend-ews.h"
 #include "e-cal-backend-ews-utils.h"
diff --git a/src/calendar/e-cal-backend-ews.h b/src/EWS/calendar/e-cal-backend-ews.h
similarity index 97%
rename from src/calendar/e-cal-backend-ews.h
rename to src/EWS/calendar/e-cal-backend-ews.h
index 498b179f..f7a42296 100644
--- a/src/calendar/e-cal-backend-ews.h
+++ b/src/EWS/calendar/e-cal-backend-ews.h
@@ -9,7 +9,7 @@
 
 #include <libedata-cal/libedata-cal.h>
 
-#include "server/e-ews-connection.h"
+#include "common/e-ews-connection.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/calendar/windowsZones.xml b/src/EWS/calendar/windowsZones.xml
similarity index 100%
rename from src/calendar/windowsZones.xml
rename to src/EWS/calendar/windowsZones.xml
diff --git a/src/camel/CMakeLists.txt b/src/EWS/camel/CMakeLists.txt
similarity index 96%
rename from src/camel/CMakeLists.txt
rename to src/EWS/camel/CMakeLists.txt
index 469eb0f9..bf80fd4b 100644
--- a/src/camel/CMakeLists.txt
+++ b/src/EWS/camel/CMakeLists.txt
@@ -60,8 +60,8 @@ target_compile_options(camelews-priv PUBLIC
 target_include_directories(camelews-priv PUBLIC
        ${CMAKE_BINARY_DIR}
        ${CMAKE_SOURCE_DIR}
-       ${CMAKE_BINARY_DIR}/src
-       ${CMAKE_SOURCE_DIR}/src
+       ${CMAKE_BINARY_DIR}/src/EWS
+       ${CMAKE_SOURCE_DIR}/src/EWS
        ${CMAKE_CURRENT_BINARY_DIR}
        ${CAMEL_INCLUDE_DIRS}
        ${EVOLUTION_SHELL_INCLUDE_DIRS}
@@ -123,8 +123,8 @@ target_compile_options(camelews PUBLIC
 target_include_directories(camelews PUBLIC
        ${CMAKE_BINARY_DIR}
        ${CMAKE_SOURCE_DIR}
-       ${CMAKE_BINARY_DIR}/src
-       ${CMAKE_SOURCE_DIR}/src
+       ${CMAKE_BINARY_DIR}/src/EWS
+       ${CMAKE_SOURCE_DIR}/src/EWS
        ${CMAKE_CURRENT_BINARY_DIR}
        ${CAMEL_INCLUDE_DIRS}
        ${EVOLUTION_SHELL_INCLUDE_DIRS}
diff --git a/src/camel/camel-ews-enums.h b/src/EWS/camel/camel-ews-enums.h
similarity index 100%
rename from src/camel/camel-ews-enums.h
rename to src/EWS/camel/camel-ews-enums.h
diff --git a/src/camel/camel-ews-folder.c b/src/EWS/camel/camel-ews-folder.c
similarity index 99%
rename from src/camel/camel-ews-folder.c
rename to src/EWS/camel/camel-ews-folder.c
index 1c829825..5d6b77fb 100644
--- a/src/camel/camel-ews-folder.c
+++ b/src/EWS/camel/camel-ews-folder.c
@@ -23,11 +23,11 @@ which needs to be better organized via functions */
 #include <glib/gstdio.h>
 #include <libecal/libecal.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-camel-common.h"
-#include "server/e-ews-connection.h"
-#include "server/e-ews-item-change.h"
-#include "server/e-ews-message.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-camel-common.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-item-change.h"
+#include "common/e-ews-message.h"
 
 #include "camel-ews-folder.h"
 #include "camel-ews-private.h"
diff --git a/src/camel/camel-ews-folder.h b/src/EWS/camel/camel-ews-folder.h
similarity index 100%
rename from src/camel/camel-ews-folder.h
rename to src/EWS/camel/camel-ews-folder.h
diff --git a/src/camel/camel-ews-message-info.c b/src/EWS/camel/camel-ews-message-info.c
similarity index 100%
rename from src/camel/camel-ews-message-info.c
rename to src/EWS/camel/camel-ews-message-info.c
diff --git a/src/camel/camel-ews-message-info.h b/src/EWS/camel/camel-ews-message-info.h
similarity index 100%
rename from src/camel/camel-ews-message-info.h
rename to src/EWS/camel/camel-ews-message-info.h
diff --git a/src/camel/camel-ews-private.h b/src/EWS/camel/camel-ews-private.h
similarity index 100%
rename from src/camel/camel-ews-private.h
rename to src/EWS/camel/camel-ews-private.h
diff --git a/src/camel/camel-ews-provider.c b/src/EWS/camel/camel-ews-provider.c
similarity index 99%
rename from src/camel/camel-ews-provider.c
rename to src/EWS/camel/camel-ews-provider.c
index fae6a5b2..1c389dbd 100644
--- a/src/camel/camel-ews-provider.c
+++ b/src/EWS/camel/camel-ews-provider.c
@@ -14,7 +14,7 @@
 #include <glib/gi18n-lib.h>
 #include <gmodule.h>
 
-#include "server/camel-sasl-xoauth2-office365.h"
+#include "common/camel-sasl-xoauth2-office365.h"
 
 #include "camel-ews-store.h"
 #include "camel-ews-transport.h"
diff --git a/src/camel/camel-ews-search.c b/src/EWS/camel/camel-ews-search.c
similarity index 99%
rename from src/camel/camel-ews-search.c
rename to src/EWS/camel/camel-ews-search.c
index 16da4cd3..dae80f77 100644
--- a/src/camel/camel-ews-search.c
+++ b/src/EWS/camel/camel-ews-search.c
@@ -11,7 +11,7 @@
 #include <camel/camel-search-private.h>
 #include <e-util/e-util.h>
 
-#include "server/e-ews-query-to-restriction.h"
+#include "common/e-ews-query-to-restriction.h"
 
 #include "camel-ews-folder.h"
 #include "camel-ews-search.h"
diff --git a/src/camel/camel-ews-search.h b/src/EWS/camel/camel-ews-search.h
similarity index 100%
rename from src/camel/camel-ews-search.h
rename to src/EWS/camel/camel-ews-search.h
diff --git a/src/camel/camel-ews-store-summary.c b/src/EWS/camel/camel-ews-store-summary.c
similarity index 99%
rename from src/camel/camel-ews-store-summary.c
rename to src/EWS/camel/camel-ews-store-summary.c
index 776abf0b..dd4cc55c 100644
--- a/src/camel/camel-ews-store-summary.c
+++ b/src/EWS/camel/camel-ews-store-summary.c
@@ -12,7 +12,7 @@
 #include <string.h>
 #include "camel-ews-store-summary.h"
 
-#include "server/e-ews-folder.h"
+#include "common/e-ews-folder.h"
 
 #define S_LOCK(x) (g_rec_mutex_lock(&(x)->priv->s_lock))
 #define S_UNLOCK(x) (g_rec_mutex_unlock(&(x)->priv->s_lock))
diff --git a/src/camel/camel-ews-store-summary.h b/src/EWS/camel/camel-ews-store-summary.h
similarity index 99%
rename from src/camel/camel-ews-store-summary.h
rename to src/EWS/camel/camel-ews-store-summary.h
index 15c023be..16ab0117 100644
--- a/src/camel/camel-ews-store-summary.h
+++ b/src/EWS/camel/camel-ews-store-summary.h
@@ -9,7 +9,7 @@
 
 #include <camel/camel.h>
 
-#include "server/e-ews-enums.h"
+#include "common/e-ews-enums.h"
 
 /* Standard GObject macros */
 #define CAMEL_TYPE_EWS_STORE_SUMMARY \
diff --git a/src/camel/camel-ews-store.c b/src/EWS/camel/camel-ews-store.c
similarity index 99%
rename from src/camel/camel-ews-store.c
rename to src/EWS/camel/camel-ews-store.c
index 744f8e58..b46e8b99 100644
--- a/src/camel/camel-ews-store.c
+++ b/src/EWS/camel/camel-ews-store.c
@@ -20,10 +20,10 @@
 
 #include <libemail-engine/libemail-engine.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-item-change.h"
-#include "server/e-ews-message.h"
-#include "server/e-ews-oof-settings.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-item-change.h"
+#include "common/e-ews-message.h"
+#include "common/e-ews-oof-settings.h"
 
 #include "camel-ews-folder.h"
 #include "camel-ews-store.h"
diff --git a/src/camel/camel-ews-store.h b/src/EWS/camel/camel-ews-store.h
similarity index 98%
rename from src/camel/camel-ews-store.h
rename to src/EWS/camel/camel-ews-store.h
index e210d3d1..614c8ac2 100644
--- a/src/camel/camel-ews-store.h
+++ b/src/EWS/camel/camel-ews-store.h
@@ -11,7 +11,7 @@
 #include <camel/camel.h>
 #include <camel/camel-ews-enums.h>
 
-#include "server/e-ews-connection.h"
+#include "common/e-ews-connection.h"
 
 #include "camel-ews-store-summary.h"
 
diff --git a/src/camel/camel-ews-summary.c b/src/EWS/camel/camel-ews-summary.c
similarity index 100%
rename from src/camel/camel-ews-summary.c
rename to src/EWS/camel/camel-ews-summary.c
diff --git a/src/camel/camel-ews-summary.h b/src/EWS/camel/camel-ews-summary.h
similarity index 100%
rename from src/camel/camel-ews-summary.h
rename to src/EWS/camel/camel-ews-summary.h
diff --git a/src/camel/camel-ews-transport.c b/src/EWS/camel/camel-ews-transport.c
similarity index 99%
rename from src/camel/camel-ews-transport.c
rename to src/EWS/camel/camel-ews-transport.c
index fbf49618..54f2984d 100644
--- a/src/camel/camel-ews-transport.c
+++ b/src/EWS/camel/camel-ews-transport.c
@@ -14,8 +14,8 @@
 
 #include <libemail-engine/libemail-engine.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-camel-common.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-camel-common.h"
 
 #include "camel-ews-store.h"
 #include "camel-ews-transport.h"
diff --git a/src/camel/camel-ews-transport.h b/src/EWS/camel/camel-ews-transport.h
similarity index 100%
rename from src/camel/camel-ews-transport.h
rename to src/EWS/camel/camel-ews-transport.h
diff --git a/src/camel/camel-ews-utils.c b/src/EWS/camel/camel-ews-utils.c
similarity index 99%
rename from src/camel/camel-ews-utils.c
rename to src/EWS/camel/camel-ews-utils.c
index 02467c31..7c08e8f8 100644
--- a/src/camel/camel-ews-utils.c
+++ b/src/EWS/camel/camel-ews-utils.c
@@ -17,10 +17,10 @@
 #include <libemail-engine/libemail-engine.h>
 #include <e-util/e-util.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-camel-common.h"
-#include "server/e-ews-item-change.h"
-#include "server/e-ews-message.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-camel-common.h"
+#include "common/e-ews-item-change.h"
+#include "common/e-ews-message.h"
 
 #include "camel-ews-utils.h"
 
diff --git a/src/camel/camel-ews-utils.h b/src/EWS/camel/camel-ews-utils.h
similarity index 98%
rename from src/camel/camel-ews-utils.h
rename to src/EWS/camel/camel-ews-utils.h
index 15986587..db0e6a17 100644
--- a/src/camel/camel-ews-utils.h
+++ b/src/EWS/camel/camel-ews-utils.h
@@ -9,7 +9,7 @@
 
 #include <camel/camel.h>
 
-#include "server/e-ews-connection.h"
+#include "common/e-ews-connection.h"
 
 #include "camel-ews-store.h"
 #include "camel-ews-folder.h"
diff --git a/src/camel/libcamelews.urls b/src/EWS/camel/libcamelews.urls
similarity index 100%
rename from src/camel/libcamelews.urls
rename to src/EWS/camel/libcamelews.urls
diff --git a/src/server/CMakeLists.txt b/src/EWS/common/CMakeLists.txt
similarity index 97%
rename from src/server/CMakeLists.txt
rename to src/EWS/common/CMakeLists.txt
index 94dcb0b4..036d8e5a 100644
--- a/src/server/CMakeLists.txt
+++ b/src/EWS/common/CMakeLists.txt
@@ -65,8 +65,8 @@ target_compile_options(evolution-ews PUBLIC
 target_include_directories(evolution-ews PUBLIC
        ${CMAKE_BINARY_DIR}
        ${CMAKE_SOURCE_DIR}
-       ${CMAKE_BINARY_DIR}/src
-       ${CMAKE_SOURCE_DIR}/src
+       ${CMAKE_BINARY_DIR}/src/EWS
+       ${CMAKE_SOURCE_DIR}/src/EWS
        ${CMAKE_CURRENT_BINARY_DIR}
        ${CAMEL_INCLUDE_DIRS}
        ${EVOLUTION_CALENDAR_INCLUDE_DIRS}
diff --git a/src/server/camel-ews-settings.c b/src/EWS/common/camel-ews-settings.c
similarity index 100%
rename from src/server/camel-ews-settings.c
rename to src/EWS/common/camel-ews-settings.c
diff --git a/src/server/camel-ews-settings.h b/src/EWS/common/camel-ews-settings.h
similarity index 100%
rename from src/server/camel-ews-settings.h
rename to src/EWS/common/camel-ews-settings.h
diff --git a/src/server/camel-sasl-xoauth2-office365.c b/src/EWS/common/camel-sasl-xoauth2-office365.c
similarity index 100%
rename from src/server/camel-sasl-xoauth2-office365.c
rename to src/EWS/common/camel-sasl-xoauth2-office365.c
diff --git a/src/server/camel-sasl-xoauth2-office365.h b/src/EWS/common/camel-sasl-xoauth2-office365.h
similarity index 100%
rename from src/server/camel-sasl-xoauth2-office365.h
rename to src/EWS/common/camel-sasl-xoauth2-office365.h
diff --git a/src/server/e-ews-calendar-utils.c b/src/EWS/common/e-ews-calendar-utils.c
similarity index 100%
rename from src/server/e-ews-calendar-utils.c
rename to src/EWS/common/e-ews-calendar-utils.c
diff --git a/src/server/e-ews-calendar-utils.h b/src/EWS/common/e-ews-calendar-utils.h
similarity index 94%
rename from src/server/e-ews-calendar-utils.h
rename to src/EWS/common/e-ews-calendar-utils.h
index 5f1f4672..4b640df9 100644
--- a/src/server/e-ews-calendar-utils.h
+++ b/src/EWS/common/e-ews-calendar-utils.h
@@ -10,8 +10,8 @@
 #include <time.h>
 #include <libecal/libecal.h>
 
-#include "server/e-soap-message.h"
-#include "server/e-ews-item.h"
+#include "common/e-soap-message.h"
+#include "common/e-ews-item.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/server/e-ews-camel-common.c b/src/EWS/common/e-ews-camel-common.c
similarity index 99%
rename from src/server/e-ews-camel-common.c
rename to src/EWS/common/e-ews-camel-common.c
index 9e22ca14..3bd5a356 100644
--- a/src/server/e-ews-camel-common.c
+++ b/src/EWS/common/e-ews-camel-common.c
@@ -9,8 +9,8 @@
 #include <glib/gi18n-lib.h>
 #include <glib/gstdio.h>
 
-#include "server/e-ews-message.h"
-#include "server/e-ews-item-change.h"
+#include "common/e-ews-message.h"
+#include "common/e-ews-item-change.h"
 
 #include "e-ews-camel-common.h"
 
diff --git a/src/server/e-ews-camel-common.h b/src/EWS/common/e-ews-camel-common.h
similarity index 91%
rename from src/server/e-ews-camel-common.h
rename to src/EWS/common/e-ews-camel-common.h
index 873084c8..b1c6add0 100644
--- a/src/server/e-ews-camel-common.h
+++ b/src/EWS/common/e-ews-camel-common.h
@@ -9,8 +9,8 @@
 
 #include <camel/camel.h>
 
-#include "server/e-ews-connection.h"
-#include "server/e-ews-folder.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-folder.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/server/e-ews-connection-utils.c b/src/EWS/common/e-ews-connection-utils.c
similarity index 100%
rename from src/server/e-ews-connection-utils.c
rename to src/EWS/common/e-ews-connection-utils.c
diff --git a/src/server/e-ews-connection-utils.h b/src/EWS/common/e-ews-connection-utils.h
similarity index 97%
rename from src/server/e-ews-connection-utils.h
rename to src/EWS/common/e-ews-connection-utils.h
index fc612d70..95deb9f2 100644
--- a/src/server/e-ews-connection-utils.h
+++ b/src/EWS/common/e-ews-connection-utils.h
@@ -8,7 +8,7 @@
 #define E_EWS_CONNECTION_UTILS_H
 
 #include <glib.h>
-#include <server/e-ews-connection.h>
+#include <common/e-ews-connection.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/server/e-ews-connection.c b/src/EWS/common/e-ews-connection.c
similarity index 100%
rename from src/server/e-ews-connection.c
rename to src/EWS/common/e-ews-connection.c
diff --git a/src/server/e-ews-connection.h b/src/EWS/common/e-ews-connection.h
similarity index 100%
rename from src/server/e-ews-connection.h
rename to src/EWS/common/e-ews-connection.h
diff --git a/src/server/e-ews-debug.c b/src/EWS/common/e-ews-debug.c
similarity index 100%
rename from src/server/e-ews-debug.c
rename to src/EWS/common/e-ews-debug.c
diff --git a/src/server/e-ews-debug.h b/src/EWS/common/e-ews-debug.h
similarity index 94%
rename from src/server/e-ews-debug.h
rename to src/EWS/common/e-ews-debug.h
index 0f648511..46daf228 100644
--- a/src/server/e-ews-debug.h
+++ b/src/EWS/common/e-ews-debug.h
@@ -8,7 +8,7 @@
 #define E_EWS_DEBUG_H
 
 #include <glib.h>
-#include <server/e-ews-connection.h>
+#include <common/e-ews-connection.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/server/e-ews-enums.h b/src/EWS/common/e-ews-enums.h
similarity index 100%
rename from src/server/e-ews-enums.h
rename to src/EWS/common/e-ews-enums.h
diff --git a/src/server/e-ews-folder.c b/src/EWS/common/e-ews-folder.c
similarity index 100%
rename from src/server/e-ews-folder.c
rename to src/EWS/common/e-ews-folder.c
diff --git a/src/server/e-ews-folder.h b/src/EWS/common/e-ews-folder.h
similarity index 100%
rename from src/server/e-ews-folder.h
rename to src/EWS/common/e-ews-folder.h
diff --git a/src/server/e-ews-item-change.c b/src/EWS/common/e-ews-item-change.c
similarity index 100%
rename from src/server/e-ews-item-change.c
rename to src/EWS/common/e-ews-item-change.c
diff --git a/src/server/e-ews-item-change.h b/src/EWS/common/e-ews-item-change.h
similarity index 100%
rename from src/server/e-ews-item-change.h
rename to src/EWS/common/e-ews-item-change.h
diff --git a/src/server/e-ews-item.c b/src/EWS/common/e-ews-item.c
similarity index 100%
rename from src/server/e-ews-item.c
rename to src/EWS/common/e-ews-item.c
diff --git a/src/server/e-ews-item.h b/src/EWS/common/e-ews-item.h
similarity index 100%
rename from src/server/e-ews-item.h
rename to src/EWS/common/e-ews-item.h
diff --git a/src/server/e-ews-message.c b/src/EWS/common/e-ews-message.c
similarity index 100%
rename from src/server/e-ews-message.c
rename to src/EWS/common/e-ews-message.c
diff --git a/src/server/e-ews-message.h b/src/EWS/common/e-ews-message.h
similarity index 100%
rename from src/server/e-ews-message.h
rename to src/EWS/common/e-ews-message.h
diff --git a/src/server/e-ews-notification.c b/src/EWS/common/e-ews-notification.c
similarity index 100%
rename from src/server/e-ews-notification.c
rename to src/EWS/common/e-ews-notification.c
diff --git a/src/server/e-ews-notification.h b/src/EWS/common/e-ews-notification.h
similarity index 100%
rename from src/server/e-ews-notification.h
rename to src/EWS/common/e-ews-notification.h
diff --git a/src/server/e-ews-oof-settings.c b/src/EWS/common/e-ews-oof-settings.c
similarity index 100%
rename from src/server/e-ews-oof-settings.c
rename to src/EWS/common/e-ews-oof-settings.c
diff --git a/src/server/e-ews-oof-settings.h b/src/EWS/common/e-ews-oof-settings.h
similarity index 98%
rename from src/server/e-ews-oof-settings.h
rename to src/EWS/common/e-ews-oof-settings.h
index e8b87bd3..a6c2e1c8 100644
--- a/src/server/e-ews-oof-settings.h
+++ b/src/EWS/common/e-ews-oof-settings.h
@@ -7,8 +7,8 @@
 #ifndef E_EWS_OOF_SETTINGS_H
 #define E_EWS_OOF_SETTINGS_H
 
-#include <server/e-ews-connection.h>
-#include <server/e-ews-enums.h>
+#include <common/e-ews-connection.h>
+#include <common/e-ews-enums.h>
 
 /* Standard GObject macros */
 #define E_TYPE_EWS_OOF_SETTINGS \
diff --git a/src/server/e-ews-query-to-restriction.c b/src/EWS/common/e-ews-query-to-restriction.c
similarity index 99%
rename from src/server/e-ews-query-to-restriction.c
rename to src/EWS/common/e-ews-query-to-restriction.c
index ab879424..cedbf9c1 100644
--- a/src/server/e-ews-query-to-restriction.c
+++ b/src/EWS/common/e-ews-query-to-restriction.c
@@ -13,7 +13,7 @@
 #include <camel/camel.h>
 #include <libedata-cal/libedata-cal.h>
 
-#include "server/e-ews-message.h"
+#include "e-ews-message.h"
 
 #include "e-ews-query-to-restriction.h"
 
diff --git a/src/server/e-ews-query-to-restriction.h b/src/EWS/common/e-ews-query-to-restriction.h
similarity index 89%
rename from src/server/e-ews-query-to-restriction.h
rename to src/EWS/common/e-ews-query-to-restriction.h
index 486b20e5..6cc00cec 100644
--- a/src/server/e-ews-query-to-restriction.h
+++ b/src/EWS/common/e-ews-query-to-restriction.h
@@ -8,8 +8,8 @@
 #ifndef E_EWS_QUERY_TO_RESTRICTION_H
 #define E_EWS_QUERY_TO_RESTRICTION_H
 
-#include "server/e-soap-message.h"
-#include "server/e-ews-folder.h"
+#include "common/e-soap-message.h"
+#include "common/e-ews-folder.h"
 
 gboolean       e_ews_query_check_applicable    (const gchar *query,
                                                 EEwsFolderType type);
diff --git a/src/server/e-oauth2-service-office365.c b/src/EWS/common/e-oauth2-service-office365.c
similarity index 99%
rename from src/server/e-oauth2-service-office365.c
rename to src/EWS/common/e-oauth2-service-office365.c
index 8f3a3d8f..1e50e680 100644
--- a/src/server/e-oauth2-service-office365.c
+++ b/src/EWS/common/e-oauth2-service-office365.c
@@ -9,7 +9,7 @@
 #include <glib/gi18n-lib.h>
 #include <libedataserver/libedataserver.h>
 
-#include "server/camel-ews-settings.h"
+#include "camel-ews-settings.h"
 
 #include "e-oauth2-service-office365.h"
 
diff --git a/src/server/e-oauth2-service-office365.h b/src/EWS/common/e-oauth2-service-office365.h
similarity index 100%
rename from src/server/e-oauth2-service-office365.h
rename to src/EWS/common/e-oauth2-service-office365.h
diff --git a/src/server/e-soap-message.c b/src/EWS/common/e-soap-message.c
similarity index 100%
rename from src/server/e-soap-message.c
rename to src/EWS/common/e-soap-message.c
diff --git a/src/server/e-soap-message.h b/src/EWS/common/e-soap-message.h
similarity index 100%
rename from src/server/e-soap-message.h
rename to src/EWS/common/e-soap-message.h
diff --git a/src/server/e-soap-response.c b/src/EWS/common/e-soap-response.c
similarity index 100%
rename from src/server/e-soap-response.c
rename to src/EWS/common/e-soap-response.c
diff --git a/src/server/e-soap-response.h b/src/EWS/common/e-soap-response.h
similarity index 100%
rename from src/server/e-soap-response.h
rename to src/EWS/common/e-soap-response.h
diff --git a/src/server/e-soup-auth-negotiate.c b/src/EWS/common/e-soup-auth-negotiate.c
similarity index 100%
rename from src/server/e-soup-auth-negotiate.c
rename to src/EWS/common/e-soup-auth-negotiate.c
diff --git a/src/server/e-soup-auth-negotiate.h b/src/EWS/common/e-soup-auth-negotiate.h
similarity index 100%
rename from src/server/e-soup-auth-negotiate.h
rename to src/EWS/common/e-soup-auth-negotiate.h
diff --git a/src/server/e-source-ews-folder.c b/src/EWS/common/e-source-ews-folder.c
similarity index 100%
rename from src/server/e-source-ews-folder.c
rename to src/EWS/common/e-source-ews-folder.c
diff --git a/src/server/e-source-ews-folder.h b/src/EWS/common/e-source-ews-folder.h
similarity index 99%
rename from src/server/e-source-ews-folder.h
rename to src/EWS/common/e-source-ews-folder.h
index 596c20ca..984b5e57 100644
--- a/src/server/e-source-ews-folder.h
+++ b/src/EWS/common/e-source-ews-folder.h
@@ -8,7 +8,7 @@
 #define E_SOURCE_EWS_FOLDER_H
 
 #include <libedataserver/libedataserver.h>
-#include "server/e-ews-folder.h"
+#include "common/e-ews-folder.h"
 
 /* Standard GObject macros */
 #define E_TYPE_SOURCE_EWS_FOLDER \
diff --git a/src/server/ews-errors.c b/src/EWS/common/ews-errors.c
similarity index 100%
rename from src/server/ews-errors.c
rename to src/EWS/common/ews-errors.c
diff --git a/src/server/ews-errors.h b/src/EWS/common/ews-errors.h
similarity index 99%
rename from src/server/ews-errors.h
rename to src/EWS/common/ews-errors.h
index a7350702..262903b2 100644
--- a/src/server/ews-errors.h
+++ b/src/EWS/common/ews-errors.h
@@ -7,7 +7,7 @@
 #ifndef EWS_ERRORS_H
 #define EWS_ERRORS_H
 
-#include <server/e-soap-response.h>
+#include <common/e-soap-response.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/configuration/CMakeLists.txt b/src/EWS/evolution/CMakeLists.txt
similarity index 100%
rename from src/configuration/CMakeLists.txt
rename to src/EWS/evolution/CMakeLists.txt
diff --git a/src/configuration/e-book-config-ews.c b/src/EWS/evolution/e-book-config-ews.c
similarity index 98%
rename from src/configuration/e-book-config-ews.c
rename to src/EWS/evolution/e-book-config-ews.c
index d9e40cea..6a4eec19 100644
--- a/src/configuration/e-book-config-ews.c
+++ b/src/EWS/evolution/e-book-config-ews.c
@@ -8,8 +8,8 @@
 
 #include <glib/gi18n-lib.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-source-ews-folder.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-source-ews-folder.h"
 
 #include "e-book-config-ews.h"
 
diff --git a/src/configuration/e-book-config-ews.h b/src/EWS/evolution/e-book-config-ews.h
similarity index 100%
rename from src/configuration/e-book-config-ews.h
rename to src/EWS/evolution/e-book-config-ews.h
diff --git a/src/configuration/e-cal-config-ews.c b/src/EWS/evolution/e-cal-config-ews.c
similarity index 100%
rename from src/configuration/e-cal-config-ews.c
rename to src/EWS/evolution/e-cal-config-ews.c
diff --git a/src/configuration/e-cal-config-ews.h b/src/EWS/evolution/e-cal-config-ews.h
similarity index 100%
rename from src/configuration/e-cal-config-ews.h
rename to src/EWS/evolution/e-cal-config-ews.h
diff --git a/src/configuration/e-ews-config-lookup.c b/src/EWS/evolution/e-ews-config-lookup.c
similarity index 99%
rename from src/configuration/e-ews-config-lookup.c
rename to src/EWS/evolution/e-ews-config-lookup.c
index b818ccc2..947c4c14 100644
--- a/src/configuration/e-ews-config-lookup.c
+++ b/src/EWS/evolution/e-ews-config-lookup.c
@@ -10,8 +10,8 @@
 
 #include <e-util/e-util.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-connection.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-connection.h"
 #include "e-ews-config-lookup.h"
 
 #define E_TYPE_EWS_CONFIG_LOOKUP_RESULT \
diff --git a/src/configuration/e-ews-config-lookup.h b/src/EWS/evolution/e-ews-config-lookup.h
similarity index 100%
rename from src/configuration/e-ews-config-lookup.h
rename to src/EWS/evolution/e-ews-config-lookup.h
diff --git a/src/configuration/e-ews-config-ui-extension.c b/src/EWS/evolution/e-ews-config-ui-extension.c
similarity index 100%
rename from src/configuration/e-ews-config-ui-extension.c
rename to src/EWS/evolution/e-ews-config-ui-extension.c
diff --git a/src/configuration/e-ews-config-ui-extension.h b/src/EWS/evolution/e-ews-config-ui-extension.h
similarity index 100%
rename from src/configuration/e-ews-config-ui-extension.h
rename to src/EWS/evolution/e-ews-config-ui-extension.h
diff --git a/src/configuration/e-ews-config-utils.c b/src/EWS/evolution/e-ews-config-utils.c
similarity index 99%
rename from src/configuration/e-ews-config-utils.c
rename to src/EWS/evolution/e-ews-config-utils.c
index 34a93227..68baab67 100644
--- a/src/configuration/e-ews-config-utils.c
+++ b/src/EWS/evolution/e-ews-config-utils.c
@@ -23,9 +23,9 @@
 #include <shell/e-shell-view.h>
 #include <shell/e-shell-window.h>
 
-#include "server/e-ews-connection.h"
-#include "server/e-ews-connection-utils.h"
-#include "server/e-source-ews-folder.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-connection-utils.h"
+#include "common/e-source-ews-folder.h"
 
 #include "e-ews-edit-folder-permissions.h"
 
diff --git a/src/configuration/e-ews-config-utils.h b/src/EWS/evolution/e-ews-config-utils.h
similarity index 98%
rename from src/configuration/e-ews-config-utils.h
rename to src/EWS/evolution/e-ews-config-utils.h
index ce4c8c35..14db9377 100644
--- a/src/configuration/e-ews-config-utils.h
+++ b/src/EWS/evolution/e-ews-config-utils.h
@@ -11,7 +11,7 @@
 
 #include <shell/e-shell-view.h>
 
-#include "server/e-ews-connection.h"
+#include "common/e-ews-connection.h"
 #include "camel/camel-ews-store.h"
 
 typedef void           (* EEwsSetupFunc)                                       (GObject *with_object,
diff --git a/src/configuration/e-ews-edit-folder-permissions.c 
b/src/EWS/evolution/e-ews-edit-folder-permissions.c
similarity index 100%
rename from src/configuration/e-ews-edit-folder-permissions.c
rename to src/EWS/evolution/e-ews-edit-folder-permissions.c
diff --git a/src/configuration/e-ews-edit-folder-permissions.h 
b/src/EWS/evolution/e-ews-edit-folder-permissions.h
similarity index 86%
rename from src/configuration/e-ews-edit-folder-permissions.h
rename to src/EWS/evolution/e-ews-edit-folder-permissions.h
index ba559c9a..55429dc1 100644
--- a/src/configuration/e-ews-edit-folder-permissions.h
+++ b/src/EWS/evolution/e-ews-edit-folder-permissions.h
@@ -10,9 +10,9 @@
 #include <gtk/gtk.h>
 #include <libedataserver/libedataserver.h>
 
-#include "server/e-ews-item.h"
-#include "server/e-ews-folder.h"
-#include "server/camel-ews-settings.h"
+#include "common/e-ews-item.h"
+#include "common/e-ews-folder.h"
+#include "common/camel-ews-settings.h"
 
 void   e_ews_edit_folder_permissions   (GtkWindow *parent,
                                         ESourceRegistry *registry,
diff --git a/src/configuration/e-ews-ooo-notificator.c b/src/EWS/evolution/e-ews-ooo-notificator.c
similarity index 99%
rename from src/configuration/e-ews-ooo-notificator.c
rename to src/EWS/evolution/e-ews-ooo-notificator.c
index 2fcb34a3..19a96bda 100644
--- a/src/configuration/e-ews-ooo-notificator.c
+++ b/src/EWS/evolution/e-ews-ooo-notificator.c
@@ -8,7 +8,7 @@
 
 #include "e-ews-ooo-notificator.h"
 #include "camel/camel-ews-store.h"
-#include "server/e-ews-oof-settings.h"
+#include "common/e-ews-oof-settings.h"
 
 #include <shell/e-shell.h>
 #include <shell/e-shell-view.h>
diff --git a/src/configuration/e-ews-ooo-notificator.h b/src/EWS/evolution/e-ews-ooo-notificator.h
similarity index 100%
rename from src/configuration/e-ews-ooo-notificator.h
rename to src/EWS/evolution/e-ews-ooo-notificator.h
diff --git a/src/configuration/e-ews-photo-source.c b/src/EWS/evolution/e-ews-photo-source.c
similarity index 98%
rename from src/configuration/e-ews-photo-source.c
rename to src/EWS/evolution/e-ews-photo-source.c
index edf5f391..732d6390 100644
--- a/src/configuration/e-ews-photo-source.c
+++ b/src/EWS/evolution/e-ews-photo-source.c
@@ -8,8 +8,8 @@
 
 #include <e-util/e-util.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-connection.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-connection.h"
 
 #include "e-ews-photo-source.h"
 
diff --git a/src/configuration/e-ews-photo-source.h b/src/EWS/evolution/e-ews-photo-source.h
similarity index 100%
rename from src/configuration/e-ews-photo-source.h
rename to src/EWS/evolution/e-ews-photo-source.h
diff --git a/src/configuration/e-ews-search-user.c b/src/EWS/evolution/e-ews-search-user.c
similarity index 100%
rename from src/configuration/e-ews-search-user.c
rename to src/EWS/evolution/e-ews-search-user.c
diff --git a/src/configuration/e-ews-search-user.h b/src/EWS/evolution/e-ews-search-user.h
similarity index 87%
rename from src/configuration/e-ews-search-user.h
rename to src/EWS/evolution/e-ews-search-user.h
index 4e4b8eae..ae34017c 100644
--- a/src/configuration/e-ews-search-user.h
+++ b/src/EWS/evolution/e-ews-search-user.h
@@ -8,8 +8,8 @@
 #define E_EWS_SEARCH_USER_H
 
 #include <gtk/gtk.h>
-#include "server/e-ews-connection.h"
-#include "server/e-ews-item.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-item.h"
 
 gboolean       e_ews_search_user_modal (GtkWindow *parent,
                                         EEwsConnection *conn,
diff --git a/src/configuration/e-ews-subscribe-foreign-folder.c 
b/src/EWS/evolution/e-ews-subscribe-foreign-folder.c
similarity index 99%
rename from src/configuration/e-ews-subscribe-foreign-folder.c
rename to src/EWS/evolution/e-ews-subscribe-foreign-folder.c
index 5df92e99..5204d54e 100644
--- a/src/configuration/e-ews-subscribe-foreign-folder.c
+++ b/src/EWS/evolution/e-ews-subscribe-foreign-folder.c
@@ -15,7 +15,7 @@
 #include "camel/camel-ews-store-summary.h"
 #include "camel/camel-ews-utils.h"
 
-#include "server/e-ews-calendar-utils.h"
+#include "common/e-ews-calendar-utils.h"
 
 #include "e-ews-config-utils.h"
 #include "e-ews-search-user.h"
diff --git a/src/configuration/e-ews-subscribe-foreign-folder.h 
b/src/EWS/evolution/e-ews-subscribe-foreign-folder.h
similarity index 100%
rename from src/configuration/e-ews-subscribe-foreign-folder.h
rename to src/EWS/evolution/e-ews-subscribe-foreign-folder.h
diff --git a/src/configuration/e-mail-config-ews-autodiscover.c 
b/src/EWS/evolution/e-mail-config-ews-autodiscover.c
similarity index 99%
rename from src/configuration/e-mail-config-ews-autodiscover.c
rename to src/EWS/evolution/e-mail-config-ews-autodiscover.c
index c0603489..00ea11e1 100644
--- a/src/configuration/e-mail-config-ews-autodiscover.c
+++ b/src/EWS/evolution/e-mail-config-ews-autodiscover.c
@@ -14,8 +14,8 @@
 #include <shell/e-shell.h>
 #include <mail/e-mail-config-service-page.h>
 
-#include "server/e-ews-connection.h"
-#include "server/e-ews-connection-utils.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-connection-utils.h"
 
 #define E_MAIL_CONFIG_EWS_AUTODISCOVER_GET_PRIVATE(obj) \
        (G_TYPE_INSTANCE_GET_PRIVATE \
diff --git a/src/configuration/e-mail-config-ews-autodiscover.h 
b/src/EWS/evolution/e-mail-config-ews-autodiscover.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-autodiscover.h
rename to src/EWS/evolution/e-mail-config-ews-autodiscover.h
diff --git a/src/configuration/e-mail-config-ews-backend.c b/src/EWS/evolution/e-mail-config-ews-backend.c
similarity index 99%
rename from src/configuration/e-mail-config-ews-backend.c
rename to src/EWS/evolution/e-mail-config-ews-backend.c
index 8c9d081b..abdabf27 100644
--- a/src/configuration/e-mail-config-ews-backend.c
+++ b/src/EWS/evolution/e-mail-config-ews-backend.c
@@ -16,7 +16,7 @@
 #include <mail/e-mail-config-auth-check.h>
 #include <mail/e-mail-config-receiving-page.h>
 
-#include "server/camel-ews-settings.h"
+#include "common/camel-ews-settings.h"
 
 #include "e-mail-config-ews-autodiscover.h"
 #include "e-ews-config-utils.h"
diff --git a/src/configuration/e-mail-config-ews-backend.h b/src/EWS/evolution/e-mail-config-ews-backend.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-backend.h
rename to src/EWS/evolution/e-mail-config-ews-backend.h
diff --git a/src/configuration/e-mail-config-ews-delegates-page.c 
b/src/EWS/evolution/e-mail-config-ews-delegates-page.c
similarity index 99%
rename from src/configuration/e-mail-config-ews-delegates-page.c
rename to src/EWS/evolution/e-mail-config-ews-delegates-page.c
index 1872f2e2..e95e29c8 100644
--- a/src/configuration/e-mail-config-ews-delegates-page.c
+++ b/src/EWS/evolution/e-mail-config-ews-delegates-page.c
@@ -15,10 +15,10 @@
 #include <e-util/e-util.h>
 #include <mail/e-mail-backend.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-connection.h"
-#include "server/e-ews-connection-utils.h"
-#include "server/e-ews-oof-settings.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-connection-utils.h"
+#include "common/e-ews-oof-settings.h"
 #include "camel/camel-ews-store.h"
 
 #include "e-ews-config-utils.h"
diff --git a/src/configuration/e-mail-config-ews-delegates-page.h 
b/src/EWS/evolution/e-mail-config-ews-delegates-page.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-delegates-page.h
rename to src/EWS/evolution/e-mail-config-ews-delegates-page.h
diff --git a/src/configuration/e-mail-config-ews-folder-sizes-page.c 
b/src/EWS/evolution/e-mail-config-ews-folder-sizes-page.c
similarity index 100%
rename from src/configuration/e-mail-config-ews-folder-sizes-page.c
rename to src/EWS/evolution/e-mail-config-ews-folder-sizes-page.c
diff --git a/src/configuration/e-mail-config-ews-folder-sizes-page.h 
b/src/EWS/evolution/e-mail-config-ews-folder-sizes-page.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-folder-sizes-page.h
rename to src/EWS/evolution/e-mail-config-ews-folder-sizes-page.h
diff --git a/src/configuration/e-mail-config-ews-gal.c b/src/EWS/evolution/e-mail-config-ews-gal.c
similarity index 99%
rename from src/configuration/e-mail-config-ews-gal.c
rename to src/EWS/evolution/e-mail-config-ews-gal.c
index 4a0eb6c3..d9e32dc4 100644
--- a/src/configuration/e-mail-config-ews-gal.c
+++ b/src/EWS/evolution/e-mail-config-ews-gal.c
@@ -12,7 +12,7 @@
 
 #include <mail/e-mail-config-provider-page.h>
 
-#include "server/camel-ews-settings.h"
+#include "common/camel-ews-settings.h"
 
 #include "e-mail-config-ews-oal-combo-box.h"
 
diff --git a/src/configuration/e-mail-config-ews-gal.h b/src/EWS/evolution/e-mail-config-ews-gal.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-gal.h
rename to src/EWS/evolution/e-mail-config-ews-gal.h
diff --git a/src/configuration/e-mail-config-ews-notebook.c b/src/EWS/evolution/e-mail-config-ews-notebook.c
similarity index 100%
rename from src/configuration/e-mail-config-ews-notebook.c
rename to src/EWS/evolution/e-mail-config-ews-notebook.c
diff --git a/src/configuration/e-mail-config-ews-notebook.h b/src/EWS/evolution/e-mail-config-ews-notebook.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-notebook.h
rename to src/EWS/evolution/e-mail-config-ews-notebook.h
diff --git a/src/configuration/e-mail-config-ews-oal-combo-box.c 
b/src/EWS/evolution/e-mail-config-ews-oal-combo-box.c
similarity index 99%
rename from src/configuration/e-mail-config-ews-oal-combo-box.c
rename to src/EWS/evolution/e-mail-config-ews-oal-combo-box.c
index 4ef8f79b..9c61787f 100644
--- a/src/configuration/e-mail-config-ews-oal-combo-box.c
+++ b/src/EWS/evolution/e-mail-config-ews-oal-combo-box.c
@@ -8,8 +8,8 @@
 
 #include <mail/e-mail-config-service-page.h>
 
-#include "server/e-ews-connection.h"
-#include "server/e-ews-connection-utils.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-connection-utils.h"
 
 #include "e-ews-config-utils.h"
 #include "e-mail-config-ews-oal-combo-box.h"
diff --git a/src/configuration/e-mail-config-ews-oal-combo-box.h 
b/src/EWS/evolution/e-mail-config-ews-oal-combo-box.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-oal-combo-box.h
rename to src/EWS/evolution/e-mail-config-ews-oal-combo-box.h
diff --git a/src/configuration/e-mail-config-ews-offline-options.c 
b/src/EWS/evolution/e-mail-config-ews-offline-options.c
similarity index 100%
rename from src/configuration/e-mail-config-ews-offline-options.c
rename to src/EWS/evolution/e-mail-config-ews-offline-options.c
diff --git a/src/configuration/e-mail-config-ews-offline-options.h 
b/src/EWS/evolution/e-mail-config-ews-offline-options.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-offline-options.h
rename to src/EWS/evolution/e-mail-config-ews-offline-options.h
diff --git a/src/configuration/e-mail-config-ews-ooo-page.c b/src/EWS/evolution/e-mail-config-ews-ooo-page.c
similarity index 99%
rename from src/configuration/e-mail-config-ews-ooo-page.c
rename to src/EWS/evolution/e-mail-config-ews-ooo-page.c
index b0e3e75e..5cf74985 100644
--- a/src/configuration/e-mail-config-ews-ooo-page.c
+++ b/src/EWS/evolution/e-mail-config-ews-ooo-page.c
@@ -14,10 +14,10 @@
 
 #include <e-util/e-util.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-connection.h"
-#include "server/e-ews-connection-utils.h"
-#include "server/e-ews-oof-settings.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-connection-utils.h"
+#include "common/e-ews-oof-settings.h"
 
 #include "e-ews-config-utils.h"
 
diff --git a/src/configuration/e-mail-config-ews-ooo-page.h b/src/EWS/evolution/e-mail-config-ews-ooo-page.h
similarity index 100%
rename from src/configuration/e-mail-config-ews-ooo-page.h
rename to src/EWS/evolution/e-mail-config-ews-ooo-page.h
diff --git a/src/configuration/module-ews-configuration.c b/src/EWS/evolution/module-ews-configuration.c
similarity index 94%
rename from src/configuration/module-ews-configuration.c
rename to src/EWS/evolution/module-ews-configuration.c
index 49ca159a..978e7cb3 100644
--- a/src/configuration/module-ews-configuration.c
+++ b/src/EWS/evolution/module-ews-configuration.c
@@ -24,9 +24,9 @@
 #include "e-ews-photo-source.h"
 
 #include "e-ews-config-ui-extension.h"
-#include "server/camel-sasl-xoauth2-office365.h"
-#include "server/e-oauth2-service-office365.h"
-#include "server/e-source-ews-folder.h"
+#include "common/camel-sasl-xoauth2-office365.h"
+#include "common/e-oauth2-service-office365.h"
+#include "common/e-source-ews-folder.h"
 
 /* Module Entry Points */
 void e_module_load (GTypeModule *type_module);
diff --git a/src/configuration/module-ews-configuration.error.xml 
b/src/EWS/evolution/module-ews-configuration.error.xml
similarity index 100%
rename from src/configuration/module-ews-configuration.error.xml
rename to src/EWS/evolution/module-ews-configuration.error.xml
diff --git a/src/collection/CMakeLists.txt b/src/EWS/registry/CMakeLists.txt
similarity index 100%
rename from src/collection/CMakeLists.txt
rename to src/EWS/registry/CMakeLists.txt
diff --git a/src/collection/e-ews-backend-factory.c b/src/EWS/registry/e-ews-backend-factory.c
similarity index 100%
rename from src/collection/e-ews-backend-factory.c
rename to src/EWS/registry/e-ews-backend-factory.c
diff --git a/src/collection/e-ews-backend-factory.h b/src/EWS/registry/e-ews-backend-factory.h
similarity index 100%
rename from src/collection/e-ews-backend-factory.h
rename to src/EWS/registry/e-ews-backend-factory.h
diff --git a/src/collection/e-ews-backend.c b/src/EWS/registry/e-ews-backend.c
similarity index 99%
rename from src/collection/e-ews-backend.c
rename to src/EWS/registry/e-ews-backend.c
index 4669ad39..eaa82ba3 100644
--- a/src/collection/e-ews-backend.c
+++ b/src/EWS/registry/e-ews-backend.c
@@ -10,8 +10,8 @@
 
 #include <glib/gi18n-lib.h>
 
-#include "server/e-ews-connection-utils.h"
-#include "server/e-source-ews-folder.h"
+#include "common/e-ews-connection-utils.h"
+#include "common/e-source-ews-folder.h"
 
 #define E_EWS_BACKEND_GET_PRIVATE(obj) \
        (G_TYPE_INSTANCE_GET_PRIVATE \
diff --git a/src/collection/e-ews-backend.h b/src/EWS/registry/e-ews-backend.h
similarity index 98%
rename from src/collection/e-ews-backend.h
rename to src/EWS/registry/e-ews-backend.h
index 9bcc6a2f..82c72844 100644
--- a/src/collection/e-ews-backend.h
+++ b/src/EWS/registry/e-ews-backend.h
@@ -9,7 +9,7 @@
 
 #include <libebackend/libebackend.h>
 
-#include "server/e-ews-connection.h"
+#include "common/e-ews-connection.h"
 
 /* Standard GObject macros */
 #define E_TYPE_EWS_BACKEND \
diff --git a/src/collection/module-ews-backend.c b/src/EWS/registry/module-ews-backend.c
similarity index 91%
rename from src/collection/module-ews-backend.c
rename to src/EWS/registry/module-ews-backend.c
index 41c98c10..6e899abb 100644
--- a/src/collection/module-ews-backend.c
+++ b/src/EWS/registry/module-ews-backend.c
@@ -8,8 +8,8 @@
 
 #include <glib/gi18n-lib.h>
 
-#include "server/e-oauth2-service-office365.h"
-#include "server/e-source-ews-folder.h"
+#include "common/e-oauth2-service-office365.h"
+#include "common/e-source-ews-folder.h"
 
 #include "e-ews-backend.h"
 #include "e-ews-backend-factory.h"
diff --git a/src/Microsoft365/CMakeLists.txt b/src/Microsoft365/CMakeLists.txt
new file mode 100644
index 00000000..2c187fec
--- /dev/null
+++ b/src/Microsoft365/CMakeLists.txt
@@ -0,0 +1,64 @@
+macro(add_simple_module_m365 _name _sourcesvar _depsvar _defsvar _cflagsvar _incdirsvar _ldflagsvar 
_destination)
+       set(DEPENDENCIES
+               evolution-microsoft365
+       )
+
+       set(SOURCES
+               ${${_sourcesvar}}
+       )
+
+       add_library(${_name} MODULE
+               ${SOURCES}
+       )
+
+       set_target_properties(${_name} PROPERTIES
+               PREFIX ""
+       )
+
+       add_dependencies(${_name}
+               ${DEPENDENCIES}
+               ${${_depsvar}}
+       )
+
+       target_compile_definitions(${_name} PRIVATE
+               -DG_LOG_DOMAIN=\"${_name}\"
+               -DM365_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+               ${${_defsvar}}
+       )
+
+       target_compile_options(${_name} PUBLIC
+               ${LIBEBACKEND_CFLAGS}
+               ${LIBEDATASERVER_CFLAGS}
+               ${SOUP_CFLAGS}
+               ${${_cflagsvar}}
+       )
+
+       target_include_directories(${_name} PUBLIC
+               ${CMAKE_BINARY_DIR}
+               ${CMAKE_SOURCE_DIR}
+               ${LIBEBACKEND_INCLUDE_DIRS}
+               ${LIBEDATASERVER_INCLUDE_DIRS}
+               ${SOUP_INCLUDE_DIRS}
+               ${${_incdirsvar}}
+       )
+
+       target_link_libraries(${_name}
+               ${DEPENDENCIES}
+               ${${_depsvar}}
+               ${LIBEBACKEND_LDFLAGS}
+               ${LIBEDATASERVER_LDFLAGS}
+               ${SOUP_LDFLAGS}
+               ${${_ldflagsvar}}
+       )
+
+       install(TARGETS ${_name}
+               DESTINATION ${_destination}
+       )
+endmacro(add_simple_module_m365)
+
+add_subdirectory(addressbook)
+add_subdirectory(calendar)
+add_subdirectory(camel)
+add_subdirectory(common)
+add_subdirectory(evolution)
+add_subdirectory(registry)
diff --git a/src/Microsoft365/addressbook/CMakeLists.txt b/src/Microsoft365/addressbook/CMakeLists.txt
new file mode 100644
index 00000000..2789a140
--- /dev/null
+++ b/src/Microsoft365/addressbook/CMakeLists.txt
@@ -0,0 +1,65 @@
+set(DEPENDENCIES
+       evolution-microsoft365
+)
+
+set(SOURCES
+       e-book-backend-m365.c
+       e-book-backend-m365.h
+       e-book-backend-m365-factory.c
+)
+
+add_library(ebookbackendmicrosoft365 MODULE
+       ${SOURCES}
+)
+
+add_dependencies(ebookbackendmicrosoft365
+       ${DEPENDENCIES}
+)
+
+target_compile_definitions(ebookbackendmicrosoft365 PRIVATE
+       -DG_LOG_DOMAIN=\"ebookbackendmicrosoft365\"
+       -DBACKENDDIR=\"${ebook_backenddir}\"
+       -DM365_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+)
+
+target_compile_options(ebookbackendmicrosoft365 PUBLIC
+       ${LIBEBACKEND_CFLAGS}
+       ${LIBEBOOK_CFLAGS}
+       ${LIBEDATABOOK_CFLAGS}
+       ${LIBEDATASERVER_CFLAGS}
+       ${LIBICAL_GLIB_CFLAGS}
+       ${MSPACK_CFLAGS}
+       ${SOUP_CFLAGS}
+)
+
+target_include_directories(ebookbackendmicrosoft365 PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_SOURCE_DIR}
+       ${CMAKE_BINARY_DIR}/src/Microsoft365
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365
+       ${CMAKE_CURRENT_BINARY_DIR}
+       ${CMAKE_CURRENT_SOURCE_DIR}
+       ${LIBEBACKEND_INCLUDE_DIRS}
+       ${LIBEBOOK_INCLUDE_DIRS}
+       ${LIBEDATABOOK_INCLUDE_DIRS}
+       ${LIBEDATASERVER_INCLUDE_DIRS}
+       ${LIBICAL_GLIB_INCLUDE_DIRS}
+       ${MSPACK_INCLUDE_DIRS}
+       ${SOUP_INCLUDE_DIRS}
+)
+
+target_link_libraries(ebookbackendmicrosoft365
+       ${DEPENDENCIES}
+       ${LIBEBACKEND_LDFLAGS}
+       ${LIBEBOOK_LDFLAGS}
+       ${LIBEDATABOOK_LDFLAGS}
+       ${LIBEDATASERVER_LDFLAGS}
+       ${LIBICAL_GLIB_LDFLAGS}
+       ${MATH_LDFLAGS}
+       ${MSPACK_LDFLAGS}
+       ${SOUP_LDFLAGS}
+)
+
+install(TARGETS ebookbackendmicrosoft365
+       DESTINATION ${ebook_backenddir}
+)
diff --git a/src/Microsoft365/addressbook/e-book-backend-m365-factory.c 
b/src/Microsoft365/addressbook/e-book-backend-m365-factory.c
new file mode 100644
index 00000000..fcef9c21
--- /dev/null
+++ b/src/Microsoft365/addressbook/e-book-backend-m365-factory.c
@@ -0,0 +1,74 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include <libedata-book/libedata-book.h>
+
+#include "common/e-oauth2-service-microsoft365.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-book-backend-m365.h"
+
+typedef EBookBackendFactory EBookBackendM365Factory;
+typedef EBookBackendFactoryClass EBookBackendM365FactoryClass;
+
+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_book_backend_m365_factory_get_type (void);
+
+G_DEFINE_DYNAMIC_TYPE (EBookBackendM365Factory, e_book_backend_m365_factory, E_TYPE_BOOK_BACKEND_FACTORY)
+
+static void
+e_book_backend_m365_factory_class_init (EBookBackendFactoryClass *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 = "microsoft365";
+       class->backend_type = E_TYPE_BOOK_BACKEND_M365;
+}
+
+static void
+e_book_backend_m365_factory_class_finalize (EBookBackendFactoryClass *class)
+{
+}
+
+static void
+e_book_backend_m365_factory_init (EBookBackendFactory *factory)
+{
+}
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+       bindtextdomain (GETTEXT_PACKAGE, M365_LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+       e_module = E_MODULE (type_module);
+
+       e_oauth2_service_microsoft365_type_register (type_module);
+       e_source_m365_folder_type_register (type_module);
+
+       if (g_strcmp0 (g_getenv ("ENABLE_M365"), "1") == 0)
+               e_book_backend_m365_factory_register_type (type_module);
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+       e_module = NULL;
+}
diff --git a/src/Microsoft365/addressbook/e-book-backend-m365.c 
b/src/Microsoft365/addressbook/e-book-backend-m365.c
new file mode 100644
index 00000000..bfe19958
--- /dev/null
+++ b/src/Microsoft365/addressbook/e-book-backend-m365.c
@@ -0,0 +1,1933 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+#include <time.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <libedata-book/libedata-book.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-book-backend-m365.h"
+
+#ifdef G_OS_WIN32
+#ifdef gmtime_r
+#undef gmtime_r
+#endif
+
+/* The gmtime() in Microsoft's C library is MT-safe */
+#define gmtime_r(tp,tmp) (gmtime(tp)?(*(tmp)=*gmtime(tp),(tmp)):0)
+#endif
+
+#define EC_ERROR_EX(_code,_msg) e_client_error_create (_code, _msg)
+#define EBC_ERROR_EX(_code,_msg) e_book_client_error_create (_code, _msg)
+
+#define LOCK(_bb) g_rec_mutex_lock (&_bb->priv->property_lock)
+#define UNLOCK(_bb) g_rec_mutex_unlock (&_bb->priv->property_lock)
+
+struct _EBookBackendM365Private {
+       GRecMutex property_lock;
+       EM365Connection *cnc;
+       gchar *folder_id;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EBookBackendM365, e_book_backend_m365, E_TYPE_BOOK_META_BACKEND)
+
+static void
+ebb_m365_contact_get_string_attribute (EM365Contact *m365_contact,
+                                      EContact *inout_contact,
+                                      EContactField field_id,
+                                      const gchar * (*m365_get_func) (EM365Contact *contact))
+{
+       e_contact_set (inout_contact, field_id, m365_get_func (m365_contact));
+}
+
+static void
+ebb_m365_contact_add_string_attribute (EContact *new_contact,
+                                      EContact *old_contact,
+                                      EContactField field_id,
+                                      JsonBuilder *builder,
+                                      void (* m365_add_func) (JsonBuilder *builder,
+                                                              const gchar *value))
+{
+       const gchar *new_value, *old_value;
+
+       g_return_if_fail (m365_add_func != NULL);
+
+       new_value = e_contact_get_const (new_contact, field_id);
+       old_value = old_contact ? e_contact_get_const (old_contact, field_id) : NULL;
+
+       if (g_strcmp0 (new_value, old_value) != 0)
+               m365_add_func (builder, new_value);
+}
+
+static gboolean
+ebb_m365_contact_get_rev (EBookBackendM365 *bbm365,
+                         EM365Contact *m365_contact,
+                         EContact *inout_contact,
+                         EContactField field_id,
+                         EM365Connection *cnc,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       gchar time_string[100] = { 0 };
+       struct tm stm;
+       time_t value;
+
+       value = e_m365_contact_get_last_modified_date_time (m365_contact);
+
+       if (value <= (time_t) 0)
+               value = time (NULL);
+
+       gmtime_r (&value, &stm);
+       strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", &stm);
+
+       e_contact_set (inout_contact, field_id, time_string);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_birthday (EBookBackendM365 *bbm365,
+                              EM365Contact *m365_contact,
+                              EContact *inout_contact,
+                              EContactField field_id,
+                              EM365Connection *cnc,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       time_t value;
+
+       value = e_m365_contact_get_birthday (m365_contact);
+
+       if (value > (time_t) 0) {
+               EContactDate dt;
+               struct tm stm;
+
+               gmtime_r (&value, &stm);
+
+               dt.year = stm.tm_year + 1900;
+               dt.month = stm.tm_mon + 1;
+               dt.day = stm.tm_mday;
+
+               e_contact_set (inout_contact, field_id, &dt);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_add_birthday (EBookBackendM365 *bbm365,
+                              EContact *new_contact,
+                              EContact *old_contact,
+                              EContactField field_id,
+                              const gchar *m365_id,
+                              JsonBuilder *builder,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       EContactDate *old_dt = NULL;
+       EContactDate *new_dt = NULL;
+
+       new_dt = e_contact_get (new_contact, field_id);
+       old_dt = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!e_contact_date_equal (new_dt, old_dt)) {
+               if (new_dt) {
+                       GDateTime *gdt;
+                       time_t value = (time_t) 0;
+
+                       gdt = g_date_time_new_local (new_dt->year, new_dt->month, new_dt->day, 11, 59, 0.0);
+
+                       if (gdt) {
+                               value = g_date_time_to_unix (gdt);
+                               value = value - (value % (24 * 60 * 60));
+                               value = value + (((12 * 60) - 1) * 60);
+
+                               g_date_time_unref (gdt);
+                       }
+
+                       e_m365_contact_add_birthday (builder, value);
+               } else {
+                       e_m365_contact_add_birthday (builder, (time_t) 0);
+               }
+       }
+
+       e_contact_date_free (new_dt);
+       e_contact_date_free (old_dt);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_address (EBookBackendM365 *bbm365,
+                             EM365Contact *m365_contact,
+                             EContact *inout_contact,
+                             EContactField field_id,
+                             EM365Connection *cnc,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       EM365PhysicalAddress *phys_address = NULL;
+
+       if (field_id == E_CONTACT_ADDRESS_WORK)
+               phys_address = e_m365_contact_get_business_address (m365_contact);
+       else if (field_id == E_CONTACT_ADDRESS_HOME)
+               phys_address = e_m365_contact_get_home_address (m365_contact);
+       else if (field_id == E_CONTACT_ADDRESS_OTHER)
+               phys_address = e_m365_contact_get_other_address (m365_contact);
+       else
+               g_warning ("%s: Uncaught field '%s'", G_STRFUNC, e_contact_vcard_attribute (field_id));
+
+       if (phys_address) {
+               EContactAddress addr;
+
+               memset (&addr, 0, sizeof (EContactAddress));
+
+               addr.locality = (gchar *) e_m365_physical_address_get_city (phys_address);
+               addr.country = (gchar *) e_m365_physical_address_get_country_or_region (phys_address);
+               addr.code = (gchar *) e_m365_physical_address_get_postal_code (phys_address);
+               addr.region = (gchar *) e_m365_physical_address_get_state (phys_address);
+               addr.street = (gchar *) e_m365_physical_address_get_street (phys_address);
+
+               if (addr.locality || addr.country || addr.code || addr.region || addr.street)
+                       e_contact_set (inout_contact, field_id, &addr);
+               else
+                       e_contact_set (inout_contact, field_id, NULL);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_address_equal (const EContactAddress *addr1,
+                               const EContactAddress *addr2)
+{
+       if (!addr1 && !addr2)
+               return TRUE;
+
+       if ((addr1 && !addr2) || (!addr1 && addr2))
+               return FALSE;
+
+       return /* g_strcmp0 (addr1->address_format, addr2->address_format) == 0 && */
+               g_strcmp0 (addr1->po, addr2->po) == 0 &&
+               g_strcmp0 (addr1->ext, addr2->ext) == 0 &&
+               g_strcmp0 (addr1->street, addr2->street) == 0 &&
+               g_strcmp0 (addr1->locality, addr2->locality) == 0 &&
+               g_strcmp0 (addr1->region, addr2->region) == 0 &&
+               g_strcmp0 (addr1->code, addr2->code) == 0 &&
+               g_strcmp0 (addr1->country, addr2->country) == 0;
+}
+
+static gboolean
+ebb_m365_contact_add_address (EBookBackendM365 *bbm365,
+                             EContact *new_contact,
+                             EContact *old_contact,
+                             EContactField field_id,
+                             const gchar *m365_id,
+                             JsonBuilder *builder,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       EContactAddress *new_addr, *old_addr;
+
+       new_addr = e_contact_get (new_contact, field_id);
+       old_addr = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!ebb_m365_contact_address_equal (new_addr, old_addr)) {
+               void (* add_func) (JsonBuilder *builder,
+                                  const gchar *city,
+                                  const gchar *country_or_region,
+                                  const gchar *postal_code,
+                                  const gchar *state,
+                                  const gchar *street) = NULL;
+
+               if (field_id == E_CONTACT_ADDRESS_WORK)
+                       add_func = e_m365_contact_add_business_address;
+               else if (field_id == E_CONTACT_ADDRESS_HOME)
+                       add_func = e_m365_contact_add_home_address;
+               else if (field_id == E_CONTACT_ADDRESS_OTHER)
+                       add_func = e_m365_contact_add_other_address;
+               else
+                       g_warning ("%s: Uncaught field '%s'", G_STRFUNC, e_contact_vcard_attribute 
(field_id));
+
+               if (add_func) {
+                       if (new_addr) {
+                               add_func (builder, new_addr->locality, new_addr->country, new_addr->code, 
new_addr->region, new_addr->street);
+                       } else {
+                               add_func (builder, NULL, NULL, NULL, NULL, NULL);
+                       }
+               }
+       }
+
+       e_contact_address_free (new_addr);
+       e_contact_address_free (old_addr);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_string_values_equal (GSList *new_values, /* const gchar * */
+                             GSList *old_values) /* const gchar * */
+{
+       GHashTable *values;
+       GSList *link;
+       gboolean equal = TRUE;
+
+       if (g_slist_length (new_values) != g_slist_length (old_values))
+               return FALSE;
+
+       values = g_hash_table_new (g_str_hash, g_str_equal);
+
+       for (link = new_values; link; link = g_slist_next (link)) {
+               gchar *value = link->data;
+
+               if (value)
+                       g_hash_table_add (values, value);
+       }
+
+       for (link = old_values; link && equal; link = g_slist_next (link)) {
+               const gchar *value = link->data;
+
+               if (value)
+                       equal = g_hash_table_remove (values, value);
+       }
+
+       equal = equal && !g_hash_table_size (values);
+
+       g_hash_table_destroy (values);
+
+       return equal;
+}
+
+static gboolean
+ebb_m365_string_list_values_equal (GList *new_values, /* const gchar * */
+                                  GList *old_values) /* const gchar * */
+{
+       GHashTable *values;
+       GList *link;
+       gboolean equal = TRUE;
+
+       if (g_list_length (new_values) != g_list_length (old_values))
+               return FALSE;
+
+       values = g_hash_table_new (g_str_hash, g_str_equal);
+
+       for (link = new_values; link; link = g_list_next (link)) {
+               gchar *value = link->data;
+
+               if (value)
+                       g_hash_table_add (values, value);
+       }
+
+       for (link = old_values; link && equal; link = g_list_next (link)) {
+               const gchar *value = link->data;
+
+               if (value)
+                       equal = g_hash_table_remove (values, value);
+       }
+
+       equal = equal && !g_hash_table_size (values);
+
+       g_hash_table_destroy (values);
+
+       return equal;
+}
+
+static gboolean
+ebb_m365_contact_get_phone (EBookBackendM365 *bbm365,
+                           EM365Contact *m365_contact,
+                           EContact *inout_contact,
+                           EContactField field_id,
+                           EM365Connection *cnc,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       JsonArray *values = NULL;
+       const gchar *type_val = NULL;
+
+       if (field_id == E_CONTACT_PHONE_BUSINESS) {
+               values = e_m365_contact_get_business_phones (m365_contact);
+               type_val = "WORK";
+       } else if (field_id == E_CONTACT_PHONE_HOME) {
+               values = e_m365_contact_get_home_phones (m365_contact);
+               type_val = "HOME";
+       } else {
+               g_warning ("%s: Uncaught field '%s'", G_STRFUNC, e_contact_vcard_attribute (field_id));
+       }
+
+       if (values) {
+               EVCard *vcard = E_VCARD (inout_contact);
+               guint ii, len;
+
+               len = json_array_get_length (values);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *str = json_array_get_string_element (values, len - ii - 1);
+
+                       if (str && *str) {
+                               EVCardAttributeParam *param;
+                               EVCardAttribute *attr;
+
+                               attr = e_vcard_attribute_new (NULL, EVC_TEL);
+                               param = e_vcard_attribute_param_new (EVC_TYPE);
+
+                               e_vcard_attribute_add_param_with_value (attr, param, type_val);
+                               e_vcard_add_attribute_with_value (vcard, attr, str);
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+static GSList * /* gchar * */
+ebb_m365_extract_phones (EContact *contact,
+                        const gchar *only_type) /* NULL for anything but known types */
+{
+       GSList *phones = NULL;
+       GList *attrs, *link;
+
+       if (!contact)
+               return NULL;
+
+       attrs = e_vcard_get_attributes (E_VCARD (contact));
+
+       for (link = attrs; link; link = g_list_next (link)) {
+               EVCardAttribute *attr = link->data;
+               gboolean use_it = FALSE;
+
+               if (!attr || !e_vcard_attribute_get_name (attr) ||
+                   g_ascii_strcasecmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+                       continue;
+
+               if (only_type) {
+                       use_it = e_vcard_attribute_has_type (attr, only_type);
+               } else {
+                       use_it = !e_vcard_attribute_has_type (attr, "WORK") &&
+                                !e_vcard_attribute_has_type (attr, "CELL");
+               }
+
+               if (use_it)
+                       phones = g_slist_prepend (phones, e_vcard_attribute_get_value (attr));
+       }
+
+       return g_slist_reverse (phones);
+}
+
+static gboolean
+ebb_m365_contact_add_phone (EBookBackendM365 *bbm365,
+                           EContact *new_contact,
+                           EContact *old_contact,
+                           EContactField field_id,
+                           const gchar *m365_id,
+                           JsonBuilder *builder,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       void (* begin_func) (JsonBuilder *builder) = NULL;
+       void (* end_func) (JsonBuilder *builder) = NULL;
+       void (* add_func) (JsonBuilder *builder, const gchar *value) = NULL;
+       const gchar *type_val = NULL;
+       GSList *new_values, *old_values;
+
+       if (field_id == E_CONTACT_PHONE_BUSINESS) {
+               begin_func = e_m365_contact_begin_business_phones;
+               end_func = e_m365_contact_end_business_phones;
+               add_func = e_m365_contact_add_business_phone;
+               type_val = "WORK";
+       } else if (field_id == E_CONTACT_PHONE_HOME) {
+               begin_func = e_m365_contact_begin_home_phones;
+               end_func = e_m365_contact_end_home_phones;
+               add_func = e_m365_contact_add_home_phone;
+               type_val = NULL; /* everything else is treated as "HOME" phone */
+       } else {
+               g_warning ("%s: Uncaught field '%s'", G_STRFUNC, e_contact_vcard_attribute (field_id));
+       }
+
+       new_values = ebb_m365_extract_phones (new_contact, type_val);
+       old_values = ebb_m365_extract_phones (old_contact, type_val);
+
+       if (!ebb_m365_string_values_equal (new_values, old_values)) {
+               GSList *link;
+
+               begin_func (builder);
+
+               for (link = new_values; link; link = g_slist_next (link)) {
+                       const gchar *value = link->data;
+
+                       add_func (builder, value);
+               }
+
+               end_func (builder);
+       }
+
+       g_slist_free_full (new_values, g_free);
+       g_slist_free_full (old_values, g_free);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_categories (EBookBackendM365 *bbm365,
+                                EM365Contact *m365_contact,
+                                EContact *inout_contact,
+                                EContactField field_id,
+                                EM365Connection *cnc,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       JsonArray *values;
+
+       values = e_m365_contact_get_categories (m365_contact);
+
+       if (values) {
+               GString *categories_str = NULL;
+               guint ii, len;
+
+               len = json_array_get_length (values);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *str = json_array_get_string_element (values, ii);
+
+                       if (str && *str) {
+                               if (!categories_str) {
+                                       categories_str = g_string_new (str);
+                               } else {
+                                       g_string_append_c (categories_str,  ',');
+                                       g_string_append (categories_str, str);
+                               }
+                       }
+               }
+
+               if (categories_str) {
+                       e_contact_set (inout_contact, field_id, categories_str->str);
+                       g_string_free (categories_str, TRUE);
+               }
+       }
+
+       return TRUE;
+}
+
+static GSList *
+ebb_m365_extract_categories (EContact *contact,
+                            EContactField field_id)
+{
+       GSList *categories = NULL;
+       const gchar *str;
+
+       if (!contact)
+               return NULL;
+
+       str = e_contact_get_const (contact, field_id);
+
+       if (str && *str) {
+               gchar **split_str;
+               gint ii;
+
+               split_str = g_strsplit (str, ",", -1);
+
+               for (ii = 0; split_str && split_str[ii]; ii++) {
+                       gchar *item = split_str[ii];
+
+                       if (item && *item)
+                               categories = g_slist_prepend (categories, item);
+                       else
+                               g_free (item);
+
+                       split_str[ii] = NULL;
+               }
+
+               g_free (split_str);
+       }
+
+       return g_slist_reverse (categories);
+}
+
+static gboolean
+ebb_m365_contact_add_categories (EBookBackendM365 *bbm365,
+                                EContact *new_contact,
+                                EContact *old_contact,
+                                EContactField field_id,
+                                const gchar *m365_id,
+                                JsonBuilder *builder,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       GSList *new_values, *old_values;
+
+       new_values = ebb_m365_extract_categories (new_contact, field_id);
+       old_values = ebb_m365_extract_categories (old_contact, field_id);
+
+       if (!ebb_m365_string_values_equal (new_values, old_values)) {
+               GSList *link;
+
+               e_m365_contact_begin_categories (builder);
+
+               for (link = new_values; link; link = g_slist_next (link)) {
+                       const gchar *value = link->data;
+
+                       e_m365_contact_add_category (builder, value);
+               }
+
+               e_m365_contact_end_categories (builder);
+       }
+
+       g_slist_free_full (new_values, g_free);
+       g_slist_free_full (old_values, g_free);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_emails (EBookBackendM365 *bbm365,
+                            EM365Contact *m365_contact,
+                            EContact *inout_contact,
+                            EContactField field_id,
+                            EM365Connection *cnc,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       JsonArray *values;
+
+       values = e_m365_contact_get_email_addresses (m365_contact);
+
+       if (values) {
+               EVCard *vcard = E_VCARD (inout_contact);
+               guint ii, len;
+
+               len = json_array_get_length (values);
+
+               for (ii = 0; ii < len; ii++) {
+                       EM365EmailAddress *address = json_array_get_object_element (values, len - ii - 1);
+
+                       if (address) {
+                               EVCardAttribute *attr;
+
+                               attr = e_vcard_attribute_new (NULL, EVC_EMAIL);
+                               e_vcard_attribute_add_param_with_value (attr, e_vcard_attribute_param_new 
(EVC_TYPE), "OTHER");
+
+                               if (g_strcmp0 (e_m365_email_address_get_name (address), 
e_m365_email_address_get_address (address)) == 0) {
+                                       e_vcard_add_attribute_with_value (vcard, attr, 
e_m365_email_address_get_address (address));
+                               } else {
+                                       gchar *formatted;
+
+                                       formatted = camel_internet_address_format_address (
+                                               e_m365_email_address_get_name (address),
+                                               e_m365_email_address_get_address (address));
+
+                                       if (formatted && *formatted)
+                                               e_vcard_add_attribute_with_value (vcard, attr, formatted);
+                                       else
+                                               e_vcard_attribute_free (attr);
+
+                                       g_free (formatted);
+                               }
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_parse_qp_email (const gchar *string,
+                        gchar **name,
+                        gchar **email)
+{
+       struct _camel_header_address *address;
+       gboolean res = FALSE;
+
+       address = camel_header_address_decode (string, "UTF-8");
+
+       if (address) {
+               /* report success only when we have filled both name and email address */
+               if (address->type == CAMEL_HEADER_ADDRESS_NAME && address->name && *address->name && 
address->v.addr && *address->v.addr) {
+                       *name = g_strdup (address->name);
+                       *email = g_strdup (address->v.addr);
+                       res = TRUE;
+               }
+
+               camel_header_address_unref (address);
+       }
+
+       if (!res) {
+               CamelInternetAddress *addr = camel_internet_address_new ();
+               const gchar *const_name = NULL, *const_email = NULL;
+
+               if (camel_address_unformat (CAMEL_ADDRESS (addr), string) == 1 &&
+                   camel_internet_address_get (addr, 0, &const_name, &const_email) &&
+                   const_name && *const_name && const_email && *const_email) {
+                       *name = g_strdup (const_name);
+                       *email = g_strdup (const_email);
+                       res = TRUE;
+               }
+
+               g_clear_object (&addr);
+       }
+
+       return res;
+}
+
+static gboolean
+ebb_m365_contact_add_emails (EBookBackendM365 *bbm365,
+                            EContact *new_contact,
+                            EContact *old_contact,
+                            EContactField field_id,
+                            const gchar *m365_id,
+                            JsonBuilder *builder,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       GList *new_values, *old_values;
+
+       new_values = e_contact_get (new_contact, field_id);
+       old_values = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!ebb_m365_string_list_values_equal (new_values, old_values)) {
+               GList *link;
+
+               e_m365_contact_begin_email_addresses (builder);
+
+               for (link = new_values; link; link = g_list_next (link)) {
+                       const gchar *value = link->data;
+                       gchar *name = NULL, *address = NULL;
+
+                       if (ebb_m365_parse_qp_email (value, &name, &address))
+                               e_m365_add_email_address (builder, NULL, name, address);
+                       else
+                               e_m365_add_email_address (builder, NULL, NULL, value);
+
+                       g_free (name);
+                       g_free (address);
+               }
+
+               e_m365_contact_end_email_addresses (builder);
+       }
+
+       g_list_free_full (new_values, g_free);
+       g_list_free_full (old_values, g_free);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_add_file_as (EBookBackendM365 *bbm365,
+                             EContact *new_contact,
+                             EContact *old_contact,
+                             EContactField field_id,
+                             const gchar *m365_id,
+                             JsonBuilder *builder,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       const gchar *new_value;
+
+       ebb_m365_contact_add_string_attribute (new_contact, old_contact, field_id, builder, 
e_m365_contact_add_file_as);
+
+       new_value = e_contact_get_const (new_contact, E_CONTACT_FILE_AS);
+
+       /* Set it always, to not be overwritten by server re-calculations on other property changes */
+       e_m365_contact_add_display_name (builder, new_value);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_generation (EBookBackendM365 *bbm365,
+                                EM365Contact *m365_contact,
+                                EContact *inout_contact,
+                                EContactField field_id,
+                                EM365Connection *cnc,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       const gchar *value;
+
+       value = e_m365_contact_get_generation (m365_contact);
+
+       if (value && *value) {
+               EContactName *name = e_contact_get (inout_contact, field_id);
+               gchar *prev;
+
+               if (!name)
+                       name = e_contact_name_new ();
+
+               prev = name->suffixes;
+               name->suffixes = (gchar *) value;
+
+               e_contact_set (inout_contact, field_id, name);
+
+               name->suffixes = prev;
+               e_contact_name_free (name);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_add_generation (EBookBackendM365 *bbm365,
+                                EContact *new_contact,
+                                EContact *old_contact,
+                                EContactField field_id,
+                                const gchar *m365_id,
+                                JsonBuilder *builder,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       EContactName *new_value, *old_value;
+
+       new_value = e_contact_get (new_contact, field_id);
+       old_value = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!(new_value && old_value && g_strcmp0 (new_value->suffixes, old_value->suffixes) == 0))
+               e_m365_contact_add_generation (builder, new_value ? new_value->suffixes : NULL);
+
+       e_contact_name_free (new_value);
+       e_contact_name_free (old_value);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_im_addresses (EBookBackendM365 *bbm365,
+                                  EM365Contact *m365_contact,
+                                  EContact *inout_contact,
+                                  EContactField field_id,
+                                  EM365Connection *cnc,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       JsonArray *values;
+
+       values = e_m365_contact_get_im_addresses (m365_contact);
+
+       if (values) {
+               EVCard *vcard = E_VCARD (inout_contact);
+               const gchar *field_name = e_contact_vcard_attribute (field_id);
+               guint ii, len;
+
+               len = json_array_get_length (values);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *str = json_array_get_string_element (values, len - ii - 1);
+
+                       if (str && *str) {
+                               EVCardAttribute *attr;
+
+                               attr = e_vcard_attribute_new (NULL, field_name);
+
+                               e_vcard_add_attribute_with_value (vcard, attr, str);
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+static GSList * /* gchar * */
+ebb_m365_extract_im_addresses (EContact *contact)
+{
+       GSList *ims = NULL;
+       GList *attrs, *link;
+
+       if (!contact)
+               return NULL;
+
+       attrs = e_vcard_get_attributes (E_VCARD (contact));
+
+       for (link = attrs; link; link = g_list_next (link)) {
+               EVCardAttribute *attr = link->data;
+               const gchar *name;
+
+               if (!attr)
+                       continue;
+
+               name = e_vcard_attribute_get_name (attr);
+
+               if (!name || (
+                   g_ascii_strcasecmp (name, EVC_X_GOOGLE_TALK) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_SKYPE) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_GADUGADU) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_AIM) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_GROUPWISE) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_JABBER) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_YAHOO) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_MSN) != 0 &&
+                   g_ascii_strcasecmp (name, EVC_X_ICQ) != 0))
+                       continue;
+
+               ims = g_slist_prepend (ims, e_vcard_attribute_get_value (attr));
+       }
+
+       return g_slist_reverse (ims);
+}
+
+static gboolean
+ebb_m365_contact_add_im_addresses (EBookBackendM365 *bbm365,
+                                  EContact *new_contact,
+                                  EContact *old_contact,
+                                  EContactField field_id,
+                                  const gchar *m365_id,
+                                  JsonBuilder *builder,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       GSList *new_values, *old_values;
+
+       new_values = ebb_m365_extract_im_addresses (new_contact);
+       old_values = ebb_m365_extract_im_addresses (old_contact);
+
+       if (!ebb_m365_string_values_equal (new_values, old_values)) {
+               GSList *link;
+
+               e_m365_contact_begin_im_addresses (builder);
+
+               for (link = new_values; link; link = g_slist_next (link)) {
+                       const gchar *value = link->data;
+
+                       if (value && *value)
+                               e_m365_contact_add_im_address (builder, value);
+               }
+
+               e_m365_contact_end_im_addresses (builder);
+       }
+
+       g_slist_free_full (new_values, g_free);
+       g_slist_free_full (old_values, g_free);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_middle_name (EBookBackendM365 *bbm365,
+                                 EM365Contact *m365_contact,
+                                 EContact *inout_contact,
+                                 EContactField field_id,
+                                 EM365Connection *cnc,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       const gchar *value;
+
+       value = e_m365_contact_get_middle_name (m365_contact);
+
+       if (value && *value) {
+               EContactName *name = e_contact_get (inout_contact, field_id);
+               gchar *prev;
+
+               if (!name)
+                       name = e_contact_name_new ();
+
+               prev = name->additional;
+               name->additional = (gchar *) value;
+
+               e_contact_set (inout_contact, field_id, name);
+
+               name->additional = prev;
+               e_contact_name_free (name);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_add_middle_name (EBookBackendM365 *bbm365,
+                                 EContact *new_contact,
+                                 EContact *old_contact,
+                                 EContactField field_id,
+                                 const gchar *m365_id,
+                                 JsonBuilder *builder,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       EContactName *new_value, *old_value;
+
+       new_value = e_contact_get (new_contact, field_id);
+       old_value = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!(new_value && old_value && g_strcmp0 (new_value->additional, old_value->additional) == 0))
+               e_m365_contact_add_middle_name (builder, new_value ? new_value->additional : NULL);
+
+       e_contact_name_free (new_value);
+       e_contact_name_free (old_value);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_title (EBookBackendM365 *bbm365,
+                           EM365Contact *m365_contact,
+                           EContact *inout_contact,
+                           EContactField field_id,
+                           EM365Connection *cnc,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       const gchar *value;
+
+       value = e_m365_contact_get_title (m365_contact);
+
+       if (value && *value) {
+               EContactName *name = e_contact_get (inout_contact, field_id);
+               gchar *prev;
+
+               if (!name)
+                       name = e_contact_name_new ();
+
+               prev = name->prefixes;
+               name->prefixes = (gchar *) value;
+
+               e_contact_set (inout_contact, field_id, name);
+
+               name->prefixes = prev;
+               e_contact_name_free (name);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_add_title (EBookBackendM365 *bbm365,
+                           EContact *new_contact,
+                           EContact *old_contact,
+                           EContactField field_id,
+                           const gchar *m365_id,
+                           JsonBuilder *builder,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       EContactName *new_value, *old_value;
+
+       new_value = e_contact_get (new_contact, field_id);
+       old_value = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!(new_value && old_value && g_strcmp0 (new_value->prefixes, old_value->prefixes) == 0))
+               e_m365_contact_add_title (builder, new_value ? new_value->prefixes : NULL);
+
+       e_contact_name_free (new_value);
+       e_contact_name_free (old_value);
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_get_photo (EBookBackendM365 *bbm365,
+                           EM365Contact *m365_contact,
+                           EContact *inout_contact,
+                           EContactField field_id,
+                           EM365Connection *cnc,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       GByteArray *photo_data = NULL;
+       GError *local_error = NULL;
+
+       LOCK (bbm365);
+
+       if (e_m365_connection_get_contact_photo_sync (cnc, NULL, bbm365->priv->folder_id,
+               e_m365_contact_get_id (m365_contact), &photo_data, cancellable, &local_error) &&
+           photo_data && photo_data->len) {
+               EContactPhoto *photo;
+
+               photo = e_contact_photo_new ();
+               e_contact_photo_set_inlined (photo, photo_data->data, photo_data->len);
+               e_contact_photo_set_mime_type (photo, "image/jpeg");
+               e_contact_set (inout_contact, field_id, photo);
+               e_contact_photo_free (photo);
+       }
+
+       UNLOCK (bbm365);
+
+       if (photo_data)
+               g_byte_array_unref (photo_data);
+       g_clear_error (&local_error);
+
+       /* Even it could fail, ignore it and read as many contacts as possible, rather than stop on the first 
error */
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_contact_photo_equal (EContactPhoto *photo1,
+                             EContactPhoto *photo2)
+{
+       const guchar *data1, *data2;
+       gsize len1 = 0, len2 = 0;
+
+       if (!photo1 && !photo2)
+               return TRUE;
+
+       if ((photo1 && !photo2) || (!photo1 && photo2))
+               return FALSE;
+
+       data1 = e_contact_photo_get_inlined (photo1, &len1);
+       data2 = e_contact_photo_get_inlined (photo2, &len2);
+
+       if (!data1 && !data2)
+               return TRUE;
+
+       return len1 == len2 &&
+               memcmp (data1, data2, len1) == 0;
+}
+
+static gboolean
+ebb_m365_contact_add_photo (EBookBackendM365 *bbm365,
+                           EContact *new_contact,
+                           EContact *old_contact,
+                           EContactField field_id,
+                           const gchar *m365_id,
+                           JsonBuilder *builder,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       EContactPhoto *new_value, *old_value;
+
+       new_value = e_contact_get (new_contact, field_id);
+       old_value = old_contact ? e_contact_get (old_contact, field_id) : NULL;
+
+       if (!ebb_m365_contact_photo_equal (new_value, old_value)) {
+               GByteArray *jpeg_photo = NULL, tmp;
+               GError *local_error = NULL;
+
+               if (new_value) {
+                       gsize len = 0;
+
+                       tmp.data = (guchar *) e_contact_photo_get_inlined (new_value, &len);
+
+                       if (len && tmp.data) {
+                               tmp.len = len;
+                               jpeg_photo = &tmp;
+                       }
+               }
+
+               LOCK (bbm365);
+
+               if (!e_m365_connection_update_contact_photo_sync (bbm365->priv->cnc, NULL, 
bbm365->priv->folder_id,
+                       m365_id ? m365_id : e_contact_get_const (new_contact, E_CONTACT_UID), jpeg_photo, 
cancellable, &local_error)) {
+                       if (local_error) {
+                               g_propagate_error (error, local_error);
+                               local_error = NULL;
+                       }
+               }
+
+               UNLOCK (bbm365);
+
+               g_clear_error (&local_error);
+       }
+
+       e_contact_photo_free (new_value);
+       e_contact_photo_free (old_value);
+
+       return TRUE;
+}
+
+#define STRING_FIELD(fldid, getfn, addfn) { fldid, FALSE, getfn, NULL, addfn, NULL }
+#define COMPLEX_FIELD(fldid, getfn, addfn) { fldid, FALSE, NULL, getfn, NULL, addfn }
+#define COMPLEX_FIELD_2(fldid, getfn, addfn) { fldid, TRUE, NULL, getfn, NULL, addfn }
+#define COMPLEX_ADDFN(fldid, getfn, addfn) { fldid, FALSE, getfn, NULL, NULL, addfn }
+
+struct _mappings {
+       EContactField field_id;
+       gboolean add_in_second_go;
+       const gchar *   (* m365_get_func)       (EM365Contact *m365_contact);
+       gboolean        (* get_func)            (EBookBackendM365 *bbm365,
+                                                EM365Contact *m365_contact,
+                                                EContact *inout_contact,
+                                                EContactField field_id,
+                                                EM365Connection *cnc,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       void            (* m365_add_func)       (JsonBuilder *builder,
+                                                const gchar *value);
+       gboolean        (* add_func)            (EBookBackendM365 *bbm365,
+                                                EContact *new_contact,
+                                                EContact *old_contact, /* nullable */
+                                                EContactField field_id,
+                                                const gchar *m365_id,
+                                                JsonBuilder *builder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+} mappings[] = {
+       STRING_FIELD    (E_CONTACT_UID,                 e_m365_contact_get_id,                  NULL),
+       COMPLEX_FIELD   (E_CONTACT_REV,                 ebb_m365_contact_get_rev,               NULL),
+       STRING_FIELD    (E_CONTACT_ASSISTANT,           e_m365_contact_get_assistant_name,      
e_m365_contact_add_assistant_name),
+       COMPLEX_FIELD   (E_CONTACT_BIRTH_DATE,          ebb_m365_contact_get_birthday,          
ebb_m365_contact_add_birthday),
+       COMPLEX_FIELD   (E_CONTACT_ADDRESS_WORK,        ebb_m365_contact_get_address,           
ebb_m365_contact_add_address),
+       STRING_FIELD    (E_CONTACT_HOMEPAGE_URL,        e_m365_contact_get_business_home_page,  
e_m365_contact_add_business_home_page),
+       COMPLEX_FIELD   (E_CONTACT_PHONE_BUSINESS,      ebb_m365_contact_get_phone,             
ebb_m365_contact_add_phone),
+       COMPLEX_FIELD   (E_CONTACT_CATEGORIES,          ebb_m365_contact_get_categories,        
ebb_m365_contact_add_categories),
+       STRING_FIELD    (E_CONTACT_ORG,                 e_m365_contact_get_company_name,        
e_m365_contact_add_company_name),
+       STRING_FIELD    (E_CONTACT_ORG_UNIT,            e_m365_contact_get_department,          
e_m365_contact_add_department),
+       COMPLEX_FIELD   (E_CONTACT_EMAIL,               ebb_m365_contact_get_emails,            
ebb_m365_contact_add_emails),
+       COMPLEX_FIELD   (E_CONTACT_NAME,                ebb_m365_contact_get_generation,        
ebb_m365_contact_add_generation),
+       STRING_FIELD    (E_CONTACT_GIVEN_NAME,          e_m365_contact_get_given_name,          
e_m365_contact_add_given_name),
+       COMPLEX_FIELD   (E_CONTACT_ADDRESS_HOME,        ebb_m365_contact_get_address,           
ebb_m365_contact_add_address),
+       COMPLEX_FIELD   (E_CONTACT_PHONE_HOME,          ebb_m365_contact_get_phone,             
ebb_m365_contact_add_phone),
+       COMPLEX_FIELD   (E_CONTACT_IM_MSN,              ebb_m365_contact_get_im_addresses,      
ebb_m365_contact_add_im_addresses),
+       /* STRING_FIELD (???,                           e_m365_contact_get_initials,            
e_m365_contact_add_initials), */
+       STRING_FIELD    (E_CONTACT_TITLE,               e_m365_contact_get_job_title,           
e_m365_contact_add_job_title),
+       STRING_FIELD    (E_CONTACT_MANAGER,             e_m365_contact_get_manager,             
e_m365_contact_add_manager),
+       COMPLEX_FIELD   (E_CONTACT_NAME,                ebb_m365_contact_get_middle_name,       
ebb_m365_contact_add_middle_name),
+       STRING_FIELD    (E_CONTACT_PHONE_MOBILE,        e_m365_contact_get_mobile_phone,        
e_m365_contact_add_mobile_phone),
+       STRING_FIELD    (E_CONTACT_NICKNAME,            e_m365_contact_get_nick_name,           
e_m365_contact_add_nick_name),
+       STRING_FIELD    (E_CONTACT_OFFICE,              e_m365_contact_get_office_location,     
e_m365_contact_add_office_location),
+       COMPLEX_FIELD   (E_CONTACT_ADDRESS_OTHER,       ebb_m365_contact_get_address,           
ebb_m365_contact_add_address),
+       STRING_FIELD    (E_CONTACT_NOTE,                e_m365_contact_get_personal_notes,      
e_m365_contact_add_personal_notes),
+       STRING_FIELD    (E_CONTACT_ROLE,                e_m365_contact_get_profession,          
e_m365_contact_add_profession),
+       STRING_FIELD    (E_CONTACT_SPOUSE,              e_m365_contact_get_spouse_name,         
e_m365_contact_add_spouse_name),
+       STRING_FIELD    (E_CONTACT_FAMILY_NAME,         e_m365_contact_get_surname,             
e_m365_contact_add_surname),
+       COMPLEX_FIELD   (E_CONTACT_NAME,                ebb_m365_contact_get_title,             
ebb_m365_contact_add_title),
+       /* STRING_FIELD (???,                           e_m365_contact_get_yomi_company_name,   
e_m365_contact_add_yomi_company_name), */
+       /* STRING_FIELD (???,                           e_m365_contact_get_yomi_given_name,     
e_m365_contact_add_yomi_given_name), */
+       /* STRING_FIELD (???,                           e_m365_contact_get_yomi_surname,        
e_m365_contact_add_yomi_surname), */
+       COMPLEX_ADDFN   (E_CONTACT_FILE_AS,             e_m365_contact_get_file_as,             
ebb_m365_contact_add_file_as),
+       COMPLEX_FIELD_2 (E_CONTACT_PHOTO,               ebb_m365_contact_get_photo,             
ebb_m365_contact_add_photo)
+};
+
+static EContact *
+ebb_m365_json_contact_to_vcard (EBookBackendM365 *bbm365,
+                               EM365Contact *m365_contact,
+                               EM365Connection *cnc,
+                               gchar **out_object,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       EContact *contact;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (m365_contact != NULL, NULL);
+       g_return_val_if_fail (out_object != NULL, NULL);
+
+       *out_object = NULL;
+
+       contact = e_contact_new ();
+
+       for (ii = 0; success && ii < G_N_ELEMENTS (mappings); ii++) {
+               if (mappings[ii].m365_get_func) {
+                       ebb_m365_contact_get_string_attribute (m365_contact, contact, mappings[ii].field_id, 
mappings[ii].m365_get_func);
+               } else if (mappings[ii].get_func) {
+                       success = mappings[ii].get_func (bbm365, m365_contact, contact, 
mappings[ii].field_id, cnc, cancellable, error);
+               }
+       }
+
+       if (success)
+               *out_object = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+       else
+               g_clear_object (&contact);
+
+       return contact;
+}
+
+static JsonBuilder *
+ebb_m365_contact_to_json_locked (EBookBackendM365 *bbm365,
+                                EContact *new_contact,
+                                EContact *old_contact, /* nullable */
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       JsonBuilder *builder;
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (new_contact != NULL, NULL);
+
+       builder = json_builder_new_immutable ();
+       e_m365_json_begin_object_member (builder, NULL);
+
+       for (ii = 0; success && ii < G_N_ELEMENTS (mappings); ii++) {
+               if (mappings[ii].m365_add_func) {
+                       ebb_m365_contact_add_string_attribute (new_contact, old_contact, 
mappings[ii].field_id, builder, mappings[ii].m365_add_func);
+               } else if (!mappings[ii].add_in_second_go && mappings[ii].add_func) {
+                       success = mappings[ii].add_func (bbm365, new_contact, old_contact, 
mappings[ii].field_id, NULL, builder, cancellable, error);
+               }
+       }
+
+       e_m365_json_end_object_member (builder);
+
+       if (!success)
+               g_clear_object (&builder);
+
+       return builder;
+}
+
+static gboolean
+ebb_m365_contact_to_json_2nd_go_locked (EBookBackendM365 *bbm365,
+                                       EContact *new_contact,
+                                       EContact *old_contact, /* nullable */
+                                       const gchar *m365_id,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       gint ii;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (new_contact != NULL, FALSE);
+
+       for (ii = 0; success && ii < G_N_ELEMENTS (mappings); ii++) {
+               if (mappings[ii].add_in_second_go && mappings[ii].add_func) {
+                       success = mappings[ii].add_func (bbm365, new_contact, old_contact, 
mappings[ii].field_id, m365_id, NULL, cancellable, error);
+               }
+       }
+
+       return success;
+}
+
+static void
+ebb_m365_convert_error_to_client_error (GError **perror)
+{
+       GError *error = NULL;
+
+       if (!perror || !*perror || (*perror)->domain == E_CLIENT_ERROR || (*perror)->domain == 
E_BOOK_CLIENT_ERROR)
+               return;
+
+       /*if ((*perror)->domain == EWS_CONNECTION_ERROR) {
+               switch ((*perror)->code) {
+               case EWS_CONNECTION_ERROR_AUTHENTICATION_FAILED:
+                       error = EC_ERROR_EX (E_CLIENT_ERROR_AUTHENTICATION_FAILED, (*perror)->message);
+                       break;
+               case EWS_CONNECTION_ERROR_FOLDERNOTFOUND:
+               case EWS_CONNECTION_ERROR_MANAGEDFOLDERNOTFOUND:
+               case EWS_CONNECTION_ERROR_PARENTFOLDERNOTFOUND:
+               case EWS_CONNECTION_ERROR_PUBLICFOLDERSERVERNOTFOUND:
+                       error = EBC_ERROR_EX (E_BOOK_CLIENT_ERROR_NO_SUCH_BOOK, (*perror)->message);
+                       break;
+               case EWS_CONNECTION_ERROR_EVENTNOTFOUND:
+               case EWS_CONNECTION_ERROR_ITEMNOTFOUND:
+                       error = EBC_ERROR_EX (E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND, (*perror)->message);
+                       break;
+               case EWS_CONNECTION_ERROR_UNAVAILABLE:
+                       g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND, 
(*perror)->message);
+                       break;
+               }
+
+               if (!error)
+                       error = EC_ERROR_EX (E_CLIENT_ERROR_OTHER_ERROR, (*perror)->message);
+       }*/
+
+       if (error) {
+               g_error_free (*perror);
+               *perror = error;
+       }
+}
+
+static void
+ebb_m365_maybe_disconnect_sync (EBookBackendM365 *bbm365,
+                               GError **in_perror,
+                               GCancellable *cancellable)
+{
+       g_return_if_fail (E_IS_BOOK_BACKEND_M365 (bbm365));
+
+       if (in_perror && g_error_matches (*in_perror, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_FAILED)) {
+               e_book_meta_backend_disconnect_sync (E_BOOK_META_BACKEND (bbm365), cancellable, NULL);
+               e_backend_schedule_credentials_required (E_BACKEND (bbm365), 
E_SOURCE_CREDENTIALS_REASON_REJECTED, NULL, 0, NULL, NULL, G_STRFUNC);
+       }
+}
+
+static gboolean
+ebb_m365_unset_connection_sync (EBookBackendM365 *bbm365,
+                               gboolean is_disconnect,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (bbm365), FALSE);
+
+       LOCK (bbm365);
+
+       if (bbm365->priv->cnc) {
+               if (is_disconnect)
+                       success = e_m365_connection_disconnect_sync (bbm365->priv->cnc, cancellable, error);
+       }
+
+       g_clear_object (&bbm365->priv->cnc);
+       g_clear_pointer (&bbm365->priv->folder_id, g_free);
+
+       UNLOCK (bbm365);
+
+       return success;
+}
+
+static gboolean
+ebb_m365_connect_sync (EBookMetaBackend *meta_backend,
+                      const ENamedParameters *credentials,
+                      ESourceAuthenticationResult *out_auth_result,
+                      gchar **out_certificate_pem,
+                      GTlsCertificateFlags *out_certificate_errors,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       EBookBackendM365 *bbm365;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
+
+       bbm365 = E_BOOK_BACKEND_M365 (meta_backend);
+
+       LOCK (bbm365);
+
+       if (bbm365->priv->cnc) {
+               UNLOCK (bbm365);
+
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+
+               return TRUE;
+       } else {
+               EBackend *backend;
+               ESourceRegistry *registry;
+               ESource *source;
+               EM365Connection *cnc = NULL;
+               ESourceM365Folder *m365_folder_extension;
+               CamelM365Settings *m365_settings;
+               gchar *folder_id;
+
+               backend = E_BACKEND (bbm365);
+               source = e_backend_get_source (backend);
+               registry = e_book_backend_get_registry (E_BOOK_BACKEND (bbm365));
+               m365_settings = camel_m365_settings_get_from_backend (backend, registry);
+               g_warn_if_fail (m365_settings != NULL);
+
+               m365_folder_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_M365_FOLDER);
+               folder_id = e_source_m365_folder_dup_id (m365_folder_extension);
+
+               if (folder_id) {
+                       cnc = e_m365_connection_new_for_backend (backend, registry, source, m365_settings);
+
+                       *out_auth_result = e_m365_connection_authenticate_sync (cnc, NULL, 
E_M365_FOLDER_KIND_CONTACTS, NULL, folder_id,
+                               out_certificate_pem, out_certificate_errors, cancellable, error);
+
+                       if (*out_auth_result == E_SOURCE_AUTHENTICATION_ACCEPTED) {
+                               bbm365->priv->cnc = g_object_ref (cnc);
+
+                               g_warn_if_fail (bbm365->priv->folder_id == NULL);
+
+                               g_free (bbm365->priv->folder_id);
+                               bbm365->priv->folder_id = folder_id;
+
+                               folder_id = NULL;
+                               success = TRUE;
+
+                               e_book_backend_set_writable (E_BOOK_BACKEND (bbm365), TRUE);
+                       }
+               } else {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+                       g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_OTHER_ERROR, _("Folder ID is 
not set")));
+               }
+
+               g_clear_object (&cnc);
+               g_free (folder_id);
+       }
+
+       UNLOCK (bbm365);
+
+       ebb_m365_convert_error_to_client_error (error);
+
+       return success;
+}
+
+static gboolean
+ebb_m365_disconnect_sync (EBookMetaBackend *meta_backend,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+
+       return ebb_m365_unset_connection_sync (E_BOOK_BACKEND_M365 (meta_backend), TRUE, cancellable, error);
+}
+
+typedef struct _ObjectsDeltaData {
+       EBookBackendM365 *bbm365;
+       ECache *cache;
+       GSList **out_created_objects;
+       GSList **out_modified_objects;
+       GSList **out_removed_objects;
+} ObjectsDeltaData;
+
+static gboolean
+ebb_m365_get_objects_delta_cb (EM365Connection *cnc,
+                              const GSList *results, /* JsonObject * - the returned objects from the server 
*/
+                              gpointer user_data,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       ObjectsDeltaData *odd = user_data;
+       GSList *link;
+
+       g_return_val_if_fail (odd != NULL, FALSE);
+
+       for (link = (GSList *) results; link && !g_cancellable_is_cancelled (cancellable); link = 
g_slist_next (link)) {
+               EM365Contact *contact = link->data;
+               const gchar *id;
+
+               if (!contact)
+                       continue;
+
+               id = e_m365_contact_get_id (contact);
+
+               if (!id)
+                       continue;
+
+               if (e_m365_delta_is_removed_object (contact)) {
+                       *(odd->out_removed_objects) = g_slist_prepend (*(odd->out_removed_objects),
+                               e_book_meta_backend_info_new (id, NULL, NULL, NULL));
+               } else {
+                       GSList **out_slist;
+                       EContact *vcard;
+                       gchar *object;
+
+                       if (e_cache_contains (odd->cache, id, E_CACHE_INCLUDE_DELETED))
+                               out_slist = odd->out_modified_objects;
+                       else
+                               out_slist = odd->out_created_objects;
+
+                       vcard = ebb_m365_json_contact_to_vcard (odd->bbm365, contact, cnc, &object, 
cancellable, error);
+
+                       g_clear_object (&vcard);
+
+                       if (!g_cancellable_is_cancelled (cancellable))
+                               g_warn_if_fail (object != NULL);
+
+                       if (object) {
+                               EBookMetaBackendInfo *nfo;
+
+                               nfo = e_book_meta_backend_info_new (id,
+                                       e_m365_contact_get_change_key (contact),
+                                       object, NULL);
+
+                               nfo->extra = object; /* assumes ownership, to avoid unnecessary re-allocation 
*/
+
+                               *out_slist = g_slist_prepend (*out_slist, nfo);
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ebb_m365_get_changes_sync (EBookMetaBackend *meta_backend,
+                          const gchar *last_sync_tag,
+                          gboolean is_repeat,
+                          gchar **out_new_sync_tag,
+                          gboolean *out_repeat,
+                          GSList **out_created_objects,
+                          GSList **out_modified_objects,
+                          GSList **out_removed_objects,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       EBookBackendM365 *bbm365;
+       EBookCache *book_cache;
+       ObjectsDeltaData odd;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_repeat != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
+
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       bbm365 = E_BOOK_BACKEND_M365 (meta_backend);
+
+       book_cache = e_book_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+
+       odd.bbm365 = bbm365;
+       odd.cache = E_CACHE (book_cache);
+       odd.out_created_objects = out_created_objects;
+       odd.out_modified_objects = out_modified_objects;
+       odd.out_removed_objects = out_removed_objects;
+
+       LOCK (bbm365);
+
+       success = e_m365_connection_get_objects_delta_sync (bbm365->priv->cnc, NULL,
+               E_M365_FOLDER_KIND_CONTACTS, bbm365->priv->folder_id, NULL, last_sync_tag, 0,
+               ebb_m365_get_objects_delta_cb, &odd,
+               out_new_sync_tag, cancellable, &local_error);
+
+       if (e_m365_connection_util_delta_token_failed (local_error)) {
+               GSList *known_uids = NULL, *link;
+
+               g_clear_error (&local_error);
+
+               if (e_book_cache_search_uids (book_cache, NULL, &known_uids, cancellable, error)) {
+                       for (link = known_uids; link; link = g_slist_next (link)) {
+                               const gchar *uid = link->data;
+
+                               if (uid) {
+                                       *out_removed_objects = g_slist_prepend (*out_removed_objects,
+                                               e_book_meta_backend_info_new (uid, NULL, NULL, NULL));
+                               }
+                       }
+               }
+
+               e_cache_remove_all (E_CACHE (book_cache), cancellable, NULL);
+
+               g_slist_free_full (known_uids, g_free);
+
+               success = e_m365_connection_get_objects_delta_sync (bbm365->priv->cnc, NULL,
+                       E_M365_FOLDER_KIND_CONTACTS, bbm365->priv->folder_id, NULL, NULL, 0,
+                       ebb_m365_get_objects_delta_cb, &odd,
+                       out_new_sync_tag, cancellable, &local_error);
+       } else if (local_error) {
+               g_propagate_error (error, local_error);
+       }
+
+       UNLOCK (bbm365);
+
+       ebb_m365_convert_error_to_client_error (error);
+       ebb_m365_maybe_disconnect_sync (bbm365, error, cancellable);
+
+       g_clear_object (&book_cache);
+
+       return success;
+}
+
+static gboolean
+ebb_m365_load_contact_sync (EBookMetaBackend *meta_backend,
+                           const gchar *uid,
+                           const gchar *extra,
+                           EContact **out_contact,
+                           gchar **out_extra,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       EBookBackendM365 *bbm365;
+       EM365Contact *contact = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (out_contact, FALSE);
+       g_return_val_if_fail (out_extra != NULL, FALSE);
+
+       bbm365 = E_BOOK_BACKEND_M365 (meta_backend);
+
+       LOCK (bbm365);
+
+       success = e_m365_connection_get_contact_sync (bbm365->priv->cnc, NULL,
+               bbm365->priv->folder_id, uid, &contact, cancellable, error);
+
+       if (success) {
+               *out_contact = ebb_m365_json_contact_to_vcard (bbm365, contact, bbm365->priv->cnc, out_extra, 
cancellable, error);
+
+               if (contact)
+                       json_object_unref (contact);
+       }
+
+       UNLOCK (bbm365);
+
+       ebb_m365_convert_error_to_client_error (error);
+       ebb_m365_maybe_disconnect_sync (bbm365, error, cancellable);
+
+       return success;
+}
+
+static gboolean
+ebb_m365_save_contact_sync (EBookMetaBackend *meta_backend,
+                           gboolean overwrite_existing,
+                           EConflictResolution conflict_resolution,
+                           /* const */ EContact *contact,
+                           const gchar *extra,
+                           guint32 opflags,
+                           gchar **out_new_uid,
+                           gchar **out_new_extra,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       EBookBackendM365 *bbm365;
+       EContact *tmp_contact = NULL, *old_contact = NULL;
+       JsonBuilder *builder;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+       g_return_val_if_fail (out_new_uid != NULL, FALSE);
+       g_return_val_if_fail (out_new_extra != NULL, FALSE);
+
+       if (GPOINTER_TO_INT (e_contact_get (contact, E_CONTACT_IS_LIST))) {
+               g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_NOT_SUPPORTED, _("Cannot save contact 
list into a Microsoft 365 address book")));
+               return FALSE;
+       }
+
+       bbm365 = E_BOOK_BACKEND_M365 (meta_backend);
+
+       LOCK (bbm365);
+
+       if (e_vcard_get_attribute (E_VCARD (contact), EVC_PHOTO)) {
+               tmp_contact = e_contact_duplicate (contact);
+               contact = tmp_contact;
+
+               e_contact_inline_local_photos (contact, NULL);
+       }
+
+       if (extra && *extra)
+               old_contact = e_contact_new_from_vcard (extra);
+
+       builder = ebb_m365_contact_to_json_locked (bbm365, contact, old_contact, cancellable, error);
+
+       if (builder) {
+               if (overwrite_existing) {
+                       const gchar *uid = e_contact_get_const (contact, E_CONTACT_UID);
+
+                       success = e_m365_connection_update_contact_sync (bbm365->priv->cnc, NULL, 
bbm365->priv->folder_id,
+                               uid, builder, cancellable, error);
+
+                       if (success)
+                               success = ebb_m365_contact_to_json_2nd_go_locked (bbm365, contact, 
old_contact, uid, cancellable, error);
+
+                       if (success)
+                               *out_new_extra = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+               } else {
+                       EM365Contact *created_contact = NULL;
+
+                       success = e_m365_connection_create_contact_sync (bbm365->priv->cnc, NULL, 
bbm365->priv->folder_id,
+                               builder, &created_contact, cancellable, error);
+
+                       if (success && created_contact) {
+                               const gchar *m365_id = e_m365_contact_get_id (created_contact);
+
+                               success = ebb_m365_contact_to_json_2nd_go_locked (bbm365, contact, 
old_contact, m365_id, cancellable, error);
+                       }
+
+                       if (success && created_contact) {
+                               EContact *vcard;
+
+                               *out_new_uid = g_strdup (e_m365_contact_get_id (created_contact));
+
+                               vcard = ebb_m365_json_contact_to_vcard (bbm365, created_contact, 
bbm365->priv->cnc, out_new_extra, cancellable, error);
+
+                               if (!vcard)
+                                       success = FALSE;
+
+                               g_clear_object (&vcard);
+                       }
+
+                       if (created_contact)
+                               json_object_unref (created_contact);
+               }
+
+               g_clear_object (&builder);
+       }
+
+       UNLOCK (bbm365);
+
+       ebb_m365_convert_error_to_client_error (error);
+       ebb_m365_maybe_disconnect_sync (bbm365, error, cancellable);
+
+       g_clear_object (&old_contact);
+       g_clear_object (&tmp_contact);
+
+       return success;
+}
+
+static gboolean
+ebb_m365_remove_contact_sync (EBookMetaBackend *meta_backend,
+                             EConflictResolution conflict_resolution,
+                             const gchar *uid,
+                             const gchar *extra,
+                             const gchar *object,
+                             guint32 opflags,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       EBookBackendM365 *bbm365;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+
+       bbm365 = E_BOOK_BACKEND_M365 (meta_backend);
+
+       LOCK (bbm365);
+
+       success = e_m365_connection_delete_contact_sync (bbm365->priv->cnc, NULL,
+               bbm365->priv->folder_id, uid, cancellable, error);
+
+       UNLOCK (bbm365);
+
+       ebb_m365_convert_error_to_client_error (error);
+       ebb_m365_maybe_disconnect_sync (bbm365, error, cancellable);
+
+       return success;
+}
+
+static gboolean
+ebb_m365_search_sync (EBookMetaBackend *meta_backend,
+                     const gchar *expr,
+                     gboolean meta_contact,
+                     GSList **out_contacts,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+
+       /* Ignore errors, just try its best */
+       /*ebb_m365_update_cache_for_expression (E_BOOK_BACKEND_M365 (meta_backend), expr, cancellable, 
NULL);*/
+
+       /* Chain up to parent's method */
+       return E_BOOK_META_BACKEND_CLASS (e_book_backend_m365_parent_class)->search_sync (meta_backend, expr, 
meta_contact, out_contacts, cancellable, error);
+}
+
+static gboolean
+ebb_m365_search_uids_sync (EBookMetaBackend *meta_backend,
+                          const gchar *expr,
+                          GSList **out_uids,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (meta_backend), FALSE);
+
+       /* Ignore errors, just try its best */
+       /*ebb_m365_update_cache_for_expression (E_BOOK_BACKEND_M365 (meta_backend), expr, cancellable, 
NULL);*/
+
+       /* Chain up to parent's method */
+       return E_BOOK_META_BACKEND_CLASS (e_book_backend_m365_parent_class)->search_uids_sync (meta_backend, 
expr,
+               out_uids, cancellable, error);
+}
+
+static gchar *
+ebb_m365_get_backend_property (EBookBackend *book_backend,
+                              const gchar *prop_name)
+{
+       g_return_val_if_fail (E_IS_BOOK_BACKEND_M365 (book_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strjoin (",",
+                       "net",
+                       "contact-lists",
+                       "do-initial-query",
+                       e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+                       NULL);
+       } else if (g_str_equal (prop_name, E_BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
+               return g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));
+       } else if (g_str_equal (prop_name, E_BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
+               GString *buffer;
+               gchar *fields;
+               gint ii;
+
+               buffer = g_string_sized_new (1024);
+
+               for (ii = 0; ii < G_N_ELEMENTS (mappings); ii++) {
+                       if (buffer->len > 0)
+                               g_string_append_c (buffer, ',');
+
+                       g_string_append (buffer, e_contact_field_name (mappings[ii].field_id));
+               }
+
+               fields = g_strjoin (
+                       ",",
+                       buffer->str,
+                       e_contact_field_name (E_CONTACT_FULL_NAME),
+                       e_contact_field_name (E_CONTACT_EMAIL_1),
+                       e_contact_field_name (E_CONTACT_EMAIL_2),
+                       e_contact_field_name (E_CONTACT_EMAIL_3),
+                       e_contact_field_name (E_CONTACT_EMAIL_4),
+                       NULL);
+
+               g_string_free (buffer, TRUE);
+
+               return fields;
+       }
+
+       /* Chain up to parent's method. */
+       return E_BOOK_BACKEND_CLASS (e_book_backend_m365_parent_class)->impl_get_backend_property 
(book_backend, prop_name);
+}
+
+static gboolean
+ebb_m365_get_destination_address (EBackend *backend,
+                                 gchar **host,
+                                 guint16 *port)
+{
+       g_return_val_if_fail (port != NULL, FALSE);
+       g_return_val_if_fail (host != NULL, FALSE);
+
+       /* Sanity checking */
+       if (!e_book_backend_get_registry (E_BOOK_BACKEND (backend)) ||
+           !e_backend_get_source (backend))
+               return FALSE;
+
+       *host = g_strdup ("graph.microsoft.com");
+       *port = 443;
+
+       return TRUE;
+}
+
+static void
+e_book_backend_m365_dispose (GObject *object)
+{
+       EBookBackendM365 *bbm365 = E_BOOK_BACKEND_M365 (object);
+
+       ebb_m365_unset_connection_sync (bbm365, FALSE, NULL, NULL);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_m365_parent_class)->dispose (object);
+}
+
+static void
+e_book_backend_m365_finalize (GObject *object)
+{
+       EBookBackendM365 *bbm365 = E_BOOK_BACKEND_M365 (object);
+
+       g_rec_mutex_clear (&bbm365->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_book_backend_m365_parent_class)->finalize (object);
+}
+
+static void
+e_book_backend_m365_init (EBookBackendM365 *bbm365)
+{
+       bbm365->priv = e_book_backend_m365_get_instance_private (bbm365);
+
+       g_rec_mutex_init (&bbm365->priv->property_lock);
+}
+
+static void
+e_book_backend_m365_class_init (EBookBackendM365Class *klass)
+{
+       GObjectClass *object_class;
+       EBackendClass *backend_class;
+       EBookBackendClass *book_backend_class;
+       EBookMetaBackendClass *book_meta_backend_class;
+
+       book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+       book_meta_backend_class->backend_module_filename = "libebookbackendmicrosoft365.so";
+       book_meta_backend_class->backend_factory_type_name = "EBookBackendM365Factory";
+       book_meta_backend_class->connect_sync = ebb_m365_connect_sync;
+       book_meta_backend_class->disconnect_sync = ebb_m365_disconnect_sync;
+       book_meta_backend_class->get_changes_sync = ebb_m365_get_changes_sync;
+       book_meta_backend_class->load_contact_sync = ebb_m365_load_contact_sync;
+       book_meta_backend_class->save_contact_sync = ebb_m365_save_contact_sync;
+       book_meta_backend_class->remove_contact_sync = ebb_m365_remove_contact_sync;
+       book_meta_backend_class->search_sync = ebb_m365_search_sync;
+       book_meta_backend_class->search_uids_sync = ebb_m365_search_uids_sync;
+
+       book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+       book_backend_class->impl_get_backend_property = ebb_m365_get_backend_property;
+
+       backend_class = E_BACKEND_CLASS (klass);
+       backend_class->get_destination_address = ebb_m365_get_destination_address;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->dispose = e_book_backend_m365_dispose;
+       object_class->finalize = e_book_backend_m365_finalize;
+}
diff --git a/src/Microsoft365/addressbook/e-book-backend-m365.h 
b/src/Microsoft365/addressbook/e-book-backend-m365.h
new file mode 100644
index 00000000..4639648b
--- /dev/null
+++ b/src/Microsoft365/addressbook/e-book-backend-m365.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_BOOK_BACKEND_M365_H
+#define E_BOOK_BACKEND_M365_H
+
+#include <libedata-book/libedata-book.h>
+
+#define E_TYPE_BOOK_BACKEND_M365         (e_book_backend_m365_get_type ())
+#define E_BOOK_BACKEND_M365(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), E_TYPE_BOOK_BACKEND_M365, 
EBookBackendM365))
+#define E_BOOK_BACKEND_M365_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), E_TYPE_BOOK_BACKEND_M365, 
EBookBackendM365Class))
+#define E_IS_BOOK_BACKEND_M365(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), E_TYPE_BOOK_BACKEND_M365))
+#define E_IS_BOOK_BACKEND_M365_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), E_TYPE_BOOK_BACKEND_M365))
+#define E_BOOK_BACKEND_M365_GET_CLASS(k) (G_TYPE_INSTANCE_GET_CLASS ((obj), E_TYPE_BOOK_BACKEND_M365, 
EBookBackenM365Class))
+
+typedef struct _EBookBackendM365Private EBookBackendM365Private;
+
+typedef struct {
+       EBookMetaBackend parent_object;
+       EBookBackendM365Private *priv;
+} EBookBackendM365;
+
+typedef struct {
+       EBookMetaBackendClass parent_class;
+} EBookBackendM365Class;
+
+GType       e_book_backend_m365_get_type (void);
+
+#endif /* E_BOOK_BACKEND_M365_H */
diff --git a/src/Microsoft365/calendar/CMakeLists.txt b/src/Microsoft365/calendar/CMakeLists.txt
new file mode 100644
index 00000000..56f36cee
--- /dev/null
+++ b/src/Microsoft365/calendar/CMakeLists.txt
@@ -0,0 +1,61 @@
+set(DEPENDENCIES
+       evolution-microsoft365
+)
+
+set(SOURCES
+       e-cal-backend-m365.c
+       e-cal-backend-m365.h
+       e-cal-backend-m365-factory.c
+)
+
+add_library(ecalbackendmicrosoft365 MODULE
+       ${SOURCES}
+)
+
+add_dependencies(ecalbackendmicrosoft365
+       ${DEPENDENCIES}
+)
+
+target_compile_definitions(ecalbackendmicrosoft365 PRIVATE
+       -DG_LOG_DOMAIN=\"ecalbackendmicrosoft365\"
+       -DM365_DATADIR=\"${ewsdatadir}\"
+       -DM365_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+       -DM365_SRCDIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"
+)
+
+target_compile_options(ecalbackendmicrosoft365 PUBLIC
+       ${CAMEL_CFLAGS}
+       ${EVOLUTION_CALENDAR_CFLAGS}
+       ${LIBEBACKEND_CFLAGS}
+       ${LIBECAL_CFLAGS}
+       ${LIBEDATACAL_CFLAGS}
+       ${SOUP_CFLAGS}
+)
+
+target_include_directories(ecalbackendmicrosoft365 PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_SOURCE_DIR}
+       ${CMAKE_BINARY_DIR}/src/Microsoft365
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365
+       ${CMAKE_CURRENT_BINARY_DIR}
+       ${CAMEL_INCLUDE_DIRS}
+       ${EVOLUTION_CALENDAR_INCLUDE_DIRS}
+       ${LIBEBACKEND_INCLUDE_DIRS}
+       ${LIBECAL_INCLUDE_DIRS}
+       ${LIBEDATACAL_INCLUDE_DIRS}
+       ${SOUP_INCLUDE_DIRS}
+)
+
+target_link_libraries(ecalbackendmicrosoft365
+       ${DEPENDENCIES}
+       ${CAMEL_LDFLAGS}
+       ${EVOLUTION_CALENDAR_LDFLAGS}
+       ${LIBEBACKEND_LDFLAGS}
+       ${LIBECAL_LDFLAGS}
+       ${LIBEDATACAL_LDFLAGS}
+       ${SOUP_LDFLAGS}
+)
+
+install(TARGETS ecalbackendmicrosoft365
+       DESTINATION ${ecal_backenddir}
+)
diff --git a/src/Microsoft365/calendar/e-cal-backend-m365-factory.c 
b/src/Microsoft365/calendar/e-cal-backend-m365-factory.c
new file mode 100644
index 00000000..1d29ad38
--- /dev/null
+++ b/src/Microsoft365/calendar/e-cal-backend-m365-factory.c
@@ -0,0 +1,139 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+#include <glib/gi18n-lib.h>
+
+#include <libedata-cal/libedata-cal.h>
+
+#include "common/e-oauth2-service-microsoft365.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-cal-backend-m365.h"
+
+#define FACTORY_NAME "microsoft365"
+
+typedef ECalBackendFactory ECalBackendM365EventsFactory;
+typedef ECalBackendFactoryClass ECalBackendM365EventsFactoryClass;
+
+typedef ECalBackendFactory ECalBackendM365JournalFactory;
+typedef ECalBackendFactoryClass ECalBackendM365JournalFactoryClass;
+
+typedef ECalBackendFactory ECalBackendM365TodosFactory;
+typedef ECalBackendFactoryClass ECalBackendM365TodosFactoryClass;
+
+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_m365_events_factory_get_type (void);
+GType e_cal_backend_m365_journal_factory_get_type (void);
+GType e_cal_backend_m365_todos_factory_get_type (void);
+
+G_DEFINE_DYNAMIC_TYPE (ECalBackendM365EventsFactory, e_cal_backend_m365_events_factory, 
E_TYPE_CAL_BACKEND_FACTORY)
+G_DEFINE_DYNAMIC_TYPE (ECalBackendM365JournalFactory, e_cal_backend_m365_journal_factory, 
E_TYPE_CAL_BACKEND_FACTORY)
+G_DEFINE_DYNAMIC_TYPE (ECalBackendM365TodosFactory, e_cal_backend_m365_todos_factory, 
E_TYPE_CAL_BACKEND_FACTORY)
+
+static void
+e_cal_backend_m365_events_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 = FACTORY_NAME;
+       class->component_kind = I_CAL_VEVENT_COMPONENT;
+       class->backend_type = E_TYPE_CAL_BACKEND_M365;
+}
+
+static void
+e_cal_backend_m365_events_factory_class_finalize (ECalBackendFactoryClass *class)
+{
+}
+
+static void
+e_cal_backend_m365_events_factory_init (ECalBackendFactory *factory)
+{
+}
+
+static void
+e_cal_backend_m365_journal_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 = FACTORY_NAME;
+       class->component_kind = I_CAL_VJOURNAL_COMPONENT;
+       class->backend_type = E_TYPE_CAL_BACKEND_M365;
+}
+
+static void
+e_cal_backend_m365_journal_factory_class_finalize (ECalBackendFactoryClass *class)
+{
+}
+
+static void
+e_cal_backend_m365_journal_factory_init (ECalBackendFactory *factory)
+{
+}
+
+static void
+e_cal_backend_m365_todos_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 = FACTORY_NAME;
+       class->component_kind = I_CAL_VTODO_COMPONENT;
+       class->backend_type = E_TYPE_CAL_BACKEND_M365;
+}
+
+static void
+e_cal_backend_m365_todos_factory_class_finalize (ECalBackendFactoryClass *class)
+{
+}
+
+static void
+e_cal_backend_m365_todos_factory_init (ECalBackendFactory *factory)
+{
+}
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+       bindtextdomain (GETTEXT_PACKAGE, M365_LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+       e_module = E_MODULE (type_module);
+
+       e_oauth2_service_microsoft365_type_register (type_module);
+       e_source_m365_folder_type_register (type_module);
+
+       if (g_strcmp0 (g_getenv ("ENABLE_M365"), "1") == 0) {
+               e_cal_backend_m365_events_factory_register_type (type_module);
+               e_cal_backend_m365_journal_factory_register_type (type_module);
+               e_cal_backend_m365_todos_factory_register_type (type_module);
+       }
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+       e_module = NULL;
+}
diff --git a/src/Microsoft365/calendar/e-cal-backend-m365.c b/src/Microsoft365/calendar/e-cal-backend-m365.c
new file mode 100644
index 00000000..6f125dfc
--- /dev/null
+++ b/src/Microsoft365/calendar/e-cal-backend-m365.c
@@ -0,0 +1,3710 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+
+#include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
+
+#include <libedata-cal/libedata-cal.h>
+#include <libecal/libecal.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+#include "common/e-m365-tz-utils.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-cal-backend-m365.h"
+
+#define EC_ERROR(_code) e_client_error_create (_code, NULL)
+#define EC_ERROR_EX(_code, _msg) e_client_error_create (_code, _msg)
+#define ECC_ERROR(_code) e_cal_client_error_create (_code, NULL)
+#define ECC_ERROR_EX(_code, _msg) e_cal_client_error_create (_code, _msg)
+
+#define LOCK(_cb) g_rec_mutex_lock (&_cb->priv->property_lock)
+#define UNLOCK(_cb) g_rec_mutex_unlock (&_cb->priv->property_lock)
+
+struct _ECalBackendM365Private {
+       GRecMutex property_lock;
+       EM365Connection *cnc;
+       gchar *group_id;
+       gchar *folder_id;
+       gchar *attachments_dir;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (ECalBackendM365, e_cal_backend_m365, E_TYPE_CAL_META_BACKEND)
+
+static void
+ecb_m365_convert_error_to_client_error (GError **perror)
+{
+       GError *error = NULL;
+
+       if (!perror || !*perror ||
+           (*perror)->domain == E_CLIENT_ERROR ||
+           (*perror)->domain == E_CAL_CLIENT_ERROR)
+               return;
+
+       /*if ((*perror)->domain == M365_CONNECTION_ERROR) {
+               switch ((*perror)->code) {
+               case M365_CONNECTION_ERROR_AUTHENTICATION_FAILED:
+                       error = EC_ERROR_EX (E_CLIENT_ERROR_AUTHENTICATION_FAILED, (*perror)->message);
+                       break;
+               case M365_CONNECTION_ERROR_FOLDERNOTFOUND:
+               case M365_CONNECTION_ERROR_MANAGEDFOLDERNOTFOUND:
+               case M365_CONNECTION_ERROR_PARENTFOLDERNOTFOUND:
+               case M365_CONNECTION_ERROR_PUBLICFOLDERSERVERNOTFOUND:
+                       error = ECC_ERROR_EX (E_CAL_CLIENT_ERROR_NO_SUCH_CALENDAR, (*perror)->message);
+                       break;
+               case M365_CONNECTION_ERROR_EVENTNOTFOUND:
+               case M365_CONNECTION_ERROR_ITEMNOTFOUND:
+                       error = ECC_ERROR_EX (E_CAL_CLIENT_ERROR_OBJECT_NOT_FOUND, (*perror)->message);
+                       break;
+               case M365_CONNECTION_ERROR_UNAVAILABLE:
+                       g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND, 
(*perror)->message);
+                       break;
+               }
+
+               if (!error)
+                       error = EC_ERROR_EX (E_CLIENT_ERROR_OTHER_ERROR, (*perror)->message);
+       }*/
+
+       if (error) {
+               g_error_free (*perror);
+               *perror = error;
+       }
+}
+
+static const gchar *
+ecb_m365_get_component_from_extra (const gchar *extra)
+{
+       gchar *enter;
+
+       if (!extra)
+               return NULL;
+
+       enter = strchr (extra, '\n');
+
+       if (!enter)
+               return NULL;
+
+       return enter + 1;
+}
+
+/* Modifies inout_extra, cannot be called multiple times with the same arguments */
+static void
+ecb_m365_split_extra (gchar *inout_extra,
+                     const gchar **out_change_key,
+                     const gchar **out_ical_comp)
+{
+       gchar *enter;
+
+       if (!inout_extra)
+               return;
+
+       enter = (gchar *) ecb_m365_get_component_from_extra (inout_extra);
+       g_return_if_fail (enter != NULL);
+
+       enter[-1] = '\0';
+
+       if (out_change_key)
+               *out_change_key = inout_extra;
+
+       if (out_ical_comp)
+               *out_ical_comp = enter;
+}
+
+static void
+ecb_m365_get_uid (ECalBackendM365 *cbm365,
+                 JsonObject *m365_object,
+                 ICalComponent *inout_comp,
+                 ICalPropertyKind prop_kind)
+{
+       const gchar *id;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               id = e_m365_event_get_id (m365_object);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               id = e_m365_task_get_id (m365_object);
+               break;
+       default:
+               g_warn_if_reached ();
+               return;
+       }
+
+       i_cal_component_set_uid (inout_comp, id);
+}
+
+static void
+ecb_m365_get_date_time (ECalBackendM365 *cbm365,
+                       JsonObject *m365_object,
+                       ICalComponent *inout_comp,
+                       ICalPropertyKind prop_kind)
+{
+       time_t tt = (time_t) 0;
+
+       if (prop_kind == I_CAL_CREATED_PROPERTY) {
+               switch (i_cal_component_isa (inout_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       tt = e_m365_event_get_created_date_time (m365_object);
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       tt = e_m365_task_get_created_date_time (m365_object);
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       } else if (prop_kind == I_CAL_LASTMODIFIED_PROPERTY) {
+               switch (i_cal_component_isa (inout_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       tt = e_m365_event_get_last_modified_date_time (m365_object);
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       tt = e_m365_task_get_last_modified_date_time (m365_object);
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       } else {
+               g_warn_if_reached ();
+       }
+
+       if (tt > (time_t) 0) {
+               ICalProperty *prop;
+               ICalTime *itt;
+
+               itt = i_cal_time_new_from_timet_with_zone (tt, FALSE, i_cal_timezone_get_utc_timezone ());
+
+               if (prop_kind == I_CAL_CREATED_PROPERTY)
+                       prop = i_cal_property_new_created (itt);
+               else /* I_CAL_LASTMODIFIED_PROPERTY */
+                       prop = i_cal_property_new_lastmodified (itt);
+
+               i_cal_component_take_property (inout_comp, prop);
+
+               g_clear_object (&itt);
+       }
+}
+
+static ICalTimezone *
+ecb_m365_get_timezone_sync (ECalBackendM365 *cbm365,
+                           const gchar *tzid)
+{
+       ICalTimezone *zone;
+       ECalCache *cal_cache;
+
+       if (!tzid)
+               return NULL;
+
+       cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbm365));
+
+       if (!cal_cache)
+               return NULL;
+
+       zone = e_cal_cache_resolve_timezone_cb (tzid, cal_cache, NULL, NULL);
+
+       g_object_unref (cal_cache);
+
+       return zone;
+}
+
+static void
+ecb_m365_get_date_time_zone (ECalBackendM365 *cbm365,
+                            JsonObject *m365_object,
+                            ICalComponent *inout_comp,
+                            ICalPropertyKind prop_kind)
+{
+       EM365DateTimeWithZone *value;
+       ICalTimezone *tz;
+       ICalTime *itt;
+       time_t tt;
+       const gchar *tzid, *zone;
+       gboolean is_date;
+
+       if (prop_kind == I_CAL_DTSTART_PROPERTY) {
+               switch (i_cal_component_isa (inout_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       value = e_m365_event_get_start (m365_object);
+                       tzid = e_m365_event_get_original_start_timezone (m365_object);
+                       is_date = e_m365_event_get_is_all_day (m365_object);
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       value = e_m365_task_get_start_date_time (m365_object);
+                       tzid = "UTC";
+                       is_date = TRUE;
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       } else if (prop_kind == I_CAL_DTEND_PROPERTY) {
+               value = e_m365_event_get_end (m365_object);
+               tzid = e_m365_event_get_original_end_timezone (m365_object);
+               is_date = e_m365_event_get_is_all_day (m365_object);
+       } else if (prop_kind == I_CAL_COMPLETED_PROPERTY) {
+               value = e_m365_task_get_completed_date_time (m365_object);
+               tzid = "UTC";
+               is_date = TRUE;
+       } else if (prop_kind == I_CAL_DUE_PROPERTY) {
+               value = e_m365_task_get_due_date_time (m365_object);
+               tzid = "UTC";
+               is_date = TRUE;
+       } else {
+               g_warn_if_reached ();
+               return;
+       }
+
+       if (!value)
+               return;
+
+       tt = e_m365_date_time_get_date_time (value);
+       zone = e_m365_date_time_get_time_zone (value);
+
+       if (zone && *zone)
+               zone = e_m365_tz_utils_get_ical_equivalent (zone);
+
+       tz = zone && *zone ? ecb_m365_get_timezone_sync (cbm365, zone) : NULL;
+
+       if (!tz)
+               tz = i_cal_timezone_get_utc_timezone ();
+
+       itt = i_cal_time_new_from_timet_with_zone (tt, is_date, tz);
+
+       tzid = e_m365_tz_utils_get_ical_equivalent (tzid);
+
+       if (!tzid)
+               tzid = "UTC";
+
+       tz = ecb_m365_get_timezone_sync (cbm365, tzid);
+
+       if (tz && !is_date)
+               i_cal_time_convert_to_zone_inplace (itt, tz);
+
+       if (prop_kind == I_CAL_DTSTART_PROPERTY)
+               i_cal_component_set_dtstart (inout_comp, itt);
+       else if (prop_kind == I_CAL_DTEND_PROPERTY)
+               i_cal_component_set_dtend (inout_comp, itt);
+       else if (prop_kind == I_CAL_COMPLETED_PROPERTY)
+               i_cal_component_take_property (inout_comp, i_cal_property_new_completed (itt));
+       else /* if (prop_kind == I_CAL_DUE_PROPERTY) */
+               i_cal_component_set_due (inout_comp, itt);
+
+       g_clear_object (&itt);
+}
+
+static void
+ecb_m365_add_date_time_zone (ECalBackendM365 *cbm365,
+                            ICalComponent *new_comp,
+                            ICalComponent *old_comp,
+                            ICalPropertyKind prop_kind,
+                            JsonBuilder *builder)
+{
+       ICalProperty *new_prop;
+       ICalParameter *new_param;
+       ICalTime *old_value, *new_value;
+       const gchar *new_tzid = NULL;
+       void (* add_func) (JsonBuilder *builder, time_t date_time, const gchar *zone) = NULL;
+       gboolean same = FALSE;
+
+       if (prop_kind == I_CAL_DTSTART_PROPERTY) {
+               new_value = i_cal_component_get_dtstart (new_comp);
+               old_value = old_comp ? i_cal_component_get_dtstart (old_comp) : NULL;
+
+               switch (i_cal_component_isa (new_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       add_func = e_m365_event_add_start;
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       add_func = e_m365_task_add_start_date_time;
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       } else if (prop_kind == I_CAL_DTEND_PROPERTY) {
+               new_value = i_cal_component_get_dtend (new_comp);
+               old_value = old_comp ? i_cal_component_get_dtend (old_comp) : NULL;
+               add_func = e_m365_event_add_end;
+       } else if (prop_kind == I_CAL_COMPLETED_PROPERTY) {
+               ICalProperty *new_prop, *old_prop;
+
+               new_prop = i_cal_component_get_first_property (new_comp, prop_kind);
+               old_prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+               new_value = new_prop ? i_cal_property_get_completed (new_prop) : NULL;
+               old_value = old_prop ? i_cal_property_get_completed (old_prop) : NULL;
+               add_func = e_m365_task_add_completed_date_time;
+
+               g_clear_object (&new_prop);
+               g_clear_object (&old_prop);
+       } else if (prop_kind == I_CAL_DUE_PROPERTY) {
+               new_value = i_cal_component_get_due (new_comp);
+               old_value = old_comp ? i_cal_component_get_due (old_comp) : NULL;
+               add_func = e_m365_task_add_due_date_time;
+       } else {
+               g_warn_if_reached ();
+               return;
+       }
+
+       if (!new_value && !old_value)
+               return;
+
+       new_prop = i_cal_component_get_first_property (new_comp, prop_kind);
+       new_param = new_prop ? i_cal_property_get_first_parameter (new_prop, I_CAL_TZID_PARAMETER) : NULL;
+
+       if (new_param)
+               new_tzid = i_cal_parameter_get_tzid (new_param);
+
+       if (new_value && old_value) {
+               same = i_cal_time_compare (new_value, old_value) == 0;
+
+               if (same) {
+                       ICalProperty *old_prop;
+                       ICalParameter *old_param;
+                       const gchar *old_tzid;
+
+                       old_prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+                       old_param = old_prop ? i_cal_property_get_first_parameter (old_prop, 
I_CAL_TZID_PARAMETER) : NULL;
+                       old_tzid = old_param ? i_cal_parameter_get_tzid (old_param) : NULL;
+
+                       same = g_strcmp0 (old_tzid, new_tzid) == 0;
+
+                       g_clear_object (&old_param);
+                       g_clear_object (&old_prop);
+               }
+       }
+
+       if (!same) {
+               ICalTimezone *izone = NULL;
+               const gchar *wzone = NULL;
+               time_t tt;
+
+               if (new_tzid) {
+                       izone = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cbm365), new_tzid);
+
+                       if (izone)
+                               wzone = e_m365_tz_utils_get_msdn_equivalent (i_cal_timezone_get_location 
(izone));
+               }
+
+               tt = i_cal_time_as_timet_with_zone (new_value, wzone ? NULL : izone);
+
+               add_func (builder, tt, wzone);
+       }
+
+       g_clear_object (&new_prop);
+       g_clear_object (&new_param);
+       g_clear_object (&new_value);
+       g_clear_object (&old_value);
+}
+
+static void
+ecb_m365_get_categories (ECalBackendM365 *cbm365,
+                        JsonObject *m365_object,
+                        ICalComponent *inout_comp,
+                        ICalPropertyKind prop_kind)
+{
+       JsonArray *categories;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               categories = e_m365_event_get_categories (m365_object);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               categories = e_m365_task_get_categories (m365_object);
+               break;
+       default:
+               g_warn_if_reached ();
+               return;
+       }
+
+       if (categories) {
+               GString *categories_str = NULL;
+               guint ii, len;
+
+               len = json_array_get_length (categories);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *category;
+
+                       category = json_array_get_string_element (categories, ii);
+
+                       if (category && *category) {
+                               gchar *ical_str = i_cal_value_encode_ical_string (category);
+
+                               if (ical_str && *ical_str) {
+                                       if (!categories_str) {
+                                               categories_str = g_string_new (ical_str);
+                                       } else {
+                                               g_string_append_c (categories_str, ',');
+                                               g_string_append (categories_str, ical_str);
+                                       }
+                               }
+
+                               g_free (ical_str);
+                       }
+               }
+
+               if (categories_str) {
+                       i_cal_component_take_property (inout_comp, i_cal_property_new_categories 
(categories_str->str));
+
+                       g_string_free (categories_str, TRUE);
+               }
+       }
+}
+
+static void
+ecb_m365_extract_categories (ICalComponent *comp,
+                            GHashTable **out_hash, /* gchar * ~> NULL */
+                            GSList **out_slist) /* gchar * */
+{
+       ICalProperty *prop;
+
+       if (!comp)
+               return;
+
+       for (prop = i_cal_component_get_first_property (comp, I_CAL_CATEGORIES_PROPERTY);
+            prop;
+            g_object_unref (prop), prop = i_cal_component_get_next_property (comp, 
I_CAL_CATEGORIES_PROPERTY)) {
+               const gchar *categories;
+
+               categories = i_cal_property_get_categories (prop);
+
+               if (categories && *categories) {
+                       if (out_hash && !*out_hash)
+                               *out_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+                       if (strchr (categories, ',')) {
+                               gchar **strv;
+                               guint ii;
+
+                               strv = g_strsplit (categories, ",", -1);
+
+                               for (ii = 0; strv[ii]; ii++) {
+                                       gchar *category = g_strchomp (strv[ii]);
+
+                                       if (*category) {
+                                               if (out_hash) {
+                                                       g_hash_table_insert (*out_hash, category, NULL);
+                                               } else if (out_slist) {
+                                                       *out_slist = g_slist_prepend (*out_slist, category);
+                                               } else {
+                                                       g_warn_if_reached ();
+                                                       g_free (category);
+                                               }
+                                       } else {
+                                               g_free (category);
+                                       }
+                               }
+
+                               g_free (strv);
+                       } else if (out_hash) {
+                               g_hash_table_insert (*out_hash, g_strchomp (g_strdup (categories)), NULL);
+                       } else if (out_slist) {
+                               *out_slist = g_slist_prepend (*out_slist, g_strchomp (g_strdup (categories)));
+                       } else {
+                               g_warn_if_reached ();
+                       }
+               }
+       }
+
+       g_clear_object (&prop);
+
+       if (out_slist && *out_slist)
+               *out_slist = g_slist_reverse (*out_slist);
+}
+
+static void
+ecb_m365_add_categories (ECalBackendM365 *cbm365,
+                        ICalComponent *new_comp,
+                        ICalComponent *old_comp,
+                        ICalPropertyKind prop_kind,
+                        JsonBuilder *builder)
+{
+       GHashTable *old_value = NULL;
+       GSList *new_value = NULL;
+       void (* begin_categories_func) (JsonBuilder *builder);
+       void (* end_categories_func) (JsonBuilder *builder);
+       void (* add_category_func) (JsonBuilder *builder, const gchar *category);
+
+       switch (i_cal_component_isa (new_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               begin_categories_func = e_m365_event_begin_categories;
+               end_categories_func = e_m365_event_end_categories;
+               add_category_func = e_m365_event_add_category;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               begin_categories_func = e_m365_task_begin_categories;
+               end_categories_func = e_m365_task_end_categories;
+               add_category_func = e_m365_task_add_category;
+               break;
+       default:
+               g_warn_if_reached ();
+               return;
+       }
+
+       ecb_m365_extract_categories (new_comp, NULL, &new_value);
+       ecb_m365_extract_categories (old_comp, &old_value, NULL);
+
+       if (!new_value && !old_value)
+               return;
+
+       if (new_value) {
+               GSList *link;
+               gboolean same = FALSE;
+
+               if (old_value && g_hash_table_size (old_value) == g_slist_length (new_value)) {
+                       same = TRUE;
+
+                       for (link = new_value; link && same; link = g_slist_next (link)) {
+                               const gchar *category = link->data;
+
+                               same = g_hash_table_contains (old_value, category);
+                       }
+               }
+
+               if (!same) {
+                       begin_categories_func (builder);
+
+                       for (link = new_value; link; link = g_slist_next (link)) {
+                               const gchar *category = link->data;
+
+                               add_category_func (builder, category);
+                       }
+
+                       end_categories_func (builder);
+               }
+       } else {
+               begin_categories_func (builder);
+               end_categories_func (builder);
+       }
+
+       if (new_value)
+               g_slist_free_full (new_value, g_free);
+       if (old_value)
+               g_hash_table_destroy (old_value);
+}
+
+static void
+ecb_m365_get_subject (ECalBackendM365 *cbm365,
+                     EM365Event *m365_object,
+                     ICalComponent *inout_comp,
+                     ICalPropertyKind prop_kind)
+{
+       const gchar *subject;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               subject = e_m365_event_get_subject (m365_object);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               subject = e_m365_task_get_subject (m365_object);
+               break;
+       default:
+               g_warn_if_reached ();
+               return;
+       }
+
+       if (subject)
+               i_cal_component_set_summary (inout_comp, subject);
+}
+
+static void
+ecb_m365_add_subject (ECalBackendM365 *cbm365,
+                     ICalComponent *new_comp,
+                     ICalComponent *old_comp,
+                     ICalPropertyKind prop_kind,
+                     JsonBuilder *builder)
+{
+       const gchar *new_value, *old_value;
+
+       new_value = i_cal_component_get_summary (new_comp);
+       old_value = old_comp ? i_cal_component_get_summary (old_comp) : NULL;
+
+       if (g_strcmp0 (new_value, old_value) != 0) {
+               switch (i_cal_component_isa (new_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       e_m365_event_add_subject (builder, new_value ? new_value : "");
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       e_m365_task_add_subject (builder, new_value ? new_value : "");
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       }
+}
+
+static void
+ecb_m365_get_body (ECalBackendM365 *cbm365,
+                  JsonObject *m365_object,
+                  ICalComponent *inout_comp,
+                  ICalPropertyKind prop_kind)
+{
+       EM365ItemBody *value;
+       const gchar *content;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               value = e_m365_event_get_body (m365_object);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               value = e_m365_task_get_body (m365_object);
+               break;
+       default:
+               g_warn_if_reached ();
+               return;
+       }
+
+       content = value ? e_m365_item_body_get_content (value) : NULL;
+
+       if (content && *content && strcmp (content, "\r\n") != 0)
+               i_cal_component_set_description (inout_comp, content);
+}
+
+static void
+ecb_m365_add_body (ECalBackendM365 *cbm365,
+                  ICalComponent *new_comp,
+                  ICalComponent *old_comp,
+                  ICalPropertyKind prop_kind,
+                  JsonBuilder *builder)
+{
+       const gchar *new_value, *old_value;
+
+       new_value = i_cal_component_get_description (new_comp);
+       old_value = old_comp ? i_cal_component_get_description (old_comp) : NULL;
+
+       if (g_strcmp0 (new_value, old_value) != 0) {
+               switch (i_cal_component_isa (new_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       e_m365_event_add_body (builder, E_M365_ITEM_BODY_CONTENT_TYPE_TEXT, new_value);
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       e_m365_task_add_body (builder, E_M365_ITEM_BODY_CONTENT_TYPE_TEXT, new_value);
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       }
+}
+
+static void
+ecb_m365_get_sensitivity (ECalBackendM365 *cbm365,
+                         JsonObject *m365_object,
+                         ICalComponent *inout_comp,
+                         ICalPropertyKind prop_kind)
+{
+       EM365SensitivityType value;
+       ICalProperty_Class cls = I_CAL_CLASS_NONE;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               value = e_m365_event_get_sensitivity (m365_object);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               value = e_m365_task_get_sensitivity (m365_object);
+               break;
+       default:
+               g_warn_if_reached ();
+               return;
+       }
+
+       if (value == E_M365_SENSITIVITY_NORMAL)
+               cls = I_CAL_CLASS_PUBLIC;
+       else if (value == E_M365_SENSITIVITY_PERSONAL || value == E_M365_SENSITIVITY_PRIVATE)
+               cls = I_CAL_CLASS_PRIVATE;
+       else if (value == E_M365_SENSITIVITY_CONFIDENTIAL)
+               cls = I_CAL_CLASS_CONFIDENTIAL;
+
+       if (cls != I_CAL_CLASS_NONE)
+               i_cal_component_take_property (inout_comp, i_cal_property_new_class (cls));
+}
+
+static void
+ecb_m365_add_sensitivity (ECalBackendM365 *cbm365,
+                         ICalComponent *new_comp,
+                         ICalComponent *old_comp,
+                         ICalPropertyKind prop_kind,
+                         JsonBuilder *builder)
+{
+       ICalProperty_Class new_value = I_CAL_CLASS_NONE, old_value = I_CAL_CLASS_NONE;
+       ICalProperty *prop;
+
+       prop = i_cal_component_get_first_property (new_comp, prop_kind);
+
+       if (prop) {
+               new_value = i_cal_property_get_class (prop);
+               g_clear_object (&prop);
+       }
+
+       prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+
+       if (prop) {
+               old_value = i_cal_property_get_class (prop);
+               g_clear_object (&prop);
+       }
+
+       if (new_value != old_value) {
+               EM365SensitivityType value = E_M365_SENSITIVITY_NOT_SET;
+
+               if (new_value == I_CAL_CLASS_PUBLIC)
+                       value = E_M365_SENSITIVITY_NORMAL;
+               else if (new_value == I_CAL_CLASS_PRIVATE)
+                       value = E_M365_SENSITIVITY_PRIVATE;
+               else if (new_value == I_CAL_CLASS_CONFIDENTIAL)
+                       value = E_M365_SENSITIVITY_CONFIDENTIAL;
+
+               switch (i_cal_component_isa (new_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       e_m365_event_add_sensitivity (builder, value);
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       e_m365_task_add_sensitivity (builder, value);
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       return;
+               }
+       }
+}
+
+static void
+ecb_m365_get_show_as (ECalBackendM365 *cbm365,
+                     EM365Event *m365_event,
+                     ICalComponent *inout_comp,
+                     ICalPropertyKind prop_kind)
+{
+       EM365FreeBusyStatusType value;
+       ICalPropertyTransp transp = I_CAL_TRANSP_NONE;
+
+       value = e_m365_event_get_show_as (m365_event);
+
+       if (value == E_M365_FREE_BUSY_STATUS_FREE)
+               transp = I_CAL_TRANSP_TRANSPARENT;
+       else if (value == E_M365_FREE_BUSY_STATUS_BUSY)
+               transp = I_CAL_TRANSP_OPAQUE;
+
+       if (transp != I_CAL_TRANSP_NONE)
+               i_cal_component_take_property (inout_comp, i_cal_property_new_transp (transp));
+}
+
+static void
+ecb_m365_add_show_as (ECalBackendM365 *cbm365,
+                     ICalComponent *new_comp,
+                     ICalComponent *old_comp,
+                     ICalPropertyKind prop_kind,
+                     JsonBuilder *builder)
+{
+       ICalPropertyTransp new_value = I_CAL_TRANSP_NONE, old_value = I_CAL_TRANSP_NONE;
+       ICalProperty *prop;
+
+       prop = i_cal_component_get_first_property (new_comp, prop_kind);
+
+       if (prop) {
+               new_value = i_cal_property_get_transp (prop);
+               g_clear_object (&prop);
+       }
+
+       prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+
+       if (prop) {
+               old_value = i_cal_property_get_transp (prop);
+               g_clear_object (&prop);
+       }
+
+       if (new_value != old_value) {
+               EM365FreeBusyStatusType value = E_M365_FREE_BUSY_STATUS_NOT_SET;
+
+               if (new_value == I_CAL_TRANSP_TRANSPARENT)
+                       value = E_M365_FREE_BUSY_STATUS_FREE;
+               else if (new_value == I_CAL_TRANSP_OPAQUE)
+                       value = E_M365_FREE_BUSY_STATUS_BUSY;
+
+               e_m365_event_add_show_as (builder, value);
+       }
+}
+
+static void
+ecb_m365_get_location (ECalBackendM365 *cbm365,
+                      EM365Event *m365_event,
+                      ICalComponent *inout_comp,
+                      ICalPropertyKind prop_kind)
+{
+       EM365Location *value;
+       const gchar *tmp;
+
+       value = e_m365_event_get_location (m365_event);
+
+       if (!value)
+               return;
+
+       tmp = e_m365_location_get_display_name (value);
+
+       if (tmp && *tmp)
+               i_cal_component_set_location (inout_comp, tmp);
+}
+
+static void
+ecb_m365_add_location (ECalBackendM365 *cbm365,
+                      ICalComponent *new_comp,
+                      ICalComponent *old_comp,
+                      ICalPropertyKind prop_kind,
+                      JsonBuilder *builder)
+{
+       const gchar *new_value, *old_value;
+
+       new_value = i_cal_component_get_location (new_comp);
+       old_value = old_comp ? i_cal_component_get_location (old_comp) : NULL;
+
+       if (g_strcmp0 (new_value, old_value) != 0) {
+               if (new_value && *new_value) {
+                       e_m365_event_begin_location (builder);
+                       e_m365_location_add_display_name (builder, new_value);
+                       e_m365_event_end_location (builder);
+               } else {
+                       e_m365_event_add_null_location (builder);
+               }
+       }
+}
+
+static void
+ecb_m365_get_organizer (ECalBackendM365 *cbm365,
+                       EM365Event *m365_event,
+                       ICalComponent *inout_comp,
+                       ICalPropertyKind prop_kind)
+{
+       EM365Recipient *value;
+       JsonArray *attendees;
+       const gchar *name;
+       const gchar *address;
+
+       value = e_m365_event_get_organizer (m365_event);
+
+       if (!value)
+               return;
+
+       /* Include the organizer only if there is at least one attendee */
+       attendees = e_m365_event_get_attendees (m365_event);
+
+       if (!attendees || !json_array_get_length (attendees))
+               return;
+
+       name = e_m365_recipient_get_name (value);
+       address = e_m365_recipient_get_address (value);
+
+       if (address && *address) {
+               ECalComponentOrganizer *organizer;
+               gchar *mailto_addr;
+
+               mailto_addr = g_strconcat ("mailto:";, address, NULL);
+               organizer = e_cal_component_organizer_new ();
+               e_cal_component_organizer_set_value (organizer, mailto_addr);
+               g_free (mailto_addr);
+
+               if (name && *name)
+                       e_cal_component_organizer_set_cn (organizer, name);
+
+               i_cal_component_take_property (inout_comp, e_cal_component_organizer_get_as_property 
(organizer));
+               e_cal_component_organizer_free (organizer);
+       }
+}
+
+static const gchar *
+ecb_m365_strip_mailto (const gchar *value)
+{
+       if (value && g_ascii_strncasecmp (value, "mailto:";, 7) == 0)
+               return value + 7;
+
+       return value;
+}
+
+static void
+ecb_m365_add_organizer (ECalBackendM365 *cbm365,
+                       ICalComponent *new_comp,
+                       ICalComponent *old_comp,
+                       ICalPropertyKind prop_kind,
+                       JsonBuilder *builder)
+{
+       ECalComponentOrganizer *new_value = NULL, *old_value = NULL;
+       ICalProperty *prop;
+
+       prop = i_cal_component_get_first_property (new_comp, prop_kind);
+
+       if (prop) {
+               new_value = e_cal_component_organizer_new_from_property (prop);
+               g_clear_object (&prop);
+       }
+
+       prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+
+       if (prop) {
+               old_value = e_cal_component_organizer_new_from_property (prop);
+               g_clear_object (&prop);
+       }
+
+       if (new_value != old_value && (
+           g_strcmp0 (new_value ? e_cal_component_organizer_get_cn (new_value) : NULL,
+                      old_value ? e_cal_component_organizer_get_cn (old_value) : NULL) != 0 ||
+           g_strcmp0 (new_value ? ecb_m365_strip_mailto (e_cal_component_organizer_get_value (new_value)) : 
NULL,
+                      old_value ? ecb_m365_strip_mailto (e_cal_component_organizer_get_value (old_value)) : 
NULL) != 0)) {
+               if (new_value) {
+                       e_m365_event_add_organizer (builder,
+                                                   e_cal_component_organizer_get_cn (new_value),
+                                                   ecb_m365_strip_mailto 
(e_cal_component_organizer_get_value (new_value)));
+               } else {
+                       e_m365_event_add_null_organizer (builder);
+               }
+       }
+
+       e_cal_component_organizer_free (new_value);
+       e_cal_component_organizer_free (old_value);
+}
+
+static void
+ecb_m365_get_attendees (ECalBackendM365 *cbm365,
+                       EM365Event *m365_event,
+                       ICalComponent *inout_comp,
+                       ICalPropertyKind prop_kind)
+{
+       JsonArray *array;
+       guint ii, sz;
+
+       array = e_m365_event_get_attendees (m365_event);
+
+       if (!array)
+               return;
+
+       sz = json_array_get_length (array);
+
+       for (ii = 0; ii < sz; ii++) {
+               EM365Attendee *m365_attendee;
+               EM365ResponseStatus *m365_status;
+               EM365AttendeeType m365_att_type;
+               EM365EmailAddress *m365_address;
+               ECalComponentAttendee *e_attendee;
+               ICalParameterRole role = I_CAL_ROLE_NONE;
+               gchar *mailto_addr;
+
+               m365_attendee = json_array_get_object_element (array, ii);
+
+               if (!m365_attendee)
+                       continue;
+
+               m365_address = e_m365_attendee_get_email_address (m365_attendee);
+
+               if (!m365_address || !e_m365_email_address_get_address (m365_address))
+                       continue;
+
+               e_attendee = e_cal_component_attendee_new ();
+
+               mailto_addr = g_strconcat ("mailto:";, e_m365_email_address_get_address (m365_address), NULL);
+               e_cal_component_attendee_set_value (e_attendee, mailto_addr);
+               g_free (mailto_addr);
+
+               if (e_m365_email_address_get_name (m365_address))
+                       e_cal_component_attendee_set_cn (e_attendee, e_m365_email_address_get_name 
(m365_address));
+
+               m365_status = e_m365_attendee_get_status (m365_attendee);
+
+               if (m365_status) {
+                       EM365ResponseType m365_response;
+                       ICalParameterPartstat partstat = I_CAL_PARTSTAT_NONE;
+
+                       m365_response = e_m365_response_status_get_response (m365_status);
+
+                       if (m365_response == E_M365_RESPONSE_TENTATIVELY_ACCEPTED)
+                               partstat = I_CAL_PARTSTAT_TENTATIVE;
+                       else if (m365_response == E_M365_RESPONSE_ACCEPTED)
+                               partstat = I_CAL_PARTSTAT_ACCEPTED;
+                       else if (m365_response == E_M365_RESPONSE_DECLINED)
+                               partstat = I_CAL_PARTSTAT_DECLINED;
+                       else if (m365_response == E_M365_RESPONSE_NOT_RESPONDED)
+                               partstat = I_CAL_PARTSTAT_NEEDSACTION;
+
+                       if (partstat != I_CAL_PARTSTAT_NONE) {
+                               time_t tt;
+
+                               e_cal_component_attendee_set_partstat (e_attendee, partstat);
+
+                               tt = e_m365_response_status_get_time (m365_status);
+
+                               if (tt > (time_t) 0) {
+                                       ECalComponentParameterBag *params;
+                                       ICalParameter *param;
+                                       gchar *tmp;
+
+                                       tmp = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) tt);
+                                       params = e_cal_component_attendee_get_parameter_bag (e_attendee);
+
+                                       param = i_cal_parameter_new_x (tmp);
+                                       i_cal_parameter_set_xname (param, "X-M365-STATUS-TIME");
+
+                                       e_cal_component_parameter_bag_take (params, param);
+
+                                       g_free (tmp);
+                               }
+                       }
+               }
+
+               m365_att_type = e_m365_attendee_get_type (m365_attendee);
+
+               if (m365_att_type == E_M365_ATTENDEE_REQUIRED) {
+                       role = I_CAL_ROLE_REQPARTICIPANT;
+                       e_cal_component_attendee_set_cutype (e_attendee, I_CAL_CUTYPE_INDIVIDUAL);
+               } else if (m365_att_type == E_M365_ATTENDEE_OPTIONAL) {
+                       role = I_CAL_ROLE_OPTPARTICIPANT;
+                       e_cal_component_attendee_set_cutype (e_attendee, I_CAL_CUTYPE_INDIVIDUAL);
+               } else if (m365_att_type == E_M365_ATTENDEE_RESOURCE) {
+                       e_cal_component_attendee_set_cutype (e_attendee, I_CAL_CUTYPE_RESOURCE);
+               }
+
+               if (role != I_CAL_ROLE_NONE)
+                       e_cal_component_attendee_set_role (e_attendee, role);
+
+               i_cal_component_take_property (inout_comp, e_cal_component_attendee_get_as_property 
(e_attendee));
+
+               e_cal_component_attendee_free (e_attendee);
+       }
+}
+
+static void
+ecb_m365_extract_attendees (ICalComponent *comp,
+                           GHashTable **out_hash, /* const gchar *ECalComponentAttendee::value ~> 
ECalComponentAttendee * */
+                           GSList **out_slist) /* ECalComponentAttendee * */
+{
+       ICalProperty *prop;
+
+       if (!comp)
+               return;
+
+       for (prop = i_cal_component_get_first_property (comp, I_CAL_ATTENDEE_PROPERTY);
+            prop;
+            g_object_unref (prop), prop = i_cal_component_get_next_property (comp, I_CAL_ATTENDEE_PROPERTY)) 
{
+               ECalComponentAttendee *attendee;
+
+               attendee = e_cal_component_attendee_new_from_property (prop);
+
+               if (attendee && e_cal_component_attendee_get_value (attendee)) {
+                       if (out_hash) {
+                               if (!*out_hash)
+                                       *out_hash = g_hash_table_new_full (camel_strcase_hash, 
camel_strcase_equal, NULL, e_cal_component_attendee_free);
+
+                               g_hash_table_insert (*out_hash, (gpointer) e_cal_component_attendee_get_value 
(attendee), attendee);
+                       } else if (out_slist) {
+                               *out_slist = g_slist_prepend (*out_slist, attendee);
+                       } else {
+                               g_warn_if_reached ();
+                               e_cal_component_attendee_free (attendee);
+                       }
+               } else {
+                       e_cal_component_attendee_free (attendee);
+               }
+       }
+
+       g_clear_object (&prop);
+
+       if (out_slist && *out_slist)
+               *out_slist = g_slist_reverse (*out_slist);
+}
+
+static void
+ecb_m365_add_attendees (ECalBackendM365 *cbm365,
+                       ICalComponent *new_comp,
+                       ICalComponent *old_comp,
+                       ICalPropertyKind prop_kind,
+                       JsonBuilder *builder)
+{
+       GHashTable *old_value = NULL;
+       GSList *new_value = NULL;
+
+       ecb_m365_extract_attendees (new_comp, NULL, &new_value);
+       ecb_m365_extract_attendees (old_comp, &old_value, NULL);
+
+       if (!new_value && !old_value)
+               return;
+
+       if (new_value) {
+               GSList *link;
+               gboolean same = FALSE;
+
+               if (old_value && g_hash_table_size (old_value) == g_slist_length (new_value)) {
+                       same = TRUE;
+
+                       for (link = new_value; link && same; link = g_slist_next (link)) {
+                               ECalComponentAttendee *new_att = link->data, *old_att;
+
+                               old_att = g_hash_table_lookup (old_value, e_cal_component_attendee_get_value 
(new_att));
+
+                               same = old_att && e_cal_component_attendee_get_value (old_att) && 
e_cal_component_attendee_get_value (new_att) &&
+                                       g_ascii_strcasecmp (e_cal_component_attendee_get_value (old_att), 
e_cal_component_attendee_get_value (new_att)) == 0 &&
+                                       g_strcmp0 (e_cal_component_attendee_get_cn (old_att), 
e_cal_component_attendee_get_cn (new_att)) == 0 &&
+                                       e_cal_component_attendee_get_partstat (old_att) == 
e_cal_component_attendee_get_partstat (new_att) &&
+                                       e_cal_component_attendee_get_cutype (old_att) == 
e_cal_component_attendee_get_cutype (new_att) &&
+                                       e_cal_component_attendee_get_role (old_att) == 
e_cal_component_attendee_get_role (new_att);
+                       }
+               }
+
+               if (!same) {
+                       e_m365_event_begin_attendees (builder);
+
+                       for (link = new_value; link; link = g_slist_next (link)) {
+                               ECalComponentAttendee *attendee = link->data;
+                               EM365AttendeeType att_type;
+                               EM365ResponseType response = E_M365_RESPONSE_NONE;
+                               time_t response_time = (time_t) 0;
+                               ICalParameterPartstat partstat;
+                               const gchar *address;
+
+                               address = ecb_m365_strip_mailto (e_cal_component_attendee_get_value 
(attendee));
+
+                               if (e_cal_component_attendee_get_cutype (attendee) == I_CAL_CUTYPE_RESOURCE)
+                                       att_type = E_M365_ATTENDEE_RESOURCE;
+                               else if (e_cal_component_attendee_get_role (attendee) == 
I_CAL_ROLE_REQPARTICIPANT ||
+                                        e_cal_component_attendee_get_role (attendee) == I_CAL_ROLE_CHAIR)
+                                       att_type = E_M365_ATTENDEE_REQUIRED;
+                               else if (e_cal_component_attendee_get_role (attendee) == 
I_CAL_ROLE_OPTPARTICIPANT)
+                                       att_type = E_M365_ATTENDEE_OPTIONAL;
+                               else /* Fallback */
+                                       att_type = E_M365_ATTENDEE_REQUIRED;
+
+                               partstat = e_cal_component_attendee_get_partstat (attendee);
+
+                               if (partstat == I_CAL_PARTSTAT_TENTATIVE)
+                                       response = E_M365_RESPONSE_TENTATIVELY_ACCEPTED;
+                               else if (partstat == I_CAL_PARTSTAT_ACCEPTED)
+                                       response = E_M365_RESPONSE_ACCEPTED;
+                               else if (partstat == I_CAL_PARTSTAT_DECLINED)
+                                       response = E_M365_RESPONSE_DECLINED;
+                               else if (partstat == I_CAL_PARTSTAT_NEEDSACTION)
+                                       response = E_M365_RESPONSE_NOT_RESPONDED;
+
+                               if (response != E_M365_RESPONSE_NONE) {
+                                       ECalComponentParameterBag *params;
+                                       guint ii, sz;
+
+                                       params = e_cal_component_attendee_get_parameter_bag (attendee);
+                                       sz = e_cal_component_parameter_bag_get_count (params);
+
+                                       for (ii = 0; ii < sz; ii++) {
+                                               ICalParameter *param;
+
+                                               param = e_cal_component_parameter_bag_get (params, ii);
+
+                                               if (param && i_cal_parameter_isa (param) == I_CAL_X_PARAMETER 
&&
+                                                   i_cal_parameter_get_xname (param) &&
+                                                   g_ascii_strcasecmp (i_cal_parameter_get_xname (param), 
"X-M365-STATUS-TIME") == 0) {
+                                                       const gchar *xvalue;
+
+                                                       xvalue = i_cal_parameter_get_xvalue (param);
+
+                                                       if (xvalue && *xvalue) {
+                                                               gint64 value;
+
+                                                               value = g_ascii_strtoll (xvalue, NULL, 10);
+
+                                                               if (value)
+                                                                       response_time = (time_t) value;
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               e_m365_event_add_attendee (builder, att_type, response, response_time, 
e_cal_component_attendee_get_cn (attendee), address);
+                       }
+
+                       e_m365_event_end_attendees (builder);
+               }
+       } else {
+               e_m365_event_add_null_attendees (builder);
+       }
+
+       if (new_value)
+               g_slist_free_full (new_value, e_cal_component_attendee_free);
+       if (old_value)
+               g_hash_table_destroy (old_value);
+}
+
+static void
+ecb_m365_get_importance (ECalBackendM365 *cbm365,
+                        EM365Event *m365_event,
+                        ICalComponent *inout_comp,
+                        ICalPropertyKind prop_kind)
+{
+       EM365ImportanceType value;
+       ICalProperty *prop = NULL;
+
+       value = e_m365_event_get_importance (m365_event);
+
+       if (value == E_M365_IMPORTANCE_LOW)
+               prop = i_cal_property_new_priority (9);
+       else if (value == E_M365_IMPORTANCE_NORMAL)
+               prop = i_cal_property_new_priority (5);
+       else if (value == E_M365_IMPORTANCE_HIGH)
+               prop = i_cal_property_new_priority (1);
+
+       if (prop)
+               i_cal_component_take_property (inout_comp, prop);
+}
+
+static void
+ecb_m365_add_importance (ECalBackendM365 *cbm365,
+                        ICalComponent *new_comp,
+                        ICalComponent *old_comp,
+                        ICalPropertyKind prop_kind,
+                        JsonBuilder *builder)
+{
+       gint old_value = -1, new_value = -1;
+       ICalProperty *prop;
+
+       prop = i_cal_component_get_first_property (new_comp, prop_kind);
+
+       if (prop) {
+               new_value = i_cal_property_get_priority (prop);
+               g_clear_object (&prop);
+       }
+
+       prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+
+       if (prop) {
+               old_value = i_cal_property_get_priority (prop);
+               g_clear_object (&prop);
+       }
+
+       if (new_value != old_value) {
+               EM365ImportanceType value = E_M365_IMPORTANCE_NOT_SET;
+
+               if (new_value >= 1 && new_value <= 4) {
+                       value = E_M365_IMPORTANCE_HIGH;
+               } else if (new_value == 5) {
+                       value = E_M365_IMPORTANCE_NORMAL;
+               } else if (new_value >= 6 && new_value <= 9) {
+                       value = E_M365_IMPORTANCE_LOW;
+               }
+
+               e_m365_event_add_importance (builder, value);
+       }
+}
+
+static void
+ecb_m365_get_event_status (ECalBackendM365 *cbm365,
+                          EM365Event *m365_event,
+                          ICalComponent *inout_comp,
+                          ICalPropertyKind prop_kind)
+{
+       ICalPropertyStatus status = I_CAL_STATUS_NONE;
+
+       if (e_m365_event_get_is_cancelled (m365_event)) {
+               status = I_CAL_STATUS_CANCELLED;
+       } else {
+               EM365ResponseStatus *response_status;
+
+               response_status = e_m365_event_get_response_status (m365_event);
+
+               if (response_status) {
+                       EM365ResponseType response;
+
+                       response = e_m365_response_status_get_response (response_status);
+
+                       if (response == E_M365_RESPONSE_TENTATIVELY_ACCEPTED)
+                               status = I_CAL_STATUS_TENTATIVE;
+                       else if (response == E_M365_RESPONSE_ACCEPTED)
+                               status = I_CAL_STATUS_CONFIRMED;
+                       else if (response == E_M365_RESPONSE_DECLINED)
+                               status = I_CAL_STATUS_CANCELLED;
+                       else if (response == E_M365_RESPONSE_NOT_RESPONDED)
+                               status = I_CAL_STATUS_NEEDSACTION;
+               }
+       }
+
+       if (status != I_CAL_STATUS_NONE)
+               i_cal_component_take_property (inout_comp, i_cal_property_new_status (status));
+}
+
+static ICalRecurrenceWeekday
+ecb_m365_day_of_week_to_ical (EM365DayOfWeekType dow)
+{
+       switch (dow) {
+       case E_M365_DAY_OF_WEEK_SUNDAY:
+               return I_CAL_SUNDAY_WEEKDAY;
+       case E_M365_DAY_OF_WEEK_MONDAY:
+               return I_CAL_MONDAY_WEEKDAY;
+       case E_M365_DAY_OF_WEEK_TUESDAY:
+               return I_CAL_TUESDAY_WEEKDAY;
+       case E_M365_DAY_OF_WEEK_WEDNESDAY:
+               return I_CAL_WEDNESDAY_WEEKDAY;
+       case E_M365_DAY_OF_WEEK_THURSDAY:
+               return I_CAL_THURSDAY_WEEKDAY;
+       case E_M365_DAY_OF_WEEK_FRIDAY:
+               return I_CAL_FRIDAY_WEEKDAY;
+       case E_M365_DAY_OF_WEEK_SATURDAY:
+               return I_CAL_SATURDAY_WEEKDAY;
+       default:
+               break;
+       }
+
+       return I_CAL_NO_WEEKDAY;
+}
+
+static EM365DayOfWeekType
+ecb_m365_day_of_week_from_ical (ICalRecurrenceWeekday dow)
+{
+       switch (dow) {
+       case I_CAL_SUNDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_SUNDAY;
+               break;
+       case I_CAL_MONDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_MONDAY;
+               break;
+       case I_CAL_TUESDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_TUESDAY;
+               break;
+       case I_CAL_WEDNESDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_WEDNESDAY;
+               break;
+       case I_CAL_THURSDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_THURSDAY;
+               break;
+       case I_CAL_FRIDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_FRIDAY;
+               break;
+       case I_CAL_SATURDAY_WEEKDAY:
+               return E_M365_DAY_OF_WEEK_SATURDAY;
+               break;
+       default:
+               break;
+       }
+
+       return E_M365_DAY_OF_WEEK_UNKNOWN;
+}
+
+static void
+ecb_m365_set_index_to_ical (ICalRecurrence *recr,
+                           EM365WeekIndexType index)
+{
+       gint by_pos = -2;
+
+       switch (index) {
+       case E_M365_WEEK_INDEX_FIRST:
+               by_pos = 1;
+               break;
+       case E_M365_WEEK_INDEX_SECOND:
+               by_pos = 2;
+               break;
+       case E_M365_WEEK_INDEX_THIRD:
+               by_pos = 3;
+               break;
+       case E_M365_WEEK_INDEX_FOURTH:
+               by_pos = 4;
+               break;
+       case E_M365_WEEK_INDEX_LAST:
+               by_pos = -1;
+               break;
+       default:
+               break;
+       }
+
+       if (by_pos != -2)
+               i_cal_recurrence_set_by_set_pos (recr, 0, by_pos);
+}
+
+static void
+ecb_m365_add_index_from_ical (JsonBuilder *builder,
+                             gint by_pos)
+{
+       EM365WeekIndexType index = E_M365_WEEK_INDEX_UNKNOWN;
+
+       if (by_pos == 1)
+               index = E_M365_WEEK_INDEX_FIRST;
+       else if (by_pos == 2)
+               index = E_M365_WEEK_INDEX_SECOND;
+       else if (by_pos == 3)
+               index = E_M365_WEEK_INDEX_THIRD;
+       else if (by_pos == 4)
+               index = E_M365_WEEK_INDEX_FOURTH;
+       else if (by_pos == -1)
+               index = E_M365_WEEK_INDEX_LAST;
+
+       if (index != E_M365_WEEK_INDEX_UNKNOWN)
+               e_m365_recurrence_pattern_add_index (builder, index);
+}
+
+static void
+ecb_m365_set_days_of_week_to_ical (ICalRecurrence *recr,
+                                  JsonArray *days_of_week)
+{
+       gint ii, jj, sz;
+
+       if (!days_of_week)
+               return;
+
+       ii = 0;
+       sz = json_array_get_length (days_of_week);
+
+       for (jj = 0; jj < sz; jj++) {
+               ICalRecurrenceWeekday week_day;
+
+               week_day = ecb_m365_day_of_week_to_ical (e_m365_array_get_day_of_week_element (days_of_week, 
jj));
+
+               if (week_day != I_CAL_SUNDAY_WEEKDAY) {
+                       i_cal_recurrence_set_by_day (recr, ii, week_day);
+                       ii++;
+               }
+       }
+
+       i_cal_recurrence_set_by_day (recr, ii, I_CAL_RECURRENCE_ARRAY_MAX);
+}
+
+static void
+ecb_m365_add_days_of_week_from_ical (JsonBuilder *builder,
+                                    ICalRecurrence *recr)
+{
+       gint ii;
+
+       e_m365_recurrence_pattern_begin_days_of_week (builder);
+
+       for (ii = 0; ii < I_CAL_BY_DAY_SIZE; ii++) {
+               ICalRecurrenceWeekday week_day;
+               EM365DayOfWeekType m365_week_day;
+
+               week_day = i_cal_recurrence_get_by_day (recr, ii);
+
+               if (((gint) week_day) == I_CAL_RECURRENCE_ARRAY_MAX)
+                       break;
+
+               m365_week_day = ecb_m365_day_of_week_from_ical (week_day);
+
+               if (m365_week_day != E_M365_DAY_OF_WEEK_UNKNOWN)
+                       e_m365_recurrence_pattern_add_day_of_week (builder, m365_week_day);
+       }
+
+       e_m365_recurrence_pattern_end_days_of_week (builder);
+}
+
+static gboolean
+ecb_m365_get_recurrence (ECalBackendM365 *cbm365,
+                        JsonObject *m365_object,
+                        ICalComponent *inout_comp,
+                        ICalPropertyKind prop_kind,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       EM365PatternedRecurrence *m365_recr;
+       EM365RecurrencePattern *m365_pattern;
+       EM365RecurrenceRange *m365_range;
+       ICalRecurrence *ical_recr;
+       ICalRecurrenceWeekday week_day;
+       gint month;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               m365_recr = e_m365_event_get_recurrence (m365_object);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               m365_recr = e_m365_task_get_recurrence (m365_object);
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       m365_pattern = m365_recr ? e_m365_patterned_recurrence_get_pattern (m365_recr) : NULL;
+       m365_range = m365_recr ? e_m365_patterned_recurrence_get_range (m365_recr) : NULL;
+
+       if (!m365_recr || !m365_pattern || !m365_range)
+               return TRUE;
+
+       ical_recr = i_cal_recurrence_new ();
+
+       switch (e_m365_recurrence_pattern_get_type (m365_pattern)) {
+       case E_M365_RECURRENCE_PATTERN_DAILY:
+               i_cal_recurrence_set_freq (ical_recr, I_CAL_DAILY_RECURRENCE);
+               i_cal_recurrence_set_interval (ical_recr, e_m365_recurrence_pattern_get_interval 
(m365_pattern));
+               ecb_m365_set_days_of_week_to_ical (ical_recr, e_m365_recurrence_pattern_get_days_of_week 
(m365_pattern));
+               break;
+       case E_M365_RECURRENCE_PATTERN_WEEKLY:
+               i_cal_recurrence_set_freq (ical_recr, I_CAL_WEEKLY_RECURRENCE);
+               i_cal_recurrence_set_interval (ical_recr, e_m365_recurrence_pattern_get_interval 
(m365_pattern));
+
+               week_day = ecb_m365_day_of_week_to_ical (e_m365_recurrence_pattern_get_first_day_of_week 
(m365_pattern));
+
+               if (week_day != I_CAL_NO_WEEKDAY)
+                       i_cal_recurrence_set_week_start (ical_recr, week_day);
+
+               ecb_m365_set_days_of_week_to_ical (ical_recr, e_m365_recurrence_pattern_get_days_of_week 
(m365_pattern));
+               break;
+       case E_M365_RECURRENCE_PATTERN_ABSOLUTE_MONTHLY:
+               i_cal_recurrence_set_freq (ical_recr, I_CAL_MONTHLY_RECURRENCE);
+               i_cal_recurrence_set_interval (ical_recr, e_m365_recurrence_pattern_get_interval 
(m365_pattern));
+               i_cal_recurrence_set_by_month_day (ical_recr, 0, e_m365_recurrence_pattern_get_day_of_month 
(m365_pattern));
+               break;
+       case E_M365_RECURRENCE_PATTERN_RELATIVE_MONTHLY:
+               i_cal_recurrence_set_freq (ical_recr, I_CAL_MONTHLY_RECURRENCE);
+               i_cal_recurrence_set_interval (ical_recr, e_m365_recurrence_pattern_get_interval 
(m365_pattern));
+               ecb_m365_set_days_of_week_to_ical (ical_recr, e_m365_recurrence_pattern_get_days_of_week 
(m365_pattern));
+               week_day = ecb_m365_day_of_week_to_ical (e_m365_recurrence_pattern_get_first_day_of_week 
(m365_pattern));
+
+               if (week_day != I_CAL_NO_WEEKDAY)
+                       i_cal_recurrence_set_week_start (ical_recr, week_day);
+
+               ecb_m365_set_index_to_ical (ical_recr, e_m365_recurrence_pattern_get_index (m365_pattern));
+               break;
+       case E_M365_RECURRENCE_PATTERN_ABSOLUTE_YEARLY:
+               i_cal_recurrence_set_freq (ical_recr, I_CAL_YEARLY_RECURRENCE);
+               i_cal_recurrence_set_interval (ical_recr, e_m365_recurrence_pattern_get_interval 
(m365_pattern));
+               i_cal_recurrence_set_by_month_day (ical_recr, 0, e_m365_recurrence_pattern_get_day_of_month 
(m365_pattern));
+
+               month = e_m365_recurrence_pattern_get_month (m365_pattern);
+
+               if (month >= 1 && month <= 12)
+                       i_cal_recurrence_set_by_month (ical_recr, 0, month);
+               break;
+       case E_M365_RECURRENCE_PATTERN_RELATIVE_YEARLY:
+               i_cal_recurrence_set_freq (ical_recr, I_CAL_YEARLY_RECURRENCE);
+               i_cal_recurrence_set_interval (ical_recr, e_m365_recurrence_pattern_get_interval 
(m365_pattern));
+               ecb_m365_set_days_of_week_to_ical (ical_recr, e_m365_recurrence_pattern_get_days_of_week 
(m365_pattern));
+               week_day = ecb_m365_day_of_week_to_ical (e_m365_recurrence_pattern_get_first_day_of_week 
(m365_pattern));
+
+               if (week_day != I_CAL_NO_WEEKDAY)
+                       i_cal_recurrence_set_week_start (ical_recr, week_day);
+
+               ecb_m365_set_index_to_ical (ical_recr, e_m365_recurrence_pattern_get_index (m365_pattern));
+
+               month = e_m365_recurrence_pattern_get_month (m365_pattern);
+
+               if (month >= 1 && month <= 12)
+                       i_cal_recurrence_set_by_month (ical_recr, 0, month);
+               break;
+       default:
+               g_object_unref (ical_recr);
+               g_warning ("%s: Unknown pattern type: %d", G_STRFUNC, e_m365_recurrence_pattern_get_type 
(m365_pattern));
+               /* Ignore the error (in the code) and continue. */
+               return TRUE;
+       }
+
+       switch (e_m365_recurrence_range_get_type (m365_range)) {
+       case E_M365_RECURRENCE_RANGE_ENDDATE:
+               if (e_m365_recurrence_range_get_end_date (m365_range) > 0) {
+                       gint yy = 0, mm = 0, dd = 0;
+
+                       if (e_m365_date_decode (e_m365_recurrence_range_get_end_date (m365_range), &yy, &mm, 
&dd)) {
+                               ICalTime *itt;
+
+                               itt = i_cal_time_new ();
+                               i_cal_time_set_date (itt, yy, mm, dd);
+                               i_cal_time_set_is_date (itt, TRUE);
+
+                               i_cal_recurrence_set_until (ical_recr, itt);
+
+                               g_clear_object (&itt);
+                       }
+               }
+               break;
+       case E_M365_RECURRENCE_RANGE_NOEND:
+               break;
+       case E_M365_RECURRENCE_RANGE_NUMBERED:
+               i_cal_recurrence_set_count (ical_recr, e_m365_recurrence_range_get_number_of_occurrences 
(m365_range));
+               break;
+       default:
+               g_warning ("%s: Unknown range type: %d", G_STRFUNC, e_m365_recurrence_range_get_type 
(m365_range));
+               g_object_unref (ical_recr);
+               /* Ignore the error (in the code) and continue. */
+               return TRUE;
+       }
+
+       i_cal_component_take_property (inout_comp, i_cal_property_new_rrule (ical_recr));
+
+       g_object_unref (ical_recr);
+
+       return TRUE;
+}
+
+static gboolean
+ecb_m365_add_recurrence (ECalBackendM365 *cbm365,
+                        ICalComponent *new_comp,
+                        ICalComponent *old_comp,
+                        ICalPropertyKind prop_kind,
+                        const gchar *m365_id,
+                        JsonBuilder *builder,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       ICalProperty *new_value, *old_value;
+       gboolean success = TRUE;
+       void (* begin_recurrence_func) (JsonBuilder *builder);
+       void (* end_recurrence_func) (JsonBuilder *builder);
+       void (* add_null_recurrence_func) (JsonBuilder *builder);
+
+       switch (i_cal_component_isa (new_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               begin_recurrence_func = e_m365_event_begin_recurrence;
+               end_recurrence_func = e_m365_event_end_recurrence;
+               add_null_recurrence_func = e_m365_event_add_null_recurrence;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               begin_recurrence_func = e_m365_task_begin_recurrence;
+               end_recurrence_func = e_m365_task_end_recurrence;
+               add_null_recurrence_func = e_m365_task_add_null_recurrence;
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       if (i_cal_component_count_properties (new_comp, prop_kind) > 1) {
+               g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_NOT_SUPPORTED,
+                       _("Microsoft 365 calendar cannot store more than one recurrence")));
+
+               return FALSE;
+       }
+
+       if (i_cal_component_count_properties (new_comp, I_CAL_RDATE_PROPERTY) > 0 ||
+           i_cal_component_count_properties (new_comp, I_CAL_EXDATE_PROPERTY) > 0 ||
+           i_cal_component_count_properties (new_comp, I_CAL_EXRULE_PROPERTY) > 0) {
+               g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_NOT_SUPPORTED,
+                       _("Microsoft 365 calendar cannot store component with RDATE, EXDATE or RRULE 
properties")));
+
+               return FALSE;
+       }
+
+       new_value = i_cal_component_get_first_property (new_comp, prop_kind);
+       old_value = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+
+       if (!new_value && !old_value)
+               return TRUE;
+
+       if (new_value) {
+               ICalRecurrence *new_rrule;
+               gboolean same = FALSE;
+
+               new_rrule = i_cal_property_get_rrule (new_value);
+
+               if (old_value && new_rrule) {
+                       ICalRecurrence *old_rrule;
+
+                       old_rrule = i_cal_property_get_rrule (old_value);
+
+                       if (old_rrule) {
+                               gchar *new_str, *old_str;
+
+                               new_str = i_cal_recurrence_to_string (new_rrule);
+                               old_str = i_cal_recurrence_to_string (old_rrule);
+
+                               same = g_strcmp0 (new_str, old_str) == 0;
+
+                               g_free (new_str);
+                               g_free (old_str);
+                       }
+
+                       g_clear_object (&old_rrule);
+               }
+
+               if (!same && new_rrule) {
+                       EM365DayOfWeekType week_day;
+                       ICalTime *dtstart;
+                       gint by_pos, month, yy = 0, mm = 0, dd = 0;
+
+                       begin_recurrence_func (builder);
+                       e_m365_patterned_recurrence_begin_pattern (builder);
+
+                       switch (i_cal_recurrence_get_freq (new_rrule)) {
+                       case I_CAL_DAILY_RECURRENCE:
+                               e_m365_recurrence_pattern_add_type (builder, E_M365_RECURRENCE_PATTERN_DAILY);
+                               e_m365_recurrence_pattern_add_interval (builder, 
i_cal_recurrence_get_interval (new_rrule));
+                               ecb_m365_add_days_of_week_from_ical (builder, new_rrule);
+                               break;
+                       case I_CAL_WEEKLY_RECURRENCE:
+                               e_m365_recurrence_pattern_add_type (builder, 
E_M365_RECURRENCE_PATTERN_WEEKLY);
+                               e_m365_recurrence_pattern_add_interval (builder, 
i_cal_recurrence_get_interval (new_rrule));
+
+                               week_day = ecb_m365_day_of_week_from_ical (i_cal_recurrence_get_week_start 
(new_rrule));
+
+                               if (week_day != E_M365_DAY_OF_WEEK_UNKNOWN)
+                                       e_m365_recurrence_pattern_add_first_day_of_week (builder, week_day);
+
+                               ecb_m365_add_days_of_week_from_ical (builder, new_rrule);
+                               break;
+                       case I_CAL_MONTHLY_RECURRENCE:
+                               by_pos = i_cal_recurrence_get_by_set_pos (new_rrule, 0);
+
+                               e_m365_recurrence_pattern_add_interval (builder, 
i_cal_recurrence_get_interval (new_rrule));
+
+                               if (by_pos == I_CAL_RECURRENCE_ARRAY_MAX) {
+                                       e_m365_recurrence_pattern_add_type (builder, 
E_M365_RECURRENCE_PATTERN_ABSOLUTE_MONTHLY);
+                                       e_m365_recurrence_pattern_add_day_of_month (builder, 
i_cal_recurrence_get_by_month_day (new_rrule, 0));
+                               } else {
+                                       e_m365_recurrence_pattern_add_type (builder, 
E_M365_RECURRENCE_PATTERN_RELATIVE_MONTHLY);
+
+                                       week_day = ecb_m365_day_of_week_from_ical 
(i_cal_recurrence_get_week_start (new_rrule));
+
+                                       if (week_day != E_M365_DAY_OF_WEEK_UNKNOWN)
+                                               e_m365_recurrence_pattern_add_first_day_of_week (builder, 
week_day);
+
+                                       ecb_m365_add_days_of_week_from_ical (builder, new_rrule);
+                                       ecb_m365_add_index_from_ical (builder, by_pos);
+                               }
+                               break;
+                       case I_CAL_YEARLY_RECURRENCE:
+                               by_pos = i_cal_recurrence_get_by_set_pos (new_rrule, 0);
+
+                               e_m365_recurrence_pattern_add_interval (builder, 
i_cal_recurrence_get_interval (new_rrule));
+
+                               month = i_cal_recurrence_get_by_month (new_rrule, 0);
+
+                               if (month >= 1 && month <= 12)
+                                       e_m365_recurrence_pattern_add_month (builder, month);
+
+                               if (by_pos == I_CAL_RECURRENCE_ARRAY_MAX) {
+                                       e_m365_recurrence_pattern_add_type (builder, 
E_M365_RECURRENCE_PATTERN_ABSOLUTE_YEARLY);
+                                       e_m365_recurrence_pattern_add_day_of_month (builder, 
i_cal_recurrence_get_by_month_day (new_rrule, 0));
+                               } else {
+                                       e_m365_recurrence_pattern_add_type (builder, 
E_M365_RECURRENCE_PATTERN_RELATIVE_YEARLY);
+
+                                       week_day = ecb_m365_day_of_week_from_ical 
(i_cal_recurrence_get_week_start (new_rrule));
+
+                                       if (week_day != E_M365_DAY_OF_WEEK_UNKNOWN)
+                                               e_m365_recurrence_pattern_add_first_day_of_week (builder, 
week_day);
+
+                                       ecb_m365_add_days_of_week_from_ical (builder, new_rrule);
+                                       ecb_m365_add_index_from_ical (builder, by_pos);
+                               }
+
+                               break;
+                       default:
+                               g_set_error (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED,
+                                       _("Unknown recurrence frequency (%d)"), i_cal_recurrence_get_freq 
(new_rrule));
+
+                               success = FALSE;
+                               break;
+                       }
+
+                       e_m365_patterned_recurrence_end_pattern (builder);
+                       e_m365_patterned_recurrence_begin_range (builder);
+
+                       dtstart = i_cal_component_get_dtstart (new_comp);
+                       i_cal_time_get_date (dtstart, &yy, &mm, &dd);
+                       g_clear_object (&dtstart);
+
+                       e_m365_recurrence_range_add_start_date (builder, e_m365_date_encode (yy, mm, dd));
+
+                       if (!i_cal_recurrence_get_count (new_rrule)) {
+                               ICalTime *until;
+                               gint yy = 0, mm = 0, dd = 0;
+
+                               until = i_cal_recurrence_get_until (new_rrule);
+
+                               if (until)
+                                       i_cal_time_get_date (until, &yy, &mm, &dd);
+
+                               if (!until || yy == 0) {
+                                       e_m365_recurrence_range_add_type (builder, 
E_M365_RECURRENCE_RANGE_NOEND);
+                               } else {
+                                       e_m365_recurrence_range_add_type (builder, 
E_M365_RECURRENCE_RANGE_ENDDATE);
+                                       e_m365_recurrence_range_add_end_date (builder, e_m365_date_encode 
(yy, mm, dd));
+                               }
+
+                               g_clear_object (&until);
+                       } else {
+                               e_m365_recurrence_range_add_type (builder, E_M365_RECURRENCE_RANGE_NUMBERED);
+                               e_m365_recurrence_range_add_number_of_occurrences (builder, 
i_cal_recurrence_get_count (new_rrule));
+                       }
+
+                       e_m365_patterned_recurrence_end_range (builder);
+                       end_recurrence_func (builder);
+               }
+
+               g_clear_object (&new_rrule);
+       } else {
+               add_null_recurrence_func (builder);
+       }
+
+       g_clear_object (&new_value);
+       g_clear_object (&old_value);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_get_reminder (ECalBackendM365 *cbm365,
+                      EM365Event *m365_object,
+                      ICalComponent *inout_comp,
+                      ICalPropertyKind prop_kind,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               if (e_m365_event_get_is_reminder_on (m365_object)) {
+                       ECalComponentAlarm *alarm;
+                       ECalComponentAlarmTrigger *trigger;
+                       ICalDuration *duration;
+
+                       duration = i_cal_duration_new_from_int (-60 * 
e_m365_event_get_reminder_minutes_before_start (m365_object));
+                       trigger = e_cal_component_alarm_trigger_new_relative 
(E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START, duration);
+                       g_object_unref (duration);
+
+                       alarm = e_cal_component_alarm_new ();
+                       e_cal_component_alarm_set_action (alarm, E_CAL_COMPONENT_ALARM_DISPLAY);
+                       e_cal_component_alarm_take_summary (alarm, e_cal_component_text_new 
(e_m365_event_get_subject (m365_object), NULL));
+                       e_cal_component_alarm_take_description (alarm, e_cal_component_text_new 
(e_m365_event_get_subject (m365_object), NULL));
+                       e_cal_component_alarm_take_trigger (alarm, trigger);
+
+                       i_cal_component_take_component (inout_comp, e_cal_component_alarm_get_as_component 
(alarm));
+
+                       e_cal_component_alarm_free (alarm);
+               }
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               if (e_m365_task_get_is_reminder_on (m365_object)) {
+                       EM365DateTimeWithZone *reminder_dt;
+
+                       reminder_dt = e_m365_task_get_reminder_date_time (m365_object);
+
+                       if (reminder_dt) {
+                               ECalComponentAlarm *alarm;
+                               ECalComponentAlarmTrigger *trigger;
+                               ICalTimezone *tz;
+                               ICalTime *itt;
+                               time_t tt;
+                               const gchar *zone;
+
+                               tt = e_m365_date_time_get_date_time (reminder_dt);
+                               zone = e_m365_date_time_get_time_zone (reminder_dt);
+
+                               if (zone && *zone)
+                                       zone = e_m365_tz_utils_get_ical_equivalent (zone);
+
+                               tz = zone && *zone ? ecb_m365_get_timezone_sync (cbm365, zone) : NULL;
+
+                               if (!tz)
+                                       tz = i_cal_timezone_get_utc_timezone ();
+
+                               itt = i_cal_time_new_from_timet_with_zone (tt, FALSE, tz);
+                               trigger = e_cal_component_alarm_trigger_new_absolute (itt);
+                               g_object_unref (itt);
+
+                               alarm = e_cal_component_alarm_new ();
+                               e_cal_component_alarm_set_action (alarm, E_CAL_COMPONENT_ALARM_DISPLAY);
+                               e_cal_component_alarm_take_summary (alarm, e_cal_component_text_new 
(e_m365_task_get_subject (m365_object), NULL));
+                               e_cal_component_alarm_take_description (alarm, e_cal_component_text_new 
(e_m365_task_get_subject (m365_object), NULL));
+                               e_cal_component_alarm_take_trigger (alarm, trigger);
+
+                               i_cal_component_take_component (inout_comp, 
e_cal_component_alarm_get_as_component (alarm));
+
+                               e_cal_component_alarm_free (alarm);
+                       }
+               }
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+ecb_m365_add_reminder (ECalBackendM365 *cbm365,
+                      ICalComponent *new_comp,
+                      ICalComponent *old_comp,
+                      ICalPropertyKind prop_kind,
+                      const gchar *m365_id,
+                      JsonBuilder *builder,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       ICalComponent *new_value, *old_value;
+       gboolean success = TRUE;
+
+       if (i_cal_component_count_components (new_comp, I_CAL_VALARM_COMPONENT) > 1) {
+               g_propagate_error (error, ECC_ERROR_EX (E_CAL_CLIENT_ERROR_INVALID_OBJECT, _("Microsoft 365 
calendar cannot store more that one event reminder")));
+               return FALSE;
+       }
+
+       new_value = i_cal_component_get_first_component (new_comp, I_CAL_VALARM_COMPONENT);
+       old_value = old_comp ? i_cal_component_get_first_component (old_comp, I_CAL_VALARM_COMPONENT) : NULL;
+
+       if (!new_value && !old_value)
+               return TRUE;
+
+       if (new_value) {
+               ECalComponentAlarm *new_alarm;
+               ECalComponentAlarmTrigger *new_trigger;
+               ICalComponentKind kind;
+               ICalDuration *new_duration = NULL;
+               ICalTime *new_absolute_time = NULL;
+               gboolean changed = TRUE;
+
+               kind = i_cal_component_isa (new_comp);
+
+               new_alarm = e_cal_component_alarm_new_from_component (new_value);
+               new_trigger = new_alarm ? e_cal_component_alarm_get_trigger (new_alarm) : NULL;
+
+               switch (kind) {
+               case I_CAL_VEVENT_COMPONENT:
+                       success = new_trigger && e_cal_component_alarm_trigger_get_kind (new_trigger) == 
E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START;
+                       if (success) {
+                               new_duration = e_cal_component_alarm_trigger_get_duration (new_trigger);
+
+                               success = new_duration && i_cal_duration_as_int (new_duration) <= 0;
+                       }
+
+                       if (!success) {
+                               g_propagate_error (error, ECC_ERROR_EX (E_CAL_CLIENT_ERROR_INVALID_OBJECT, 
_("Microsoft 365 event can have only a reminder before event start")));
+                       }
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       success = new_trigger && e_cal_component_alarm_trigger_get_kind (new_trigger) == 
E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE;
+
+                       if (success) {
+                               new_absolute_time = e_cal_component_alarm_trigger_get_absolute_time 
(new_trigger);
+
+                               success = new_absolute_time != NULL;
+                       }
+
+                       if (!success) {
+                               g_propagate_error (error, ECC_ERROR_EX (E_CAL_CLIENT_ERROR_INVALID_OBJECT, 
_("Microsoft 365 task can have only a reminder with absolute time")));
+                       }
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       success = FALSE;
+                       break;
+               }
+
+               if (success && old_value && new_trigger) {
+                       ECalComponentAlarm *old_alarm;
+                       ECalComponentAlarmTrigger *old_trigger;
+
+                       old_alarm = e_cal_component_alarm_new_from_component (old_value);
+                       old_trigger = old_alarm ? e_cal_component_alarm_get_trigger (old_alarm) : NULL;
+
+                       if (old_trigger) {
+                               changed = e_cal_component_alarm_trigger_get_kind (new_trigger) != 
e_cal_component_alarm_trigger_get_kind (old_trigger);
+
+                               if (!changed) {
+                                       ICalDuration *old_duration;
+                                       ICalTime *old_absolute_time;
+
+                                       switch (kind) {
+                                       case I_CAL_VEVENT_COMPONENT:
+                                               old_duration = e_cal_component_alarm_trigger_get_duration 
(old_trigger);
+
+                                               changed = !old_duration || i_cal_duration_as_int 
(new_duration) != i_cal_duration_as_int (old_duration);
+                                               break;
+                                       case I_CAL_VTODO_COMPONENT:
+                                               old_absolute_time = 
e_cal_component_alarm_trigger_get_absolute_time (old_trigger);
+
+                                               changed = !old_absolute_time || i_cal_time_compare 
(new_absolute_time, old_absolute_time) != 0;
+                                               break;
+                                       default:
+                                               g_warn_if_reached ();
+                                               changed = FALSE;
+                                               break;
+                                       }
+                               }
+                       }
+
+                       e_cal_component_alarm_free (old_alarm);
+               }
+
+               if (success && changed) {
+                       ICalTimezone *izone = NULL;
+                       const gchar *wzone = NULL;
+                       time_t tt;
+
+                       switch (kind) {
+                       case I_CAL_VEVENT_COMPONENT:
+                               e_m365_event_add_is_reminder_on (builder, TRUE);
+                               e_m365_event_add_reminder_minutes_before_start (builder, 
i_cal_duration_as_int (new_duration) / -60);
+                               break;
+                       case I_CAL_VTODO_COMPONENT:
+                               izone = i_cal_time_get_timezone (new_absolute_time);
+
+                               if (izone)
+                                       wzone = e_m365_tz_utils_get_msdn_equivalent 
(i_cal_timezone_get_location (izone));
+
+                               tt = i_cal_time_as_timet_with_zone (new_absolute_time, wzone ? NULL : izone);
+
+                               e_m365_task_add_is_reminder_on (builder, TRUE);
+                               e_m365_task_add_reminder_date_time (builder, tt, wzone);
+                               break;
+                       default:
+                               g_warn_if_reached ();
+                               break;
+                       }
+               }
+
+               e_cal_component_alarm_free (new_alarm);
+       } else {
+               switch (i_cal_component_isa (new_comp)) {
+               case I_CAL_VEVENT_COMPONENT:
+                       e_m365_event_add_is_reminder_on (builder, FALSE);
+                       break;
+               case I_CAL_VTODO_COMPONENT:
+                       e_m365_task_add_is_reminder_on (builder, FALSE);
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       break;
+               }
+       }
+
+       g_clear_object (&new_value);
+       g_clear_object (&old_value);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_get_attachments (ECalBackendM365 *cbm365,
+                         JsonObject *m365_object,
+                         ICalComponent *inout_comp,
+                         ICalPropertyKind prop_kind,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       GSList *attachments = NULL, *link;
+       const gchar *id;
+       gboolean success = TRUE;
+
+       switch (i_cal_component_isa (inout_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               if (!e_m365_event_get_has_attachments (m365_object))
+                       return TRUE;
+
+               id = e_m365_event_get_id (m365_object);
+
+               if (!e_m365_connection_list_event_attachments_sync (cbm365->priv->cnc, NULL,
+                       cbm365->priv->group_id, cbm365->priv->folder_id, id, 
"id,name,contentType,contentBytes",
+                       &attachments, cancellable, error)) {
+                       return FALSE;
+               }
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               if (!e_m365_task_get_has_attachments (m365_object))
+                       return TRUE;
+
+               id = e_m365_task_get_id (m365_object);
+
+               if (!e_m365_connection_list_task_attachments_sync (cbm365->priv->cnc, NULL,
+                       cbm365->priv->group_id, cbm365->priv->folder_id, id, 
"id,name,contentType,contentBytes",
+                       &attachments, cancellable, error)) {
+                       return FALSE;
+               }
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       for (link = attachments; link && success; link = g_slist_next (link)) {
+               CamelStream *content_stream;
+               EM365Attachment *m365_attach = link->data;
+               gchar *filename;
+
+               if (!m365_attach || e_m365_attachment_get_data_type (m365_attach) != 
E_M365_ATTACHMENT_DATA_TYPE_FILE ||
+                   !e_m365_attachment_get_name (m365_attach))
+                       continue;
+
+               filename = g_build_filename (cbm365->priv->attachments_dir, id, e_m365_attachment_get_id 
(m365_attach), NULL);
+
+               content_stream = camel_stream_fs_new_with_name (filename, O_CREAT | O_TRUNC | O_WRONLY, 0666, 
error);
+
+               if (content_stream) {
+                       CamelMimeFilter *filter;
+                       CamelStream *filter_stream;
+                       const gchar *base64_data;
+
+                       filter_stream = camel_stream_filter_new (content_stream);
+
+                       filter = camel_mime_filter_basic_new (CAMEL_MIME_FILTER_BASIC_BASE64_DEC);
+                       camel_stream_filter_add (CAMEL_STREAM_FILTER (filter_stream), filter);
+                       g_object_unref (filter);
+
+                       base64_data = e_m365_file_attachment_get_content_bytes (m365_attach);
+
+                       if (base64_data && *base64_data)
+                               success = camel_stream_write (filter_stream, base64_data, strlen 
(base64_data), cancellable, error) != -1;
+
+                       camel_stream_flush (filter_stream, cancellable, NULL);
+                       g_object_unref (filter_stream);
+
+                       camel_stream_flush (content_stream, cancellable, NULL);
+                       g_object_unref (content_stream);
+
+                       if (success) {
+                               gchar *uri;
+
+                               uri = g_filename_to_uri (filename, NULL, error);
+
+                               if (uri) {
+                                       ICalAttach *ical_attach;
+                                       ICalParameter *param;
+                                       ICalProperty *prop;
+                                       gchar *enc_uri;
+                                       const gchar *tmp;
+
+                                       enc_uri = i_cal_value_encode_ical_string (uri);
+                                       ical_attach = i_cal_attach_new_from_url (enc_uri);
+                                       prop = i_cal_property_new_attach (ical_attach);
+
+                                       tmp = e_m365_attachment_get_name (m365_attach);
+
+                                       if (!tmp || !*tmp)
+                                               tmp = "attachment.dat";
+
+                                       param = i_cal_parameter_new_filename (tmp);
+                                       i_cal_property_take_parameter (prop, param);
+
+                                       tmp = e_m365_attachment_get_content_type (m365_attach);
+
+                                       if (tmp && *tmp) {
+                                               param = i_cal_parameter_new_fmttype (tmp);
+                                               i_cal_property_take_parameter (prop, param);
+                                       }
+
+                                       param = i_cal_parameter_new_x (e_m365_attachment_get_id 
(m365_attach));
+                                       i_cal_parameter_set_xname (param, "X-M365-ATTACHMENTID");
+                                       i_cal_property_take_parameter (prop, param);
+
+                                       i_cal_component_take_property (inout_comp, prop);
+
+                                       g_object_unref (ical_attach);
+                                       g_free (enc_uri);
+                                       g_free (uri);
+                               } else {
+                                       success = FALSE;
+                               }
+                       }
+               } else {
+                       success = FALSE;
+               }
+
+               g_free (filename);
+       }
+
+       g_slist_free_full (attachments, (GDestroyNotify) json_object_unref);
+
+       return success;
+}
+
+static void
+ecb_m365_extract_attachments (ICalComponent *comp,
+                             GHashTable **out_hash, /* gchar *attachment_id ~> ICalProperty * */
+                             GSList **out_slist) /* ICalProperty * */
+{
+       ICalProperty *prop;
+
+       if (!comp)
+               return;
+
+       for (prop = i_cal_component_get_first_property (comp, I_CAL_ATTACH_PROPERTY);
+            prop;
+            g_object_unref (prop), prop = i_cal_component_get_next_property (comp, I_CAL_ATTACH_PROPERTY)) {
+               if (out_slist) {
+                       *out_slist = g_slist_prepend (*out_slist, g_object_ref (prop));
+               } else if (out_hash) {
+                       gchar *attach_id;
+
+                       attach_id = i_cal_property_get_parameter_as_string (prop, "X-M365-ATTACHMENTID");
+                       g_warn_if_fail (attach_id != NULL);
+
+                       if (attach_id) {
+                               if (!*out_hash)
+                                       *out_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
g_object_unref);
+
+                               g_hash_table_insert (*out_hash, attach_id, g_object_ref (prop));
+                       }
+               } else {
+                       g_warn_if_reached ();
+               }
+       }
+
+       g_clear_object (&prop);
+
+       if (out_slist && *out_slist)
+               *out_slist = g_slist_reverse (*out_slist);
+}
+
+static gboolean
+ecb_m365_add_attachments (ECalBackendM365 *cbm365,
+                         ICalComponent *new_comp,
+                         ICalComponent *old_comp,
+                         ICalPropertyKind prop_kind,
+                         const gchar *m365_id,
+                         JsonBuilder *builder,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       GSList *new_attachs = NULL;
+       GHashTable *old_attachs = NULL;
+       gboolean (* add_attachment_func) (EM365Connection *cnc,
+                                         const gchar *user_override,
+                                         const gchar *group_id,
+                                         const gchar *folder_id,
+                                         const gchar *item_id,
+                                         JsonBuilder *in_attachment,
+                                         EM365Attachment **out_attachment,
+                                         GCancellable *cancellable,
+                                         GError **error);
+       gboolean (* delete_attachment_func) (EM365Connection *cnc,
+                                            const gchar *user_override,
+                                            const gchar *group_id,
+                                            const gchar *folder_id,
+                                            const gchar *item_id,
+                                            const gchar *attachment_id,
+                                            GCancellable *cancellable,
+                                            GError **error);
+       gboolean success = TRUE;
+
+       switch (i_cal_component_isa (new_comp)) {
+       case I_CAL_VEVENT_COMPONENT:
+               add_attachment_func = e_m365_connection_add_event_attachment_sync;
+               delete_attachment_func = e_m365_connection_delete_event_attachment_sync;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               add_attachment_func = e_m365_connection_add_task_attachment_sync;
+               delete_attachment_func = e_m365_connection_delete_task_attachment_sync;
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       if (!i_cal_component_count_properties (new_comp, I_CAL_ATTACH_PROPERTY) &&
+           !(old_comp ? i_cal_component_count_properties (old_comp, I_CAL_ATTACH_PROPERTY) : 0)) {
+               return TRUE;
+       }
+
+       ecb_m365_extract_attachments (new_comp, NULL, &new_attachs);
+       ecb_m365_extract_attachments (old_comp, &old_attachs, NULL);
+
+       if (new_attachs) {
+               GSList *link, *save_attachs = new_attachs;
+
+               if (old_attachs) {
+                       save_attachs = NULL;
+
+                       for (link = new_attachs; link; link = g_slist_next (link)) {
+                               ICalProperty *prop = link->data;
+                               gchar *attach_id;
+
+                               attach_id = i_cal_property_get_parameter_as_string (prop, 
"X-M365-ATTACHMENTID");
+
+                               if (!attach_id || !g_hash_table_remove (old_attachs, attach_id)) {
+                                       save_attachs = g_slist_prepend (save_attachs, g_object_ref (prop));
+                               }
+                       }
+
+                       if (save_attachs)
+                               save_attachs = g_slist_reverse (save_attachs);
+               }
+
+               for (link = save_attachs; link && success; link = g_slist_next (link)) {
+                       ICalProperty *prop = link->data;
+                       ICalAttach *attach;
+                       JsonBuilder *builder = NULL;
+
+                       attach = i_cal_property_get_attach (prop);
+
+                       if (!attach)
+                               continue;
+
+                       if (i_cal_attach_get_is_url (attach)) {
+                               const gchar *data;
+                               gchar *uri;
+
+                               data = i_cal_attach_get_url (attach);
+                               uri = i_cal_value_decode_ical_string (data);
+
+                               if (uri && g_ascii_strncasecmp (uri, "file://", 7) == 0) {
+                                       CamelStream *content_stream;
+                                       gchar *filename;
+
+                                       filename = g_filename_from_uri (uri, NULL, error);
+                                       content_stream = filename ? camel_stream_fs_new_with_name (filename, 
O_RDONLY, 0, error) : NULL;
+
+                                       if (content_stream) {
+                                               CamelMimeFilter *filter;
+                                               CamelStream *filter_stream;
+                                               CamelStream *base64_stream;
+
+                                               base64_stream = camel_stream_mem_new ();
+                                               filter_stream = camel_stream_filter_new (base64_stream);
+
+                                               filter = camel_mime_filter_basic_new 
(CAMEL_MIME_FILTER_BASIC_BASE64_ENC);
+                                               camel_stream_filter_add (CAMEL_STREAM_FILTER (filter_stream), 
filter);
+                                               g_object_unref (filter);
+
+                                               success = camel_stream_write_to_stream (content_stream, 
filter_stream, cancellable, error) != -1;
+
+                                               camel_stream_flush (filter_stream, cancellable, NULL);
+                                               g_object_unref (filter_stream);
+
+                                               /* Ensure the stream is NUL-terminated, thus it can be used 
as a string */
+                                               camel_stream_write (base64_stream, "\0", 1, cancellable, 
NULL);
+
+                                               camel_stream_flush (base64_stream, cancellable, NULL);
+                                               g_object_unref (content_stream);
+
+                                               if (success) {
+                                                       GByteArray *bytes;
+
+                                                       bytes = camel_stream_mem_get_byte_array 
(CAMEL_STREAM_MEM (base64_stream));
+
+                                                       builder = json_builder_new_immutable ();
+                                                       e_m365_attachment_begin_attachment (builder, 
E_M365_ATTACHMENT_DATA_TYPE_FILE);
+                                                       e_m365_file_attachment_add_content_bytes (builder, 
(const gchar *) bytes->data);
+                                               }
+
+                                               g_object_unref (base64_stream);
+                                       } else {
+                                               success = FALSE;
+                                       }
+
+                                       g_free (filename);
+                               } else {
+                                       success = FALSE;
+
+                                       if (uri)
+                                               g_set_error (error, E_CLIENT_ERROR, 
E_CLIENT_ERROR_OTHER_ERROR, _("Cannot store attachment with URI ā€œ%sā€"), uri);
+                                       else
+                                               g_propagate_error (error, EC_ERROR_EX 
(E_CLIENT_ERROR_OTHER_ERROR, _("Failed to read attachment URI")));
+                               }
+
+                               g_free (uri);
+                       } else {
+                               const gchar *base64_data;
+
+                               base64_data = i_cal_attach_get_data (attach);
+
+                               if (base64_data) {
+                                       builder = json_builder_new_immutable ();
+                                       e_m365_attachment_begin_attachment (builder, 
E_M365_ATTACHMENT_DATA_TYPE_FILE);
+                                       e_m365_file_attachment_add_content_bytes (builder, base64_data);
+                               } else {
+                                       g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_OTHER_ERROR, 
_("Failed to get inline attachment data")));
+                               }
+                       }
+
+                       if (builder) {
+                               ICalParameter *param;
+                               const gchar *tmp;
+
+                               param = i_cal_property_get_first_parameter (prop, I_CAL_FILENAME_PARAMETER);
+
+                               if (param) {
+                                       tmp = i_cal_parameter_get_filename (param);
+
+                                       if (tmp && *tmp)
+                                               e_m365_attachment_add_name (builder, tmp);
+
+                                       g_clear_object (&param);
+                               }
+
+                               param = i_cal_property_get_first_parameter (prop, I_CAL_FMTTYPE_PARAMETER);
+
+                               if (param) {
+                                       tmp = i_cal_parameter_get_fmttype (param);
+
+                                       if (tmp && *tmp)
+                                               e_m365_attachment_add_content_type (builder, tmp);
+                                       else
+                                               e_m365_attachment_add_content_type (builder, 
"application/octet-stream");
+
+                                       g_clear_object (&param);
+                               } else {
+                                       e_m365_attachment_add_content_type (builder, 
"application/octet-stream");
+                               }
+
+                               e_m365_attachment_end_attachment (builder);
+
+                               success = add_attachment_func (cbm365->priv->cnc, NULL,
+                                       cbm365->priv->group_id, cbm365->priv->folder_id, m365_id,
+                                       builder, NULL, cancellable, error);
+
+                               g_object_unref (builder);
+                       }
+
+                       g_object_unref (attach);
+               }
+
+               if (save_attachs != new_attachs)
+                       g_slist_free_full (save_attachs, g_object_unref);
+       }
+
+       if (old_attachs && success) {
+               GHashTableIter iter;
+               gpointer key;
+
+               g_hash_table_iter_init (&iter, old_attachs);
+
+               while (g_hash_table_iter_next (&iter, &key, NULL) && success) {
+                       const gchar *attachment_id = key;
+
+                       success = delete_attachment_func (cbm365->priv->cnc, NULL,
+                               cbm365->priv->group_id, cbm365->priv->folder_id, i_cal_component_get_uid 
(new_comp),
+                               attachment_id, cancellable, error);
+               }
+       }
+
+       if (old_attachs)
+               g_hash_table_destroy (old_attachs);
+       g_slist_free_full (new_attachs, g_object_unref);
+
+       return success;
+}
+
+static void
+ecb_m365_get_task_status (ECalBackendM365 *cbm365,
+                         EM365Task *m365_task,
+                         ICalComponent *inout_comp,
+                         ICalPropertyKind prop_kind)
+{
+       ICalPropertyStatus status = I_CAL_STATUS_NONE;
+
+       switch (e_m365_task_get_status (m365_task)) {
+       case E_M365_STATUS_NOT_STARTED:
+               break;
+       case E_M365_STATUS_IN_PROGRESS:
+       case E_M365_STATUS_WAITING_ON_OTHERS:
+               status = I_CAL_STATUS_INPROCESS;
+               break;
+       case E_M365_STATUS_COMPLETED:
+               status = I_CAL_STATUS_COMPLETED;
+               break;
+       case E_M365_STATUS_DEFERRED:
+               status = I_CAL_STATUS_CANCELLED;
+               break;
+       default:
+               break;
+       }
+
+       if (status != I_CAL_STATUS_NONE)
+               i_cal_component_take_property (inout_comp, i_cal_property_new_status (status));
+}
+
+static void
+ecb_m365_add_task_status (ECalBackendM365 *cbm365,
+                         ICalComponent *new_comp,
+                         ICalComponent *old_comp,
+                         ICalPropertyKind prop_kind,
+                         JsonBuilder *builder)
+{
+       ICalProperty *new_prop, *old_prop;
+       ICalPropertyStatus new_value, old_value;
+
+       new_prop = i_cal_component_get_first_property (new_comp, prop_kind);
+       old_prop = old_comp ? i_cal_component_get_first_property (old_comp, prop_kind) : NULL;
+
+       if (!new_prop && !old_prop)
+               return;
+
+       new_value = new_prop ? i_cal_property_get_status (new_prop) : I_CAL_STATUS_NONE;
+       old_value = old_prop ? i_cal_property_get_status (old_prop) : I_CAL_STATUS_NONE;
+
+       if (new_value != old_value) {
+               EM365StatusType value = E_M365_STATUS_UNKNOWN;
+
+               switch (new_value) {
+               case I_CAL_STATUS_NONE:
+                       value = E_M365_STATUS_NOT_STARTED;
+                       break;
+               case I_CAL_STATUS_INPROCESS:
+                       value = E_M365_STATUS_IN_PROGRESS;
+                       break;
+               case I_CAL_STATUS_COMPLETED:
+                       value = E_M365_STATUS_COMPLETED;
+                       break;
+               case I_CAL_STATUS_CANCELLED:
+                       value = E_M365_STATUS_DEFERRED;
+                       break;
+               default:
+                       break;
+               }
+
+               if (value != E_M365_STATUS_UNKNOWN)
+                       e_m365_task_add_status (builder, value);
+       }
+
+       g_clear_object (&new_prop);
+       g_clear_object (&old_prop);
+}
+
+#define SIMPLE_FIELD(propknd, getfn, addfn) { propknd, FALSE, getfn, NULL, addfn, NULL }
+#define COMPLEX_FIELD(propknd, getfn, addfn) { propknd, FALSE, NULL, getfn, NULL, addfn }
+#define COMPLEX_FIELD_2(propknd, getfn, addfn) { propknd, TRUE, NULL, getfn, NULL, addfn }
+
+struct _mappings {
+       ICalPropertyKind prop_kind;
+       gboolean add_in_second_go;
+       void            (* get_simple_func)     (ECalBackendM365 *cbm365,
+                                                EM365Event *m365_event,
+                                                ICalComponent *inout_comp,
+                                                ICalPropertyKind prop_kind);
+       gboolean        (* get_func)            (ECalBackendM365 *cbm365,
+                                                EM365Event *m365_event,
+                                                ICalComponent *inout_comp,
+                                                ICalPropertyKind prop_kind,
+                                                GCancellable *cancellable,
+                                                GError **error);
+       void            (* add_simple_func)     (ECalBackendM365 *cbm365,
+                                                ICalComponent *new_comp,
+                                                ICalComponent *old_comp, /* nullable */
+                                                ICalPropertyKind prop_kind,
+                                                JsonBuilder *builder);
+       gboolean        (* add_func)            (ECalBackendM365 *cbm365,
+                                                ICalComponent *new_comp,
+                                                ICalComponent *old_comp, /* nullable */
+                                                ICalPropertyKind prop_kind,
+                                                const gchar *m365_id,
+                                                JsonBuilder *builder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+} event_mappings[] = {
+       SIMPLE_FIELD    (I_CAL_UID_PROPERTY,            ecb_m365_get_uid,               NULL),
+       SIMPLE_FIELD    (I_CAL_CREATED_PROPERTY,        ecb_m365_get_date_time,         NULL),
+       SIMPLE_FIELD    (I_CAL_LASTMODIFIED_PROPERTY,   ecb_m365_get_date_time,         NULL),
+       SIMPLE_FIELD    (I_CAL_DTSTART_PROPERTY,        ecb_m365_get_date_time_zone,    
ecb_m365_add_date_time_zone),
+       SIMPLE_FIELD    (I_CAL_DTEND_PROPERTY,          ecb_m365_get_date_time_zone,    
ecb_m365_add_date_time_zone),
+       SIMPLE_FIELD    (I_CAL_CATEGORIES_PROPERTY,     ecb_m365_get_categories,        
ecb_m365_add_categories),
+       SIMPLE_FIELD    (I_CAL_SUMMARY_PROPERTY,        ecb_m365_get_subject,           ecb_m365_add_subject),
+       SIMPLE_FIELD    (I_CAL_DESCRIPTION_PROPERTY,    ecb_m365_get_body,              ecb_m365_add_body),
+       SIMPLE_FIELD    (I_CAL_CLASS_PROPERTY,          ecb_m365_get_sensitivity,       
ecb_m365_add_sensitivity),
+       SIMPLE_FIELD    (I_CAL_TRANSP_PROPERTY,         ecb_m365_get_show_as,           ecb_m365_add_show_as),
+       SIMPLE_FIELD    (I_CAL_LOCATION_PROPERTY,       ecb_m365_get_location,          
ecb_m365_add_location),
+       SIMPLE_FIELD    (I_CAL_ORGANIZER_PROPERTY,      ecb_m365_get_organizer,         
ecb_m365_add_organizer),
+       SIMPLE_FIELD    (I_CAL_ATTENDEE_PROPERTY,       ecb_m365_get_attendees,         
ecb_m365_add_attendees),
+       SIMPLE_FIELD    (I_CAL_PRIORITY_PROPERTY,       ecb_m365_get_importance,        
ecb_m365_add_importance),
+       SIMPLE_FIELD    (I_CAL_STATUS_PROPERTY,         ecb_m365_get_event_status,      NULL),
+       COMPLEX_FIELD   (I_CAL_RRULE_PROPERTY,          ecb_m365_get_recurrence,        
ecb_m365_add_recurrence),
+       COMPLEX_FIELD   (I_CAL_X_PROPERTY,              ecb_m365_get_reminder,          
ecb_m365_add_reminder),
+       COMPLEX_FIELD_2 (I_CAL_ATTACH_PROPERTY,         ecb_m365_get_attachments,       
ecb_m365_add_attachments)
+}, task_mappings[] = {
+       SIMPLE_FIELD    (I_CAL_UID_PROPERTY,            ecb_m365_get_uid,               NULL),
+       SIMPLE_FIELD    (I_CAL_CREATED_PROPERTY,        ecb_m365_get_date_time,         NULL),
+       SIMPLE_FIELD    (I_CAL_LASTMODIFIED_PROPERTY,   ecb_m365_get_date_time,         NULL),
+       SIMPLE_FIELD    (I_CAL_DTSTART_PROPERTY,        ecb_m365_get_date_time_zone,    
ecb_m365_add_date_time_zone),
+       SIMPLE_FIELD    (I_CAL_DUE_PROPERTY,            ecb_m365_get_date_time_zone,    
ecb_m365_add_date_time_zone),
+       SIMPLE_FIELD    (I_CAL_COMPLETED_PROPERTY,      ecb_m365_get_date_time_zone,    
ecb_m365_add_date_time_zone),
+       SIMPLE_FIELD    (I_CAL_CATEGORIES_PROPERTY,     ecb_m365_get_categories,        
ecb_m365_add_categories),
+       SIMPLE_FIELD    (I_CAL_SUMMARY_PROPERTY,        ecb_m365_get_subject,           ecb_m365_add_subject),
+       SIMPLE_FIELD    (I_CAL_DESCRIPTION_PROPERTY,    ecb_m365_get_body,              ecb_m365_add_body),
+       SIMPLE_FIELD    (I_CAL_CLASS_PROPERTY,          ecb_m365_get_sensitivity,       
ecb_m365_add_sensitivity),
+       SIMPLE_FIELD    (I_CAL_STATUS_PROPERTY,         ecb_m365_get_task_status,       
ecb_m365_add_task_status),
+       COMPLEX_FIELD   (I_CAL_RRULE_PROPERTY,          ecb_m365_get_recurrence,        
ecb_m365_add_recurrence),
+       COMPLEX_FIELD   (I_CAL_X_PROPERTY,              ecb_m365_get_reminder,          
ecb_m365_add_reminder),
+       COMPLEX_FIELD_2 (I_CAL_ATTACH_PROPERTY,         ecb_m365_get_attachments,       
ecb_m365_add_attachments)
+};
+
+static const struct _mappings *
+ecb_m365_get_mappings_for_backend (ECalBackendM365 *cbm365,
+                                  guint *out_n_elements)
+{
+       ICalComponentKind kind;
+
+       kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbm365));
+
+       if (kind == I_CAL_VEVENT_COMPONENT) {
+               *out_n_elements = G_N_ELEMENTS (event_mappings);
+               return event_mappings;
+       }
+
+       if (kind == I_CAL_VTODO_COMPONENT) {
+               *out_n_elements = G_N_ELEMENTS (task_mappings);
+               return task_mappings;
+       }
+
+       g_warn_if_reached ();
+
+       return NULL;
+}
+
+static gchar *
+ecb_m365_join_to_extra (const gchar *change_key,
+                       const gchar *ical_comp)
+{
+       if (!change_key && !ical_comp)
+               return NULL;
+
+       return g_strconcat (change_key ? change_key : "", "\n", ical_comp, NULL);
+}
+
+static ICalComponent *
+ecb_m365_json_to_ical (ECalBackendM365 *cbm365,
+                      JsonObject *m365_object,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       const struct _mappings *mappings;
+       ICalComponent *icomp = NULL;
+       ICalComponentKind kind;
+       guint ii, n_elements = 0;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (m365_object != NULL, NULL);
+
+       mappings = ecb_m365_get_mappings_for_backend (cbm365, &n_elements);
+       g_return_val_if_fail (mappings != NULL, NULL);
+
+       kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbm365));
+
+       if (kind == I_CAL_VEVENT_COMPONENT)
+               icomp = i_cal_component_new_vevent ();
+       else if (kind == I_CAL_VTODO_COMPONENT)
+               icomp = i_cal_component_new_vtodo ();
+       else
+               g_warn_if_reached ();
+
+       if (!icomp)
+               return NULL;
+
+       for (ii = 0; success && ii < n_elements; ii++) {
+               if (mappings[ii].get_simple_func) {
+                       mappings[ii].get_simple_func (cbm365, m365_object, icomp, mappings[ii].prop_kind);
+               } else if (mappings[ii].get_func) {
+                       success = mappings[ii].get_func (cbm365, m365_object, icomp, mappings[ii].prop_kind, 
cancellable, error);
+               }
+       }
+
+       if (!success)
+               g_clear_object (&icomp);
+
+       return icomp;
+}
+
+static ECalMetaBackendInfo *
+ecb_m365_json_to_ical_nfo (ECalBackendM365 *cbm365,
+                          EM365Event *m365_event,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       ECalMetaBackendInfo *nfo;
+       ICalComponent *icomp;
+
+       icomp = ecb_m365_json_to_ical (cbm365, m365_event, cancellable, error);
+
+       if (!icomp)
+               return NULL;
+
+       nfo = e_cal_meta_backend_info_new (i_cal_component_get_uid (icomp),
+               e_m365_event_get_change_key (m365_event),
+               NULL, NULL);
+
+       if (nfo) {
+               nfo->object = i_cal_component_as_ical_string (icomp);
+               nfo->extra = ecb_m365_join_to_extra (e_m365_event_get_change_key (m365_event), nfo->object);
+       }
+
+       g_clear_object (&icomp);
+
+       return nfo;
+}
+
+static JsonBuilder *
+ecb_m365_ical_to_json_locked (ECalBackendM365 *cbm365,
+                             ICalComponent *new_comp,
+                             ICalComponent *old_comp, /* nullable */
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       const struct _mappings *mappings;
+       JsonBuilder *builder;
+       guint ii, n_elements = 0;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (new_comp != NULL, NULL);
+
+       mappings = ecb_m365_get_mappings_for_backend (cbm365, &n_elements);
+       g_return_val_if_fail (mappings != NULL, NULL);
+
+       builder = json_builder_new_immutable ();
+       e_m365_json_begin_object_member (builder, NULL);
+
+       for (ii = 0; success && ii < n_elements; ii++) {
+               if (mappings[ii].add_simple_func) {
+                       mappings[ii].add_simple_func (cbm365, new_comp, old_comp, mappings[ii].prop_kind, 
builder);
+               } else if (!mappings[ii].add_in_second_go && mappings[ii].add_func) {
+                       success = mappings[ii].add_func (cbm365, new_comp, old_comp, mappings[ii].prop_kind, 
NULL, builder, cancellable, error);
+               }
+       }
+
+       e_m365_json_end_object_member (builder);
+
+       if (!success)
+               g_clear_object (&builder);
+
+       return builder;
+}
+
+static gboolean
+ecb_m365_ical_to_json_2nd_go_locked (ECalBackendM365 *cbm365,
+                                    ICalComponent *new_comp,
+                                    ICalComponent *old_comp, /* nullable */
+                                    const gchar *m365_id,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       const struct _mappings *mappings;
+       guint ii, n_elements = 0;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (new_comp != NULL, FALSE);
+
+       mappings = ecb_m365_get_mappings_for_backend (cbm365, &n_elements);
+       g_return_val_if_fail (mappings != NULL, FALSE);
+
+       for (ii = 0; success && ii < n_elements; ii++) {
+               if (mappings[ii].add_in_second_go && mappings[ii].add_func) {
+                       success = mappings[ii].add_func (cbm365, new_comp, old_comp, mappings[ii].prop_kind, 
m365_id, NULL, cancellable, error);
+               }
+       }
+
+       return success;
+}
+
+static gboolean
+ecb_m365_download_changes_locked (ECalBackendM365 *cbm365,
+                                 const GSList *ids,
+                                 GSList **out_info_objects,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       GSList *items = NULL, *link;
+
+       if (!ids)
+               return TRUE;
+
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbm365))) {
+       case I_CAL_VEVENT_COMPONENT:
+               if (!e_m365_connection_get_events_sync (cbm365->priv->cnc, NULL, cbm365->priv->group_id, 
cbm365->priv->folder_id, ids, NULL, NULL, &items, cancellable, error))
+                       return FALSE;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               if (!e_m365_connection_get_tasks_sync (cbm365->priv->cnc, NULL, cbm365->priv->group_id, 
cbm365->priv->folder_id, ids, NULL, NULL, &items, cancellable, error))
+                       return FALSE;
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       for (link = items; link; link = g_slist_next (link)) {
+               JsonObject *item = link->data;
+               ECalMetaBackendInfo *nfo;
+
+               if (!item)
+                       continue;
+
+               nfo = ecb_m365_json_to_ical_nfo (cbm365, item, cancellable, error);
+
+               if (nfo)
+                       *out_info_objects = g_slist_prepend (*out_info_objects, nfo);
+       }
+
+       g_slist_free_full (items, (GDestroyNotify) json_object_unref);
+
+       return TRUE;
+}
+
+static void
+ecb_m365_maybe_disconnect_sync (ECalBackendM365 *cbm365,
+                               GError **in_perror,
+                               GCancellable *cancellable)
+{
+       g_return_if_fail (E_IS_CAL_BACKEND_M365 (cbm365));
+
+       if (in_perror && g_error_matches (*in_perror, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_FAILED)) {
+               e_cal_meta_backend_disconnect_sync (E_CAL_META_BACKEND (cbm365), cancellable, NULL);
+               e_backend_schedule_credentials_required (E_BACKEND (cbm365), 
E_SOURCE_CREDENTIALS_REASON_REJECTED, NULL, 0, NULL, NULL, G_STRFUNC);
+       }
+}
+
+static gboolean
+ecb_m365_unset_connection_sync (ECalBackendM365 *cbm365,
+                               gboolean is_disconnect,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (cbm365), FALSE);
+
+       LOCK (cbm365);
+
+       if (cbm365->priv->cnc) {
+               if (is_disconnect)
+                       success = e_m365_connection_disconnect_sync (cbm365->priv->cnc, cancellable, error);
+       }
+
+       g_clear_object (&cbm365->priv->cnc);
+       g_clear_pointer (&cbm365->priv->group_id, g_free);
+       g_clear_pointer (&cbm365->priv->folder_id, g_free);
+
+       UNLOCK (cbm365);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_connect_sync (ECalMetaBackend *meta_backend,
+                      const ENamedParameters *credentials,
+                      ESourceAuthenticationResult *out_auth_result,
+                      gchar **out_certificate_pem,
+                      GTlsCertificateFlags *out_certificate_errors,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       ECalBackendM365 *cbm365;
+       EM365FolderKind folder_kind;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (out_auth_result != NULL, FALSE);
+
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend))) {
+       case I_CAL_VEVENT_COMPONENT:
+               folder_kind = E_M365_FOLDER_KIND_CALENDAR;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               folder_kind = E_M365_FOLDER_KIND_TASKS;
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       cbm365 = E_CAL_BACKEND_M365 (meta_backend);
+
+       LOCK (cbm365);
+
+       if (cbm365->priv->cnc) {
+               UNLOCK (cbm365);
+
+               *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+
+               return TRUE;
+       } else {
+               EBackend *backend;
+               ESourceRegistry *registry;
+               ESource *source;
+               EM365Connection *cnc = NULL;
+               ESourceM365Folder *m365_folder_extension;
+               CamelM365Settings *m365_settings;
+               gchar *group_id;
+               gchar *folder_id;
+
+               backend = E_BACKEND (cbm365);
+               source = e_backend_get_source (backend);
+               registry = e_cal_backend_get_registry (E_CAL_BACKEND (cbm365));
+               m365_settings = camel_m365_settings_get_from_backend (backend, registry);
+               g_warn_if_fail (m365_settings != NULL);
+
+               m365_folder_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_M365_FOLDER);
+               group_id = e_source_m365_folder_dup_group_id (m365_folder_extension);
+               folder_id = e_source_m365_folder_dup_id (m365_folder_extension);
+
+               if (folder_id) {
+                       cnc = e_m365_connection_new_for_backend (backend, registry, source, m365_settings);
+
+                       *out_auth_result = e_m365_connection_authenticate_sync (cnc, NULL, folder_kind, 
group_id, folder_id,
+                               out_certificate_pem, out_certificate_errors, cancellable, error);
+
+                       if (*out_auth_result == E_SOURCE_AUTHENTICATION_ACCEPTED) {
+                               cbm365->priv->cnc = g_object_ref (cnc);
+
+                               g_warn_if_fail (cbm365->priv->group_id == NULL);
+                               g_warn_if_fail (cbm365->priv->folder_id == NULL);
+
+                               g_free (cbm365->priv->group_id);
+                               cbm365->priv->group_id = group_id;
+
+                               g_free (cbm365->priv->folder_id);
+                               cbm365->priv->folder_id = folder_id;
+
+                               group_id = NULL;
+                               folder_id = NULL;
+                               success = TRUE;
+
+                               e_cal_backend_set_writable (E_CAL_BACKEND (cbm365), TRUE);
+                       }
+               } else {
+                       *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+                       g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_OTHER_ERROR, _("Folder ID is 
not set")));
+               }
+
+               g_clear_object (&cnc);
+               g_free (group_id);
+               g_free (folder_id);
+       }
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_disconnect_sync (ECalMetaBackend *meta_backend,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (meta_backend), FALSE);
+
+       return ecb_m365_unset_connection_sync (E_CAL_BACKEND_M365 (meta_backend), TRUE, cancellable, error);
+}
+
+static gboolean
+ecb_m365_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)
+{
+       ECalBackendM365 *cbm365;
+       ECalCache *cal_cache;
+       GSList *items = NULL, *link;
+       gboolean full_read;
+       gboolean success = TRUE;
+       gboolean (* list_items_func) (EM365Connection *cnc,
+                                     const gchar *user_override,
+                                     const gchar *group_id,
+                                     const gchar *calendar_id,
+                                     const gchar *prefer_outlook_timezone,
+                                     const gchar *select,
+                                     GSList **out_items,
+                                     GCancellable *cancellable,
+                                     GError **error);
+       const gchar *(* get_id_func) (JsonObject *item);
+       const gchar *(* get_change_key_func) (JsonObject *item);
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+       g_return_val_if_fail (out_repeat != NULL, FALSE);
+       g_return_val_if_fail (out_created_objects != NULL, FALSE);
+       g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+       g_return_val_if_fail (out_removed_objects != NULL, FALSE);
+
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend))) {
+       case I_CAL_VEVENT_COMPONENT:
+               list_items_func = e_m365_connection_list_events_sync;
+               get_id_func = e_m365_event_get_id;
+               get_change_key_func = e_m365_event_get_change_key;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               list_items_func = e_m365_connection_list_tasks_sync;
+               get_id_func = e_m365_task_get_id;
+               get_change_key_func = e_m365_task_get_change_key;
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       *out_created_objects = NULL;
+       *out_modified_objects = NULL;
+       *out_removed_objects = NULL;
+
+       cbm365 = E_CAL_BACKEND_M365 (meta_backend);
+
+       cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+       g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+       LOCK (cbm365);
+
+       full_read = !e_cache_get_count (E_CACHE (cal_cache), E_CACHE_INCLUDE_DELETED, cancellable, NULL);
+
+       success = list_items_func (cbm365->priv->cnc, NULL, cbm365->priv->group_id, cbm365->priv->folder_id, 
NULL,
+               full_read ? NULL : "id,changeKey", &items, cancellable, error);
+
+       if (success) {
+               GSList *new_ids = NULL; /* const gchar *, borrowed from 'items' objects */
+               GSList *changed_ids = NULL; /* const gchar *, borrowed from 'items' objects */
+
+               for (link = items; link && !g_cancellable_is_cancelled (cancellable); link = g_slist_next 
(link)) {
+                       JsonObject *item = link->data;
+                       const gchar *id, *change_key;
+                       gchar *extra = NULL;
+
+                       if (!item)
+                               continue;
+
+                       id = get_id_func (item);
+                       change_key = get_change_key_func (item);
+
+                       if (e_cal_cache_get_component_extra (cal_cache, id, NULL, &extra, cancellable, NULL)) 
{
+                               const gchar *saved_change_key = NULL;
+
+                               ecb_m365_split_extra (extra, &saved_change_key, NULL);
+
+                               if (g_strcmp0 (saved_change_key, change_key) == 0) {
+                                       g_free (extra);
+                                       continue;
+                               } else if (full_read) {
+                                       ECalMetaBackendInfo *nfo;
+
+                                       nfo = ecb_m365_json_to_ical_nfo (cbm365, item, cancellable, NULL);
+
+                                       if (nfo)
+                                               *out_modified_objects = g_slist_prepend 
(*out_modified_objects, nfo);
+                               } else {
+                                       changed_ids = g_slist_prepend (changed_ids, (gpointer) id);
+                               }
+
+                               g_free (extra);
+                       } else if (full_read) {
+                               ECalMetaBackendInfo *nfo;
+
+                               nfo = ecb_m365_json_to_ical_nfo (cbm365, item, cancellable, NULL);
+
+                               if (nfo)
+                                       *out_created_objects = g_slist_prepend (*out_created_objects, nfo);
+                       } else {
+                               new_ids = g_slist_prepend (new_ids, (gpointer) id);
+                       }
+               }
+
+               if (new_ids) {
+                       new_ids = g_slist_reverse (new_ids);
+                       success = ecb_m365_download_changes_locked (cbm365, new_ids, out_created_objects, 
cancellable, error);
+               }
+
+               if (success && changed_ids) {
+                       changed_ids = g_slist_reverse (changed_ids);
+                       success = ecb_m365_download_changes_locked (cbm365, changed_ids, 
out_modified_objects, cancellable, error);
+               }
+
+               g_slist_free (new_ids);
+               g_slist_free (changed_ids);
+       }
+
+       g_slist_free_full (items, (GDestroyNotify) json_object_unref);
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+
+       g_clear_object (&cal_cache);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_load_component_sync (ECalMetaBackend *meta_backend,
+                             const gchar *uid,
+                             const gchar *extra,
+                             ICalComponent **out_component,
+                             gchar **out_extra,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       ECalBackendM365 *cbm365;
+       JsonObject *item = NULL;
+       const gchar *(* get_change_key_func) (JsonObject *item);
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (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);
+
+       cbm365 = E_CAL_BACKEND_M365 (meta_backend);
+
+       LOCK (cbm365);
+
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbm365))) {
+       case I_CAL_VEVENT_COMPONENT:
+               success = e_m365_connection_get_event_sync (cbm365->priv->cnc, NULL, cbm365->priv->group_id,
+                       cbm365->priv->folder_id, uid, NULL, NULL, &item, cancellable, error);
+               get_change_key_func = e_m365_event_get_change_key;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               success = e_m365_connection_get_task_sync (cbm365->priv->cnc, NULL, cbm365->priv->group_id,
+                       cbm365->priv->folder_id, uid, NULL, NULL, &item, cancellable, error);
+               get_change_key_func = e_m365_task_get_change_key;
+               break;
+       default:
+               success = FALSE;
+               break;
+       }
+
+       if (success) {
+               *out_component = ecb_m365_json_to_ical (cbm365, item, cancellable, error);
+
+               if (*out_component) {
+                       gchar *ical_str;
+
+                       ical_str = i_cal_component_as_ical_string (*out_component);
+
+                       *out_extra = ecb_m365_join_to_extra (get_change_key_func (item), ical_str);
+
+                       g_free (ical_str);
+               } else {
+                       success = FALSE;
+               }
+       }
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_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)
+{
+       ECalBackendM365 *cbm365;
+       ICalComponent *new_comp, *old_comp = NULL;
+       JsonBuilder *builder;
+       gboolean success = FALSE;
+       gboolean (* create_item_func) (EM365Connection *cnc,
+                                      const gchar *user_override,
+                                      const gchar *group_id,
+                                      const gchar *folder_id,
+                                      JsonBuilder *item,
+                                      JsonObject **out_created_item,
+                                      GCancellable *cancellable,
+                                      GError **error);
+       gboolean (* update_item_func) (EM365Connection *cnc,
+                                      const gchar *user_override,
+                                      const gchar *group_id,
+                                      const gchar *folder_id,
+                                      const gchar *item_id,
+                                      JsonBuilder *item,
+                                      GCancellable *cancellable,
+                                      GError **error);
+       const gchar *(* get_id_func) (JsonObject *item);
+       const gchar *(* get_change_key_func) (JsonObject *item);
+
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (instances != NULL, FALSE);
+
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend))) {
+       case I_CAL_VEVENT_COMPONENT:
+               if (instances->next) {
+                       g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_NOT_SUPPORTED,
+                               _("Can store only simple events into Microsoft 365 calendar")));
+
+                       return FALSE;
+               }
+
+               create_item_func = e_m365_connection_create_event_sync;
+               update_item_func = e_m365_connection_update_event_sync;
+               get_id_func = e_m365_event_get_id;
+               get_change_key_func = e_m365_event_get_change_key;
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               if (instances->next) {
+                       g_propagate_error (error, EC_ERROR_EX (E_CLIENT_ERROR_NOT_SUPPORTED,
+                               _("Can store only simple tasks into Microsoft 365 task folder")));
+
+                       return FALSE;
+               }
+
+               create_item_func = e_m365_connection_create_task_sync;
+               update_item_func = e_m365_connection_update_task_sync;
+               get_id_func = e_m365_task_get_id;
+               get_change_key_func = e_m365_task_get_change_key;
+               break;
+       default:
+               g_warn_if_reached ();
+               return FALSE;
+       }
+
+       cbm365 = E_CAL_BACKEND_M365 (meta_backend);
+
+       LOCK (cbm365);
+
+       new_comp = e_cal_component_get_icalcomponent (instances->data);
+
+       if (extra && *extra) {
+               const gchar *comp_str;
+
+               comp_str = ecb_m365_get_component_from_extra (extra);
+
+               if (comp_str)
+                       old_comp = i_cal_component_new_from_string (comp_str);
+       }
+
+       builder = ecb_m365_ical_to_json_locked (cbm365, new_comp, old_comp, cancellable, error);
+
+       if (builder) {
+               if (overwrite_existing) {
+                       const gchar *uid = i_cal_component_get_uid (new_comp);
+
+                       success = update_item_func (cbm365->priv->cnc, NULL, cbm365->priv->group_id,
+                               cbm365->priv->folder_id, uid, builder, cancellable, error);
+
+                       if (success)
+                               success = ecb_m365_ical_to_json_2nd_go_locked (cbm365, new_comp, old_comp, 
uid, cancellable, error);
+
+                       if (success) {
+                               /* To re-read it from the server */
+                               *out_new_uid = g_strdup (uid);
+                       }
+               } else {
+                       JsonObject *created_item = NULL;
+
+                       success = create_item_func (cbm365->priv->cnc, NULL, cbm365->priv->group_id,
+                               cbm365->priv->folder_id, builder, &created_item, cancellable, error);
+
+                       if (success && created_item) {
+                               const gchar *m365_id = get_id_func (created_item);
+
+                               success = ecb_m365_ical_to_json_2nd_go_locked (cbm365, new_comp, old_comp, 
m365_id, cancellable, error);
+                       }
+
+                       if (success && created_item) {
+                               ICalComponent *icomp;
+
+                               *out_new_uid = g_strdup (get_id_func (created_item));
+
+                               icomp = ecb_m365_json_to_ical (cbm365, created_item, cancellable, error);
+
+                               if (icomp) {
+                                       gchar *ical_str;
+
+                                       ical_str = i_cal_component_as_ical_string (icomp);
+
+                                       *out_new_extra = ecb_m365_join_to_extra (get_change_key_func 
(created_item), ical_str);
+
+                                       g_clear_object (&icomp);
+                                       g_free (ical_str);
+                               } else {
+                                       success = FALSE;
+                               }
+                       }
+
+                       if (created_item)
+                               json_object_unref (created_item);
+               }
+
+               g_clear_object (&builder);
+       }
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+
+       g_clear_object (&old_comp);
+
+       return success;
+}
+
+static gboolean
+ecb_m365_remove_component_sync (ECalMetaBackend *meta_backend,
+                               EConflictResolution conflict_resolution,
+                               const gchar *uid,
+                               const gchar *extra,
+                               const gchar *object,
+                               guint32 opflags,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       ECalBackendM365 *cbm365;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (meta_backend), FALSE);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       cbm365 = E_CAL_BACKEND_M365 (meta_backend);
+
+       LOCK (cbm365);
+
+       switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbm365))) {
+       case I_CAL_VEVENT_COMPONENT:
+               success = e_m365_connection_delete_event_sync (cbm365->priv->cnc, NULL, 
cbm365->priv->group_id,
+                       cbm365->priv->folder_id, uid, cancellable, error);
+               break;
+       case I_CAL_VTODO_COMPONENT:
+               success = e_m365_connection_delete_task_sync (cbm365->priv->cnc, NULL, cbm365->priv->group_id,
+                       cbm365->priv->folder_id, uid, cancellable, error);
+               break;
+       default:
+               g_warn_if_reached ();
+               success = FALSE;
+       }
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+
+       return success;
+}
+
+static void
+ecb_m365_discard_alarm_sync (ECalBackendSync *cal_backend_sync,
+                            EDataCal *cal,
+                            GCancellable *cancellable,
+                            const gchar *uid,
+                            const gchar *rid,
+                            const gchar *auid,
+                            guint32 opflags,
+                            GError **error)
+{
+       ECalBackendM365 *cbm365;
+
+       g_return_if_fail (E_IS_CAL_BACKEND_M365 (cal_backend_sync));
+       g_return_if_fail (uid != NULL);
+
+       if (e_cal_backend_get_kind (E_CAL_BACKEND (cal_backend_sync)) != I_CAL_VEVENT_COMPONENT) {
+               g_propagate_error (error, EC_ERROR (E_CLIENT_ERROR_NOT_SUPPORTED));
+               return;
+       }
+
+       cbm365 = E_CAL_BACKEND_M365 (cal_backend_sync);
+
+       if (!e_cal_meta_backend_ensure_connected_sync (E_CAL_META_BACKEND (cbm365), cancellable, error))
+               return;
+
+       LOCK (cbm365);
+
+       e_m365_connection_dismiss_reminder_sync (cbm365->priv->cnc, NULL, cbm365->priv->group_id,
+               cbm365->priv->folder_id, uid, cancellable, error);
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+}
+
+static void
+ecb_m365_get_free_busy_sync (ECalBackendSync *cal_backend_sync,
+                            EDataCal *cal,
+                            GCancellable *cancellable,
+                            const GSList *users,
+                            time_t start,
+                            time_t end,
+                            GSList **out_freebusyobjs,
+                            GError **error)
+{
+       ECalBackendM365 *cbm365;
+       GSList *infos = NULL;
+       gboolean success;
+
+       g_return_if_fail (E_IS_CAL_BACKEND_M365 (cal_backend_sync));
+       g_return_if_fail (users != NULL);
+       g_return_if_fail (out_freebusyobjs != NULL);
+
+       if (e_cal_backend_get_kind (E_CAL_BACKEND (cal_backend_sync)) != I_CAL_VEVENT_COMPONENT) {
+               g_propagate_error (error, EC_ERROR (E_CLIENT_ERROR_NOT_SUPPORTED));
+               return;
+       }
+
+       cbm365 = E_CAL_BACKEND_M365 (cal_backend_sync);
+
+       if (!e_cal_meta_backend_ensure_connected_sync (E_CAL_META_BACKEND (cbm365), cancellable, error))
+               return;
+
+       LOCK (cbm365);
+
+       success = e_m365_connection_get_schedule_sync (cbm365->priv->cnc, NULL, 30, start, end, users, 
&infos, cancellable, error);
+
+       UNLOCK (cbm365);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+
+       if (success) {
+               ICalTimezone *utc_zone = i_cal_timezone_get_utc_timezone ();
+               GSList *link;
+
+               *out_freebusyobjs = NULL;
+
+               for (link = infos; link; link = g_slist_next (link)) {
+                       EM365ScheduleInformation *schinfo = link->data;
+                       ICalComponent *vfb = NULL;
+                       JsonArray *array;
+                       guint ii, sz;
+
+                       if (!schinfo || !e_m365_schedule_information_get_schedule_id (schinfo))
+                               continue;
+
+                       array = e_m365_schedule_information_get_schedule_items (schinfo);
+                       sz = array ? json_array_get_length (array) : 0;
+
+                       for (ii = 0; ii < sz; ii++) {
+                               EM365ScheduleItem *schitem = json_array_get_object_element (array, ii);
+                               EM365DateTimeWithZone *dt;
+                               ICalProperty *prop;
+                               ICalPeriod *ipt;
+                               ICalTime *itt;
+                               const gchar *tmp;
+
+                               if (!schitem || !e_m365_schedule_item_get_start (schitem) || 
!e_m365_schedule_item_get_end (schitem))
+                                       continue;
+
+                               ipt = i_cal_period_new_null_period ();
+
+                               dt = e_m365_schedule_item_get_start (schitem);
+                               itt = i_cal_time_new_from_timet_with_zone (e_m365_date_time_get_date_time 
(dt), 0, utc_zone);
+                               i_cal_period_set_start (ipt, itt);
+                               g_clear_object (&itt);
+
+                               dt = e_m365_schedule_item_get_end (schitem);
+                               itt = i_cal_time_new_from_timet_with_zone (e_m365_date_time_get_date_time 
(dt), 0, utc_zone);
+                               i_cal_period_set_end (ipt, itt);
+                               g_clear_object (&itt);
+
+                               prop = i_cal_property_new_freebusy (ipt);
+                               g_clear_object (&ipt);
+
+                               switch (e_m365_schedule_item_get_status (schitem)) {
+                               case E_M365_FREE_BUSY_STATUS_FREE:
+                                       i_cal_property_set_parameter_from_string (prop, "FBTYPE", "FREE");
+                                       break;
+                               case E_M365_FREE_BUSY_STATUS_TENTATIVE:
+                                       i_cal_property_set_parameter_from_string (prop, "FBTYPE", 
"BUSY-TENTATIVE");
+                                       break;
+                               case E_M365_FREE_BUSY_STATUS_BUSY:
+                                       i_cal_property_set_parameter_from_string (prop, "FBTYPE", "BUSY");
+                                       break;
+                               case E_M365_FREE_BUSY_STATUS_OOF:
+                               case E_M365_FREE_BUSY_STATUS_WORKING_ELSEWHERE:
+                                       i_cal_property_set_parameter_from_string (prop, "FBTYPE", 
"BUSY-UNAVAILABLE");
+                                       break;
+                               default:
+                                       break;
+                               }
+
+                               tmp = e_m365_schedule_item_get_subject (schitem);
+
+                               if (tmp && *tmp)
+                                       i_cal_property_set_parameter_from_string (prop, "X-SUMMARY", tmp);
+
+                               tmp = e_m365_schedule_item_get_location (schitem);
+
+                               if (tmp && *tmp)
+                                       i_cal_property_set_parameter_from_string (prop, "X-LOCATION", tmp);
+
+                               if (!vfb)
+                                       vfb = i_cal_component_new_vfreebusy ();
+
+                               i_cal_component_take_property (vfb, prop);
+                       }
+
+                       if (vfb) {
+                               gchar *mailto;
+
+                               mailto = g_strconcat ("mailto:";, e_m365_schedule_information_get_schedule_id 
(schinfo), NULL);
+                               i_cal_component_take_property (vfb, i_cal_property_new_attendee (mailto));
+                               g_free (mailto);
+
+                               *out_freebusyobjs = g_slist_prepend (*out_freebusyobjs, 
i_cal_component_as_ical_string (vfb));
+
+                               g_clear_object (&vfb);
+                       }
+               }
+
+               *out_freebusyobjs = g_slist_reverse (*out_freebusyobjs);
+       }
+
+       g_slist_free_full (infos, (GDestroyNotify) json_object_unref);
+
+       ecb_m365_convert_error_to_client_error (error);
+       ecb_m365_maybe_disconnect_sync (cbm365, error, cancellable);
+}
+
+static gchar *
+ecb_m365_get_backend_property (ECalBackend *cal_backend,
+                             const gchar *prop_name)
+{
+       ECalBackendM365 *cbm365;
+
+       g_return_val_if_fail (E_IS_CAL_BACKEND_M365 (cal_backend), NULL);
+       g_return_val_if_fail (prop_name != NULL, NULL);
+
+       cbm365 = E_CAL_BACKEND_M365 (cal_backend);
+
+       if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+               return g_strjoin (
+                       ",",
+                       E_CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS,
+                       E_CAL_STATIC_CAPABILITY_NO_AUDIO_ALARMS,
+                       E_CAL_STATIC_CAPABILITY_NO_PROCEDURE_ALARMS,
+                       E_CAL_STATIC_CAPABILITY_ONE_ALARM_ONLY,
+                       E_CAL_STATIC_CAPABILITY_REMOVE_ALARMS,
+                       E_CAL_STATIC_CAPABILITY_NO_THISANDPRIOR,
+                       E_CAL_STATIC_CAPABILITY_NO_THISANDFUTURE,
+                       E_CAL_STATIC_CAPABILITY_NO_CONV_TO_ASSIGN_TASK,
+                       E_CAL_STATIC_CAPABILITY_NO_TASK_ASSIGNMENT,
+                       E_CAL_STATIC_CAPABILITY_SAVE_SCHEDULES,
+                       E_CAL_STATIC_CAPABILITY_NO_ALARM_AFTER_START,
+                       E_CAL_STATIC_CAPABILITY_NO_MEMO_START_DATE,
+                       E_CAL_STATIC_CAPABILITY_ALL_DAY_EVENT_AS_TIME,
+                       E_CAL_STATIC_CAPABILITY_TASK_DATE_ONLY,
+                       E_CAL_STATIC_CAPABILITY_TASK_NO_ALARM,
+                       E_CAL_STATIC_CAPABILITY_TASK_CAN_RECUR,
+                       E_CAL_STATIC_CAPABILITY_TASK_HANDLE_RECUR,
+                       e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cbm365)),
+                       NULL);
+       } else if (g_str_equal (prop_name, E_CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS)) {
+               /* return email address of the person who opened the calendar */
+               CamelM365Settings *m365_settings;
+
+               m365_settings = camel_m365_settings_get_from_backend (E_BACKEND (cal_backend), 
e_cal_backend_get_registry (cal_backend));
+
+               return camel_m365_settings_dup_email (m365_settings);
+       } else if (g_str_equal (prop_name, E_CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
+               /* Microsoft 365 does not support email based alarms */
+               return NULL;
+       }
+
+       /* Chain up to parent's method. */
+       return E_CAL_BACKEND_CLASS (e_cal_backend_m365_parent_class)->impl_get_backend_property (cal_backend, 
prop_name);
+}
+
+static gboolean
+ecb_m365_get_destination_address (EBackend *backend,
+                                 gchar **host,
+                                 guint16 *port)
+{
+       g_return_val_if_fail (port != NULL, FALSE);
+       g_return_val_if_fail (host != NULL, FALSE);
+
+       /* Sanity checking */
+       if (!e_cal_backend_get_registry (E_CAL_BACKEND (backend)) ||
+           !e_backend_get_source (backend))
+               return FALSE;
+
+       *host = g_strdup ("graph.microsoft.com");
+       *port = 443;
+
+       return TRUE;
+}
+
+static gchar *
+ecb_m365_dup_component_revision (ECalCache *cal_cache,
+                                ICalComponent *icomp,
+                                gpointer user_data)
+{
+       g_return_val_if_fail (icomp != NULL, NULL);
+
+       return e_cal_util_component_dup_x_property (icomp, "X-EVOLUTION-CHANGEKEY");
+}
+
+static void
+ecb_m365_constructed (GObject *object)
+{
+       ECalBackendM365 *cbm365 = E_CAL_BACKEND_M365 (object);
+       ECalCache *cal_cache;
+       gchar *cache_dirname;
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_m365_parent_class)->constructed (object);
+
+       /* Reset the connectable, it steals data from Authentication extension,
+          where is written incorrect address */
+       e_backend_set_connectable (E_BACKEND (object), NULL);
+
+       cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbm365));
+       g_return_if_fail (cal_cache != NULL);
+
+       cache_dirname = g_path_get_dirname (e_cache_get_filename (E_CACHE (cal_cache)));
+       g_signal_connect (cal_cache, "dup-component-revision", G_CALLBACK (ecb_m365_dup_component_revision), 
NULL);
+
+       g_clear_object (&cal_cache);
+
+       cbm365->priv->attachments_dir = g_build_filename (cache_dirname, "attachments", NULL);
+       g_mkdir_with_parents (cbm365->priv->attachments_dir, 0777);
+
+       g_free (cache_dirname);
+
+       e_m365_tz_utils_ref_windows_zones ();
+}
+
+static void
+ecb_m365_dispose (GObject *object)
+{
+       ECalBackendM365 *cbm365 = E_CAL_BACKEND_M365 (object);
+
+       ecb_m365_unset_connection_sync (cbm365, FALSE, NULL, NULL);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_m365_parent_class)->dispose (object);
+}
+
+static void
+ecb_m365_finalize (GObject *object)
+{
+       ECalBackendM365 *cbm365 = E_CAL_BACKEND_M365 (object);
+
+       g_free (cbm365->priv->attachments_dir);
+
+       g_rec_mutex_clear (&cbm365->priv->property_lock);
+
+       e_m365_tz_utils_unref_windows_zones ();
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_cal_backend_m365_parent_class)->finalize (object);
+}
+
+static void
+e_cal_backend_m365_init (ECalBackendM365 *cbm365)
+{
+       cbm365->priv = e_cal_backend_m365_get_instance_private (cbm365);
+
+       g_rec_mutex_init (&cbm365->priv->property_lock);
+}
+
+static void
+e_cal_backend_m365_class_init (ECalBackendM365Class *klass)
+{
+       GObjectClass *object_class;
+       EBackendClass *backend_class;
+       ECalBackendClass *cal_backend_class;
+       ECalBackendSyncClass *cal_backend_sync_class;
+       ECalMetaBackendClass *cal_meta_backend_class;
+
+       cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+       cal_meta_backend_class->connect_sync = ecb_m365_connect_sync;
+       cal_meta_backend_class->disconnect_sync = ecb_m365_disconnect_sync;
+       cal_meta_backend_class->get_changes_sync = ecb_m365_get_changes_sync;
+       cal_meta_backend_class->load_component_sync = ecb_m365_load_component_sync;
+       cal_meta_backend_class->save_component_sync = ecb_m365_save_component_sync;
+       cal_meta_backend_class->remove_component_sync = ecb_m365_remove_component_sync;
+
+       cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+       cal_backend_sync_class->discard_alarm_sync = ecb_m365_discard_alarm_sync;
+       cal_backend_sync_class->get_free_busy_sync = ecb_m365_get_free_busy_sync;
+
+       cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+       cal_backend_class->impl_get_backend_property = ecb_m365_get_backend_property;
+
+       backend_class = E_BACKEND_CLASS (klass);
+       backend_class->get_destination_address = ecb_m365_get_destination_address;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = ecb_m365_constructed;
+       object_class->dispose = ecb_m365_dispose;
+       object_class->finalize = ecb_m365_finalize;
+}
diff --git a/src/Microsoft365/calendar/e-cal-backend-m365.h b/src/Microsoft365/calendar/e-cal-backend-m365.h
new file mode 100644
index 00000000..7963dcae
--- /dev/null
+++ b/src/Microsoft365/calendar/e-cal-backend-m365.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_CAL_BACKEND_M365_H
+#define E_CAL_BACKEND_M365_H
+
+#include <libedata-cal/libedata-cal.h>
+
+G_BEGIN_DECLS
+
+#define E_TYPE_CAL_BACKEND_M365            (e_cal_backend_m365_get_type ())
+#define E_CAL_BACKEND_M365(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_CAL_BACKEND_M365,      
  ECalBackendM365))
+#define E_CAL_BACKEND_M365_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), E_TYPE_CAL_BACKEND_M365, 
ECalBackendM365Class))
+#define E_IS_CAL_BACKEND_M365(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_CAL_BACKEND_M365))
+#define E_IS_CAL_BACKEND_M365_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), E_TYPE_CAL_BACKEND_M365))
+
+typedef struct _ECalBackendM365        ECalBackendM365;
+typedef struct _ECalBackendM365Class   ECalBackendM365Class;
+typedef struct _ECalBackendM365Private ECalBackendM365Private;
+
+struct _ECalBackendM365 {
+       ECalMetaBackend parent_object;
+       ECalBackendM365Private *priv;
+};
+
+struct _ECalBackendM365Class {
+       ECalMetaBackendClass parent_class;
+};
+
+GType   e_cal_backend_m365_get_type (void);
+
+G_END_DECLS
+
+#endif /* E_CAL_BACKEND_M365_H */
diff --git a/src/Microsoft365/camel/CMakeLists.txt b/src/Microsoft365/camel/CMakeLists.txt
new file mode 100644
index 00000000..928b837a
--- /dev/null
+++ b/src/Microsoft365/camel/CMakeLists.txt
@@ -0,0 +1,75 @@
+install(FILES libcamelmicrosoft365.urls
+       DESTINATION ${camel_providerdir}
+)
+
+set(DEPENDENCIES
+       evolution-microsoft365
+)
+
+set(SOURCES
+       camel-m365-folder.c
+       camel-m365-folder.h
+       camel-m365-folder-summary.c
+       camel-m365-folder-summary.h
+       camel-m365-message-info.c
+       camel-m365-message-info.h
+       camel-m365-provider.c
+       camel-m365-store.c
+       camel-m365-store.h
+       camel-m365-store-summary.c
+       camel-m365-store-summary.h
+       camel-m365-transport.c
+       camel-m365-transport.h
+       camel-m365-utils.c
+       camel-m365-utils.h
+)
+
+add_library(camelmicrosoft365 MODULE
+       ${SOURCES}
+)
+
+add_dependencies(camelmicrosoft365
+       ${DEPENDENCIES}
+)
+
+target_compile_definitions(camelmicrosoft365 PRIVATE
+       -DG_LOG_DOMAIN=\"camel-microsoft365-provider\"
+       -DM365_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+)
+
+target_compile_options(camelmicrosoft365 PUBLIC
+       ${CAMEL_CFLAGS}
+       ${EVOLUTION_SHELL_CFLAGS}
+       ${EVOLUTION_MAIL_CFLAGS}
+       ${LIBEDATASERVER_CFLAGS}
+       ${LIBECAL_CFLAGS}
+       ${SOUP_CFLAGS}
+)
+
+target_include_directories(camelmicrosoft365 PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_SOURCE_DIR}
+       ${CMAKE_BINARY_DIR}/src/Microsoft365
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365
+       ${CMAKE_CURRENT_BINARY_DIR}
+       ${CAMEL_INCLUDE_DIRS}
+       ${EVOLUTION_SHELL_INCLUDE_DIRS}
+       ${EVOLUTION_MAIL_INCLUDE_DIRS}
+       ${LIBEDATASERVER_INCLUDE_DIRS}
+       ${LIBECAL_INCLUDE_DIRS}
+       ${SOUP_INCLUDE_DIRS}
+)
+
+target_link_libraries(camelmicrosoft365
+       ${DEPENDENCIES}
+       ${CAMEL_LDFLAGS}
+       ${EVOLUTION_SHELL_LDFLAGS}
+       ${EVOLUTION_MAIL_LDFLAGS}
+       ${LIBEDATASERVER_LDFLAGS}
+       ${LIBECAL_LDFLAGS}
+       ${SOUP_LDFLAGS}
+)
+
+install(TARGETS camelmicrosoft365
+       DESTINATION ${camel_providerdir}
+)
diff --git a/src/Microsoft365/camel/camel-m365-folder-summary.c 
b/src/Microsoft365/camel/camel-m365-folder-summary.c
new file mode 100644
index 00000000..1d46f026
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-folder-summary.c
@@ -0,0 +1,306 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib.h>
+
+#include "camel-m365-folder.h"
+
+#include "camel-m365-folder-summary.h"
+
+#define LOCK(_summary) g_mutex_lock (&_summary->priv->property_lock)
+#define UNLOCK(_summary) g_mutex_unlock (&_summary->priv->property_lock)
+
+struct _CamelM365FolderSummaryPrivate {
+       GMutex property_lock;
+       gchar *delta_link;
+       gint32 version;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelM365FolderSummary, camel_m365_folder_summary, CAMEL_TYPE_FOLDER_SUMMARY)
+
+static gboolean
+m365_folder_summary_header_load (CamelFolderSummary *summary,
+                                CamelFIRecord *mir)
+{
+       CamelM365FolderSummary *m365_summary = CAMEL_M365_FOLDER_SUMMARY (summary);
+       const gchar *delta_link = NULL;
+       gchar *part;
+
+       if (!CAMEL_FOLDER_SUMMARY_CLASS (camel_m365_folder_summary_parent_class)->summary_header_load 
(summary, mir))
+               return FALSE;
+
+       m365_summary->priv->version = 0;
+
+       part = mir->bdata;
+
+       if (part)
+               m365_summary->priv->version = camel_util_bdata_get_number (&part, 0);
+
+       if (part && *part && part[1])
+               delta_link = part + 1;
+
+       /* Do not call camel_m365_folder_summary_set_sync_state() here,
+          to not mark the summary dirty after load. */
+       LOCK (m365_summary);
+
+       if (g_strcmp0 (m365_summary->priv->delta_link, delta_link) != 0) {
+               g_free (m365_summary->priv->delta_link);
+               m365_summary->priv->delta_link = g_strdup (delta_link);
+       }
+
+       UNLOCK (m365_summary);
+
+       return TRUE;
+}
+
+static CamelFIRecord *
+m365_folder_summary_header_save (CamelFolderSummary *summary,
+                                GError **error)
+{
+       CamelM365FolderSummary *m365_summary = CAMEL_M365_FOLDER_SUMMARY (summary);
+       CamelFIRecord *fir;
+       gchar *delta_link;
+
+       fir = CAMEL_FOLDER_SUMMARY_CLASS (camel_m365_folder_summary_parent_class)->summary_header_save 
(summary, error);
+
+       if (!fir)
+               return NULL;
+
+       delta_link = camel_m365_folder_summary_dup_delta_link (m365_summary);
+
+       fir->bdata = g_strdup_printf ("%d %s", CAMEL_M365_FOLDER_SUMMARY_VERSION, delta_link ? delta_link : 
"");
+
+       g_free (delta_link);
+
+       LOCK (m365_summary);
+
+       m365_summary->priv->version = CAMEL_M365_FOLDER_SUMMARY_VERSION;
+
+       UNLOCK (m365_summary);
+
+       return fir;
+}
+
+static void
+m365_folder_summary_finalize (GObject *object)
+{
+       CamelM365FolderSummary *m365_summary = CAMEL_M365_FOLDER_SUMMARY (object);
+
+       g_free (m365_summary->priv->delta_link);
+       g_mutex_clear (&m365_summary->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_folder_summary_parent_class)->finalize (object);
+}
+
+static void
+camel_m365_folder_summary_class_init (CamelM365FolderSummaryClass *class)
+{
+       CamelFolderSummaryClass *folder_summary_class;
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->finalize = m365_folder_summary_finalize;
+
+       folder_summary_class = CAMEL_FOLDER_SUMMARY_CLASS (class);
+       folder_summary_class->message_info_type = CAMEL_TYPE_M365_MESSAGE_INFO;
+       folder_summary_class->summary_header_save = m365_folder_summary_header_save;
+       folder_summary_class->summary_header_load = m365_folder_summary_header_load;
+}
+
+static void
+camel_m365_folder_summary_init (CamelM365FolderSummary *m365_summary)
+{
+       m365_summary->priv = camel_m365_folder_summary_get_instance_private (m365_summary);
+
+       g_mutex_init (&m365_summary->priv->property_lock);
+}
+
+CamelFolderSummary *
+camel_m365_folder_summary_new (struct _CamelFolder *folder)
+{
+       CamelFolderSummary *summary;
+
+       summary = g_object_new (CAMEL_TYPE_M365_FOLDER_SUMMARY, "folder", folder, NULL);
+
+       camel_folder_summary_load (summary, NULL);
+
+       return summary;
+}
+
+gint
+camel_m365_folder_summary_get_version (CamelM365FolderSummary *m365_summary)
+{
+       gint version;
+
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER_SUMMARY (m365_summary), -1);
+
+       LOCK (m365_summary);
+
+       version = m365_summary->priv->version;
+
+       UNLOCK (m365_summary);
+
+       return version;
+}
+
+void
+camel_m365_folder_summary_set_delta_link (CamelM365FolderSummary *m365_summary,
+                                         const gchar *delta_link)
+{
+       gboolean changed = FALSE;
+
+       g_return_if_fail (CAMEL_IS_M365_FOLDER_SUMMARY (m365_summary));
+
+       LOCK (m365_summary);
+
+       if (g_strcmp0 (m365_summary->priv->delta_link, delta_link) != 0) {
+               gchar *dup = g_strdup (delta_link);
+
+               g_free (m365_summary->priv->delta_link);
+               m365_summary->priv->delta_link = dup;
+
+               changed = TRUE;
+       }
+
+       UNLOCK (m365_summary);
+
+       if (changed)
+               camel_folder_summary_touch (CAMEL_FOLDER_SUMMARY (m365_summary));
+}
+
+gchar *
+camel_m365_folder_summary_dup_delta_link (CamelM365FolderSummary *m365_summary)
+{
+       gchar *delta_link;
+
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER_SUMMARY (m365_summary), NULL);
+
+       LOCK (m365_summary);
+
+       delta_link = g_strdup (m365_summary->priv->delta_link);
+
+       UNLOCK (m365_summary);
+
+       return delta_link;
+}
+
+void
+camel_m365_folder_summary_clear (CamelFolderSummary *summary)
+{
+       CamelFolderChangeInfo *changes;
+       GPtrArray *known_uids;
+       gint i;
+
+       changes = camel_folder_change_info_new ();
+       known_uids = camel_folder_summary_get_array (summary);
+
+       for (i = 0; i < known_uids->len; i++) {
+               const gchar *uid = g_ptr_array_index (known_uids, i);
+
+               if (!uid)
+                       continue;
+
+               camel_folder_change_info_remove_uid (changes, uid);
+       }
+
+       camel_folder_summary_clear (summary, NULL);
+
+       if (camel_folder_change_info_changed (changes))
+               camel_folder_changed (camel_folder_summary_get_folder (summary), changes);
+       camel_folder_change_info_free (changes);
+       camel_folder_summary_free_array (known_uids);
+}
+
+static gboolean
+m365_folder_summary_update_user_flags (CamelMessageInfo *info,
+                                      const CamelNamedFlags *server_user_flags)
+{
+       gboolean changed = FALSE;
+       gboolean set_cal = FALSE, set_note = FALSE;
+
+       if (camel_message_info_get_user_flag (info, "$has_cal"))
+               set_cal = TRUE;
+       if (camel_message_info_get_user_flag (info, "$has_note"))
+               set_note = TRUE;
+
+       changed = camel_message_info_take_user_flags (info, camel_named_flags_copy (server_user_flags));
+
+       /* reset the flags as they were set in messageinfo before */
+       if (set_cal)
+               camel_message_info_set_user_flag (info, "$has_cal", TRUE);
+       if (set_note)
+               camel_message_info_set_user_flag (info, "$has_note", TRUE);
+
+       return changed;
+}
+
+gboolean
+camel_m365_folder_summary_update_message_info_flags (CamelFolderSummary *summary,
+                                                    CamelMessageInfo *info,
+                                                    guint32 server_flags,
+                                                    const CamelNamedFlags *server_user_flags)
+{
+       CamelM365MessageInfo *omi;
+       gboolean changed = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER_SUMMARY (summary), FALSE);
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (info), FALSE);
+
+       omi = CAMEL_M365_MESSAGE_INFO (info);
+
+       if (server_flags != camel_m365_message_info_get_server_flags (omi)) {
+               guint32 server_set, server_cleared;
+
+               server_set = server_flags & ~camel_m365_message_info_get_server_flags (omi);
+               server_cleared = camel_m365_message_info_get_server_flags (omi) & ~server_flags;
+
+               camel_message_info_set_flags (info, server_set | server_cleared, 
(camel_message_info_get_flags (info) | server_set) & ~server_cleared);
+               camel_m365_message_info_set_server_flags (omi, server_flags);
+               changed = TRUE;
+       }
+
+       if (server_user_flags && m365_folder_summary_update_user_flags (info, server_user_flags))
+               changed = TRUE;
+
+       return changed;
+}
+
+gboolean
+camel_m365_folder_summary_add_message (CamelFolderSummary *summary,
+                                      const gchar *uid,
+                                      const gchar *change_key,
+                                      CamelMessageInfo *info,
+                                      CamelMimeMessage *message)
+{
+       CamelMessageInfo *mi;
+
+       g_return_val_if_fail (uid != NULL, FALSE);
+       g_return_val_if_fail (info != NULL, FALSE);
+       g_return_val_if_fail (message != NULL, FALSE);
+
+       mi = camel_folder_summary_info_new_from_message (summary, message);
+       g_return_val_if_fail (mi != NULL, FALSE);
+
+       camel_message_info_set_abort_notifications (mi, TRUE);
+
+       camel_m365_message_info_set_change_key (CAMEL_M365_MESSAGE_INFO (mi), change_key);
+       camel_message_info_set_flags (mi, ~0, camel_message_info_get_flags (info));
+       camel_message_info_take_user_flags (mi, camel_message_info_dup_user_flags (info));
+       camel_message_info_take_user_tags (mi, camel_message_info_dup_user_tags (info));
+       camel_message_info_set_size (mi, camel_message_info_get_size (info));
+       camel_message_info_set_uid (mi, uid);
+
+       camel_message_info_set_abort_notifications (mi, FALSE);
+
+       camel_folder_summary_add (summary, mi, FALSE);
+
+       g_object_unref (mi);
+
+       return TRUE;
+}
diff --git a/src/Microsoft365/camel/camel-m365-folder-summary.h 
b/src/Microsoft365/camel/camel-m365-folder-summary.h
new file mode 100644
index 00000000..086a20ab
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-folder-summary.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_FOLDER_SUMMARY_H
+#define CAMEL_M365_FOLDER_SUMMARY_H
+
+#include <camel/camel.h>
+
+#include "camel-m365-message-info.h"
+
+#define CAMEL_M365_FOLDER_SUMMARY_VERSION (1)
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_FOLDER_SUMMARY \
+       (camel_m365_folder_summary_get_type ())
+#define CAMEL_M365_FOLDER_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_FOLDER_SUMMARY, CamelM365FolderSummary))
+#define CAMEL_M365_FOLDER_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_FOLDER_SUMMARY, CamelM365FolderSummaryClass))
+#define CAMEL_IS_M365_FOLDER_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_FOLDER_SUMMARY))
+#define CAMEL_IS_M365_FOLDER_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_FOLDER_SUMMARY))
+#define CAMEL_M365_FOLDER_SUMMARY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_FOLDER_SUMMARY, CamelM365FolderSummaryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelM365FolderSummary CamelM365FolderSummary;
+typedef struct _CamelM365FolderSummaryClass CamelM365FolderSummaryClass;
+typedef struct _CamelM365FolderSummaryPrivate CamelM365FolderSummaryPrivate;
+
+struct _CamelM365FolderSummary {
+       CamelFolderSummary parent;
+       CamelM365FolderSummaryPrivate *priv;
+};
+
+struct _CamelM365FolderSummaryClass {
+       CamelFolderSummaryClass parent_class;
+};
+
+GType          camel_m365_folder_summary_get_type      (void);
+
+CamelFolderSummary *
+               camel_m365_folder_summary_new           (CamelFolder *folder);
+gint           camel_m365_folder_summary_get_version   (CamelM365FolderSummary *m365_summary);
+void           camel_m365_folder_summary_set_delta_link(CamelM365FolderSummary *m365_summary,
+                                                        const gchar *delta_link);
+gchar *                camel_m365_folder_summary_dup_delta_link(CamelM365FolderSummary *m365_summary);
+void           camel_m365_folder_summary_clear         (CamelFolderSummary *summary);
+gboolean       camel_m365_folder_summary_update_message_info_flags
+                                                       (CamelFolderSummary *summary,
+                                                        CamelMessageInfo *info,
+                                                        guint32 server_flags,
+                                                        const CamelNamedFlags *server_user_flags);
+gboolean       camel_m365_folder_summary_add_message   (CamelFolderSummary *summary,
+                                                        const gchar *uid,
+                                                        const gchar *change_key,
+                                                        CamelMessageInfo *info,
+                                                        CamelMimeMessage *message);
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_FOLDER_SUMMARY_H */
diff --git a/src/Microsoft365/camel/camel-m365-folder.c b/src/Microsoft365/camel/camel-m365-folder.c
new file mode 100644
index 00000000..8cb54599
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-folder.c
@@ -0,0 +1,1847 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+
+#include <glib/gi18n-lib.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+
+#include "camel-m365-folder-summary.h"
+#include "camel-m365-store.h"
+#include "camel-m365-store-summary.h"
+#include "camel-m365-utils.h"
+
+#include "camel-m365-folder.h"
+
+#define M365_LOCAL_CACHE_PATH "cur"
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0 */
+#define M365_FETCH_SUMMARY_PROPERTIES  "categories," \
+                                       "ccRecipients," \
+                                       "changeKey," \
+                                       "flag," \
+                                       "from," \
+                                       "hasAttachments," \
+                                       "id," \
+                                       "importance," \
+                                       "internetMessageHeaders," \
+                                       "internetMessageId," \
+                                       "isRead," \
+                                       "receivedDateTime," \
+                                       "sender," \
+                                       "sentDateTime," \
+                                       "subject," \
+                                       "toRecipients"
+
+#define LOCK_CACHE(_folder) g_rec_mutex_lock (&_folder->priv->cache_lock)
+#define UNLOCK_CACHE(_folder) g_rec_mutex_unlock (&_folder->priv->cache_lock)
+
+#define LOCK_SEARCH(_folder) g_mutex_lock (&_folder->priv->search_lock)
+#define UNLOCK_SEARCH(_folder) g_mutex_unlock (&_folder->priv->search_lock)
+
+struct _CamelM365FolderPrivate {
+       gchar *id; /* folder ID; stays the same for the full life of the folder */
+
+       GRecMutex cache_lock;
+       CamelDataCache *cache;
+
+       GMutex search_lock;
+       CamelFolderSearch *search;
+
+       /* To not download the same message multiple times from different threads */
+       GMutex get_message_lock;
+       GCond get_message_cond;
+       GHashTable *get_message_hash; /* borrowed gchar *uid ~> NULL */
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelM365Folder, camel_m365_folder, CAMEL_TYPE_OFFLINE_FOLDER)
+
+static GChecksum *
+m365_folder_cache_new_checksum (const gchar *id)
+{
+       GChecksum *checksum;
+
+       g_return_val_if_fail (id != NULL, NULL);
+
+       /* No need to use SHA here, the string is short, will not collide with the others */
+       checksum = g_checksum_new (G_CHECKSUM_MD5);
+
+       g_checksum_update (checksum, (const guchar *) id, strlen (id));
+
+       return checksum;
+}
+
+static CamelStream *
+m365_folder_cache_add (CamelM365Folder *m365_folder,
+                      const gchar *id,
+                      GError **error)
+{
+       GIOStream *base_stream;
+       CamelStream *stream = NULL;
+       GChecksum *checksum;
+
+       checksum = m365_folder_cache_new_checksum (id);
+
+       LOCK_CACHE (m365_folder);
+       base_stream = camel_data_cache_add (m365_folder->priv->cache, M365_LOCAL_CACHE_PATH, 
g_checksum_get_string (checksum), error);
+       UNLOCK_CACHE (m365_folder);
+
+       g_checksum_free (checksum);
+
+       if (base_stream) {
+               stream = camel_stream_new (base_stream);
+               g_object_unref (base_stream);
+       }
+
+       return stream;
+}
+
+static gint
+m365_folder_cache_remove (CamelM365Folder *m365_folder,
+                         const gchar *id,
+                         GError **error)
+{
+       GChecksum *checksum;
+       gint ret;
+
+       checksum = m365_folder_cache_new_checksum (id);
+
+       LOCK_CACHE (m365_folder);
+       ret = camel_data_cache_remove (m365_folder->priv->cache, M365_LOCAL_CACHE_PATH, g_checksum_get_string 
(checksum), error);
+       UNLOCK_CACHE (m365_folder);
+
+       g_checksum_free (checksum);
+
+       return ret;
+}
+
+static CamelStream *
+m365_folder_cache_get (CamelM365Folder *m365_folder,
+                      const gchar *id,
+                      GError **error)
+{
+       GChecksum *checksum;
+       CamelStream *stream = NULL;
+       GIOStream *base_stream;
+
+       checksum = m365_folder_cache_new_checksum (id);
+
+       LOCK_CACHE (m365_folder);
+       base_stream = camel_data_cache_get (m365_folder->priv->cache, M365_LOCAL_CACHE_PATH, 
g_checksum_get_string (checksum), error);
+       UNLOCK_CACHE (m365_folder);
+
+       g_checksum_free (checksum);
+
+       if (base_stream) {
+               stream = camel_stream_new (base_stream);
+               g_object_unref (base_stream);
+       }
+
+       return stream;
+}
+
+static gchar *
+m365_folder_cache_dup_filename (CamelM365Folder *m365_folder,
+                               const gchar *id)
+{
+       GChecksum *checksum;
+       gchar *filename;
+
+       checksum = m365_folder_cache_new_checksum (id);
+
+       LOCK_CACHE (m365_folder);
+       filename = camel_data_cache_get_filename (m365_folder->priv->cache, M365_LOCAL_CACHE_PATH, 
g_checksum_get_string (checksum));
+       UNLOCK_CACHE (m365_folder);
+
+       g_checksum_free (checksum);
+
+       return filename;
+}
+
+static CamelMimeMessage *
+m365_folder_get_message_from_cache (CamelM365Folder *m365_folder,
+                                   const gchar *uid,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       CamelStream *stream;
+       CamelMimeMessage *msg;
+
+       stream = m365_folder_cache_get (m365_folder, uid, error);
+
+       if (!stream)
+               return NULL;
+
+       msg = camel_mime_message_new ();
+
+       if (!camel_data_wrapper_construct_from_stream_sync (CAMEL_DATA_WRAPPER (msg), stream, cancellable, 
error))
+               g_clear_object (&msg);
+
+       g_object_unref (stream);
+
+       return msg;
+}
+
+static void
+m365_folder_save_summary (CamelM365Folder *m365_folder)
+{
+       CamelFolderSummary *summary;
+
+       g_return_if_fail (CAMEL_IS_M365_FOLDER (m365_folder));
+
+       summary = camel_folder_get_folder_summary (CAMEL_FOLDER (m365_folder));
+
+       if (summary) {
+               GError *error = NULL;
+
+               if (!camel_folder_summary_save (summary, &error))
+                       g_warning ("%s: Failed to save summary: %s", G_STRFUNC, error ? error->message : 
"Unknown error");
+
+               g_clear_error (&error);
+       }
+}
+
+static void
+m365_folder_forget_all_mails (CamelM365Folder *m365_folder)
+{
+       CamelFolder *folder;
+       CamelFolderChangeInfo *changes;
+       CamelFolderSummary *folder_summary;
+       GPtrArray *known_uids;
+       gint ii;
+
+       g_return_if_fail (CAMEL_IS_M365_FOLDER (m365_folder));
+
+       folder = CAMEL_FOLDER (m365_folder);
+       g_return_if_fail (folder != NULL);
+
+       known_uids = camel_folder_summary_get_array (camel_folder_get_folder_summary (folder));
+
+       if (!known_uids)
+               return;
+
+       changes = camel_folder_change_info_new ();
+       folder_summary = camel_folder_get_folder_summary (folder);
+
+       camel_folder_summary_lock (folder_summary);
+
+       for (ii = 0; ii < known_uids->len; ii++) {
+               const gchar *uid = g_ptr_array_index (known_uids, ii);
+
+               camel_folder_change_info_remove_uid (changes, uid);
+               m365_folder_cache_remove (m365_folder, uid, NULL);
+       }
+
+       camel_folder_summary_clear (folder_summary, NULL);
+       camel_folder_summary_unlock (folder_summary);
+
+       m365_folder_save_summary (m365_folder);
+
+       if (camel_folder_change_info_changed (changes))
+               camel_folder_changed (folder, changes);
+
+       camel_folder_change_info_free (changes);
+       camel_folder_summary_free_array (known_uids);
+}
+
+static guint32
+m365_folder_get_permanent_flags (CamelFolder *folder)
+{
+       return CAMEL_MESSAGE_ANSWERED |
+               CAMEL_MESSAGE_DELETED |
+               CAMEL_MESSAGE_DRAFT |
+               CAMEL_MESSAGE_FLAGGED |
+               CAMEL_MESSAGE_SEEN |
+               CAMEL_MESSAGE_FORWARDED |
+               CAMEL_MESSAGE_USER;
+}
+
+static CamelMimeMessage *
+m365_folder_get_message_cached (CamelFolder *folder,
+                               const gchar *message_uid,
+                               GCancellable *cancellable)
+{
+       return m365_folder_get_message_from_cache (CAMEL_M365_FOLDER (folder), message_uid, cancellable, 
NULL);
+}
+
+static void
+m365_folder_exec_search (CamelFolder *folder,
+                        const gchar *expression,
+                        GPtrArray *uids,
+                        GPtrArray **out_matches,
+                        guint32 *out_count,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       CamelM365Folder *m365_folder;
+
+       g_return_if_fail (CAMEL_IS_M365_FOLDER (folder));
+
+       m365_folder = CAMEL_M365_FOLDER (folder);
+
+       LOCK_SEARCH (m365_folder);
+
+       camel_folder_search_set_folder (m365_folder->priv->search, folder);
+
+       if (out_matches)
+               *out_matches = camel_folder_search_search (m365_folder->priv->search, expression, uids, 
cancellable, error);
+
+       if (out_count)
+               *out_count = camel_folder_search_count (m365_folder->priv->search, expression, cancellable, 
error);
+
+       UNLOCK_SEARCH (m365_folder);
+}
+
+static GPtrArray *
+m365_folder_search_by_expression (CamelFolder *folder,
+                                 const gchar *expression,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       GPtrArray *matches = NULL;
+
+       m365_folder_exec_search (folder, expression, NULL, &matches, NULL, cancellable, error);
+
+       return matches;
+}
+
+static guint32
+m365_folder_count_by_expression (CamelFolder *folder,
+                                const gchar *expression,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       guint32 count = 0;
+
+       m365_folder_exec_search (folder, expression, NULL, NULL, &count, cancellable, error);
+
+       return count;
+}
+
+static GPtrArray *
+m365_folder_search_by_uids (CamelFolder *folder,
+                           const gchar *expression,
+                           GPtrArray *uids,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       GPtrArray *matches = NULL;
+
+       if (uids->len == 0)
+               return g_ptr_array_new ();
+
+       m365_folder_exec_search (folder, expression, uids, &matches, NULL, cancellable, error);
+
+       return matches;
+}
+
+static void
+m365_folder_search_free (CamelFolder *folder,
+                        GPtrArray *uids)
+{
+       CamelM365Folder *m365_folder;
+
+       if (!uids)
+               return;
+
+       g_return_if_fail (CAMEL_IS_M365_FOLDER (folder));
+
+       m365_folder = CAMEL_M365_FOLDER (folder);
+
+       LOCK_SEARCH (m365_folder);
+
+       camel_folder_search_free_result (m365_folder->priv->search, uids);
+
+       UNLOCK_SEARCH (m365_folder);
+}
+
+static gint
+m365_folder_cmp_uids (CamelFolder *folder,
+                     const gchar *uid1,
+                     const gchar *uid2)
+{
+       g_return_val_if_fail (uid1 != NULL, 0);
+       g_return_val_if_fail (uid2 != NULL, 0);
+
+       return strcmp (uid1, uid2);
+}
+
+static gboolean
+m365_folder_download_message_cb (EM365Connection *cnc,
+                                SoupMessage *message,
+                                GInputStream *raw_data_stream,
+                                gpointer user_data,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       CamelStream *cache_stream = user_data;
+       gssize expected_size = 0, wrote_size = 0, last_percent = -1;
+       gint last_progress_notify = 0;
+       gsize buffer_size = 65535;
+       gchar *buffer;
+       gboolean success;
+
+       g_return_val_if_fail (CAMEL_IS_STREAM (cache_stream), FALSE);
+       g_return_val_if_fail (G_IS_INPUT_STREAM (raw_data_stream), FALSE);
+
+       if (message && message->response_headers) {
+               const gchar *content_length_str;
+
+               content_length_str = soup_message_headers_get_one (message->response_headers, 
"Content-Length");
+
+               if (content_length_str && *content_length_str)
+                       expected_size = (gssize) g_ascii_strtoll (content_length_str, NULL, 10);
+       }
+
+       buffer = g_malloc (buffer_size);
+
+       do {
+               success = !g_cancellable_set_error_if_cancelled (cancellable, error);
+
+               if (success) {
+                       gssize n_read, n_wrote;
+
+                       n_read = g_input_stream_read (raw_data_stream, buffer, buffer_size, cancellable, 
error);
+
+                       if (n_read == -1) {
+                               success = FALSE;
+                       } else if (!n_read) {
+                               break;
+                       } else {
+                               n_wrote = camel_stream_write (cache_stream, buffer, n_read, cancellable, 
error);
+                               success = n_read == n_wrote;
+
+                               if (success && expected_size > 0) {
+                                       gssize percent;
+
+                                       wrote_size += n_wrote;
+
+                                       percent = wrote_size * 100.0 / expected_size;
+
+                                       if (percent > 100)
+                                               percent = 100;
+
+                                       if (percent != last_percent) {
+                                               gint64 now = g_get_monotonic_time ();
+
+                                               /* Notify only 10 times per second, not more */
+                                               if (percent == 100 || now - last_progress_notify > 
G_USEC_PER_SEC / 10) {
+                                                       last_progress_notify = now;
+                                                       last_percent = percent;
+
+                                                       camel_operation_progress (cancellable, percent);
+                                               }
+                                       }
+                               }
+                       }
+               }
+       } while (success);
+
+       g_free (buffer);
+
+       if (success)
+               camel_stream_flush (cache_stream, cancellable, NULL);
+
+       return success;
+}
+
+static void
+m365_folder_get_message_cancelled_cb (GCancellable *cancellable,
+                                     gpointer user_data)
+{
+       CamelM365Folder *m365_folder = user_data;
+
+       g_return_if_fail (CAMEL_IS_M365_FOLDER (m365_folder));
+
+       g_mutex_lock (&m365_folder->priv->get_message_lock);
+       g_cond_broadcast (&m365_folder->priv->get_message_cond);
+       g_mutex_unlock (&m365_folder->priv->get_message_lock);
+}
+
+static CamelMimeMessage *
+m365_folder_get_message_sync (CamelFolder *folder,
+                             const gchar *uid,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       CamelMimeMessage *message = NULL;
+       CamelM365Folder *m365_folder;
+       CamelM365Store *m365_store;
+       CamelStore *parent_store;
+       CamelStream *cache_stream = NULL;
+       EM365Connection *cnc = NULL;
+       GError *local_error = NULL;
+       const gchar *folder_id;
+       gboolean success = TRUE, remove_from_hash = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER (folder), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       parent_store = camel_folder_get_parent_store (folder);
+
+       if (!parent_store) {
+               g_set_error_literal (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID_STATE,
+                       _("Invalid folder state (missing parent store)"));
+               return NULL;
+       }
+
+       m365_folder = CAMEL_M365_FOLDER (folder);
+       m365_store = CAMEL_M365_STORE (parent_store);
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+               return NULL;
+
+       folder_id = camel_m365_folder_get_id (m365_folder);
+
+       g_mutex_lock (&m365_folder->priv->get_message_lock);
+
+       if (g_hash_table_contains (m365_folder->priv->get_message_hash, uid)) {
+               gulong handler_id = 0;
+
+               if (cancellable) {
+                       handler_id = g_signal_connect (cancellable, "cancelled",
+                               G_CALLBACK (m365_folder_get_message_cancelled_cb), m365_folder);
+               }
+
+               while (success = !g_cancellable_set_error_if_cancelled (cancellable, error),
+                      success && g_hash_table_contains (m365_folder->priv->get_message_hash, uid)) {
+                       g_cond_wait (&m365_folder->priv->get_message_cond, 
&m365_folder->priv->get_message_lock);
+               }
+
+               if (success)
+                       message = m365_folder_get_message_from_cache (m365_folder, uid, cancellable, NULL);
+
+               if (handler_id)
+                       g_signal_handler_disconnect (cancellable, handler_id);
+       }
+
+       if (success && !message) {
+               g_hash_table_insert (m365_folder->priv->get_message_hash, (gpointer) uid, NULL);
+               remove_from_hash = TRUE;
+       }
+
+       g_mutex_unlock (&m365_folder->priv->get_message_lock);
+
+       if (success && !message) {
+               cache_stream = m365_folder_cache_add (m365_folder, uid, error);
+
+               success = cache_stream != NULL;
+
+               success = success && e_m365_connection_get_mail_message_sync (cnc, NULL, folder_id, uid,
+                       m365_folder_download_message_cb, cache_stream, cancellable, &local_error);
+
+               if (local_error) {
+                       camel_m365_store_maybe_disconnect (m365_store, local_error);
+
+                       g_propagate_error (error, local_error);
+                       success = FALSE;
+               }
+
+               if (success) {
+                       /* First free the cache stream, thus the follwing call opens a new instance,
+                          which is rewinded at the beginning of the stream. */
+                       g_clear_object (&cache_stream);
+
+                       message = m365_folder_get_message_from_cache (m365_folder, uid, cancellable, error);
+               }
+       }
+
+       g_clear_object (&cache_stream);
+       g_clear_object (&cnc);
+
+       if (remove_from_hash) {
+               g_mutex_lock (&m365_folder->priv->get_message_lock);
+               g_hash_table_remove (m365_folder->priv->get_message_hash, uid);
+               g_cond_broadcast (&m365_folder->priv->get_message_cond);
+               g_mutex_unlock (&m365_folder->priv->get_message_lock);
+       }
+
+       return message;
+}
+
+static gboolean
+m365_folder_append_message_sync (CamelFolder *folder,
+                                CamelMimeMessage *message,
+                                CamelMessageInfo *info,
+                                gchar **appended_uid,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       /* Cannot put existing messages from other providers, because:
+          1) those are always set as drafts
+          2) the set sentDateTime property is not respected
+          3) internetMessageHeaders is limited to 5! headers only:
+             {
+               "error": {
+                       "code": "InvalidInternetMessageHeaderCollection",
+                       "message": "Maximum number of headers in one message should be less than or equal to 
5.",
+                       "innerError": {
+                               "date": "2020-07-01T10:03:34",
+                               "request-id": "a46da0ea-8933-43c6-932d-7c751f226516"
+                       }
+               }
+             }
+          4) there are likely to be more limitations on the graph API, not spotted yet.
+
+          There is opened a feture request, which may eventually fix this, but it's currently not done yet 
(as of 2020-07-01):
+          
https://microsoftgraph.uservoice.com/forums/920506-microsoft-graph-feature-requests/suggestions/35049175-put-edit-mime-email-content-with-microsoft-graph
+
+          Thus just error out for now.
+       */
+
+#ifdef ENABLE_MAINTAINER_MODE /* Only for easier testing */
+       CamelStore *parent_store;
+       CamelM365Store *m365_store;
+       EM365Connection *cnc = NULL;
+       gboolean success;
+       GError *local_error = NULL;
+
+       parent_store = camel_folder_get_parent_store (folder);
+
+       if (!CAMEL_IS_M365_STORE (parent_store)) {
+               g_set_error_literal (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID_STATE,
+                       _("Invalid folder state (missing parent store)"));
+               return FALSE;
+       }
+
+       m365_store = CAMEL_M365_STORE (parent_store);
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+               return FALSE;
+
+       success = camel_m365_utils_create_message_sync (cnc, camel_m365_folder_get_id (CAMEL_M365_FOLDER 
(folder)),
+               message, info, appended_uid, cancellable, &local_error);
+
+       g_clear_object (&cnc);
+
+       if (!success)
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+
+       if (local_error)
+               g_propagate_error (error, local_error);
+
+       return success;
+#else
+       g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+               _("Cannot add messages into an Microsoft 365 account from another account. Only messages from 
the same account can be moved/copied between the Microsoft 365 folders."));
+       return FALSE;
+#endif
+}
+
+static gboolean
+m365_folder_merge_server_user_flags (CamelMessageInfo *mi,
+                                    EM365MailMessage *mail)
+{
+       CamelFolderSummary *summary;
+       JsonArray *categories;
+       GHashTable *current_labels;
+       const CamelNamedFlags *user_flags;
+       guint ii, len;
+       gboolean changed = FALSE;
+
+       summary = camel_message_info_ref_summary (mi);
+       if (summary)
+               camel_folder_summary_lock (summary);
+       camel_message_info_property_lock (mi);
+       camel_message_info_freeze_notifications (mi);
+
+       current_labels = g_hash_table_new (g_str_hash, g_str_equal);
+
+       user_flags = camel_message_info_get_user_flags (mi);
+       len = camel_named_flags_get_length (user_flags);
+
+       for (ii = 0; ii < len; ii++) {
+               const gchar *name = camel_named_flags_get (user_flags, ii);
+
+               if (!camel_m365_utils_is_system_user_flag (name))
+                       g_hash_table_insert (current_labels, (gpointer) name, NULL);
+       }
+
+       categories = e_m365_mail_message_get_categories (mail);
+
+       if (categories) {
+               len = json_array_get_length (categories);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *name = json_array_get_string_element (categories, ii);
+
+                       name = camel_m365_utils_rename_label (name, TRUE);
+
+                       if (name && *name) {
+                               gchar *flag;
+
+                               flag = camel_m365_utils_encode_category_name (name);
+
+                               if (!g_hash_table_remove (current_labels, flag)) {
+                                       changed = TRUE;
+
+                                       camel_message_info_set_user_flag (mi, flag, TRUE);
+                               }
+
+                               g_free (flag);
+                       }
+               }
+       }
+
+       /* Those left here are to be removed */
+       if (g_hash_table_size (current_labels)) {
+               GHashTableIter iter;
+               gpointer key;
+
+               changed = TRUE;
+
+               g_hash_table_iter_init (&iter, current_labels);
+
+               while (g_hash_table_iter_next (&iter, &key, NULL)) {
+                       camel_message_info_set_user_flag (mi, key, FALSE);
+               }
+       }
+
+       camel_message_info_thaw_notifications (mi);
+       camel_message_info_property_unlock (mi);
+       if (summary)
+               camel_folder_summary_unlock (summary);
+       g_clear_object (&summary);
+
+       return changed;
+}
+
+static gboolean
+m365_folder_update_message_info (CamelMessageInfo *mi,
+                                EM365MailMessage *mail)
+{
+       CamelM365MessageInfo *m365_mi;
+       guint32 flags = 0;
+       gboolean changed = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (mi), FALSE);
+       g_return_val_if_fail (mail != NULL, FALSE);
+
+       m365_mi = CAMEL_M365_MESSAGE_INFO (mi);
+
+       if (e_m365_mail_message_get_has_attachments (mail))
+               flags |= CAMEL_MESSAGE_ATTACHMENTS;
+
+       if (e_m365_mail_message_get_is_draft (mail))
+               flags |= CAMEL_MESSAGE_DRAFT;
+
+       if (e_m365_mail_message_get_is_read (mail))
+               flags |= CAMEL_MESSAGE_SEEN;
+
+       if (e_m365_mail_message_get_importance (mail) == E_M365_IMPORTANCE_HIGH)
+               flags |= CAMEL_MESSAGE_FLAGGED;
+
+       /* 2020-06-24 - cannot make it work, even with 
https://stackoverflow.com/questions/58205494/access-the-replied-forwarded-etc-state-from-rest */
+       /* CAMEL_MESSAGE_ANSWERED
+       CAMEL_MESSAGE_FORWARDED */
+
+       if (camel_m365_message_info_set_server_flags (m365_mi, flags)) {
+               guint32 mask;
+
+               mask = CAMEL_MESSAGE_ATTACHMENTS | CAMEL_MESSAGE_DRAFT | CAMEL_MESSAGE_SEEN | 
CAMEL_MESSAGE_FLAGGED;
+
+               camel_message_info_set_flags (mi, mask, flags);
+
+               changed = TRUE;
+       }
+
+       changed = m365_folder_merge_server_user_flags (mi, mail) || changed;
+
+       return changed;
+}
+
+static gchar *
+m365_folder_recipients_as_string (JsonArray *recipients) /* EM365Recipient * */
+{
+       CamelInternetAddress *addrs;
+       guint ii, len;
+       gchar *res;
+
+       if (!recipients)
+               return NULL;
+
+       addrs = camel_internet_address_new ();
+
+       len = json_array_get_length (recipients);
+       for (ii = 0; ii < len; ii++) {
+               EM365Recipient *recipient = json_array_get_object_element (recipients, ii);
+               const gchar *name, *address;
+
+               name = e_m365_recipient_get_name (recipient);
+               address = e_m365_recipient_get_address (recipient);
+
+               if (address && *address)
+                       camel_internet_address_add (addrs, name, address);
+       }
+
+       if (camel_address_length (CAMEL_ADDRESS (addrs)) > 0) {
+               res = camel_address_format (CAMEL_ADDRESS (addrs));
+       } else {
+               res = NULL;
+       }
+
+       g_clear_object (&addrs);
+
+       return res;
+}
+
+static CamelMessageInfo *
+m365_folder_new_message_info_from_mail_message (CamelFolder *folder,
+                                               EM365MailMessage *mail)
+{
+       CamelMessageInfo *mi = NULL;
+       CamelNameValueArray *headers = NULL;
+       JsonArray *json_headers;
+       EM365Recipient *from;
+       const gchar *ctmp;
+       time_t tt;
+       gchar *tmp;
+
+       g_return_val_if_fail (CAMEL_IS_FOLDER (folder), NULL);
+       g_return_val_if_fail (mail != NULL, NULL);
+
+       json_headers = e_m365_mail_message_get_internet_message_headers (mail);
+
+       if (json_headers && json_array_get_length (json_headers) > 0) {
+               guint ii, len = json_array_get_length (json_headers);
+
+               headers = camel_name_value_array_new_sized (len);
+
+               for (ii = 0; ii < len; ii++) {
+                       EM365InternetMessageHeader *header = json_array_get_object_element (json_headers, ii);
+                       const gchar *name, *value;
+
+                       name = e_m365_internet_message_header_get_name (header);
+                       value = e_m365_internet_message_header_get_value (header);
+
+                       if (name && *name)
+                               camel_name_value_array_append (headers, name, value ? value : "");
+               }
+
+               if (camel_name_value_array_get_length (headers)) {
+                       mi = camel_message_info_new_from_headers (camel_folder_get_folder_summary (folder), 
headers);
+               } else {
+                       camel_name_value_array_free (headers);
+                       headers = NULL;
+               }
+       }
+
+       if (!mi)
+               mi = camel_message_info_new (camel_folder_get_folder_summary (folder));
+
+       camel_message_info_set_abort_notifications (mi, TRUE);
+
+       ctmp = e_m365_mail_message_get_subject (mail);
+
+       if (ctmp)
+               camel_message_info_set_subject  (mi, ctmp);
+
+       from = e_m365_mail_message_get_from (mail);
+
+       if (from) {
+               const gchar *name, *address;
+
+               name = e_m365_recipient_get_name (from);
+               address = e_m365_recipient_get_address (from);
+
+               if (address && *address) {
+                       tmp = camel_internet_address_format_address (name, address);
+
+                       if (tmp) {
+                               camel_message_info_set_from (mi, tmp);
+
+                               g_free (tmp);
+                       }
+               }
+       }
+
+       tmp = m365_folder_recipients_as_string (e_m365_mail_message_get_to_recipients (mail));
+
+       if (tmp) {
+               camel_message_info_set_to (mi, tmp);
+               g_free (tmp);
+       }
+
+       tmp = m365_folder_recipients_as_string (e_m365_mail_message_get_cc_recipients (mail));
+
+       if (tmp) {
+               camel_message_info_set_cc (mi, tmp);
+               g_free (tmp);
+       }
+
+       tt = e_m365_mail_message_get_sent_date_time (mail);
+
+       if (tt)
+               camel_message_info_set_date_sent (mi, (gint64) tt);
+
+       tt = e_m365_mail_message_get_received_date_time (mail);
+
+       if (tt)
+               camel_message_info_set_date_received (mi, (gint64) tt);
+
+       ctmp = e_m365_mail_message_get_internet_message_id (mail);
+
+       if (ctmp && *ctmp) {
+               GChecksum *checksum;
+               CamelSummaryMessageID message_id;
+               guint8 *digest;
+               gsize length;
+
+               length = g_checksum_type_get_length (G_CHECKSUM_MD5);
+               digest = g_alloca (length);
+
+               checksum = g_checksum_new (G_CHECKSUM_MD5);
+               g_checksum_update (checksum, (const guchar *) ctmp, -1);
+               g_checksum_get_digest (checksum, digest, &length);
+               g_checksum_free (checksum);
+
+               memcpy (message_id.id.hash, digest, sizeof (message_id.id.hash));
+
+               camel_message_info_set_message_id (mi, message_id.id.id);
+       }
+
+       camel_message_info_set_uid (mi, e_m365_mail_message_get_id (mail));
+
+       if (headers)
+               camel_message_info_take_headers (mi, headers);
+
+       camel_message_info_set_abort_notifications (mi, FALSE);
+
+       m365_folder_update_message_info (mi, mail);
+
+       return mi;
+}
+
+typedef struct _SummaryDeltaData {
+       CamelFolder *folder;
+       CamelFolderChangeInfo *changes;
+       GList *removed_uids; /* gchar * - from the Camel string pool */
+} SummaryDeltaData;
+
+static gboolean
+m365_folder_got_summary_messages_cb (EM365Connection *cnc,
+                                    const GSList *results, /* JsonObject * - the returned objects from the 
server */
+                                    gpointer user_data,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       SummaryDeltaData *sdd = user_data;
+       CamelFolderSummary *summary;
+       GSList *link;
+
+       g_return_val_if_fail (sdd != NULL, FALSE);
+
+       summary = camel_folder_get_folder_summary (sdd->folder);
+
+       if (!summary)
+               return FALSE;
+
+       for (link = (GSList *) results; link; link = g_slist_next (link)) {
+               EM365MailMessage *mail = link->data;
+               const gchar *id;
+
+               id = e_m365_mail_message_get_id (mail);
+
+               if (!id)
+                       continue;
+
+               if (!sdd->changes)
+                       sdd->changes = camel_folder_change_info_new ();
+
+               if (e_m365_delta_is_removed_object (mail)) {
+                       sdd->removed_uids = g_list_prepend (sdd->removed_uids, (gpointer) 
camel_pstring_strdup (id));
+
+                       camel_folder_change_info_remove_uid (sdd->changes, id);
+               } else {
+                       CamelMessageInfo *info;
+
+                       info = camel_folder_summary_get (summary, id);
+
+                       if (info) {
+                               if (m365_folder_update_message_info (info, mail))
+                                       camel_folder_change_info_change_uid (sdd->changes, id);
+
+                               g_object_unref (info);
+                       } else {
+                               info = m365_folder_new_message_info_from_mail_message (sdd->folder, mail);
+
+                               if (info) {
+                                       camel_folder_summary_add (summary, info, TRUE);
+
+                                       /* Unset folder-flagged flag, which ahd been set by the 
camel_folder_summary_add(),
+                                          to avoid re-sync on the just added message. */
+                                       camel_message_info_set_folder_flagged (info, FALSE);
+
+                                       camel_folder_change_info_add_uid (sdd->changes, id);
+                                       camel_folder_change_info_recent_uid (sdd->changes, id);
+
+                                       g_object_unref (info);
+                               }
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+m365_folder_refresh_info_sync (CamelFolder *folder,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       CamelM365Folder *m365_folder;
+       CamelM365FolderSummary *m365_folder_summary;
+       CamelM365Store *m365_store;
+       CamelFolderSummary *folder_summary;
+       CamelStore *parent_store;
+       EM365Connection *cnc = NULL;
+       SummaryDeltaData sdd;
+       GError *local_error = NULL;
+       const gchar *folder_id;
+       gchar *curr_delta_link, *new_delta_link = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER (folder), FALSE);
+
+       parent_store = camel_folder_get_parent_store (folder);
+
+       if (!parent_store) {
+               g_set_error_literal (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID_STATE,
+                       _("Invalid folder state (missing parent store)"));
+               return FALSE;
+       }
+
+       m365_folder = CAMEL_M365_FOLDER (folder);
+       m365_store = CAMEL_M365_STORE (parent_store);
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+               return FALSE;
+
+       folder_id = camel_m365_folder_get_id (m365_folder);
+       folder_summary = camel_folder_get_folder_summary (folder);
+       m365_folder_summary = CAMEL_M365_FOLDER_SUMMARY (folder_summary);
+
+       curr_delta_link = camel_m365_folder_summary_dup_delta_link (m365_folder_summary);
+
+       sdd.folder = folder;
+       sdd.changes = NULL;
+       sdd.removed_uids = NULL;
+
+       success = e_m365_connection_get_objects_delta_sync (cnc, NULL, E_M365_FOLDER_KIND_MAIL, folder_id, 
M365_FETCH_SUMMARY_PROPERTIES,
+               curr_delta_link, 0, m365_folder_got_summary_messages_cb, &sdd,
+               &new_delta_link, cancellable, &local_error);
+
+       if (curr_delta_link && e_m365_connection_util_delta_token_failed (local_error)) {
+               g_clear_error (&local_error);
+               g_clear_pointer (&curr_delta_link, g_free);
+
+               camel_m365_folder_summary_set_delta_link (m365_folder_summary, NULL);
+
+               m365_folder_forget_all_mails (m365_folder);
+
+               success = e_m365_connection_get_objects_delta_sync (cnc, NULL, E_M365_FOLDER_KIND_MAIL, 
folder_id, M365_FETCH_SUMMARY_PROPERTIES,
+                       NULL, 0, m365_folder_got_summary_messages_cb, &sdd,
+                       &new_delta_link, cancellable, &local_error);
+       }
+
+       if (success && new_delta_link)
+               camel_m365_folder_summary_set_delta_link (m365_folder_summary, new_delta_link);
+
+       if (sdd.removed_uids) {
+               camel_folder_summary_remove_uids (folder_summary, sdd.removed_uids);
+
+               g_list_free_full (sdd.removed_uids, (GDestroyNotify) camel_pstring_free);
+       }
+
+       m365_folder_save_summary (m365_folder);
+
+       if (sdd.changes) {
+               if (camel_folder_change_info_changed (sdd.changes))
+                       camel_folder_changed (folder, sdd.changes);
+
+               camel_folder_change_info_free (sdd.changes);
+       }
+
+       if (local_error) {
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+
+               g_propagate_error (error, local_error);
+               success = FALSE;
+       }
+
+       g_clear_object (&cnc);
+       g_free (curr_delta_link);
+       g_free (new_delta_link);
+
+       return success;
+}
+
+static gboolean
+m365_folder_copy_move_to_folder_sync (CamelFolder *folder,
+                                     CamelM365Store *m365_store,
+                                     const GSList *uids,
+                                     const gchar *des_folder_id,
+                                     gboolean do_copy,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       CamelM365StoreSummary *m365_store_summary;
+       EM365Connection *cnc = NULL;
+       GSList *des_ids = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (des_folder_id != NULL, FALSE);
+
+       m365_store_summary = camel_m365_store_ref_store_summary (m365_store);
+
+       if (g_strcmp0 (des_folder_id, "junkemail") == 0) {
+               des_folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store_summary, 
CAMEL_FOLDER_TYPE_JUNK);
+       } else if (g_strcmp0 (des_folder_id, "deleteditems") == 0) {
+               des_folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store_summary, 
CAMEL_FOLDER_TYPE_TRASH);
+       } else if (g_strcmp0 (des_folder_id, "inbox") == 0) {
+               des_folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store_summary, 
CAMEL_FOLDER_TYPE_INBOX);
+       }
+
+       g_clear_object (&m365_store_summary);
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+               return FALSE;
+
+       success = e_m365_connection_copy_move_mail_messages_sync (cnc, NULL, uids, des_folder_id, do_copy,
+               &des_ids, cancellable, error);
+
+       g_clear_object (&cnc);
+
+       if (!do_copy) {
+               CamelFolderChangeInfo *src_changes;
+               CamelM365Folder *m365_folder;
+               GSList *des_link, *src_link;
+               GList *removed_uids = NULL;
+
+               src_changes = camel_folder_change_info_new ();
+               m365_folder = CAMEL_M365_FOLDER (folder);
+
+               camel_folder_lock (folder);
+
+               /* Can succeed partially, thus always check the moved ids */
+               for (src_link = (GSList *) uids, des_link = des_ids;
+                    src_link && des_link;
+                    src_link = g_slist_next (src_link), des_link = g_slist_next (des_link)) {
+                       const gchar *src_uid = src_link->data;
+
+                       m365_folder_cache_remove (m365_folder, src_uid, NULL);
+
+                       removed_uids = g_list_prepend (removed_uids, (gpointer) src_uid);
+                       camel_folder_change_info_remove_uid (src_changes, src_uid);
+               }
+
+               if (removed_uids) {
+                       CamelFolderSummary *summary;
+
+                       summary = camel_folder_get_folder_summary (folder);
+                       camel_folder_summary_remove_uids (summary, removed_uids);
+
+                       g_list_free (removed_uids);
+               }
+
+               if (camel_folder_change_info_changed (src_changes))
+                       camel_folder_changed (folder, src_changes);
+
+               camel_folder_change_info_free (src_changes);
+
+               camel_folder_unlock (folder);
+       }
+
+       g_slist_free_full (des_ids, (GDestroyNotify) camel_pstring_free);
+
+       return success;
+}
+
+static gboolean
+m365_folder_delete_messages_sync (CamelFolder *folder,
+                                 CamelM365Store *m365_store,
+                                 const GSList *uids,
+                                 gboolean is_trash_folder,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       EM365Connection *cnc = NULL;
+       gboolean success;
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+               return FALSE;
+
+       if (is_trash_folder) {
+               GSList *deleted_uids = NULL, *link;
+
+               success = e_m365_connection_delete_mail_messages_sync (cnc, NULL, uids, &deleted_uids, 
cancellable, error);
+
+               if (deleted_uids) {
+                       CamelFolderChangeInfo *changes;
+                       CamelM365Folder *m365_folder;
+                       GList *removed_uids = NULL;
+
+                       m365_folder = CAMEL_M365_FOLDER (folder);
+                       changes = camel_folder_change_info_new ();
+
+                       camel_folder_lock (folder);
+
+                       /* Can succeed partially, thus always check the moved ids */
+                       for (link = deleted_uids; link; link = g_slist_next (link)) {
+                               const gchar *uid = link->data;
+
+                               m365_folder_cache_remove (m365_folder, uid, NULL);
+
+                               removed_uids = g_list_prepend (removed_uids, (gpointer) uid);
+                               camel_folder_change_info_remove_uid (changes, uid);
+                       }
+
+                       if (removed_uids) {
+                               CamelFolderSummary *summary;
+
+                               summary = camel_folder_get_folder_summary (folder);
+                               camel_folder_summary_remove_uids (summary, removed_uids);
+
+                               g_list_free (removed_uids);
+                       }
+
+                       if (camel_folder_change_info_changed (changes))
+                               camel_folder_changed (folder, changes);
+
+                       camel_folder_change_info_free (changes);
+
+                       camel_folder_unlock (folder);
+
+                       g_slist_free (deleted_uids);
+               }
+       } else {
+               success = m365_folder_copy_move_to_folder_sync (folder, m365_store,
+                       uids, "deleteditems", FALSE, cancellable, error);
+       }
+
+       g_clear_object (&cnc);
+
+       return success;
+}
+
+static JsonBuilder *
+m365_folder_message_info_changes_to_json (CamelMessageInfo *mi)
+{
+       JsonBuilder *builder;
+
+       builder = json_builder_new_immutable ();
+       e_m365_json_begin_object_member (builder, NULL);
+
+       camel_m365_utils_add_message_flags (builder, mi, NULL);
+
+       e_m365_json_end_object_member (builder);
+
+       return builder;
+}
+
+static gboolean
+m365_folder_save_flags_sync (CamelFolder *folder,
+                            CamelM365Store *m365_store,
+                            GSList *mi_list, /* CamelMessageInfo * */
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       EM365Connection *cnc = NULL;
+       gboolean success = TRUE;
+
+       /* Trap an error, but do not stop other processing */
+       g_return_val_if_fail (mi_list != NULL, TRUE);
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+               return FALSE;
+
+       if (mi_list->next) {
+               GSList *link;
+               GPtrArray *requests;
+
+               requests = g_ptr_array_new_full (g_slist_length (mi_list), g_object_unref);
+
+               for (link = mi_list; link && success; link = g_slist_next (link)) {
+                       CamelMessageInfo *mi = link->data;
+                       SoupMessage *message;
+                       JsonBuilder *builder;
+
+                       builder = m365_folder_message_info_changes_to_json (mi);
+
+                       message = e_m365_connection_prepare_update_mail_message (cnc, NULL,
+                               camel_message_info_get_uid (mi), builder, error);
+
+                       g_clear_object (&builder);
+
+                       if (!message)
+                               success = FALSE;
+                       else
+                               g_ptr_array_add (requests, message);
+               }
+
+               if (success)
+                       success = e_m365_connection_batch_request_sync (cnc, E_M365_API_V1_0, requests, 
cancellable, error);
+
+               g_ptr_array_free (requests, TRUE);
+       } else {
+               CamelMessageInfo *mi = mi_list->data;
+               JsonBuilder *builder;
+
+               builder = m365_folder_message_info_changes_to_json (mi);
+
+               success = e_m365_connection_update_mail_message_sync (cnc, NULL,
+                       camel_message_info_get_uid (mi), builder, cancellable, error);
+
+               g_clear_object (&builder);
+       }
+
+       g_object_unref (cnc);
+
+       if (success) {
+               GSList *link;
+
+               camel_folder_lock (folder);
+
+               for (link = mi_list; link; link = g_slist_next (link)) {
+                       CamelMessageInfo *mi = link->data;
+
+                       camel_message_info_set_folder_flagged (mi, FALSE);
+               }
+
+               camel_folder_unlock (folder);
+       }
+
+       return success;
+}
+
+static gboolean
+m365_folder_is_of_type (CamelFolder *folder,
+                       guint32 folder_type)
+{
+       CamelStore *parent_store;
+       CamelM365Store *m365_store;
+       CamelM365StoreSummary *m365_store_summary;
+       gboolean is_of_type;
+       const gchar *folder_id;
+
+       g_return_val_if_fail (folder != NULL, FALSE);
+
+       parent_store = camel_folder_get_parent_store (folder);
+
+       if (!parent_store)
+               return FALSE;
+
+       m365_store = CAMEL_M365_STORE (parent_store);
+
+       g_return_val_if_fail (m365_store != NULL, FALSE);
+
+       m365_store_summary = camel_m365_store_ref_store_summary (m365_store);
+
+       folder_type = folder_type & CAMEL_FOLDER_TYPE_MASK;
+       folder_id = camel_m365_folder_get_id (CAMEL_M365_FOLDER (folder));
+       is_of_type = folder_id &&
+               (camel_m365_store_summary_get_folder_flags (m365_store_summary, folder_id) & 
CAMEL_FOLDER_TYPE_MASK) == folder_type;
+
+       g_clear_object (&m365_store_summary);
+
+       return is_of_type;
+}
+
+static gboolean
+m365_folder_synchronize_sync (CamelFolder *folder,
+                             gboolean expunge,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       CamelM365Store *m365_store;
+       CamelStore *parent_store;
+       CamelFolderSummary *folder_summary;
+       GPtrArray *uids;
+       GSList *mi_list = NULL, *deleted_uids = NULL, *junk_uids = NULL, *inbox_uids = NULL;
+       gint mi_list_len = 0;
+       gboolean is_junk_folder;
+       gboolean success = TRUE;
+       guint ii;
+       GError *local_error = NULL;
+
+       parent_store = camel_folder_get_parent_store (folder);
+
+       if (!parent_store) {
+               g_set_error_literal (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID_STATE,
+                       _("Invalid folder state (missing parent store)"));
+               return FALSE;
+       }
+
+       m365_store = CAMEL_M365_STORE (parent_store);
+
+       if (!camel_m365_store_ensure_connected (m365_store, NULL, cancellable, error))
+               return FALSE;
+
+       folder_summary = camel_folder_get_folder_summary (folder);
+
+       if (camel_folder_summary_get_deleted_count (folder_summary) > 0 ||
+           camel_folder_summary_get_junk_count (folder_summary) > 0) {
+               camel_folder_summary_prepare_fetch_all (folder_summary, NULL);
+               uids = camel_folder_summary_get_array (folder_summary);
+       } else {
+               uids = camel_folder_summary_get_changed (folder_summary);
+       }
+
+       if (!uids || !uids->len) {
+               camel_folder_summary_free_array (uids);
+               return TRUE;
+       }
+
+       is_junk_folder = m365_folder_is_of_type (folder, CAMEL_FOLDER_TYPE_JUNK);
+
+       for (ii = 0; success && ii < uids->len; ii++) {
+               guint32 flags_changed, flags_set;
+               CamelMessageInfo *mi;
+               const gchar *uid;
+
+               uid = uids->pdata[ii];
+               mi = camel_folder_summary_get (folder_summary, uid);
+
+               if (!mi)
+                       continue;
+
+               flags_set = camel_message_info_get_flags (mi);
+               flags_changed = camel_m365_message_info_get_server_flags (CAMEL_M365_MESSAGE_INFO (mi)) ^ 
flags_set;
+
+               if ((flags_set & CAMEL_MESSAGE_FOLDER_FLAGGED) != 0 &&
+                   (flags_changed & (CAMEL_MESSAGE_SEEN | CAMEL_MESSAGE_ANSWERED | CAMEL_MESSAGE_FORWARDED | 
CAMEL_MESSAGE_FLAGGED)) != 0) {
+                       mi_list = g_slist_prepend (mi_list, mi);
+                       mi_list_len++;
+
+                       if (flags_set & CAMEL_MESSAGE_DELETED)
+                               deleted_uids = g_slist_prepend (deleted_uids, (gpointer) camel_pstring_strdup 
(uid));
+                       else if (flags_set & CAMEL_MESSAGE_JUNK)
+                               junk_uids = g_slist_prepend (junk_uids, (gpointer) camel_pstring_strdup 
(uid));
+                       else if (is_junk_folder && (flags_set & CAMEL_MESSAGE_NOTJUNK) != 0)
+                               inbox_uids = g_slist_prepend (inbox_uids, (gpointer) camel_pstring_strdup 
(uid));
+               } else if (flags_set & CAMEL_MESSAGE_DELETED) {
+                       deleted_uids = g_slist_prepend (deleted_uids, (gpointer) camel_pstring_strdup (uid));
+                       g_clear_object (&mi);
+               } else if (flags_set & CAMEL_MESSAGE_JUNK) {
+                       junk_uids = g_slist_prepend (junk_uids, (gpointer) camel_pstring_strdup (uid));
+                       g_clear_object (&mi);
+               } else if (is_junk_folder && (flags_set & CAMEL_MESSAGE_NOTJUNK) != 0) {
+                       inbox_uids = g_slist_prepend (inbox_uids, (gpointer) camel_pstring_strdup (uid));
+                       g_clear_object (&mi);
+               } else if ((flags_set & CAMEL_MESSAGE_FOLDER_FLAGGED) != 0) {
+                       /* OK, the change must have been the labels */
+                       mi_list = g_slist_prepend (mi_list, mi);
+                       mi_list_len++;
+               } else {
+                       g_clear_object (&mi);
+               }
+
+               if (mi_list_len == E_M365_BATCH_MAX_REQUESTS) {
+                       success = m365_folder_save_flags_sync (folder, m365_store, mi_list, cancellable, 
&local_error);
+                       g_slist_free_full (mi_list, g_object_unref);
+                       mi_list = NULL;
+                       mi_list_len = 0;
+               }
+       }
+
+       if (mi_list != NULL && success)
+               success = m365_folder_save_flags_sync (folder, m365_store, mi_list, cancellable, 
&local_error);
+       g_slist_free_full (mi_list, g_object_unref);
+
+       if (deleted_uids && success)
+               success = m365_folder_delete_messages_sync (folder, m365_store, deleted_uids, 
m365_folder_is_of_type (folder, CAMEL_FOLDER_TYPE_TRASH), cancellable, &local_error);
+       g_slist_free_full (deleted_uids, (GDestroyNotify) camel_pstring_free);
+
+       if (junk_uids && success)
+               success = m365_folder_copy_move_to_folder_sync (folder, m365_store, junk_uids, "junkemail", 
FALSE, cancellable, &local_error);
+       g_slist_free_full (junk_uids, (GDestroyNotify) camel_pstring_free);
+
+       if (inbox_uids && success)
+               success = m365_folder_copy_move_to_folder_sync (folder, m365_store, inbox_uids, "inbox", 
FALSE, cancellable, &local_error);
+       g_slist_free_full (inbox_uids, (GDestroyNotify) camel_pstring_free);
+
+       camel_folder_summary_save (folder_summary, NULL);
+       camel_folder_summary_free_array (uids);
+
+       if (local_error) {
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       return success;
+}
+
+static gboolean
+m365_folder_expunge_sync (CamelFolder *folder,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       /* it does nothing special here, everything is done as part of the m365_folder_synchronize_sync() */
+
+       return TRUE;
+}
+
+static gboolean
+m365_folder_transfer_messages_to_sync (CamelFolder *source,
+                                      GPtrArray *uids,
+                                      CamelFolder *destination,
+                                      gboolean delete_originals,
+                                      GPtrArray **transferred_uids,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       CamelStore *parent_store;
+       CamelM365Store *m365_store;
+       GSList *uids_list = NULL;
+       gboolean success;
+       guint ii;
+       GError *local_error = NULL;
+
+       /* The parent class ensures this, but recheck anyway, for completeness */
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER (source), FALSE);
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER (destination), FALSE);
+       g_return_val_if_fail (uids != NULL, FALSE);
+
+       parent_store = camel_folder_get_parent_store (source);
+
+       if (!parent_store) {
+               g_set_error_literal (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID_STATE,
+                       _("Invalid folder state (missing parent store)"));
+               return FALSE;
+       }
+
+       /* The parent class ensures this, but recheck anyway, for completeness */
+       g_return_val_if_fail (camel_folder_get_parent_store (destination) == parent_store, FALSE);
+
+       m365_store = CAMEL_M365_STORE (parent_store);
+
+       if (!camel_m365_store_ensure_connected (m365_store, NULL, cancellable, error))
+               return FALSE;
+
+       for (ii = 0; ii < uids->len; ii++) {
+               uids_list = g_slist_prepend (uids_list, g_ptr_array_index (uids, ii));
+       }
+
+       uids_list = g_slist_reverse (uids_list);
+
+       success = m365_folder_copy_move_to_folder_sync (source, m365_store,
+               uids_list, camel_m365_folder_get_id (CAMEL_M365_FOLDER (destination)),
+               !delete_originals, cancellable, &local_error);
+
+       g_slist_free (uids_list);
+
+       /* Update destination folder only if not frozen, to not update
+          for each single message transfer during filtering.
+        */
+       if (success && !camel_folder_is_frozen (destination)) {
+               camel_operation_progress (cancellable, -1);
+
+               m365_folder_refresh_info_sync (destination, cancellable, NULL);
+       }
+
+       if (local_error) {
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       return success;
+}
+
+static void
+m365_folder_prepare_content_refresh (CamelFolder *folder)
+{
+       g_return_if_fail (CAMEL_IS_M365_FOLDER (folder));
+
+       camel_m365_folder_summary_set_delta_link (CAMEL_M365_FOLDER_SUMMARY (camel_folder_get_folder_summary 
(folder)), NULL);
+}
+
+static gchar *
+m365_folder_get_filename (CamelFolder *folder,
+                         const gchar *uid,
+                         GError **error)
+{
+       CamelM365Folder *m365_folder = CAMEL_M365_FOLDER (folder);
+
+       return m365_folder_cache_dup_filename (m365_folder, uid);
+}
+
+static void
+m365_folder_constructed (GObject *object)
+{
+       CamelSettings *settings;
+       CamelStore *parent_store;
+       CamelService *service;
+       CamelFolder *folder;
+       const gchar *full_name;
+       gchar *description;
+       gchar *user;
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_folder_parent_class)->constructed (object);
+
+       folder = CAMEL_FOLDER (object);
+       full_name = camel_folder_get_full_name (folder);
+       parent_store = camel_folder_get_parent_store (folder);
+
+       service = CAMEL_SERVICE (parent_store);
+
+       settings = camel_service_ref_settings (service);
+       user = camel_network_settings_dup_user (CAMEL_NETWORK_SETTINGS (settings));
+
+       g_object_unref (settings);
+
+       description = g_strdup_printf ("%s@Microsoft365:%s", user, full_name);
+       camel_folder_set_description (folder, description);
+       g_free (description);
+
+       g_free (user);
+}
+
+static void
+m365_folder_dispose (GObject *object)
+{
+       CamelM365Folder *m365_folder = CAMEL_M365_FOLDER (object);
+       CamelFolderSummary *summary;
+
+       summary = camel_folder_get_folder_summary (CAMEL_FOLDER (m365_folder));
+
+       if (summary)
+               m365_folder_save_summary (m365_folder);
+
+       LOCK_CACHE (m365_folder);
+       g_clear_object (&m365_folder->priv->cache);
+       UNLOCK_CACHE (m365_folder);
+
+       LOCK_SEARCH (m365_folder);
+       g_clear_object (&m365_folder->priv->search);
+       UNLOCK_SEARCH (m365_folder);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_folder_parent_class)->dispose (object);
+}
+
+static void
+m365_folder_finalize (GObject *object)
+{
+       CamelM365Folder *m365_folder = CAMEL_M365_FOLDER (object);
+
+       g_rec_mutex_clear (&m365_folder->priv->cache_lock);
+       g_mutex_clear (&m365_folder->priv->search_lock);
+       g_mutex_clear (&m365_folder->priv->get_message_lock);
+       g_cond_clear (&m365_folder->priv->get_message_cond);
+
+       g_hash_table_destroy (m365_folder->priv->get_message_hash);
+
+       g_clear_pointer (&m365_folder->priv->id, g_free);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_folder_parent_class)->finalize (object);
+}
+
+static void
+camel_m365_folder_class_init (CamelM365FolderClass *klass)
+{
+       GObjectClass *object_class;
+       CamelFolderClass *folder_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = m365_folder_constructed;
+       object_class->dispose = m365_folder_dispose;
+       object_class->finalize = m365_folder_finalize;
+
+       folder_class = CAMEL_FOLDER_CLASS (klass);
+       folder_class->get_permanent_flags = m365_folder_get_permanent_flags;
+       folder_class->get_message_cached = m365_folder_get_message_cached;
+       folder_class->search_by_expression = m365_folder_search_by_expression;
+       folder_class->count_by_expression = m365_folder_count_by_expression;
+       folder_class->search_by_uids = m365_folder_search_by_uids;
+       folder_class->search_free = m365_folder_search_free;
+       folder_class->cmp_uids = m365_folder_cmp_uids;
+       folder_class->append_message_sync = m365_folder_append_message_sync;
+       folder_class->get_message_sync = m365_folder_get_message_sync;
+       folder_class->refresh_info_sync = m365_folder_refresh_info_sync;
+       folder_class->synchronize_sync = m365_folder_synchronize_sync;
+       folder_class->expunge_sync = m365_folder_expunge_sync;
+       folder_class->transfer_messages_to_sync = m365_folder_transfer_messages_to_sync;
+       folder_class->prepare_content_refresh = m365_folder_prepare_content_refresh;
+       folder_class->get_filename = m365_folder_get_filename;
+}
+
+static void
+camel_m365_folder_init (CamelM365Folder *m365_folder)
+{
+       CamelFolder *folder = CAMEL_FOLDER (m365_folder);
+
+       m365_folder->priv = camel_m365_folder_get_instance_private (m365_folder);
+
+       g_rec_mutex_init (&m365_folder->priv->cache_lock);
+       g_mutex_init (&m365_folder->priv->search_lock);
+       g_mutex_init (&m365_folder->priv->get_message_lock);
+       g_cond_init (&m365_folder->priv->get_message_cond);
+
+       m365_folder->priv->get_message_hash = g_hash_table_new (g_str_hash, g_str_equal);
+
+       camel_folder_set_flags (folder, CAMEL_FOLDER_HAS_SUMMARY_CAPABILITY);
+       camel_folder_set_lock_async (folder, TRUE);
+}
+
+static gboolean
+m365_folder_has_inbox_type (CamelM365Store *m365_store,
+                           const gchar *full_name)
+{
+       CamelM365StoreSummary *summary;
+       guint32 flags;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (m365_store), FALSE);
+       g_return_val_if_fail (full_name != NULL, FALSE);
+
+       summary = camel_m365_store_ref_store_summary (m365_store);
+
+       if (!summary)
+               return FALSE;
+
+       flags = camel_m365_store_summary_get_folder_flags_for_full_name (summary, full_name);
+
+       g_object_unref (summary);
+
+       return (flags & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_INBOX;
+}
+
+CamelFolder *
+camel_m365_folder_new (CamelStore *store,
+                      const gchar *display_name,
+                      const gchar *full_name,
+                      const gchar *folder_dir,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       CamelFolder *folder;
+       CamelFolderSummary *folder_summary;
+       CamelM365Folder *m365_folder;
+       CamelM365StoreSummary *m365_store_summary;
+       CamelSettings *settings;
+       gboolean filter_inbox = FALSE;
+       gboolean filter_junk = FALSE;
+       gboolean filter_junk_inbox = FALSE;
+       gboolean offline_limit_by_age = FALSE;
+       CamelTimeUnit offline_limit_unit;
+       gint offline_limit_value = 0;
+       guint32 add_folder_flags = 0;
+       gchar *state_file;
+       gchar *folder_id;
+
+       m365_store_summary = camel_m365_store_ref_store_summary (CAMEL_M365_STORE (store));
+       folder_id = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store_summary, full_name);
+       g_clear_object (&m365_store_summary);
+
+       if (!folder_id) {
+               g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID_PATH,
+                       _("Folder ā€œ%sā€ doesn't correspond to any known folder"), full_name);
+               return NULL;
+       }
+
+       folder = g_object_new (CAMEL_TYPE_M365_FOLDER,
+               "display_name", display_name,
+               "full-name", full_name,
+               "parent_store", store,
+               NULL);
+
+       m365_folder = CAMEL_M365_FOLDER (folder);
+       m365_folder->priv->id = folder_id;
+
+       folder_summary = camel_m365_folder_summary_new (folder);
+
+       if (!folder_summary) {
+               g_object_unref (folder);
+               g_set_error (
+                       error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
+                       _("Could not load summary for ā€œ%sā€"), full_name);
+               return NULL;
+       }
+
+       settings = camel_service_ref_settings (CAMEL_SERVICE (store));
+
+       g_object_get (
+               settings,
+               "filter-inbox", &filter_inbox,
+               "filter-junk", &filter_junk,
+               "filter-junk-inbox", &filter_junk_inbox,
+               "limit-by-age", &offline_limit_by_age,
+               "limit-unit", &offline_limit_unit,
+               "limit-value", &offline_limit_value,
+               NULL);
+
+       g_clear_object (&settings);
+
+       camel_folder_take_folder_summary (folder, folder_summary);
+
+       /* set/load persistent state */
+       state_file = g_build_filename (folder_dir, "cmeta", NULL);
+       camel_object_set_state_filename (CAMEL_OBJECT (folder), state_file);
+       camel_object_state_read (CAMEL_OBJECT (folder));
+       g_free (state_file);
+
+       m365_folder->priv->cache = camel_data_cache_new (folder_dir, error);
+       if (!m365_folder->priv->cache) {
+               g_object_unref (folder);
+               return NULL;
+       }
+
+       if (camel_offline_folder_can_downsync (CAMEL_OFFLINE_FOLDER (folder))) {
+               time_t when = (time_t) 0;
+
+               if (offline_limit_by_age)
+                       when = camel_time_value_apply (when, offline_limit_unit, offline_limit_value);
+
+               if (when <= (time_t) 0)
+                       when = (time_t) -1;
+
+               /* Ensure cache will expire when set up, otherwise
+                * it causes redownload of messages too soon. */
+               camel_data_cache_set_expire_age (m365_folder->priv->cache, when);
+               camel_data_cache_set_expire_access (m365_folder->priv->cache, when);
+       } else {
+               /* Set cache expiration for one week. */
+               camel_data_cache_set_expire_age (m365_folder->priv->cache, 60 * 60 * 24 * 7);
+               camel_data_cache_set_expire_access (m365_folder->priv->cache, 60 * 60 * 24 * 7);
+       }
+
+       camel_binding_bind_property (store, "online",
+               m365_folder->priv->cache, "expire-enabled",
+               G_BINDING_SYNC_CREATE);
+
+       if (m365_folder_has_inbox_type (CAMEL_M365_STORE (store), full_name)) {
+               if (filter_inbox)
+                       add_folder_flags |= CAMEL_FOLDER_FILTER_RECENT;
+
+               if (filter_junk)
+                       add_folder_flags |= CAMEL_FOLDER_FILTER_JUNK;
+       } else {
+               if (filter_junk && !filter_junk_inbox)
+                       add_folder_flags |= CAMEL_FOLDER_FILTER_JUNK;
+       }
+
+       if (add_folder_flags)
+               camel_folder_set_flags (folder, camel_folder_get_flags (folder) | add_folder_flags);
+
+       camel_m365_store_connect_folder_summary (CAMEL_M365_STORE (store), folder_summary);
+
+       m365_folder->priv->search = camel_folder_search_new ();
+
+       return folder;
+}
+
+const gchar *
+camel_m365_folder_get_id (CamelM365Folder *m365_folder)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_FOLDER (m365_folder), NULL);
+
+       return m365_folder->priv->id;
+}
diff --git a/src/Microsoft365/camel/camel-m365-folder.h b/src/Microsoft365/camel/camel-m365-folder.h
new file mode 100644
index 00000000..c2463839
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-folder.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_FOLDER_H
+#define CAMEL_M365_FOLDER_H
+
+#include <camel/camel.h>
+
+#include "camel-m365-folder-summary.h"
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_FOLDER \
+       (camel_m365_folder_get_type ())
+#define CAMEL_M365_FOLDER(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_FOLDER, CamelM365Folder))
+#define CAMEL_M365_FOLDER_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_FOLDER, CamelM365FolderClass))
+#define CAMEL_IS_M365_FOLDER(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_FOLDER))
+#define CAMEL_IS_M365_FOLDER_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_FOLDER))
+#define CAMEL_M365_FOLDER_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_FOLDER, CamelM365FolderClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelM365Folder CamelM365Folder;
+typedef struct _CamelM365FolderClass CamelM365FolderClass;
+typedef struct _CamelM365FolderPrivate CamelM365FolderPrivate;
+
+struct _CamelM365Folder {
+       CamelOfflineFolder parent;
+       CamelM365FolderPrivate *priv;
+};
+
+struct _CamelM365FolderClass {
+       CamelOfflineFolderClass parent_class;
+};
+
+GType          camel_m365_folder_get_type      (void);
+
+CamelFolder *  camel_m365_folder_new           (CamelStore *store,
+                                                const gchar *display_name,
+                                                const gchar *full_name,
+                                                const gchar *folder_dir,
+                                                GCancellable *cancellable,
+                                                GError **error);
+const gchar *  camel_m365_folder_get_id        (CamelM365Folder *m365_folder);
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_FOLDER_H */
diff --git a/src/Microsoft365/camel/camel-m365-message-info.c 
b/src/Microsoft365/camel/camel-m365-message-info.c
new file mode 100644
index 00000000..453ef492
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-message-info.c
@@ -0,0 +1,409 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <stdio.h>
+
+#include "camel/camel.h"
+
+#include "camel-m365-message-info.h"
+
+struct _CamelM365MessageInfoPrivate {
+       guint32 server_flags;
+       gint32 item_type;
+       gchar *change_key;
+};
+
+enum {
+       PROP_0,
+       PROP_SERVER_FLAGS,
+       PROP_ITEM_TYPE,
+       PROP_CHANGE_KEY,
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelM365MessageInfo, camel_m365_message_info, CAMEL_TYPE_MESSAGE_INFO_BASE)
+
+static CamelMessageInfo *
+m365_message_info_clone (const CamelMessageInfo *mi,
+                        CamelFolderSummary *assign_summary)
+{
+       CamelMessageInfo *result;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (mi), NULL);
+
+       result = CAMEL_MESSAGE_INFO_CLASS (camel_m365_message_info_parent_class)->clone (mi, assign_summary);
+       if (!result)
+               return NULL;
+
+       if (CAMEL_IS_M365_MESSAGE_INFO (result)) {
+               CamelM365MessageInfo *omi, *omi_result;
+
+               omi = CAMEL_M365_MESSAGE_INFO (mi);
+               omi_result = CAMEL_M365_MESSAGE_INFO (result);
+
+               camel_m365_message_info_set_server_flags (omi_result, 
camel_m365_message_info_get_server_flags (omi));
+               camel_m365_message_info_set_item_type (omi_result, camel_m365_message_info_get_item_type 
(omi));
+               camel_m365_message_info_take_change_key (omi_result, camel_m365_message_info_dup_change_key 
(omi));
+       }
+
+       return result;
+}
+
+static gboolean
+m365_message_info_load (CamelMessageInfo *mi,
+                       const CamelMIRecord *record,
+                       /* const */ gchar **bdata_ptr)
+{
+       CamelM365MessageInfo *omi;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (mi), FALSE);
+       g_return_val_if_fail (record != NULL, FALSE);
+       g_return_val_if_fail (bdata_ptr != NULL, FALSE);
+
+       if (!CAMEL_MESSAGE_INFO_CLASS (camel_m365_message_info_parent_class)->load ||
+           !CAMEL_MESSAGE_INFO_CLASS (camel_m365_message_info_parent_class)->load (mi, record, bdata_ptr))
+               return FALSE;
+
+       omi = CAMEL_M365_MESSAGE_INFO (mi);
+
+       if (*bdata_ptr) {
+               gchar **values;
+
+               values = g_strsplit (*bdata_ptr, " ", -1);
+
+               if (values && values[0] && values[1] && values[2]) {
+                       camel_m365_message_info_set_server_flags (omi, g_ascii_strtoll (values[0], NULL, 10));
+                       camel_m365_message_info_set_item_type (omi, g_ascii_strtoll (values[1], NULL, 10));
+                       camel_m365_message_info_set_change_key (omi, values[2]);
+               }
+
+               g_strfreev (values);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+m365_message_info_save (const CamelMessageInfo *mi,
+                       CamelMIRecord *record,
+                       GString *bdata_str)
+{
+       CamelM365MessageInfo *omi;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (mi), FALSE);
+       g_return_val_if_fail (record != NULL, FALSE);
+       g_return_val_if_fail (bdata_str != NULL, FALSE);
+
+       if (!CAMEL_MESSAGE_INFO_CLASS (camel_m365_message_info_parent_class)->save ||
+           !CAMEL_MESSAGE_INFO_CLASS (camel_m365_message_info_parent_class)->save (mi, record, bdata_str))
+               return FALSE;
+
+       omi = CAMEL_M365_MESSAGE_INFO (mi);
+
+       g_string_append_printf (bdata_str, "%u %d %s",
+               camel_m365_message_info_get_server_flags (omi),
+               camel_m365_message_info_get_item_type (omi),
+               camel_m365_message_info_get_change_key (omi));
+
+       return TRUE;
+}
+
+static void
+m365_message_info_set_property (GObject *object,
+                               guint property_id,
+                               const GValue *value,
+                               GParamSpec *pspec)
+{
+       CamelM365MessageInfo *omi = CAMEL_M365_MESSAGE_INFO (object);
+
+       switch (property_id) {
+       case PROP_SERVER_FLAGS:
+               camel_m365_message_info_set_server_flags (omi, g_value_get_uint (value));
+               return;
+
+       case PROP_ITEM_TYPE:
+               camel_m365_message_info_set_item_type (omi, g_value_get_int (value));
+               return;
+
+       case PROP_CHANGE_KEY:
+               camel_m365_message_info_set_change_key (omi, g_value_get_string (value));
+               return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_message_info_get_property (GObject *object,
+                               guint property_id,
+                               GValue *value,
+                               GParamSpec *pspec)
+{
+       CamelM365MessageInfo *omi = CAMEL_M365_MESSAGE_INFO (object);
+
+       switch (property_id) {
+
+       case PROP_SERVER_FLAGS:
+               g_value_set_uint (value, camel_m365_message_info_get_server_flags (omi));
+               return;
+
+       case PROP_ITEM_TYPE:
+               g_value_set_int (value, camel_m365_message_info_get_item_type (omi));
+               return;
+
+       case PROP_CHANGE_KEY:
+               g_value_take_string (value, camel_m365_message_info_dup_change_key (omi));
+               return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_message_info_dispose (GObject *object)
+{
+       CamelM365MessageInfo *omi = CAMEL_M365_MESSAGE_INFO (object);
+
+       g_free (omi->priv->change_key);
+       omi->priv->change_key = NULL;
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_message_info_parent_class)->dispose (object);
+}
+
+static void
+camel_m365_message_info_class_init (CamelM365MessageInfoClass *class)
+{
+       CamelMessageInfoClass *mi_class;
+       GObjectClass *object_class;
+
+       mi_class = CAMEL_MESSAGE_INFO_CLASS (class);
+       mi_class->clone = m365_message_info_clone;
+       mi_class->load = m365_message_info_load;
+       mi_class->save = m365_message_info_save;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = m365_message_info_set_property;
+       object_class->get_property = m365_message_info_get_property;
+       object_class->dispose = m365_message_info_dispose;
+
+       /*
+        * CamelM365MessageInfo:server-flags
+        *
+        * Last known server flags of the message.
+        */
+       g_object_class_install_property (
+               object_class,
+               PROP_SERVER_FLAGS,
+               g_param_spec_uint (
+                       "server-flags",
+                       "Server Flags",
+                       NULL,
+                       0, G_MAXUINT32, 0,
+                       G_PARAM_READWRITE));
+
+       /*
+        * CamelM365MessageInfo:item-type
+        *
+        * Item type of the message.
+        */
+       g_object_class_install_property (
+               object_class,
+               PROP_ITEM_TYPE,
+               g_param_spec_int (
+                       "item-type",
+                       "Item Type",
+                       NULL,
+                       0, G_MAXINT32, 0,
+                       G_PARAM_READWRITE));
+
+       /*
+        * CamelM365MessageInfo:change-key
+        *
+        * Change key of the message on the server.
+        */
+       g_object_class_install_property (
+               object_class,
+               PROP_CHANGE_KEY,
+               g_param_spec_string (
+                       "change-key",
+                       "Change Key",
+                       NULL,
+                       NULL,
+                       G_PARAM_READWRITE));
+}
+
+static void
+camel_m365_message_info_init (CamelM365MessageInfo *omi)
+{
+       omi->priv = camel_m365_message_info_get_instance_private (omi);
+}
+
+guint32
+camel_m365_message_info_get_server_flags (const CamelM365MessageInfo *omi)
+{
+       CamelMessageInfo *mi;
+       guint32 result;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), 0);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+       result = omi->priv->server_flags;
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+gboolean
+camel_m365_message_info_set_server_flags (CamelM365MessageInfo *omi,
+                                         guint32 server_flags)
+{
+       CamelMessageInfo *mi;
+       gboolean changed;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), FALSE);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+
+       changed = omi->priv->server_flags != server_flags;
+
+       if (changed)
+               omi->priv->server_flags = server_flags;
+
+       camel_message_info_property_unlock (mi);
+
+       if (changed && !camel_message_info_get_abort_notifications (mi)) {
+               g_object_notify (G_OBJECT (omi), "server-flags");
+               camel_message_info_set_dirty (mi, TRUE);
+       }
+
+       return changed;
+}
+
+gint32
+camel_m365_message_info_get_item_type (const CamelM365MessageInfo *omi)
+{
+       CamelMessageInfo *mi;
+       gint32 result;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), 0);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+       result = omi->priv->item_type;
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+gboolean
+camel_m365_message_info_set_item_type (CamelM365MessageInfo *omi,
+                                      gint32 item_type)
+{
+       CamelMessageInfo *mi;
+       gboolean changed;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), FALSE);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+
+       changed = omi->priv->item_type != item_type;
+
+       if (changed)
+               omi->priv->item_type = item_type;
+
+       camel_message_info_property_unlock (mi);
+
+       if (changed && !camel_message_info_get_abort_notifications (mi)) {
+               g_object_notify (G_OBJECT (omi), "item-type");
+               camel_message_info_set_dirty (mi, TRUE);
+       }
+
+       return changed;
+}
+
+const gchar *
+camel_m365_message_info_get_change_key (const CamelM365MessageInfo *omi)
+{
+       CamelMessageInfo *mi;
+       const gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), NULL);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+       result = omi->priv->change_key;
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+gchar *
+camel_m365_message_info_dup_change_key (const CamelM365MessageInfo *omi)
+{
+       CamelMessageInfo *mi;
+       gchar *result;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), NULL);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+       result = g_strdup (omi->priv->change_key);
+       camel_message_info_property_unlock (mi);
+
+       return result;
+}
+
+gboolean
+camel_m365_message_info_set_change_key (CamelM365MessageInfo *omi,
+                                       const gchar *change_key)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), FALSE);
+
+       return camel_m365_message_info_take_change_key (omi, g_strdup (change_key));
+}
+
+gboolean
+camel_m365_message_info_take_change_key (CamelM365MessageInfo *omi,
+                                        gchar *change_key)
+{
+       CamelMessageInfo *mi;
+       gboolean changed;
+
+       g_return_val_if_fail (CAMEL_IS_M365_MESSAGE_INFO (omi), FALSE);
+
+       mi = CAMEL_MESSAGE_INFO (omi);
+
+       camel_message_info_property_lock (mi);
+
+       changed = g_strcmp0 (omi->priv->change_key, change_key) != 0;
+
+       if (changed) {
+               g_free (omi->priv->change_key);
+               omi->priv->change_key = change_key;
+       } else if (change_key != omi->priv->change_key) {
+               g_free (change_key);
+       }
+
+       camel_message_info_property_unlock (mi);
+
+       if (changed && !camel_message_info_get_abort_notifications (mi)) {
+               g_object_notify (G_OBJECT (omi), "change-key");
+               camel_message_info_set_dirty (mi, TRUE);
+       }
+
+       return changed;
+}
diff --git a/src/Microsoft365/camel/camel-m365-message-info.h 
b/src/Microsoft365/camel/camel-m365-message-info.h
new file mode 100644
index 00000000..a0c65dc1
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-message-info.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_MESSAGE_INFO_H
+#define CAMEL_M365_MESSAGE_INFO_H
+
+#include <glib-object.h>
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_MESSAGE_INFO \
+       (camel_m365_message_info_get_type ())
+#define CAMEL_M365_MESSAGE_INFO(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_MESSAGE_INFO, CamelM365MessageInfo))
+#define CAMEL_M365_MESSAGE_INFO_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_MESSAGE_INFO, CamelM365MessageInfoClass))
+#define CAMEL_IS_M365_MESSAGE_INFO(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_MESSAGE_INFO))
+#define CAMEL_IS_M365_MESSAGE_INFO_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_MESSAGE_INFO))
+#define CAMEL_M365_MESSAGE_INFO_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_MESSAGE_INFO, CamelM365MessageInfoClass))
+
+G_BEGIN_DECLS
+
+/* extra summary flags*/
+enum {
+       CAMEL_M365_MESSAGE_MSGFLAG_RN_PENDING = CAMEL_MESSAGE_FOLDER_FLAGGED << 1
+};
+
+typedef struct _CamelM365MessageInfo CamelM365MessageInfo;
+typedef struct _CamelM365MessageInfoClass CamelM365MessageInfoClass;
+typedef struct _CamelM365MessageInfoPrivate CamelM365MessageInfoPrivate;
+
+struct _CamelM365MessageInfo {
+       CamelMessageInfoBase parent;
+       CamelM365MessageInfoPrivate *priv;
+};
+
+struct _CamelM365MessageInfoClass {
+       CamelMessageInfoBaseClass parent_class;
+};
+
+GType          camel_m365_message_info_get_type        (void);
+
+guint32                camel_m365_message_info_get_server_flags(const CamelM365MessageInfo *omi);
+gboolean       camel_m365_message_info_set_server_flags(CamelM365MessageInfo *omi,
+                                                        guint32 server_flags);
+gint32         camel_m365_message_info_get_item_type   (const CamelM365MessageInfo *omi);
+gboolean       camel_m365_message_info_set_item_type   (CamelM365MessageInfo *omi,
+                                                        gint32 item_type);
+const gchar *  camel_m365_message_info_get_change_key  (const CamelM365MessageInfo *omi);
+gchar *                camel_m365_message_info_dup_change_key  (const CamelM365MessageInfo *omi);
+gboolean       camel_m365_message_info_set_change_key  (CamelM365MessageInfo *omi,
+                                                        const gchar *change_key);
+gboolean       camel_m365_message_info_take_change_key (CamelM365MessageInfo *omi,
+                                                        gchar *change_key);
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_MESSAGE_INFO_H */
diff --git a/src/Microsoft365/camel/camel-m365-provider.c b/src/Microsoft365/camel/camel-m365-provider.c
new file mode 100644
index 00000000..200ffeaf
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-provider.c
@@ -0,0 +1,122 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+
+#include <glib/gi18n-lib.h>
+#include <gmodule.h>
+
+#include "common/camel-sasl-xoauth2-microsoft365.h"
+
+#include "camel-m365-store.h"
+#include "camel-m365-transport.h"
+
+static void add_hash (guint *hash, gchar *s);
+static guint m365_url_hash (gconstpointer key);
+static gint m365_url_equal (gconstpointer a, gconstpointer b);
+
+static CamelProviderConfEntry m365_conf_entries[] = {
+       { CAMEL_PROVIDER_CONF_SECTION_START, "mailcheck", NULL,
+         N_("Checking for new mail") },
+       { CAMEL_PROVIDER_CONF_CHECKBOX, "check-all", NULL,
+         N_("C_heck for new messages in all folders"), "1" },
+       { CAMEL_PROVIDER_CONF_SECTION_END },
+
+       { CAMEL_PROVIDER_CONF_SECTION_START, "general", NULL, N_("Options") },
+       { CAMEL_PROVIDER_CONF_CHECKBOX, "filter-inbox", NULL,
+         N_("_Apply filters to new messages in Inbox on this server"), "0" },
+       { CAMEL_PROVIDER_CONF_CHECKBOX, "filter-junk", NULL,
+         N_("Check new messages for _Junk contents"), "0" },
+       { CAMEL_PROVIDER_CONF_CHECKBOX, "filter-junk-inbox", "filter-junk",
+         N_("Only check for Junk messages in the IN_BOX folder"), "0" },
+       { CAMEL_PROVIDER_CONF_CHECKBOX, "stay-synchronized", NULL,
+         N_("Synchroni_ze remote mail locally in all folders"), "0" },
+       { CAMEL_PROVIDER_CONF_PLACEHOLDER, "m365-limit-by-age-placeholder", NULL },
+       { CAMEL_PROVIDER_CONF_SECTION_END },
+
+       { CAMEL_PROVIDER_CONF_SECTION_START, "connection", NULL, N_("Connection") },
+       { CAMEL_PROVIDER_CONF_CHECKSPIN, "timeout", NULL,
+         /* Translators: '%s' is preplaced with a widget, where "
+          * user can select how long the timeout should be. */
+         N_("Connection _timeout (in seconds) %s"), "0:1:0:32768" },
+       { CAMEL_PROVIDER_CONF_CHECKSPIN, "concurrent-connections", NULL,
+         N_("Numbe_r of concurrent connections to use"), "y:1:1:7" },
+       { CAMEL_PROVIDER_CONF_SECTION_END },
+
+       { CAMEL_PROVIDER_CONF_END }
+};
+
+static CamelProvider m365_provider = {
+       "microsoft365",
+       N_("Microsoft 365"),
+
+       N_("For accessing Microsoft 365 server"),
+
+       "mail",
+
+       CAMEL_PROVIDER_IS_REMOTE | CAMEL_PROVIDER_IS_SOURCE |
+       CAMEL_PROVIDER_IS_STORAGE | CAMEL_PROVIDER_IS_EXTERNAL,
+
+       CAMEL_URL_ALLOW_USER | CAMEL_URL_ALLOW_AUTH | CAMEL_URL_HIDDEN_HOST,
+
+       m365_conf_entries,
+
+       /* ... */
+};
+
+void
+camel_provider_module_init (void)
+{
+       bindtextdomain (GETTEXT_PACKAGE, M365_LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+       m365_provider.url_hash = m365_url_hash;
+       m365_provider.url_equal = m365_url_equal;
+       m365_provider.authtypes = NULL;
+       m365_provider.translation_domain = GETTEXT_PACKAGE;
+       m365_provider.object_types[CAMEL_PROVIDER_STORE] =  CAMEL_TYPE_M365_STORE;
+       m365_provider.object_types[CAMEL_PROVIDER_TRANSPORT] = CAMEL_TYPE_M365_TRANSPORT;
+
+       g_type_ensure (CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365);
+
+       if (g_strcmp0 (g_getenv ("ENABLE_M365"), "1") == 0)
+               camel_provider_register (&m365_provider);
+}
+
+static void
+add_hash (guint *hash,
+          gchar *s)
+{
+       if (s)
+               *hash ^= g_str_hash(s);
+}
+
+static guint
+m365_url_hash (gconstpointer key)
+{
+       const CamelURL *u = (CamelURL *) key;
+       guint hash = 0;
+
+       add_hash (&hash, u->user);
+       add_hash (&hash, u->host);
+       hash ^= u->port;
+
+       return hash;
+}
+
+static gint
+m365_url_equal (gconstpointer a,
+               gconstpointer b)
+{
+       const CamelURL *u1 = a, *u2 = b;
+
+       return ((g_strcmp0 (u1->protocol, u2->protocol) == 0)
+               && (g_strcmp0 (u1->user, u2->user) == 0)
+               && (g_strcmp0 (u1->host, u2->host) == 0)
+               && (u1->port == u2->port));
+}
diff --git a/src/Microsoft365/camel/camel-m365-store-summary.c 
b/src/Microsoft365/camel/camel-m365-store-summary.c
new file mode 100644
index 00000000..7b340052
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-store-summary.c
@@ -0,0 +1,1439 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+
+#include "camel-m365-store-summary.h"
+
+#define STORE_GROUP_NAME "##storepriv##"
+#define CATEGORIES_KEY "Categories"
+#define DATA_VERSION 1
+
+#define LOCK(_summary) g_rec_mutex_lock (&(_summary->priv->property_lock))
+#define UNLOCK(_summary) g_rec_mutex_unlock (&(_summary->priv->property_lock))
+
+struct _CamelM365StoreSummaryPrivate {
+       GRecMutex property_lock;
+       gchar *filename;
+       GKeyFile *key_file;
+       GFileMonitor *monitor_delete;
+       gboolean dirty;
+
+       /* Note: We use the *same* strings in both of these hash tables, and
+        * only id_full_name_hash has g_free() hooked up as the destructor func.
+        * So entries must always be removed from full_name_id_hash *first*. */
+       GHashTable *id_full_name_hash; /* id ~> folder full name */
+       GHashTable *full_name_id_hash; /* folder full name ~> id */
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelM365StoreSummary, camel_m365_store_summary, G_TYPE_OBJECT)
+
+static gchar *
+m365_store_summary_encode_folder_name (const gchar *display_name)
+{
+       GString *encoded;
+       const gchar *pos;
+
+       if (!display_name || !*display_name)
+               return NULL;
+
+       encoded = g_string_sized_new (strlen (display_name) + 4);
+
+       for (pos = display_name; *pos; pos++) {
+               if (strchr ("%?/", *pos))
+                       g_string_append_printf (encoded, "%%%02x", *pos);
+               else
+                       g_string_append_c (encoded, *pos);
+       }
+
+       return g_string_free (encoded, FALSE);
+}
+
+#if 0
+static gchar *
+m365_store_summary_decode_folder_name (gchar *pathpart)
+{
+       gchar *pos, *write_pos;
+
+       if (!pathpart || !*pathpart)
+               return pathpart;
+
+       pos = pathpart;
+       write_pos = pathpart;
+
+       while (*pos) {
+               if (*pos == '%' &&
+                   g_ascii_isxdigit (pos[1]) &&
+                   g_ascii_isxdigit (pos[2])) {
+                       *write_pos = (g_ascii_xdigit_value (pos[1]) << 4) + g_ascii_xdigit_value (pos[2]);
+
+                       pos += 2;
+               } else if (write_pos != pos) {
+                       *write_pos = *pos;
+               }
+
+               pos++;
+               write_pos++;
+       }
+
+       if (write_pos != pos)
+               *write_pos = 0;
+
+       return pathpart;
+}
+#endif
+
+static void
+camel_m365_store_summary_migrate_data_locked (CamelM365StoreSummary *store_summary,
+                                             gint from_version)
+{
+       /* Here will be any future migration of old data in the summary to new data. */
+
+       g_key_file_set_integer (store_summary->priv->key_file, STORE_GROUP_NAME, "Version", DATA_VERSION);
+}
+
+static void
+m365_store_summary_delete_cb (GFileMonitor *monitor,
+                             GFile *file,
+                             GFile *other_file,
+                             GFileMonitorEvent event,
+                             gpointer user_data)
+{
+       CamelM365StoreSummary *store_summary = user_data;
+
+       if (event == G_FILE_MONITOR_EVENT_DELETED) {
+               LOCK (store_summary);
+
+               if (store_summary->priv->key_file)
+                       camel_m365_store_summary_clear (store_summary);
+
+               UNLOCK (store_summary);
+       }
+}
+
+static void
+m365_store_summary_dispose (GObject *object)
+{
+       CamelM365StoreSummary *store_summary = CAMEL_M365_STORE_SUMMARY (object);
+
+       LOCK (store_summary);
+
+       if (store_summary->priv->monitor_delete) {
+               g_signal_handlers_disconnect_by_func (store_summary->priv->monitor_delete,
+                       G_CALLBACK (m365_store_summary_delete_cb), store_summary);
+
+               g_clear_object (&store_summary->priv->monitor_delete);
+       }
+
+       UNLOCK (store_summary);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_store_summary_parent_class)->dispose (object);
+}
+
+static void
+m365_store_summary_finalize (GObject *object)
+{
+       CamelM365StoreSummary *store_summary = CAMEL_M365_STORE_SUMMARY (object);
+
+       g_rec_mutex_clear (&store_summary->priv->property_lock);
+       g_hash_table_destroy (store_summary->priv->full_name_id_hash);
+       g_hash_table_destroy (store_summary->priv->id_full_name_hash);
+       g_key_file_free (store_summary->priv->key_file);
+       g_free (store_summary->priv->filename);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_store_summary_parent_class)->finalize (object);
+}
+
+static void
+camel_m365_store_summary_class_init (CamelM365StoreSummaryClass *class)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->dispose = m365_store_summary_dispose;
+       object_class->finalize = m365_store_summary_finalize;
+}
+
+static void
+camel_m365_store_summary_init (CamelM365StoreSummary *store_summary)
+{
+       store_summary->priv = camel_m365_store_summary_get_instance_private (store_summary);
+       store_summary->priv->key_file = g_key_file_new ();
+       store_summary->priv->id_full_name_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
g_free);
+       store_summary->priv->full_name_id_hash = g_hash_table_new (g_str_hash, g_str_equal); /* shared data 
with 'id_full_name_hash' */
+
+       g_rec_mutex_init (&store_summary->priv->property_lock);
+}
+
+CamelM365StoreSummary *
+camel_m365_store_summary_new (const gchar *filename)
+{
+       CamelM365StoreSummary *store_summary;
+       GError *error = NULL;
+       GFile *file;
+
+       g_return_val_if_fail (filename != NULL, NULL);
+
+       file = g_file_new_for_path (filename);
+
+       store_summary = g_object_new (CAMEL_TYPE_M365_STORE_SUMMARY, NULL);
+       store_summary->priv->filename = g_strdup (filename);
+       store_summary->priv->monitor_delete = g_file_monitor_file (file, G_FILE_MONITOR_SEND_MOVED, NULL, 
&error);
+
+       if (!error) {
+               g_signal_connect (
+                       store_summary->priv->monitor_delete, "changed",
+                       G_CALLBACK (m365_store_summary_delete_cb), store_summary);
+       } else {
+               g_warning ("%s: Failed to create monitor_delete: %s", G_STRFUNC, error->message);
+               g_clear_error (&error);
+       }
+
+       g_object_unref (file);
+
+       return store_summary;
+}
+
+gboolean
+camel_m365_store_summary_load (CamelM365StoreSummary *store_summary,
+                              GError **error)
+{
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), FALSE);
+
+       LOCK (store_summary);
+
+       g_hash_table_remove_all (store_summary->priv->full_name_id_hash);
+       g_hash_table_remove_all (store_summary->priv->id_full_name_hash);
+
+       store_summary->priv->dirty = FALSE;
+
+       success = g_key_file_load_from_file (store_summary->priv->key_file, store_summary->priv->filename, 
G_KEY_FILE_NONE, &local_error);
+
+       if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+               g_key_file_set_integer (store_summary->priv->key_file, STORE_GROUP_NAME, "Version", 
DATA_VERSION);
+
+               g_clear_error (&local_error);
+               success = TRUE;
+       } else if (local_error) {
+               g_propagate_error (error, local_error);
+       } else {
+               gint version;
+
+               version = g_key_file_get_integer (store_summary->priv->key_file, STORE_GROUP_NAME, "Version", 
NULL);
+
+               if (version && version < DATA_VERSION)
+                       camel_m365_store_summary_migrate_data_locked (store_summary, version);
+
+               camel_m365_store_summary_rebuild_hashes (store_summary);
+       }
+
+       UNLOCK (store_summary);
+
+       return success;
+}
+
+gboolean
+camel_m365_store_summary_save (CamelM365StoreSummary *store_summary,
+                              GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), FALSE);
+
+       LOCK (store_summary);
+
+       if (store_summary->priv->dirty) {
+               success = g_key_file_save_to_file (store_summary->priv->key_file, 
store_summary->priv->filename, error);
+
+               if (success)
+                       store_summary->priv->dirty = FALSE;
+       }
+
+       UNLOCK (store_summary);
+
+       return success;
+}
+
+void
+camel_m365_store_summary_clear (CamelM365StoreSummary *store_summary)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+
+       LOCK (store_summary);
+
+       store_summary->priv->dirty = g_hash_table_size (store_summary->priv->id_full_name_hash) > 0;
+
+       g_key_file_free (store_summary->priv->key_file);
+       store_summary->priv->key_file = g_key_file_new ();
+
+       g_hash_table_remove_all (store_summary->priv->full_name_id_hash);
+       g_hash_table_remove_all (store_summary->priv->id_full_name_hash);
+
+       UNLOCK (store_summary);
+}
+
+void
+camel_m365_store_summary_lock (CamelM365StoreSummary *store_summary)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+
+       LOCK (store_summary);
+}
+
+void
+camel_m365_store_summary_unlock (CamelM365StoreSummary *store_summary)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+
+       UNLOCK (store_summary);
+}
+
+static void
+m365_store_summary_build_full_name (const gchar *id,
+                                   GHashTable *id_folder_name,
+                                   GHashTable *id_parent_id,
+                                   GHashTable *covered,
+                                   GString *inout_full_name)
+{
+       const gchar *parent_id;
+
+       g_return_if_fail (id != NULL);
+
+       if (g_hash_table_contains (covered, id))
+               return;
+
+       g_hash_table_insert (covered, (gpointer) id, NULL);
+
+       parent_id = g_hash_table_lookup (id_parent_id, id);
+
+       if (parent_id && *parent_id && g_hash_table_contains (id_folder_name, parent_id))
+               m365_store_summary_build_full_name (parent_id, id_folder_name, id_parent_id, covered, 
inout_full_name);
+
+       if (inout_full_name->len)
+               g_string_append_c (inout_full_name, '/');
+
+       g_string_append (inout_full_name, g_hash_table_lookup (id_folder_name, id));
+}
+
+void
+camel_m365_store_summary_rebuild_hashes (CamelM365StoreSummary *store_summary)
+{
+       GHashTable *id_folder_name;
+       GHashTable *id_parent_id;
+       gchar **groups;
+       gint ii;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+
+       LOCK (store_summary);
+
+       g_hash_table_remove_all (store_summary->priv->full_name_id_hash);
+       g_hash_table_remove_all (store_summary->priv->id_full_name_hash);
+
+       id_folder_name = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+       id_parent_id = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+
+       groups = g_key_file_get_groups (store_summary->priv->key_file, NULL);
+
+       for (ii = 0; groups[ii]; ii++) {
+               const gchar *group = groups[ii];
+
+               if (g_ascii_strcasecmp (group, STORE_GROUP_NAME) != 0 &&
+                   g_key_file_has_key (store_summary->priv->key_file, group, "DisplayName", NULL)) {
+                       gchar *display_name, *folder_name;
+
+                       display_name = g_key_file_get_string (store_summary->priv->key_file, group, 
"DisplayName", NULL);
+                       folder_name = m365_store_summary_encode_folder_name (display_name);
+
+                       g_hash_table_insert (id_folder_name, (gpointer) group, folder_name);
+                       g_hash_table_insert (id_parent_id, (gpointer) group,
+                               camel_m365_store_summary_dup_folder_parent_id (store_summary, group));
+
+                       g_free (display_name);
+               }
+       }
+
+       if (g_hash_table_size (id_folder_name)) {
+               GHashTable *covered;
+               GHashTableIter iter;
+               gpointer key;
+
+               covered = g_hash_table_new (g_str_hash, g_str_equal);
+
+               g_hash_table_iter_init (&iter, id_folder_name);
+
+               while (g_hash_table_iter_next (&iter, &key, NULL)) {
+                       const gchar *id = key;
+                       GString *full_name_str;
+
+                       g_hash_table_remove_all (covered);
+
+                       full_name_str = g_string_sized_new (16);
+
+                       m365_store_summary_build_full_name (id, id_folder_name, id_parent_id, covered, 
full_name_str);
+
+                       if (full_name_str->len) {
+                               gchar *id_dup = g_strdup (id);
+                               gchar *full_name = g_string_free (full_name_str, FALSE);
+
+                               g_hash_table_insert (store_summary->priv->id_full_name_hash, id_dup, 
full_name);
+                               g_hash_table_insert (store_summary->priv->full_name_id_hash, full_name, 
id_dup);
+                       } else {
+                               g_string_free (full_name_str, TRUE);
+                       }
+               }
+
+               g_hash_table_destroy (covered);
+       }
+
+       g_hash_table_destroy (id_folder_name);
+       g_hash_table_destroy (id_parent_id);
+       g_strfreev (groups);
+
+       UNLOCK (store_summary);
+}
+
+void
+camel_m365_store_summary_set_delta_link (CamelM365StoreSummary *store_summary,
+                                        const gchar *delta_link)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+
+       LOCK (store_summary);
+
+       g_key_file_set_string (store_summary->priv->key_file, STORE_GROUP_NAME, "DeltaLink", delta_link ? 
delta_link : "");
+
+       store_summary->priv->dirty = TRUE;
+
+       UNLOCK (store_summary);
+}
+
+gchar *
+camel_m365_store_summary_dup_delta_link (CamelM365StoreSummary *store_summary)
+{
+       gchar *value;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+
+       LOCK (store_summary);
+
+       value = g_key_file_get_string (store_summary->priv->key_file, STORE_GROUP_NAME, "DeltaLink", NULL);
+
+       UNLOCK (store_summary);
+
+       if (value && !*value) {
+               g_clear_pointer (&value, g_free);
+       }
+
+       return value;
+}
+
+gboolean
+camel_m365_store_summary_has_folder (CamelM365StoreSummary *store_summary,
+                                    const gchar *id)
+{
+       gboolean has;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), FALSE);
+       g_return_val_if_fail (id != NULL, FALSE);
+
+       LOCK (store_summary);
+
+       has = g_hash_table_contains (store_summary->priv->id_full_name_hash, id);
+
+       UNLOCK (store_summary);
+
+       return has;
+}
+
+gboolean
+camel_m365_store_summary_has_full_name (CamelM365StoreSummary *store_summary,
+                                       const gchar *full_name)
+{
+       gboolean has;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), FALSE);
+       g_return_val_if_fail (full_name != NULL, FALSE);
+
+       LOCK (store_summary);
+
+       has = g_hash_table_contains (store_summary->priv->full_name_id_hash, full_name);
+
+       UNLOCK (store_summary);
+
+       return has;
+}
+
+void
+camel_m365_store_summary_remove_folder (CamelM365StoreSummary *store_summary,
+                                       const gchar *id)
+{
+       const gchar *full_name;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+
+       LOCK (store_summary);
+
+       full_name = g_hash_table_lookup (store_summary->priv->id_full_name_hash, id);
+
+       if (full_name) {
+               g_hash_table_remove (store_summary->priv->full_name_id_hash, full_name);
+               g_hash_table_remove (store_summary->priv->id_full_name_hash, id);
+
+               store_summary->priv->dirty = store_summary->priv->dirty ||
+                       g_key_file_has_group (store_summary->priv->key_file, id);
+
+               g_key_file_remove_group (store_summary->priv->key_file, id, NULL);
+       }
+
+       UNLOCK (store_summary);
+}
+
+void
+camel_m365_store_summary_set_folder (CamelM365StoreSummary *store_summary,
+                                    gboolean with_hashes_update,
+                                    const gchar *id,
+                                    const gchar *parent_id,
+                                    const gchar *display_name,
+                                    gint32 total_count,
+                                    gint32 unread_count,
+                                    guint32 flags,
+                                    EM365FolderKind kind,
+                                    gboolean is_foreign,
+                                    gboolean is_public)
+{
+       gboolean changed = FALSE;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+       g_return_if_fail (display_name != NULL);
+
+       LOCK (store_summary);
+
+       camel_m365_store_summary_update_folder (store_summary, with_hashes_update, id, parent_id, 
display_name, total_count, unread_count, -1);
+
+       camel_m365_store_summary_set_folder_parent_id (store_summary, id, parent_id);
+       camel_m365_store_summary_set_folder_total_count (store_summary, id, total_count);
+       camel_m365_store_summary_set_folder_unread_count (store_summary, id, unread_count);
+       camel_m365_store_summary_set_folder_flags (store_summary, id, flags);
+
+       if (g_key_file_get_integer (store_summary->priv->key_file, id, "Kind", NULL) != kind) {
+               g_key_file_set_integer (store_summary->priv->key_file, id, "Kind", kind);
+               changed = TRUE;
+       }
+
+       if (g_key_file_get_boolean (store_summary->priv->key_file, id, "IsForeign", NULL) != is_foreign) {
+               g_key_file_set_boolean (store_summary->priv->key_file, id, "IsForeign", is_foreign);
+               changed = TRUE;
+       }
+
+       if (g_key_file_get_boolean (store_summary->priv->key_file, id, "IsPublic", NULL) != is_public) {
+               g_key_file_set_boolean (store_summary->priv->key_file, id, "IsPublic", is_public);
+               changed = TRUE;
+       }
+
+       /* Set display name as the last, because it updates internal hashes and depends on the stored data */
+       camel_m365_store_summary_set_folder_display_name (store_summary, id, display_name, 
with_hashes_update);
+
+       if (changed)
+               store_summary->priv->dirty = TRUE;
+
+       UNLOCK (store_summary);
+}
+
+void
+camel_m365_store_summary_update_folder (CamelM365StoreSummary *store_summary,
+                                       gboolean with_hashes_update,
+                                       const gchar *id,
+                                       const gchar *parent_id,
+                                       const gchar *display_name,
+                                       gint32 total_count,
+                                       gint32 unread_count,
+                                       gint32 children_count)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+       g_return_if_fail (display_name != NULL);
+
+       LOCK (store_summary);
+
+       camel_m365_store_summary_set_folder_parent_id (store_summary, id, parent_id);
+       camel_m365_store_summary_set_folder_total_count (store_summary, id, total_count);
+       camel_m365_store_summary_set_folder_unread_count (store_summary, id, unread_count);
+
+       if (children_count != -1) {
+               guint32 flags = camel_m365_store_summary_get_folder_flags (store_summary, id);
+
+               flags = (flags & (~(CAMEL_FOLDER_CHILDREN | CAMEL_FOLDER_NOCHILDREN))) |
+                       (children_count ? CAMEL_FOLDER_CHILDREN : CAMEL_FOLDER_NOCHILDREN);
+
+               camel_m365_store_summary_set_folder_flags (store_summary, id, flags);
+       }
+
+       /* Set display name as the last, because it updates internal hashes and depends on the stored data */
+       camel_m365_store_summary_set_folder_display_name (store_summary, id, display_name, 
with_hashes_update);
+
+       UNLOCK (store_summary);
+}
+
+gboolean
+camel_m365_store_summary_get_folder (CamelM365StoreSummary *store_summary,
+                                    const gchar *id,
+                                    gchar **out_full_name,
+                                    gchar **out_display_name,
+                                    gchar **out_parent_id,
+                                    gint32 *out_total_count,
+                                    gint32 *out_unread_count,
+                                    guint32 *out_flags,
+                                    EM365FolderKind *out_kind,
+                                    gboolean *out_is_foreign,
+                                    gboolean *out_is_public)
+{
+       gboolean found;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), FALSE);
+       g_return_val_if_fail (id != NULL, FALSE);
+
+       LOCK (store_summary);
+
+       found = g_key_file_has_group (store_summary->priv->key_file, id);
+
+       if (found) {
+               if (out_full_name)
+                       *out_full_name = g_strdup (g_hash_table_lookup 
(store_summary->priv->id_full_name_hash, id));
+
+               if (out_display_name)
+                       *out_display_name = g_key_file_get_string (store_summary->priv->key_file, id, 
"DisplayName", NULL);
+
+               if (out_parent_id)
+                       *out_parent_id = g_key_file_get_string (store_summary->priv->key_file, id, 
"ParentId", NULL);
+
+               if (out_total_count)
+                       *out_total_count = g_key_file_get_integer (store_summary->priv->key_file, id, 
"TotalCount", NULL);
+
+               if (out_unread_count)
+                       *out_unread_count = g_key_file_get_integer (store_summary->priv->key_file, id, 
"UnreadCount", NULL);
+
+               if (out_flags)
+                       *out_flags = g_key_file_get_uint64 (store_summary->priv->key_file, id, "Flags", NULL);
+
+               if (out_kind)
+                       *out_kind = g_key_file_get_integer (store_summary->priv->key_file, id, "Kind", NULL);
+
+               if (out_is_foreign)
+                       *out_is_foreign = g_key_file_get_boolean (store_summary->priv->key_file, id, 
"IsForeign", NULL);
+
+               if (out_is_public)
+                       *out_is_public = g_key_file_get_boolean (store_summary->priv->key_file, id, 
"IsPublic", NULL);
+       }
+
+       UNLOCK (store_summary);
+
+       return found;
+}
+
+gchar *
+camel_m365_store_summary_dup_folder_full_name (CamelM365StoreSummary *store_summary,
+                                              const gchar *id)
+{
+       gchar *value = NULL;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, &value, NULL, NULL, NULL, NULL, NULL, 
NULL, NULL, NULL))
+               value = NULL;
+
+       return value;
+}
+
+gchar *
+camel_m365_store_summary_dup_folder_id_for_full_name (CamelM365StoreSummary *store_summary,
+                                                     const gchar *full_name)
+{
+       gchar *id;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+       g_return_val_if_fail (full_name != NULL, NULL);
+
+       LOCK (store_summary);
+
+       id = g_strdup (g_hash_table_lookup (store_summary->priv->full_name_id_hash, full_name));
+
+       UNLOCK (store_summary);
+
+       return id;
+}
+
+gchar *
+camel_m365_store_summary_dup_folder_id_for_type (CamelM365StoreSummary *store_summary,
+                                                guint32 folder_type)
+{
+       GHashTableIter iter;
+       gpointer key;
+       gchar *id = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+
+       folder_type = folder_type & CAMEL_FOLDER_TYPE_MASK;
+       g_return_val_if_fail (folder_type != 0, NULL);
+
+       LOCK (store_summary);
+
+       g_hash_table_iter_init (&iter, store_summary->priv->id_full_name_hash);
+
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               guint32 flags;
+
+               flags = camel_m365_store_summary_get_folder_flags (store_summary, key);
+
+               if ((flags & CAMEL_FOLDER_TYPE_MASK) == folder_type) {
+                       id = g_strdup (key);
+                       break;
+               }
+       }
+
+       UNLOCK (store_summary);
+
+       return id;
+}
+
+typedef struct _IdFullNameData {
+       gchar *id;
+       gchar *full_name;
+} IdFullNameData;
+
+static IdFullNameData *
+id_full_name_data_new (gchar *id,
+                      gchar *full_name)
+{
+       IdFullNameData *ifnd;
+
+       ifnd = g_slice_new (IdFullNameData);
+       ifnd->id = id;
+       ifnd->full_name = full_name;
+
+       return ifnd;
+}
+
+static void
+id_full_name_data_free (gpointer ptr)
+{
+       IdFullNameData *ifnd = ptr;
+
+       if (ifnd) {
+               g_free (ifnd->id);
+               g_free (ifnd->full_name);
+               g_slice_free (IdFullNameData, ifnd);
+       }
+}
+
+typedef struct _RemovePrefixedData {
+       GHashTable *full_name_id_hash;
+       const gchar *prefix;
+       gint prefix_len;
+       GSList *removed; /* IdFullNameData * */
+} RemovePrefixedData;
+
+static gboolean
+m365_remove_prefixed_cb (gpointer key,
+                        gpointer value,
+                        gpointer user_data)
+{
+       RemovePrefixedData *rpd = user_data;
+       gchar *id = key, *full_name = value;
+
+       g_return_val_if_fail (rpd != NULL, FALSE);
+       g_return_val_if_fail (full_name != NULL, FALSE);
+
+       if (g_str_has_prefix (full_name, rpd->prefix) &&
+           (!full_name[rpd->prefix_len] || full_name[rpd->prefix_len] == '/')) {
+               g_hash_table_remove (rpd->full_name_id_hash, full_name);
+
+               rpd->removed = g_slist_prepend (rpd->removed, id_full_name_data_new (id, full_name));
+
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+static gchar *
+m365_store_summary_build_new_full_name (const gchar *old_full_name,
+                                       const gchar *new_display_name)
+{
+       gchar *encoded;
+       GString *full_name;
+       const gchar *last_slash;
+
+       g_return_val_if_fail (old_full_name != NULL, NULL);
+       g_return_val_if_fail (new_display_name != NULL, NULL);
+
+       last_slash = strrchr (old_full_name, '/');
+       encoded = m365_store_summary_encode_folder_name (new_display_name);
+       full_name = g_string_sized_new ((last_slash ? (last_slash - old_full_name) : 0) + strlen (encoded) + 
2);
+
+       if (last_slash)
+               g_string_append_len (full_name, old_full_name, last_slash - old_full_name + 1);
+
+       g_string_append (full_name, encoded);
+
+       g_free (encoded);
+
+       return g_string_free (full_name, FALSE);
+}
+
+gboolean
+camel_m365_store_summary_set_folder_display_name (CamelM365StoreSummary *store_summary,
+                                                 const gchar *id,
+                                                 const gchar *display_name,
+                                                 gboolean with_hashes_update)
+{
+       gchar *current_display_name;
+       gboolean changed = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), FALSE);
+       g_return_val_if_fail (id != NULL, FALSE);
+       g_return_val_if_fail (display_name != NULL, FALSE);
+
+       LOCK (store_summary);
+
+       current_display_name = g_key_file_get_string (store_summary->priv->key_file, id, "DisplayName", NULL);
+
+       if (g_strcmp0 (current_display_name, display_name) != 0) {
+               const gchar *old_full_name;
+
+               g_key_file_set_string (store_summary->priv->key_file, id, "DisplayName", display_name);
+               store_summary->priv->dirty = TRUE;
+
+               changed = TRUE;
+
+               if (with_hashes_update) {
+                       old_full_name = g_hash_table_lookup (store_summary->priv->id_full_name_hash, id);
+
+                       if (old_full_name) {
+                               RemovePrefixedData rpd;
+                               gchar *new_full_name;
+                               gint diff;
+                               GSList *link;
+
+                               rpd.full_name_id_hash = store_summary->priv->full_name_id_hash;
+                               rpd.prefix = old_full_name;
+                               rpd.prefix_len = strlen (old_full_name);
+                               rpd.removed = NULL;
+
+                               g_hash_table_foreach_steal (store_summary->priv->id_full_name_hash, 
m365_remove_prefixed_cb, &rpd);
+
+                               new_full_name = m365_store_summary_build_new_full_name (old_full_name, 
display_name);
+                               diff = strlen (new_full_name) - rpd.prefix_len;
+
+                               for (link = rpd.removed; link; link = g_slist_next (link)) {
+                                       IdFullNameData *ifnd = link->data;
+                                       GString *fixed_full_name_str;
+                                       gchar *fixed_full_name;
+                                       gint old_full_name_len;
+
+                                       old_full_name_len = strlen (ifnd->full_name);
+                                       fixed_full_name_str = g_string_sized_new (old_full_name_len + diff + 
2);
+
+                                       g_string_append (fixed_full_name_str, new_full_name);
+
+                                       if (old_full_name_len > rpd.prefix_len)
+                                               g_string_append (fixed_full_name_str, ifnd->full_name + 
rpd.prefix_len);
+
+                                       fixed_full_name = g_string_free (fixed_full_name_str, FALSE);
+
+                                       g_hash_table_insert (store_summary->priv->id_full_name_hash, 
ifnd->id, fixed_full_name);
+                                       g_hash_table_insert (store_summary->priv->full_name_id_hash, 
fixed_full_name, ifnd->id);
+
+                                       /* To not be freed by id_full_name_data_free() below */
+                                       ifnd->id = NULL;
+                               }
+
+                               g_slist_free_full (rpd.removed, id_full_name_data_free);
+                               g_free (new_full_name);
+                       } else {
+                               gchar *parent_id, *encoded_folder_name, *id_copy, *new_full_name = NULL;
+
+                               encoded_folder_name = m365_store_summary_encode_folder_name (display_name);
+
+                               parent_id = camel_m365_store_summary_dup_folder_parent_id (store_summary, id);
+
+                               if (parent_id && *parent_id) {
+                                       const gchar *parent_full_name;
+
+                                       parent_full_name = g_hash_table_lookup 
(store_summary->priv->id_full_name_hash, parent_id);
+
+                                       if (parent_full_name && *parent_full_name)
+                                               new_full_name = g_strconcat (parent_full_name, "/", 
encoded_folder_name, NULL);
+                               }
+
+                               if (!new_full_name) {
+                                       new_full_name = encoded_folder_name;
+                                       encoded_folder_name = NULL;
+                               }
+
+                               g_free (encoded_folder_name);
+                               g_free (parent_id);
+
+                               id_copy = g_strdup (id);
+
+                               g_hash_table_insert (store_summary->priv->id_full_name_hash, id_copy, 
new_full_name);
+                               g_hash_table_insert (store_summary->priv->full_name_id_hash, new_full_name, 
id_copy);
+                       }
+               }
+       }
+
+       g_free (current_display_name);
+
+       UNLOCK (store_summary);
+
+       return changed;
+}
+
+gchar *
+camel_m365_store_summary_dup_folder_display_name (CamelM365StoreSummary *store_summary,
+                                                 const gchar *id)
+{
+       gchar *value = NULL;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, &value, NULL, NULL, NULL, NULL, 
NULL, NULL, NULL))
+               value = NULL;
+
+       return value;
+}
+
+void
+camel_m365_store_summary_set_folder_parent_id (CamelM365StoreSummary *store_summary,
+                                              const gchar *id,
+                                              const gchar *parent_id)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+
+       LOCK (store_summary);
+
+       if (parent_id && *parent_id) {
+               gchar *current_parent_id;
+
+               current_parent_id = g_key_file_get_string (store_summary->priv->key_file, id, "ParentId", 
NULL);
+
+               if (g_strcmp0 (current_parent_id, parent_id) != 0) {
+                       g_key_file_set_string (store_summary->priv->key_file, id, "ParentId", parent_id);
+                       store_summary->priv->dirty = TRUE;
+               }
+
+               g_free (current_parent_id);
+       } else if (g_key_file_has_key (store_summary->priv->key_file, id, "ParentId", NULL)) {
+               g_key_file_remove_key (store_summary->priv->key_file, id, "ParentId", NULL);
+               store_summary->priv->dirty = TRUE;
+       }
+
+       UNLOCK (store_summary);
+}
+
+gchar *
+camel_m365_store_summary_dup_folder_parent_id (CamelM365StoreSummary *store_summary,
+                                              const gchar *id)
+{
+       gchar *value = NULL;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, &value, NULL, NULL, NULL, 
NULL, NULL, NULL))
+               value = NULL;
+
+       return value;
+}
+
+void
+camel_m365_store_summary_set_folder_total_count (CamelM365StoreSummary *store_summary,
+                                                const gchar *id,
+                                                gint32 total_count)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+
+       LOCK (store_summary);
+
+       if (g_key_file_get_integer (store_summary->priv->key_file, id, "TotalCount", NULL) != total_count) {
+               g_key_file_set_integer (store_summary->priv->key_file, id, "TotalCount", total_count);
+               store_summary->priv->dirty = TRUE;
+       }
+
+       UNLOCK (store_summary);
+}
+
+gint32
+camel_m365_store_summary_get_folder_total_count (CamelM365StoreSummary *store_summary,
+                                                const gchar *id)
+{
+       gint32 value = 0;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, NULL, &value, NULL, NULL, 
NULL, NULL, NULL))
+               value = 0;
+
+       return value;
+}
+
+void
+camel_m365_store_summary_set_folder_unread_count (CamelM365StoreSummary *store_summary,
+                                                 const gchar *id,
+                                                 gint32 unread_count)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+
+       LOCK (store_summary);
+
+       if (g_key_file_get_integer (store_summary->priv->key_file, id, "UnreadCount", NULL) != unread_count) {
+               g_key_file_set_integer (store_summary->priv->key_file, id, "UnreadCount", unread_count);
+               store_summary->priv->dirty = TRUE;
+       }
+
+       UNLOCK (store_summary);
+}
+
+gint32
+camel_m365_store_summary_get_folder_unread_count (CamelM365StoreSummary *store_summary,
+                                                 const gchar *id)
+{
+       gint32 value = 0;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, NULL, NULL, &value, NULL, 
NULL, NULL, NULL))
+               value = 0;
+
+       return value;
+}
+
+void
+camel_m365_store_summary_set_folder_flags (CamelM365StoreSummary *store_summary,
+                                          const gchar *id,
+                                          guint32 flags)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (id != NULL);
+
+       LOCK (store_summary);
+
+       if (g_key_file_get_uint64 (store_summary->priv->key_file, id, "Flags", NULL) != flags) {
+               g_key_file_set_uint64 (store_summary->priv->key_file, id, "Flags", flags);
+               store_summary->priv->dirty = TRUE;
+       }
+
+       UNLOCK (store_summary);
+}
+
+guint32
+camel_m365_store_summary_get_folder_flags (CamelM365StoreSummary *store_summary,
+                                          const gchar *id)
+{
+       guint32 value = 0;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, NULL, NULL, NULL, &value, 
NULL, NULL, NULL))
+               value = 0;
+
+       return value;
+}
+
+guint32
+camel_m365_store_summary_get_folder_flags_for_full_name (CamelM365StoreSummary *store_summary,
+                                                        const gchar *full_name)
+{
+       const gchar *id;
+       guint32 flags = 0;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), 0);
+       g_return_val_if_fail (full_name != NULL, 0);
+
+       LOCK (store_summary);
+
+       id = g_hash_table_lookup (store_summary->priv->full_name_id_hash, full_name);
+
+       if (id)
+               flags = camel_m365_store_summary_get_folder_flags (store_summary, id);
+
+       UNLOCK (store_summary);
+
+       return flags;
+}
+
+EM365FolderKind
+camel_m365_store_summary_get_folder_kind (CamelM365StoreSummary *store_summary,
+                                         const gchar *id)
+{
+       EM365FolderKind value = E_M365_FOLDER_KIND_UNKNOWN;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, NULL, NULL, NULL, NULL, 
&value, NULL, NULL))
+               value = E_M365_FOLDER_KIND_UNKNOWN;
+
+       return value;
+}
+
+gboolean
+camel_m365_store_summary_get_folder_is_foreign (CamelM365StoreSummary *store_summary,
+                                               const gchar *id)
+{
+       gboolean value = FALSE;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, NULL, NULL, NULL, NULL, 
NULL, &value, NULL))
+               value = FALSE;
+
+       return value;
+}
+
+gboolean
+camel_m365_store_summary_get_folder_is_public (CamelM365StoreSummary *store_summary,
+                                              const gchar *id)
+{
+       gboolean value = FALSE;
+
+       if (!camel_m365_store_summary_get_folder (store_summary, id, NULL, NULL, NULL, NULL, NULL, NULL, 
NULL, NULL, &value))
+               value = FALSE;
+
+       return value;
+}
+
+GSList * /* gchar * */
+camel_m365_store_summary_list_folder_ids (CamelM365StoreSummary *store_summary)
+{
+       GSList *ids = NULL;
+       gchar **groups;
+       gint ii;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+
+       LOCK (store_summary);
+
+       groups = g_key_file_get_groups (store_summary->priv->key_file, NULL);
+
+       for (ii = 0; groups[ii]; ii++) {
+               gchar *group = groups[ii];
+
+               if (g_ascii_strcasecmp (group, STORE_GROUP_NAME) != 0 &&
+                   g_key_file_has_key (store_summary->priv->key_file, group, "DisplayName", NULL)) {
+                       ids = g_slist_prepend (ids, group);
+               } else {
+                       g_free (group);
+               }
+       }
+
+       UNLOCK (store_summary);
+
+       /* The array items are moved into the 'ids' GSList or freed */
+       g_free (groups);
+
+       return ids;
+}
+
+CamelFolderInfo *
+camel_m365_store_summary_build_folder_info_for_id (CamelM365StoreSummary *store_summary,
+                                                  const gchar *id)
+{
+       CamelFolderInfo *info;
+       gchar *full_name = NULL;
+       gchar *display_name = NULL;
+       gint32 total_count = 0;
+       gint32 unread_count = 0;
+       guint32 flags = 0;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+       g_return_val_if_fail (id != NULL, NULL);
+
+       LOCK (store_summary);
+
+       if (camel_m365_store_summary_get_folder (store_summary, id, &full_name, &display_name, NULL, 
&total_count, &unread_count, &flags, NULL, NULL, NULL)) {
+               info = camel_folder_info_new ();
+               info->full_name = full_name;
+               info->display_name = display_name;
+               info->flags = flags;
+               info->unread = unread_count;
+               info->total = total_count;
+       } else {
+               info = NULL;
+       }
+
+       UNLOCK (store_summary);
+
+       return info;
+}
+
+typedef struct _GatherInfosData {
+       CamelM365StoreSummary *store_summary;
+       GPtrArray *folder_infos;
+       const gchar *prefix;
+       gint prefix_len;
+       gboolean recursive;
+} GatherInfosData;
+
+static void
+m365_store_summary_gather_folder_infos (gpointer key,
+                                       gpointer value,
+                                       gpointer user_data)
+{
+       const gchar *id = key, *full_name = value;
+       GatherInfosData *gid = user_data;
+
+       g_return_if_fail (full_name != NULL);
+       g_return_if_fail (gid != NULL);
+
+       if (!gid->prefix_len || (g_str_has_prefix (full_name, gid->prefix) &&
+           (full_name[gid->prefix_len] == '/' || !full_name[gid->prefix_len]))) {
+               const gchar *without_prefix = full_name + gid->prefix_len + (gid->prefix_len > 0 ? 1 : 0);
+
+               if (gid->recursive || !*without_prefix) {
+                       CamelFolderInfo *info;
+
+                       info = camel_m365_store_summary_build_folder_info_for_id (gid->store_summary, id);
+
+                       if (info)
+                               g_ptr_array_add (gid->folder_infos, info);
+                       else
+                               g_warning ("%s: Failed to build folder info for id:'%s' full_name:'%s'", 
G_STRFUNC, id, full_name);
+               }
+       }
+}
+
+CamelFolderInfo *
+camel_m365_store_summary_build_folder_info (CamelM365StoreSummary *store_summary,
+                                           const gchar *top,
+                                           gboolean recursive)
+{
+       CamelFolderInfo *info = NULL;
+       GatherInfosData gid;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+
+       if (!top)
+               top = "";
+
+       LOCK (store_summary);
+
+       gid.store_summary = store_summary;
+       gid.folder_infos = g_ptr_array_new ();
+       gid.prefix = top;
+       gid.prefix_len = strlen (top);
+       gid.recursive = recursive;
+
+       g_hash_table_foreach (store_summary->priv->id_full_name_hash, m365_store_summary_gather_folder_infos, 
&gid);
+
+       info = camel_folder_info_build (gid.folder_infos, top, '/', TRUE);
+
+       UNLOCK (store_summary);
+
+       g_ptr_array_free (gid.folder_infos, TRUE);
+
+       return info;
+}
+
+static void
+m365_store_summary_folder_count_notify_cb (CamelFolderSummary *folder_summary,
+                                          GParamSpec *param,
+                                          CamelM365StoreSummary *store_summary)
+{
+       CamelFolder *folder;
+       gchar *folder_id;
+       gint count;
+
+       g_return_if_fail (CAMEL_IS_FOLDER_SUMMARY (folder_summary));
+       g_return_if_fail (param != NULL);
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+
+       folder = camel_folder_summary_get_folder (folder_summary);
+
+       if (!folder)
+               return;
+
+       folder_id = camel_m365_store_summary_dup_folder_id_for_full_name (store_summary, 
camel_folder_get_full_name (folder));
+
+       /* This can happen on folder delete/unsubscribe, after folder summary clear */
+       if (!folder_id)
+               return;
+
+       if (g_strcmp0 (g_param_spec_get_name (param), "saved-count") == 0) {
+               count = camel_folder_summary_get_saved_count (folder_summary);
+               camel_m365_store_summary_set_folder_total_count (store_summary, folder_id, count);
+       } else if (g_strcmp0 (g_param_spec_get_name (param), "unread-count") == 0) {
+               count = camel_folder_summary_get_unread_count (folder_summary);
+               camel_m365_store_summary_set_folder_unread_count (store_summary, folder_id, count);
+       } else {
+               g_warn_if_reached ();
+       }
+
+       g_free (folder_id);
+}
+
+void
+camel_m365_store_summary_connect_folder_summary (CamelM365StoreSummary *store_summary,
+                                                CamelFolderSummary *folder_summary)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (CAMEL_IS_FOLDER_SUMMARY (folder_summary));
+
+       g_signal_connect_object (folder_summary, "notify::saved-count", G_CALLBACK 
(m365_store_summary_folder_count_notify_cb), store_summary, 0);
+       g_signal_connect_object (folder_summary, "notify::unread-count", G_CALLBACK 
(m365_store_summary_folder_count_notify_cb), store_summary, 0);
+}
+
+static gchar *
+camel_m365_category_to_string (const CamelM365Category *cat)
+{
+       gchar *id, *display_name, *color = NULL, *str;
+
+       g_return_val_if_fail (cat != NULL, NULL);
+
+       id = g_uri_escape_string (cat->id, NULL, TRUE);
+       display_name = g_uri_escape_string (cat->display_name, NULL, TRUE);
+
+       if (cat->color)
+               color = g_uri_escape_string (cat->color, NULL, TRUE);
+
+       str = g_strconcat (
+               id ? id : "", "\t",
+               display_name ? display_name : "", "\t",
+               color ? color : "",
+               NULL);
+
+       g_free (id);
+       g_free (display_name);
+       g_free (color);
+
+       return str;
+}
+
+static CamelM365Category *
+camel_m365_category_from_string (const gchar *str)
+{
+       CamelM365Category *cat;
+       gchar **strv, *id, *display_name, *color;
+
+       g_return_val_if_fail (str != NULL, NULL);
+
+       strv = g_strsplit (str, "\t", -1);
+       if (!strv || !strv[0] || !strv[1]) {
+               g_strfreev (strv);
+               return NULL;
+       }
+
+       id = g_uri_unescape_string (strv[0], NULL);
+       display_name = g_uri_unescape_string (strv[1], NULL);
+       color = (strv[2] && strv[2][0]) ? g_uri_unescape_string (strv[2], NULL) : NULL;
+
+       cat = camel_m365_category_new (id, display_name, color);
+
+       g_free (id);
+       g_free (display_name);
+       g_free (color);
+       g_strfreev (strv);
+
+       return cat;
+}
+
+GHashTable * /* gchar *id ~> CamelM365Category * */
+camel_m365_store_summary_get_categories (CamelM365StoreSummary *store_summary)
+{
+       GHashTable *categories;
+       gchar **strv;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary), NULL);
+
+       LOCK (store_summary);
+
+       strv = g_key_file_get_string_list (store_summary->priv->key_file, STORE_GROUP_NAME, CATEGORIES_KEY, 
NULL, NULL);
+
+       UNLOCK (store_summary);
+
+       categories = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, camel_m365_category_free);
+
+       if (strv) {
+               gint ii;
+
+               for (ii = 0; strv[ii]; ii++) {
+                       CamelM365Category *cat;
+
+                       cat = camel_m365_category_from_string (strv[ii]);
+                       if (cat)
+                               g_hash_table_insert (categories, cat->id, cat);
+               }
+
+               g_strfreev (strv);
+       }
+
+       return categories;
+}
+
+void
+camel_m365_store_summary_set_categories (CamelM365StoreSummary *store_summary,
+                                        GHashTable *categories) /* gchar *id ~> CamelM365Category * */
+{
+       GPtrArray *array;
+       GHashTableIter iter;
+       gpointer value;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE_SUMMARY (store_summary));
+       g_return_if_fail (categories != NULL);
+
+       array = g_ptr_array_new_full (g_hash_table_size (categories), g_free);
+
+       g_hash_table_iter_init (&iter, categories);
+
+       while (g_hash_table_iter_next (&iter, NULL, &value)) {
+               CamelM365Category *cat = value;
+
+               if (cat) {
+                       gchar *str;
+
+                       str = camel_m365_category_to_string (cat);
+
+                       if (str)
+                               g_ptr_array_add (array, str);
+               }
+       }
+
+       LOCK (store_summary);
+
+       g_key_file_set_string_list (store_summary->priv->key_file, STORE_GROUP_NAME, CATEGORIES_KEY,
+               (const gchar * const *) array->pdata, array->len);
+
+       store_summary->priv->dirty = TRUE;
+
+       UNLOCK (store_summary);
+
+       g_ptr_array_free (array, TRUE);
+}
+
+CamelM365Category *
+camel_m365_category_new (const gchar *id,
+                        const gchar *display_name,
+                        const gchar *color)
+{
+       CamelM365Category *cat;
+
+       g_return_val_if_fail (id != NULL, NULL);
+       g_return_val_if_fail (display_name != NULL, NULL);
+
+       cat = g_slice_new0 (CamelM365Category);
+       cat->id = g_strdup (id);
+       cat->display_name = g_strdup (display_name);
+       cat->color = g_strdup (color);
+
+       return cat;
+}
+
+void
+camel_m365_category_free (gpointer ptr)
+{
+       CamelM365Category *cat = ptr;
+
+       if (cat) {
+               g_free (cat->id);
+               g_free (cat->display_name);
+               g_free (cat->color);
+               g_slice_free (CamelM365Category, cat);
+       }
+}
diff --git a/src/Microsoft365/camel/camel-m365-store-summary.h 
b/src/Microsoft365/camel/camel-m365-store-summary.h
new file mode 100644
index 00000000..29f82a4d
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-store-summary.h
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_STORE_SUMMARY_H
+#define CAMEL_M365_STORE_SUMMARY_H
+
+#include <camel/camel.h>
+
+#include "common/e-m365-enums.h"
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_STORE_SUMMARY \
+       (camel_m365_store_summary_get_type ())
+#define CAMEL_M365_STORE_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_STORE_SUMMARY, CamelM365StoreSummary))
+#define CAMEL_M365_STORE_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_STORE_SUMMARY, CamelM365StoreSummaryClass))
+#define CAMEL_IS_M365_STORE_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_STORE_SUMMARY))
+#define CAMEL_IS_M365_STORE_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_STORE_SUMMARY))
+#define CAMEL_M365_STORE_SUMMARY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_STORE_SUMMARY, CamelM365StoreSummaryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelM365StoreSummary CamelM365StoreSummary;
+typedef struct _CamelM365StoreSummaryClass CamelM365StoreSummaryClass;
+typedef struct _CamelM365StoreSummaryPrivate CamelM365StoreSummaryPrivate;
+
+struct _CamelM365StoreSummary {
+       GObject parent;
+       CamelM365StoreSummaryPrivate *priv;
+};
+
+struct _CamelM365StoreSummaryClass {
+       GObjectClass parent_class;
+};
+
+GType          camel_m365_store_summary_get_type       (void);
+
+CamelM365StoreSummary *
+               camel_m365_store_summary_new            (const gchar *filename);
+gboolean       camel_m365_store_summary_load           (CamelM365StoreSummary *store_summary,
+                                                        GError **error);
+gboolean       camel_m365_store_summary_save           (CamelM365StoreSummary *store_summary,
+                                                        GError **error);
+void           camel_m365_store_summary_clear          (CamelM365StoreSummary *store_summary);
+void           camel_m365_store_summary_lock           (CamelM365StoreSummary *store_summary);
+void           camel_m365_store_summary_unlock         (CamelM365StoreSummary *store_summary);
+void           camel_m365_store_summary_rebuild_hashes (CamelM365StoreSummary *store_summary);
+void           camel_m365_store_summary_set_delta_link (CamelM365StoreSummary *store_summary,
+                                                        const gchar *delta_link);
+gchar *                camel_m365_store_summary_dup_delta_link (CamelM365StoreSummary *store_summary);
+gboolean       camel_m365_store_summary_has_folder     (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+gboolean       camel_m365_store_summary_has_full_name  (CamelM365StoreSummary *store_summary,
+                                                        const gchar *full_name);
+void           camel_m365_store_summary_remove_folder  (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+void           camel_m365_store_summary_set_folder     (CamelM365StoreSummary *store_summary,
+                                                        gboolean with_hashes_update,
+                                                        const gchar *id,
+                                                        const gchar *parent_id,
+                                                        const gchar *display_name,
+                                                        gint32 total_count,
+                                                        gint32 unread_count,
+                                                        guint32 flags,
+                                                        EM365FolderKind kind,
+                                                        gboolean is_foreign,
+                                                        gboolean is_public);
+void           camel_m365_store_summary_update_folder  (CamelM365StoreSummary *store_summary,
+                                                        gboolean with_hashes_update,
+                                                        const gchar *id,
+                                                        const gchar *parent_id,
+                                                        const gchar *display_name,
+                                                        gint32 total_count,
+                                                        gint32 unread_count,
+                                                        gint32 children_count);
+gboolean       camel_m365_store_summary_get_folder     (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id,
+                                                        gchar **out_full_name,
+                                                        gchar **out_display_name,
+                                                        gchar **out_parent_id,
+                                                        gint32 *out_total_count,
+                                                        gint32 *out_unread_count,
+                                                        guint32 *out_flags,
+                                                        EM365FolderKind *out_kind,
+                                                        gboolean *out_is_foreign,
+                                                        gboolean *out_is_public);
+gchar *                camel_m365_store_summary_dup_folder_full_name
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+gchar *                camel_m365_store_summary_dup_folder_id_for_full_name
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *full_name);
+gchar *                camel_m365_store_summary_dup_folder_id_for_type
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        guint32 folder_type); /* The CAMEL_FOLDER_TYPE_... 
of the CamelFolderInfoFlags */
+gboolean       camel_m365_store_summary_set_folder_display_name
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id,
+                                                        const gchar *display_name,
+                                                        gboolean with_hashes_update);
+gchar *                camel_m365_store_summary_dup_folder_display_name
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+void           camel_m365_store_summary_set_folder_parent_id
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id,
+                                                        const gchar *parent_id);
+gchar *                camel_m365_store_summary_dup_folder_parent_id
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+void           camel_m365_store_summary_set_folder_total_count
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id,
+                                                        gint32 total_count);
+gint32         camel_m365_store_summary_get_folder_total_count
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+void           camel_m365_store_summary_set_folder_unread_count
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id,
+                                                        gint32 unread_count);
+gint32         camel_m365_store_summary_get_folder_unread_count
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+void           camel_m365_store_summary_set_folder_flags
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id,
+                                                        guint32 flags);
+guint32                camel_m365_store_summary_get_folder_flags
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+guint32                camel_m365_store_summary_get_folder_flags_for_full_name
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *full_name);
+EM365FolderKind        camel_m365_store_summary_get_folder_kind(CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+gboolean       camel_m365_store_summary_get_folder_is_foreign
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+gboolean       camel_m365_store_summary_get_folder_is_public
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+GSList *       camel_m365_store_summary_list_folder_ids(CamelM365StoreSummary *store_summary); /* gchar 
*folder_id */
+CamelFolderInfo *
+               camel_m365_store_summary_build_folder_info_for_id
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *id);
+CamelFolderInfo *
+               camel_m365_store_summary_build_folder_info
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        const gchar *top,
+                                                        gboolean recursive);
+void           camel_m365_store_summary_connect_folder_summary
+                                                       (CamelM365StoreSummary *store_summary,
+                                                        CamelFolderSummary *folder_summary);
+
+typedef struct _CamelM365Category {
+       gchar *id;
+       gchar *display_name;
+       gchar *color;
+} CamelM365Category;
+
+GHashTable *   camel_m365_store_summary_get_categories /* gchar *id ~> CamelM365Category * */
+                                               (CamelM365StoreSummary *store_summary);
+void           camel_m365_store_summary_set_categories
+                                               (CamelM365StoreSummary *store_summary,
+                                                GHashTable *categories); /* gchar *id ~> CamelM365Category * 
*/
+
+CamelM365Category *
+               camel_m365_category_new         (const gchar *id,
+                                                const gchar *display_name,
+                                                const gchar *color);
+void           camel_m365_category_free        (gpointer ptr); /* CamelM365Category * */
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_STORE_SUMMARY_H */
diff --git a/src/Microsoft365/camel/camel-m365-store.c b/src/Microsoft365/camel/camel-m365-store.c
new file mode 100644
index 00000000..4bb2e1ec
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-store.c
@@ -0,0 +1,1831 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include <e-util/e-util.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+#include "camel-m365-folder.h"
+#include "camel-m365-store-summary.h"
+#include "camel-m365-utils.h"
+
+#include "camel-m365-store.h"
+
+#define LOCK(_store) g_rec_mutex_lock (&(_store->priv->property_lock))
+#define UNLOCK(_store) g_rec_mutex_unlock (&(_store->priv->property_lock))
+
+struct _CamelM365StorePrivate {
+       GRecMutex property_lock;
+       gchar *storage_path;
+       CamelM365StoreSummary *summary;
+       EM365Connection *cnc;
+       GHashTable *default_folders;
+};
+
+static void camel_m365_store_initable_init (GInitableIface *iface);
+static void camel_m365_subscribable_init (CamelSubscribableInterface *iface);
+static GInitableIface *parent_initable_interface;
+
+enum {
+       PROP_0,
+       PROP_CONNECTABLE,
+       PROP_HOST_REACHABLE
+};
+
+G_DEFINE_TYPE_WITH_CODE (CamelM365Store, camel_m365_store, CAMEL_TYPE_OFFLINE_STORE,
+       G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, camel_m365_store_initable_init)
+       G_IMPLEMENT_INTERFACE (CAMEL_TYPE_NETWORK_SERVICE, NULL)
+       G_IMPLEMENT_INTERFACE (CAMEL_TYPE_SUBSCRIBABLE, camel_m365_subscribable_init)
+       G_ADD_PRIVATE (CamelM365Store))
+
+static gboolean
+m365_store_construct (CamelService *service,
+                     CamelSession *session,
+                     CamelProvider *provider,
+                     GError **error)
+{
+       CamelM365Store *m365_store;
+       gchar *summary_file, *session_storage_path;
+       guint32 store_flags;
+       GError *local_error = NULL;
+
+       m365_store = (CamelM365Store *) service;
+
+       store_flags = camel_store_get_flags (CAMEL_STORE (m365_store));
+
+       /* Disable virtual trash and junk folders. Microsoft365 has real folders for that */
+       store_flags &= ~(CAMEL_STORE_VTRASH | CAMEL_STORE_VJUNK);
+       store_flags |= CAMEL_STORE_REAL_JUNK_FOLDER;
+       camel_store_set_flags (CAMEL_STORE (m365_store), store_flags);
+
+       session_storage_path = g_strdup (camel_service_get_user_cache_dir (service));
+
+       if (!session_storage_path) {
+               g_set_error (
+                       error, CAMEL_STORE_ERROR,
+                       CAMEL_STORE_ERROR_INVALID,
+                       _("Session has no storage path"));
+               return FALSE;
+       }
+
+       m365_store->priv->storage_path = session_storage_path;
+
+       g_mkdir_with_parents (m365_store->priv->storage_path, 0700);
+
+       summary_file = g_build_filename (m365_store->priv->storage_path, "folder-tree", NULL);
+       m365_store->priv->summary = camel_m365_store_summary_new (summary_file);
+
+       if (!camel_m365_store_summary_load (m365_store->priv->summary, &local_error))
+               g_warning ("%s: Failed to load store summary '%s': %s", G_STRFUNC, summary_file, local_error 
? local_error->message : "Unknown error");
+
+       g_clear_error (&local_error);
+       g_free (summary_file);
+
+       return TRUE;
+}
+
+static gboolean
+m365_store_initable_init (GInitable *initable,
+                         GCancellable *cancellable,
+                         GError **error)
+{
+       CamelService *service;
+       CamelSession *session;
+       CamelStore *store;
+       gboolean ret;
+
+       store = CAMEL_STORE (initable);
+       service = CAMEL_SERVICE (initable);
+
+       camel_store_set_flags (store, camel_store_get_flags (store) |
+               CAMEL_STORE_USE_CACHE_DIR |
+               CAMEL_STORE_SUPPORTS_INITIAL_SETUP |
+               CAMEL_STORE_CAN_DELETE_FOLDERS_AT_ONCE);
+
+       /* Chain up to parent interface's method. */
+       if (!parent_initable_interface->init (initable, cancellable, error))
+               return FALSE;
+
+       session = camel_service_ref_session (service);
+
+       ret = m365_store_construct (service, session, NULL, error);
+
+       g_object_unref (session);
+
+       return ret;
+}
+
+static GList *
+m365_store_query_auth_types_sync (CamelService *service,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (service), NULL);
+
+       return NULL;
+}
+
+static gchar *
+m365_store_get_name (CamelService *service,
+                    gboolean brief)
+{
+       gchar *name;
+
+       if (brief)
+               name = g_strdup (_("Microsoft 365 server"));
+       else
+               name = g_strdup (_("Mail receive via Microsoft 365"));
+
+       return name;
+}
+
+static gboolean
+m365_store_read_default_folders (CamelM365Store *m365_store,
+                                EM365Connection *cnc,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       struct _default_folders {
+               const gchar *name;
+               guint32 flags;
+       } default_folders[] = {
+               { "archive",            CAMEL_FOLDER_TYPE_ARCHIVE },
+               { "deleteditems",       CAMEL_FOLDER_TYPE_TRASH },
+               { "drafts",             CAMEL_FOLDER_TYPE_DRAFTS },
+               { "inbox",              CAMEL_FOLDER_TYPE_INBOX },
+               { "junkemail",          CAMEL_FOLDER_TYPE_JUNK },
+               { "outbox",             CAMEL_FOLDER_TYPE_OUTBOX },
+               { "sentitems",          CAMEL_FOLDER_TYPE_SENT }
+       };
+       GPtrArray *requests;
+       gboolean success;
+       guint ii;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (m365_store), FALSE);
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+
+       LOCK (m365_store);
+
+       if (g_hash_table_size (m365_store->priv->default_folders)) {
+               UNLOCK (m365_store);
+               return TRUE;
+       }
+
+       UNLOCK (m365_store);
+
+       requests = g_ptr_array_new_full (G_N_ELEMENTS (default_folders), g_object_unref);
+
+       for (ii = 0; ii < G_N_ELEMENTS (default_folders); ii++) {
+               SoupMessage *message;
+               gchar *uri;
+
+               uri = e_m365_connection_construct_uri (cnc, TRUE, NULL, E_M365_API_V1_0, NULL,
+                       "mailFolders",
+                       NULL,
+                       default_folders[ii].name,
+                       "$select", "id",
+                       NULL);
+
+               message = soup_message_new (SOUP_METHOD_GET, uri);
+
+               if (!message) {
+                       g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Malformed URI: ā€œ%sā€"), 
uri);
+
+                       g_ptr_array_unref (requests);
+                       g_free (uri);
+
+                       return FALSE;
+               }
+
+               g_free (uri);
+
+               g_ptr_array_add (requests, message);
+       }
+
+       success = e_m365_connection_batch_request_sync (cnc, E_M365_API_V1_0, requests, cancellable, error);
+
+       if (success) {
+               g_warn_if_fail (requests->len == G_N_ELEMENTS (default_folders));
+
+               LOCK (m365_store);
+
+               for (ii = 0; ii < requests->len; ii++) {
+                       SoupMessage *message = g_ptr_array_index (requests, ii);
+                       JsonNode *node = NULL;
+
+                       if (message->status_code > 0 && SOUP_STATUS_IS_SUCCESSFUL (message->status_code) &&
+                           e_m365_connection_json_node_from_message (message, NULL, &node, cancellable, 
NULL) &&
+                           node && JSON_NODE_HOLDS_OBJECT (node)) {
+                               JsonObject *object = json_node_get_object (node);
+
+                               if (object) {
+                                       const gchar *id;
+
+                                       id = e_m365_json_get_string_member (object, "id", NULL);
+
+                                       if (id && *id) {
+                                               g_hash_table_insert (m365_store->priv->default_folders, 
g_strdup (id),
+                                                       GUINT_TO_POINTER (default_folders[ii].flags));
+                                       }
+                               }
+                       }
+
+                       if (node)
+                               json_node_unref (node);
+               }
+
+               UNLOCK (m365_store);
+       }
+
+       g_ptr_array_unref (requests);
+
+       return success;
+}
+
+static gboolean
+m365_store_equal_label_tag_cb (gconstpointer ptr1,
+                              gconstpointer ptr2)
+{
+       const gchar *evo_label_def = ptr1;
+       const gchar *tag = ptr2;
+       const gchar *pos;
+
+       if (!evo_label_def || !tag || !*tag)
+               return FALSE;
+
+       pos = g_strrstr (evo_label_def, tag);
+
+       return pos > evo_label_def && pos[-1] == '|' && !pos[strlen (tag)];
+}
+
+static gboolean
+m365_store_find_in_ptr_array (GPtrArray *haystack,
+                             gconstpointer needle,
+                             GEqualFunc equal_func,
+                             guint *out_index)
+{
+       guint ii;
+
+       if (!haystack)
+               return FALSE;
+
+       if (!equal_func)
+               equal_func = g_direct_equal;
+
+       for (ii = 0; ii < haystack->len; ii++) {
+               if (equal_func (haystack->pdata[ii], needle)) {
+                       if (out_index)
+                               *out_index = ii;
+
+                       return TRUE;
+               }
+       }
+
+       return FALSE;
+}
+
+/* Returns whether had been done any changes */
+static gboolean
+m365_store_save_category_changes (GHashTable *old_categories, /* gchar *id ~> CamelM365Category * */
+                                 GHashTable *new_categories) /* gchar *id ~> CamelM365Category * */
+{
+       GHashTableIter iter;
+       GSettings *settings;
+       GPtrArray *evo_labels; /* gchar * (encoded label definition) */
+       gchar **strv;
+       gint ii;
+       gpointer value;
+       gboolean changed = FALSE;
+
+       if (!old_categories || !new_categories)
+               return new_categories != NULL;
+
+       evo_labels = g_ptr_array_new_full (5, g_free);
+
+       settings = e_util_ref_settings ("org.gnome.evolution.mail");
+       strv = g_settings_get_strv (settings, "labels");
+
+       for (ii = 0; strv && strv[ii]; ii++) {
+               g_ptr_array_add (evo_labels, g_strdup (strv[ii]));
+       }
+
+       g_strfreev (strv);
+
+       g_hash_table_iter_init (&iter, new_categories);
+       while (g_hash_table_iter_next (&iter, NULL, &value)) {
+               CamelM365Category *new_cat = value, *old_cat;
+               gchar *tag = NULL;
+
+               if (!new_cat)
+                       continue;
+
+               old_cat = g_hash_table_lookup (old_categories, new_cat->id);
+               if (old_cat) {
+                       if (g_strcmp0 (old_cat->display_name, new_cat->display_name) != 0 ||
+                           g_strcmp0 (old_cat->color, new_cat->color) != 0) {
+                               /* Old category changed name or color */
+                               tag = camel_m365_utils_encode_category_name (new_cat->display_name);
+                       }
+               } else {
+                       /* This is a new category */
+                       tag = camel_m365_utils_encode_category_name (new_cat->display_name);
+               }
+
+               if (tag && *tag) {
+                       guint index = (guint) -1;
+                       gchar *label_def;
+
+                       changed = TRUE;
+
+                       /* Sanitize value */
+                       for (ii = 0; tag[ii]; ii++) {
+                               if (tag[ii] == '|')
+                                       tag[ii] = '-';
+                       }
+
+                       if (old_cat && g_strcmp0 (old_cat->display_name, new_cat->display_name) != 0) {
+                               gchar *old_tag = camel_m365_utils_encode_category_name 
(old_cat->display_name);
+
+                               if (old_tag && *old_tag) {
+                                       if (!m365_store_find_in_ptr_array (evo_labels, old_tag, 
m365_store_equal_label_tag_cb, &index))
+                                               index = (guint) -1;
+                               }
+
+                               g_free (old_tag);
+                       }
+
+                       for (ii = 0; new_cat->display_name[ii]; ii++) {
+                               if (new_cat->display_name[ii] == '|')
+                                       new_cat->display_name[ii] = '-';
+                       }
+
+                       if (index == (guint) -1 &&
+                           !m365_store_find_in_ptr_array (evo_labels, tag, m365_store_equal_label_tag_cb, 
&index))
+                               index = (guint) -1;
+
+                       label_def = g_strconcat (new_cat->display_name, "|", new_cat->color ? new_cat->color 
: "#FF0000", "|", tag, NULL);
+
+                       if (index == (guint) -1 || index >= (gint) evo_labels->len) {
+                               g_ptr_array_add (evo_labels, label_def);
+                       } else {
+                               g_free (evo_labels->pdata[index]);
+                               evo_labels->pdata[index] = label_def;
+                       }
+               }
+
+               g_hash_table_remove (old_categories, new_cat->id);
+
+               g_free (tag);
+       }
+
+       if (g_hash_table_size (old_categories) > 0) {
+               /* Some categories had been removed */
+               changed = TRUE;
+
+               g_hash_table_iter_init (&iter, old_categories);
+               while (g_hash_table_iter_next (&iter, NULL, &value)) {
+                       CamelM365Category *old_cat = value;
+                       gchar *old_tag;
+                       guint index;
+
+                       if (!old_cat)
+                               continue;
+
+                       old_tag = camel_m365_utils_encode_category_name (old_cat->display_name);
+
+                       for (ii = 0; old_tag && old_tag[ii]; ii++) {
+                               if (old_tag[ii] == '|')
+                                       old_tag[ii] = '-';
+                       }
+
+                       if (old_tag &&
+                           m365_store_find_in_ptr_array (evo_labels, old_tag, m365_store_equal_label_tag_cb, 
&index))
+                               g_ptr_array_remove_index (evo_labels, index);
+
+                       g_free (old_tag);
+               }
+       }
+
+       if (changed) {
+               /* NULL-terminated array of strings */
+               g_ptr_array_add (evo_labels, NULL);
+
+               g_settings_set_strv (settings, "labels", (const gchar * const *) evo_labels->pdata);
+       }
+
+       g_ptr_array_free (evo_labels, TRUE);
+       g_object_unref (settings);
+
+       return changed;
+}
+
+static void
+m365_store_get_categories_cb (CamelSession *session,
+                             GCancellable *cancellable,
+                             gpointer user_data,
+                             GError **error)
+{
+       CamelM365Store *m365_store = user_data;
+       EM365Connection *cnc;
+       GSList *categories = NULL;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE (m365_store));
+
+       cnc = camel_m365_store_ref_connection (m365_store);
+
+       if (!cnc)
+               return;
+
+       if (e_m365_connection_get_categories_sync (cnc, NULL, &categories, cancellable, error)) {
+               GHashTable *old_categories, *new_categories;
+               GSList *link;
+
+               new_categories = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, 
camel_m365_category_free);
+
+               for (link = categories; link; link = g_slist_next (link)) {
+                       EM365Category *category = link->data;
+                       CamelM365Category *cat;
+                       const gchar *id, *display_name, *color;
+
+                       if (!category)
+                               continue;
+
+                       id = e_m365_category_get_id (category);
+                       display_name = e_m365_category_get_display_name (category);
+                       color = e_m365_category_get_color (category);
+
+                       if (!id || !display_name)
+                               continue;
+
+                       if (display_name != camel_m365_utils_rename_label (display_name, TRUE))
+                               continue;
+
+                       cat = camel_m365_category_new (id, display_name, color);
+
+                       if (cat)
+                               g_hash_table_insert (new_categories, cat->id, cat);
+               }
+
+               g_slist_free_full (categories, (GDestroyNotify) json_object_unref);
+
+               old_categories = camel_m365_store_summary_get_categories (m365_store->priv->summary);
+
+               if (m365_store_save_category_changes (old_categories, new_categories)) {
+                       camel_m365_store_summary_set_categories (m365_store->priv->summary, new_categories);
+                       camel_m365_store_summary_save (m365_store->priv->summary, NULL);
+               }
+
+               g_hash_table_destroy (new_categories);
+               g_hash_table_destroy (old_categories);
+       }
+
+       g_object_unref (cnc);
+}
+
+static gboolean
+m365_store_connect_sync (CamelService *service,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       CamelM365Store *m365_store;
+       EM365Connection *cnc;
+       gboolean success = FALSE;
+
+       /* Chain up to parent's method. */
+       if (!CAMEL_SERVICE_CLASS (camel_m365_store_parent_class)->connect_sync (service, cancellable, error))
+               return FALSE;
+
+       if (camel_service_get_connection_status (service) == CAMEL_SERVICE_DISCONNECTED)
+               return FALSE;
+
+       m365_store = CAMEL_M365_STORE (service);
+       cnc = camel_m365_store_ref_connection (m365_store);
+
+       if (!cnc) {
+               cnc = camel_m365_utils_new_connection (service, NULL);
+
+               if (cnc) {
+                       LOCK (m365_store);
+
+                       m365_store->priv->cnc = g_object_ref (cnc);
+
+                       UNLOCK (m365_store);
+               }
+       }
+
+       if (cnc) {
+               CamelSession *session;
+
+               session = camel_service_ref_session (service);
+
+               success = camel_session_authenticate_sync (session, service, "Microsoft365", cancellable, 
error);
+
+               if (success) {
+                       camel_session_submit_job (
+                               session, _("Look up Microsoft 365 categories"),
+                               m365_store_get_categories_cb,
+                               g_object_ref (m365_store),
+                               g_object_unref);
+               }
+
+               g_clear_object (&session);
+               g_clear_object (&cnc);
+       } else {
+               g_set_error_literal (error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_UNAVAILABLE, _("Failed 
to create connection"));
+       }
+
+       return success;
+}
+
+static gboolean
+m365_store_disconnect_sync (CamelService *service,
+                           gboolean clean,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       CamelM365Store *m365_store = CAMEL_M365_STORE (service);
+       EM365Connection *cnc;
+       gboolean success = TRUE;
+
+       cnc = camel_m365_store_ref_connection (m365_store);
+
+       if (cnc) {
+               success = e_m365_connection_disconnect_sync (cnc, cancellable, error);
+
+               g_clear_object (&cnc);
+       }
+
+       if (!success)
+               return FALSE;
+
+       /* Chain up to parent's method. */
+       return CAMEL_SERVICE_CLASS (camel_m365_store_parent_class)->disconnect_sync (service, clean, 
cancellable, error);
+}
+
+static CamelAuthenticationResult
+m365_store_authenticate_sync (CamelService *service,
+                             const gchar *mechanism,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       CamelAuthenticationResult result;
+       CamelM365Store *m365_store;
+       EM365Connection *cnc;
+
+       m365_store = CAMEL_M365_STORE (service);
+       cnc = camel_m365_store_ref_connection (m365_store);
+
+       if (!cnc)
+               return CAMEL_AUTHENTICATION_ERROR;
+
+       switch (e_m365_connection_authenticate_sync (cnc, NULL, E_M365_FOLDER_KIND_MAIL, NULL, NULL, NULL, 
NULL, cancellable, error)) {
+       case E_SOURCE_AUTHENTICATION_ERROR:
+       case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+       default:
+               result = CAMEL_AUTHENTICATION_ERROR;
+               break;
+       case E_SOURCE_AUTHENTICATION_ACCEPTED:
+               result = CAMEL_AUTHENTICATION_ACCEPTED;
+
+               m365_store_read_default_folders (m365_store, cnc, cancellable, NULL);
+               break;
+       case E_SOURCE_AUTHENTICATION_REJECTED:
+       case E_SOURCE_AUTHENTICATION_REQUIRED:
+               result = CAMEL_AUTHENTICATION_REJECTED;
+               break;
+       }
+
+       g_clear_object (&cnc);
+
+       return result;
+}
+
+static CamelFolder *
+m365_store_get_folder_sync (CamelStore *store,
+                           const gchar *folder_name,
+                           guint32 flags,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       CamelM365Store *m365_store;
+       CamelFolder *folder = NULL;
+       gchar *fid, *folder_dir, *display_name;
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       fid = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store->priv->summary, folder_name);
+
+       if (!fid) {
+               g_set_error (
+                       error, CAMEL_STORE_ERROR,
+                       CAMEL_STORE_ERROR_NO_FOLDER,
+                       _("No such folder: %s"), folder_name);
+               return NULL;
+       }
+
+       display_name = camel_m365_store_summary_dup_folder_display_name (m365_store->priv->summary, fid);
+       folder_dir = g_build_filename (m365_store->priv->storage_path, "folders", folder_name, NULL);
+
+       folder = camel_m365_folder_new (store, display_name, folder_name, folder_dir, cancellable, error);
+
+       g_free (display_name);
+       g_free (folder_dir);
+       g_free (fid);
+
+       if (folder && (flags & CAMEL_STORE_FOLDER_INFO_REFRESH) != 0)
+               camel_folder_prepare_content_refresh (folder);
+
+       return folder;
+}
+
+static void
+m365_store_save_summary (CamelM365StoreSummary *summary,
+                        const gchar *where)
+{
+       GError *error = NULL;
+
+       if (!camel_m365_store_summary_save (summary, &error))
+               g_warning ("%s: Failed to save store summary: %s", where, error ? error->message : "Unknown 
error");
+
+       g_clear_error (&error);
+}
+
+static CamelFolderInfo *
+m365_store_create_folder_sync (CamelStore *store,
+                              const gchar *parent_name,
+                              const gchar *folder_name,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       CamelM365Store *m365_store;
+       EM365MailFolder *mail_folder = NULL;
+       gchar *fid = NULL;
+       gchar *full_name;
+       EM365Connection *cnc;
+       CamelFolderInfo *fi = NULL;
+       guint32 flags;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), NULL);
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       if (parent_name && *parent_name)
+               full_name = g_strdup_printf ("%s/%s", parent_name, folder_name);
+       else
+               full_name = g_strdup (folder_name);
+
+       fid = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store->priv->summary, full_name);
+
+       if (fid) {
+               g_free (fid);
+               g_set_error (
+                       error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
+                       _("Cannot create folder ā€œ%sā€, folder already exists"),
+                       full_name);
+               g_free (full_name);
+               return NULL;
+       }
+
+       g_free (full_name);
+
+       /* Get Parent folder ID */
+       if (parent_name && parent_name[0]) {
+               fid = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store->priv->summary, 
parent_name);
+
+               if (!fid) {
+                       g_set_error (error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
+                               _("Parent folder ā€œ%sā€ does not exist"),
+                               parent_name);
+                       return NULL;
+               }
+       }
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error)) {
+               g_free (fid);
+
+               return NULL;
+       }
+
+       success = e_m365_connection_create_mail_folder_sync (cnc, NULL, fid, folder_name, &mail_folder, 
cancellable, &local_error);
+
+       g_object_unref (cnc);
+       g_free (fid);
+
+       if (!success) {
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+               g_propagate_error (error, local_error);
+
+               return NULL;
+       }
+
+       flags = e_m365_mail_folder_get_child_folder_count (mail_folder) ? CAMEL_STORE_INFO_FOLDER_CHILDREN : 
CAMEL_STORE_INFO_FOLDER_NOCHILDREN;
+
+       camel_m365_store_summary_set_folder (m365_store->priv->summary, TRUE,
+               e_m365_folder_get_id (mail_folder),
+               e_m365_folder_get_parent_folder_id (mail_folder),
+               e_m365_folder_get_display_name (mail_folder),
+               e_m365_mail_folder_get_total_item_count (mail_folder),
+               e_m365_mail_folder_get_unread_item_count (mail_folder),
+               flags, E_M365_FOLDER_KIND_MAIL, FALSE, FALSE);
+
+       fi = camel_m365_store_summary_build_folder_info_for_id (m365_store->priv->summary, 
e_m365_folder_get_id (mail_folder));
+
+       camel_store_folder_created (store, fi);
+       camel_subscribable_folder_subscribed (CAMEL_SUBSCRIBABLE (m365_store), fi);
+
+       json_object_unref (mail_folder);
+
+       m365_store_save_summary (m365_store->priv->summary, G_STRFUNC);
+
+       return fi;
+}
+
+static void
+m365_store_notify_created_recursive (CamelStore *store,
+                                    CamelFolderInfo *folder_info)
+{
+       while (folder_info) {
+               camel_store_folder_created (store, folder_info);
+               camel_subscribable_folder_subscribed (CAMEL_SUBSCRIBABLE (store), folder_info);
+
+               if (folder_info->child)
+                       m365_store_notify_created_recursive (store, folder_info->child);
+
+               folder_info = folder_info->next;
+       }
+}
+
+static gboolean
+m365_store_move_mail_folder (CamelM365Store *m365_store,
+                            EM365Connection *cnc,
+                            const gchar *folder_id,
+                            const gchar *des_folder_id,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       EM365MailFolder *moved_mail_folder = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (m365_store), FALSE);
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+       g_return_val_if_fail (des_folder_id != NULL, FALSE);
+       g_return_val_if_fail (g_strcmp0 (folder_id, des_folder_id) != 0, FALSE);
+
+       success = e_m365_connection_copy_move_mail_folder_sync (cnc, NULL, folder_id, des_folder_id, FALSE, 
&moved_mail_folder, cancellable, error);
+
+       if (success && moved_mail_folder) {
+               CamelFolderInfo *fi;
+               gchar *new_full_name;
+
+               fi = camel_m365_store_summary_build_folder_info_for_id (m365_store->priv->summary, folder_id);
+
+               camel_m365_store_summary_set_folder_parent_id (m365_store->priv->summary, folder_id, 
e_m365_folder_get_parent_folder_id (moved_mail_folder));
+               camel_m365_store_summary_rebuild_hashes (m365_store->priv->summary);
+
+               camel_subscribable_folder_unsubscribed (CAMEL_SUBSCRIBABLE (m365_store), fi);
+               camel_store_folder_deleted (CAMEL_STORE (m365_store), fi);
+
+               camel_folder_info_free (fi);
+
+               new_full_name = camel_m365_store_summary_dup_folder_full_name (m365_store->priv->summary, 
folder_id);
+               g_warn_if_fail (new_full_name != NULL);
+
+               fi = camel_m365_store_summary_build_folder_info (m365_store->priv->summary, new_full_name, 
TRUE);
+
+               m365_store_notify_created_recursive (CAMEL_STORE (m365_store), fi);
+
+               json_object_unref (moved_mail_folder);
+               camel_folder_info_free (fi);
+               g_free (new_full_name);
+       }
+
+       return success;
+}
+
+static void
+m365_store_delete_folders_from_summary_recursive (CamelM365Store *m365_store,
+                                                 CamelFolderInfo *fi,
+                                                 gboolean send_signals)
+{
+       CamelStore *store = send_signals ? CAMEL_STORE (m365_store) : NULL;
+       CamelSubscribable *subscribable = send_signals ? CAMEL_SUBSCRIBABLE (m365_store) : NULL;
+
+       while (fi) {
+               gchar *folder_id;
+
+               if (fi->child)
+                       m365_store_delete_folders_from_summary_recursive (m365_store, fi->child, 
send_signals);
+
+               folder_id = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store->priv->summary, 
fi->full_name);
+               if (folder_id) {
+                       camel_m365_store_summary_remove_folder (m365_store->priv->summary, folder_id);
+                       g_free (folder_id);
+               }
+
+               if (send_signals) {
+                       camel_subscribable_folder_unsubscribed (subscribable, fi);
+                       camel_store_folder_deleted (store, fi);
+               }
+
+               fi = fi->next;
+       }
+}
+
+static gboolean
+m365_store_delete_folder_sync (CamelStore *store,
+                              const gchar *folder_name,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       CamelM365Store *m365_store;
+       CamelFolderInfo *folder_info;
+       EM365Connection *cnc = NULL;
+       gchar *folder_id;
+       gchar *trash_folder_id;
+       gchar *trash_full_name;
+       gboolean success;
+       gboolean is_under_trash_folder, claim_unsubscribe = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), FALSE);
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       folder_info = camel_store_get_folder_info_sync (store, folder_name,
+               CAMEL_STORE_FOLDER_INFO_RECURSIVE | CAMEL_STORE_FOLDER_INFO_SUBSCRIBED,
+               cancellable, &local_error);
+
+       if (!folder_info) {
+               if (local_error)
+                       g_propagate_error (error, local_error);
+
+               return FALSE;
+       }
+
+       folder_id = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store->priv->summary, 
folder_name);
+
+       if (!folder_id) {
+               camel_folder_info_free (folder_info);
+
+               g_set_error_literal (error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, _("Folder does not exist"));
+
+               return FALSE;
+       }
+
+       trash_folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store->priv->summary, 
CAMEL_FOLDER_TYPE_TRASH);
+       trash_full_name = camel_m365_store_summary_dup_folder_full_name (m365_store->priv->summary, 
trash_folder_id);
+
+       if (!trash_full_name) {
+               camel_folder_info_free (folder_info);
+               g_free (trash_folder_id);
+               g_free (folder_id);
+
+               g_set_error_literal (error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, _("Cannot find ā€œDeleted Itemsā€ 
folder"));
+
+               return FALSE;
+       }
+
+       is_under_trash_folder = g_str_has_prefix (folder_name, trash_full_name);
+
+       if (is_under_trash_folder) {
+               gint len = strlen (trash_full_name);
+
+               is_under_trash_folder = len > 0 && (trash_full_name[len - 1] == '/' || folder_name[len] == 
'/');
+       }
+
+       g_free (trash_full_name);
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error)) {
+               camel_folder_info_free (folder_info);
+               g_free (trash_folder_id);
+               g_free (folder_id);
+
+               return FALSE;
+       }
+
+       if (camel_m365_store_summary_get_folder_is_foreign (m365_store->priv->summary, folder_id) ||
+           camel_m365_store_summary_get_folder_is_public (m365_store->priv->summary, folder_id)) {
+               /* do not delete foreign or public folders,
+                * only remove them from the local cache */
+               success = TRUE;
+       } else if (is_under_trash_folder) {
+               success = e_m365_connection_delete_mail_folder_sync (cnc, NULL, folder_id, cancellable, 
&local_error);
+       } else {
+               success = m365_store_move_mail_folder (m365_store, cnc, folder_id, "deleteditems", 
cancellable, &local_error);
+               claim_unsubscribe = FALSE;
+       }
+
+       g_clear_object (&cnc);
+
+       if (!success) {
+               camel_folder_info_free (folder_info);
+               g_free (trash_folder_id);
+               g_free (folder_id);
+
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+               g_propagate_error (error, local_error);
+
+               return FALSE;
+       }
+
+       if (is_under_trash_folder)
+               m365_store_delete_folders_from_summary_recursive (m365_store, folder_info, FALSE);
+
+       if (claim_unsubscribe) {
+               camel_subscribable_folder_unsubscribed (CAMEL_SUBSCRIBABLE (m365_store), folder_info);
+               camel_store_folder_deleted (store, folder_info);
+       }
+
+       camel_folder_info_free (folder_info);
+
+       m365_store_save_summary (m365_store->priv->summary, G_STRFUNC);
+
+       g_free (trash_folder_id);
+       g_free (folder_id);
+
+       return TRUE;
+}
+
+static gboolean
+m365_store_rename_folder_sync (CamelStore *store,
+                              const gchar *old_name,
+                              const gchar *new_name,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       CamelM365Store *m365_store;
+       EM365Connection *cnc;
+       const gchar *old_slash, *new_slash;
+       gint parent_len;
+       gchar *folder_id;
+       gboolean success = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), FALSE);
+
+       if (!g_strcmp0 (old_name, new_name))
+               return TRUE;
+
+       m365_store = CAMEL_M365_STORE (store);
+       folder_id = camel_m365_store_summary_dup_folder_id_for_full_name (m365_store->priv->summary, 
old_name);
+
+       if (!folder_id) {
+               g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER,
+                       _("Folder ā€œ%sā€ does not exist"), old_name);
+
+               return FALSE;
+       }
+
+       if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error)) {
+               g_free (folder_id);
+               return FALSE;
+       }
+
+       old_slash = g_strrstr (old_name, "/");
+       new_slash = g_strrstr (new_name, "/");
+
+       if (old_slash)
+               old_slash++;
+       else
+               old_slash = old_name;
+
+       if (new_slash)
+               new_slash++;
+       else
+               new_slash = new_name;
+
+       parent_len = old_slash - old_name;
+
+       /* First move the folder, if needed */
+       if (new_slash - new_name != parent_len ||
+           strncmp (old_name, new_name, parent_len)) {
+               gchar *new_parent_id;
+
+               if (new_slash - new_name > 0) {
+                       gchar *new_parent;
+
+                       new_parent = g_strndup (new_name, new_slash - new_name - 1);
+                       new_parent_id = camel_m365_store_summary_dup_folder_id_for_full_name 
(m365_store->priv->summary, new_parent);
+
+                       if (!new_parent_id) {
+                               g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER,
+                                       _("Folder ā€œ%sā€ does not exist"), new_parent);
+
+                               g_free (new_parent);
+                               g_free (folder_id);
+
+                               return FALSE;
+                       }
+
+                       g_free (new_parent);
+               } else {
+                       new_parent_id = NULL;
+               }
+
+               success = m365_store_move_mail_folder (m365_store, cnc, folder_id, new_parent_id ? 
new_parent_id : "msgfolderroot", cancellable, &local_error);
+
+               g_free (new_parent_id);
+       }
+
+       /* Then rename the folder, if needed */
+       if (success && g_strcmp0 (old_slash, new_slash) != 0) {
+               EM365MailFolder *mail_folder = NULL;
+
+               success = e_m365_connection_rename_mail_folder_sync (cnc, NULL, folder_id, new_slash, 
&mail_folder, cancellable, &local_error);
+
+               if (mail_folder) {
+                       camel_m365_store_summary_set_folder_display_name (m365_store->priv->summary, 
folder_id,
+                               e_m365_folder_get_display_name (mail_folder), TRUE);
+
+                       json_object_unref (mail_folder);
+               }
+       }
+
+       if (success) {
+               CamelFolderInfo *fi;
+
+               fi = camel_m365_store_summary_build_folder_info_for_id (m365_store->priv->summary, folder_id);
+
+               if (fi) {
+                       camel_store_folder_renamed (store, old_name, fi);
+                       camel_folder_info_free (fi);
+               }
+       }
+
+       m365_store_save_summary (m365_store->priv->summary, G_STRFUNC);
+
+       if (!success && local_error) {
+               camel_m365_store_maybe_disconnect (m365_store, local_error);
+               g_propagate_error (error, local_error);
+       }
+
+       g_free (folder_id);
+
+       return success;
+}
+
+typedef struct _FolderRenamedData {
+       gchar *id;
+       gchar *old_name;
+} FolderRenamedData;
+
+static FolderRenamedData *
+folder_renamed_data_new (gchar *id,
+                        gchar *old_name)
+{
+       FolderRenamedData *frd;
+
+       frd = g_slice_new (FolderRenamedData);
+       frd->id = id;
+       frd->old_name = old_name;
+
+       return frd;
+}
+
+static void
+folder_renamed_data_free (gpointer ptr)
+{
+       FolderRenamedData *frd = ptr;
+
+       if (frd) {
+               g_free (frd->id);
+               g_free (frd->old_name);
+               g_slice_free (FolderRenamedData, frd);
+       }
+}
+
+typedef struct _FoldersDeltaData {
+       CamelM365Store *m365_store;
+       GSList *added_ids; /* gchar *, folder ids */
+       GSList *renamed_data; /* FolderRenamedData * */
+       GSList *removed_fis; /* CamelFolderInfo * */
+} FoldersDeltaData;
+
+static gboolean
+camel_m365_got_folders_delta_cb (EM365Connection *cnc,
+                                const GSList *results, /* JsonObject * - the returned objects from the 
server */
+                                gpointer user_data,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       FoldersDeltaData *fdd = user_data;
+       GSList *link;
+
+       g_return_val_if_fail (fdd, FALSE);
+
+       LOCK (fdd->m365_store);
+
+       for (link = (GSList *) results; link; link = g_slist_next (link)) {
+               JsonObject *object = link->data;
+               const gchar *id = e_m365_folder_get_id (object);
+
+               if (e_m365_delta_is_removed_object (object)) {
+                       CamelFolderInfo *info;
+
+                       info = camel_m365_store_summary_build_folder_info_for_id 
(fdd->m365_store->priv->summary, id);
+
+                       if (info)
+                               fdd->removed_fis = g_slist_prepend (fdd->removed_fis, info);
+
+                       camel_m365_store_summary_remove_folder (fdd->m365_store->priv->summary, id);
+               } else {
+                       gchar *old_full_name = NULL;
+                       guint32 flags;
+
+                       if (camel_m365_store_summary_has_folder (fdd->m365_store->priv->summary, id))
+                               old_full_name = camel_m365_store_summary_dup_folder_full_name 
(fdd->m365_store->priv->summary, id);
+
+                       flags = e_m365_mail_folder_get_child_folder_count (object) ? 
CAMEL_STORE_INFO_FOLDER_CHILDREN : CAMEL_STORE_INFO_FOLDER_NOCHILDREN;
+
+                       flags |= GPOINTER_TO_UINT (g_hash_table_lookup 
(fdd->m365_store->priv->default_folders, id));
+
+                       camel_m365_store_summary_set_folder (fdd->m365_store->priv->summary, FALSE, id,
+                               e_m365_folder_get_parent_folder_id (object),
+                               e_m365_folder_get_display_name (object),
+                               e_m365_mail_folder_get_total_item_count (object),
+                               e_m365_mail_folder_get_unread_item_count (object),
+                               flags, E_M365_FOLDER_KIND_MAIL, FALSE, FALSE);
+
+                       if (old_full_name)
+                               fdd->renamed_data = g_slist_prepend (fdd->renamed_data, 
folder_renamed_data_new (g_strdup (id), old_full_name));
+                       else
+                               fdd->added_ids = g_slist_prepend (fdd->added_ids, g_strdup (id));
+               }
+       }
+
+       UNLOCK (fdd->m365_store);
+
+       return TRUE;
+}
+
+static void
+m365_store_forget_all_folders (CamelM365Store *m365_store)
+{
+       CamelStore *store;
+       CamelSubscribable *subscribable;
+       GSList *ids, *link;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE (m365_store));
+
+       store = CAMEL_STORE (m365_store);
+       subscribable = CAMEL_SUBSCRIBABLE (m365_store);
+       ids = camel_m365_store_summary_list_folder_ids (m365_store->priv->summary);
+
+       if (!ids)
+               return;
+
+       for (link = ids; link; link = g_slist_next (link)) {
+               const gchar *id = link->data;
+               CamelFolderInfo *fi;
+
+               fi = camel_m365_store_summary_build_folder_info_for_id (m365_store->priv->summary, id);
+               camel_subscribable_folder_unsubscribed (subscribable, fi);
+               camel_store_folder_deleted (store, fi);
+               camel_folder_info_free (fi);
+       }
+
+       g_slist_free_full (ids, g_free);
+
+       camel_m365_store_summary_set_delta_link (m365_store->priv->summary, "");
+       camel_m365_store_summary_clear (m365_store->priv->summary);
+}
+
+static CamelFolderInfo *
+m365_store_get_folder_info_sync (CamelStore *store,
+                                const gchar *top,
+                                guint32 flags,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       CamelM365Store *m365_store;
+       CamelFolderInfo *fi;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), NULL);
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       if (camel_offline_store_get_online (CAMEL_OFFLINE_STORE (m365_store))) {
+               gboolean refresh_online;
+
+               refresh_online = !(flags & CAMEL_STORE_FOLDER_INFO_FAST) ||
+                                 (flags & CAMEL_STORE_FOLDER_INFO_REFRESH) != 0;
+
+               if (!refresh_online) {
+                       gchar *delta_link;
+
+                       LOCK (m365_store);
+
+                       delta_link = camel_m365_store_summary_dup_delta_link (m365_store->priv->summary);
+                       refresh_online = !delta_link || !*delta_link;
+                       g_free (delta_link);
+
+                       UNLOCK (m365_store);
+               }
+
+               if (refresh_online) {
+                       EM365Connection *cnc;
+
+                       cnc = camel_m365_store_ref_connection (m365_store);
+
+                       if (cnc) {
+                               FoldersDeltaData fdd;
+                               gchar *old_delta_link, *new_delta_link = NULL;
+                               GError *local_error = NULL;
+
+                               LOCK (m365_store);
+
+                               old_delta_link = camel_m365_store_summary_dup_delta_link 
(m365_store->priv->summary);
+
+                               UNLOCK (m365_store);
+
+                               fdd.m365_store = m365_store;
+                               fdd.added_ids = NULL;
+                               fdd.renamed_data = NULL;
+                               fdd.removed_fis = NULL;
+
+                               success = e_m365_connection_get_folders_delta_sync (cnc, NULL, 
E_M365_FOLDER_KIND_MAIL, NULL, old_delta_link, 0,
+                                       camel_m365_got_folders_delta_cb, &fdd, &new_delta_link, cancellable, 
&local_error);
+
+                               if (old_delta_link && *old_delta_link && 
e_m365_connection_util_delta_token_failed (local_error)) {
+                                       g_clear_pointer (&old_delta_link, g_free);
+                                       g_clear_error (&local_error);
+
+                                       m365_store_forget_all_folders (m365_store);
+
+                                       success = e_m365_connection_get_folders_delta_sync (cnc, NULL, 
E_M365_FOLDER_KIND_MAIL, NULL, NULL, 0,
+                                               camel_m365_got_folders_delta_cb, &fdd, &new_delta_link, 
cancellable, error);
+                               }
+
+                               if (local_error)
+                                       g_propagate_error (error, local_error);
+
+                               if (success) {
+                                       CamelSubscribable *subscribable = CAMEL_SUBSCRIBABLE (m365_store);
+                                       CamelFolderInfo *info;
+                                       GSList *link;
+
+                                       LOCK (m365_store);
+
+                                       camel_m365_store_summary_set_delta_link (m365_store->priv->summary, 
new_delta_link);
+                                       m365_store_save_summary (m365_store->priv->summary, G_STRFUNC);
+
+                                       fdd.added_ids = g_slist_reverse (fdd.added_ids);
+                                       fdd.renamed_data = g_slist_reverse (fdd.renamed_data);
+                                       fdd.removed_fis = g_slist_reverse (fdd.removed_fis);
+
+                                       if (fdd.added_ids || fdd.renamed_data || fdd.removed_fis)
+                                               camel_m365_store_summary_rebuild_hashes 
(m365_store->priv->summary);
+
+                                       for (link = fdd.removed_fis; link; link = g_slist_next (link)) {
+                                               info = link->data;
+
+                                               camel_subscribable_folder_unsubscribed (subscribable, info);
+                                               camel_store_folder_deleted (store, info);
+                                       }
+
+                                       for (link = fdd.added_ids; link; link = g_slist_next (link)) {
+                                               const gchar *id = link->data;
+
+                                               info = camel_m365_store_summary_build_folder_info_for_id 
(m365_store->priv->summary, id);
+
+                                               if (info) {
+                                                       camel_store_folder_created (store, info);
+                                                       camel_subscribable_folder_subscribed (subscribable, 
info);
+                                                       camel_folder_info_free (info);
+                                               }
+                                       }
+
+                                       for (link = fdd.renamed_data; link; link = g_slist_next (link)) {
+                                               const FolderRenamedData *frd = link->data;
+
+                                               info = camel_m365_store_summary_build_folder_info_for_id 
(m365_store->priv->summary, frd->id);
+
+                                               if (info) {
+                                                       camel_store_folder_renamed (store, frd->old_name, 
info);
+                                                       camel_folder_info_free (info);
+                                               }
+                                       }
+
+                                       UNLOCK (m365_store);
+                               }
+
+                               g_slist_free_full (fdd.added_ids, g_free);
+                               g_slist_free_full (fdd.renamed_data, folder_renamed_data_free);
+                               g_slist_free_full (fdd.removed_fis, (GDestroyNotify) camel_folder_info_free);
+
+                               g_clear_object (&cnc);
+                               g_free (old_delta_link);
+                               g_free (new_delta_link);
+                       }
+               }
+       }
+
+       if (success) {
+               LOCK (m365_store);
+
+               fi = camel_m365_store_summary_build_folder_info (m365_store->priv->summary, top, (flags & 
CAMEL_STORE_FOLDER_INFO_RECURSIVE) != 0);
+
+               UNLOCK (m365_store);
+       } else {
+               fi = NULL;
+       }
+
+       return fi;
+}
+
+/* Hold the property lock before calling this function */
+static void
+m365_store_save_setup_folder_locked (CamelM365Store *m365_store,
+                                    GHashTable *save_setup,
+                                    guint32 folder_type, /* one of TYPE constants from CamelFolderInfoFlags 
*/
+                                    const gchar *property_name)
+{
+       gchar *folder_id;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE (m365_store));
+       g_return_if_fail (save_setup != NULL);
+       g_return_if_fail (folder_type != 0);
+       g_return_if_fail (property_name != NULL);
+
+       folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store->priv->summary, folder_type);
+
+       if (folder_id) {
+               gchar *fullname;
+
+               fullname = camel_m365_store_summary_dup_folder_full_name (m365_store->priv->summary, 
folder_id);
+
+               if (fullname && *fullname) {
+                       g_hash_table_insert (save_setup,
+                               g_strdup (property_name),
+                               fullname);
+
+                       fullname = NULL;
+               }
+
+               g_free (fullname);
+               g_free (folder_id);
+       }
+}
+
+static gboolean
+m365_store_initial_setup_with_connection_sync (CamelStore *store,
+                                              GHashTable *save_setup,
+                                              EM365Connection *cnc,
+                                              GCancellable *cancellable,
+                                              GError **error)
+{
+       CamelM365Store *m365_store;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), FALSE);
+
+       if (g_cancellable_set_error_if_cancelled (cancellable, error))
+               return FALSE;
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       if (cnc) {
+               g_object_ref (cnc);
+       } else {
+               if (!camel_m365_store_ensure_connected (m365_store, &cnc, cancellable, error))
+                       return FALSE;
+
+               g_return_val_if_fail (cnc != NULL, FALSE);
+       }
+
+       if (!m365_store_read_default_folders (m365_store, cnc, cancellable, error)) {
+               g_clear_object (&cnc);
+               return FALSE;
+       }
+
+       if (save_setup) {
+               LOCK (m365_store);
+
+               m365_store_save_setup_folder_locked (m365_store, save_setup, CAMEL_FOLDER_TYPE_SENT, 
CAMEL_STORE_SETUP_SENT_FOLDER);
+               m365_store_save_setup_folder_locked (m365_store, save_setup, CAMEL_FOLDER_TYPE_DRAFTS, 
CAMEL_STORE_SETUP_DRAFTS_FOLDER);
+               m365_store_save_setup_folder_locked (m365_store, save_setup, CAMEL_FOLDER_TYPE_ARCHIVE, 
CAMEL_STORE_SETUP_ARCHIVE_FOLDER);
+
+               UNLOCK (m365_store);
+       }
+
+       g_clear_object (&cnc);
+
+       return TRUE;
+}
+
+static gboolean
+m365_store_initial_setup_sync (CamelStore *store,
+                              GHashTable *save_setup,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       return m365_store_initial_setup_with_connection_sync (store, save_setup, NULL, cancellable, error);
+}
+
+static CamelFolder *
+m365_store_get_trash_folder_sync (CamelStore *store,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       CamelM365Store *m365_store;
+       CamelFolder *folder = NULL;
+       gchar *folder_id, *folder_name;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), NULL);
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       LOCK (m365_store);
+
+       folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store->priv->summary, 
CAMEL_FOLDER_TYPE_TRASH);
+
+       if (!folder_id) {
+               UNLOCK (m365_store);
+               g_set_error_literal (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER, _("Could not 
locate Trash folder"));
+               return NULL;
+       }
+
+       folder_name = camel_m365_store_summary_dup_folder_full_name (m365_store->priv->summary, folder_id);
+
+       UNLOCK (m365_store);
+
+       folder = camel_store_get_folder_sync (store, folder_name, 0, cancellable, error);
+
+       g_free (folder_name);
+       g_free (folder_id);
+
+       if (folder) {
+               GPtrArray *folders;
+               gboolean can = TRUE;
+               guint ii;
+
+               /* Save content of all opened folders, thus any messages deleted in them
+                  are moved to the Deleted Items folder first, thus in case of the trash
+                  folder instance being used to expunge messages will contain all of them.
+               */
+               folders = camel_store_dup_opened_folders (store);
+
+               for (ii = 0; ii < folders->len; ii++) {
+                       CamelFolder *secfolder = folders->pdata[ii];
+
+                       if (secfolder != folder && can)
+                               can = camel_folder_synchronize_sync (secfolder, FALSE, cancellable, NULL);
+
+                       g_object_unref (secfolder);
+               }
+               g_ptr_array_free (folders, TRUE);
+
+               /* To return 'Deleted Items' folder with current content,
+                  not with possibly stale locally cached copy. */
+               camel_folder_refresh_info_sync (folder, cancellable, NULL);
+       }
+
+       return folder;
+}
+
+static CamelFolder *
+m365_store_get_junk_folder_sync (CamelStore *store,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       CamelM365Store *m365_store;
+       CamelFolder *folder = NULL;
+       gchar *folder_id, *folder_name;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (store), NULL);
+
+       m365_store = CAMEL_M365_STORE (store);
+
+       folder_id = camel_m365_store_summary_dup_folder_id_for_type (m365_store->priv->summary, 
CAMEL_FOLDER_TYPE_JUNK);
+
+       if (!folder_id) {
+               g_set_error_literal (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER, _("Could not 
locate Junk folder"));
+               return NULL;
+       }
+
+       folder_name = camel_m365_store_summary_dup_folder_full_name (m365_store->priv->summary, folder_id);
+
+       folder = camel_store_get_folder_sync (store, folder_name, 0, cancellable, error);
+
+       g_free (folder_name);
+       g_free (folder_id);
+
+       return folder;
+}
+
+static gboolean
+m365_store_can_refresh_folder (CamelStore *store,
+                              CamelFolderInfo *info,
+                              GError **error)
+{
+       CamelSettings *settings;
+       CamelM365Settings *m365_settings;
+       gboolean check_all;
+
+       /* Skip unselectable folders from automatic refresh */
+       if (info && (info->flags & CAMEL_FOLDER_NOSELECT) != 0)
+               return FALSE;
+
+       settings = camel_service_ref_settings (CAMEL_SERVICE (store));
+
+       m365_settings = CAMEL_M365_SETTINGS (settings);
+       check_all = camel_m365_settings_get_check_all (m365_settings);
+
+       g_object_unref (settings);
+
+       if (check_all)
+               return TRUE;
+
+       /* Delegate decision to parent class */
+       return CAMEL_STORE_CLASS (camel_m365_store_parent_class)->can_refresh_folder (store, info, error);
+}
+
+static gboolean
+m365_store_folder_is_subscribed (CamelSubscribable *subscribable,
+                                const gchar *folder_name)
+{
+       CamelM365Store *m365_store = CAMEL_M365_STORE (subscribable);
+
+       return camel_m365_store_summary_has_full_name (m365_store->priv->summary, folder_name);
+}
+
+static gboolean
+m365_store_subscribe_folder_sync (CamelSubscribable *subscribable,
+                                 const gchar *folder_name,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       return TRUE;
+}
+
+static gboolean
+m365_store_unsubscribe_folder_sync (CamelSubscribable *subscribable,
+                                   const gchar *folder_name,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       return TRUE;
+}
+
+static void
+m365_store_set_property (GObject *object,
+                        guint property_id,
+                        const GValue *value,
+                        GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CONNECTABLE:
+                       camel_network_service_set_connectable (
+                               CAMEL_NETWORK_SERVICE (object),
+                               g_value_get_object (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_store_get_property (GObject *object,
+                        guint property_id,
+                        GValue *value,
+                        GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CONNECTABLE:
+                       g_value_take_object (
+                               value,
+                               camel_network_service_ref_connectable (
+                                       CAMEL_NETWORK_SERVICE (object)));
+                       return;
+               case PROP_HOST_REACHABLE:
+                       g_value_set_boolean (
+                               value,
+                               camel_network_service_get_host_reachable (
+                                       CAMEL_NETWORK_SERVICE (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_store_dispose (GObject *object)
+{
+       CamelM365Store *m365_store = CAMEL_M365_STORE (object);
+
+       LOCK (m365_store);
+
+       if (m365_store->priv->summary) {
+               m365_store_save_summary (m365_store->priv->summary, G_STRFUNC);
+               g_clear_object (&m365_store->priv->summary);
+       }
+
+       g_clear_object (&m365_store->priv->cnc);
+
+       UNLOCK (m365_store);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_store_parent_class)->dispose (object);
+}
+
+static void
+m365_store_finalize (GObject *object)
+{
+       CamelM365Store *m365_store;
+
+       m365_store = CAMEL_M365_STORE (object);
+
+       g_rec_mutex_clear (&m365_store->priv->property_lock);
+       g_hash_table_destroy (m365_store->priv->default_folders);
+       g_free (m365_store->priv->storage_path);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_store_parent_class)->finalize (object);
+}
+
+static void
+camel_m365_store_class_init (CamelM365StoreClass *class)
+{
+       GObjectClass *object_class;
+       CamelServiceClass *service_class;
+       CamelStoreClass *store_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = m365_store_set_property;
+       object_class->get_property = m365_store_get_property;
+       object_class->dispose = m365_store_dispose;
+       object_class->finalize = m365_store_finalize;
+
+       /* Inherited from CamelNetworkService */
+       g_object_class_override_property (
+               object_class,
+               PROP_CONNECTABLE,
+               "connectable");
+
+       /* Inherited from CamelNetworkService */
+       g_object_class_override_property (
+               object_class,
+               PROP_HOST_REACHABLE,
+               "host-reachable");
+
+       service_class = CAMEL_SERVICE_CLASS (class);
+       service_class->settings_type = CAMEL_TYPE_M365_SETTINGS;
+       service_class->query_auth_types_sync = m365_store_query_auth_types_sync;
+       service_class->get_name = m365_store_get_name;
+       service_class->connect_sync = m365_store_connect_sync;
+       service_class->disconnect_sync = m365_store_disconnect_sync;
+       service_class->authenticate_sync = m365_store_authenticate_sync;
+
+       store_class = CAMEL_STORE_CLASS (class);
+       store_class->get_folder_sync = m365_store_get_folder_sync;
+       store_class->create_folder_sync = m365_store_create_folder_sync;
+       store_class->delete_folder_sync = m365_store_delete_folder_sync;
+       store_class->rename_folder_sync = m365_store_rename_folder_sync;
+       store_class->get_folder_info_sync = m365_store_get_folder_info_sync;
+       store_class->initial_setup_sync = m365_store_initial_setup_sync;
+       store_class->get_trash_folder_sync = m365_store_get_trash_folder_sync;
+       store_class->get_junk_folder_sync = m365_store_get_junk_folder_sync;
+       store_class->can_refresh_folder = m365_store_can_refresh_folder;
+}
+
+static void
+camel_m365_store_initable_init (GInitableIface *iface)
+{
+       parent_initable_interface = g_type_interface_peek_parent (iface);
+
+       iface->init = m365_store_initable_init;
+}
+
+static void
+camel_m365_subscribable_init (CamelSubscribableInterface *iface)
+{
+       iface->folder_is_subscribed = m365_store_folder_is_subscribed;
+       iface->subscribe_folder_sync = m365_store_subscribe_folder_sync;
+       iface->unsubscribe_folder_sync = m365_store_unsubscribe_folder_sync;
+}
+
+static void
+camel_m365_store_init (CamelM365Store *m365_store)
+{
+       m365_store->priv = camel_m365_store_get_instance_private (m365_store);
+
+       g_rec_mutex_init (&m365_store->priv->property_lock);
+       m365_store->priv->default_folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+CamelM365StoreSummary *
+camel_m365_store_ref_store_summary (CamelM365Store *m365_store)
+{
+       CamelM365StoreSummary *summary;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (m365_store), NULL);
+
+       LOCK (m365_store);
+
+       summary = m365_store->priv->summary;
+
+       if (summary)
+               g_object_ref (summary);
+
+       UNLOCK (m365_store);
+
+       return summary;
+}
+
+EM365Connection *
+camel_m365_store_ref_connection (CamelM365Store *m365_store)
+{
+       EM365Connection *cnc = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (m365_store), NULL);
+
+       LOCK (m365_store);
+
+       if (m365_store->priv->cnc)
+               cnc = g_object_ref (m365_store->priv->cnc);
+
+       UNLOCK (m365_store);
+
+       return cnc;
+}
+
+gboolean
+camel_m365_store_ensure_connected (CamelM365Store *m365_store,
+                                  EM365Connection **out_cnc, /* out, nullable, transfer full */
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_STORE (m365_store), FALSE);
+
+       if (!camel_offline_store_get_online (CAMEL_OFFLINE_STORE (m365_store))) {
+               g_set_error_literal (error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_UNAVAILABLE,
+                       _("You must be working online to complete this operation"));
+
+               return FALSE;
+       }
+
+       if (!camel_service_connect_sync ((CamelService *) m365_store, cancellable, error))
+               return FALSE;
+
+       if (out_cnc) {
+               *out_cnc = camel_m365_store_ref_connection (m365_store);
+
+               if (!*out_cnc) {
+                       g_set_error_literal (error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_UNAVAILABLE,
+                               _("You must be working online to complete this operation"));
+
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+void
+camel_m365_store_maybe_disconnect (CamelM365Store *m365_store,
+                                  const GError *error)
+{
+       CamelService *service;
+
+       g_return_if_fail (CAMEL_IS_M365_STORE (m365_store));
+
+       if (!error)
+               return;
+
+       service = CAMEL_SERVICE (m365_store);
+
+       if (camel_service_get_connection_status (service) != CAMEL_SERVICE_CONNECTED)
+               return;
+
+#if 0
+       if (g_error_matches (error, M365_CONNECTION_ERROR, M365_CONNECTION_ERROR_NORESPONSE) ||
+           g_error_matches (error, M365_CONNECTION_ERROR, M365_CONNECTION_ERROR_AUTHENTICATION_FAILED))
+               camel_service_disconnect_sync (service, FALSE, NULL, NULL);
+#endif
+}
+
+void
+camel_m365_store_connect_folder_summary (CamelM365Store *m365_store,
+                                        CamelFolderSummary *folder_summary)
+{
+       g_return_if_fail (CAMEL_IS_M365_STORE (m365_store));
+       g_return_if_fail (CAMEL_IS_FOLDER_SUMMARY (folder_summary));
+
+       LOCK (m365_store);
+
+       if (m365_store->priv->summary)
+               camel_m365_store_summary_connect_folder_summary (m365_store->priv->summary, folder_summary);
+
+       UNLOCK (m365_store);
+}
diff --git a/src/Microsoft365/camel/camel-m365-store.h b/src/Microsoft365/camel/camel-m365-store.h
new file mode 100644
index 00000000..c20d207c
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-store.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_STORE_H
+#define CAMEL_M365_STORE_H
+
+#include <camel/camel.h>
+
+#include "common/e-m365-connection.h"
+#include "camel-m365-store-summary.h"
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_STORE \
+       (camel_m365_store_get_type ())
+#define CAMEL_M365_STORE(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_STORE, CamelM365Store))
+#define CAMEL_M365_STORE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_STORE, CamelM365StoreClass))
+#define CAMEL_IS_M365_STORE(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_STORE))
+#define CAMEL_IS_M365_STORE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_STORE))
+#define CAMEL_M365_STORE_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_STORE, CamelM365StoreClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelM365Store CamelM365Store;
+typedef struct _CamelM365StoreClass CamelM365StoreClass;
+typedef struct _CamelM365StorePrivate CamelM365StorePrivate;
+
+struct _CamelM365Store {
+       CamelOfflineStore parent;
+       CamelM365StorePrivate *priv;
+};
+
+struct _CamelM365StoreClass {
+       CamelOfflineStoreClass parent_class;
+};
+
+GType          camel_m365_store_get_type       (void);
+
+CamelM365StoreSummary *
+               camel_m365_store_ref_store_summary
+                                               (CamelM365Store *m365_store);
+EM365Connection *
+               camel_m365_store_ref_connection (CamelM365Store *m365_store);
+gboolean       camel_m365_store_ensure_connected
+                                               (CamelM365Store *m365_store,
+                                                EM365Connection **out_cnc, /* out, nullable, transfer full */
+                                                GCancellable *cancellable,
+                                                GError **error);
+void           camel_m365_store_maybe_disconnect
+                                               (CamelM365Store *m365_store,
+                                                const GError *error);
+void           camel_m365_store_connect_folder_summary
+                                               (CamelM365Store *m365_store,
+                                                CamelFolderSummary *folder_summary);
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_STORE_H */
diff --git a/src/Microsoft365/camel/camel-m365-transport.c b/src/Microsoft365/camel/camel-m365-transport.c
new file mode 100644
index 00000000..fb12f412
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-transport.c
@@ -0,0 +1,400 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+
+#include <glib/gi18n-lib.h>
+
+#include <libemail-engine/libemail-engine.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+#include "camel-m365-store.h"
+#include "camel-m365-utils.h"
+
+#include "camel-m365-transport.h"
+
+#define LOCK(_transport) g_mutex_lock (&_transport->priv->property_lock)
+#define UNLOCK(_transport) g_mutex_unlock (&_transport->priv->property_lock)
+
+struct _CamelM365TransportPrivate
+{
+       GMutex property_lock;
+       EM365Connection *cnc;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelM365Transport, camel_m365_transport, CAMEL_TYPE_TRANSPORT)
+
+static gboolean
+m365_transport_is_server_side_sent_folder (CamelService *service,
+                                          GCancellable *cancellable)
+{
+       CamelSession *session;
+       ESourceRegistry *registry;
+       ESource *sibling, *source = NULL;
+       gboolean is_server_side = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_M365_TRANSPORT (service), FALSE);
+
+       session = camel_service_ref_session (service);
+
+       if (session && E_IS_MAIL_SESSION (session))
+               registry = g_object_ref (e_mail_session_get_registry (E_MAIL_SESSION (session)));
+       else
+               registry = e_source_registry_new_sync (cancellable, NULL);
+
+       if (!registry) {
+               g_clear_object (&session);
+               return FALSE;
+       }
+
+       sibling = e_source_registry_ref_source (registry, camel_service_get_uid (service));
+
+       if (sibling) {
+               GList *sources, *siter;
+
+               sources = e_source_registry_list_sources (registry, E_SOURCE_EXTENSION_MAIL_SUBMISSION);
+
+               for (siter = sources; siter; siter = siter->next) {
+                       source = siter->data;
+
+                       if (!source || g_strcmp0 (e_source_get_parent (source), e_source_get_parent 
(sibling)) != 0 ||
+                           !e_source_has_extension (source, E_SOURCE_EXTENSION_MAIL_SUBMISSION) ||
+                           !e_source_has_extension (source, E_SOURCE_EXTENSION_MAIL_COMPOSITION))
+                               source = NULL;
+                       else
+                               break;
+               }
+
+               if (source &&
+                   e_source_has_extension (source, E_SOURCE_EXTENSION_MAIL_SUBMISSION) &&
+                   e_source_has_extension (source, E_SOURCE_EXTENSION_MAIL_COMPOSITION)) {
+                       ESourceMailSubmission *subm_extension;
+                       CamelStore *store = NULL;
+                       gchar *folder_name = NULL;
+
+                       subm_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_MAIL_SUBMISSION);
+
+                       /* Copy messages on the server side only if the replies
+                          might not be saved to the original folder, which is handled
+                          by the evolution itself. */
+                       if (!e_source_mail_submission_get_replies_to_origin_folder (subm_extension) &&
+                           e_source_mail_submission_get_sent_folder (subm_extension) &&
+                           e_mail_folder_uri_parse (session,
+                               e_source_mail_submission_get_sent_folder (subm_extension),
+                               &store, &folder_name, NULL) && CAMEL_IS_M365_STORE (store)) {
+                               CamelM365Store *m365_store = CAMEL_M365_STORE (store);
+                               CamelM365StoreSummary *m365_store_summary;
+                               gchar *folder_id_str;
+
+                               m365_store_summary = camel_m365_store_ref_store_summary (m365_store);
+                               folder_id_str = camel_m365_store_summary_dup_folder_id_for_full_name 
(m365_store_summary, folder_name);
+                               if (folder_id_str && *folder_id_str) {
+                                       guint32 flags;
+
+                                       flags = camel_m365_store_summary_get_folder_flags 
(m365_store_summary, folder_id_str);
+
+                                       if ((flags & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_SENT) {
+                                               is_server_side = TRUE;
+                                       }
+                               }
+
+                               g_clear_object (&m365_store_summary);
+                               g_free (folder_id_str);
+                       }
+
+                       g_clear_object (&store);
+                       g_free (folder_name);
+               }
+
+               g_list_free_full (sources, g_object_unref);
+               g_object_unref (sibling);
+       }
+
+       g_object_unref (registry);
+       g_clear_object (&session);
+
+       return is_server_side;
+}
+
+static EM365Connection *
+m365_transport_ref_connection (CamelM365Transport *m365_transport)
+{
+       EM365Connection *cnc = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_M365_TRANSPORT (m365_transport), NULL);
+
+       LOCK (m365_transport);
+
+       if (m365_transport->priv->cnc)
+               cnc = g_object_ref (m365_transport->priv->cnc);
+
+       UNLOCK (m365_transport);
+
+       return cnc;
+}
+
+static gboolean
+m365_transport_connect_sync (CamelService *service,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       CamelM365Transport *m365_transport;
+       EM365Connection *cnc;
+       gboolean success = FALSE;
+
+       /* Chain up to parent's method. */
+       if (!CAMEL_SERVICE_CLASS (camel_m365_transport_parent_class)->connect_sync (service, cancellable, 
error))
+               return FALSE;
+
+       if (camel_service_get_connection_status (service) == CAMEL_SERVICE_DISCONNECTED)
+               return FALSE;
+
+       m365_transport = CAMEL_M365_TRANSPORT (service);
+       cnc = m365_transport_ref_connection (m365_transport);
+
+       if (!cnc) {
+               cnc = camel_m365_utils_new_connection (service, NULL);
+
+               if (cnc) {
+                       LOCK (m365_transport);
+
+                       m365_transport->priv->cnc = g_object_ref (cnc);
+
+                       UNLOCK (m365_transport);
+               }
+       }
+
+       if (cnc) {
+               CamelSession *session;
+
+               session = camel_service_ref_session (service);
+
+               success = camel_session_authenticate_sync (session, service, "Microsoft365", cancellable, 
error);
+
+               g_clear_object (&session);
+               g_clear_object (&cnc);
+       } else {
+               g_set_error_literal (error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_UNAVAILABLE, _("Failed 
to create connection"));
+       }
+
+       return success;
+}
+
+static gboolean
+m365_transport_disconnect_sync (CamelService *service,
+                               gboolean clean,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       CamelM365Transport *m365_transport = CAMEL_M365_TRANSPORT (service);
+       EM365Connection *cnc;
+       gboolean success = TRUE;
+
+       cnc = m365_transport_ref_connection (m365_transport);
+
+       if (cnc) {
+               success = e_m365_connection_disconnect_sync (cnc, cancellable, error);
+
+               g_clear_object (&cnc);
+       }
+
+       if (!success)
+               return FALSE;
+
+       /* Chain up to parent's method. */
+       return CAMEL_SERVICE_CLASS (camel_m365_transport_parent_class)->disconnect_sync (service, clean, 
cancellable, error);
+}
+
+static CamelAuthenticationResult
+m365_transport_authenticate_sync (CamelService *service,
+                                 const gchar *mechanism,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       CamelAuthenticationResult result;
+       CamelM365Transport *m365_transport;
+       EM365Connection *cnc;
+
+       m365_transport = CAMEL_M365_TRANSPORT (service);
+       cnc = m365_transport_ref_connection (m365_transport);
+
+       if (!cnc)
+               return CAMEL_AUTHENTICATION_ERROR;
+
+       switch (e_m365_connection_authenticate_sync (cnc, NULL, E_M365_FOLDER_KIND_MAIL, NULL, NULL, NULL, 
NULL, cancellable, error)) {
+       case E_SOURCE_AUTHENTICATION_ERROR:
+       case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+       default:
+               result = CAMEL_AUTHENTICATION_ERROR;
+               break;
+       case E_SOURCE_AUTHENTICATION_ACCEPTED:
+               result = CAMEL_AUTHENTICATION_ACCEPTED;
+               break;
+       case E_SOURCE_AUTHENTICATION_REJECTED:
+       case E_SOURCE_AUTHENTICATION_REQUIRED:
+               result = CAMEL_AUTHENTICATION_REJECTED;
+               break;
+       }
+
+       g_clear_object (&cnc);
+
+       return result;
+}
+
+static gchar *
+m365_transport_get_name (CamelService *service,
+                        gboolean brief)
+{
+       gchar *name;
+
+       if (brief)
+               name = g_strdup (_("Microsoft 365 server"));
+       else
+               name = g_strdup (_("Mail delivery via Microsoft 365"));
+
+       return name;
+}
+
+static gboolean
+m365_send_to_sync (CamelTransport *transport,
+                  CamelMimeMessage *message,
+                  CamelAddress *from,
+                  CamelAddress *recipients,
+                  gboolean *out_sent_message_saved,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+       CamelInternetAddress *use_from;
+       CamelService *service;
+       EM365Connection *cnc;
+       JsonBuilder *builder;
+       gchar *appended_id = NULL;
+       gboolean is_server_side_sent;
+       gboolean success = FALSE;
+
+       service = CAMEL_SERVICE (transport);
+
+       if (CAMEL_IS_INTERNET_ADDRESS (from))
+               use_from = CAMEL_INTERNET_ADDRESS (from);
+       else
+               use_from = camel_mime_message_get_from (message);
+
+       if (!use_from || camel_address_length (CAMEL_ADDRESS (use_from)) == 0) {
+               g_set_error_literal (
+                       error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
+                       _("Cannot send message with no From address"));
+               return FALSE;
+
+       } else if (camel_address_length (CAMEL_ADDRESS (use_from)) > 1) {
+               g_set_error_literal (
+                       error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
+                       _("Microsoft 365 server cannot send message with multiple From addresses"));
+               return FALSE;
+
+       } else {
+               const gchar *used_email = NULL;
+
+               if (!camel_internet_address_get (use_from, 0, NULL, &used_email)) {
+                       g_set_error_literal (
+                               error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
+                               _("Failed to read From address"));
+                       return FALSE;
+               }
+       }
+
+       cnc = m365_transport_ref_connection (CAMEL_M365_TRANSPORT (service));
+
+       if (!cnc) {
+               g_set_error_literal (
+                       error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_NOT_CONNECTED,
+                       _("Service not connected"));
+               return FALSE;
+       }
+
+       is_server_side_sent = m365_transport_is_server_side_sent_folder (service, cancellable);
+
+       if (is_server_side_sent && out_sent_message_saved)
+               *out_sent_message_saved = TRUE;
+
+       builder = json_builder_new_immutable ();
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_begin_object_member (builder, "message");
+
+       success = camel_m365_utils_fill_message_object_sync (builder, message, NULL, from, recipients, TRUE, 
NULL, cancellable, error);
+
+       e_m365_json_end_object_member (builder); /* message */
+
+       if (!is_server_side_sent)
+               e_m365_json_add_boolean_member (builder, "saveToSentItems", FALSE);
+
+       e_m365_json_end_object_member (builder);
+
+       success = success && e_m365_connection_send_mail_sync (cnc, NULL, builder, cancellable, error);
+
+       g_object_unref (cnc);
+       g_free (appended_id);
+
+       return success;
+}
+
+static void
+m365_transport_dispose (GObject *object)
+{
+       CamelM365Transport *m365_transport = CAMEL_M365_TRANSPORT (object);
+
+       LOCK (m365_transport);
+
+       g_clear_object (&m365_transport->priv->cnc);
+
+       UNLOCK (m365_transport);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_transport_parent_class)->dispose (object);
+}
+
+static void
+m365_transport_finalize (GObject *object)
+{
+       CamelM365Transport *m365_transport = CAMEL_M365_TRANSPORT (object);
+
+       g_mutex_clear (&m365_transport->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_transport_parent_class)->finalize (object);
+}
+
+static void
+camel_m365_transport_class_init (CamelM365TransportClass *class)
+{
+       GObjectClass *object_class;
+       CamelServiceClass *service_class;
+       CamelTransportClass *transport_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->dispose = m365_transport_dispose;
+       object_class->finalize = m365_transport_finalize;
+
+       service_class = CAMEL_SERVICE_CLASS (class);
+       service_class->settings_type = CAMEL_TYPE_M365_SETTINGS;
+       service_class->get_name = m365_transport_get_name;
+       service_class->connect_sync = m365_transport_connect_sync;
+       service_class->disconnect_sync = m365_transport_disconnect_sync;
+       service_class->authenticate_sync = m365_transport_authenticate_sync;
+
+       transport_class = CAMEL_TRANSPORT_CLASS (class);
+       transport_class->send_to_sync = m365_send_to_sync;
+}
+
+static void
+camel_m365_transport_init (CamelM365Transport *m365_transport)
+{
+       m365_transport->priv = camel_m365_transport_get_instance_private (m365_transport);
+
+       g_mutex_init (&m365_transport->priv->property_lock);
+}
diff --git a/src/Microsoft365/camel/camel-m365-transport.h b/src/Microsoft365/camel/camel-m365-transport.h
new file mode 100644
index 00000000..f25c5856
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-transport.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_TRANSPORT_H
+#define CAMEL_M365_TRANSPORT_H
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_TRANSPORT \
+       (camel_m365_transport_get_type ())
+#define CAMEL_M365_TRANSPORT(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_TRANSPORT, CamelM365Transport))
+#define CAMEL_M365_TRANSPORT_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_TRANSPORT, CamelM365TransportClass))
+#define CAMEL_IS_M365_TRANSPORT(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_TRANSPORT))
+#define CAMEL_IS_M365_TRANSPORT_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_TRANSPORT))
+#define CAMEL_M365_TRANSPORT_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_TRANSPORT, CamelM365TransportClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelM365Transport CamelM365Transport;
+typedef struct _CamelM365TransportClass CamelM365TransportClass;
+typedef struct _CamelM365TransportPrivate CamelM365TransportPrivate;
+
+struct _CamelM365Transport {
+       CamelTransport parent;
+       CamelM365TransportPrivate *priv;
+};
+
+struct _CamelM365TransportClass {
+       CamelTransportClass parent_class;
+};
+
+GType camel_m365_transport_get_type (void);
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_TRANSPORT_H */
diff --git a/src/Microsoft365/camel/camel-m365-utils.c b/src/Microsoft365/camel/camel-m365-utils.c
new file mode 100644
index 00000000..9d496693
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-utils.c
@@ -0,0 +1,1004 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <libemail-engine/libemail-engine.h>
+
+#include "camel-m365-utils.h"
+
+/* Unref with g_object_unref() when done with it */
+static ESource *
+camel_m365_utils_ref_corresponding_source (CamelService *service,
+                                          GCancellable *cancellable)
+{
+       ESourceRegistry *registry = NULL;
+       CamelSession *session;
+       ESource *source = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_SERVICE (service), NULL);
+
+       session = camel_service_ref_session (service);
+       if (E_IS_MAIL_SESSION (session)) {
+               registry = e_mail_session_get_registry (E_MAIL_SESSION (session));
+               if (registry)
+                       g_object_ref (registry);
+       }
+
+       g_clear_object (&session);
+
+       if (!registry)
+               registry = e_source_registry_new_sync (cancellable, NULL);
+
+       if (registry) {
+               source = e_source_registry_ref_source (registry, camel_service_get_uid (service));
+
+               if (source) {
+                       ESource *parent;
+
+                       parent = e_source_registry_find_extension (registry, source, 
E_SOURCE_EXTENSION_COLLECTION);
+
+                       g_clear_object (&source);
+                       source = parent;
+               }
+       }
+
+       g_clear_object (&registry);
+
+       return source;
+}
+
+EM365Connection *
+camel_m365_utils_new_connection (CamelService *service,
+                                GCancellable *cancellable)
+{
+       CamelSettings *settings;
+       EM365Connection *cnc;
+       ESource *source;
+
+       g_return_val_if_fail (CAMEL_IS_SERVICE (service), NULL);
+
+       source = camel_m365_utils_ref_corresponding_source (service, cancellable);
+
+       if (!source)
+               return NULL;
+
+       settings = camel_service_ref_settings (service);
+
+       cnc = e_m365_connection_new (source, CAMEL_M365_SETTINGS (settings));
+
+       e_binding_bind_property (
+               service, "proxy-resolver",
+               cnc, "proxy-resolver",
+               G_BINDING_SYNC_CREATE);
+
+       g_clear_object (&settings);
+       g_clear_object (&source);
+
+       return cnc;
+}
+
+/* From Outlook name (which allows spaces) to Evolution name */
+gchar *
+camel_m365_utils_encode_category_name (const gchar *name)
+{
+       if (name && strchr (name, ' ')) {
+               GString *str;
+
+               str = g_string_sized_new (strlen (name) + 16);
+
+               while (*name) {
+                       if (*name == '_')
+                               g_string_append_c (str, '_');
+
+                       g_string_append_c (str, *name == ' ' ? '_' : *name);
+
+                       name++;
+               }
+
+               return g_string_free (str, FALSE);
+       }
+
+       return g_strdup (name);
+}
+
+/* From Evolution name to Outlook name (which allows spaces) */
+gchar *
+camel_m365_utils_decode_category_name (const gchar *flag)
+{
+       if (flag && strchr (flag, '_')) {
+               GString *str = g_string_sized_new (strlen (flag));
+
+               while (*flag) {
+                       if (*flag == '_') {
+                               if (flag[1] == '_') {
+                                       g_string_append_c (str, '_');
+                                       flag++;
+                               } else {
+                                       g_string_append_c (str, ' ');
+                               }
+                       } else {
+                               g_string_append_c (str, *flag);
+                       }
+
+                       flag++;
+               }
+
+               return g_string_free (str, FALSE);
+       }
+
+       return g_strdup (flag);
+}
+
+gboolean
+camel_m365_utils_is_system_user_flag (const gchar *name)
+{
+       if (!name)
+               return FALSE;
+
+       return g_str_equal (name, "receipt-handled") ||
+               g_str_equal (name, "$has-cal");
+}
+
+const gchar *
+camel_m365_utils_rename_label (const gchar *cat,
+                              gboolean from_cat)
+{
+       gint ii;
+
+       /* This is a mapping from Outlook categories to
+        * Evolution labels based on the standard colors */
+       const gchar *labels[] = {
+               "Red category", "$Labelimportant",
+               "Orange category", "$Labelwork",
+               "Green category", "$Labelpersonal",
+               "Blue category", "$Labeltodo",
+               "Purple category", "$Labellater",
+               NULL, NULL
+       };
+
+       if (!cat || !*cat)
+               return "";
+
+       for (ii = 0; labels[ii]; ii += 2) {
+               if (from_cat) {
+                       if (!g_ascii_strcasecmp (cat, labels[ii]))
+                               return labels[ii + 1];
+               } else {
+                       if (!g_ascii_strcasecmp (cat, labels[ii + 1]))
+                               return labels[ii];
+               }
+       }
+
+       return cat;
+}
+
+static void
+m365_utils_add_address (JsonBuilder *builder,
+                       CamelInternetAddress *addr,
+                       void (* add_func) (JsonBuilder *builder,
+                                          const gchar *name,
+                                          const gchar *address))
+{
+       const gchar *name = NULL, *address = NULL;
+
+       g_return_if_fail (add_func != NULL);
+
+       if (!addr || camel_address_length (CAMEL_ADDRESS (addr)) < 1)
+               return;
+
+       if (camel_internet_address_get (addr, 0, &name, &address))
+               add_func (builder, name, address);
+}
+
+static void
+m365_utils_add_address_array (JsonBuilder *builder,
+                             CamelInternetAddress *addr,
+                             void (* begin_func) (JsonBuilder *builder),
+                             void (* end_func) (JsonBuilder *builder),
+                             GHashTable *known_recipients,
+                             CamelAddress *expected_recipients)
+{
+       gint ii, len;
+       gboolean did_add = FALSE;
+
+       g_return_if_fail (begin_func != NULL);
+       g_return_if_fail (end_func != NULL);
+
+       if (!addr)
+               return;
+
+       len = camel_address_length (CAMEL_ADDRESS (addr));
+
+       for (ii = 0; ii < len; ii++) {
+               const gchar *name = NULL, *address = NULL;
+
+               if (camel_internet_address_get (addr, 0, &name, &address)) {
+                       if (!did_add) {
+                               did_add = TRUE;
+                               begin_func (builder);
+                       }
+
+                       if (known_recipients && address && *address)
+                               g_hash_table_add (known_recipients, (gpointer) address);
+
+                       e_m365_add_recipient (builder, NULL, name, address);
+               }
+       }
+
+       if (known_recipients && expected_recipients && CAMEL_IS_INTERNET_ADDRESS (expected_recipients)) {
+               CamelInternetAddress *iaddr = CAMEL_INTERNET_ADDRESS (expected_recipients);
+
+               len = camel_address_length (expected_recipients);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *name = NULL, *address = NULL;
+
+                       if (camel_internet_address_get (iaddr, 0, &name, &address) && address && *address &&
+                           !g_hash_table_contains (known_recipients, address)) {
+                               if (!did_add) {
+                                       did_add = TRUE;
+                                       begin_func (builder);
+                               }
+
+                               if (known_recipients && address && *address)
+                                       g_hash_table_add (known_recipients, (gpointer) address);
+
+                               e_m365_add_recipient (builder, NULL, name, address);
+                       }
+               }
+       }
+
+       if (did_add)
+               end_func (builder);
+}
+
+static void
+m365_utils_add_headers (JsonBuilder *builder,
+                       const CamelNameValueArray *headers,
+                       CamelInternetAddress **out_sender,
+                       gboolean *out_request_read_receipt)
+{
+       guint ii, len;
+       gint did_add = 0;
+
+       if (!headers)
+               return;
+
+       len = camel_name_value_array_get_length (headers);
+
+       for (ii = 0; ii < len; ii++) {
+               const gchar *name = NULL, *value = NULL;
+
+               if (camel_name_value_array_get (headers, ii, &name, &value) && name && *name && value &&
+                   g_ascii_strcasecmp (name, "X-Evolution-Source") != 0) {
+                       /* The Graph API allows only X- headers to be saved */
+                       if (g_ascii_strncasecmp (name, "X-", 2) == 0) {
+                               if (!did_add)
+                                       e_m365_mail_message_begin_internet_message_headers (builder);
+
+                               did_add++;
+
+                               /* Preserve only the first five... (see the comment at 
m365_folder_append_message_sync()) */
+                               if (did_add < 5)
+                                       e_m365_add_internet_message_header (builder, name, value);
+                       }
+
+                       if (out_sender && g_ascii_strcasecmp (name, "Sender") == 0) {
+                               CamelInternetAddress *addr;
+
+                               addr = camel_internet_address_new ();
+
+                               if (camel_address_decode (CAMEL_ADDRESS (addr), value) != -1) {
+                                       *out_sender = addr;
+                                       addr = NULL;
+                               }
+
+                               g_clear_object (&addr);
+
+                               /* To not compare the header name again */
+                               out_sender = NULL;
+                       } else if (out_request_read_receipt && g_ascii_strcasecmp (name, 
"Disposition-Notification-To") == 0) {
+                               *out_request_read_receipt = TRUE;
+
+                               /* To not compare the header name again */
+                               out_request_read_receipt = NULL;
+                       }
+               }
+       }
+
+       if (did_add)
+               e_m365_mail_message_end_internet_message_headers (builder);
+}
+
+static CamelStream *
+m365_utils_get_content_stream (CamelMimePart *part,
+                              gssize *out_wrote_bytes,
+                              GCancellable *cancellable)
+{
+       CamelStream *content_stream;
+       CamelStream *filter_stream = NULL;
+       CamelMimeFilterWindows *windows = NULL;
+       CamelDataWrapper *dw;
+       gssize wrote_bytes;
+
+       g_return_val_if_fail (part != NULL, NULL);
+
+       dw = camel_medium_get_content (CAMEL_MEDIUM (part));
+       g_return_val_if_fail (dw != NULL, NULL);
+
+       content_stream = camel_stream_mem_new ();
+
+       if (camel_mime_part_get_content_type (part)) {
+               const gchar *charset = camel_content_type_param (camel_mime_part_get_content_type (part), 
"charset");
+
+               if (charset && *charset && g_ascii_strcasecmp (charset, "utf8") != 0 && g_ascii_strcasecmp 
(charset, "utf-8") != 0) {
+                       if (g_ascii_strncasecmp (charset, "iso-8859-", 9) == 0) {
+                               CamelStream *null;
+
+                               /* Since a few Windows mailers like to claim they sent
+                                * out iso-8859-# encoded text when they really sent
+                                * out windows-cp125#, do some simple sanity checking
+                                * before we move on... */
+
+                               null = camel_stream_null_new ();
+                               filter_stream = camel_stream_filter_new (null);
+                               g_object_unref (null);
+
+                               windows = (CamelMimeFilterWindows *)camel_mime_filter_windows_new (charset);
+                               camel_stream_filter_add (CAMEL_STREAM_FILTER (filter_stream), 
CAMEL_MIME_FILTER (windows));
+
+                               camel_data_wrapper_decode_to_stream_sync (dw, CAMEL_STREAM (filter_stream), 
cancellable, NULL);
+                               camel_stream_flush (CAMEL_STREAM (filter_stream), cancellable, NULL);
+                               g_object_unref (filter_stream);
+
+                               charset = camel_mime_filter_windows_real_charset (windows);
+                       }
+
+                       if (charset && *charset) {
+                               CamelMimeFilter *filter;
+
+                               filter_stream = camel_stream_filter_new (content_stream);
+
+                               if ((filter = camel_mime_filter_charset_new (charset, "UTF-8"))) {
+                                       camel_stream_filter_add (CAMEL_STREAM_FILTER (filter_stream), 
CAMEL_MIME_FILTER (filter));
+                                       g_object_unref (filter);
+                               } else {
+                                       g_object_unref (filter_stream);
+                                       filter_stream = NULL;
+                               }
+                       }
+               }
+       }
+
+       if (filter_stream) {
+               wrote_bytes = camel_data_wrapper_decode_to_stream_sync (dw, CAMEL_STREAM (filter_stream), 
cancellable, NULL);
+               camel_stream_flush (filter_stream, cancellable, NULL);
+               g_object_unref (filter_stream);
+       } else {
+               wrote_bytes = camel_data_wrapper_decode_to_stream_sync (dw, CAMEL_STREAM (content_stream), 
cancellable, NULL);
+       }
+
+       if (windows)
+               g_object_unref (windows);
+
+       g_seekable_seek (G_SEEKABLE (content_stream), 0, G_SEEK_SET, NULL, NULL);
+
+       if (out_wrote_bytes)
+               *out_wrote_bytes = wrote_bytes;
+
+       return content_stream;
+}
+
+static gboolean
+m365_utils_part_is_attachment (CamelMimePart *part,
+                              gboolean *out_is_inline)
+{
+       const CamelContentDisposition *content_disposition;
+
+       g_return_val_if_fail (CAMEL_IS_MIME_PART (part), FALSE);
+
+       content_disposition = camel_mime_part_get_content_disposition (part);
+
+       if (!content_disposition)
+               return FALSE;
+
+       if (out_is_inline) {
+               *out_is_inline = content_disposition && content_disposition->disposition &&
+                               g_ascii_strcasecmp (content_disposition->disposition, "inline") == 0;
+       }
+
+       return content_disposition &&
+               content_disposition->disposition && (
+               g_ascii_strcasecmp (content_disposition->disposition, "attachment") == 0 ||
+               g_ascii_strcasecmp (content_disposition->disposition, "inline") == 0);
+}
+
+enum {
+       ADD_ATTACHMENT_WITH_CONTENT_TYPE = 1 << 0,
+       ADD_ATTACHMENT_PREFIX_CONTENT_TYPE_HEADER = 1 << 1,
+       ADD_ATTACHMENT_DECODE_CONTENT = 1 << 2
+};
+
+static void
+m365_utils_add_file_attachment_content (JsonBuilder *builder,
+                                       CamelDataWrapper *dw,
+                                       guint32 add_flags,
+                                       GCancellable *cancellable)
+{
+       CamelMimeFilter *filter;
+       CamelStream *content_stream, *filter_stream;
+       GByteArray *data;
+       CamelContentType *ct;
+       gchar *content_type_str;
+       const gchar *content_id;
+       gboolean is_inline = FALSE;
+
+       ct = camel_data_wrapper_get_mime_type_field (dw);
+       content_type_str = camel_content_type_format (ct);
+
+       if ((add_flags & ADD_ATTACHMENT_WITH_CONTENT_TYPE) != 0)
+               e_m365_attachment_add_content_type (builder, content_type_str);
+
+       content_stream = camel_stream_mem_new ();
+       filter_stream = camel_stream_filter_new (content_stream);
+
+       filter = camel_mime_filter_basic_new (CAMEL_MIME_FILTER_BASIC_BASE64_ENC);
+       camel_stream_filter_add (CAMEL_STREAM_FILTER (filter_stream), filter);
+       g_object_unref (filter);
+
+       if ((add_flags & ADD_ATTACHMENT_PREFIX_CONTENT_TYPE_HEADER) != 0) {
+               gchar *content_type_unfolded;
+
+               content_type_unfolded = camel_header_unfold (content_type_str);
+
+               #define wstr(str) camel_stream_write (filter_stream, str, strlen (str), cancellable, NULL)
+               wstr ("Content-Type: ");
+               wstr (content_type_unfolded);
+               wstr ("\r\n\r\n");
+               #undef wstr
+
+               g_free (content_type_unfolded);
+       }
+
+       g_free (content_type_str);
+
+       if (CAMEL_IS_MIME_PART (dw)) {
+               CamelMimePart *part = CAMEL_MIME_PART (dw);
+
+               content_id = camel_mime_part_get_content_id (part);
+               if (content_id)
+                       e_m365_file_attachment_add_content_id (builder, content_id);
+
+               if (m365_utils_part_is_attachment (part, &is_inline) && is_inline)
+                       e_m365_attachment_add_is_inline (builder, TRUE);
+
+               dw = camel_medium_get_content (CAMEL_MEDIUM (part));
+       }
+
+       if ((add_flags & ADD_ATTACHMENT_DECODE_CONTENT) != 0)
+               camel_data_wrapper_decode_to_stream_sync (dw, filter_stream, cancellable, NULL);
+       else
+               camel_data_wrapper_write_to_stream_sync (dw, filter_stream, cancellable, NULL);
+       camel_stream_flush (filter_stream, cancellable, NULL);
+       g_object_unref (filter_stream);
+
+       camel_stream_flush (content_stream, cancellable, NULL);
+
+       data = camel_stream_mem_get_byte_array (CAMEL_STREAM_MEM (content_stream));
+
+       /* Ensure the string is NUL-terminated */
+       g_byte_array_append (data, (const guchar *) "\0", 1);
+
+       e_m365_file_attachment_add_content_bytes (builder, (const gchar *) data->data);
+
+       g_object_unref (content_stream);
+}
+
+static void
+m365_utils_add_file_attachment (JsonBuilder *builder,
+                               CamelDataWrapper *dw,
+                               GCancellable *cancellable)
+{
+       const gchar *filename = NULL;
+
+       g_return_if_fail (builder != NULL);
+       g_return_if_fail (dw != NULL);
+
+       m365_utils_add_file_attachment_content (builder, dw, ADD_ATTACHMENT_WITH_CONTENT_TYPE | 
ADD_ATTACHMENT_DECODE_CONTENT, cancellable);
+
+       if (CAMEL_IS_MIME_PART (dw))
+               filename = camel_mime_part_get_filename (CAMEL_MIME_PART (dw));
+
+       if (filename)
+               e_m365_attachment_add_name (builder, filename);
+       else
+               e_m365_attachment_add_name (builder, "attachment.dat");
+}
+
+static void
+m365_utils_add_smime_encrypted_attachment (JsonBuilder *builder,
+                                          CamelDataWrapper *dw,
+                                          GCancellable *cancellable)
+{
+       g_return_if_fail (builder != NULL);
+       g_return_if_fail (dw != NULL);
+
+       e_m365_attachment_add_name (builder, "smime.p7m");
+
+       m365_utils_add_file_attachment_content (builder, dw, ADD_ATTACHMENT_WITH_CONTENT_TYPE | 
ADD_ATTACHMENT_DECODE_CONTENT, cancellable);
+}
+
+static void
+m365_utils_add_smime_signed_attachment (JsonBuilder *builder,
+                                       CamelDataWrapper *dw,
+                                       GCancellable *cancellable)
+{
+       e_m365_attachment_add_content_type (builder, "multipart/signed");
+       e_m365_attachment_add_name (builder, "smime.txt");
+
+       m365_utils_add_file_attachment_content (builder, dw, ADD_ATTACHMENT_PREFIX_CONTENT_TYPE_HEADER, 
cancellable);
+}
+
+static gboolean
+m365_utils_do_smime_signed (CamelMultipart *multipart,
+                           CamelMimePart **out_body_part,
+                           GSList **out_attachments,
+                           GCancellable *cancellable)
+{
+       CamelMimePart *content, *signature;
+       /*CamelContentType *ct;*/
+
+       content = camel_multipart_get_part (multipart, CAMEL_MULTIPART_SIGNED_CONTENT);
+       signature = camel_multipart_get_part (multipart, CAMEL_MULTIPART_SIGNED_SIGNATURE);
+
+       g_return_val_if_fail (content != NULL, FALSE);
+       g_return_val_if_fail (signature != NULL, FALSE);
+
+       /*ct = camel_mime_part_get_content_type (content);
+
+       if (camel_content_type_is (ct, "text", "plain")) {
+               g_clear_object (out_body_part);
+               *out_body_part = g_object_ref (content);
+       } else if (camel_content_type_is (ct, "text", "html")) {
+               g_clear_object (out_body_part);
+               *out_body_part = g_object_ref (content);
+       } else {
+               *out_attachments = g_slist_prepend (*out_attachments, g_object_ref (content));
+       }*/
+
+       *out_attachments = g_slist_prepend (*out_attachments, g_object_ref (multipart));
+
+       return TRUE;
+}
+
+static gboolean
+m365_utils_do_multipart (CamelMultipart *mp,
+                        gboolean *is_first,
+                        CamelMimePart **out_body_part,
+                        GSList **out_attachments,
+                        GCancellable *cancellable)
+{
+       CamelDataWrapper *dw;
+       CamelContentType *type;
+       CamelMimePart *part;
+       gboolean parent_is_alternative;
+       gint nn, ii;
+
+       g_return_val_if_fail (is_first != NULL, FALSE);
+
+       type = camel_data_wrapper_get_mime_type_field (CAMEL_DATA_WRAPPER (mp));
+       parent_is_alternative = type && camel_content_type_is (type, "multipart", "alternative");
+
+       nn = camel_multipart_get_number (mp);
+
+       for (ii = 0; ii < nn; ii++) {
+               part = camel_multipart_get_part (mp, ii);
+
+               if (!part)
+                       continue;
+
+               dw = camel_medium_get_content (CAMEL_MEDIUM (part));
+
+               if (CAMEL_IS_MULTIPART (dw)) {
+                       if (!m365_utils_do_multipart (CAMEL_MULTIPART (dw), is_first, out_body_part, 
out_attachments, cancellable))
+                               return FALSE;
+                       continue;
+               }
+
+               type = camel_mime_part_get_content_type (part);
+
+               if (ii == 0 && (*is_first) && camel_content_type_is (type, "text", "plain")) {
+                       g_clear_object (out_body_part);
+                       *out_body_part = g_object_ref (part);
+
+                       *is_first = FALSE;
+               } else if ((ii == 0 || parent_is_alternative) &&
+                          camel_content_type_is (type, "text", "html") &&
+                          !m365_utils_part_is_attachment (part, NULL)) {
+                       g_clear_object (out_body_part);
+                       *out_body_part = g_object_ref (part);
+               } else {
+                       *out_attachments = g_slist_prepend (*out_attachments, g_object_ref (part));
+               }
+       }
+
+       return TRUE;
+}
+
+static CamelMimePart *
+m365_utils_get_body_part (CamelMimeMessage *message,
+                         GSList **out_attachments,
+                         GCancellable *cancellable)
+{
+       CamelContentType *ct;
+       CamelMimePart *body_part = NULL;
+
+       ct = camel_data_wrapper_get_mime_type_field (CAMEL_DATA_WRAPPER (message));
+       g_return_val_if_fail (ct != NULL, NULL);
+
+       if (camel_content_type_is (ct, "application", "x-pkcs7-mime") ||
+           camel_content_type_is (ct, "application", "pkcs7-mime")) {
+               *out_attachments = g_slist_prepend (*out_attachments, g_object_ref (message));
+       } else {
+               CamelDataWrapper *dw = NULL;
+               CamelMultipart *multipart;
+
+               /* contents body */
+               dw = camel_medium_get_content (CAMEL_MEDIUM (message));
+
+               if (CAMEL_IS_MULTIPART (dw)) {
+                       gboolean is_first = TRUE;
+
+                       multipart = CAMEL_MULTIPART (dw);
+                       if (CAMEL_IS_MULTIPART_SIGNED (multipart) && camel_multipart_get_number (multipart) 
== 2) {
+                               m365_utils_do_smime_signed (multipart, &body_part, out_attachments, 
cancellable);
+                       } else {
+                               m365_utils_do_multipart (multipart, &is_first, &body_part, out_attachments, 
cancellable);
+                       }
+               } else if (dw) {
+                       CamelContentType *type;
+                       CamelMimePart *part = CAMEL_MIME_PART (message);
+
+                       type = camel_data_wrapper_get_mime_type_field (dw);
+
+                       if (camel_content_type_is (type, "text", "plain")) {
+                               body_part = g_object_ref (part);
+                       } else if (camel_content_type_is (type, "text", "html")) {
+                               body_part = g_object_ref (part);
+                       } else {
+                               *out_attachments = g_slist_prepend (*out_attachments, g_object_ref (part));
+                       }
+               }
+       }
+
+       *out_attachments = g_slist_reverse (*out_attachments);
+
+       return body_part;
+}
+
+void
+camel_m365_utils_add_message_flags (JsonBuilder *builder,
+                                   CamelMessageInfo *info,
+                                   CamelMimeMessage *message)
+{
+       guint32 flags = 0;
+
+       if (info) {
+               const CamelNamedFlags *user_flags;
+               gboolean did_add = FALSE;
+               guint ii, len;
+
+               flags = camel_message_info_get_flags (info);
+
+               user_flags = camel_message_info_get_user_flags (info);
+               len = camel_named_flags_get_length (user_flags);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *name = camel_named_flags_get (user_flags, ii);
+
+                       if (!camel_m365_utils_is_system_user_flag (name)) {
+                               const gchar *renamed;
+
+                               renamed = camel_m365_utils_rename_label (name, FALSE);
+
+                               if (renamed && *renamed && renamed != name) {
+                                       if (!did_add) {
+                                               did_add = TRUE;
+                                               e_m365_mail_message_begin_categories (builder);
+                                       }
+
+                                       e_m365_mail_message_add_category (builder, renamed);
+                               } else if (renamed == name && name && *name) {
+                                       gchar *cat;
+
+                                       cat = camel_m365_utils_decode_category_name (name);
+
+                                       if (cat && *cat) {
+                                               if (!did_add) {
+                                                       did_add = TRUE;
+                                                       e_m365_mail_message_begin_categories (builder);
+                                               }
+
+                                               e_m365_mail_message_add_category (builder, cat);
+                                       }
+
+                                       g_free (cat);
+                               }
+                       }
+               }
+
+               if (did_add)
+                       e_m365_mail_message_end_categories (builder);
+       }
+
+       if (message && !(flags & CAMEL_MESSAGE_FLAGGED)) {
+               CamelMedium *medium = CAMEL_MEDIUM (message);
+               const gchar *value;
+
+               value = camel_medium_get_header (medium, "X-Priority");
+
+               if (g_strcmp0 (value, "1") == 0) {
+                       flags |= CAMEL_MESSAGE_FLAGGED;
+               } else {
+                       value = camel_medium_get_header (medium, "Importance");
+
+                       if (value && g_ascii_strcasecmp (value, "High") == 0)
+                               flags |= CAMEL_MESSAGE_FLAGGED;
+               }
+       }
+
+       e_m365_mail_message_add_importance (builder,
+               (flags & CAMEL_MESSAGE_FLAGGED) != 0 ? E_M365_IMPORTANCE_HIGH : E_M365_IMPORTANCE_NORMAL);
+
+       e_m365_mail_message_add_is_read (builder, (flags & CAMEL_MESSAGE_SEEN) != 0);
+}
+
+static void
+m365_utils_add_attachment_object (JsonBuilder *builder,
+                                 CamelDataWrapper *dw,
+                                 GCancellable *cancellable)
+{
+       CamelContentType *ct;
+
+       ct = camel_data_wrapper_get_mime_type_field (dw);
+
+       e_m365_attachment_begin_attachment (builder, E_M365_ATTACHMENT_DATA_TYPE_FILE);
+
+       if (camel_content_type_is (ct, "application", "x-pkcs7-mime") ||
+           camel_content_type_is (ct, "application", "pkcs7-mime")) {
+               m365_utils_add_smime_encrypted_attachment (builder, dw, cancellable);
+       } else if (CAMEL_IS_MULTIPART_SIGNED (dw)) {
+               m365_utils_add_smime_signed_attachment (builder, dw, cancellable);
+       } else {
+               m365_utils_add_file_attachment (builder, dw, cancellable);
+       }
+
+       e_m365_json_end_object_member (builder);
+}
+
+gboolean
+camel_m365_utils_fill_message_object_sync (JsonBuilder *builder,
+                                          CamelMimeMessage *message,
+                                          CamelMessageInfo *info,
+                                          CamelAddress *override_from,
+                                          CamelAddress *override_recipients, /* it merges them, not really 
override */
+                                          gboolean is_send,
+                                          GSList **out_attachments,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       CamelInternetAddress *addr, *sender = NULL;
+       CamelMimePart *body_part;
+       GHashTable *known_recipients = NULL;
+       GSList *attachments = NULL;
+       time_t tt;
+       gint offset = 0;
+       const gchar *tmp;
+       gboolean success = TRUE, request_read_receipt = FALSE;
+
+       g_return_val_if_fail (builder != NULL, FALSE);
+       g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), FALSE);
+
+       tmp = camel_mime_message_get_message_id (message);
+       if (tmp && *tmp)
+               e_m365_mail_message_add_internet_message_id (builder, tmp);
+
+       tmp = camel_mime_message_get_subject (message);
+       e_m365_mail_message_add_subject (builder, tmp ? tmp : "");
+
+       tt = camel_mime_message_get_date (message, &offset);
+
+       if (tt > (time_t) 0) {
+               /* Convert to UTC */
+               tt += (offset / 100) * 60 * 60;
+               tt += (offset % 100) * 60;
+
+               e_m365_mail_message_add_sent_date_time (builder, tt);
+       }
+
+       offset = 0;
+       tt = camel_mime_message_get_date_received (message, &offset);
+
+       if (tt > (time_t) 0) {
+               /* Convert to UTC */
+               tt += (offset / 100) * 60 * 60;
+               tt += (offset % 100) * 60;
+
+               e_m365_mail_message_add_received_date_time (builder, tt);
+       }
+
+       if (override_recipients)
+               known_recipients = g_hash_table_new (camel_strcase_hash, camel_strcase_equal);
+
+       if (override_from && CAMEL_IS_INTERNET_ADDRESS (override_from))
+               addr = CAMEL_INTERNET_ADDRESS (override_from);
+       else
+               addr = camel_mime_message_get_from (message);
+       m365_utils_add_address (builder, addr, e_m365_mail_message_add_from);
+
+       addr = camel_mime_message_get_reply_to (message);
+       m365_utils_add_address_array (builder, addr, e_m365_mail_message_begin_reply_to, 
e_m365_mail_message_end_reply_to, NULL, NULL);
+
+       addr = camel_mime_message_get_recipients (message, CAMEL_RECIPIENT_TYPE_TO);
+       m365_utils_add_address_array (builder, addr, e_m365_mail_message_begin_to_recipients, 
e_m365_mail_message_end_to_recipients, known_recipients, NULL);
+
+       addr = camel_mime_message_get_recipients (message, CAMEL_RECIPIENT_TYPE_CC);
+       m365_utils_add_address_array (builder, addr, e_m365_mail_message_begin_cc_recipients, 
e_m365_mail_message_end_cc_recipients, known_recipients, NULL);
+
+       addr = camel_mime_message_get_recipients (message, CAMEL_RECIPIENT_TYPE_BCC);
+       m365_utils_add_address_array (builder, addr, e_m365_mail_message_begin_bcc_recipients, 
e_m365_mail_message_end_bcc_recipients, known_recipients, override_recipients);
+
+       if (known_recipients) {
+               g_hash_table_destroy (known_recipients);
+               known_recipients = NULL;
+       }
+
+       m365_utils_add_headers (builder, camel_medium_get_headers (CAMEL_MEDIUM (message)), &sender, 
&request_read_receipt);
+
+       if (sender) {
+               const gchar *name = NULL, *address = NULL;
+
+               if (camel_internet_address_get (sender, 0, &name, &address) && ((name && *name) || (address 
&& *address)))
+                       e_m365_mail_message_add_sender (builder, name, address);
+
+               g_clear_object (&sender);
+       } else if (override_from) {
+               /* Possibly force the Sender when the passed-in From doesn't match the account user address */
+       }
+
+       if (request_read_receipt)
+               e_m365_mail_message_add_is_read_receipt_requested (builder, TRUE);
+
+       body_part = m365_utils_get_body_part (message, &attachments, cancellable);
+
+       if (body_part) {
+               CamelContentType *ct;
+               EM365ItemBodyContentTypeType m365_content_type = E_M365_ITEM_BODY_CONTENT_TYPE_UNKNOWN;
+
+               ct = camel_mime_part_get_content_type (body_part);
+
+               if (ct && camel_content_type_is (ct, "text", "html"))
+                       m365_content_type = E_M365_ITEM_BODY_CONTENT_TYPE_HTML;
+               else if (ct && camel_content_type_is (ct, "text", "plain"))
+                       m365_content_type = E_M365_ITEM_BODY_CONTENT_TYPE_TEXT;
+
+               if (m365_content_type != E_M365_ITEM_BODY_CONTENT_TYPE_UNKNOWN) {
+                       CamelStream *mem;
+                       gssize wrote = -1;
+
+                       mem = m365_utils_get_content_stream (body_part, &wrote, cancellable);
+
+                       if (mem && wrote >= 0) {
+                               GByteArray *byte_array;
+
+                               camel_stream_flush (mem, cancellable, NULL);
+
+                               byte_array = camel_stream_mem_get_byte_array (CAMEL_STREAM_MEM (mem));
+
+                               /* Ensure the string is NUL-terminated */
+                               g_byte_array_append (byte_array, (const guchar *) "\0", 1);
+
+                               e_m365_mail_message_add_body (builder, m365_content_type, (const gchar *) 
byte_array->data);
+                       }
+
+                       g_clear_object (&mem);
+               }
+
+               g_object_unref (body_part);
+       } else {
+               e_m365_json_add_null_member (builder, "body");
+       }
+
+       if (info || is_send)
+               camel_m365_utils_add_message_flags (builder, info, is_send ? message : NULL);
+
+       if (out_attachments) {
+               *out_attachments = attachments;
+       } else if (attachments) {
+               GSList *link;
+
+               e_m365_json_begin_array_member (builder, "attachments");
+
+               for (link = attachments; link && success; link = g_slist_next (link)) {
+                       CamelDataWrapper *dw = link->data;
+
+                       m365_utils_add_attachment_object (builder, dw, cancellable);
+               }
+
+               e_m365_json_end_array_member (builder);
+
+               g_slist_free_full (attachments, g_object_unref);
+       }
+
+       return success;
+}
+
+gboolean
+camel_m365_utils_create_message_sync (EM365Connection *cnc,
+                                     const gchar *folder_id,
+                                     CamelMimeMessage *message,
+                                     CamelMessageInfo *info,
+                                     gchar **out_appended_id,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       EM365MailMessage *appended_message = NULL;
+       GSList *attachments = NULL;
+       JsonBuilder *builder;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), FALSE);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+
+       if (!camel_m365_utils_fill_message_object_sync (builder, message, info, NULL, NULL, FALSE, 
&attachments, cancellable, error)) {
+               g_slist_free_full (attachments, g_object_unref);
+               g_object_unref (builder);
+
+               return FALSE;
+       }
+
+       e_m365_json_end_object_member (builder);
+
+       success = e_m365_connection_create_mail_message_sync (cnc, NULL, folder_id, builder, 
&appended_message, cancellable, error);
+
+       g_warn_if_fail ((success && appended_message) || (!success && !appended_message));
+
+       g_object_unref (builder);
+
+       if (success && appended_message) {
+               GSList *link;
+               const gchar *message_id;
+
+               message_id = e_m365_mail_message_get_id (appended_message);
+
+               if (out_appended_id)
+                       *out_appended_id = g_strdup (message_id);
+
+               for (link = attachments; link && success; link = g_slist_next (link)) {
+                       CamelDataWrapper *dw = link->data;
+
+                       builder = json_builder_new_immutable ();
+
+                       m365_utils_add_attachment_object (builder, dw, cancellable);
+
+                       success = e_m365_connection_add_mail_message_attachment_sync (cnc, NULL, message_id, 
builder, NULL, cancellable, error);
+
+                       g_object_unref (builder);
+               }
+       }
+
+       g_slist_free_full (attachments, g_object_unref);
+
+       if (appended_message)
+               json_object_unref (appended_message);
+
+       return success;
+}
diff --git a/src/Microsoft365/camel/camel-m365-utils.h b/src/Microsoft365/camel/camel-m365-utils.h
new file mode 100644
index 00000000..aff72a3a
--- /dev/null
+++ b/src/Microsoft365/camel/camel-m365-utils.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_UTILS_H
+#define CAMEL_M365_UTILS_H
+
+#include <camel/camel.h>
+
+#include "common/e-m365-connection.h"
+
+EM365Connection *
+               camel_m365_utils_new_connection (CamelService *service,
+                                                GCancellable *cancellable);
+gchar *                camel_m365_utils_encode_category_name
+                                               (const gchar *name);
+gchar *                camel_m365_utils_decode_category_name
+                                               (const gchar *flag);
+gboolean       camel_m365_utils_is_system_user_flag
+                                               (const gchar *name);
+const gchar *  camel_m365_utils_rename_label   (const gchar *cat,
+                                                gboolean from_cat);
+void           camel_m365_utils_add_message_flags
+                                               (JsonBuilder *builder,
+                                                CamelMessageInfo *info,
+                                                CamelMimeMessage *message);
+gboolean       camel_m365_utils_fill_message_object_sync
+                                               (JsonBuilder *builder,
+                                                CamelMimeMessage *message,
+                                                CamelMessageInfo *info,
+                                                CamelAddress *override_from,
+                                                CamelAddress *override_recipients, /* it merges them, not 
really override */
+                                                gboolean is_send,
+                                                GSList **out_attachments,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       camel_m365_utils_create_message_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *folder_id,
+                                                CamelMimeMessage *message,
+                                                CamelMessageInfo *info,
+                                                gchar **out_appended_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+#endif /* CAMEL_M365_UTILS_H */
diff --git a/src/Microsoft365/camel/libcamelmicrosoft365.urls 
b/src/Microsoft365/camel/libcamelmicrosoft365.urls
new file mode 100644
index 00000000..a74a6fc6
--- /dev/null
+++ b/src/Microsoft365/camel/libcamelmicrosoft365.urls
@@ -0,0 +1 @@
+microsoft365
diff --git a/src/Microsoft365/common/CMakeLists.txt b/src/Microsoft365/common/CMakeLists.txt
new file mode 100644
index 00000000..671ae495
--- /dev/null
+++ b/src/Microsoft365/common/CMakeLists.txt
@@ -0,0 +1,72 @@
+glib_mkenums(e-m365-enumtypes e-m365-enums.h E_M365_ENUMTYPES_H)
+
+set(SOURCES
+       camel-sasl-xoauth2-microsoft365.c
+       camel-sasl-xoauth2-microsoft365.h
+       camel-m365-settings.c
+       camel-m365-settings.h
+       e-m365-connection.c
+       e-m365-connection.h
+       e-m365-enums.h
+       e-m365-json-utils.c
+       e-m365-json-utils.h
+       e-m365-tz-utils.c
+       e-m365-tz-utils.h
+       e-oauth2-service-microsoft365.c
+       e-oauth2-service-microsoft365.h
+       e-source-m365-folder.c
+       e-source-m365-folder.h
+       ${CMAKE_CURRENT_BINARY_DIR}/e-m365-enumtypes.c
+       ${CMAKE_CURRENT_BINARY_DIR}/e-m365-enumtypes.h
+)
+
+add_library(evolution-microsoft365 SHARED
+       ${SOURCES}
+)
+
+target_compile_definitions(evolution-microsoft365 PRIVATE
+       -DG_LOG_DOMAIN=\"evolution-microsoft365\"
+       -DM365_DATADIR=\"${ewsdatadir}\"
+)
+
+target_compile_options(evolution-microsoft365 PUBLIC
+       ${CAMEL_CFLAGS}
+       ${EVOLUTION_CALENDAR_CFLAGS}
+       ${JSON_GLIB_CFLAGS}
+       ${LIBEBACKEND_CFLAGS}
+       ${LIBECAL_CFLAGS}
+       ${LIBEDATACAL_CFLAGS}
+       ${LIBEDATASERVER_CFLAGS}
+       ${SOUP_CFLAGS}
+)
+
+target_include_directories(evolution-microsoft365 PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_SOURCE_DIR}
+       ${CMAKE_BINARY_DIR}/src/Microsoft365
+       ${CMAKE_SOURCE_DIR}/src/Microsoft365
+       ${CMAKE_CURRENT_BINARY_DIR}
+       ${CAMEL_INCLUDE_DIRS}
+       ${EVOLUTION_CALENDAR_INCLUDE_DIRS}
+       ${JSON_GLIB_INCLUDE_DIRS}
+       ${LIBEBACKEND_INCLUDE_DIRS}
+       ${LIBECAL_INCLUDE_DIRS}
+       ${LIBEDATACAL_INCLUDE_DIRS}
+       ${LIBEDATASERVER_INCLUDE_DIRS}
+       ${SOUP_INCLUDE_DIRS}
+)
+
+target_link_libraries(evolution-microsoft365
+       ${CAMEL_LDFLAGS}
+       ${EVOLUTION_CALENDAR_LDFLAGS}
+       ${JSON_GLIB_LDFLAGS}
+       ${LIBEBACKEND_LDFLAGS}
+       ${LIBECAL_LDFLAGS}
+       ${LIBEDATACAL_LDFLAGS}
+       ${LIBEDATASERVER_LDFLAGS}
+       ${SOUP_LDFLAGS}
+)
+
+install(TARGETS evolution-microsoft365
+       DESTINATION ${privsolibdir}
+)
diff --git a/src/Microsoft365/common/camel-m365-settings.c b/src/Microsoft365/common/camel-m365-settings.c
new file mode 100644
index 00000000..daeac748
--- /dev/null
+++ b/src/Microsoft365/common/camel-m365-settings.c
@@ -0,0 +1,938 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <libebackend/libebackend.h>
+#include <libedataserver/libedataserver.h>
+
+#include "camel-m365-settings.h"
+
+struct _CamelM365SettingsPrivate {
+       GMutex property_lock;
+       gboolean use_impersonation;
+       gboolean check_all;
+       gboolean filter_junk;
+       gboolean filter_junk_inbox;
+       gboolean override_oauth2;
+       guint timeout;
+       guint concurrent_connections;
+       gchar *impersonate_user;
+       gchar *email;
+       gchar *oauth2_tenant;
+       gchar *oauth2_client_id;
+       gchar *oauth2_redirect_uri;
+};
+
+enum {
+       PROP_0,
+       PROP_AUTH_MECHANISM,
+       PROP_CHECK_ALL,
+       PROP_EMAIL,
+       PROP_FILTER_JUNK,
+       PROP_FILTER_JUNK_INBOX,
+       PROP_HOST,
+       PROP_PORT,
+       PROP_SECURITY_METHOD,
+       PROP_TIMEOUT,
+       PROP_USER,
+       PROP_USE_IMPERSONATION,
+       PROP_IMPERSONATE_USER,
+       PROP_OVERRIDE_OAUTH2,
+       PROP_OAUTH2_TENANT,
+       PROP_OAUTH2_CLIENT_ID,
+       PROP_OAUTH2_REDIRECT_URI,
+       PROP_CONCURRENT_CONNECTIONS
+};
+
+G_DEFINE_TYPE_WITH_CODE (CamelM365Settings, camel_m365_settings, CAMEL_TYPE_OFFLINE_SETTINGS,
+       G_IMPLEMENT_INTERFACE (CAMEL_TYPE_NETWORK_SETTINGS, NULL)
+       G_ADD_PRIVATE (CamelM365Settings))
+
+static void
+m365_settings_set_property (GObject *object,
+                           guint property_id,
+                           const GValue *value,
+                           GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_AUTH_MECHANISM:
+                       camel_network_settings_set_auth_mechanism (
+                               CAMEL_NETWORK_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_CHECK_ALL:
+                       camel_m365_settings_set_check_all (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_EMAIL:
+                       camel_m365_settings_set_email (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_FILTER_JUNK:
+                       camel_m365_settings_set_filter_junk (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_FILTER_JUNK_INBOX:
+                       camel_m365_settings_set_filter_junk_inbox (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_HOST:
+                       camel_network_settings_set_host (
+                               CAMEL_NETWORK_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_PORT:
+                       camel_network_settings_set_port (
+                               CAMEL_NETWORK_SETTINGS (object),
+                               g_value_get_uint (value));
+                       return;
+
+               case PROP_SECURITY_METHOD:
+                       camel_network_settings_set_security_method (
+                               CAMEL_NETWORK_SETTINGS (object),
+                               g_value_get_enum (value));
+                       return;
+
+               case PROP_TIMEOUT:
+                       camel_m365_settings_set_timeout (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_uint (value));
+                       return;
+
+               case PROP_USER:
+                       camel_network_settings_set_user (
+                               CAMEL_NETWORK_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_USE_IMPERSONATION:
+                       camel_m365_settings_set_use_impersonation (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_IMPERSONATE_USER:
+                       camel_m365_settings_set_impersonate_user (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_OVERRIDE_OAUTH2:
+                       camel_m365_settings_set_override_oauth2 (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_OAUTH2_TENANT:
+                       camel_m365_settings_set_oauth2_tenant (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_OAUTH2_CLIENT_ID:
+                       camel_m365_settings_set_oauth2_client_id (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_OAUTH2_REDIRECT_URI:
+                       camel_m365_settings_set_oauth2_redirect_uri (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_CONCURRENT_CONNECTIONS:
+                       camel_m365_settings_set_concurrent_connections (
+                               CAMEL_M365_SETTINGS (object),
+                               g_value_get_uint (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_settings_get_property (GObject *object,
+                           guint property_id,
+                           GValue *value,
+                           GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_AUTH_MECHANISM:
+                       g_value_take_string (
+                               value,
+                               camel_network_settings_dup_auth_mechanism (
+                               CAMEL_NETWORK_SETTINGS (object)));
+                       return;
+
+               case PROP_CHECK_ALL:
+                       g_value_set_boolean (
+                               value,
+                               camel_m365_settings_get_check_all (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_EMAIL:
+                       g_value_take_string (
+                               value,
+                               camel_m365_settings_dup_email (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_FILTER_JUNK:
+                       g_value_set_boolean (
+                               value,
+                               camel_m365_settings_get_filter_junk (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_FILTER_JUNK_INBOX:
+                       g_value_set_boolean (
+                               value,
+                               camel_m365_settings_get_filter_junk_inbox (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_HOST:
+                       g_value_take_string (
+                               value,
+                               camel_network_settings_dup_host (
+                               CAMEL_NETWORK_SETTINGS (object)));
+                       return;
+
+               case PROP_PORT:
+                       g_value_set_uint (
+                               value,
+                               camel_network_settings_get_port (
+                               CAMEL_NETWORK_SETTINGS (object)));
+                       return;
+
+               case PROP_SECURITY_METHOD:
+                       g_value_set_enum (
+                               value,
+                               camel_network_settings_get_security_method (
+                               CAMEL_NETWORK_SETTINGS (object)));
+                       return;
+
+               case PROP_TIMEOUT:
+                       g_value_set_uint (
+                               value,
+                               camel_m365_settings_get_timeout (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_USER:
+                       g_value_take_string (
+                               value,
+                               camel_network_settings_dup_user (
+                               CAMEL_NETWORK_SETTINGS (object)));
+                       return;
+
+               case PROP_USE_IMPERSONATION:
+                       g_value_set_boolean (
+                               value,
+                               camel_m365_settings_get_use_impersonation (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_IMPERSONATE_USER:
+                       g_value_take_string (
+                               value,
+                               camel_m365_settings_dup_impersonate_user (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_OVERRIDE_OAUTH2:
+                       g_value_set_boolean (
+                               value,
+                               camel_m365_settings_get_override_oauth2 (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_OAUTH2_TENANT:
+                       g_value_take_string (
+                               value,
+                               camel_m365_settings_dup_oauth2_tenant (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_OAUTH2_CLIENT_ID:
+                       g_value_take_string (
+                               value,
+                               camel_m365_settings_dup_oauth2_client_id (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_OAUTH2_REDIRECT_URI:
+                       g_value_take_string (
+                               value,
+                               camel_m365_settings_dup_oauth2_redirect_uri (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+
+               case PROP_CONCURRENT_CONNECTIONS:
+                       g_value_set_uint (
+                               value,
+                               camel_m365_settings_get_concurrent_connections (
+                               CAMEL_M365_SETTINGS (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_settings_finalize (GObject *object)
+{
+       CamelM365Settings *m365_settings = CAMEL_M365_SETTINGS (object);
+
+       g_mutex_clear (&m365_settings->priv->property_lock);
+
+       g_free (m365_settings->priv->email);
+       g_free (m365_settings->priv->oauth2_tenant);
+       g_free (m365_settings->priv->oauth2_client_id);
+       g_free (m365_settings->priv->oauth2_redirect_uri);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_m365_settings_parent_class)->finalize (object);
+}
+
+static void
+camel_m365_settings_class_init (CamelM365SettingsClass *class)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = m365_settings_set_property;
+       object_class->get_property = m365_settings_get_property;
+       object_class->finalize = m365_settings_finalize;
+
+       /* Inherited from CamelNetworkSettings. */
+       g_object_class_override_property (
+               object_class,
+               PROP_AUTH_MECHANISM,
+               "auth-mechanism");
+
+       g_object_class_install_property (
+               object_class,
+               PROP_CHECK_ALL,
+               g_param_spec_boolean (
+                       "check-all",
+                       "Check All",
+                       "Check all folders for new messages",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_EMAIL,
+               g_param_spec_string (
+                       "email",
+                       "Email",
+                       "Email",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_FILTER_JUNK,
+               g_param_spec_boolean (
+                       "filter-junk",
+                       "Filter Junk",
+                       "Whether to filter junk from all folders",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_FILTER_JUNK_INBOX,
+               g_param_spec_boolean (
+                       "filter-junk-inbox",
+                       "Filter Junk Inbox",
+                       "Whether to filter junk from Inbox only",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       /* Inherited from CamelNetworkSettings. */
+       g_object_class_override_property (
+               object_class,
+               PROP_HOST,
+               "host");
+
+       /* Inherited from CamelNetworkSettings. */
+       g_object_class_override_property (
+               object_class,
+               PROP_PORT,
+               "port");
+
+       g_object_class_install_property (
+               object_class,
+               PROP_TIMEOUT,
+               g_param_spec_uint (
+                       "timeout",
+                       "timeout",
+                       "Connection timeout in seconds",
+                       0, G_MAXUINT, 120,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       /* Inherited from CamelNetworkSettings. */
+       g_object_class_override_property (
+               object_class,
+               PROP_SECURITY_METHOD,
+               "security-method");
+
+       /* Inherited from CamelNetworkSettings. */
+       g_object_class_override_property (
+               object_class,
+               PROP_USER,
+               "user");
+
+       g_object_class_install_property (
+               object_class,
+               PROP_USE_IMPERSONATION,
+               g_param_spec_boolean (
+                       "use-impersonation",
+                       "Use Impersonation",
+                       "Use Impersonation",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_IMPERSONATE_USER,
+               g_param_spec_string (
+                       "impersonate-user",
+                       "Impersonate User",
+                       "Impersonate User",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_OVERRIDE_OAUTH2,
+               g_param_spec_boolean (
+                       "override-oauth2",
+                       "Override OAuth2",
+                       "Whether to override OAuth2 values",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_OAUTH2_TENANT,
+               g_param_spec_string (
+                       "oauth2-tenant",
+                       "OAuth2 Tenant",
+                       "OAuth2 Tenant to use, only if override-oauth2 is TRUE, otherwise the compile-time 
value is used",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_OAUTH2_CLIENT_ID,
+               g_param_spec_string (
+                       "oauth2-client-id",
+                       "OAuth2 Client ID",
+                       "OAuth2 Client-ID to use, only if override-oauth2 is TRUE, otherwise the compile-time 
value is used",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_OAUTH2_REDIRECT_URI,
+               g_param_spec_string (
+                       "oauth2-redirect-uri",
+                       "OAuth2 Redirect URI",
+                       "OAuth2 Redirect URI to use, only if override-oauth2 is TRUE, otherwise the 
compile-time value is used",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_CONCURRENT_CONNECTIONS,
+               g_param_spec_uint (
+                       "concurrent-connections",
+                       "Concurrent Connections",
+                       "Number of concurrent connections to use",
+                       MIN_CONCURRENT_CONNECTIONS,
+                       MAX_CONCURRENT_CONNECTIONS,
+                       1,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+}
+
+static void
+camel_m365_settings_init (CamelM365Settings *settings)
+{
+       settings->priv = camel_m365_settings_get_instance_private (settings);
+
+       g_mutex_init (&settings->priv->property_lock);
+}
+
+/* transfer none. Checks the settings from the ESource extension related to backend. */
+CamelM365Settings *
+camel_m365_settings_get_from_backend (struct _EBackend *backend,
+                                     struct _ESourceRegistry *registry)
+{
+       ESource *source;
+       ESource *collection;
+       ESourceCamel *extension;
+       CamelSettings *settings;
+       const gchar *extension_name;
+
+       g_return_val_if_fail (E_IS_BACKEND (backend), NULL);
+       if (registry)
+               g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL);
+
+       source = e_backend_get_source (backend);
+       extension_name = e_source_camel_get_extension_name ("microsoft365");
+       e_source_camel_generate_subtype ("microsoft365", CAMEL_TYPE_M365_SETTINGS);
+
+       if (registry) {
+               /* It's either in the 'source' or in the collection parent. */
+               collection = e_source_registry_find_extension (registry, source, extension_name);
+
+               g_return_val_if_fail (collection != NULL, NULL);
+       } else {
+               collection = g_object_ref (source);
+       }
+
+       extension = e_source_get_extension (collection, extension_name);
+       settings = e_source_camel_get_settings (extension);
+
+       g_object_unref (collection);
+
+       return CAMEL_M365_SETTINGS (settings);
+}
+
+gboolean
+camel_m365_settings_get_use_impersonation (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), FALSE);
+
+       return settings->priv->use_impersonation;
+}
+
+void
+camel_m365_settings_set_use_impersonation (CamelM365Settings *settings,
+                                          gboolean use_impersonation)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       if ((settings->priv->use_impersonation ? 1 : 0) == (use_impersonation ? 1 : 0))
+               return;
+
+       settings->priv->use_impersonation = use_impersonation;
+
+       g_object_notify (G_OBJECT (settings), "use-impersonation");
+}
+
+const gchar *
+camel_m365_settings_get_impersonate_user (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       return settings->priv->impersonate_user;
+}
+
+gchar *
+camel_m365_settings_dup_impersonate_user (CamelM365Settings *settings)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       protected = camel_m365_settings_get_impersonate_user (settings);
+       duplicate = g_strdup (protected);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       return duplicate;
+}
+
+void
+camel_m365_settings_set_impersonate_user (CamelM365Settings *settings,
+                                         const gchar *impersonate_user)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       if (g_strcmp0 (settings->priv->impersonate_user, impersonate_user) == 0) {
+               g_mutex_unlock (&settings->priv->property_lock);
+               return;
+       }
+
+       g_free (settings->priv->impersonate_user);
+       settings->priv->impersonate_user = e_util_strdup_strip (impersonate_user);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       g_object_notify (G_OBJECT (settings), "impersonate-user");
+}
+
+gboolean
+camel_m365_settings_get_check_all (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), FALSE);
+
+       return settings->priv->check_all;
+}
+
+void
+camel_m365_settings_set_check_all (CamelM365Settings *settings,
+                                  gboolean check_all)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       if ((settings->priv->check_all ? 1 : 0) == (check_all ? 1 : 0))
+               return;
+
+       settings->priv->check_all = check_all;
+
+       g_object_notify (G_OBJECT (settings), "check-all");
+}
+
+const gchar *
+camel_m365_settings_get_email (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       return settings->priv->email;
+}
+
+gchar *
+camel_m365_settings_dup_email (CamelM365Settings *settings)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       protected = camel_m365_settings_get_email (settings);
+       duplicate = g_strdup (protected);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       return duplicate;
+}
+
+void
+camel_m365_settings_set_email (CamelM365Settings *settings,
+                              const gchar *email)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       if (g_strcmp0 (settings->priv->email, email) == 0) {
+               g_mutex_unlock (&settings->priv->property_lock);
+               return;
+       }
+
+       g_free (settings->priv->email);
+       settings->priv->email = e_util_strdup_strip (email);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       g_object_notify (G_OBJECT (settings), "email");
+}
+
+gboolean
+camel_m365_settings_get_filter_junk (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), FALSE);
+
+       return settings->priv->filter_junk;
+}
+
+void
+camel_m365_settings_set_filter_junk (CamelM365Settings *settings,
+                                    gboolean filter_junk)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       if ((settings->priv->filter_junk ? 1 : 0) == (filter_junk ? 1 : 0))
+               return;
+
+       settings->priv->filter_junk = filter_junk;
+
+       g_object_notify (G_OBJECT (settings), "filter-junk");
+}
+
+gboolean
+camel_m365_settings_get_filter_junk_inbox (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), FALSE);
+
+       return settings->priv->filter_junk_inbox;
+}
+
+void
+camel_m365_settings_set_filter_junk_inbox (CamelM365Settings *settings,
+                                          gboolean filter_junk_inbox)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       if ((settings->priv->filter_junk_inbox ? 1 : 0) == (filter_junk_inbox ? 1 : 0))
+               return;
+
+       settings->priv->filter_junk_inbox = filter_junk_inbox;
+
+       g_object_notify (G_OBJECT (settings), "filter-junk-inbox");
+}
+
+guint
+camel_m365_settings_get_timeout (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), 0);
+
+       return settings->priv->timeout;
+}
+
+void
+camel_m365_settings_set_timeout (CamelM365Settings *settings,
+                                guint timeout)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       if (settings->priv->timeout == timeout)
+               return;
+
+       settings->priv->timeout = timeout;
+
+       g_object_notify (G_OBJECT (settings), "timeout");
+}
+
+gboolean
+camel_m365_settings_get_override_oauth2 (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), FALSE);
+
+       return settings->priv->override_oauth2;
+}
+
+void
+camel_m365_settings_set_override_oauth2 (CamelM365Settings *settings,
+                                        gboolean override_oauth2)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       if ((settings->priv->override_oauth2 ? 1 : 0) == (override_oauth2 ? 1 : 0))
+               return;
+
+       settings->priv->override_oauth2 = override_oauth2;
+
+       g_object_notify (G_OBJECT (settings), "override-oauth2");
+}
+
+const gchar *
+camel_m365_settings_get_oauth2_tenant (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       return settings->priv->oauth2_tenant;
+}
+
+gchar *
+camel_m365_settings_dup_oauth2_tenant (CamelM365Settings *settings)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       protected = camel_m365_settings_get_oauth2_tenant (settings);
+       duplicate = g_strdup (protected);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       return duplicate;
+}
+
+void
+camel_m365_settings_set_oauth2_tenant (CamelM365Settings *settings,
+                                      const gchar *tenant)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       if (g_strcmp0 (settings->priv->oauth2_tenant, tenant) == 0) {
+               g_mutex_unlock (&settings->priv->property_lock);
+               return;
+       }
+
+       g_free (settings->priv->oauth2_tenant);
+       settings->priv->oauth2_tenant = e_util_strdup_strip (tenant);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       g_object_notify (G_OBJECT (settings), "oauth2-tenant");
+}
+
+const gchar *
+camel_m365_settings_get_oauth2_client_id (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       return settings->priv->oauth2_client_id;
+}
+
+gchar *
+camel_m365_settings_dup_oauth2_client_id (CamelM365Settings *settings)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       protected = camel_m365_settings_get_oauth2_client_id (settings);
+       duplicate = g_strdup (protected);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       return duplicate;
+}
+
+void
+camel_m365_settings_set_oauth2_client_id (CamelM365Settings *settings,
+                                         const gchar *client_id)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       if (g_strcmp0 (settings->priv->oauth2_client_id, client_id) == 0) {
+               g_mutex_unlock (&settings->priv->property_lock);
+               return;
+       }
+
+       g_free (settings->priv->oauth2_client_id);
+       settings->priv->oauth2_client_id = e_util_strdup_strip (client_id);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       g_object_notify (G_OBJECT (settings), "oauth2-client-id");
+}
+
+const gchar *
+camel_m365_settings_get_oauth2_redirect_uri (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       return settings->priv->oauth2_redirect_uri;
+}
+
+gchar *
+camel_m365_settings_dup_oauth2_redirect_uri (CamelM365Settings *settings)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       protected = camel_m365_settings_get_oauth2_redirect_uri (settings);
+       duplicate = g_strdup (protected);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       return duplicate;
+}
+
+void
+camel_m365_settings_set_oauth2_redirect_uri (CamelM365Settings *settings,
+                                            const gchar *redirect_uri)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       g_mutex_lock (&settings->priv->property_lock);
+
+       if (g_strcmp0 (settings->priv->oauth2_redirect_uri, redirect_uri) == 0) {
+               g_mutex_unlock (&settings->priv->property_lock);
+               return;
+       }
+
+       g_free (settings->priv->oauth2_redirect_uri);
+       settings->priv->oauth2_redirect_uri = e_util_strdup_strip (redirect_uri);
+
+       g_mutex_unlock (&settings->priv->property_lock);
+
+       g_object_notify (G_OBJECT (settings), "oauth2-redirect-uri");
+}
+
+guint
+camel_m365_settings_get_concurrent_connections (CamelM365Settings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), 1);
+
+       return settings->priv->concurrent_connections;
+}
+
+void
+camel_m365_settings_set_concurrent_connections (CamelM365Settings *settings,
+                                               guint concurrent_connections)
+{
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+
+       concurrent_connections = CLAMP (
+               concurrent_connections,
+               MIN_CONCURRENT_CONNECTIONS,
+               MAX_CONCURRENT_CONNECTIONS);
+
+       if (settings->priv->concurrent_connections == concurrent_connections)
+               return;
+
+       settings->priv->concurrent_connections = concurrent_connections;
+
+       g_object_notify (G_OBJECT (settings), "concurrent-connections");
+}
diff --git a/src/Microsoft365/common/camel-m365-settings.h b/src/Microsoft365/common/camel-m365-settings.h
new file mode 100644
index 00000000..b2c6835f
--- /dev/null
+++ b/src/Microsoft365/common/camel-m365-settings.h
@@ -0,0 +1,126 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_M365_SETTINGS_H
+#define CAMEL_M365_SETTINGS_H
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_M365_SETTINGS \
+       (camel_m365_settings_get_type ())
+#define CAMEL_M365_SETTINGS(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_M365_SETTINGS, CamelM365Settings))
+#define CAMEL_M365_SETTINGS_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_M365_SETTINGS, CamelM365SettingsClass))
+#define CAMEL_IS_M365_SETTINGS(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_M365_SETTINGS))
+#define CAMEL_IS_M365_SETTINGS_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_M365_SETTINGS))
+#define CAMEL_M365_SETTINGS_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_M365_SETTINGS))
+
+#define MIN_CONCURRENT_CONNECTIONS 1
+#define MAX_CONCURRENT_CONNECTIONS 7
+
+G_BEGIN_DECLS
+
+/* Forward declaration, to not include libedataserver.h/libebackend.h here */
+struct _EBackend;
+struct _ESourceRegistry;
+
+typedef struct _CamelM365Settings CamelM365Settings;
+typedef struct _CamelM365SettingsClass CamelM365SettingsClass;
+typedef struct _CamelM365SettingsPrivate CamelM365SettingsPrivate;
+
+struct _CamelM365Settings {
+       CamelOfflineSettings parent;
+       CamelM365SettingsPrivate *priv;
+};
+
+struct _CamelM365SettingsClass {
+       CamelOfflineSettingsClass parent_class;
+};
+
+GType          camel_m365_settings_get_type    (void) G_GNUC_CONST;
+CamelM365Settings *
+               camel_m365_settings_get_from_backend
+                                               (struct _EBackend *backend,
+                                                struct _ESourceRegistry *registry);
+gboolean       camel_m365_settings_get_use_impersonation
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_use_impersonation
+                                               (CamelM365Settings *settings,
+                                                gboolean use_impersonation);
+const gchar *  camel_m365_settings_get_impersonate_user
+                                               (CamelM365Settings *settings);
+gchar *                camel_m365_settings_dup_impersonate_user
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_impersonate_user
+                                               (CamelM365Settings *settings,
+                                                const gchar *impersonate_user);
+gboolean       camel_m365_settings_get_check_all
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_check_all
+                                               (CamelM365Settings *settings,
+                                                gboolean check_all);
+const gchar *  camel_m365_settings_get_email   (CamelM365Settings *settings);
+gchar *                camel_m365_settings_dup_email   (CamelM365Settings *settings);
+void           camel_m365_settings_set_email   (CamelM365Settings *settings,
+                                                const gchar *email);
+gboolean       camel_m365_settings_get_filter_junk
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_filter_junk
+                                               (CamelM365Settings *settings,
+                                                gboolean filter_junk);
+gboolean       camel_m365_settings_get_filter_junk_inbox
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_filter_junk_inbox
+                                               (CamelM365Settings *settings,
+                                                gboolean filter_junk_inbox);
+guint          camel_m365_settings_get_timeout (CamelM365Settings *settings);
+void           camel_m365_settings_set_timeout (CamelM365Settings *settings,
+                                                guint timeout);
+gboolean       camel_m365_settings_get_override_oauth2
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_override_oauth2
+                                               (CamelM365Settings *settings,
+                                                gboolean override_oauth2);
+const gchar *  camel_m365_settings_get_oauth2_tenant
+                                               (CamelM365Settings *settings);
+gchar *                camel_m365_settings_dup_oauth2_tenant
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_oauth2_tenant
+                                               (CamelM365Settings *settings,
+                                                const gchar *tenant);
+const gchar *  camel_m365_settings_get_oauth2_client_id
+                                               (CamelM365Settings *settings);
+gchar *                camel_m365_settings_dup_oauth2_client_id
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_oauth2_client_id
+                                               (CamelM365Settings *settings,
+                                                const gchar *client_id);
+const gchar *  camel_m365_settings_get_oauth2_redirect_uri
+                                               (CamelM365Settings *settings);
+gchar *                camel_m365_settings_dup_oauth2_redirect_uri
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_oauth2_redirect_uri
+                                               (CamelM365Settings *settings,
+                                                const gchar *redirect_uri);
+guint          camel_m365_settings_get_concurrent_connections
+                                               (CamelM365Settings *settings);
+void           camel_m365_settings_set_concurrent_connections
+                                               (CamelM365Settings *settings,
+                                                guint concurrent_connections);
+
+G_END_DECLS
+
+#endif /* CAMEL_M365_SETTINGS_H */
diff --git a/src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.c 
b/src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.c
new file mode 100644
index 00000000..60c56dfa
--- /dev/null
+++ b/src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.c
@@ -0,0 +1,45 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "camel-sasl-xoauth2-microsoft365.h"
+
+static CamelServiceAuthType sasl_xoauth2_microsoft365_auth_type = {
+       N_("OAuth2 (Microsoft 365)"),
+       N_("This option will use an OAuth 2.0 access token to connect to the Microsoft 365 server"),
+       "Microsoft365",
+       FALSE
+};
+
+G_DEFINE_DYNAMIC_TYPE (CamelSaslXOAuth2Microsoft365, camel_sasl_xoauth2_microsoft365, 
CAMEL_TYPE_SASL_XOAUTH2)
+
+static void
+camel_sasl_xoauth2_microsoft365_class_init (CamelSaslXOAuth2Microsoft365Class *klass)
+{
+       CamelSaslClass *sasl_class;
+
+       sasl_class = CAMEL_SASL_CLASS (klass);
+       sasl_class->auth_type = &sasl_xoauth2_microsoft365_auth_type;
+}
+
+static void
+camel_sasl_xoauth2_microsoft365_class_finalize (CamelSaslXOAuth2Microsoft365Class *klass)
+{
+}
+
+static void
+camel_sasl_xoauth2_microsoft365_init (CamelSaslXOAuth2Microsoft365 *sasl)
+{
+}
+
+void
+camel_sasl_xoauth2_microsoft365_type_register (GTypeModule *type_module)
+{
+       camel_sasl_xoauth2_microsoft365_register_type (type_module);
+}
diff --git a/src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.h 
b/src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.h
new file mode 100644
index 00000000..b6380ed5
--- /dev/null
+++ b/src/Microsoft365/common/camel-sasl-xoauth2-microsoft365.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_SASL_XOAUTH2_MICROSOFT365_H
+#define CAMEL_SASL_XOAUTH2_MICROSOFT365_H
+
+#include <gmodule.h>
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365 \
+       (camel_sasl_xoauth2_microsoft365_get_type ())
+#define CAMEL_SASL_XOAUTH2_MICROSOFT365(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365, CamelSaslXOAuth2Microsoft365))
+#define CAMEL_SASL_XOAUTH2_MICROSOFT365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365, CamelSaslXOAuth2Microsoft365Class))
+#define CAMEL_IS_SASL_XOAUTH2_MICROSOFT365(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365))
+#define CAMEL_IS_SASL_XOAUTH2_MICROSOFT365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365))
+#define CAMEL_SASL_XOAUTH2_MICROSOFT365_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_SASL_XOAUTH2_MICROSOFT365, CamelSaslXOAuth2Microsoft365Class))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelSaslXOAuth2Microsoft365 CamelSaslXOAuth2Microsoft365;
+typedef struct _CamelSaslXOAuth2Microsoft365Class CamelSaslXOAuth2Microsoft365Class;
+typedef struct _CamelSaslXOAuth2Microsoft365Private CamelSaslXOAuth2Microsoft365Private;
+
+struct _CamelSaslXOAuth2Microsoft365 {
+       CamelSaslXOAuth2 parent;
+       CamelSaslXOAuth2Microsoft365Private *priv;
+};
+
+struct _CamelSaslXOAuth2Microsoft365Class {
+       CamelSaslXOAuth2Class parent_class;
+};
+
+GType          camel_sasl_xoauth2_microsoft365_get_type(void) G_GNUC_CONST;
+
+void           camel_sasl_xoauth2_microsoft365_type_register
+                                                       (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* CAMEL_SASL_XOAUTH2_MICROSOFT365_H */
diff --git a/src/Microsoft365/common/e-m365-connection.c b/src/Microsoft365/common/e-m365-connection.c
new file mode 100644
index 00000000..cf8e1d2d
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-connection.c
@@ -0,0 +1,6007 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <string.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <json-glib/json-glib.h>
+
+#include "camel-m365-settings.h"
+#include "e-m365-json-utils.h"
+
+#include "e-m365-connection.h"
+
+#define LOCK(x) g_rec_mutex_lock (&(x->priv->property_lock))
+#define UNLOCK(x) g_rec_mutex_unlock (&(x->priv->property_lock))
+
+#define X_EVO_M365_DATA "X-EVO-M365-DATA"
+
+typedef enum _CSMFlags {
+       CSM_DEFAULT             = 0,
+       CSM_DISABLE_RESPONSE    = 1 << 0
+} CSMFlags;
+
+struct _EM365ConnectionPrivate {
+       GRecMutex property_lock;
+
+       ESource *source;
+       CamelM365Settings *settings;
+       SoupSession *soup_session;
+       GProxyResolver *proxy_resolver;
+       ESoupAuthBearer *bearer_auth;
+
+       gchar *user; /* The default user for the URL */
+       gchar *impersonate_user;
+
+       gboolean ssl_info_set;
+       gchar *ssl_certificate_pem;
+       GTlsCertificateFlags ssl_certificate_errors;
+
+       gchar *hash_key; /* in the opened connections hash */
+
+       /* How many microseconds to wait, until can execute a new request.
+          This is to cover throttling and server unavailable responses.
+          https://docs.microsoft.com/en-us/graph/best-practices-concept#handling-expected-errors */
+       gint64 backoff_for_usec;
+};
+
+enum {
+       PROP_0,
+       PROP_PROXY_RESOLVER,
+       PROP_SETTINGS,
+       PROP_SOURCE,
+       PROP_CONCURRENT_CONNECTIONS,
+       PROP_USER,                      /* This one is hidden, write only */
+       PROP_USE_IMPERSONATION,         /* This one is hidden, write only */
+       PROP_IMPERSONATE_USER           /* This one is hidden, write only */
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EM365Connection, e_m365_connection, G_TYPE_OBJECT)
+
+static GHashTable *opened_connections = NULL;
+G_LOCK_DEFINE_STATIC (opened_connections);
+
+static gboolean
+m365_log_enabled (void)
+{
+       static gint log_enabled = -1;
+
+       if (log_enabled == -1)
+               log_enabled = g_strcmp0 (g_getenv ("M365_DEBUG"), "1") == 0 ? 1 : 0;
+
+       return log_enabled == 1;
+}
+
+static SoupSession *
+m365_connection_ref_soup_session (EM365Connection *cnc)
+{
+       SoupSession *soup_session = NULL;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+
+       LOCK (cnc);
+
+       if (cnc->priv->soup_session)
+               soup_session = g_object_ref (cnc->priv->soup_session);
+
+       UNLOCK (cnc);
+
+       return soup_session;
+}
+
+static void
+m365_connection_utils_ensure_bearer_auth_usage (SoupSession *session,
+                                               SoupMessage *message,
+                                               ESoupAuthBearer *bearer)
+{
+       SoupAuthManager *auth_manager;
+       SoupSessionFeature *feature;
+       SoupURI *soup_uri;
+
+       g_return_if_fail (SOUP_IS_SESSION (session));
+
+       /* Preload the SoupAuthManager with a valid "Bearer" token
+        * when using OAuth 2.0. This avoids an extra unauthorized
+        * HTTP round-trip, which apparently Google doesn't like. */
+
+       feature = soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_AUTH_MANAGER);
+
+       if (!soup_session_feature_has_feature (feature, E_TYPE_SOUP_AUTH_BEARER)) {
+               /* Add the "Bearer" auth type to support OAuth 2.0. */
+               soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
+       }
+
+       soup_uri = message ? soup_message_get_uri (message) : NULL;
+       if (soup_uri && soup_uri->host && *soup_uri->host) {
+               soup_uri = soup_uri_copy_host (soup_uri);
+       } else {
+               soup_uri = NULL;
+       }
+
+       g_return_if_fail (soup_uri != NULL);
+
+       auth_manager = SOUP_AUTH_MANAGER (feature);
+
+       /* This will make sure the 'bearer' is used regardless of the current 'auth_manager' state.
+          See https://gitlab.gnome.org/GNOME/libsoup/-/issues/196 for more information. */
+       soup_auth_manager_clear_cached_credentials (auth_manager);
+       soup_auth_manager_use_auth (auth_manager, soup_uri, SOUP_AUTH (bearer));
+
+       soup_uri_free (soup_uri);
+}
+
+static gboolean
+m365_connection_utils_setup_bearer_auth (EM365Connection *cnc,
+                                        SoupSession *session,
+                                        SoupMessage *message,
+                                        gboolean is_in_authenticate_handler,
+                                        ESoupAuthBearer *bearer,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       ESource *source;
+       gchar *access_token = NULL;
+       gint expires_in_seconds = -1;
+       gboolean success = FALSE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
+
+       source = e_m365_connection_get_source (cnc);
+
+       success = e_source_get_oauth2_access_token_sync (source, cancellable,
+               &access_token, &expires_in_seconds, error);
+
+       if (success) {
+               e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
+
+               if (!is_in_authenticate_handler) {
+                       if (session)
+                               g_object_ref (session);
+                       else
+                               session = m365_connection_ref_soup_session (cnc);
+
+                       m365_connection_utils_ensure_bearer_auth_usage (session, message, bearer);
+
+                       g_clear_object (&session);
+               }
+       }
+
+       g_free (access_token);
+
+       return success;
+}
+
+static gboolean
+m365_connection_utils_prepare_bearer_auth (EM365Connection *cnc,
+                                          SoupSession *session,
+                                          SoupMessage *message,
+                                          GCancellable *cancellable)
+{
+       ESource *source;
+       ESoupAuthBearer *using_bearer_auth;
+       gboolean success;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+
+       source = e_m365_connection_get_source (cnc);
+       if (!source)
+               return TRUE;
+
+       using_bearer_auth = e_m365_connection_ref_bearer_auth (cnc);
+       if (using_bearer_auth) {
+               success = m365_connection_utils_setup_bearer_auth (cnc, session, message, FALSE, 
using_bearer_auth, cancellable, &local_error);
+               g_clear_object (&using_bearer_auth);
+       } else {
+               SoupAuth *soup_auth;
+               SoupURI *soup_uri;
+
+               soup_uri = message ? soup_message_get_uri (message) : NULL;
+               if (soup_uri && soup_uri->host && *soup_uri->host) {
+                       soup_uri = soup_uri_copy_host (soup_uri);
+               } else {
+                       soup_uri = NULL;
+               }
+
+               g_warn_if_fail (soup_uri != NULL);
+
+               if (!soup_uri) {
+                       soup_message_set_status_full (message, SOUP_STATUS_MALFORMED, "Cannot get host from 
message");
+                       return FALSE;
+               }
+
+               soup_auth = g_object_new (E_TYPE_SOUP_AUTH_BEARER, SOUP_AUTH_HOST, soup_uri->host, NULL);
+
+               success = m365_connection_utils_setup_bearer_auth (cnc, session, message, FALSE, 
E_SOUP_AUTH_BEARER (soup_auth), cancellable, &local_error);
+               if (success)
+                       e_m365_connection_set_bearer_auth (cnc, E_SOUP_AUTH_BEARER (soup_auth));
+
+               g_object_unref (soup_auth);
+               soup_uri_free (soup_uri);
+       }
+
+       if (!success) {
+               if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                       soup_message_set_status (message, SOUP_STATUS_CANCELLED);
+               else if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) ||
+                        g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+                       soup_message_set_status_full (message, SOUP_STATUS_UNAUTHORIZED, 
local_error->message);
+               else
+                       soup_message_set_status_full (message, SOUP_STATUS_MALFORMED, local_error ? 
local_error->message : _("Unknown error"));
+       }
+
+       g_clear_error (&local_error);
+
+       return success;
+}
+
+static void
+m365_connection_authenticate (SoupSession *session,
+                             SoupMessage *msg,
+                             SoupAuth *auth,
+                             gboolean retrying,
+                             gpointer user_data)
+{
+       EM365Connection *cnc = user_data;
+       ESoupAuthBearer *using_bearer_auth;
+       GError *local_error = NULL;
+
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       using_bearer_auth = e_m365_connection_ref_bearer_auth (cnc);
+
+       if (E_IS_SOUP_AUTH_BEARER (auth)) {
+               g_object_ref (auth);
+               g_warn_if_fail ((gpointer) using_bearer_auth == (gpointer) auth);
+
+               g_clear_object (&using_bearer_auth);
+               using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
+
+               e_m365_connection_set_bearer_auth (cnc, using_bearer_auth);
+       }
+
+       if (!using_bearer_auth) {
+               g_warn_if_reached ();
+               return;
+       }
+
+       m365_connection_utils_setup_bearer_auth (cnc, session, msg, TRUE, E_SOUP_AUTH_BEARER (auth), NULL, 
&local_error);
+
+       if (local_error)
+               soup_message_set_status_full (msg, SOUP_STATUS_IO_ERROR, local_error->message);
+
+       g_object_unref (using_bearer_auth);
+       g_clear_error (&local_error);
+}
+
+static gboolean
+m365_connection_utils_prepare_message (EM365Connection *cnc,
+                                      SoupSession *session,
+                                      SoupMessage *message,
+                                      GCancellable *cancellable)
+{
+       ESoupAuthBearer *using_bearer_auth;
+       ESource *source;
+       GError *local_error = NULL;
+
+       source = e_m365_connection_get_source (cnc);
+       if (source)
+               e_soup_ssl_trust_connect (message, source);
+
+       if (!m365_connection_utils_prepare_bearer_auth (cnc, session, message, cancellable))
+               return FALSE;
+
+       using_bearer_auth = e_m365_connection_ref_bearer_auth (cnc);
+
+       if (using_bearer_auth &&
+           e_soup_auth_bearer_is_expired (using_bearer_auth) &&
+           !m365_connection_utils_setup_bearer_auth (cnc, session, message, FALSE, using_bearer_auth, 
cancellable, &local_error)) {
+               if (local_error) {
+                       soup_message_set_status_full (message, SOUP_STATUS_BAD_REQUEST, local_error->message);
+                       g_clear_error (&local_error);
+               } else {
+                       soup_message_set_status (message, SOUP_STATUS_BAD_REQUEST);
+               }
+
+               g_object_unref (using_bearer_auth);
+
+               return FALSE;
+       }
+
+       g_clear_object (&using_bearer_auth);
+
+       return TRUE;
+}
+
+static void
+m365_connection_set_settings (EM365Connection *cnc,
+                             CamelM365Settings *settings)
+{
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+       g_return_if_fail (CAMEL_IS_M365_SETTINGS (settings));
+       g_return_if_fail (cnc->priv->settings == NULL);
+
+       cnc->priv->settings = g_object_ref (settings);
+
+       e_binding_bind_property (
+               cnc->priv->settings, "user",
+               cnc, "user",
+               G_BINDING_DEFAULT |
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               cnc->priv->settings, "use-impersonation",
+               cnc, "use-impersonation",
+               G_BINDING_DEFAULT |
+               G_BINDING_SYNC_CREATE);
+
+       /* No need to G_BINDING_SYNC_CREATE, because the 'use-impersonation' already updated the value */
+       e_binding_bind_property (
+               cnc->priv->settings, "impersonate-user",
+               cnc, "impersonate-user",
+               G_BINDING_DEFAULT);
+}
+
+static void
+m365_connection_set_source (EM365Connection *cnc,
+                           ESource *source)
+{
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+       g_return_if_fail (E_IS_SOURCE (source));
+       g_return_if_fail (cnc->priv->source == NULL);
+
+       cnc->priv->source = g_object_ref (source);
+}
+
+static void
+m365_connection_take_user (EM365Connection *cnc,
+                          gchar *user)
+{
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       LOCK (cnc);
+
+       if (!user || !*user)
+               g_clear_pointer (&user, g_free);
+
+       g_free (cnc->priv->user);
+       cnc->priv->user = user;
+
+       UNLOCK (cnc);
+}
+
+static void
+m365_connection_take_impersonate_user (EM365Connection *cnc,
+                                      gchar *impersonate_user)
+{
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       LOCK (cnc);
+
+       if (!impersonate_user || !*impersonate_user ||
+           !camel_m365_settings_get_use_impersonation (cnc->priv->settings)) {
+               g_clear_pointer (&impersonate_user, g_free);
+       }
+
+       if (g_strcmp0 (impersonate_user, cnc->priv->impersonate_user) != 0) {
+               g_free (cnc->priv->impersonate_user);
+               cnc->priv->impersonate_user = impersonate_user;
+       } else {
+               g_clear_pointer (&impersonate_user, g_free);
+       }
+
+       UNLOCK (cnc);
+}
+
+static void
+m365_connection_set_use_impersonation (EM365Connection *cnc,
+                                      gboolean use_impersonation)
+{
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       LOCK (cnc);
+
+       if (!use_impersonation)
+               m365_connection_take_impersonate_user (cnc, NULL);
+       else
+               m365_connection_take_impersonate_user (cnc, camel_m365_settings_dup_impersonate_user 
(cnc->priv->settings));
+
+       UNLOCK (cnc);
+}
+
+static void
+m365_connection_set_property (GObject *object,
+                             guint property_id,
+                             const GValue *value,
+                             GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_PROXY_RESOLVER:
+                       e_m365_connection_set_proxy_resolver (
+                               E_M365_CONNECTION (object),
+                               g_value_get_object (value));
+                       return;
+
+               case PROP_SETTINGS:
+                       m365_connection_set_settings (
+                               E_M365_CONNECTION (object),
+                               g_value_get_object (value));
+                       return;
+
+               case PROP_SOURCE:
+                       m365_connection_set_source (
+                               E_M365_CONNECTION (object),
+                               g_value_get_object (value));
+                       return;
+
+               case PROP_CONCURRENT_CONNECTIONS:
+                       e_m365_connection_set_concurrent_connections (
+                               E_M365_CONNECTION (object),
+                               g_value_get_uint (value));
+                       return;
+
+               case PROP_USER:
+                       m365_connection_take_user (
+                               E_M365_CONNECTION (object),
+                               g_value_dup_string (value));
+                       return;
+
+               case PROP_USE_IMPERSONATION:
+                       m365_connection_set_use_impersonation (
+                               E_M365_CONNECTION (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_IMPERSONATE_USER:
+                       m365_connection_take_impersonate_user (
+                               E_M365_CONNECTION (object),
+                               g_value_dup_string (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_connection_get_property (GObject *object,
+                             guint property_id,
+                             GValue *value,
+                             GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_PROXY_RESOLVER:
+                       g_value_take_object (
+                               value,
+                               e_m365_connection_ref_proxy_resolver (
+                               E_M365_CONNECTION (object)));
+                       return;
+
+               case PROP_SETTINGS:
+                       g_value_set_object (
+                               value,
+                               e_m365_connection_get_settings (
+                               E_M365_CONNECTION (object)));
+                       return;
+
+               case PROP_SOURCE:
+                       g_value_set_object (
+                               value,
+                               e_m365_connection_get_source (
+                               E_M365_CONNECTION (object)));
+                       return;
+
+               case PROP_CONCURRENT_CONNECTIONS:
+                       g_value_set_uint (
+                               value,
+                               e_m365_connection_get_concurrent_connections (
+                               E_M365_CONNECTION (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+m365_connection_constructed (GObject *object)
+{
+       EM365Connection *cnc = E_M365_CONNECTION (object);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_m365_connection_parent_class)->constructed (object);
+
+       if (m365_log_enabled ()) {
+               SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
+
+               soup_session_add_feature (cnc->priv->soup_session, SOUP_SESSION_FEATURE (logger));
+
+               g_object_unref (logger);
+       }
+
+       soup_session_add_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_COOKIE_JAR);
+       soup_session_add_feature_by_type (cnc->priv->soup_session, E_TYPE_SOUP_AUTH_BEARER);
+       soup_session_remove_feature_by_type (cnc->priv->soup_session, SOUP_TYPE_AUTH_BASIC);
+
+       g_signal_connect (
+               cnc->priv->soup_session, "authenticate",
+               G_CALLBACK (m365_connection_authenticate), cnc);
+
+       cnc->priv->hash_key = camel_network_settings_dup_user (CAMEL_NETWORK_SETTINGS (cnc->priv->settings));
+
+       if (!cnc->priv->hash_key)
+               cnc->priv->hash_key = g_strdup ("no-user");
+
+       e_binding_bind_property (
+               cnc->priv->settings, "timeout",
+               cnc->priv->soup_session, SOUP_SESSION_TIMEOUT,
+               G_BINDING_SYNC_CREATE);
+}
+
+static void
+m365_connection_dispose (GObject *object)
+{
+       EM365Connection *cnc = E_M365_CONNECTION (object);
+
+       G_LOCK (opened_connections);
+
+       /* Remove the connection from the opened connections */
+       if (opened_connections &&
+           g_hash_table_lookup (opened_connections, cnc->priv->hash_key) == (gpointer) object) {
+               g_hash_table_remove (opened_connections, cnc->priv->hash_key);
+               if (g_hash_table_size (opened_connections) == 0) {
+                       g_hash_table_destroy (opened_connections);
+                       opened_connections = NULL;
+               }
+       }
+
+       G_UNLOCK (opened_connections);
+
+       LOCK (cnc);
+
+       if (cnc->priv->soup_session) {
+               g_signal_handlers_disconnect_by_func (
+                       cnc->priv->soup_session,
+                       m365_connection_authenticate, object);
+       }
+
+       g_clear_object (&cnc->priv->source);
+       g_clear_object (&cnc->priv->settings);
+       g_clear_object (&cnc->priv->soup_session);
+       g_clear_object (&cnc->priv->proxy_resolver);
+       g_clear_object (&cnc->priv->bearer_auth);
+
+       UNLOCK (cnc);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_m365_connection_parent_class)->dispose (object);
+}
+
+static void
+m365_connection_finalize (GObject *object)
+{
+       EM365Connection *cnc = E_M365_CONNECTION (object);
+
+       g_rec_mutex_clear (&cnc->priv->property_lock);
+       g_clear_pointer (&cnc->priv->ssl_certificate_pem, g_free);
+       g_clear_pointer (&cnc->priv->user, g_free);
+       g_clear_pointer (&cnc->priv->impersonate_user, g_free);
+       g_free (cnc->priv->hash_key);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_m365_connection_parent_class)->finalize (object);
+}
+
+static void
+e_m365_connection_class_init (EM365ConnectionClass *class)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = m365_connection_set_property;
+       object_class->get_property = m365_connection_get_property;
+       object_class->constructed = m365_connection_constructed;
+       object_class->dispose = m365_connection_dispose;
+       object_class->finalize = m365_connection_finalize;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_PROXY_RESOLVER,
+               g_param_spec_object (
+                       "proxy-resolver",
+                       "Proxy Resolver",
+                       "The proxy resolver for this backend",
+                       G_TYPE_PROXY_RESOLVER,
+                       G_PARAM_READWRITE |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_SETTINGS,
+               g_param_spec_object (
+                       "settings",
+                       "Settings",
+                       "Connection settings",
+                       CAMEL_TYPE_M365_SETTINGS,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_SOURCE,
+               g_param_spec_object (
+                       "source",
+                       "Source",
+                       "Corresponding ESource",
+                       E_TYPE_SOURCE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT_ONLY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_CONCURRENT_CONNECTIONS,
+               g_param_spec_uint (
+                       "concurrent-connections",
+                       "Concurrent Connections",
+                       "Number of concurrent connections to use",
+                       MIN_CONCURRENT_CONNECTIONS,
+                       MAX_CONCURRENT_CONNECTIONS,
+                       1,
+                       /* Do not construct it, otherwise it overrides the value derived from 
CamelM365Settings */
+                       G_PARAM_READWRITE |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_USER,
+               g_param_spec_string (
+                       "user",
+                       NULL,
+                       NULL,
+                       NULL,
+                       G_PARAM_WRITABLE |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_USE_IMPERSONATION,
+               g_param_spec_boolean (
+                       "use-impersonation",
+                       NULL,
+                       NULL,
+                       FALSE,
+                       G_PARAM_WRITABLE |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_IMPERSONATE_USER,
+               g_param_spec_string (
+                       "impersonate-user",
+                       NULL,
+                       NULL,
+                       NULL,
+                       G_PARAM_WRITABLE |
+                       G_PARAM_STATIC_STRINGS));
+}
+
+static void
+e_m365_connection_init (EM365Connection *cnc)
+{
+       cnc->priv = e_m365_connection_get_instance_private (cnc);
+
+       g_rec_mutex_init (&cnc->priv->property_lock);
+
+       cnc->priv->backoff_for_usec = 0;
+       cnc->priv->soup_session = soup_session_new_with_options (
+               SOUP_SESSION_TIMEOUT, 90,
+               SOUP_SESSION_SSL_STRICT, TRUE,
+               SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
+               NULL);
+
+       /* Do not use G_BINDING_SYNC_CREATE, because we don't have a GProxyResolver yet anyway. */
+       e_binding_bind_property (
+               cnc, "proxy-resolver",
+               cnc->priv->soup_session, "proxy-resolver",
+               G_BINDING_DEFAULT);
+}
+
+gboolean
+e_m365_connection_util_delta_token_failed (const GError *error)
+{
+       return g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
+              g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_BAD_REQUEST);
+}
+
+EM365Connection *
+e_m365_connection_new (ESource *source,
+                      CamelM365Settings *settings)
+{
+       g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       return e_m365_connection_new_full (source, settings, TRUE);
+}
+
+EM365Connection *
+e_m365_connection_new_for_backend (EBackend *backend,
+                                  ESourceRegistry *registry,
+                                  ESource *source,
+                                  CamelM365Settings *settings)
+{
+       ESource *backend_source, *parent_source;
+
+       g_return_val_if_fail (E_IS_BACKEND (backend), NULL);
+       g_return_val_if_fail (E_IS_SOURCE_REGISTRY (registry), NULL);
+       g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+       g_return_val_if_fail (CAMEL_IS_M365_SETTINGS (settings), NULL);
+
+       backend_source = e_backend_get_source (backend);
+
+       if (!backend_source)
+               return e_m365_connection_new (source, settings);
+
+       parent_source = e_source_registry_find_extension (registry, source, E_SOURCE_EXTENSION_COLLECTION);
+
+       if (parent_source) {
+               EM365Connection *cnc;
+
+               cnc = e_m365_connection_new (parent_source, settings);
+
+               g_object_unref (parent_source);
+
+               return cnc;
+       }
+
+       return e_m365_connection_new (source, settings);
+}
+
+EM365Connection *
+e_m365_connection_new_full (ESource *source,
+                           CamelM365Settings *settings,
+                           gboolean allow_reuse)
+{
+       EM365Connection *cnc;
+
+       if (allow_reuse) {
+               gchar *hash_key = camel_network_settings_dup_user (CAMEL_NETWORK_SETTINGS (settings));
+
+               if (hash_key) {
+                       G_LOCK (opened_connections);
+
+                       if (opened_connections) {
+                               cnc = g_hash_table_lookup (opened_connections, hash_key);
+
+                               if (cnc) {
+                                       g_object_ref (cnc);
+                                       G_UNLOCK (opened_connections);
+
+                                       g_free (hash_key);
+
+                                       return cnc;
+                               }
+                       }
+
+                       G_UNLOCK (opened_connections);
+               }
+
+               g_free (hash_key);
+       }
+
+       cnc = g_object_new (E_TYPE_M365_CONNECTION,
+               "source", source,
+               "settings", settings,
+               NULL);
+
+       if (allow_reuse && cnc->priv->hash_key) {
+               G_LOCK (opened_connections);
+
+               if (!opened_connections)
+                       opened_connections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+               g_hash_table_insert (opened_connections, g_strdup (cnc->priv->hash_key), cnc);
+
+               G_UNLOCK (opened_connections);
+       }
+
+       return cnc;
+}
+
+ESource *
+e_m365_connection_get_source (EM365Connection *cnc)
+{
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+
+       return cnc->priv->source;
+}
+
+CamelM365Settings *
+e_m365_connection_get_settings (EM365Connection *cnc)
+{
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+
+       return cnc->priv->settings;
+}
+
+guint
+e_m365_connection_get_concurrent_connections (EM365Connection *cnc)
+{
+       guint current_cc = 0;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), 1);
+
+       LOCK (cnc);
+
+       g_object_get (G_OBJECT (cnc->priv->soup_session), SOUP_SESSION_MAX_CONNS, &current_cc, NULL);
+
+       UNLOCK (cnc);
+
+       return current_cc;
+}
+
+void
+e_m365_connection_set_concurrent_connections (EM365Connection *cnc,
+                                             guint concurrent_connections)
+{
+       guint current_cc;
+
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       concurrent_connections = CLAMP (concurrent_connections, MIN_CONCURRENT_CONNECTIONS, 
MAX_CONCURRENT_CONNECTIONS);
+
+       current_cc = e_m365_connection_get_concurrent_connections (cnc);
+
+       if (current_cc == concurrent_connections)
+               return;
+
+       LOCK (cnc);
+
+       g_object_set (G_OBJECT (cnc->priv->soup_session),
+               SOUP_SESSION_MAX_CONNS, concurrent_connections,
+               SOUP_SESSION_MAX_CONNS_PER_HOST, concurrent_connections,
+               NULL);
+
+       UNLOCK (cnc);
+
+       g_object_notify (G_OBJECT (cnc), "concurrent-connections");
+}
+
+GProxyResolver *
+e_m365_connection_ref_proxy_resolver (EM365Connection *cnc)
+{
+       GProxyResolver *proxy_resolver = NULL;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+
+       LOCK (cnc);
+
+       if (cnc->priv->proxy_resolver)
+               proxy_resolver = g_object_ref (cnc->priv->proxy_resolver);
+
+       UNLOCK (cnc);
+
+       return proxy_resolver;
+}
+
+void
+e_m365_connection_set_proxy_resolver (EM365Connection *cnc,
+                                     GProxyResolver *proxy_resolver)
+{
+       gboolean notify = FALSE;
+
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       LOCK (cnc);
+
+       /* Emitting a "notify" signal unnecessarily might have
+        * unwanted side effects like cancelling a SoupMessage.
+        * Only emit if we now have a different GProxyResolver. */
+
+       if (proxy_resolver != cnc->priv->proxy_resolver) {
+               g_clear_object (&cnc->priv->proxy_resolver);
+               cnc->priv->proxy_resolver = proxy_resolver;
+
+               if (proxy_resolver)
+                       g_object_ref (proxy_resolver);
+
+               notify = TRUE;
+       }
+
+       UNLOCK (cnc);
+
+       if (notify)
+               g_object_notify (G_OBJECT (cnc), "proxy-resolver");
+}
+
+ESoupAuthBearer *
+e_m365_connection_ref_bearer_auth (EM365Connection *cnc)
+{
+       ESoupAuthBearer *res = NULL;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+
+       LOCK (cnc);
+
+       if (cnc->priv->bearer_auth)
+               res = g_object_ref (cnc->priv->bearer_auth);
+
+       UNLOCK (cnc);
+
+       return res;
+}
+
+void
+e_m365_connection_set_bearer_auth (EM365Connection *cnc,
+                                  ESoupAuthBearer *bearer_auth)
+{
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       LOCK (cnc);
+
+       if (cnc->priv->bearer_auth != bearer_auth) {
+               g_clear_object (&cnc->priv->bearer_auth);
+
+               cnc->priv->bearer_auth = bearer_auth;
+
+               if (cnc->priv->bearer_auth)
+                       g_object_ref (cnc->priv->bearer_auth);
+       }
+
+       UNLOCK (cnc);
+}
+
+static void
+m365_connection_request_cancelled_cb (GCancellable *cancellable,
+                                     gpointer user_data)
+{
+       EFlag *flag = user_data;
+
+       g_return_if_fail (flag != NULL);
+
+       e_flag_set (flag);
+}
+
+static void
+m365_connection_extract_ssl_data (EM365Connection *cnc,
+                                 SoupMessage *message)
+{
+       GTlsCertificate *certificate = NULL;
+
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+
+       LOCK (cnc);
+
+       g_clear_pointer (&cnc->priv->ssl_certificate_pem, g_free);
+       cnc->priv->ssl_info_set = FALSE;
+
+       g_object_get (G_OBJECT (message),
+               "tls-certificate", &certificate,
+               "tls-errors", &cnc->priv->ssl_certificate_errors,
+               NULL);
+
+       if (certificate) {
+               g_object_get (certificate, "certificate-pem", &cnc->priv->ssl_certificate_pem, NULL);
+               cnc->priv->ssl_info_set = TRUE;
+
+               g_object_unref (certificate);
+       }
+
+       UNLOCK (cnc);
+}
+
+/* An example error response:
+
+  {
+    "error": {
+      "code": "BadRequest",
+      "message": "Parsing Select and Expand failed.",
+      "innerError": {
+        "request-id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
+        "date": "2020-06-10T13:44:43"
+      }
+    }
+  }
+
+ */
+static gboolean
+m365_connection_extract_error (JsonNode *node,
+                              guint status_code,
+                              GError **error)
+{
+       JsonObject *object;
+       const gchar *code, *message;
+
+       if (!node || !JSON_NODE_HOLDS_OBJECT (node))
+               return FALSE;
+
+       object = e_m365_json_get_object_member (json_node_get_object (node), "error");
+
+       if (!object)
+               return FALSE;
+
+       code = e_m365_json_get_string_member (object, "code", NULL);
+       message = e_m365_json_get_string_member (object, "message", NULL);
+
+       if (!code && !message)
+               return FALSE;
+
+       if (!status_code || SOUP_STATUS_IS_SUCCESSFUL (status_code))
+               status_code = SOUP_STATUS_MALFORMED;
+       else if (g_strcmp0 (code, "ErrorInvalidUser") == 0)
+               status_code = SOUP_STATUS_UNAUTHORIZED;
+
+       if (code && message)
+               g_set_error (error, SOUP_HTTP_ERROR, status_code, "%s: %s", code, message);
+       else
+               g_set_error_literal (error, SOUP_HTTP_ERROR, status_code, code ? code : message);
+
+       return TRUE;
+}
+
+typedef gboolean (* EM365ResponseFunc) (EM365Connection *cnc,
+                                        SoupMessage *message,
+                                        GInputStream *input_stream,
+                                        JsonNode *node,
+                                        gpointer user_data,
+                                        gchar **out_next_link,
+                                        GCancellable *cancellable,
+                                        GError **error);
+
+/* (transfer full) (nullable): Free the *out_node with json_node_unref(), if not NULL;
+   It can return 'success', even when the *out_node is NULL. */
+gboolean
+e_m365_connection_json_node_from_message (SoupMessage *message,
+                                         GInputStream *input_stream,
+                                         JsonNode **out_node,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       JsonObject *message_json_object;
+       gboolean success = TRUE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
+       g_return_val_if_fail (out_node != NULL, FALSE);
+
+       *out_node = NULL;
+
+       message_json_object = g_object_get_data (G_OBJECT (message), X_EVO_M365_DATA);
+
+       if (message_json_object) {
+               *out_node = json_node_init_object (json_node_new (JSON_NODE_OBJECT), message_json_object);
+
+               success = !m365_connection_extract_error (*out_node, message->status_code, &local_error);
+       } else {
+               const gchar *content_type;
+
+               content_type = message->response_headers ? soup_message_headers_get_content_type 
(message->response_headers, NULL) : NULL;
+
+               if (content_type && g_ascii_strcasecmp (content_type, "application/json") == 0) {
+                       JsonParser *json_parser;
+
+                       json_parser = json_parser_new_immutable ();
+
+                       if (input_stream) {
+                               success = json_parser_load_from_stream (json_parser, input_stream, 
cancellable, error);
+                       } else {
+                               SoupBuffer *sbuffer;
+
+                               sbuffer = soup_message_body_flatten (message->response_body);
+
+                               if (sbuffer) {
+                                       success = json_parser_load_from_data (json_parser, sbuffer->data, 
sbuffer->length, error);
+                                       soup_buffer_free (sbuffer);
+                               } else {
+                                       /* This should not happen, it's for safety check only, thus the 
string is not localized */
+                                       success = FALSE;
+                                       g_set_error_literal (&local_error, G_IO_ERROR, G_IO_ERROR_FAILED, "No 
JSON data found");
+                               }
+                       }
+
+                       if (success) {
+                               *out_node = json_parser_steal_root (json_parser);
+
+                               success = !m365_connection_extract_error (*out_node, message->status_code, 
&local_error);
+                       }
+
+                       g_object_unref (json_parser);
+               }
+       }
+
+       if (!success && *out_node) {
+               json_node_unref (*out_node);
+               *out_node = NULL;
+       }
+
+       if (local_error)
+               g_propagate_error (error, local_error);
+
+       return success;
+}
+
+static gboolean
+m365_connection_send_request_sync (EM365Connection *cnc,
+                                  SoupMessage *message,
+                                  EM365ResponseFunc response_func,
+                                  EM365ConnectionRawDataFunc raw_data_func,
+                                  gpointer func_user_data,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       SoupSession *soup_session;
+       gint need_retry_seconds = 5;
+       gboolean success = FALSE, need_retry = TRUE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
+       g_return_val_if_fail (response_func != NULL || raw_data_func != NULL, FALSE);
+       g_return_val_if_fail (response_func == NULL || raw_data_func == NULL, FALSE);
+
+       while (need_retry && !g_cancellable_is_cancelled (cancellable) && message->status_code != 
SOUP_STATUS_CANCELLED) {
+               need_retry = FALSE;
+
+               LOCK (cnc);
+
+               if (cnc->priv->backoff_for_usec) {
+                       EFlag *flag;
+                       gint64 wait_ms;
+                       gulong handler_id = 0;
+
+                       wait_ms = cnc->priv->backoff_for_usec / G_TIME_SPAN_MILLISECOND;
+
+                       UNLOCK (cnc);
+
+                       flag = e_flag_new ();
+
+                       if (cancellable) {
+                               handler_id = g_cancellable_connect (cancellable, G_CALLBACK 
(m365_connection_request_cancelled_cb),
+                                       flag, NULL);
+                       }
+
+                       while (wait_ms > 0 && !g_cancellable_is_cancelled (cancellable) && 
message->status_code != SOUP_STATUS_CANCELLED) {
+                               gint64 now = g_get_monotonic_time ();
+                               gint left_minutes, left_seconds;
+
+                               left_minutes = wait_ms / 60000;
+                               left_seconds = (wait_ms / 1000) % 60;
+
+                               if (left_minutes > 0) {
+                                       camel_operation_push_message (cancellable,
+                                               g_dngettext (GETTEXT_PACKAGE,
+                                                       "Microsoft 365 server is busy, waiting to retry 
(%d:%02d minute)",
+                                                       "Microsoft 365 server is busy, waiting to retry 
(%d:%02d minutes)", left_minutes),
+                                               left_minutes, left_seconds);
+                               } else {
+                                       camel_operation_push_message (cancellable,
+                                               g_dngettext (GETTEXT_PACKAGE,
+                                                       "Microsoft 365 server is busy, waiting to retry (%d 
second)",
+                                                       "Microsoft 365 server is busy, waiting to retry (%d 
seconds)", left_seconds),
+                                               left_seconds);
+                               }
+
+                               e_flag_wait_until (flag, now + (G_TIME_SPAN_MILLISECOND * (wait_ms > 1000 ? 
1000 : wait_ms)));
+                               e_flag_clear (flag);
+
+                               now = g_get_monotonic_time () - now;
+                               now = now / G_TIME_SPAN_MILLISECOND;
+
+                               if (now >= wait_ms)
+                                       wait_ms = 0;
+                               wait_ms -= now;
+
+                               camel_operation_pop_message (cancellable);
+                       }
+
+                       if (handler_id)
+                               g_cancellable_disconnect (cancellable, handler_id);
+
+                       e_flag_free (flag);
+
+                       LOCK (cnc);
+
+                       cnc->priv->backoff_for_usec = 0;
+               }
+
+               if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+                       UNLOCK (cnc);
+
+                       soup_message_set_status (message, SOUP_STATUS_CANCELLED);
+
+                       return FALSE;
+               }
+
+               soup_session = cnc->priv->soup_session ? g_object_ref (cnc->priv->soup_session) : NULL;
+
+               g_clear_pointer (&cnc->priv->ssl_certificate_pem, g_free);
+               cnc->priv->ssl_certificate_errors = 0;
+               cnc->priv->ssl_info_set = FALSE;
+
+               UNLOCK (cnc);
+
+               if (soup_session &&
+                   m365_connection_utils_prepare_message (cnc, soup_session, message, cancellable)) {
+                       GInputStream *input_stream;
+
+                       input_stream = soup_session_send (soup_session, message, cancellable, error);
+
+                       success = input_stream != NULL;
+
+                       if (success && m365_log_enabled ())
+                               input_stream = e_soup_logger_attach (message, input_stream);
+
+                       /* Throttling - https://docs.microsoft.com/en-us/graph/throttling  */
+                       if (message->status_code == 429 ||
+                           /* 
https://docs.microsoft.com/en-us/graph/best-practices-concept#handling-expected-errors */
+                           message->status_code == SOUP_STATUS_SERVICE_UNAVAILABLE) {
+                               need_retry = TRUE;
+                       } else if (message->status_code == SOUP_STATUS_SSL_FAILED) {
+                               m365_connection_extract_ssl_data (cnc, message);
+                       }
+
+                       if (need_retry) {
+                               const gchar *retry_after_str;
+                               gint64 retry_after;
+
+                               retry_after_str = message->response_headers ? soup_message_headers_get_one 
(message->response_headers, "Retry-After") : NULL;
+
+                               if (retry_after_str && *retry_after_str)
+                                       retry_after = g_ascii_strtoll (retry_after_str, NULL, 10);
+                               else
+                                       retry_after = 0;
+
+                               if (retry_after > 0)
+                                       need_retry_seconds = retry_after;
+                               else if (need_retry_seconds < 120)
+                                       need_retry_seconds *= 2;
+
+                               LOCK (cnc);
+
+                               if (cnc->priv->backoff_for_usec < need_retry_seconds * G_USEC_PER_SEC)
+                                       cnc->priv->backoff_for_usec = need_retry_seconds * G_USEC_PER_SEC;
+
+                               if (message->status_code == SOUP_STATUS_SERVICE_UNAVAILABLE)
+                                       soup_session_abort (soup_session);
+
+                               UNLOCK (cnc);
+
+                               success = FALSE;
+                       } else if (success && raw_data_func && SOUP_STATUS_IS_SUCCESSFUL 
(message->status_code)) {
+                               success = raw_data_func (cnc, message, input_stream, func_user_data, 
cancellable, error);
+                       } else if (success) {
+                               JsonNode *node = NULL;
+
+                               success = e_m365_connection_json_node_from_message (message, input_stream, 
&node, cancellable, error);
+
+                               if (success) {
+                                       gchar *next_link = NULL;
+
+                                       success = response_func && response_func (cnc, message, input_stream, 
node, func_user_data, &next_link, cancellable, error);
+
+                                       if (success && next_link && *next_link) {
+                                               SoupURI *suri;
+
+                                               suri = soup_uri_new (next_link);
+
+                                               /* Check whether the server returned correct nextLink URI */
+                                               g_warn_if_fail (suri != NULL);
+
+                                               if (suri) {
+                                                       need_retry = TRUE;
+
+                                                       soup_message_set_uri (message, suri);
+                                                       soup_uri_free (suri);
+                                               }
+                                       }
+
+                                       g_free (next_link);
+                               } else if (error && !*error && message->status_code && 
!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
+                                       if (message->status_code == SOUP_STATUS_CANCELLED) {
+                                               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
+                                                       message->reason_phrase ? message->reason_phrase : 
soup_status_get_phrase (message->status_code));
+                                       } else {
+                                               g_set_error_literal (error, SOUP_HTTP_ERROR, 
message->status_code,
+                                                       message->reason_phrase ? message->reason_phrase : 
soup_status_get_phrase (message->status_code));
+                                       }
+                               }
+
+                               if (node)
+                                       json_node_unref (node);
+                       }
+
+                       g_clear_object (&input_stream);
+               } else {
+                       if (!message->status_code)
+                               soup_message_set_status (message, SOUP_STATUS_CANCELLED);
+
+                       if (message->status_code == SOUP_STATUS_CANCELLED) {
+                               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
+                                       message->reason_phrase ? message->reason_phrase : 
soup_status_get_phrase (message->status_code));
+                       } else {
+                               g_set_error_literal (error, SOUP_HTTP_ERROR, message->status_code,
+                                       message->reason_phrase ? message->reason_phrase : 
soup_status_get_phrase (message->status_code));
+                       }
+               }
+
+               g_clear_object (&soup_session);
+
+               if (need_retry) {
+                       success = FALSE;
+                       g_clear_error (error);
+               }
+       }
+
+       return success;
+}
+
+static gboolean
+e_m365_read_no_response_cb (EM365Connection *cnc,
+                           SoupMessage *message,
+                           GInputStream *raw_data_stream,
+                           gpointer user_data,
+                           GCancellable *cancellable,
+                           GError **error)
+{
+       /* This is used when no response is expected from the server.
+          Read the data stream only if debugging is on, in case
+          the server returns anything interesting. */
+
+       if (m365_log_enabled ()) {
+               gchar buffer[4096];
+
+               while (g_input_stream_read (raw_data_stream, buffer, sizeof (buffer), cancellable, error) > 
0) {
+                       /* Do nothing, just read it, thus it's shown in the debug output */
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+e_m365_read_to_byte_array_cb (EM365Connection *cnc,
+                             SoupMessage *message,
+                             GInputStream *raw_data_stream,
+                             gpointer user_data,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       GByteArray **out_byte_array = user_data;
+       gchar buffer[4096];
+       gssize n_read;
+
+       g_return_val_if_fail (message != NULL, FALSE);
+       g_return_val_if_fail (out_byte_array != NULL, FALSE);
+
+       if (!*out_byte_array) {
+               goffset content_length;
+
+               content_length = soup_message_headers_get_content_length (message->response_headers);
+
+               if (!content_length || content_length > 65536)
+                       content_length = 65535;
+
+               *out_byte_array = g_byte_array_sized_new (content_length);
+       }
+
+       while (n_read = g_input_stream_read (raw_data_stream, buffer, sizeof (buffer), cancellable, error), 
n_read > 0) {
+               g_byte_array_append (*out_byte_array, (const guint8 *) buffer, n_read);
+       }
+
+       return !n_read;
+}
+
+typedef struct _EM365ResponseData {
+       EM365ConnectionJsonFunc json_func;
+       gpointer func_user_data;
+       gboolean read_only_once; /* To be able to just try authentication */
+       GSList **out_items; /* JsonObject * */
+       gchar **out_delta_link; /* set only if available and not NULL */
+       GPtrArray *inout_requests; /* SoupMessage *, for the batch request */
+} EM365ResponseData;
+
+static gboolean
+e_m365_read_valued_response_cb (EM365Connection *cnc,
+                               SoupMessage *message,
+                               GInputStream *input_stream,
+                               JsonNode *node,
+                               gpointer user_data,
+                               gchar **out_next_link,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       EM365ResponseData *response_data = user_data;
+       JsonObject *object;
+       JsonArray *value;
+       const gchar *delta_link;
+       GSList *items = NULL;
+       gboolean can_continue = TRUE;
+       guint ii, len;
+
+       g_return_val_if_fail (response_data != NULL, FALSE);
+       g_return_val_if_fail (out_next_link != NULL, FALSE);
+       g_return_val_if_fail (JSON_NODE_HOLDS_OBJECT (node), FALSE);
+
+       object = json_node_get_object (node);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       if (!response_data->read_only_once)
+               *out_next_link = g_strdup (e_m365_json_get_string_member (object, "@odata.nextLink", NULL));
+
+       delta_link = e_m365_json_get_string_member (object, "@odata.deltaLink", NULL);
+
+       if (delta_link && response_data->out_delta_link)
+               *response_data->out_delta_link = g_strdup (delta_link);
+
+       value = e_m365_json_get_array_member (object, "value");
+       g_return_val_if_fail (value != NULL, FALSE);
+
+       len = json_array_get_length (value);
+
+       for (ii = 0; ii < len; ii++) {
+               JsonNode *elem = json_array_get_element (value, ii);
+
+               g_warn_if_fail (JSON_NODE_HOLDS_OBJECT (elem));
+
+               if (JSON_NODE_HOLDS_OBJECT (elem)) {
+                       JsonObject *elem_object = json_node_get_object (elem);
+
+                       if (elem_object) {
+                               if (response_data->out_items)
+                                       *response_data->out_items = g_slist_prepend 
(*response_data->out_items, json_object_ref (elem_object));
+                               else
+                                       items = g_slist_prepend (items, json_object_ref (elem_object));
+                       }
+               }
+       }
+
+       if (response_data->json_func)
+               can_continue = response_data->json_func (cnc, items, response_data->func_user_data, 
cancellable, error);
+
+       g_slist_free_full (items, (GDestroyNotify) json_object_unref);
+
+       return can_continue;
+}
+
+static gboolean
+e_m365_read_json_object_response_cb (EM365Connection *cnc,
+                                    SoupMessage *message,
+                                    GInputStream *input_stream,
+                                    JsonNode *node,
+                                    gpointer user_data,
+                                    gchar **out_next_link,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       JsonObject **out_json_object = user_data;
+       JsonObject *object;
+
+       g_return_val_if_fail (out_json_object != NULL, FALSE);
+       g_return_val_if_fail (out_next_link != NULL, FALSE);
+       g_return_val_if_fail (JSON_NODE_HOLDS_OBJECT (node), FALSE);
+
+       object = json_node_get_object (node);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       *out_json_object = json_object_ref (object);
+
+       return TRUE;
+}
+
+static SoupMessage *
+m365_connection_new_soup_message (const gchar *method,
+                                 const gchar *uri,
+                                 CSMFlags csm_flags,
+                                 GError **error)
+{
+       SoupMessage *message;
+
+       g_return_val_if_fail (method != NULL, NULL);
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       message = soup_message_new (method, uri);
+
+       if (message) {
+               soup_message_headers_append (message->request_headers, "Connection", "Close");
+               soup_message_headers_append (message->request_headers, "User-Agent", "Evolution-M365/" 
VERSION);
+
+               /* Disable caching for proxies (RFC 4918, section 10.4.5) */
+               soup_message_headers_append (message->request_headers, "Cache-Control", "no-cache");
+               soup_message_headers_append (message->request_headers, "Pragma", "no-cache");
+
+               if ((csm_flags & CSM_DISABLE_RESPONSE) != 0)
+                       soup_message_headers_append (message->request_headers, "Prefer", "return=minimal");
+       } else {
+               g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Malformed URI: ā€œ%sā€"), uri);
+       }
+
+       return message;
+}
+
+gboolean
+e_m365_connection_get_ssl_error_details (EM365Connection *cnc,
+                                        gchar **out_certificate_pem,
+                                        GTlsCertificateFlags *out_certificate_errors)
+{
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_certificate_pem != NULL, FALSE);
+       g_return_val_if_fail (out_certificate_errors != NULL, FALSE);
+
+       LOCK (cnc);
+
+       if (!cnc->priv->ssl_info_set) {
+               UNLOCK (cnc);
+               return FALSE;
+       }
+
+       *out_certificate_pem = g_strdup (cnc->priv->ssl_certificate_pem);
+       *out_certificate_errors = cnc->priv->ssl_certificate_errors;
+
+       UNLOCK (cnc);
+
+       return TRUE;
+}
+
+ESourceAuthenticationResult
+e_m365_connection_authenticate_sync (EM365Connection *cnc,
+                                    const gchar *user_override,
+                                    EM365FolderKind kind,
+                                    const gchar *group_id,
+                                    const gchar *folder_id,
+                                    gchar **out_certificate_pem,
+                                    GTlsCertificateFlags *out_certificate_errors,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       ESourceAuthenticationResult result = E_SOURCE_AUTHENTICATION_ERROR;
+       JsonObject *object = NULL;
+       gboolean success = FALSE;
+       GError *local_error = NULL;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), result);
+
+       switch (kind) {
+       default:
+               g_warn_if_reached ();
+               /* Falls through */
+       case E_M365_FOLDER_KIND_UNKNOWN:
+       case E_M365_FOLDER_KIND_MAIL:
+               if (!folder_id || !*folder_id)
+                       folder_id = "inbox";
+
+               success = e_m365_connection_get_mail_folder_sync (cnc, user_override, folder_id, 
"displayName", &object, cancellable, &local_error);
+               break;
+       case E_M365_FOLDER_KIND_CONTACTS:
+               if (!folder_id || !*folder_id)
+                       folder_id = "contacts";
+
+               success = e_m365_connection_get_contacts_folder_sync (cnc, user_override, folder_id, 
"displayName", &object, cancellable, &local_error);
+               break;
+       case E_M365_FOLDER_KIND_CALENDAR:
+               if (folder_id && !*folder_id)
+                       folder_id = NULL;
+
+               success = e_m365_connection_get_calendar_folder_sync (cnc, user_override, group_id, 
folder_id, "name", &object, cancellable, error);
+               break;
+       case E_M365_FOLDER_KIND_TASKS:
+               if (!folder_id || !*folder_id)
+                       folder_id = "tasks";
+
+               success = e_m365_connection_get_task_folder_sync (cnc, user_override, group_id, folder_id, 
"name", &object, cancellable, error);
+               break;
+       }
+
+       if (success) {
+               result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+       } else {
+               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_SSL_FAILED)) {
+                       result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
+
+                       if (out_certificate_pem || out_certificate_errors)
+                               e_m365_connection_get_ssl_error_details (cnc, out_certificate_pem, 
out_certificate_errors);
+               } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+                       ESoupAuthBearer *bearer;
+
+                       bearer = e_m365_connection_ref_bearer_auth (cnc);
+
+                       if (bearer) {
+                               LOCK (cnc);
+
+                               if (cnc->priv->impersonate_user) {
+                                       g_propagate_error (error, local_error);
+                                       local_error = NULL;
+                               } else {
+                                       result = E_SOURCE_AUTHENTICATION_REJECTED;
+                               }
+
+                               UNLOCK (cnc);
+                       } else {
+                               result = E_SOURCE_AUTHENTICATION_REQUIRED;
+                       }
+
+                       g_clear_object (&bearer);
+                       g_clear_error (&local_error);
+               }
+
+               if (local_error) {
+                       g_propagate_error (error, local_error);
+                       local_error = NULL;
+               }
+       }
+
+       if (object)
+               json_object_unref (object);
+
+       g_clear_error (&local_error);
+
+       return result;
+}
+
+gboolean
+e_m365_connection_disconnect_sync (EM365Connection *cnc,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+
+       LOCK (cnc);
+
+       soup_session_abort (cnc->priv->soup_session);
+
+       UNLOCK (cnc);
+
+       return TRUE;
+}
+
+/* Expects NULL-terminated pair of parameters 'name', 'value'; if 'value' is NULL, the parameter is skipped.
+   An empty 'name' can add the 'value' into the path. These can be only before query parameters. */
+gchar *
+e_m365_connection_construct_uri (EM365Connection *cnc,
+                                gboolean include_user,
+                                const gchar *user_override,
+                                EM365ApiVersion api_version,
+                                const gchar *api_part,
+                                const gchar *resource,
+                                const gchar *id,
+                                const gchar *path,
+                                ...)
+{
+       va_list args;
+       const gchar *name, *value;
+       gboolean first_param = TRUE;
+       GString *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+
+       if (!api_part)
+               api_part = "users";
+
+       uri = g_string_sized_new (128);
+
+       /* https://graph.microsoft.com/v1.0/users/XUSERX/mailFolders */
+
+       g_string_append (uri, "https://graph.microsoft.com";);
+
+       switch (api_version) {
+       case E_M365_API_V1_0:
+               g_string_append_c (uri, '/');
+               g_string_append (uri, "v1.0");
+               break;
+       case E_M365_API_BETA:
+               g_string_append_c (uri, '/');
+               g_string_append (uri, "beta");
+               break;
+       }
+
+       if (*api_part) {
+               g_string_append_c (uri, '/');
+               g_string_append (uri, api_part);
+       }
+
+       if (include_user) {
+               const gchar *use_user;
+
+               LOCK (cnc);
+
+               if (user_override)
+                       use_user = user_override;
+               else if (cnc->priv->impersonate_user)
+                       use_user = cnc->priv->impersonate_user;
+               else
+                       use_user = cnc->priv->user;
+
+               if (use_user) {
+                       gchar *encoded;
+
+                       encoded = soup_uri_encode (use_user, NULL);
+
+                       g_string_append_c (uri, '/');
+                       g_string_append (uri, encoded);
+
+                       g_free (encoded);
+               }
+
+               UNLOCK (cnc);
+       }
+
+       if (resource && *resource) {
+               g_string_append_c (uri, '/');
+               g_string_append (uri, resource);
+       }
+
+       if (id && *id) {
+               g_string_append_c (uri, '/');
+               g_string_append (uri, id);
+       }
+
+       if (path && *path) {
+               g_string_append_c (uri, '/');
+               g_string_append (uri, path);
+       }
+
+       va_start (args, path);
+
+       name = va_arg (args, const gchar *);
+
+       while (name) {
+               value = va_arg (args, const gchar *);
+
+               if (*name && value) {
+                       g_string_append_c (uri, first_param ? '?' : '&');
+
+                       first_param = FALSE;
+
+                       g_string_append (uri, name);
+                       g_string_append_c (uri, '=');
+
+                       if (*value) {
+                               gchar *encoded;
+
+                               encoded = soup_uri_encode (value, NULL);
+
+                               g_string_append (uri, encoded);
+
+                               g_free (encoded);
+                       }
+               } else if (!*name && value && *value) {
+                       /* Warn when adding path after additional query parameters */
+                       g_warn_if_fail (first_param);
+
+                       g_string_append_c (uri, '/');
+                       g_string_append (uri, value);
+               }
+
+               name = va_arg (args, const gchar *);
+       }
+
+       va_end (args);
+
+       return g_string_free (uri, FALSE);
+}
+
+static void
+e_m365_connection_set_json_body (SoupMessage *message,
+                                JsonBuilder *builder)
+{
+       JsonGenerator *generator;
+       JsonNode *node;
+       gchar *data;
+       gsize data_length = 0;
+
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+       g_return_if_fail (builder != NULL);
+
+       node = json_builder_get_root (builder);
+
+       generator = json_generator_new ();
+       json_generator_set_root (generator, node);
+
+       data = json_generator_to_data (generator, &data_length);
+
+       soup_message_headers_set_content_type (message->request_headers, "application/json", NULL);
+
+       if (data)
+               soup_message_body_append_take (message->request_body, (guchar *) data, data_length);
+
+       g_object_unref (generator);
+       json_node_unref (node);
+}
+
+static void
+e_m365_fill_message_headers_cb (JsonObject *object,
+                               const gchar *member_name,
+                               JsonNode *member_node,
+                               gpointer user_data)
+{
+       SoupMessage *message = user_data;
+
+       g_return_if_fail (message != NULL);
+       g_return_if_fail (member_name != NULL);
+       g_return_if_fail (member_node != NULL);
+
+       if (JSON_NODE_HOLDS_VALUE (member_node)) {
+               const gchar *value;
+
+               value = json_node_get_string (member_node);
+
+               if (value)
+                       soup_message_headers_replace (message->response_headers, member_name, value);
+       }
+}
+
+static void
+e_m365_connection_fill_batch_response (SoupMessage *message,
+                                      JsonObject *object)
+{
+       JsonObject *subobject;
+
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+       g_return_if_fail (object != NULL);
+
+       message->status_code = e_m365_json_get_int_member (object, "status", SOUP_STATUS_MALFORMED);
+
+       subobject = e_m365_json_get_object_member (object, "headers");
+
+       if (subobject)
+               json_object_foreach_member (subobject, e_m365_fill_message_headers_cb, message);
+
+       subobject = e_m365_json_get_object_member (object, "body");
+
+       if (subobject)
+               g_object_set_data_full (G_OBJECT (message), X_EVO_M365_DATA, json_object_ref (subobject), 
(GDestroyNotify) json_object_unref);
+}
+
+static gboolean
+e_m365_read_batch_response_cb (EM365Connection *cnc,
+                              SoupMessage *message,
+                              GInputStream *input_stream,
+                              JsonNode *node,
+                              gpointer user_data,
+                              gchar **out_next_link,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       GPtrArray *requests = user_data;
+       JsonObject *object;
+       JsonArray *responses;
+       guint ii, len;
+
+       g_return_val_if_fail (requests != NULL, FALSE);
+       g_return_val_if_fail (out_next_link != NULL, FALSE);
+       g_return_val_if_fail (JSON_NODE_HOLDS_OBJECT (node), FALSE);
+
+       object = json_node_get_object (node);
+       g_return_val_if_fail (object != NULL, FALSE);
+
+       *out_next_link = g_strdup (e_m365_json_get_string_member (object, "@odata.nextLink", NULL));
+
+       responses = e_m365_json_get_array_member (object, "responses");
+       g_return_val_if_fail (responses != NULL, FALSE);
+
+       len = json_array_get_length (responses);
+
+       for (ii = 0; ii < len; ii++) {
+               JsonNode *elem = json_array_get_element (responses, ii);
+
+               g_warn_if_fail (JSON_NODE_HOLDS_OBJECT (elem));
+
+               if (JSON_NODE_HOLDS_OBJECT (elem)) {
+                       JsonObject *elem_object = json_node_get_object (elem);
+
+                       if (elem_object) {
+                               const gchar *id_str;
+
+                               id_str = e_m365_json_get_string_member (elem_object, "id", NULL);
+
+                               if (id_str) {
+                                       guint id;
+
+                                       id = (guint) g_ascii_strtoull (id_str, NULL, 10);
+
+                                       if (id < requests->len)
+                                               e_m365_connection_fill_batch_response (g_ptr_array_index 
(requests, id), elem_object);
+                               }
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+/* https://docs.microsoft.com/en-us/graph/json-batching */
+
+static gboolean
+e_m365_connection_batch_request_internal_sync (EM365Connection *cnc,
+                                              EM365ApiVersion api_version,
+                                              GPtrArray *requests, /* SoupMessage * */
+                                              GCancellable *cancellable,
+                                              GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success = TRUE;
+       gchar *uri, buff[128];
+       guint ii;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (requests != NULL, FALSE);
+       g_return_val_if_fail (requests->len > 0, FALSE);
+       g_return_val_if_fail (requests->len <= E_M365_BATCH_MAX_REQUESTS, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, FALSE, NULL, api_version, "",
+               "$batch", NULL, NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_begin_array_member (builder, "requests");
+
+       for (ii = 0; success && ii < requests->len; ii++) {
+               SoupMessageHeadersIter iter;
+               SoupMessage *submessage;
+               SoupURI *suri;
+               gboolean has_headers = FALSE;
+               const gchar *hdr_name, *hdr_value, *use_uri;
+               gboolean is_application_json = FALSE;
+
+               submessage = g_ptr_array_index (requests, ii);
+
+               if (!submessage)
+                       continue;
+
+               submessage->status_code = SOUP_STATUS_IO_ERROR;
+
+               suri = soup_message_get_uri (submessage);
+               uri = suri ? soup_uri_to_string (suri, TRUE) : NULL;
+
+               if (!uri) {
+                       submessage->status_code = SOUP_STATUS_MALFORMED;
+                       continue;
+               }
+
+               use_uri = uri;
+
+               /* The 'url' is without the API part, it is derived from the main request */
+               if (g_str_has_prefix (use_uri, "/v1.0/") ||
+                   g_str_has_prefix (use_uri, "/beta/"))
+                       use_uri += 5;
+
+               g_snprintf (buff, sizeof (buff), "%d", ii);
+
+               e_m365_json_begin_object_member (builder, NULL);
+
+               e_m365_json_add_string_member (builder, "id", buff);
+               e_m365_json_add_string_member (builder, "method", submessage->method);
+               e_m365_json_add_string_member (builder, "url", use_uri);
+
+               g_free (uri);
+
+               soup_message_headers_iter_init (&iter, submessage->request_headers);
+
+               while (soup_message_headers_iter_next (&iter, &hdr_name, &hdr_value)) {
+                       if (hdr_name && *hdr_name && hdr_value &&
+                           !camel_strcase_equal (hdr_name, "Connection") &&
+                           !camel_strcase_equal (hdr_name, "User-Agent")) {
+                               if (camel_strcase_equal (hdr_name, "Content-Type") &&
+                                   camel_strcase_equal (hdr_value, "application/json"))
+                                       is_application_json = TRUE;
+
+                               if (!has_headers) {
+                                       has_headers = TRUE;
+
+                                       e_m365_json_begin_object_member (builder, "headers");
+                               }
+
+                               e_m365_json_add_string_member (builder, hdr_name, hdr_value);
+                       }
+               }
+
+               if (has_headers)
+                       e_m365_json_end_object_member (builder); /* headers */
+
+               if (submessage->request_body) {
+                       SoupBuffer *sbuffer;
+
+                       sbuffer = soup_message_body_flatten (submessage->request_body);
+
+                       if (sbuffer && sbuffer->length > 0) {
+                               if (is_application_json) {
+                                       /* The server needs it unpacked, not as a plain string */
+                                       JsonParser *parser;
+                                       JsonNode *node;
+
+                                       parser = json_parser_new_immutable ();
+
+                                       success = json_parser_load_from_data (parser, sbuffer->data, 
sbuffer->length, error);
+
+                                       if (!success)
+                                               g_prefix_error (error, "%s", _("Failed to parse own Json 
data"));
+
+                                       node = success ? json_parser_steal_root (parser) : NULL;
+
+                                       if (node) {
+                                               json_builder_set_member_name (builder, "body");
+                                               json_builder_add_value (builder, node);
+                                       }
+
+                                       g_clear_object (&parser);
+                               } else {
+                                       e_m365_json_add_string_member (builder, "body", sbuffer->data);
+                               }
+                       }
+
+                       if (sbuffer)
+                               soup_buffer_free (sbuffer);
+               }
+
+               e_m365_json_end_object_member (builder); /* unnamed object */
+       }
+
+       e_m365_json_end_array_member (builder);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       soup_message_headers_append (message->request_headers, "Accept", "application/json");
+
+       g_object_unref (builder);
+
+       success = success && m365_connection_send_request_sync (cnc, message, e_m365_read_batch_response_cb, 
NULL, requests, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* The 'requests' contains a SoupMessage * objects, from which are read
+   the request data and on success the SoupMessage's 'response' properties
+   are filled accordingly.
+ */
+gboolean
+e_m365_connection_batch_request_sync (EM365Connection *cnc,
+                                     EM365ApiVersion api_version,
+                                     GPtrArray *requests, /* SoupMessage * */
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       GPtrArray *use_requests;
+       gint need_retry_seconds = 5;
+       gboolean success, need_retry = TRUE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (requests != NULL, FALSE);
+       g_return_val_if_fail (requests->len > 0, FALSE);
+       g_return_val_if_fail (requests->len <= E_M365_BATCH_MAX_REQUESTS, FALSE);
+
+       use_requests = requests;
+
+       while (need_retry) {
+               need_retry = FALSE;
+
+               success = e_m365_connection_batch_request_internal_sync (cnc, api_version, use_requests, 
cancellable, error);
+
+               if (success) {
+                       GPtrArray *new_requests = NULL;
+                       gint delay_seconds = 0;
+                       gint ii;
+
+                       for (ii = 0; ii < use_requests->len; ii++) {
+                               SoupMessage *message = g_ptr_array_index (use_requests, ii);
+
+                               if (!message)
+                                       continue;
+
+                               /* Throttling - https://docs.microsoft.com/en-us/graph/throttling  */
+                               if (message->status_code == 429 ||
+                                   /* 
https://docs.microsoft.com/en-us/graph/best-practices-concept#handling-expected-errors */
+                                   message->status_code == SOUP_STATUS_SERVICE_UNAVAILABLE) {
+                                       const gchar *retry_after_str;
+                                       gint64 retry_after;
+
+                                       need_retry = TRUE;
+
+                                       if (!new_requests)
+                                               new_requests = g_ptr_array_sized_new (use_requests->len);
+
+                                       g_ptr_array_add (new_requests, message);
+
+                                       retry_after_str = message->response_headers ? 
soup_message_headers_get_one (message->response_headers, "Retry-After") : NULL;
+
+                                       if (retry_after_str && *retry_after_str)
+                                               retry_after = g_ascii_strtoll (retry_after_str, NULL, 10);
+                                       else
+                                               retry_after = 0;
+
+                                       if (retry_after > 0)
+                                               delay_seconds = MAX (delay_seconds, retry_after);
+                                       else
+                                               delay_seconds = MAX (delay_seconds, need_retry_seconds);
+                               }
+                       }
+
+                       if (new_requests) {
+                               if (delay_seconds)
+                                       need_retry_seconds = delay_seconds;
+                               else if (need_retry_seconds < 120)
+                                       need_retry_seconds *= 2;
+
+                               LOCK (cnc);
+
+                               if (cnc->priv->backoff_for_usec < need_retry_seconds * G_USEC_PER_SEC)
+                                       cnc->priv->backoff_for_usec = need_retry_seconds * G_USEC_PER_SEC;
+
+                               UNLOCK (cnc);
+
+                               if (use_requests != requests)
+                                       g_ptr_array_free (use_requests, TRUE);
+
+                               use_requests = new_requests;
+                       }
+               }
+       }
+
+       if (use_requests != requests)
+               g_ptr_array_free (use_requests, TRUE);
+
+       return success;
+}
+
+/* This can be used as a EM365ConnectionJsonFunc function, it only
+   copies items of 'results' into 'user_data', which is supposed
+   to be a pointer to a GSList *. */
+gboolean
+e_m365_connection_call_gather_into_slist (EM365Connection *cnc,
+                                         const GSList *results, /* JsonObject * - the returned objects from 
the server */
+                                         gpointer user_data, /* expects GSList **, aka pointer to a GSList 
*, where it copies the 'results' */
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       GSList **out_results = user_data, *link;
+
+       g_return_val_if_fail (out_results != NULL, FALSE);
+
+       for (link = (GSList *) results; link; link = g_slist_next (link)) {
+               JsonObject *obj = link->data;
+
+               if (obj)
+                       *out_results = g_slist_prepend (*out_results, json_object_ref (obj));
+       }
+
+       return TRUE;
+}
+
+/* 
https://docs.microsoft.com/en-us/graph/api/outlookuser-list-mastercategories?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_categories_sync (EM365Connection *cnc,
+                                      const gchar *user_override,
+                                      GSList **out_categories, /* EM365Category * */
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_categories != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "outlook",
+               "masterCategories",
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_categories;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-list-mailfolders?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_list_mail_folders_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         const gchar *from_path, /* path for the folder to read, NULL for 
top user folder */
+                                         const gchar *select, /* properties to select, nullable */
+                                         GSList **out_folders, /* EM365MailFolder * - the returned 
mailFolder objects */
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_folders != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "mailFolders",
+               NULL,
+               from_path,
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_folders;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+gboolean
+e_m365_connection_get_folders_delta_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         EM365FolderKind kind,
+                                         const gchar *select, /* properties to select, nullable */
+                                         const gchar *delta_link, /* previous delta link */
+                                         guint max_page_size, /* 0 for default by the server */
+                                         EM365ConnectionJsonFunc func, /* function to call with each result 
set */
+                                         gpointer func_user_data, /* user data passed into the 'func' */
+                                         gchar **out_delta_link,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_delta_link != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       if (delta_link)
+               message = m365_connection_new_soup_message (SOUP_METHOD_GET, delta_link, CSM_DEFAULT, NULL);
+
+       if (!message) {
+               const gchar *kind_str = NULL;
+               gchar *uri;
+
+               switch (kind) {
+               case E_M365_FOLDER_KIND_CONTACTS:
+                       kind_str = "contactFolders";
+                       break;
+               case E_M365_FOLDER_KIND_MAIL:
+                       kind_str = "mailFolders";
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       break;
+               }
+
+               g_return_val_if_fail (kind_str != NULL, FALSE);
+
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       kind_str,
+                       NULL,
+                       "delta",
+                       "$select", select,
+                       NULL);
+
+               message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+               if (!message) {
+                       g_free (uri);
+
+                       return FALSE;
+               }
+
+               g_free (uri);
+       }
+
+       if (max_page_size > 0) {
+               gchar *prefer_value;
+
+               prefer_value = g_strdup_printf ("odata.maxpagesize=%u", max_page_size);
+
+               soup_message_headers_append (message->request_headers, "Prefer", prefer_value);
+
+               g_free (prefer_value);
+       }
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.json_func = func;
+       rd.func_user_data = func_user_data;
+       rd.out_delta_link = out_delta_link;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/mailfolder-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_mail_folder_sync (EM365Connection *cnc,
+                                       const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                       const gchar *folder_id, /* nullable - then the 'inbox' is used */
+                                       const gchar *select, /* nullable - properties to select */
+                                       EM365MailFolder **out_folder,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_folder != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "mailFolders",
+               folder_id ? folder_id : "inbox",
+               NULL,
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-post-mailfolders?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/mailfolder-post-childfolders?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_create_mail_folder_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *parent_folder_id, /* NULL for the folder root */
+                                          const gchar *display_name,
+                                          EM365MailFolder **out_mail_folder,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (display_name != NULL, FALSE);
+       g_return_val_if_fail (out_mail_folder != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "mailFolders",
+               parent_folder_id,
+               parent_folder_id ? "childFolders" : NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "displayName", display_name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_mail_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/mailfolder-delete?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_delete_mail_folder_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *folder_id,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "mailFolders", folder_id, NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/mailfolder-copy?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/mailfolder-move?view=graph-rest-1.0&tabs=http
+ */
+gboolean
+e_m365_connection_copy_move_mail_folder_sync (EM365Connection *cnc,
+                                             const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                             const gchar *src_folder_id,
+                                             const gchar *des_folder_id,
+                                             gboolean do_copy,
+                                             EM365MailFolder **out_mail_folder,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (src_folder_id != NULL, FALSE);
+       g_return_val_if_fail (des_folder_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "mailFolders",
+               src_folder_id,
+               do_copy ? "copy" : "move",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "destinationId", des_folder_id);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_mail_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/mailfolder-update?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_rename_mail_folder_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *folder_id,
+                                          const gchar *display_name,
+                                          EM365MailFolder **out_mail_folder,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+       g_return_val_if_fail (display_name != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "mailFolders",
+               folder_id,
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "displayName", display_name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_mail_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-delta?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/contact-delta?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_objects_delta_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         EM365FolderKind kind,
+                                         const gchar *folder_id, /* folder ID to get delta messages in */
+                                         const gchar *select, /* properties to select, nullable */
+                                         const gchar *delta_link, /* previous delta link */
+                                         guint max_page_size, /* 0 for default by the server */
+                                         EM365ConnectionJsonFunc func, /* function to call with each result 
set */
+                                         gpointer func_user_data, /* user data passed into the 'func' */
+                                         gchar **out_delta_link,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+       g_return_val_if_fail (out_delta_link != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       if (delta_link)
+               message = m365_connection_new_soup_message (SOUP_METHOD_GET, delta_link, CSM_DEFAULT, NULL);
+
+       if (!message) {
+               const gchar *kind_str = NULL, *kind_path_str = NULL;
+               gchar *uri;
+
+               switch (kind) {
+               case E_M365_FOLDER_KIND_CONTACTS:
+                       kind_str = "contactFolders";
+                       kind_path_str = "contacts";
+                       break;
+               case E_M365_FOLDER_KIND_MAIL:
+                       kind_str = "mailFolders";
+                       kind_path_str = "messages";
+                       break;
+               default:
+                       g_warn_if_reached ();
+                       break;
+               }
+
+               g_return_val_if_fail (kind_str != NULL && kind_path_str != NULL, FALSE);
+
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       kind_str,
+                       folder_id,
+                       kind_path_str,
+                       "", "delta",
+                       "$select", select,
+                       NULL);
+
+               message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+               if (!message) {
+                       g_free (uri);
+
+                       return FALSE;
+               }
+
+               g_free (uri);
+       }
+
+       if (max_page_size > 0) {
+               gchar *prefer_value;
+
+               prefer_value = g_strdup_printf ("odata.maxpagesize=%u", max_page_size);
+
+               soup_message_headers_append (message->request_headers, "Prefer", prefer_value);
+
+               g_free (prefer_value);
+       }
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.json_func = func;
+       rd.func_user_data = func_user_data;
+       rd.out_delta_link = out_delta_link;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_mail_message_sync (EM365Connection *cnc,
+                                        const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                        const gchar *folder_id,
+                                        const gchar *message_id,
+                                        EM365ConnectionRawDataFunc func,
+                                        gpointer func_user_data,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+       g_return_val_if_fail (message_id != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "messages",
+               message_id,
+               "$value",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, func, func_user_data, cancellable, 
error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-post-messages?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_create_mail_message_sync (EM365Connection *cnc,
+                                           const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                           const gchar *folder_id, /* if NULL, then goes to the Drafts 
folder */
+                                           JsonBuilder *mail_message, /* filled mailMessage object */
+                                           EM365MailMessage **out_created_message, /* free with 
json_object_unref() */
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (mail_message != NULL, FALSE);
+       g_return_val_if_fail (out_created_message != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               folder_id ? "mailFolders" : "messages",
+               folder_id,
+               folder_id ? "messages" : NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, mail_message);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_message, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-post-attachments?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_add_mail_message_attachment_sync (EM365Connection *cnc,
+                                                   const gchar *user_override, /* for which user, NULL to 
use the account user */
+                                                   const gchar *message_id, /* the message to add it to */
+                                                   JsonBuilder *attachment, /* filled attachment object */
+                                                   gchar **out_attachment_id,
+                                                   GCancellable *cancellable,
+                                                   GError **error)
+{
+       SoupMessage *message;
+       JsonObject *added_attachment = NULL;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (attachment != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "messages",
+               message_id,
+               "attachments",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, attachment);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
&added_attachment, cancellable, error);
+
+       if (success && added_attachment && out_attachment_id)
+               *out_attachment_id = g_strdup (e_m365_attachment_get_id (added_attachment));
+
+       if (added_attachment)
+               json_object_unref (added_attachment);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-update?view=graph-rest-1.0&tabs=http */
+
+SoupMessage *
+e_m365_connection_prepare_update_mail_message (EM365Connection *cnc,
+                                              const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                              const gchar *message_id,
+                                              JsonBuilder *mail_message, /* values to update, as a 
mailMessage object */
+                                              GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+       g_return_val_if_fail (message_id != NULL, NULL);
+       g_return_val_if_fail (mail_message != NULL, NULL);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "messages",
+               message_id,
+               NULL,
+               NULL);
+
+       /* The server returns the mailMessage object back, but it can be ignored here */
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return NULL;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, mail_message);
+
+       return message;
+}
+
+gboolean
+e_m365_connection_update_mail_message_sync (EM365Connection *cnc,
+                                           const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                           const gchar *message_id,
+                                           JsonBuilder *mail_message, /* values to update, as a mailMessage 
object */
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (message_id != NULL, FALSE);
+       g_return_val_if_fail (mail_message != NULL, FALSE);
+
+       message = e_m365_connection_prepare_update_mail_message (cnc, user_override, message_id, 
mail_message, error);
+
+       if (!message)
+               return FALSE;
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-copy?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/message-move?view=graph-rest-1.0&tabs=http
+ */
+static SoupMessage *
+e_m365_connection_prepare_copy_move_mail_message (EM365Connection *cnc,
+                                                 const gchar *user_override,
+                                                 const gchar *message_id,
+                                                 const gchar *des_folder_id,
+                                                 gboolean do_copy,
+                                                 GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+       g_return_val_if_fail (message_id != NULL, NULL);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "messages",
+               message_id,
+               do_copy ? "copy" : "move",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "destinationId", des_folder_id);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       return message;
+}
+
+/* out_des_message_ids: Camel-pooled gchar *, new ids, in the same order as in message_ids; can be partial */
+gboolean
+e_m365_connection_copy_move_mail_messages_sync (EM365Connection *cnc,
+                                               const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                               const GSList *message_ids, /* const gchar * */
+                                               const gchar *des_folder_id,
+                                               gboolean do_copy,
+                                               GSList **out_des_message_ids, /* Camel-pooled gchar * */
+                                               GCancellable *cancellable,
+                                               GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (message_ids != NULL, FALSE);
+       g_return_val_if_fail (des_folder_id != NULL, FALSE);
+       g_return_val_if_fail (out_des_message_ids != NULL, FALSE);
+
+       *out_des_message_ids = NULL;
+
+       if (g_slist_next (message_ids)) {
+               GPtrArray *requests;
+               GSList *link;
+               guint total, done = 0;
+
+               total = g_slist_length ((GSList *) message_ids);
+               requests = g_ptr_array_new_full (MIN (E_M365_BATCH_MAX_REQUESTS, MIN (total, 50)), 
g_object_unref);
+
+               for (link = (GSList *) message_ids; link && success; link = g_slist_next (link)) {
+                       const gchar *id = link->data;
+                       SoupMessage *message;
+
+                       message = e_m365_connection_prepare_copy_move_mail_message (cnc, user_override, id, 
des_folder_id, do_copy, error);
+
+                       if (!message) {
+                               success = FALSE;
+                               break;
+                       }
+
+                       g_ptr_array_add (requests, message);
+
+                       if (requests->len == E_M365_BATCH_MAX_REQUESTS || !link->next) {
+                               if (requests->len == 1) {
+                                       JsonObject *response = NULL;
+
+                                       success = m365_connection_send_request_sync (cnc, message, 
e_m365_read_json_object_response_cb, NULL, &response, cancellable, error);
+
+                                       if (response) {
+                                               *out_des_message_ids = g_slist_prepend (*out_des_message_ids,
+                                                       (gpointer) camel_pstring_strdup 
(e_m365_mail_message_get_id (response)));
+                                               json_object_unref (response);
+                                       } else {
+                                               success = FALSE;
+                                       }
+                               } else {
+                                       success = e_m365_connection_batch_request_sync (cnc, E_M365_API_V1_0, 
requests, cancellable, error);
+
+                                       if (success) {
+                                               guint ii;
+
+                                               for (ii = 0; success && ii < requests->len; ii++) {
+                                                       JsonNode *node = NULL;
+
+                                                       message = g_ptr_array_index (requests, ii);
+
+                                                       success = e_m365_connection_json_node_from_message 
(message, NULL, &node, cancellable, error);
+
+                                                       if (success && node && JSON_NODE_HOLDS_OBJECT (node)) 
{
+                                                               JsonObject *response;
+
+                                                               response = json_node_get_object (node);
+
+                                                               if (response) {
+                                                                       *out_des_message_ids = 
g_slist_prepend (*out_des_message_ids,
+                                                                               (gpointer) 
camel_pstring_strdup (e_m365_mail_message_get_id (response)));
+                                                               } else {
+                                                                       success = FALSE;
+                                                               }
+                                                       } else {
+                                                               success = FALSE;
+                                                       }
+
+                                                       if (node)
+                                                               json_node_unref (node);
+                                               }
+                                       }
+                               }
+
+                               g_ptr_array_remove_range (requests, 0, requests->len);
+
+                               done += requests->len;
+
+                               camel_operation_progress (cancellable, done * 100.0 / total);
+                       }
+               }
+
+               g_ptr_array_free (requests, TRUE);
+       } else {
+               SoupMessage *message;
+
+               message = e_m365_connection_prepare_copy_move_mail_message (cnc, user_override, 
message_ids->data, des_folder_id, do_copy, error);
+
+               if (message) {
+                       JsonObject *response = NULL;
+
+                       success = m365_connection_send_request_sync (cnc, message, 
e_m365_read_json_object_response_cb, NULL, &response, cancellable, error);
+
+                       if (response) {
+                               *out_des_message_ids = g_slist_prepend (*out_des_message_ids,
+                                       (gpointer) camel_pstring_strdup (e_m365_mail_message_get_id 
(response)));
+                               json_object_unref (response);
+                       } else {
+                               success = FALSE;
+                       }
+
+                       g_clear_object (&message);
+               } else {
+                       success = FALSE;
+               }
+       }
+
+       *out_des_message_ids = g_slist_reverse (*out_des_message_ids);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-delete?view=graph-rest-1.0&tabs=http */
+
+static SoupMessage *
+e_m365_connection_prepare_delete_mail_message (EM365Connection *cnc,
+                                              const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                              const gchar *message_id,
+                                              GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+       g_return_val_if_fail (message_id != NULL, NULL);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "messages",
+               message_id,
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return NULL;
+       }
+
+       g_free (uri);
+
+       return message;
+}
+
+gboolean
+e_m365_connection_delete_mail_messages_sync (EM365Connection *cnc,
+                                            const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                            const GSList *message_ids, /* const gchar * */
+                                            GSList **out_deleted_ids, /* (transfer container): const gchar 
*, borrowed from message_ids */
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (message_ids != NULL, FALSE);
+
+       if (g_slist_next (message_ids)) {
+               GPtrArray *requests;
+               GSList *link, *from_link = (GSList *) message_ids;
+               guint total, done = 0;
+
+               total = g_slist_length ((GSList *) message_ids);
+               requests = g_ptr_array_new_full (MIN (E_M365_BATCH_MAX_REQUESTS, MIN (total, 50)), 
g_object_unref);
+
+               for (link = (GSList *) message_ids; link && success; link = g_slist_next (link)) {
+                       const gchar *id = link->data;
+                       SoupMessage *message;
+
+                       message = e_m365_connection_prepare_delete_mail_message (cnc, user_override, id, 
error);
+
+                       if (!message) {
+                               success = FALSE;
+                               break;
+                       }
+
+                       g_ptr_array_add (requests, message);
+
+                       if (requests->len == E_M365_BATCH_MAX_REQUESTS || !link->next) {
+                               if (requests->len == 1) {
+                                       success = m365_connection_send_request_sync (cnc, message, NULL, 
e_m365_read_no_response_cb, NULL, cancellable, error);
+                               } else {
+                                       success = e_m365_connection_batch_request_sync (cnc, E_M365_API_V1_0, 
requests, cancellable, error);
+                               }
+
+                               if (success && out_deleted_ids) {
+                                       while (from_link) {
+                                               *out_deleted_ids = g_slist_prepend (*out_deleted_ids, 
from_link->data);
+
+                                               if (from_link == link)
+                                                       break;
+
+                                               from_link = g_slist_next (from_link);
+                                       }
+                               }
+
+                               g_ptr_array_remove_range (requests, 0, requests->len);
+                               from_link = g_slist_next (link);
+
+                               done += requests->len;
+
+                               camel_operation_progress (cancellable, done * 100.0 / total);
+                       }
+               }
+
+               g_ptr_array_free (requests, TRUE);
+       } else {
+               SoupMessage *message;
+
+               message = e_m365_connection_prepare_delete_mail_message (cnc, user_override, 
message_ids->data, error);
+
+               if (message) {
+                       success = m365_connection_send_request_sync (cnc, message, NULL, 
e_m365_read_no_response_cb, NULL, cancellable, error);
+
+                       if (success && out_deleted_ids)
+                               *out_deleted_ids = g_slist_prepend (*out_deleted_ids, message_ids->data);
+
+                       g_clear_object (&message);
+               } else {
+                       success = FALSE;
+               }
+       }
+
+       if (out_deleted_ids && *out_deleted_ids && g_slist_next (*out_deleted_ids))
+               *out_deleted_ids = g_slist_reverse (*out_deleted_ids);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/message-send?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_send_mail_message_sync (EM365Connection *cnc,
+                                    const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                    const gchar *message_id,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (message_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "messages",
+               message_id,
+               "send",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       soup_message_headers_append (message->request_headers, "Content-Length", "0");
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_send_mail_sync (EM365Connection *cnc,
+                                 const gchar *user_override, /* for which user, NULL to use the account user 
*/
+                                 JsonBuilder *request, /* filled sendMail object */
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (request != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "sendMail", NULL, NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, request);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/contactfolder-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_contacts_folder_sync (EM365Connection *cnc,
+                                           const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                           const gchar *folder_id, /* nullable - then the default 'contacts' 
folder is returned */
+                                           const gchar *select, /* nullable - properties to select */
+                                           EM365Folder **out_folder,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_folder != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "contactFolders",
+               folder_id ? folder_id : "contacts",
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/profilephoto-get?view=graph-rest-1.0 */
+
+gboolean
+e_m365_connection_get_contact_photo_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         const gchar *folder_id,
+                                         const gchar *contact_id,
+                                         GByteArray **out_photo,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+       g_return_val_if_fail (contact_id != NULL, FALSE);
+       g_return_val_if_fail (out_photo != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "contactFolders",
+               folder_id,
+               "contacts",
+               "", contact_id,
+               "", "photo",
+               "", "$value",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_to_byte_array_cb, 
out_photo, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/profilephoto-update?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_update_contact_photo_sync (EM365Connection *cnc,
+                                            const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                            const gchar *folder_id,
+                                            const gchar *contact_id,
+                                            const GByteArray *jpeg_photo, /* nullable, to remove the photo */
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "contactFolders",
+               folder_id,
+               "contacts",
+               "", contact_id,
+               "", "photo",
+               "", "$value",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_PUT, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       soup_message_headers_set_content_type (message->request_headers, "image/jpeg", NULL);
+       soup_message_headers_set_content_length (message->request_headers, jpeg_photo ? jpeg_photo->len : 0);
+
+       if (jpeg_photo)
+               soup_message_body_append (message->request_body, SOUP_MEMORY_STATIC, jpeg_photo->data, 
jpeg_photo->len);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/contact-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_contact_sync (EM365Connection *cnc,
+                                   const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                   const gchar *folder_id,
+                                   const gchar *contact_id,
+                                   EM365Contact **out_contact,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (folder_id != NULL, FALSE);
+       g_return_val_if_fail (contact_id != NULL, FALSE);
+       g_return_val_if_fail (out_contact != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "contactFolders",
+               folder_id,
+               "contacts",
+               "", contact_id,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_contact, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-post-contacts?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_create_contact_sync (EM365Connection *cnc,
+                                      const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                      const gchar *folder_id, /* if NULL, then goes to the Drafts folder */
+                                      JsonBuilder *contact, /* filled contact object */
+                                      EM365Contact **out_created_contact, /* free with json_object_unref() */
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (contact != NULL, FALSE);
+       g_return_val_if_fail (out_created_contact != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               folder_id ? "contactFolders" : "contacts",
+               folder_id,
+               folder_id ? "contacts" : NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, contact);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_contact, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/contact-update?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_update_contact_sync (EM365Connection *cnc,
+                                      const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                      const gchar *folder_id,
+                                      const gchar *contact_id,
+                                      JsonBuilder *contact, /* values to update, as a contact object */
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (contact_id != NULL, FALSE);
+       g_return_val_if_fail (contact != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               folder_id ? "contactFolders" : "contacts",
+               folder_id,
+               folder_id ? "contacts" : contact_id,
+               "", folder_id ? contact_id : NULL,
+               NULL);
+
+       /* The server returns the contact object back, but it can be ignored here */
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, contact);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/contact-delete?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_delete_contact_sync (EM365Connection *cnc,
+                                      const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                      const gchar *folder_id,
+                                      const gchar *contact_id,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (contact_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               folder_id ? "contactFolders" : "contacts",
+               folder_id,
+               folder_id ? "contacts" : contact_id,
+               "", folder_id ? contact_id : NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-list-calendargroups?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_list_calendar_groups_sync (EM365Connection *cnc,
+                                            const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                            GSList **out_groups, /* EM365CalendarGroup * - the returned 
calendarGroup objects */
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_groups != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "calendarGroups", NULL, NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_groups;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-post-calendargroups?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_create_calendar_group_sync (EM365Connection *cnc,
+                                             const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                             const gchar *name,
+                                             EM365CalendarGroup **out_created_group,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+       g_return_val_if_fail (out_created_group != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "calendarGroups", NULL, NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "name", name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_group, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendargroup-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_calendar_group_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *group_id,
+                                          EM365CalendarGroup **out_group,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (group_id != NULL, FALSE);
+       g_return_val_if_fail (out_group != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "calendarGroups",
+               group_id,
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_group, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendargroup-update?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_update_calendar_group_sync (EM365Connection *cnc,
+                                             const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                             const gchar *group_id,
+                                             const gchar *name,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (group_id != NULL, FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "calendarGroups",
+               group_id,
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "name", name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendargroup-delete?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_delete_calendar_group_sync (EM365Connection *cnc,
+                                             const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                             const gchar *group_id,
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (group_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "calendarGroups", group_id, NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/calendar?view=graph-rest-1.0 */
+
+gboolean
+e_m365_connection_list_calendars_sync (EM365Connection *cnc,
+                                      const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                      const gchar *group_id, /* nullable, calendar group id for group 
calendars */
+                                      const gchar *select, /* properties to select, nullable */
+                                      GSList **out_calendars, /* EM365Calendar * - the returned calendar 
objects */
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_calendars != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_calendars;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendargroup-post-calendars?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_create_calendar_sync (EM365Connection *cnc,
+                                       const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                       const gchar *group_id, /* nullable, then the default group is used */
+                                       JsonBuilder *calendar,
+                                       EM365Calendar **out_created_calendar,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar != NULL, FALSE);
+       g_return_val_if_fail (out_created_calendar != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               "calendars",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, calendar);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_calendar, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendar-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_calendar_folder_sync (EM365Connection *cnc,
+                                           const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                           const gchar *group_id, /* nullable - then the default group is 
used */
+                                           const gchar *calendar_id, /* nullable - then the default calendar 
is used */
+                                           const gchar *select, /* nullable - properties to select */
+                                           EM365Calendar **out_calendar,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_calendar != NULL, FALSE);
+
+       if (group_id && calendar_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendarGroups",
+                       group_id,
+                       "calendars",
+                       "", calendar_id,
+                       "$select", select,
+                       NULL);
+       } else if (group_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, "groups",
+                       group_id,
+                       "calendar",
+                       NULL,
+                       "$select", select,
+                       NULL);
+       } else if (calendar_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendars",
+                       calendar_id,
+                       NULL,
+                       "$select", select,
+                       NULL);
+       } else {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendar",
+                       NULL,
+                       NULL,
+                       "$select", select,
+                       NULL);
+       }
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_calendar, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendar-update?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_update_calendar_sync (EM365Connection *cnc,
+                                       const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                       const gchar *group_id, /* nullable - then the default group is used */
+                                       const gchar *calendar_id,
+                                       const gchar *name, /* nullable - to keep the existing name */
+                                       EM365CalendarColorType color,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+
+       /* Nothing to change */
+       if (!name && (color == E_M365_CALENDAR_COLOR_NOT_SET || color == E_M365_CALENDAR_COLOR_UNKNOWN))
+               return TRUE;
+
+       if (group_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendarGroups",
+                       group_id,
+                       "calendars",
+                       "", calendar_id,
+                       NULL);
+       } else {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendars",
+                       calendar_id,
+                       NULL,
+                       NULL);
+       }
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_calendar_add_name (builder, name);
+       e_m365_calendar_add_color (builder, color);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendar-delete?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_delete_calendar_sync (EM365Connection *cnc,
+                                       const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                       const gchar *group_id, /* nullable - then the default group is used */
+                                       const gchar *calendar_id,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+
+       if (group_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendarGroups",
+                       group_id,
+                       "calendars",
+                       "", calendar_id,
+                       NULL);
+       } else {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+                       "calendars",
+                       calendar_id,
+                       NULL,
+                       NULL);
+       }
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+static void
+m365_connection_prefer_outlook_timezone (SoupMessage *message,
+                                        const gchar *prefer_outlook_timezone)
+{
+       g_return_if_fail (SOUP_IS_MESSAGE (message));
+
+       if (prefer_outlook_timezone && *prefer_outlook_timezone) {
+               gchar *prefer_value;
+
+               prefer_value = g_strdup_printf ("outlook.timezone=\"%s\"", prefer_outlook_timezone);
+
+               soup_message_headers_append (message->request_headers, "Prefer", prefer_value);
+
+               g_free (prefer_value);
+       }
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-list-events?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_list_events_sync (EM365Connection *cnc,
+                                   const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                   const gchar *group_id, /* nullable, calendar group id for group calendars 
*/
+                                   const gchar *calendar_id,
+                                   const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                   const gchar *select, /* nullable - properties to select */
+                                   GSList **out_events, /* EM365Event * - the returned event objects */
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (out_events != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       m365_connection_prefer_outlook_timezone (message, prefer_outlook_timezone);
+
+       soup_message_headers_append (message->request_headers, "Prefer", 
"outlook.body-content-type=\"text\"");
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_events;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/user-post-events?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/group-post-events?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_create_event_sync (EM365Connection *cnc,
+                                    const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                    const gchar *group_id, /* nullable, then the default group is used */
+                                    const gchar *calendar_id,
+                                    JsonBuilder *event,
+                                    EM365Event **out_created_event,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event != NULL, FALSE);
+       g_return_val_if_fail (out_created_event != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, event);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_event, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-get?view=graph-rest-1.0&tabs=http */
+
+SoupMessage *
+e_m365_connection_prepare_get_event (EM365Connection *cnc,
+                                    const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                    const gchar *group_id, /* nullable, then the default group is used */
+                                    const gchar *calendar_id,
+                                    const gchar *event_id,
+                                    const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                    const gchar *select, /* nullable - properties to select */
+                                    GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+       g_return_val_if_fail (calendar_id != NULL, NULL);
+       g_return_val_if_fail (event_id != NULL, NULL);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return NULL;
+       }
+
+       g_free (uri);
+
+       m365_connection_prefer_outlook_timezone (message, prefer_outlook_timezone);
+       soup_message_headers_append (message->request_headers, "Prefer", 
"outlook.body-content-type=\"text\"");
+
+       return message;
+}
+
+gboolean
+e_m365_connection_get_event_sync (EM365Connection *cnc,
+                                 const gchar *user_override, /* for which user, NULL to use the account user 
*/
+                                 const gchar *group_id, /* nullable, then the default group is used */
+                                 const gchar *calendar_id,
+                                 const gchar *event_id,
+                                 const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                 const gchar *select, /* nullable - properties to select */
+                                 EM365Event **out_event,
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (out_event != NULL, FALSE);
+
+       message = e_m365_connection_prepare_get_event (cnc, user_override, group_id, calendar_id, event_id, 
prefer_outlook_timezone, select, error);
+
+       if (!message)
+               return FALSE;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_event, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+gboolean
+e_m365_connection_get_events_sync (EM365Connection *cnc,
+                                  const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                  const gchar *group_id, /* nullable, then the default group is used */
+                                  const gchar *calendar_id,
+                                  const GSList *event_ids, /* const gchar * */
+                                  const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                  const gchar *select, /* nullable - properties to select */
+                                  GSList **out_events, /* EM365Event *, in the same order as event_ids; can 
return partial list */
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_ids != NULL, FALSE);
+       g_return_val_if_fail (out_events != NULL, FALSE);
+
+       if (g_slist_next (event_ids)) {
+               GPtrArray *requests;
+               GSList *link;
+               guint total, done = 0;
+
+               total = g_slist_length ((GSList *) event_ids);
+               requests = g_ptr_array_new_full (MIN (E_M365_BATCH_MAX_REQUESTS, MIN (total, 50)), 
g_object_unref);
+
+               for (link = (GSList *) event_ids; link && success; link = g_slist_next (link)) {
+                       const gchar *id = link->data;
+                       SoupMessage *message;
+
+                       message = e_m365_connection_prepare_get_event (cnc, user_override, group_id, 
calendar_id, id, prefer_outlook_timezone, select, error);
+
+                       if (!message) {
+                               success = FALSE;
+                               break;
+                       }
+
+                       g_ptr_array_add (requests, message);
+
+                       if (requests->len == E_M365_BATCH_MAX_REQUESTS || !link->next) {
+                               if (requests->len == 1) {
+                                       EM365Event *event = NULL;
+
+                                       success = m365_connection_send_request_sync (cnc, message, 
e_m365_read_json_object_response_cb, NULL, &event, cancellable, error);
+
+                                       if (success)
+                                               *out_events = g_slist_prepend (*out_events, event);
+                               } else {
+                                       success = e_m365_connection_batch_request_sync (cnc, E_M365_API_V1_0, 
requests, cancellable, error);
+
+                                       if (success) {
+                                               guint ii;
+
+                                               for (ii = 0; ii < requests->len && success; ii++) {
+                                                       JsonNode *node = NULL;
+
+                                                       message = requests->pdata[ii];
+                                                       success = e_m365_connection_json_node_from_message 
(message, NULL, &node, cancellable, error);
+
+                                                       if (success && node && JSON_NODE_HOLDS_OBJECT (node)) 
{
+                                                               JsonObject *response;
+
+                                                               response = json_node_get_object (node);
+
+                                                               if (response) {
+                                                                       *out_events = g_slist_prepend 
(*out_events, json_object_ref (response));
+                                                               } else {
+                                                                       success = FALSE;
+                                                               }
+                                                       } else {
+                                                               success = FALSE;
+                                                       }
+
+                                                       if (node)
+                                                               json_node_unref (node);
+                                               }
+                                       }
+                               }
+
+                               g_ptr_array_remove_range (requests, 0, requests->len);
+
+                               done += requests->len;
+
+                               camel_operation_progress (cancellable, done * 100.0 / total);
+                       }
+               }
+
+               g_ptr_array_free (requests, TRUE);
+       } else {
+               SoupMessage *message;
+
+               message = e_m365_connection_prepare_get_event (cnc, user_override, group_id, calendar_id, 
event_ids->data, prefer_outlook_timezone, select, error);
+
+               if (message) {
+                       EM365Event *event = NULL;
+
+                       success = m365_connection_send_request_sync (cnc, message, 
e_m365_read_json_object_response_cb, NULL, &event, cancellable, error);
+
+                       if (success)
+                               *out_events = g_slist_prepend (*out_events, event);
+
+                       g_clear_object (&message);
+               } else {
+                       success = FALSE;
+               }
+       }
+
+       *out_events = g_slist_reverse (*out_events);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-update?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_update_event_sync (EM365Connection *cnc,
+                                    const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                    const gchar *group_id, /* nullable - then the default group is used */
+                                    const gchar *calendar_id,
+                                    const gchar *event_id,
+                                    JsonBuilder *event,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (event != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               NULL);
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, event);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-delete?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_delete_event_sync (EM365Connection *cnc,
+                                    const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                    const gchar *group_id, /* nullable - then the default group is used */
+                                    const gchar *calendar_id,
+                                    const gchar *event_id,
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-accept?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/event-tentativelyaccept?view=graph-rest-1.0&tabs=http
+   https://docs.microsoft.com/en-us/graph/api/event-decline?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_response_event_sync (EM365Connection *cnc,
+                                      const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                      const gchar *group_id, /* nullable - then the default group is used */
+                                      const gchar *calendar_id,
+                                      const gchar *event_id,
+                                      EM365ResponseType response, /* uses only accepted/tentatively 
accepted/declined values */
+                                      const gchar *comment, /* nullable */
+                                      gboolean send_response,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       JsonBuilder *builder;
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (response == E_M365_RESPONSE_ACCEPTED || response == 
E_M365_RESPONSE_TENTATIVELY_ACCEPTED || response == E_M365_RESPONSE_DECLINED, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "", response == E_M365_RESPONSE_TENTATIVELY_ACCEPTED ? "tentativelyAccept" :
+                   response == E_M365_RESPONSE_DECLINED ? "decline" : "accept",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_nonempty_string_member (builder, "comment", comment);
+       e_m365_json_add_boolean_member (builder, "sendResponse", send_response);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-dismissreminder?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_dismiss_reminder_sync (EM365Connection *cnc,
+                                        const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                        const gchar *group_id, /* nullable - then the default group is used 
*/
+                                        const gchar *calendar_id,
+                                        const gchar *event_id,
+                                        GCancellable *cancellable,
+                                        GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "", "dismissReminder",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-list-attachments?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_list_event_attachments_sync (EM365Connection *cnc,
+                                              const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                              const gchar *group_id, /* nullable, then the default group is 
used */
+                                              const gchar *calendar_id,
+                                              const gchar *event_id,
+                                              const gchar *select, /* nullable - properties to select */
+                                              GSList **out_attachments, /* EM365Attachment * */
+                                              GCancellable *cancellable,
+                                              GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (out_attachments != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "", "attachments",
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_attachments;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/attachment-get?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_event_attachment_sync (EM365Connection *cnc,
+                                            const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                            const gchar *group_id, /* nullable, then the default group is 
used */
+                                            const gchar *calendar_id,
+                                            const gchar *event_id,
+                                            const gchar *attachment_id,
+                                            EM365ConnectionRawDataFunc func,
+                                            gpointer func_user_data,
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (attachment_id != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "", "attachments",
+               "", attachment_id,
+               "", "$value",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, func, func_user_data, cancellable, 
error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/event-post-attachments?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_add_event_attachment_sync (EM365Connection *cnc,
+                                            const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                            const gchar *group_id, /* nullable, then the default group is 
used */
+                                            const gchar *calendar_id,
+                                            const gchar *event_id,
+                                            JsonBuilder *in_attachment,
+                                            EM365Attachment **out_attachment, /* nullable */
+                                            GCancellable *cancellable,
+                                            GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (in_attachment != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "", "attachments",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, out_attachment ? CSM_DEFAULT : 
CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, in_attachment);
+
+       success = m365_connection_send_request_sync (cnc, message, out_attachment ? 
e_m365_read_json_object_response_cb : NULL,
+               out_attachment ? NULL : e_m365_read_no_response_cb, out_attachment, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/attachment-delete?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_delete_event_attachment_sync (EM365Connection *cnc,
+                                               const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                               const gchar *group_id, /* nullable, then the default group is 
used */
+                                               const gchar *calendar_id,
+                                               const gchar *event_id,
+                                               const gchar *attachment_id,
+                                               GCancellable *cancellable,
+                                               GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (calendar_id != NULL, FALSE);
+       g_return_val_if_fail (event_id != NULL, FALSE);
+       g_return_val_if_fail (attachment_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               group_id ? "calendarGroups" : "calendars",
+               group_id,
+               group_id ? "calendars" : NULL,
+               "", calendar_id,
+               "", "events",
+               "", event_id,
+               "", "attachments",
+               "", attachment_id,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/calendar-getschedule?view=graph-rest-1.0&tabs=http */
+
+gboolean
+e_m365_connection_get_schedule_sync (EM365Connection *cnc,
+                                    const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                    gint interval_minutes, /* between 5 and 1440, -1 to use the default (30) 
*/
+                                    time_t start_time,
+                                    time_t end_time,
+                                    const GSList *email_addresses, /* const gchar * - SMTP addresses to 
query */
+                                    GSList **out_infos, /* EM365ScheduleInformation * */
+                                    GCancellable *cancellable,
+                                    GError **error)
+{
+       EM365ResponseData rd;
+       JsonBuilder *builder;
+       SoupMessage *message;
+       GSList *link;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (email_addresses != NULL, FALSE);
+       g_return_val_if_fail (out_infos != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_V1_0, NULL,
+               "calendar",
+               "getSchedule",
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+
+       if (interval_minutes > 0)
+               e_m365_json_add_int_member (builder, "interval", interval_minutes);
+
+       e_m365_add_date_time (builder, "startTime", start_time, "UTC");
+       e_m365_add_date_time (builder, "endTime", end_time, "UTC");
+       e_m365_json_begin_array_member (builder, "schedules");
+
+       for (link = (GSList *) email_addresses; link; link = g_slist_next (link)) {
+               const gchar *addr = link->data;
+
+               if (addr && *addr)
+                       json_builder_add_string_value (builder, addr);
+       }
+
+       e_m365_json_end_array_member (builder); /* "schedules" */
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_infos;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlookuser-list-taskgroups?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_list_task_groups_sync (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                GSList **out_groups, /* EM365TaskGroup * - the returned 
outlookTaskGroup objects */
+                                                GCancellable *cancellable,
+                                                GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_groups != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook", "taskGroups", NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_groups;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlookuser-post-taskgroups?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_create_task_group_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         const gchar *name,
+                                         EM365TaskGroup **out_created_group,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+       g_return_val_if_fail (out_created_group != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook", "taskGroups", NULL, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "name", name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_group, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskgroup-get?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_get_task_group_sync (EM365Connection *cnc,
+                                      const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                      const gchar *group_id,
+                                      EM365TaskGroup **out_group,
+                                      GCancellable *cancellable,
+                                      GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (group_id != NULL, FALSE);
+       g_return_val_if_fail (out_group != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               "taskGroups",
+               group_id,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_group, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskgroup-update?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_update_task_group_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         const gchar *group_id,
+                                         const gchar *name,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (group_id != NULL, FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               "taskGroups",
+               group_id,
+               NULL,
+               NULL);
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "name", name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskfolder-delete?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_delete_task_group_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         const gchar *group_id,
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (group_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook", "taskGroups", group_id, NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlookuser-list-taskfolders?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_list_task_folders_sync (EM365Connection *cnc,
+                                         const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                         const gchar *group_id, /* nullable, task group id for group task 
folders */
+                                         const gchar *select, /* properties to select, nullable */
+                                         GSList **out_folders, /* EM365TaskFolder * - the returned 
outlookTaskFolder objects */
+                                         GCancellable *cancellable,
+                                         GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (out_folders != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_folders;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlookuser-post-taskfolders?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_create_task_folder_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *group_id, /* nullable, then the default group is used 
*/
+                                          JsonBuilder *task_folder,
+                                          EM365TaskFolder **out_created_folder,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder != NULL, FALSE);
+       g_return_val_if_fail (out_created_folder != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", "taskFolders",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, task_folder);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskfolder-get?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_get_task_folder_sync (EM365Connection *cnc,
+                                       const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                       const gchar *group_id, /* nullable - then the default group is used */
+                                       const gchar *task_folder_id,
+                                       const gchar *select, /* nullable - properties to select */
+                                       EM365TaskFolder **out_task_folder,
+                                       GCancellable *cancellable,
+                                       GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (out_task_folder != NULL, FALSE);
+
+       if (group_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+                       "outlook",
+                       "taskGroups",
+                       group_id,
+                       "", "taskFolders",
+                       "", task_folder_id,
+                       "$select", select,
+                       NULL);
+       } else {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+                       "outlook",
+                       "taskFolders",
+                       task_folder_id,
+                       "$select", select,
+                       NULL);
+       }
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_task_folder, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskfolder-update?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_update_task_folder_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *group_id, /* nullable - then the default group is 
used */
+                                          const gchar *task_folder_id,
+                                          const gchar *name,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       JsonBuilder *builder;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (name != NULL, FALSE);
+
+       if (group_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+                       "outlook",
+                       "taskGroups",
+                       group_id,
+                       "", "taskFolders",
+                       "", task_folder_id,
+                       NULL);
+       } else {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+                       "outlook",
+                       "taskFolders",
+                       task_folder_id,
+                       NULL);
+       }
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       builder = json_builder_new_immutable ();
+
+       e_m365_json_begin_object_member (builder, NULL);
+       e_m365_json_add_string_member (builder, "name", name);
+       e_m365_json_end_object_member (builder);
+
+       e_m365_connection_set_json_body (message, builder);
+
+       g_object_unref (builder);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskfolder-delete?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_delete_task_folder_sync (EM365Connection *cnc,
+                                          const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                          const gchar *group_id, /* nullable - then the default group is 
used */
+                                          const gchar *task_folder_id,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+
+       if (group_id) {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+                       "outlook",
+                       "taskGroups",
+                       group_id,
+                       "", "taskFolders",
+                       "", task_folder_id,
+                       NULL);
+       } else {
+               uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+                       "outlook",
+                       "taskFolders",
+                       task_folder_id,
+                       NULL);
+       }
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktaskfolder-list-tasks?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_list_tasks_sync (EM365Connection *cnc,
+                                  const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                  const gchar *group_id, /* nullable, task group id for group task folders */
+                                  const gchar *task_folder_id,
+                                  const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                  const gchar *select, /* nullable - properties to select */
+                                  GSList **out_tasks, /* EM365Task * - the returned task objects */
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (out_tasks != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       m365_connection_prefer_outlook_timezone (message, prefer_outlook_timezone);
+       soup_message_headers_append (message->request_headers, "Prefer", 
"outlook.body-content-type=\"text\"");
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_tasks;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlookuser-post-tasks?view=graph-rest-beta&tabs=csharp */
+
+gboolean
+e_m365_connection_create_task_sync (EM365Connection *cnc,
+                                   const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                   const gchar *group_id, /* nullable, then the default group is used */
+                                   const gchar *task_folder_id,
+                                   JsonBuilder *task,
+                                   EM365Task **out_created_task,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task != NULL, FALSE);
+       g_return_val_if_fail (out_created_task != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, task);
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_created_task, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktask-get?view=graph-rest-beta&tabs=http */
+
+SoupMessage *
+e_m365_connection_prepare_get_task (EM365Connection *cnc,
+                                   const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                   const gchar *group_id, /* nullable, then the default group is used */
+                                   const gchar *task_folder_id,
+                                   const gchar *task_id,
+                                   const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                   const gchar *select, /* nullable - properties to select */
+                                   GError **error)
+{
+       SoupMessage *message;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), NULL);
+       g_return_val_if_fail (task_folder_id != NULL, NULL);
+       g_return_val_if_fail (task_id != NULL, NULL);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return NULL;
+       }
+
+       g_free (uri);
+
+       m365_connection_prefer_outlook_timezone (message, prefer_outlook_timezone);
+       soup_message_headers_append (message->request_headers, "Prefer", 
"outlook.body-content-type=\"text\"");
+
+       return message;
+}
+
+gboolean
+e_m365_connection_get_task_sync (EM365Connection *cnc,
+                                const gchar *user_override, /* for which user, NULL to use the account user 
*/
+                                const gchar *group_id, /* nullable, then the default group is used */
+                                const gchar *task_folder_id,
+                                const gchar *task_id,
+                                const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise that 
zone for the returned times */
+                                const gchar *select, /* nullable - properties to select */
+                                EM365Task **out_task,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+       g_return_val_if_fail (out_task != NULL, FALSE);
+
+       message = e_m365_connection_prepare_get_task (cnc, user_override, group_id, task_folder_id, task_id, 
prefer_outlook_timezone, select, error);
+
+       if (!message)
+               return FALSE;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_json_object_response_cb, NULL, 
out_task, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+gboolean
+e_m365_connection_get_tasks_sync (EM365Connection *cnc,
+                                 const gchar *user_override, /* for which user, NULL to use the account user 
*/
+                                 const gchar *group_id, /* nullable, then the default group is used */
+                                 const gchar *task_folder_id,
+                                 const GSList *task_ids, /* const gchar * */
+                                 const gchar *prefer_outlook_timezone, /* nullable - then UTC, otherwise 
that zone for the returned times */
+                                 const gchar *select, /* nullable - properties to select */
+                                 GSList **out_tasks, /* EM365Task *, in the same order as task_ids; can 
return partial list */
+                                 GCancellable *cancellable,
+                                 GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_ids != NULL, FALSE);
+       g_return_val_if_fail (out_tasks != NULL, FALSE);
+
+       if (g_slist_next (task_ids)) {
+               GPtrArray *requests;
+               GSList *link;
+               guint total, done = 0;
+
+               total = g_slist_length ((GSList *) task_ids);
+               requests = g_ptr_array_new_full (MIN (E_M365_BATCH_MAX_REQUESTS, MIN (total, 50)), 
g_object_unref);
+
+               for (link = (GSList *) task_ids; link && success; link = g_slist_next (link)) {
+                       const gchar *id = link->data;
+                       SoupMessage *message;
+
+                       message = e_m365_connection_prepare_get_task (cnc, user_override, group_id, 
task_folder_id, id, prefer_outlook_timezone, select, error);
+
+                       if (!message) {
+                               success = FALSE;
+                               break;
+                       }
+
+                       g_ptr_array_add (requests, message);
+
+                       if (requests->len == E_M365_BATCH_MAX_REQUESTS || !link->next) {
+                               if (requests->len == 1) {
+                                       EM365Task *task = NULL;
+
+                                       success = m365_connection_send_request_sync (cnc, message, 
e_m365_read_json_object_response_cb, NULL, &task, cancellable, error);
+
+                                       if (success)
+                                               *out_tasks = g_slist_prepend (*out_tasks, task);
+                               } else {
+                                       success = e_m365_connection_batch_request_sync (cnc, E_M365_API_BETA, 
requests, cancellable, error);
+
+                                       if (success) {
+                                               guint ii;
+
+                                               for (ii = 0; ii < requests->len && success; ii++) {
+                                                       JsonNode *node = NULL;
+
+                                                       message = requests->pdata[ii];
+                                                       success = e_m365_connection_json_node_from_message 
(message, NULL, &node, cancellable, error);
+
+                                                       if (success && node && JSON_NODE_HOLDS_OBJECT (node)) 
{
+                                                               JsonObject *response;
+
+                                                               response = json_node_get_object (node);
+
+                                                               if (response) {
+                                                                       *out_tasks = g_slist_prepend 
(*out_tasks, json_object_ref (response));
+                                                               } else {
+                                                                       success = FALSE;
+                                                               }
+                                                       } else {
+                                                               success = FALSE;
+                                                       }
+
+                                                       if (node)
+                                                               json_node_unref (node);
+                                               }
+                                       }
+                               }
+
+                               g_ptr_array_remove_range (requests, 0, requests->len);
+
+                               done += requests->len;
+
+                               camel_operation_progress (cancellable, done * 100.0 / total);
+                       }
+               }
+
+               g_ptr_array_free (requests, TRUE);
+       } else {
+               SoupMessage *message;
+
+               message = e_m365_connection_prepare_get_task (cnc, user_override, group_id, task_folder_id, 
task_ids->data, prefer_outlook_timezone, select, error);
+
+               if (message) {
+                       EM365Task *task = NULL;
+
+                       success = m365_connection_send_request_sync (cnc, message, 
e_m365_read_json_object_response_cb, NULL, &task, cancellable, error);
+
+                       if (success)
+                               *out_tasks = g_slist_prepend (*out_tasks, task);
+
+                       g_clear_object (&message);
+               } else {
+                       success = FALSE;
+               }
+       }
+
+       *out_tasks = g_slist_reverse (*out_tasks);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktask-update?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_update_task_sync (EM365Connection *cnc,
+                                   const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                   const gchar *group_id, /* nullable - then the default group is used */
+                                   const gchar *task_folder_id,
+                                   const gchar *task_id,
+                                   JsonBuilder *task,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+       g_return_val_if_fail (task != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               NULL);
+
+       message = m365_connection_new_soup_message ("PATCH", uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, task);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktask-delete?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_delete_task_sync (EM365Connection *cnc,
+                                   const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                   const gchar *group_id, /* nullable - then the default group is used */
+                                   const gchar *task_folder_id,
+                                   const gchar *task_id,
+                                   GCancellable *cancellable,
+                                   GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktask-complete?view=graph-rest-beta */
+
+gboolean
+e_m365_connection_complete_task_sync (EM365Connection *cnc,
+                                     const gchar *user_override, /* for which user, NULL to use the account 
user */
+                                     const gchar *group_id, /* nullable - then the default group is used */
+                                     const gchar *task_folder_id,
+                                     const gchar *task_id,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               "", "complete",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktask-list-attachments?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_list_task_attachments_sync (EM365Connection *cnc,
+                                             const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                             const gchar *group_id, /* nullable, then the default group is 
used */
+                                             const gchar *task_folder_id,
+                                             const gchar *task_id,
+                                             const gchar *select, /* nullable - properties to select */
+                                             GSList **out_attachments, /* EM365Attachment * */
+                                             GCancellable *cancellable,
+                                             GError **error)
+{
+       EM365ResponseData rd;
+       SoupMessage *message;
+       gchar *uri;
+       gboolean success;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+       g_return_val_if_fail (out_attachments != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               "", "attachments",
+               "$select", select,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       memset (&rd, 0, sizeof (EM365ResponseData));
+
+       rd.out_items = out_attachments;
+
+       success = m365_connection_send_request_sync (cnc, message, e_m365_read_valued_response_cb, NULL, &rd, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/attachment-get?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_get_task_attachment_sync (EM365Connection *cnc,
+                                           const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                           const gchar *group_id, /* nullable, then the default group is 
used */
+                                           const gchar *task_folder_id,
+                                           const gchar *task_id,
+                                           const gchar *attachment_id,
+                                           EM365ConnectionRawDataFunc func,
+                                           gpointer func_user_data,
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+       g_return_val_if_fail (attachment_id != NULL, FALSE);
+       g_return_val_if_fail (func != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               "", "attachments",
+               "", attachment_id,
+               "", "$value",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_GET, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, func, func_user_data, cancellable, 
error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/outlooktask-post-attachments?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_add_task_attachment_sync (EM365Connection *cnc,
+                                           const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                           const gchar *group_id, /* nullable, then the default group is 
used */
+                                           const gchar *task_folder_id,
+                                           const gchar *task_id,
+                                           JsonBuilder *in_attachment,
+                                           EM365Attachment **out_attachment, /* nullable */
+                                           GCancellable *cancellable,
+                                           GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+       g_return_val_if_fail (in_attachment != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               "", "attachments",
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_POST, uri, out_attachment ? CSM_DEFAULT : 
CSM_DISABLE_RESPONSE, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       e_m365_connection_set_json_body (message, in_attachment);
+
+       success = m365_connection_send_request_sync (cnc, message, out_attachment ? 
e_m365_read_json_object_response_cb : NULL,
+               out_attachment ? NULL : e_m365_read_no_response_cb, out_attachment, cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/attachment-delete?view=graph-rest-beta&tabs=http */
+
+gboolean
+e_m365_connection_delete_task_attachment_sync (EM365Connection *cnc,
+                                              const gchar *user_override, /* for which user, NULL to use the 
account user */
+                                              const gchar *group_id, /* nullable, then the default group is 
used */
+                                              const gchar *task_folder_id,
+                                              const gchar *task_id,
+                                              const gchar *attachment_id,
+                                              GCancellable *cancellable,
+                                              GError **error)
+{
+       SoupMessage *message;
+       gboolean success;
+       gchar *uri;
+
+       g_return_val_if_fail (E_IS_M365_CONNECTION (cnc), FALSE);
+       g_return_val_if_fail (task_folder_id != NULL, FALSE);
+       g_return_val_if_fail (task_id != NULL, FALSE);
+       g_return_val_if_fail (attachment_id != NULL, FALSE);
+
+       uri = e_m365_connection_construct_uri (cnc, TRUE, user_override, E_M365_API_BETA, NULL,
+               "outlook",
+               group_id ? "taskGroups" : "taskFolders",
+               group_id,
+               "", group_id ? "taskFolders" : NULL,
+               "", task_folder_id,
+               "", "tasks",
+               "", task_id,
+               "", "attachments",
+               "", attachment_id,
+               NULL);
+
+       message = m365_connection_new_soup_message (SOUP_METHOD_DELETE, uri, CSM_DEFAULT, error);
+
+       if (!message) {
+               g_free (uri);
+
+               return FALSE;
+       }
+
+       g_free (uri);
+
+       success = m365_connection_send_request_sync (cnc, message, NULL, e_m365_read_no_response_cb, NULL, 
cancellable, error);
+
+       g_clear_object (&message);
+
+       return success;
+}
diff --git a/src/Microsoft365/common/e-m365-connection.h b/src/Microsoft365/common/e-m365-connection.h
new file mode 100644
index 00000000..e12a18be
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-connection.h
@@ -0,0 +1,758 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_M365_CONNECTION_H
+#define E_M365_CONNECTION_H
+
+#include <glib-object.h>
+
+#include <libebackend/libebackend.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+
+#include "camel-m365-settings.h"
+#include "e-m365-enums.h"
+#include "e-m365-json-utils.h"
+
+/* Currently, as of 2020-06-17, there is a limitation to 20 requests:
+   https://docs.microsoft.com/en-us/graph/known-issues#json-batching */
+#define E_M365_BATCH_MAX_REQUESTS 20
+
+/* Standard GObject macros */
+#define E_TYPE_M365_CONNECTION \
+       (e_m365_connection_get_type ())
+#define E_M365_CONNECTION(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_M365_CONNECTION, EM365Connection))
+#define E_M365_CONNECTION_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_M365_CONNECTION, EM365ConnectionClass))
+#define E_IS_M365_CONNECTION(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_M365_CONNECTION))
+#define E_IS_M365_CONNECTION_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_M365_CONNECTION))
+#define E_M365_CONNECTION_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_M365_CONNECTION))
+
+G_BEGIN_DECLS
+
+typedef enum _EM365ApiVersion {
+       E_M365_API_V1_0,
+       E_M365_API_BETA
+} EM365ApiVersion;
+
+typedef struct _EM365Connection EM365Connection;
+typedef struct _EM365ConnectionClass EM365ConnectionClass;
+typedef struct _EM365ConnectionPrivate EM365ConnectionPrivate;
+
+/* Returns whether can continue */
+typedef gboolean (* EM365ConnectionJsonFunc)   (EM365Connection *cnc,
+                                                const GSList *results, /* JsonObject * - the returned 
objects from the server */
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+typedef gboolean (* EM365ConnectionRawDataFunc)        (EM365Connection *cnc,
+                                                SoupMessage *message,
+                                                GInputStream *raw_data_stream,
+                                                gpointer user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+struct _EM365Connection {
+       GObject parent;
+       EM365ConnectionPrivate *priv;
+};
+
+struct _EM365ConnectionClass {
+       GObjectClass parent_class;
+};
+
+gboolean       e_m365_connection_util_delta_token_failed
+                                               (const GError *error);
+
+GType          e_m365_connection_get_type      (void) G_GNUC_CONST;
+
+EM365Connection *
+               e_m365_connection_new           (ESource *source,
+                                                CamelM365Settings *settings);
+EM365Connection *
+               e_m365_connection_new_for_backend
+                                               (EBackend *backend,
+                                                ESourceRegistry *registry,
+                                                ESource *source,
+                                                CamelM365Settings *settings);
+EM365Connection *
+               e_m365_connection_new_full      (ESource *source,
+                                                CamelM365Settings *settings,
+                                                gboolean allow_reuse);
+ESource *      e_m365_connection_get_source    (EM365Connection *cnc);
+CamelM365Settings *
+               e_m365_connection_get_settings  (EM365Connection *cnc);
+guint          e_m365_connection_get_concurrent_connections
+                                               (EM365Connection *cnc);
+void           e_m365_connection_set_concurrent_connections
+                                               (EM365Connection *cnc,
+                                                guint concurrent_connections);
+GProxyResolver *e_m365_connection_ref_proxy_resolver
+                                               (EM365Connection *cnc);
+void           e_m365_connection_set_proxy_resolver
+                                               (EM365Connection *cnc,
+                                                GProxyResolver *proxy_resolver);
+ESoupAuthBearer *
+               e_m365_connection_ref_bearer_auth
+                                               (EM365Connection *cnc);
+void           e_m365_connection_set_bearer_auth
+                                               (EM365Connection *cnc,
+                                                ESoupAuthBearer *bearer_auth);
+gboolean       e_m365_connection_get_ssl_error_details
+                                               (EM365Connection *cnc,
+                                                gchar **out_certificate_pem,
+                                                GTlsCertificateFlags *out_certificate_errors);
+ESourceAuthenticationResult
+               e_m365_connection_authenticate_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override,
+                                                EM365FolderKind kind,
+                                                const gchar *group_id,
+                                                const gchar *folder_id,
+                                                gchar **out_certificate_pem,
+                                                GTlsCertificateFlags *out_certificate_errors,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_disconnect_sync
+                                               (EM365Connection *cnc,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gchar *                e_m365_connection_construct_uri (EM365Connection *cnc,
+                                                gboolean include_user,
+                                                const gchar *user_override,
+                                                EM365ApiVersion api_version,
+                                                const gchar *api_part, /* NULL for 'users', empty string to 
skip */
+                                                const gchar *resource,
+                                                const gchar *id, /* NULL to skip */
+                                                const gchar *path,
+                                                ...) G_GNUC_NULL_TERMINATED;
+gboolean       e_m365_connection_json_node_from_message
+                                               (SoupMessage *message,
+                                                GInputStream *input_stream,
+                                                JsonNode **out_node,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_batch_request_sync
+                                               (EM365Connection *cnc,
+                                                EM365ApiVersion api_version,
+                                                GPtrArray *requests, /* SoupMessage * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_call_gather_into_slist
+                                               (EM365Connection *cnc,
+                                                const GSList *results, /* JsonObject * - the returned 
objects from the server */
+                                                gpointer user_data, /* expects GSList **, aka pointer to a 
GSList *, where it copies the 'results' */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_categories_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override,
+                                                GSList **out_categories, /* EM365Category * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_mail_folders_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *from_path, /* path for the folder to read, NULL 
for top user folder */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_folders, /* EM365MailFolder * - the returned 
mailFolder objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_folders_delta_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                EM365FolderKind kind,
+                                                const gchar *select, /* nullable - properties to select */
+                                                const gchar *delta_link, /* previous delta link */
+                                                guint max_page_size, /* 0 for default by the server */
+                                                EM365ConnectionJsonFunc func, /* function to call with each 
result set */
+                                                gpointer func_user_data, /* user data passed into the 'func' 
*/
+                                                gchar **out_delta_link,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_mail_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id, /* nullable - then the 'inbox' is 
used */
+                                                const gchar *select, /* nullable - properties to select */
+                                                EM365MailFolder **out_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_mail_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *parent_folder_id, /* NULL for the folder root */
+                                                const gchar *display_name,
+                                                EM365MailFolder **out_mail_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_mail_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_copy_move_mail_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *src_folder_id,
+                                                const gchar *des_folder_id,
+                                                gboolean do_copy,
+                                                EM365MailFolder **out_mail_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_rename_mail_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *display_name,
+                                                EM365MailFolder **out_mail_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_objects_delta_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                EM365FolderKind kind,
+                                                const gchar *folder_id, /* folder ID to get delta messages 
in */
+                                                const gchar *select, /* nullable - properties to select */
+                                                const gchar *delta_link, /* previous delta link */
+                                                guint max_page_size, /* 0 for default by the server */
+                                                EM365ConnectionJsonFunc func, /* function to call with each 
result set */
+                                                gpointer func_user_data, /* user data passed into the 'func' 
*/
+                                                gchar **out_delta_link,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_mail_message_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *message_id,
+                                                EM365ConnectionRawDataFunc func,
+                                                gpointer func_user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_mail_message_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id, /* if NULL, then goes to the Drafts 
folder */
+                                                JsonBuilder *mail_message, /* filled mailMessage object */
+                                                EM365MailMessage **out_created_message, /* free with 
json_object_unref() */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_add_mail_message_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *message_id, /* the message to add it to */
+                                                JsonBuilder *attachment, /* filled attachment object */
+                                                gchar **out_attachment_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+SoupMessage *  e_m365_connection_prepare_update_mail_message
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *message_id,
+                                                JsonBuilder *mail_message, /* values to update, as a 
mailMessage object */
+                                                GError **error);
+gboolean       e_m365_connection_update_mail_message_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *message_id,
+                                                JsonBuilder *mail_message, /* values to update, as a 
mailMessage object */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_copy_move_mail_messages_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const GSList *message_ids, /* const gchar * */
+                                                const gchar *des_folder_id,
+                                                gboolean do_copy,
+                                                GSList **out_des_message_ids, /* Camel-pooled gchar *, can 
be partial */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_mail_messages_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const GSList *message_ids, /* const gchar * */
+                                                GSList **out_deleted_ids, /* (transfer container): const 
gchar *, borrowed from message_ids */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_send_mail_message_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *message_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_send_mail_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                JsonBuilder *request, /* filled sendMail object */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_contacts_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id, /* nullable - then the default 
'contacts' folder is returned */
+                                                const gchar *select, /* nullable - properties to select */
+                                                EM365Folder **out_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_contact_photo_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *contact_id,
+                                                GByteArray **out_photo,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_contact_photo_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *contact_id,
+                                                const GByteArray *jpeg_photo, /* nullable - to remove the 
photo */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_contact_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *contact_id,
+                                                EM365Contact **out_contact,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_contact_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id, /* if NULL, then goes to the Drafts 
folder */
+                                                JsonBuilder *contact, /* filled contact object */
+                                                EM365Contact **out_created_contact, /* free with 
json_object_unref() */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_contact_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *contact_id,
+                                                JsonBuilder *contact, /* values to update, as a contact 
object */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_contact_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *folder_id,
+                                                const gchar *contact_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_calendar_groups_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                GSList **out_groups, /* EM365CalendarGroup * - the returned 
calendarGroup objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_calendar_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *name,
+                                                EM365CalendarGroup **out_created_group,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_calendar_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id,
+                                                EM365CalendarGroup **out_group,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_calendar_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id,
+                                                const gchar *name,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_calendar_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_calendars_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - calendar group for 
group calendars */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_calendars, /* EM365Calendar * - the returned 
calendar objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_calendar_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                JsonBuilder *calendar,
+                                                EM365Calendar **out_created_calendar,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_calendar_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id, /* nullable - then the default 
calendar is used */
+                                                const gchar *select, /* nullable - properties to select */
+                                                EM365Calendar **out_calendar,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_calendar_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *name, /* nullable - to keep the existing name */
+                                                EM365CalendarColorType color,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_calendar_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_events_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - calendar group for 
group calendars */
+                                                const gchar *calendar_id,
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_events, /* EM365Event * - the returned event 
objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_event_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                JsonBuilder *event,
+                                                EM365Event **out_created_event,
+                                                GCancellable *cancellable,
+                                                GError **error);
+SoupMessage *  e_m365_connection_prepare_get_event
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GError **error);
+gboolean       e_m365_connection_get_event_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                EM365Event **out_event,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_events_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const GSList *event_ids, /* const gchar * */
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_events, /* EM365Event *, in the same order as 
event_ids; can return partial list */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_event_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                JsonBuilder *event,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_event_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_response_event_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                EM365ResponseType response, /* uses only 
accepted/tentatively accepted/declined values */
+                                                const gchar *comment, /* nullable */
+                                                gboolean send_response,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_dismiss_reminder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_event_attachments_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_attachments, /* EM365Attachment * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_event_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                const gchar *attachment_id,
+                                                EM365ConnectionRawDataFunc func,
+                                                gpointer func_user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_add_event_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                JsonBuilder *in_attachment,
+                                                EM365Attachment **out_attachment, /* nullable */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_event_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *calendar_id,
+                                                const gchar *event_id,
+                                                const gchar *attachment_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_schedule_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                gint interval_minutes, /* between 5 and 1440, -1 to use the 
default (30) */
+                                                time_t start_time,
+                                                time_t end_time,
+                                                const GSList *email_addresses, /* const gchar * - SMTP 
addresses to query */
+                                                GSList **out_infos, /* EM365ScheduleInformation * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_task_groups_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                GSList **out_groups, /* EM365TaskGroup * - the returned 
outlookTaskGroup objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_task_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *name,
+                                                EM365TaskGroup **out_created_group,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_task_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id,
+                                                EM365TaskGroup **out_group,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_task_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id,
+                                                const gchar *name,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_task_group_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_task_folders_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, task group id for group 
task folders */
+                                                const gchar *select, /* properties to select, nullable */
+                                                GSList **out_folders, /* EM365TaskFolder * - the returned 
outlookTaskFolder objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_task_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *select, /* nullable - properties to select */
+                                                EM365TaskFolder **out_task_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_task_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                JsonBuilder *task_folder,
+                                                EM365TaskFolder **out_created_folder,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_task_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *name,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_task_folder_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_tasks_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, task group id for group 
task folders */
+                                                const gchar *task_folder_id,
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_tasks, /* EM365Task * - the returned task 
objects */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_create_task_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                JsonBuilder *task,
+                                                EM365Task **out_created_task,
+                                                GCancellable *cancellable,
+                                                GError **error);
+SoupMessage *  e_m365_connection_prepare_get_task
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GError **error);
+gboolean       e_m365_connection_get_task_sync (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                EM365Task **out_task,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_tasks_sync(EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const GSList *task_ids, /* const gchar * */
+                                                const gchar *prefer_outlook_timezone, /* nullable - then 
UTC, otherwise that zone for the returned times */
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_tasks, /* EM365Task *, in the same order as 
task_ids; can return partial list */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_update_task_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                JsonBuilder *task,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_task_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_complete_task_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable - then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_list_task_attachments_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                const gchar *select, /* nullable - properties to select */
+                                                GSList **out_attachments, /* EM365Attachment * */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_get_task_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                const gchar *attachment_id,
+                                                EM365ConnectionRawDataFunc func,
+                                                gpointer func_user_data,
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_add_task_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                JsonBuilder *in_attachment,
+                                                EM365Attachment **out_attachment, /* nullable */
+                                                GCancellable *cancellable,
+                                                GError **error);
+gboolean       e_m365_connection_delete_task_attachment_sync
+                                               (EM365Connection *cnc,
+                                                const gchar *user_override, /* for which user, NULL to use 
the account user */
+                                                const gchar *group_id, /* nullable, then the default group 
is used */
+                                                const gchar *task_folder_id,
+                                                const gchar *task_id,
+                                                const gchar *attachment_id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+
+G_END_DECLS
+
+#endif /* E_M365_CONNECTION_H */
diff --git a/src/Microsoft365/common/e-m365-enums.h b/src/Microsoft365/common/e-m365-enums.h
new file mode 100644
index 00000000..ea5919b6
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-enums.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_M365_ENUMS_H
+#define E_M365_ENUMS_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+       E_M365_FOLDER_KIND_UNKNOWN,
+       E_M365_FOLDER_KIND_MAIL,
+       E_M365_FOLDER_KIND_CALENDAR,
+       E_M365_FOLDER_KIND_CONTACTS,
+       E_M365_FOLDER_KIND_SEARCH,
+       E_M365_FOLDER_KIND_TASKS,
+       E_M365_FOLDER_KIND_MEMOS
+} EM365FolderKind;
+
+G_END_DECLS
+
+#endif /* E_M365_ENUMS_H */
diff --git a/src/Microsoft365/common/e-m365-json-utils.c b/src/Microsoft365/common/e-m365-json-utils.c
new file mode 100644
index 00000000..9427331d
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-json-utils.c
@@ -0,0 +1,3889 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <stdio.h>
+#include <json-glib/json-glib.h>
+
+#include "e-m365-json-utils.h"
+
+typedef struct _MapData {
+       const gchar *json_value;
+       gint enum_value;
+} MapData;
+
+static gint
+m365_json_utils_json_value_as_enum (const gchar *json_value,
+                                   const MapData *items,
+                                   guint n_items,
+                                   gint not_set_value,
+                                   gint unknown_value)
+{
+       guint ii;
+
+       if (!json_value)
+               return not_set_value;
+
+       for (ii = 0; ii < n_items; ii++) {
+               if (items[ii].json_value && g_ascii_strcasecmp (items[ii].json_value, json_value) == 0)
+                       return items[ii].enum_value;
+       }
+
+       return unknown_value;
+}
+
+static gint
+m365_json_utils_get_json_as_enum (JsonObject *object,
+                                 const gchar *string_member_name,
+                                 const MapData *items,
+                                 guint n_items,
+                                 gint not_set_value,
+                                 gint unknown_value)
+{
+       return m365_json_utils_json_value_as_enum (e_m365_json_get_string_member (object, string_member_name, 
NULL),
+               items, n_items, not_set_value, unknown_value);
+}
+
+static void
+m365_json_utils_add_enum_as_json (JsonBuilder *builder,
+                                 const gchar *string_member_name,
+                                 gint enum_value,
+                                 const MapData *items,
+                                 guint n_items,
+                                 gint not_set_value,
+                                 gint default_value)
+{
+       const gchar *json_value = NULL, *default_value_str = NULL;
+       guint ii;
+
+       if (enum_value == not_set_value) {
+               if (string_member_name)
+                       e_m365_json_add_null_member (builder, string_member_name);
+               return;
+       }
+
+       for (ii = 0; ii < n_items; ii++) {
+               if (items[ii].enum_value == default_value) {
+                       default_value_str = items[ii].json_value;
+
+                       if (json_value)
+                               break;
+               }
+
+               if (items[ii].enum_value == enum_value) {
+                       json_value = items[ii].json_value;
+
+                       if (default_value_str)
+                               break;
+               }
+       }
+
+       if (!json_value) {
+               g_warning ("%s: Failed to find enum value %d for member '%s'", G_STRFUNC, enum_value, 
string_member_name);
+               json_value = default_value_str;
+       }
+
+       if (json_value) {
+               if (string_member_name)
+                       e_m365_json_add_string_member (builder, string_member_name, json_value);
+               else
+                       json_builder_add_string_value (builder, json_value ? json_value : "");
+       }
+}
+
+static MapData attachment_data_type_map[] = {
+       { "#microsoft.graph.fileAttachment",            E_M365_ATTACHMENT_DATA_TYPE_FILE },
+       { "#microsoft.graph.itemAttachment",            E_M365_ATTACHMENT_DATA_TYPE_ITEM },
+       { "#microsoft.graph.referenceAttachment",       E_M365_ATTACHMENT_DATA_TYPE_REFERENCE }
+};
+
+static MapData attendee_map[] = {
+       { "required", E_M365_ATTENDEE_REQUIRED },
+       { "optional", E_M365_ATTENDEE_OPTIONAL },
+       { "resource", E_M365_ATTENDEE_RESOURCE }
+};
+
+static struct _color_map {
+       const gchar *name;
+       const gchar *rgb;
+       EM365CalendarColorType value;
+} color_map[] = {
+       { "auto",       NULL,           E_M365_CALENDAR_COLOR_AUTO },
+       { "lightBlue",  "#0078d4",      E_M365_CALENDAR_COLOR_LIGHT_BLUE },
+       { "lightGreen", "#b67dfa",      E_M365_CALENDAR_COLOR_LIGHT_GREEN },
+       { "lightOrange","#25c4fe",      E_M365_CALENDAR_COLOR_LIGHT_ORANGE },
+       { "lightGray",  "#968681",      E_M365_CALENDAR_COLOR_LIGHT_GRAY },
+       { "lightYellow","#ffc699",      E_M365_CALENDAR_COLOR_LIGHT_YELLOW }, /* Navy in web UI */
+       { "lightTeal",  "#fc7c78",      E_M365_CALENDAR_COLOR_LIGHT_TEAL },
+       { "lightPink",  "#1cff73",      E_M365_CALENDAR_COLOR_LIGHT_PINK },
+       { "lightBrown", "#8bb256",      E_M365_CALENDAR_COLOR_LIGHT_BROWN }, /* Purple in web UI */
+       { "lightRed",   "#3af0e0",      E_M365_CALENDAR_COLOR_LIGHT_RED },
+       { "maxColor",   NULL,           E_M365_CALENDAR_COLOR_MAX_COLOR }
+};
+
+static MapData content_type_map[] = {
+       { "text", E_M365_ITEM_BODY_CONTENT_TYPE_TEXT },
+       { "html", E_M365_ITEM_BODY_CONTENT_TYPE_HTML }
+};
+
+static MapData day_of_week_map[] = {
+       { "sunday",     E_M365_DAY_OF_WEEK_SUNDAY },
+       { "monday",     E_M365_DAY_OF_WEEK_MONDAY },
+       { "tuesday",    E_M365_DAY_OF_WEEK_TUESDAY },
+       { "wednesday",  E_M365_DAY_OF_WEEK_WEDNESDAY },
+       { "thursday",   E_M365_DAY_OF_WEEK_THURSDAY },
+       { "friday",     E_M365_DAY_OF_WEEK_FRIDAY },
+       { "saturday",   E_M365_DAY_OF_WEEK_SATURDAY }
+};
+
+static MapData event_type_map[] = {
+       { "singleInstance",     E_M365_EVENT_TYPE_SINGLE_INSTANCE },
+       { "occurrence",         E_M365_EVENT_TYPE_OCCURRENCE },
+       { "exception",          E_M365_EVENT_TYPE_EXCEPTION },
+       { "seriesMaster",       E_M365_EVENT_TYPE_SERIES_MASTER }
+};
+
+static MapData flag_status_map[] = {
+       { "notFlagged", E_M365_FOLLOWUP_FLAG_STATUS_NOT_FLAGGED },
+       { "complete",   E_M365_FOLLOWUP_FLAG_STATUS_COMPLETE },
+       { "flagged",    E_M365_FOLLOWUP_FLAG_STATUS_FLAGGED }
+};
+
+static MapData free_busy_status_map[] = {
+       { "unknown",            E_M365_FREE_BUSY_STATUS_UNKNOWN },
+       { "free",               E_M365_FREE_BUSY_STATUS_FREE },
+       { "tentative",          E_M365_FREE_BUSY_STATUS_TENTATIVE },
+       { "busy",               E_M365_FREE_BUSY_STATUS_BUSY },
+       { "oof",                E_M365_FREE_BUSY_STATUS_OOF },
+       { "workingElsewhere",   E_M365_FREE_BUSY_STATUS_WORKING_ELSEWHERE }
+};
+
+static MapData importance_map[] = {
+       { "low",        E_M365_IMPORTANCE_LOW },
+       { "normal",     E_M365_IMPORTANCE_NORMAL },
+       { "high",       E_M365_IMPORTANCE_HIGH }
+};
+
+static MapData inference_classification_map[] = {
+       { "focused",    E_M365_INFERENCE_CLASSIFICATION_FOCUSED },
+       { "other",      E_M365_INFERENCE_CLASSIFICATION_OTHER }
+};
+
+static MapData location_type_map[] = {
+       { "default",            E_M365_LOCATION_DEFAULT },
+       { "conferenceRoom",     E_M365_LOCATION_CONFERENCE_ROOM },
+       { "homeAddress",        E_M365_LOCATION_HOME_ADDRESS },
+       { "businessAddress",    E_M365_LOCATION_BUSINESS_ADDRESS },
+       { "geoCoordinates",     E_M365_LOCATION_GEO_COORDINATES },
+       { "streetAddress",      E_M365_LOCATION_STREET_ADDRESS },
+       { "hotel",              E_M365_LOCATION_HOTEL },
+       { "restaurant",         E_M365_LOCATION_RESTAURANT },
+       { "localBusiness",      E_M365_LOCATION_LOCAL_BUSINESS },
+       { "postalAddress",      E_M365_LOCATION_POSTAL_ADDRESS }
+};
+
+static MapData meeting_provider_map[] = {
+       { "unknown",            E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN },
+       { "skypeForBusiness",   E_M365_ONLINE_MEETING_PROVIDER_SKYPE_FOR_BUSINESS },
+       { "skypeForConsumer",   E_M365_ONLINE_MEETING_PROVIDER_SKYPE_FOR_CONSUMER },
+       { "teamsForBusiness",   E_M365_ONLINE_MEETING_PROVIDER_TEAMS_FOR_BUSINESS }
+};
+
+static MapData phone_map[] = {
+       { "home",       E_M365_PHONE_HOME },
+       { "business",   E_M365_PHONE_BUSINESS },
+       { "mobile",     E_M365_PHONE_MOBILE },
+       { "other",      E_M365_PHONE_OTHER },
+       { "assistant",  E_M365_PHONE_ASSISTANT },
+       { "homeFax",    E_M365_PHONE_HOMEFAX },
+       { "businessFax",E_M365_PHONE_BUSINESSFAX },
+       { "otherFax",   E_M365_PHONE_OTHERFAX },
+       { "pager",      E_M365_PHONE_PAGER },
+       { "radio",      E_M365_PHONE_RADIO }
+};
+
+static MapData recurrence_pattern_map[] = {
+       { "daily",              E_M365_RECURRENCE_PATTERN_DAILY },
+       { "weekly",             E_M365_RECURRENCE_PATTERN_WEEKLY },
+       { "absoluteMonthly",    E_M365_RECURRENCE_PATTERN_ABSOLUTE_MONTHLY },
+       { "relativeMonthly",    E_M365_RECURRENCE_PATTERN_RELATIVE_MONTHLY },
+       { "absoluteYearly",     E_M365_RECURRENCE_PATTERN_ABSOLUTE_YEARLY },
+       { "relativeYearly",     E_M365_RECURRENCE_PATTERN_RELATIVE_YEARLY }
+};
+
+static MapData recurrence_range_map[] = {
+       { "endDate",    E_M365_RECURRENCE_RANGE_ENDDATE },
+       { "noEnd",      E_M365_RECURRENCE_RANGE_NOEND },
+       { "numbered",   E_M365_RECURRENCE_RANGE_NUMBERED }
+};
+
+static MapData response_map[] = {
+       { "None",               E_M365_RESPONSE_NONE },
+       { "Organizer",          E_M365_RESPONSE_ORGANIZER },
+       { "TentativelyAccepted",E_M365_RESPONSE_TENTATIVELY_ACCEPTED },
+       { "Accepted",           E_M365_RESPONSE_ACCEPTED },
+       { "Declined",           E_M365_RESPONSE_DECLINED },
+       { "NotResponded",       E_M365_RESPONSE_NOT_RESPONDED }
+};
+
+static MapData sensitivity_map[] = {
+       { "normal",             E_M365_SENSITIVITY_NORMAL },
+       { "personal",           E_M365_SENSITIVITY_PERSONAL },
+       { "private",            E_M365_SENSITIVITY_PRIVATE },
+       { "confidential",       E_M365_SENSITIVITY_CONFIDENTIAL }
+};
+
+static MapData status_map[] = {
+       { "notStarted",         E_M365_STATUS_NOT_STARTED },
+       { "inProgress",         E_M365_STATUS_IN_PROGRESS },
+       { "completed",          E_M365_STATUS_COMPLETED },
+       { "waitingOnOthers",    E_M365_STATUS_WAITING_ON_OTHERS },
+       { "deferred",           E_M365_STATUS_DEFERRED }
+};
+
+static MapData week_index_map[] = {
+       { "first",      E_M365_WEEK_INDEX_FIRST },
+       { "second",     E_M365_WEEK_INDEX_SECOND },
+       { "third",      E_M365_WEEK_INDEX_THIRD },
+       { "fourth",     E_M365_WEEK_INDEX_FOURTH },
+       { "last",       E_M365_WEEK_INDEX_LAST }
+};
+
+const gchar *
+e_m365_calendar_color_to_rgb (EM365CalendarColorType color)
+{
+       gint ii;
+
+       for (ii = 0; ii < G_N_ELEMENTS (color_map); ii++) {
+               if (color == color_map[ii].value)
+                       return color_map[ii].rgb;
+       }
+
+       return NULL;
+}
+
+EM365CalendarColorType
+e_m365_rgb_to_calendar_color (const gchar *rgb)
+{
+       EM365CalendarColorType res;
+       gint ii, rr, gg, bb;
+       gdouble distance, res_distance = -1.0;
+
+       if (!rgb || !*rgb)
+               return E_M365_CALENDAR_COLOR_NOT_SET;
+
+       for (ii = 0; ii < G_N_ELEMENTS (color_map); ii++) {
+               if (color_map[ii].rgb && g_ascii_strcasecmp (color_map[ii].rgb, rgb) == 0)
+                       return color_map[ii].value;
+       }
+
+       /* When exact match did not work, approximate to the closest */
+
+       if (sscanf (rgb, "#%02x%02x%02x", &rr, &gg, &bb) != 3)
+               return E_M365_CALENDAR_COLOR_UNKNOWN;
+
+       distance = (rr * rr) + (gg * gg) + (bb * bb);
+       res = E_M365_CALENDAR_COLOR_UNKNOWN;
+
+       for (ii = 0; ii < G_N_ELEMENTS (color_map); ii++) {
+               if (color_map[ii].rgb && sscanf (color_map[ii].rgb, "#%02x%02x%02x", &rr, &gg, &bb) == 3) {
+                       gdouble candidate_distance;
+
+                       candidate_distance = (rr * rr) + (gg * gg) + (bb * bb) - distance;
+
+                       if (candidate_distance < 0.0)
+                               candidate_distance *= -1.0;
+
+                       if (!ii || candidate_distance < res_distance) {
+                               res_distance = candidate_distance;
+                               res = color_map[ii].value;
+                       }
+               }
+       }
+
+       return res;
+}
+
+JsonArray *
+e_m365_json_get_array_member (JsonObject *object,
+                             const gchar *member_name)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, NULL);
+       g_return_val_if_fail (member_name != NULL, NULL);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node || JSON_NODE_HOLDS_NULL (node))
+               return NULL;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_ARRAY (node), NULL);
+
+       return json_node_get_array (node);
+}
+
+void
+e_m365_json_begin_array_member (JsonBuilder *builder,
+                               const gchar *member_name)
+{
+       if (member_name && *member_name)
+               json_builder_set_member_name (builder, member_name);
+
+       json_builder_begin_array (builder);
+}
+
+void
+e_m365_json_end_array_member (JsonBuilder *builder)
+{
+       json_builder_end_array (builder);
+}
+
+gboolean
+e_m365_json_get_boolean_member (JsonObject *object,
+                               const gchar *member_name,
+                               gboolean default_value)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, default_value);
+       g_return_val_if_fail (member_name != NULL, default_value);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node || JSON_NODE_HOLDS_NULL (node))
+               return default_value;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_VALUE (node), default_value);
+
+       return json_node_get_boolean (node);
+}
+
+void
+e_m365_json_add_boolean_member (JsonBuilder *builder,
+                               const gchar *member_name,
+                               gboolean value)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       json_builder_set_member_name (builder, member_name);
+       json_builder_add_boolean_value (builder, value);
+}
+
+gdouble
+e_m365_json_get_double_member (JsonObject *object,
+                              const gchar *member_name,
+                              gdouble default_value)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, default_value);
+       g_return_val_if_fail (member_name != NULL, default_value);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node || JSON_NODE_HOLDS_NULL (node))
+               return default_value;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_VALUE (node), default_value);
+
+       return json_node_get_double (node);
+}
+
+void
+e_m365_json_add_double_member (JsonBuilder *builder,
+                              const gchar *member_name,
+                              gdouble value)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       json_builder_set_member_name (builder, member_name);
+       json_builder_add_double_value (builder, value);
+}
+
+gint64
+e_m365_json_get_int_member (JsonObject *object,
+                           const gchar *member_name,
+                           gint64 default_value)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, default_value);
+       g_return_val_if_fail (member_name != NULL, default_value);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node || JSON_NODE_HOLDS_NULL (node))
+               return default_value;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_VALUE (node), default_value);
+
+       return json_node_get_int (node);
+}
+
+void
+e_m365_json_add_int_member (JsonBuilder *builder,
+                           const gchar *member_name,
+                           gint64 value)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       json_builder_set_member_name (builder, member_name);
+       json_builder_add_int_value (builder, value);
+}
+
+gboolean
+e_m365_json_get_null_member (JsonObject *object,
+                            const gchar *member_name,
+                            gboolean default_value)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, default_value);
+       g_return_val_if_fail (member_name != NULL, default_value);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node)
+               return default_value;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_NULL (node), default_value);
+
+       return json_node_is_null (node);
+}
+
+void
+e_m365_json_add_null_member (JsonBuilder *builder,
+                            const gchar *member_name)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       json_builder_set_member_name (builder, member_name);
+       json_builder_add_null_value (builder);
+}
+
+JsonObject *
+e_m365_json_get_object_member (JsonObject *object,
+                              const gchar *member_name)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, NULL);
+       g_return_val_if_fail (member_name != NULL, NULL);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node || JSON_NODE_HOLDS_NULL (node))
+               return NULL;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_OBJECT (node), NULL);
+
+       return json_node_get_object (node);
+}
+
+void
+e_m365_json_begin_object_member (JsonBuilder *builder,
+                                const gchar *member_name)
+{
+       if (member_name && *member_name)
+               json_builder_set_member_name (builder, member_name);
+
+       json_builder_begin_object (builder);
+}
+
+void
+e_m365_json_end_object_member (JsonBuilder *builder)
+{
+       json_builder_end_object (builder);
+}
+
+const gchar *
+e_m365_json_get_string_member (JsonObject *object,
+                              const gchar *member_name,
+                              const gchar *default_value)
+{
+       JsonNode *node;
+
+       g_return_val_if_fail (object != NULL, default_value);
+       g_return_val_if_fail (member_name != NULL, default_value);
+
+       node = json_object_get_member (object, member_name);
+
+       if (!node || JSON_NODE_HOLDS_NULL (node))
+               return default_value;
+
+       g_return_val_if_fail (JSON_NODE_HOLDS_VALUE (node), default_value);
+
+       return json_node_get_string (node);
+}
+
+void
+e_m365_json_add_string_member (JsonBuilder *builder,
+                              const gchar *member_name,
+                              const gchar *value)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       json_builder_set_member_name (builder, member_name);
+       json_builder_add_string_value (builder, value ? value : "");
+}
+
+void
+e_m365_json_add_nonempty_string_member (JsonBuilder *builder,
+                                       const gchar *member_name,
+                                       const gchar *value)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       if (value && *value)
+               e_m365_json_add_string_member (builder, member_name, value);
+}
+
+void
+e_m365_json_add_nonempty_or_null_string_member (JsonBuilder *builder,
+                                               const gchar *member_name,
+                                               const gchar *value)
+{
+       g_return_if_fail (member_name && *member_name);
+
+       if (value && *value)
+               e_m365_json_add_string_member (builder, member_name, value);
+       else
+               e_m365_json_add_null_member (builder, member_name);
+}
+
+EM365Date
+e_m365_date_get (JsonObject *object,
+                const gchar *member_name)
+{
+       const gchar *value;
+       gint year = 0, month = 0, day = 0;
+
+       value = e_m365_json_get_string_member (object, member_name, NULL);
+
+       if (!value || !*value)
+               return -1;
+
+       if (sscanf (value, "%04d-%02d-%02d", &year, &month, &day) != 3) {
+               g_warning ("%s: Failed to decode date '%s' of member '%s'", G_STRFUNC, value, member_name);
+               return -1;
+       }
+
+       return e_m365_date_encode (year, month, day);
+}
+
+void
+e_m365_add_date (JsonBuilder *builder,
+                const gchar *member_name,
+                EM365Date value)
+{
+       gint year, month, day;
+
+       if (e_m365_date_decode (value, &year, &month, &day)) {
+               gchar buff[128];
+
+               g_snprintf (buff, sizeof (buff), "%04d-%02d-%02d", year, month, day);
+               e_m365_json_add_string_member (builder, member_name, buff);
+       }
+}
+
+gboolean
+e_m365_date_decode (EM365Date dt,
+                   gint *out_year,
+                   gint *out_month,
+                   gint *out_day)
+{
+       g_return_val_if_fail (out_year != NULL, FALSE);
+       g_return_val_if_fail (out_month != NULL, FALSE);
+       g_return_val_if_fail (out_day != NULL, FALSE);
+
+       if (dt <= 0)
+               return FALSE;
+
+       *out_year = dt % 10000;
+       *out_month = (dt / 10000) % 100;
+       *out_day = (dt / 1000000) % 100;
+
+       return *out_year > 1000 &&
+               *out_month >= 1 && *out_month <= 12 &&
+               *out_day >= 1 && *out_day <= 31;
+}
+
+EM365Date
+e_m365_date_encode (gint year,
+                   gint month,
+                   gint day)
+{
+       g_return_val_if_fail (year > 0 && year < 10000, -1);
+       g_return_val_if_fail (month >= 1 && month <= 12, -1);
+       g_return_val_if_fail (day >= 1 && day <= 31, -1);
+
+       return year + (10000 * month) + (1000000 * day);
+}
+
+EM365TimeOfDay
+e_m365_time_of_day_get (JsonObject *object,
+                       const gchar *member_name)
+{
+       const gchar *value;
+       gint hour = 0, minute = 0, second = 0, fraction = 0;
+
+       value = e_m365_json_get_string_member (object, member_name, NULL);
+
+       if (!value || !*value)
+               return -1;
+
+       if (sscanf (value, "%02d:%02d:%02d.%07d", &hour, &minute, &second, &fraction) != 4) {
+               g_warning ("%s: Failed to decode timeOfDay '%s' of member '%s'", G_STRFUNC, value, 
member_name);
+               return -1;
+       }
+
+       return e_m365_time_of_day_encode (hour, minute, second, fraction);
+}
+
+void
+e_m365_add_time_of_day (JsonBuilder *builder,
+                       const gchar *member_name,
+                       EM365TimeOfDay value)
+{
+       gint hour, minute, second, fraction;
+
+       if (e_m365_time_of_day_decode (value, &hour, &minute, &second, &fraction)) {
+               gchar buff[128];
+
+               g_snprintf (buff, sizeof (buff), "%02d:%02d:%02d.%07d", hour, minute, second, fraction);
+               e_m365_json_add_string_member (builder, member_name, buff);
+       }
+}
+
+gboolean
+e_m365_time_of_day_decode (EM365TimeOfDay tod,
+                          gint *out_hour,
+                          gint *out_minute,
+                          gint *out_second,
+                          gint *out_fraction)
+{
+       g_return_val_if_fail (out_hour != NULL, FALSE);
+       g_return_val_if_fail (out_minute != NULL, FALSE);
+       g_return_val_if_fail (out_second != NULL, FALSE);
+       g_return_val_if_fail (out_fraction != NULL, FALSE);
+
+       if (tod <= 0)
+               return FALSE;
+
+       *out_hour = tod % 100;
+       *out_minute = (tod / 100) % 100;
+       *out_second = (tod / 10000) % 100;
+       *out_fraction = tod / 1000000;
+
+       return *out_hour >= 0 && *out_hour < 24 &&
+               *out_minute >= 0 && *out_minute < 60 &&
+               *out_second >= 0 && *out_second < 60;
+}
+
+EM365TimeOfDay
+e_m365_time_of_day_encode (gint hour,
+                          gint minute,
+                          gint second,
+                          gint fraction)
+{
+       g_return_val_if_fail (hour >= 0 && hour < 24, -1);
+       g_return_val_if_fail (minute >= 0 && minute < 60, -1);
+       g_return_val_if_fail (second >= 0 && second < 60, -1);
+       g_return_val_if_fail (fraction >= 0 && fraction < 10000000, -1);
+
+       return ((EM365TimeOfDay) hour) + (100L * minute) + (10000L * second) + (1000000L * fraction);
+}
+
+time_t
+e_m365_get_date_time_offset_member (JsonObject *object,
+                                   const gchar *member_name)
+{
+       const gchar *value;
+       time_t res = (time_t) 0;
+
+       value = e_m365_json_get_string_member (object, member_name, NULL);
+
+       if (value) {
+               GDateTime *dt;
+
+               dt = g_date_time_new_from_iso8601 (value, NULL);
+
+               if (!dt) {
+                       gint len = strlen (value);
+
+                       /* 2020-07-14T00:00:00.0000000 , eventually with 'Z' at the end */
+                       if (len == 27 && value[4] == '-' && value[7] == '-' && value[10] == 'T' && value[13] 
== ':' && value[16] == ':' && value[19] == '.') {
+                               gchar tmp[32];
+
+                               strncpy (tmp, value, 27);
+                               tmp[27] = 'Z';
+                               tmp[28] = '\0';
+
+                               dt = g_date_time_new_from_iso8601 (tmp, NULL);
+                       }
+               }
+
+               if (dt) {
+                       res = (time_t) g_date_time_to_unix (dt);
+                       g_date_time_unref (dt);
+               }
+       }
+
+       return res;
+}
+
+static void
+e_m365_add_date_time_offset_member_ex (JsonBuilder *builder,
+                                      const gchar *member_name,
+                                      time_t value,
+                                      gboolean with_utc_zone_char)
+{
+       GDateTime *dt;
+       gchar *value_str;
+
+       if (value <= (time_t) 0) {
+               e_m365_json_add_null_member (builder, member_name);
+               return;
+       }
+
+       dt = g_date_time_new_from_unix_utc (value);
+       g_return_if_fail (dt != NULL);
+
+       value_str = g_date_time_format_iso8601 (dt);
+
+       if (value_str && !with_utc_zone_char) {
+               gchar *z_pos;
+
+               z_pos = strrchr (value_str, 'Z');
+
+               if (z_pos)
+                       *z_pos = '\0';
+       }
+
+       e_m365_json_add_string_member (builder, member_name, value_str);
+
+       g_date_time_unref (dt);
+       g_free (value_str);
+}
+
+void
+e_m365_add_date_time_offset_member (JsonBuilder *builder,
+                                   const gchar *member_name,
+                                   time_t value)
+{
+       e_m365_add_date_time_offset_member_ex (builder, member_name, value, TRUE);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/datetimetimezone?view=graph-rest-1.0 */
+
+time_t
+e_m365_date_time_get_date_time (EM365DateTimeWithZone *datetime)
+{
+       return e_m365_get_date_time_offset_member (datetime, "dateTime");
+}
+
+const gchar *
+e_m365_date_time_get_time_zone (EM365DateTimeWithZone *datetime)
+{
+       return e_m365_json_get_string_member (datetime, "timeZone", NULL);
+}
+
+void
+e_m365_add_date_time (JsonBuilder *builder,
+                     const gchar *member_name,
+                     time_t date_time,
+                     const gchar *zone)
+{
+       g_return_if_fail (member_name != NULL);
+
+       if (date_time <= (time_t) 0) {
+               e_m365_json_add_null_member (builder, member_name);
+               return;
+       }
+
+       e_m365_json_begin_object_member (builder, member_name);
+
+       e_m365_add_date_time_offset_member_ex (builder, "dateTime", date_time, FALSE);
+       e_m365_json_add_string_member (builder, "timeZone", (zone && *zone) ? zone : "UTC");
+
+       e_m365_json_end_object_member (builder);
+}
+
+/* https://docs.microsoft.com/en-us/graph/delta-query-overview */
+
+gboolean
+e_m365_delta_is_removed_object (JsonObject *object)
+{
+       return json_object_has_member (object, "@removed");
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/outlookcategory?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_category_get_display_name (EM365Category *category)
+{
+       return e_m365_json_get_string_member (category, "displayName", NULL);
+}
+
+const gchar *
+e_m365_category_get_id (EM365Category *category)
+{
+       return e_m365_json_get_string_member (category, "id", NULL);
+}
+
+const gchar *
+e_m365_category_get_color (EM365Category *category)
+{
+       const gchar *colors_array[] = {
+               "#ff1a36", /* Red */
+               "#ff8c00", /* Orange */
+               "#f4b10b", /* Peach */
+               "#fff100", /* Yellow */
+               "#009e48", /* Green */
+               "#00b294", /* Teal */
+               "#89933f", /* Olive */
+               "#00bcf2", /* Blue */
+               "#8e69df", /* Purple */
+               "#f30092", /* Maroon */
+               "#6c7e9a", /* Steel */
+               "#425066", /* DarkSteel */
+               "#969696", /* Gray */
+               "#525552", /* DarkGray */
+               "#282828", /* Black */
+               "#a00023", /* DarkRed */
+               "#c45502", /* DarkOrange */
+               "#af7000", /* DarkPeach */
+               "#b59b02", /* DarkYellow */
+               "#176002", /* DarkGreen */
+               "#00725c", /* DarkTeal */
+               "#5c6022", /* DarkOlive */
+               "#036393", /* DarkBlue */
+               "#422f8e", /* DarkPurple */
+               "#960269"  /* DarkMaroon */
+       };
+       const gchar *color_str;
+       gchar *enptr = NULL;
+       gint color_index;
+
+       color_str = e_m365_json_get_string_member (category, "color", NULL);
+
+       if (!color_str ||
+           g_ascii_strcasecmp (color_str, "None") == 0 ||
+           g_ascii_strncasecmp (color_str, "preset", 6) != 0)
+               return NULL;
+
+       color_index = (gint) g_ascii_strtoll (color_str + 6, &enptr, 10);
+
+       if (enptr != color_str && color_index >= 0 && color_index < G_N_ELEMENTS (colors_array))
+               return colors_array[color_index];
+
+       return NULL;
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0
+   https://docs.microsoft.com/en-us/graph/api/resources/contactfolder?view=graph-rest-1.0
+ */
+
+const gchar *
+e_m365_folder_get_id (EM365Folder *folder)
+{
+       return e_m365_json_get_string_member (folder, "id", NULL);
+}
+
+const gchar *
+e_m365_folder_get_parent_folder_id (EM365Folder *folder)
+{
+       return e_m365_json_get_string_member (folder, "parentFolderId", NULL);
+}
+
+const gchar *
+e_m365_folder_get_display_name (EM365Folder *folder)
+{
+       return e_m365_json_get_string_member (folder, "displayName", NULL);
+}
+
+gint32
+e_m365_mail_folder_get_child_folder_count (EM365MailFolder *folder)
+{
+       return (gint32) e_m365_json_get_int_member (folder, "childFolderCount", 0);
+}
+
+gint32
+e_m365_mail_folder_get_total_item_count (EM365MailFolder *folder)
+{
+       return (gint32) e_m365_json_get_int_member (folder, "totalItemCount", 0);
+}
+
+gint32
+e_m365_mail_folder_get_unread_item_count (EM365MailFolder *folder)
+{
+       return (gint32) e_m365_json_get_int_member (folder, "unreadItemCount", 0);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/recipient?view=graph-rest-1.0
+   https://docs.microsoft.com/en-us/graph/api/resources/emailaddress?view=graph-rest-1.0
+ */
+const gchar *
+e_m365_recipient_get_name (EM365Recipient *recipient)
+{
+       JsonObject *email_address;
+
+       email_address = e_m365_json_get_object_member (recipient, "emailAddress");
+
+       if (!email_address)
+               return NULL;
+
+       return e_m365_json_get_string_member (email_address, "name", NULL);
+}
+
+const gchar *
+e_m365_recipient_get_address (EM365Recipient *recipient)
+{
+       JsonObject *email_address;
+
+       email_address = e_m365_json_get_object_member (recipient, "emailAddress");
+
+       if (!email_address)
+               return NULL;
+
+       return e_m365_json_get_string_member (email_address, "address", NULL);
+}
+
+void
+e_m365_add_recipient (JsonBuilder *builder,
+                     const gchar *member_name,
+                     const gchar *name,
+                     const gchar *address)
+{
+       g_return_if_fail ((name && *name) || (address && *address));
+
+       e_m365_json_begin_object_member (builder, member_name);
+       e_m365_json_begin_object_member (builder, "emailAddress");
+
+       e_m365_json_add_nonempty_string_member (builder, "name", name);
+       e_m365_json_add_nonempty_string_member (builder, "address", address);
+
+       e_m365_json_end_object_member (builder); /* emailAddress */
+       e_m365_json_end_object_member (builder); /* member_name */
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/internetmessageheader?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_internet_message_header_get_name (EM365InternetMessageHeader *header)
+{
+       return e_m365_json_get_string_member (header, "name", NULL);
+}
+
+const gchar *
+e_m365_internet_message_header_get_value (EM365InternetMessageHeader *header)
+{
+       return e_m365_json_get_string_member (header, "value", NULL);
+}
+
+void
+e_m365_add_internet_message_header (JsonBuilder *builder,
+                                   const gchar *name,
+                                   const gchar *value)
+{
+       g_return_if_fail (name && *name);
+       g_return_if_fail (value);
+
+       json_builder_begin_object (builder);
+
+       if (value && (*value == ' ' || *value == '\t'))
+               value++;
+
+       e_m365_json_add_string_member (builder, "name", name);
+       e_m365_json_add_string_member (builder, "value", value);
+
+       json_builder_end_object (builder);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/followupflag?view=graph-rest-1.0 */
+
+EM365DateTimeWithZone *
+e_m365_followup_flag_get_completed_date_time (EM365FollowupFlag *flag)
+{
+       return e_m365_json_get_object_member (flag, "completedDateTime");
+}
+
+void
+e_m365_followup_flag_add_completed_date_time (JsonBuilder *builder,
+                                             time_t date_time,
+                                             const gchar *zone)
+{
+       e_m365_add_date_time (builder, "completedDateTime", date_time, zone);
+}
+
+EM365DateTimeWithZone *
+e_m365_followup_flag_get_due_date_time (EM365FollowupFlag *flag)
+{
+       return e_m365_json_get_object_member (flag, "dueDateTime");
+}
+
+void
+e_m365_followup_flag_add_due_date_time (JsonBuilder *builder,
+                                       time_t date_time,
+                                       const gchar *zone)
+{
+       e_m365_add_date_time (builder, "dueDateTime", date_time, zone);
+}
+
+EM365FollowupFlagStatusType
+e_m365_followup_flag_get_flag_status (EM365FollowupFlag *flag)
+{
+       return m365_json_utils_get_json_as_enum (flag, "flagStatus",
+               flag_status_map, G_N_ELEMENTS (flag_status_map),
+               E_M365_FOLLOWUP_FLAG_STATUS_NOT_SET,
+               E_M365_FOLLOWUP_FLAG_STATUS_UNKNOWN);
+}
+
+void
+e_m365_followup_flag_add_flag_status (JsonBuilder *builder,
+                                     EM365FollowupFlagStatusType status)
+{
+       m365_json_utils_add_enum_as_json (builder, "flagStatus", status,
+               flag_status_map, G_N_ELEMENTS (flag_status_map),
+               E_M365_FOLLOWUP_FLAG_STATUS_NOT_SET,
+               E_M365_FOLLOWUP_FLAG_STATUS_NOT_FLAGGED);
+}
+
+EM365DateTimeWithZone *
+e_m365_followup_flag_get_start_date_time (EM365FollowupFlag *flag)
+{
+       return e_m365_json_get_object_member (flag, "startDateTime");
+}
+
+void
+e_m365_followup_flag_add_start_date_time (JsonBuilder *builder,
+                                         time_t date_time,
+                                         const gchar *zone)
+{
+       e_m365_add_date_time (builder, "startDateTime", date_time, zone);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/itembody?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_item_body_get_content (EM365ItemBody *item_body)
+{
+       return e_m365_json_get_string_member (item_body, "content", NULL);
+}
+
+EM365ItemBodyContentTypeType
+e_m365_item_body_get_content_type (EM365ItemBody *item_body)
+{
+       return m365_json_utils_get_json_as_enum (item_body, "contentType",
+               content_type_map, G_N_ELEMENTS (content_type_map),
+               E_M365_ITEM_BODY_CONTENT_TYPE_NOT_SET,
+               E_M365_ITEM_BODY_CONTENT_TYPE_UNKNOWN);
+}
+
+void
+e_m365_add_item_body (JsonBuilder *builder,
+                     const gchar *member_name,
+                     EM365ItemBodyContentTypeType content_type,
+                     const gchar *content)
+{
+       g_return_if_fail (member_name != NULL);
+
+       if (content_type == E_M365_ITEM_BODY_CONTENT_TYPE_NOT_SET || !content) {
+               e_m365_json_add_null_member (builder, member_name);
+               return;
+       }
+
+       e_m365_json_begin_object_member (builder, member_name);
+
+       m365_json_utils_add_enum_as_json (builder, "contentType", content_type,
+               content_type_map, G_N_ELEMENTS (content_type_map),
+               E_M365_ITEM_BODY_CONTENT_TYPE_NOT_SET,
+               E_M365_ITEM_BODY_CONTENT_TYPE_TEXT);
+
+       e_m365_json_add_string_member (builder, "content", content);
+
+       e_m365_json_end_object_member (builder);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0 */
+
+JsonArray * /* EM365Recipient * */
+e_m365_mail_message_get_bcc_recipients (EM365MailMessage *mail)
+{
+       return e_m365_json_get_array_member (mail, "bccRecipients");
+}
+
+void
+e_m365_mail_message_begin_bcc_recipients (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "bccRecipients");
+}
+
+void
+e_m365_mail_message_end_bcc_recipients (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+EM365ItemBody *
+e_m365_mail_message_get_body (EM365MailMessage *mail)
+{
+       return e_m365_json_get_object_member (mail, "body");
+}
+
+void
+e_m365_mail_message_add_body (JsonBuilder *builder,
+                             EM365ItemBodyContentTypeType content_type,
+                             const gchar *content)
+{
+       e_m365_add_item_body (builder, "body", content_type, content);
+}
+
+const gchar *
+e_m365_mail_message_get_body_preview (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "bodyPreview", NULL);
+}
+
+JsonArray * /* const gchar * */
+e_m365_mail_message_get_categories (EM365MailMessage *mail)
+{
+       return e_m365_json_get_array_member (mail, "categories");
+}
+
+void
+e_m365_mail_message_begin_categories (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "categories");
+}
+
+void
+e_m365_mail_message_end_categories (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_mail_message_add_category (JsonBuilder *builder,
+                                 const gchar *category)
+{
+       g_return_if_fail (category && *category);
+
+       json_builder_add_string_value (builder, category);
+}
+
+JsonArray * /* EM365Recipient * */
+e_m365_mail_message_get_cc_recipients (EM365MailMessage *mail)
+{
+       return e_m365_json_get_array_member (mail, "ccRecipients");
+}
+
+void
+e_m365_mail_message_begin_cc_recipients (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "ccRecipients");
+}
+
+void
+e_m365_mail_message_end_cc_recipients (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+const gchar *
+e_m365_mail_message_get_change_key (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "changeKey", NULL);
+}
+
+const gchar *
+e_m365_mail_message_get_conversation_id (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "conversationId", NULL);
+}
+
+JsonObject * /* Edm.Binary */
+e_m365_mail_message_get_conversation_index (EM365MailMessage *mail)
+{
+       return e_m365_json_get_object_member (mail, "conversationIndex");
+}
+
+time_t
+e_m365_mail_message_get_created_date_time (EM365MailMessage *mail)
+{
+       return e_m365_get_date_time_offset_member (mail, "createdDateTime");
+}
+
+EM365FollowupFlag *
+e_m365_mail_message_get_flag (EM365MailMessage *mail)
+{
+       return e_m365_json_get_object_member (mail, "flag");
+}
+
+void
+e_m365_mail_message_begin_flag (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "flag");
+}
+
+void
+e_m365_mail_message_end_flag (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+EM365Recipient *
+e_m365_mail_message_get_from (EM365MailMessage *mail)
+{
+       return e_m365_json_get_object_member (mail, "from");
+}
+
+void
+e_m365_mail_message_add_from (JsonBuilder *builder,
+                             const gchar *name,
+                             const gchar *address)
+{
+       e_m365_add_recipient (builder, "from", name, address);
+}
+
+gboolean
+e_m365_mail_message_get_has_attachments        (EM365MailMessage *mail)
+{
+       return e_m365_json_get_boolean_member (mail, "hasAttachments", FALSE);
+}
+
+const gchar *
+e_m365_mail_message_get_id (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "id", NULL);
+}
+
+EM365ImportanceType
+e_m365_mail_message_get_importance (EM365MailMessage *mail)
+{
+       return m365_json_utils_get_json_as_enum (mail, "importance",
+               importance_map, G_N_ELEMENTS (importance_map),
+               E_M365_IMPORTANCE_NOT_SET,
+               E_M365_IMPORTANCE_UNKNOWN);
+}
+
+void
+e_m365_mail_message_add_importance (JsonBuilder *builder,
+                                   EM365ImportanceType importance)
+{
+       m365_json_utils_add_enum_as_json (builder, "importance", importance,
+               importance_map, G_N_ELEMENTS (importance_map),
+               E_M365_IMPORTANCE_NOT_SET,
+               E_M365_IMPORTANCE_NOT_SET);
+}
+
+EM365InferenceClassificationType
+e_m365_mail_message_get_inference_classification (EM365MailMessage *mail)
+{
+       return m365_json_utils_get_json_as_enum (mail, "inferenceClassification",
+               inference_classification_map, G_N_ELEMENTS (inference_classification_map),
+               E_M365_INFERENCE_CLASSIFICATION_NOT_SET,
+               E_M365_INFERENCE_CLASSIFICATION_UNKNOWN);
+}
+
+JsonArray * /* EM365InternetMessageHeader * */
+e_m365_mail_message_get_internet_message_headers (EM365MailMessage *mail)
+{
+       return e_m365_json_get_array_member (mail, "internetMessageHeaders");
+}
+
+void
+e_m365_mail_message_begin_internet_message_headers (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "internetMessageHeaders");
+}
+
+void
+e_m365_mail_message_end_internet_message_headers (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+const gchar *
+e_m365_mail_message_get_internet_message_id (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "internetMessageId", NULL);
+}
+
+void
+e_m365_mail_message_add_internet_message_id (JsonBuilder *builder,
+                                            const gchar *message_id)
+{
+       e_m365_json_add_nonempty_string_member (builder, "internetMessageId", message_id);
+}
+
+gboolean
+e_m365_mail_message_get_is_delivery_receipt_requested (EM365MailMessage *mail)
+{
+       return e_m365_json_get_boolean_member (mail, "isDeliveryReceiptRequested", FALSE);
+}
+
+void
+e_m365_mail_message_add_is_delivery_receipt_requested (JsonBuilder *builder,
+                                                      gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isDeliveryReceiptRequested", value);
+}
+
+gboolean
+e_m365_mail_message_get_is_draft (EM365MailMessage *mail)
+{
+       return e_m365_json_get_boolean_member (mail, "isDraft", FALSE);
+}
+
+gboolean
+e_m365_mail_message_get_is_read (EM365MailMessage *mail)
+{
+       return e_m365_json_get_boolean_member (mail, "isRead", FALSE);
+}
+
+void
+e_m365_mail_message_add_is_read (JsonBuilder *builder,
+                                gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isRead", value);
+}
+
+gboolean
+e_m365_mail_message_get_is_read_receipt_requested (EM365MailMessage *mail)
+{
+       return e_m365_json_get_boolean_member (mail, "isReadReceiptRequested", FALSE);
+}
+
+void
+e_m365_mail_message_add_is_read_receipt_requested (JsonBuilder *builder,
+                                                  gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isReadReceiptRequested", value);
+}
+
+time_t
+e_m365_mail_message_get_last_modified_date_time (EM365MailMessage *mail)
+{
+       return e_m365_get_date_time_offset_member (mail, "lastModifiedDateTime");
+}
+
+const gchar *
+e_m365_mail_message_get_parent_folder_id (EM365MailMessage *mail)
+{
+               return e_m365_json_get_string_member (mail, "parentFolderId", NULL);
+}
+
+time_t
+e_m365_mail_message_get_received_date_time (EM365MailMessage *mail)
+{
+       return e_m365_get_date_time_offset_member (mail, "receivedDateTime");
+}
+
+void
+e_m365_mail_message_add_received_date_time (JsonBuilder *builder,
+                                           time_t value)
+{
+       e_m365_add_date_time_offset_member (builder, "receivedDateTime", value);
+}
+
+JsonArray * /* EM365Recipient * */
+e_m365_mail_message_get_reply_to (EM365MailMessage *mail)
+{
+       return e_m365_json_get_array_member (mail, "replyTo");
+}
+
+void
+e_m365_mail_message_begin_reply_to (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "replyTo");
+}
+
+void
+e_m365_mail_message_end_reply_to (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+EM365Recipient *
+e_m365_mail_message_get_sender (EM365MailMessage *mail)
+{
+       return e_m365_json_get_object_member (mail, "sender");
+}
+
+void
+e_m365_mail_message_add_sender (JsonBuilder *builder,
+                               const gchar *name,
+                               const gchar *address)
+{
+       g_return_if_fail ((name && *name) || (address && *address));
+
+       e_m365_add_recipient (builder, "sender", name, address);
+}
+
+time_t
+e_m365_mail_message_get_sent_date_time (EM365MailMessage *mail)
+{
+       return e_m365_get_date_time_offset_member (mail, "sentDateTime");
+}
+
+void
+e_m365_mail_message_add_sent_date_time (JsonBuilder *builder,
+                                       time_t value)
+{
+       e_m365_add_date_time_offset_member (builder, "sentDateTime", value);
+}
+
+const gchar *
+e_m365_mail_message_get_subject (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "subject", NULL);
+}
+
+void
+e_m365_mail_message_add_subject (JsonBuilder *builder,
+                                const gchar *subject)
+{
+       e_m365_json_add_nonempty_string_member (builder, "subject", subject);
+}
+
+JsonArray * /* EM365Recipient * */
+e_m365_mail_message_get_to_recipients (EM365MailMessage *mail)
+{
+       return e_m365_json_get_array_member (mail, "toRecipients");
+}
+
+void
+e_m365_mail_message_begin_to_recipients (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "toRecipients");
+}
+
+void
+e_m365_mail_message_end_to_recipients (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+EM365ItemBody *
+e_m365_mail_message_get_unique_body (EM365MailMessage *mail)
+{
+       return e_m365_json_get_object_member (mail, "uniqueBody");
+}
+
+const gchar *
+e_m365_mail_message_get_web_link (EM365MailMessage *mail)
+{
+       return e_m365_json_get_string_member (mail, "webLink", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/attachment?view=graph-rest-1.0 */
+
+EM365AttachmentDataType
+e_m365_attachment_get_data_type (EM365Attachment *attachment)
+{
+       return m365_json_utils_get_json_as_enum (attachment, "@odata.type",
+               attachment_data_type_map, G_N_ELEMENTS (attachment_data_type_map),
+               E_M365_ATTACHMENT_DATA_TYPE_NOT_SET,
+               E_M365_ATTACHMENT_DATA_TYPE_UNKNOWN);
+}
+
+void
+e_m365_attachment_begin_attachment (JsonBuilder *builder,
+                                   EM365AttachmentDataType data_type)
+{
+       e_m365_json_begin_object_member (builder, NULL);
+
+       m365_json_utils_add_enum_as_json (builder, "@odata.type", data_type,
+               attachment_data_type_map, G_N_ELEMENTS (attachment_data_type_map),
+               E_M365_ATTACHMENT_DATA_TYPE_NOT_SET,
+               E_M365_ATTACHMENT_DATA_TYPE_FILE);
+}
+
+void
+e_m365_attachment_end_attachment (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+const gchar *
+e_m365_attachment_get_content_type (EM365Attachment *attachment)
+{
+       return e_m365_json_get_string_member (attachment, "contentType", NULL);
+}
+
+void
+e_m365_attachment_add_content_type (JsonBuilder *builder,
+                                   const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "contentType", value);
+}
+
+const gchar *
+e_m365_attachment_get_id (EM365Attachment *attachment)
+{
+       return e_m365_json_get_string_member (attachment, "id", NULL);
+}
+
+gboolean
+e_m365_attachment_get_is_inline (EM365Attachment *attachment)
+{
+       return e_m365_json_get_boolean_member (attachment, "isInline", FALSE);
+}
+
+void
+e_m365_attachment_add_is_inline (JsonBuilder *builder,
+                                gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isInline", value);
+}
+
+time_t
+e_m365_attachment_get_last_modified_date_time (EM365Attachment *attachment)
+{
+       return e_m365_get_date_time_offset_member (attachment, "lastModifiedDateTime");
+}
+
+void
+e_m365_attachment_add_last_modified_date_time (JsonBuilder *builder,
+                                              time_t value)
+{
+       e_m365_add_date_time_offset_member (builder, "lastModifiedDateTime", value);
+}
+
+const gchar *
+e_m365_attachment_get_name (EM365Attachment *attachment)
+{
+       return e_m365_json_get_string_member (attachment, "name", NULL);
+}
+
+void
+e_m365_attachment_add_name (JsonBuilder *builder,
+                           const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "name", value);
+}
+
+gint32
+e_m365_attachment_get_size (EM365Attachment *attachment)
+{
+       return (gint32) e_m365_json_get_int_member (attachment, "size", -1);
+}
+
+void
+e_m365_attachment_add_size (JsonBuilder *builder,
+                           gint32 value)
+{
+       e_m365_json_add_int_member (builder, "size", value);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/fileattachment?view=graph-rest-1.0 */
+
+const gchar * /* base64-encoded */
+e_m365_file_attachment_get_content_bytes (EM365Attachment *attachment)
+{
+       return e_m365_json_get_string_member (attachment, "contentBytes", NULL);
+}
+
+void
+e_m365_file_attachment_add_content_bytes (JsonBuilder *builder,
+                                         const gchar *base64_value)
+{
+       e_m365_json_add_string_member (builder, "contentBytes", base64_value);
+}
+
+const gchar *
+e_m365_file_attachment_get_content_id (EM365Attachment *attachment)
+{
+       return e_m365_json_get_string_member (attachment, "contentId", NULL);
+}
+
+void
+e_m365_file_attachment_add_content_id (JsonBuilder *builder,
+                                      const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "contentId", value);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/emailaddress?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_email_address_get_name (EM365EmailAddress *email)
+{
+       return e_m365_json_get_string_member (email, "name", NULL);
+}
+
+const gchar *
+e_m365_email_address_get_address (EM365EmailAddress *email)
+{
+       return e_m365_json_get_string_member (email, "address", NULL);
+}
+
+void
+e_m365_add_email_address (JsonBuilder *builder,
+                         const gchar *member_name,
+                         const gchar *name,
+                         const gchar *address)
+{
+       g_return_if_fail ((name && *name) || (address && *address));
+
+       e_m365_json_begin_object_member (builder, member_name);
+
+       e_m365_json_add_nonempty_string_member (builder, "name", name);
+       e_m365_json_add_nonempty_string_member (builder, "address", address);
+
+       e_m365_json_end_object_member (builder); /* member_name */
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/physicaladdress?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_physical_address_get_city (EM365PhysicalAddress *address)
+{
+       return e_m365_json_get_string_member (address, "city", NULL);
+}
+
+const gchar *
+e_m365_physical_address_get_country_or_region (EM365PhysicalAddress *address)
+{
+       return e_m365_json_get_string_member (address, "countryOrRegion", NULL);
+}
+
+const gchar *
+e_m365_physical_address_get_postal_code (EM365PhysicalAddress *address)
+{
+       return e_m365_json_get_string_member (address, "postalCode", NULL);
+}
+
+const gchar *
+e_m365_physical_address_get_state (EM365PhysicalAddress *address)
+{
+       return e_m365_json_get_string_member (address, "state", NULL);
+}
+
+const gchar *
+e_m365_physical_address_get_street (EM365PhysicalAddress *address)
+{
+       return e_m365_json_get_string_member (address, "street", NULL);
+}
+
+void
+e_m365_add_physical_address (JsonBuilder *builder,
+                            const gchar *member_name,
+                            const gchar *city,
+                            const gchar *country_or_region,
+                            const gchar *postal_code,
+                            const gchar *state,
+                            const gchar *street)
+{
+       if ((city && *city) ||
+           (country_or_region && *country_or_region) ||
+           (postal_code && *postal_code) ||
+           (state && *state) ||
+           (street && *street)) {
+               e_m365_json_begin_object_member (builder, member_name);
+               e_m365_json_add_nonempty_string_member (builder, "city", city);
+               e_m365_json_add_nonempty_string_member (builder, "countryOrRegion", country_or_region);
+               e_m365_json_add_nonempty_string_member (builder, "postalCode", postal_code);
+               e_m365_json_add_nonempty_string_member (builder, "state", state);
+               e_m365_json_add_nonempty_string_member (builder, "street", street);
+               e_m365_json_end_object_member (builder);
+       } else {
+               e_m365_json_begin_object_member (builder, member_name);
+               e_m365_json_end_object_member (builder);
+       }
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/contact?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_contact_get_id (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "id", NULL);
+}
+
+const gchar *
+e_m365_contact_get_parent_folder_id (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "parentFolderId", NULL);
+}
+
+const gchar *
+e_m365_contact_get_change_key (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "changeKey", NULL);
+}
+
+time_t
+e_m365_contact_get_created_date_time (EM365Contact *contact)
+{
+       return e_m365_get_date_time_offset_member (contact, "createdDateTime");
+}
+
+time_t
+e_m365_contact_get_last_modified_date_time (EM365Contact *contact)
+{
+       return e_m365_get_date_time_offset_member (contact, "lastModifiedDateTime");
+}
+
+const gchar *
+e_m365_contact_get_assistant_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "assistantName", NULL);
+}
+
+void
+e_m365_contact_add_assistant_name (JsonBuilder *builder,
+                                  const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "assistantName", value);
+}
+
+time_t
+e_m365_contact_get_birthday (EM365Contact *contact)
+{
+       return e_m365_get_date_time_offset_member (contact, "birthday");
+}
+
+void
+e_m365_contact_add_birthday (JsonBuilder *builder,
+                            time_t value)
+{
+       e_m365_add_date_time_offset_member (builder, "birthday", value);
+}
+
+EM365PhysicalAddress *
+e_m365_contact_get_business_address (EM365Contact *contact)
+{
+       return e_m365_json_get_object_member (contact, "businessAddress");
+}
+
+void
+e_m365_contact_add_business_address (JsonBuilder *builder,
+                                    const gchar *city,
+                                    const gchar *country_or_region,
+                                    const gchar *postal_code,
+                                    const gchar *state,
+                                    const gchar *street)
+{
+       e_m365_add_physical_address (builder, "businessAddress", city, country_or_region, postal_code, state, 
street);
+}
+
+const gchar *
+e_m365_contact_get_business_home_page (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "businessHomePage", NULL);
+}
+
+void
+e_m365_contact_add_business_home_page (JsonBuilder *builder,
+                                      const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "businessHomePage", value);
+}
+
+JsonArray * /* const gchar * */
+e_m365_contact_get_business_phones (EM365Contact *contact)
+{
+       return e_m365_json_get_array_member (contact, "businessPhones");
+}
+
+void
+e_m365_contact_begin_business_phones (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "businessPhones");
+}
+
+void
+e_m365_contact_end_business_phones (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_contact_add_business_phone (JsonBuilder *builder,
+                                  const gchar *value)
+{
+       g_return_if_fail (value && *value);
+
+       json_builder_add_string_value (builder, value);
+}
+
+JsonArray * /* const gchar * */
+e_m365_contact_get_categories (EM365Contact *contact)
+{
+       return e_m365_json_get_array_member (contact, "categories");
+}
+
+void
+e_m365_contact_begin_categories (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "categories");
+}
+
+void
+e_m365_contact_end_categories (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_contact_add_category (JsonBuilder *builder,
+                            const gchar *category)
+{
+       g_return_if_fail (category && *category);
+
+       json_builder_add_string_value (builder, category);
+}
+
+JsonArray * /* const gchar * */
+e_m365_contact_get_children (EM365Contact *contact)
+{
+       return e_m365_json_get_array_member (contact, "children");
+}
+
+void
+e_m365_contact_begin_children (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "children");
+}
+
+void
+e_m365_contact_end_children (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_contact_add_child (JsonBuilder *builder,
+                         const gchar *value)
+{
+       g_return_if_fail (value && *value);
+
+       json_builder_add_string_value (builder, value);
+}
+
+const gchar *
+e_m365_contact_get_company_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "companyName", NULL);
+}
+
+void
+e_m365_contact_add_company_name (JsonBuilder *builder,
+                                const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "companyName", value);
+}
+
+const gchar *
+e_m365_contact_get_department (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "department", NULL);
+}
+
+void
+e_m365_contact_add_department (JsonBuilder *builder,
+                              const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "department", value);
+}
+
+const gchar *
+e_m365_contact_get_display_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "displayName", NULL);
+}
+
+void
+e_m365_contact_add_display_name (JsonBuilder *builder,
+                                const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "displayName", value);
+}
+
+JsonArray * /* EM365EmailAddress * */
+e_m365_contact_get_email_addresses (EM365Contact *contact)
+{
+       return e_m365_json_get_array_member (contact, "emailAddresses");
+}
+
+void
+e_m365_contact_begin_email_addresses (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "emailAddresses");
+}
+
+void
+e_m365_contact_end_email_addresses (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+const gchar *
+e_m365_contact_get_file_as (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "fileAs", NULL);
+}
+
+void
+e_m365_contact_add_file_as (JsonBuilder *builder,
+                           const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "fileAs", value);
+}
+
+const gchar *
+e_m365_contact_get_generation (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "generation", NULL);
+}
+
+void
+e_m365_contact_add_generation (JsonBuilder *builder,
+                              const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "generation", value);
+}
+
+const gchar *
+e_m365_contact_get_given_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "givenName", NULL);
+}
+
+void
+e_m365_contact_add_given_name (JsonBuilder *builder,
+                              const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "givenName", value);
+}
+
+EM365PhysicalAddress *
+e_m365_contact_get_home_address (EM365Contact *contact)
+{
+       return e_m365_json_get_object_member (contact, "homeAddress");
+}
+
+void
+e_m365_contact_add_home_address (JsonBuilder *builder,
+                                const gchar *city,
+                                const gchar *country_or_region,
+                                const gchar *postal_code,
+                                const gchar *state,
+                                const gchar *street)
+{
+       e_m365_add_physical_address (builder, "homeAddress", city, country_or_region, postal_code, state, 
street);
+}
+
+JsonArray * /* const gchar * */
+e_m365_contact_get_home_phones (EM365Contact *contact)
+{
+       return e_m365_json_get_array_member (contact, "homePhones");
+}
+
+void
+e_m365_contact_begin_home_phones (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "homePhones");
+}
+
+void
+e_m365_contact_end_home_phones (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_contact_add_home_phone (JsonBuilder *builder,
+                              const gchar *value)
+{
+       g_return_if_fail (value && *value);
+
+       json_builder_add_string_value (builder, value);
+}
+
+JsonArray * /* const gchar * */
+e_m365_contact_get_im_addresses (EM365Contact *contact)
+{
+       return e_m365_json_get_array_member (contact, "imAddresses");
+}
+
+void
+e_m365_contact_begin_im_addresses (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "imAddresses");
+}
+
+void
+e_m365_contact_end_im_addresses (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_contact_add_im_address (JsonBuilder *builder,
+                              const gchar *value)
+{
+       g_return_if_fail (value && *value);
+
+       json_builder_add_string_value (builder, value);
+}
+
+const gchar *
+e_m365_contact_get_initials (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "initials", NULL);
+}
+
+void
+e_m365_contact_add_initials (JsonBuilder *builder,
+                            const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "initials", value);
+}
+
+const gchar *
+e_m365_contact_get_job_title (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "jobTitle", NULL);
+}
+
+void
+e_m365_contact_add_job_title (JsonBuilder *builder,
+                             const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "jobTitle", value);
+}
+
+const gchar *
+e_m365_contact_get_manager (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "manager", NULL);
+}
+
+void
+e_m365_contact_add_manager (JsonBuilder *builder,
+                           const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "manager", value);
+}
+
+const gchar *
+e_m365_contact_get_middle_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "middleName", NULL);
+}
+
+void
+e_m365_contact_add_middle_name (JsonBuilder *builder,
+                               const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "middleName", value);
+}
+
+const gchar *
+e_m365_contact_get_mobile_phone (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "mobilePhone", NULL);
+}
+
+void
+e_m365_contact_add_mobile_phone (JsonBuilder *builder,
+                                const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "mobilePhone", value);
+}
+
+const gchar *
+e_m365_contact_get_nick_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "nickName", NULL);
+}
+
+void
+e_m365_contact_add_nick_name (JsonBuilder *builder,
+                             const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "nickName", value);
+}
+
+const gchar *
+e_m365_contact_get_office_location (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "officeLocation", NULL);
+}
+
+void
+e_m365_contact_add_office_location (JsonBuilder *builder,
+                                   const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "officeLocation", value);
+}
+
+EM365PhysicalAddress *
+e_m365_contact_get_other_address (EM365Contact *contact)
+{
+       return e_m365_json_get_object_member (contact, "otherAddress");
+}
+
+void
+e_m365_contact_add_other_address (JsonBuilder *builder,
+                                 const gchar *city,
+                                 const gchar *country_or_region,
+                                 const gchar *postal_code,
+                                 const gchar *state,
+                                 const gchar *street)
+{
+       e_m365_add_physical_address (builder, "otherAddress", city, country_or_region, postal_code, state, 
street);
+}
+
+const gchar *
+e_m365_contact_get_personal_notes (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "personalNotes", NULL);
+}
+
+void
+e_m365_contact_add_personal_notes (JsonBuilder *builder,
+                                  const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "personalNotes", value);
+}
+
+const gchar *
+e_m365_contact_get_profession (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "profession", NULL);
+}
+
+void
+e_m365_contact_add_profession (JsonBuilder *builder,
+                              const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "profession", value);
+}
+
+const gchar *
+e_m365_contact_get_spouse_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "spouseName", NULL);
+}
+
+void
+e_m365_contact_add_spouse_name (JsonBuilder *builder,
+                               const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "spouseName", value);
+}
+
+const gchar *
+e_m365_contact_get_surname (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "surname", NULL);
+}
+
+void
+e_m365_contact_add_surname (JsonBuilder *builder,
+                           const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "surname", value);
+}
+
+const gchar *
+e_m365_contact_get_title (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "title", NULL);
+}
+
+void
+e_m365_contact_add_title (JsonBuilder *builder,
+                         const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "title", value);
+}
+
+const gchar *
+e_m365_contact_get_yomi_company_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "yomiCompanyName", NULL);
+}
+
+void
+e_m365_contact_add_yomi_company_name (JsonBuilder *builder,
+                                     const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "yomiCompanyName", value);
+}
+
+const gchar *
+e_m365_contact_get_yomi_given_name (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "yomiGivenName", NULL);
+}
+
+void
+e_m365_contact_add_yomi_given_name (JsonBuilder *builder,
+                                   const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "yomiGivenName", value);
+}
+
+const gchar *
+e_m365_contact_get_yomi_surname (EM365Contact *contact)
+{
+       return e_m365_json_get_string_member (contact, "yomiSurname", NULL);
+}
+
+void
+e_m365_contact_add_yomi_surname (JsonBuilder *builder,
+                                const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "yomiSurname", value);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/calendargroup?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_calendar_group_get_id (EM365CalendarGroup *group)
+{
+       return e_m365_json_get_string_member (group, "id", NULL);
+}
+
+const gchar *
+e_m365_calendar_group_get_change_key (EM365CalendarGroup *group)
+{
+       return e_m365_json_get_string_member (group, "changeKey", NULL);
+}
+
+const gchar *
+e_m365_calendar_group_get_class_id (EM365CalendarGroup *group)
+{
+       return e_m365_json_get_string_member (group, "classId", NULL);
+}
+
+const gchar *
+e_m365_calendar_group_get_name (EM365CalendarGroup *group)
+{
+       return e_m365_json_get_string_member (group, "name", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/calendar?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_calendar_get_id (EM365Calendar *calendar)
+{
+       return e_m365_json_get_string_member (calendar, "id", NULL);
+}
+
+const gchar *
+e_m365_calendar_get_change_key (EM365Calendar *calendar)
+{
+       return e_m365_json_get_string_member (calendar, "changeKey", NULL);
+}
+
+gboolean
+e_m365_calendar_get_can_edit (EM365Calendar *calendar)
+{
+       return e_m365_json_get_boolean_member (calendar, "canEdit", FALSE);
+}
+
+gboolean
+e_m365_calendar_get_can_share (EM365Calendar *calendar)
+{
+       return e_m365_json_get_boolean_member (calendar, "canShare", FALSE);
+}
+
+gboolean
+e_m365_calendar_get_can_view_private_items (EM365Calendar *calendar)
+{
+       return e_m365_json_get_boolean_member (calendar, "canViewPrivateItems", FALSE);
+}
+
+gboolean
+e_m365_calendar_get_is_removable (EM365Calendar *calendar)
+{
+       return e_m365_json_get_boolean_member (calendar, "isRemovable", FALSE);
+}
+
+gboolean
+e_m365_calendar_get_is_tallying_responses (EM365Calendar *calendar)
+{
+       return e_m365_json_get_boolean_member (calendar, "isTallyingResponses", FALSE);
+}
+
+EM365EmailAddress *
+e_m365_calendar_get_owner (EM365Calendar *calendar)
+{
+       return e_m365_json_get_object_member (calendar, "owner");
+}
+
+const gchar *
+e_m365_calendar_get_name (EM365Calendar *calendar)
+{
+       return e_m365_json_get_string_member (calendar, "name", NULL);
+}
+
+void
+e_m365_calendar_add_name (JsonBuilder *builder,
+                         const gchar *name)
+{
+       e_m365_json_add_nonempty_string_member (builder, "name", name);
+}
+
+guint32 /* bit-or of EM365OnlineMeetingProviderType */
+e_m365_calendar_get_allowed_online_meeting_providers (EM365Calendar *calendar)
+{
+       guint32 providers = E_M365_ONLINE_MEETING_PROVIDER_NOT_SET;
+       JsonArray *array;
+
+       array = e_m365_json_get_array_member (calendar, "allowedOnlineMeetingProviders");
+
+       if (array) {
+               guint ii, len;
+
+               providers = E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN;
+
+               len = json_array_get_length (array);
+
+               for (ii = 0; ii < len; ii++) {
+                       const gchar *str = json_array_get_string_element (array, ii);
+                       gint enum_value;
+
+                       if (!str)
+                               continue;
+
+                       enum_value = m365_json_utils_json_value_as_enum (str,
+                               meeting_provider_map, G_N_ELEMENTS (meeting_provider_map),
+                               E_M365_ONLINE_MEETING_PROVIDER_NOT_SET,
+                               E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN);
+
+                       if (enum_value != E_M365_ONLINE_MEETING_PROVIDER_NOT_SET)
+                               providers |= enum_value;
+               }
+       }
+
+       return providers;
+}
+
+void
+e_m365_calendar_add_allowed_online_meeting_providers (JsonBuilder *builder,
+                                                     guint providers) /* bit-or of 
EM365OnlineMeetingProviderType */
+{
+       gint ii;
+
+       if (providers == E_M365_ONLINE_MEETING_PROVIDER_NOT_SET)
+               return;
+
+       e_m365_json_begin_array_member (builder, "allowedOnlineMeetingProviders");
+
+       if (providers == E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN)
+               json_builder_add_string_value (builder, "unknown");
+
+       for (ii = 0; ii < G_N_ELEMENTS (meeting_provider_map); ii++) {
+               if ((providers & meeting_provider_map[ii].enum_value) != 0)
+                       json_builder_add_string_value (builder, meeting_provider_map[ii].json_value);
+       }
+
+       e_m365_json_end_array_member (builder);
+}
+
+EM365CalendarColorType
+e_m365_calendar_get_color (EM365Calendar *calendar)
+{
+       const gchar *color;
+       gint ii;
+
+       color = e_m365_json_get_string_member (calendar, "color", NULL);
+
+       if (!color)
+               return E_M365_CALENDAR_COLOR_NOT_SET;
+
+       for (ii = 0; ii < G_N_ELEMENTS (color_map); ii++) {
+               if (g_ascii_strcasecmp (color_map[ii].name, color) == 0)
+                       return color_map[ii].value;
+       }
+
+       return E_M365_CALENDAR_COLOR_UNKNOWN;
+}
+
+void
+e_m365_calendar_add_color (JsonBuilder *builder,
+                          EM365CalendarColorType color)
+{
+       const gchar *name = NULL;
+       gint ii;
+
+       for (ii = 0; ii < G_N_ELEMENTS (color_map); ii++) {
+               if (color_map[ii].value == color) {
+                       name = color_map[ii].name;
+                       break;
+               }
+       }
+
+       if (name && g_ascii_strcasecmp (name, "maxColor") != 0)
+               e_m365_json_add_string_member (builder, "color", name);
+}
+
+EM365OnlineMeetingProviderType
+e_m365_calendar_get_default_online_meeting_provider (EM365Calendar *calendar)
+{
+       return m365_json_utils_get_json_as_enum (calendar, "defaultOnlineMeetingProvider",
+               meeting_provider_map, G_N_ELEMENTS (meeting_provider_map),
+               E_M365_ONLINE_MEETING_PROVIDER_NOT_SET,
+               E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN);
+}
+
+void
+e_m365_calendar_add_default_online_meeting_provider (JsonBuilder *builder,
+                                                    EM365OnlineMeetingProviderType provider)
+{
+       m365_json_utils_add_enum_as_json (builder, "defaultOnlineMeetingProvider", provider,
+               meeting_provider_map, G_N_ELEMENTS (meeting_provider_map),
+               E_M365_ONLINE_MEETING_PROVIDER_NOT_SET,
+               E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/responsestatus?view=graph-rest-1.0 */
+
+EM365ResponseType
+e_m365_response_status_get_response (EM365ResponseStatus *response_status)
+{
+       return m365_json_utils_get_json_as_enum (response_status, "response",
+               response_map, G_N_ELEMENTS (response_map),
+               E_M365_RESPONSE_NOT_SET,
+               E_M365_RESPONSE_UNKNOWN);
+}
+
+time_t
+e_m365_response_status_get_time (EM365ResponseStatus *response_status)
+{
+       return e_m365_get_date_time_offset_member (response_status, "time");
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/attendee?view=graph-rest-1.0 */
+
+EM365ResponseStatus *
+e_m365_attendee_get_status (EM365Attendee *attendee)
+{
+       return e_m365_json_get_object_member (attendee, "status");
+}
+
+EM365AttendeeType
+e_m365_attendee_get_type (EM365Attendee *attendee)
+{
+       return m365_json_utils_get_json_as_enum (attendee, "type",
+               attendee_map, G_N_ELEMENTS (attendee_map),
+               E_M365_ATTENDEE_NOT_SET,
+               E_M365_ATTENDEE_UNKNOWN);
+}
+
+EM365EmailAddress *
+e_m365_attendee_get_email_address (EM365Attendee *attendee)
+{
+       return e_m365_json_get_object_member (attendee, "emailAddress");
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/outlookgeocoordinates?view=graph-rest-1.0 */
+
+gdouble
+e_m365_outlook_geo_coordinates_get_accuracy (EM365OutlookGeoCoordinates *coords)
+{
+       return e_m365_json_get_double_member (coords, "accuracy", 0.0);
+}
+
+void
+e_m365_outlook_geo_coordinates_add_accuracy (JsonBuilder *builder,
+                                            gdouble value)
+{
+       e_m365_json_add_double_member (builder, "accuracy", value);
+}
+
+gdouble
+e_m365_outlook_geo_coordinates_get_altitude (EM365OutlookGeoCoordinates *coords)
+{
+       return e_m365_json_get_double_member (coords, "altitude", 0.0);
+}
+
+void
+e_m365_outlook_geo_coordinates_add_altitude (JsonBuilder *builder,
+                                            gdouble value)
+{
+       e_m365_json_add_double_member (builder, "altitude", value);
+}
+
+gdouble
+e_m365_outlook_geo_coordinates_get_altitude_accuracy (EM365OutlookGeoCoordinates *coords)
+{
+       return e_m365_json_get_double_member (coords, "altitudeAccuracy", 0.0);
+}
+
+void
+e_m365_outlook_geo_coordinates_add_altitude_accuracy (JsonBuilder *builder,
+                                                     gdouble value)
+{
+       e_m365_json_add_double_member (builder, "altitudeAccuracy", value);
+}
+
+gdouble
+e_m365_outlook_geo_coordinates_get_latitude (EM365OutlookGeoCoordinates *coords)
+{
+       return e_m365_json_get_double_member (coords, "latitude", 0.0);
+}
+
+void
+e_m365_outlook_geo_coordinates_add_latitude (JsonBuilder *builder,
+                                            gdouble value)
+{
+       e_m365_json_add_double_member (builder, "latitude", value);
+}
+
+gdouble
+e_m365_outlook_geo_coordinates_get_longitude (EM365OutlookGeoCoordinates *coords)
+{
+       return e_m365_json_get_double_member (coords, "longitude", 0.0);
+}
+
+void
+e_m365_outlook_geo_coordinates_add_longitude (JsonBuilder *builder,
+                                             gdouble value)
+{
+       e_m365_json_add_double_member (builder, "longitude", value);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/location?view=graph-rest-1.0 */
+
+EM365PhysicalAddress *
+e_m365_location_get_address (EM365Location *location)
+{
+       return e_m365_json_get_object_member (location, "address");
+}
+
+void
+e_m365_location_add_address (JsonBuilder *builder,
+                            const gchar *city,
+                            const gchar *country_or_region,
+                            const gchar *postal_code,
+                            const gchar *state,
+                            const gchar *street)
+{
+       e_m365_add_physical_address (builder, "address", city, country_or_region, postal_code, state, street);
+}
+
+EM365OutlookGeoCoordinates *
+e_m365_location_get_coordinates (EM365Location *location)
+{
+       return e_m365_json_get_object_member (location, "coordinates");
+}
+
+void
+e_m365_location_begin_coordinates (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "coordinates");
+}
+
+void
+e_m365_location_end_coordinates (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+const gchar *
+e_m365_location_get_display_name (EM365Location *location)
+{
+       return e_m365_json_get_string_member (location, "displayName", NULL);
+}
+
+void
+e_m365_location_add_display_name (JsonBuilder *builder,
+                                 const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "displayName", value);
+}
+
+const gchar *
+e_m365_location_get_email_address (EM365Location *location)
+{
+       return e_m365_json_get_string_member (location, "locationEmailAddress", NULL);
+}
+
+void
+e_m365_location_add_email_address (JsonBuilder *builder,
+                                  const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "locationEmailAddress", value);
+}
+
+const gchar *
+e_m365_location_get_uri (EM365Location *location)
+{
+       return e_m365_json_get_string_member (location, "locationUri", NULL);
+}
+
+void
+e_m365_location_add_uri (JsonBuilder *builder,
+                        const gchar *value)
+{
+       e_m365_json_add_nonempty_or_null_string_member (builder, "locationUri", value);
+}
+
+EM365LocationType
+e_m365_location_get_type (EM365Location *location)
+{
+       return m365_json_utils_get_json_as_enum (location, "locationType",
+               location_type_map, G_N_ELEMENTS (location_type_map),
+               E_M365_LOCATION_NOT_SET,
+               E_M365_LOCATION_UNKNOWN);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/phone?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_phone_get_number (EM365Phone *phone)
+{
+       return e_m365_json_get_string_member (phone, "number", NULL);
+}
+
+EM365PhoneType
+e_m365_phone_get_type (EM365Phone *phone)
+{
+       return m365_json_utils_get_json_as_enum (phone, "type",
+               phone_map, G_N_ELEMENTS (phone_map),
+               E_M365_PHONE_NOT_SET,
+               E_M365_PHONE_UNKNOWN);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/onlinemeetinginfo?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_online_meeting_info_get_conference_id (EM365OnlineMeetingInfo *meeting_info)
+{
+       return e_m365_json_get_string_member (meeting_info, "conferenceId", NULL);
+}
+
+const gchar *
+e_m365_online_meeting_info_get_join_url (EM365OnlineMeetingInfo *meeting_info)
+{
+       return e_m365_json_get_string_member (meeting_info, "joinUrl", NULL);
+}
+
+JsonArray * /* EM365Phone * */
+e_m365_online_meeting_info_get_phones (EM365OnlineMeetingInfo *meeting_info)
+{
+       return e_m365_json_get_array_member (meeting_info, "phones");
+}
+
+const gchar *
+e_m365_online_meeting_info_get_quick_dial (EM365OnlineMeetingInfo *meeting_info)
+{
+       return e_m365_json_get_string_member (meeting_info, "quickDial", NULL);
+}
+
+JsonArray * /* gchar * */
+e_m365_online_meeting_info_get_toll_free_numbers (EM365OnlineMeetingInfo *meeting_info)
+{
+       return e_m365_json_get_array_member (meeting_info, "tollFreeNumbers");
+}
+
+const gchar *
+e_m365_online_meeting_info_get_toll_number (EM365OnlineMeetingInfo *meeting_info)
+{
+       return e_m365_json_get_string_member (meeting_info, "tollNumber", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/patternedrecurrence?view=graph-rest-1.0 */
+
+EM365RecurrencePattern *
+e_m365_patterned_recurrence_get_pattern (EM365PatternedRecurrence *patterned_recurrence)
+{
+       return e_m365_json_get_object_member (patterned_recurrence, "pattern");
+}
+
+void
+e_m365_patterned_recurrence_begin_pattern (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "pattern");
+}
+
+void
+e_m365_patterned_recurrence_end_pattern (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+EM365RecurrenceRange *
+e_m365_patterned_recurrence_get_range (EM365PatternedRecurrence *patterned_recurrence)
+{
+       return e_m365_json_get_object_member (patterned_recurrence, "range");
+}
+
+void
+e_m365_patterned_recurrence_begin_range (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "range");
+}
+
+void
+e_m365_patterned_recurrence_end_range (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/recurrencepattern?view=graph-rest-1.0 */
+
+EM365DayOfWeekType
+e_m365_array_get_day_of_week_element (JsonArray *array, /* const gchar * representing EM365DayOfWeekType */
+                                     guint index)
+{
+       return m365_json_utils_json_value_as_enum (json_array_get_string_element (array, index),
+               day_of_week_map, G_N_ELEMENTS (day_of_week_map),
+               E_M365_DAY_OF_WEEK_NOT_SET,
+               E_M365_DAY_OF_WEEK_UNKNOWN);
+}
+
+gint
+e_m365_recurrence_pattern_get_day_of_month (EM365RecurrencePattern *pattern)
+{
+       return e_m365_json_get_int_member (pattern, "dayOfMonth", 0);
+}
+
+void
+e_m365_recurrence_pattern_add_day_of_month (JsonBuilder *builder,
+                                           gint value)
+{
+       e_m365_json_add_int_member (builder, "dayOfMonth", value);
+}
+
+JsonArray * /* const gchar * representing EM365DayOfWeekType, use e_m365_array_get_day_of_week_element() */
+e_m365_recurrence_pattern_get_days_of_week (EM365RecurrencePattern *pattern)
+{
+       return e_m365_json_get_array_member (pattern, "daysOfWeek");
+}
+
+void
+e_m365_recurrence_pattern_begin_days_of_week (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "daysOfWeek");
+}
+
+void
+e_m365_recurrence_pattern_end_days_of_week (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_recurrence_pattern_add_day_of_week (JsonBuilder *builder,
+                                          EM365DayOfWeekType value)
+{
+       m365_json_utils_add_enum_as_json (builder, NULL, value,
+               day_of_week_map, G_N_ELEMENTS (day_of_week_map),
+               E_M365_DAY_OF_WEEK_NOT_SET,
+               E_M365_DAY_OF_WEEK_NOT_SET);
+}
+
+EM365DayOfWeekType
+e_m365_recurrence_pattern_get_first_day_of_week (EM365RecurrencePattern *pattern)
+{
+       return m365_json_utils_get_json_as_enum (pattern, "firstDayOfWeek",
+               day_of_week_map, G_N_ELEMENTS (day_of_week_map),
+               E_M365_DAY_OF_WEEK_NOT_SET,
+               E_M365_DAY_OF_WEEK_UNKNOWN);
+}
+
+void
+e_m365_recurrence_pattern_add_first_day_of_week (JsonBuilder *builder,
+                                                EM365DayOfWeekType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "firstDayOfWeek", value,
+               day_of_week_map, G_N_ELEMENTS (day_of_week_map),
+               E_M365_DAY_OF_WEEK_NOT_SET,
+               E_M365_DAY_OF_WEEK_NOT_SET);
+}
+
+EM365WeekIndexType
+e_m365_recurrence_pattern_get_index (EM365RecurrencePattern *pattern)
+{
+       return m365_json_utils_get_json_as_enum (pattern, "index",
+               week_index_map, G_N_ELEMENTS (week_index_map),
+               E_M365_WEEK_INDEX_NOT_SET,
+               E_M365_WEEK_INDEX_UNKNOWN);
+}
+
+void
+e_m365_recurrence_pattern_add_index (JsonBuilder *builder,
+                                    EM365WeekIndexType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "index", value,
+               week_index_map, G_N_ELEMENTS (week_index_map),
+               E_M365_WEEK_INDEX_NOT_SET,
+               E_M365_WEEK_INDEX_NOT_SET);
+}
+
+gint
+e_m365_recurrence_pattern_get_interval (EM365RecurrencePattern *pattern)
+{
+       return e_m365_json_get_int_member (pattern, "interval", -1);
+}
+
+void
+e_m365_recurrence_pattern_add_interval (JsonBuilder *builder,
+                                       gint value)
+{
+       e_m365_json_add_int_member (builder, "interval", value);
+}
+
+gint
+e_m365_recurrence_pattern_get_month (EM365RecurrencePattern *pattern)
+{
+       return e_m365_json_get_int_member (pattern, "month", -1);
+}
+
+void
+e_m365_recurrence_pattern_add_month (JsonBuilder *builder,
+                                    gint value)
+{
+       e_m365_json_add_int_member (builder, "month", value);
+}
+
+EM365RecurrencePatternType
+e_m365_recurrence_pattern_get_type (EM365RecurrencePattern *pattern)
+{
+       return m365_json_utils_get_json_as_enum (pattern, "type",
+               recurrence_pattern_map, G_N_ELEMENTS (recurrence_pattern_map),
+               E_M365_RECURRENCE_PATTERN_NOT_SET,
+               E_M365_RECURRENCE_PATTERN_UNKNOWN);
+}
+
+void
+e_m365_recurrence_pattern_add_type (JsonBuilder *builder,
+                                   EM365RecurrencePatternType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "type", value,
+               recurrence_pattern_map, G_N_ELEMENTS (recurrence_pattern_map),
+               E_M365_RECURRENCE_PATTERN_NOT_SET,
+               E_M365_RECURRENCE_PATTERN_UNKNOWN);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/recurrencerange?view=graph-rest-1.0 */
+
+EM365Date
+e_m365_recurrence_range_get_end_date (EM365RecurrenceRange *range)
+{
+       return e_m365_date_get (range, "endDate");
+}
+
+void
+e_m365_recurrence_range_add_end_date (JsonBuilder *builder,
+                                     EM365Date value)
+{
+       e_m365_add_date (builder, "endDate", value);
+}
+
+gint
+e_m365_recurrence_range_get_number_of_occurrences (EM365RecurrenceRange *range)
+{
+       return e_m365_json_get_int_member (range, "numberOfOccurrences", -1);
+}
+
+void
+e_m365_recurrence_range_add_number_of_occurrences (JsonBuilder *builder,
+                                                  gint value)
+{
+       e_m365_json_add_int_member (builder, "numberOfOccurrences", value);
+}
+
+const gchar *
+e_m365_recurrence_range_get_recurrence_time_zone (EM365RecurrenceRange *range)
+{
+       return e_m365_json_get_string_member (range, "recurrenceTimeZone", NULL);
+}
+
+void
+e_m365_recurrence_range_add_recurrence_time_zone (JsonBuilder *builder,
+                                                 const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "recurrenceTimeZone", value);
+}
+
+EM365Date
+e_m365_recurrence_range_get_start_date (EM365RecurrenceRange *range)
+{
+       return e_m365_date_get (range, "startDate");
+}
+
+void
+e_m365_recurrence_range_add_start_date (JsonBuilder *builder,
+                                       EM365Date value)
+{
+       e_m365_add_date (builder, "startDate", value);
+}
+
+EM365RecurrenceRangeType
+e_m365_recurrence_range_get_type (EM365RecurrenceRange *range)
+{
+       return m365_json_utils_get_json_as_enum (range, "type",
+               recurrence_range_map, G_N_ELEMENTS (recurrence_range_map),
+               E_M365_RECURRENCE_RANGE_NOT_SET,
+               E_M365_RECURRENCE_RANGE_UNKNOWN);
+}
+
+void
+e_m365_recurrence_range_add_type (JsonBuilder *builder,
+                                 EM365RecurrenceRangeType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "type", value,
+               recurrence_range_map, G_N_ELEMENTS (recurrence_range_map),
+               E_M365_RECURRENCE_RANGE_NOT_SET,
+               E_M365_RECURRENCE_RANGE_UNKNOWN);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/event?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_event_get_id (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "id", NULL);
+}
+
+const gchar *
+e_m365_event_get_change_key (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "changeKey", NULL);
+}
+
+JsonArray * /* EM365Attendee * */
+e_m365_event_get_attendees (EM365Event *event)
+{
+       return e_m365_json_get_array_member (event, "attendees");
+}
+
+void
+e_m365_event_begin_attendees (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "attendees");
+}
+
+void
+e_m365_event_end_attendees (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_event_add_attendee (JsonBuilder *builder,
+                          EM365AttendeeType type,
+                          EM365ResponseType response,
+                          time_t response_time,
+                          const gchar *name,
+                          const gchar *address)
+{
+       if (response_time <= (time_t) 0)
+               response_time = time (NULL);
+
+       e_m365_json_begin_object_member (builder, NULL);
+
+       m365_json_utils_add_enum_as_json (builder, "type", type,
+               attendee_map, G_N_ELEMENTS (attendee_map),
+               E_M365_ATTENDEE_NOT_SET,
+               E_M365_ATTENDEE_NOT_SET);
+
+       e_m365_json_begin_object_member (builder, "status");
+
+       m365_json_utils_add_enum_as_json (builder, "response", response,
+               response_map, G_N_ELEMENTS (response_map),
+               E_M365_RESPONSE_NOT_SET,
+               E_M365_RESPONSE_UNKNOWN);
+
+       e_m365_add_date_time_offset_member (builder, "time", response_time);
+
+       e_m365_json_end_object_member (builder); /* status */
+
+       if ((name && *name) || (address && *address))
+               e_m365_add_email_address (builder, "emailAddress", name, address);
+
+       e_m365_json_end_object_member (builder);
+}
+
+void
+e_m365_event_add_null_attendees (JsonBuilder *builder)
+{
+       e_m365_json_add_null_member (builder, "attendees");
+}
+
+EM365ItemBody *
+e_m365_event_get_body (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "body");
+}
+
+void
+e_m365_event_add_body (JsonBuilder *builder,
+                      EM365ItemBodyContentTypeType content_type,
+                      const gchar *content)
+{
+       e_m365_add_item_body (builder, "body", content_type, content);
+}
+
+const gchar *
+e_m365_event_get_body_preview (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "bodyPreview", NULL);
+}
+
+JsonArray * /* const gchar * */
+e_m365_event_get_categories (EM365Event *event)
+{
+       return e_m365_json_get_array_member (event, "categories");
+}
+
+void
+e_m365_event_begin_categories (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "categories");
+}
+
+void
+e_m365_event_end_categories (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_event_add_category (JsonBuilder *builder,
+                          const gchar *category)
+{
+       g_return_if_fail (category && *category);
+
+       json_builder_add_string_value (builder, category);
+}
+
+time_t
+e_m365_event_get_created_date_time (EM365Event *event)
+{
+       return e_m365_get_date_time_offset_member (event, "createdDateTime");
+}
+
+EM365DateTimeWithZone *
+e_m365_event_get_end (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "end");
+}
+
+void
+e_m365_event_add_end (JsonBuilder *builder,
+                     time_t date_time,
+                     const gchar *zone)
+{
+       e_m365_add_date_time (builder, "end", date_time, zone);
+}
+
+gboolean
+e_m365_event_get_has_attachments (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "hasAttachments", FALSE);
+}
+
+const gchar *
+e_m365_event_get_ical_uid (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "iCalUId", NULL);
+}
+
+EM365ImportanceType
+e_m365_event_get_importance (EM365Event *event)
+{
+       return m365_json_utils_get_json_as_enum (event, "importance",
+               importance_map, G_N_ELEMENTS (importance_map),
+               E_M365_IMPORTANCE_NOT_SET,
+               E_M365_IMPORTANCE_UNKNOWN);
+}
+
+void
+e_m365_event_add_importance (JsonBuilder *builder,
+                            EM365ImportanceType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "importance", value,
+               importance_map, G_N_ELEMENTS (importance_map),
+               E_M365_IMPORTANCE_NOT_SET,
+               E_M365_IMPORTANCE_UNKNOWN);
+}
+
+gboolean
+e_m365_event_get_is_all_day (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "isAllDay", FALSE);
+}
+
+void
+e_m365_event_add_is_all_day (JsonBuilder *builder,
+                            gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isAllDay", value);
+}
+
+gboolean
+e_m365_event_get_is_cancelled (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "isCancelled", FALSE);
+}
+
+gboolean
+e_m365_event_get_is_online_meeting (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "isOnlineMeeting", FALSE);
+}
+
+void
+e_m365_event_add_is_online_meeting (JsonBuilder *builder,
+                                   gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isOnlineMeeting", value);
+}
+
+gboolean
+e_m365_event_get_is_organizer (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "isOrganizer", FALSE);
+}
+
+gboolean
+e_m365_event_get_is_reminder_on (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "isReminderOn", FALSE);
+}
+
+void
+e_m365_event_add_is_reminder_on (JsonBuilder *builder,
+                                gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isReminderOn", value);
+}
+
+time_t
+e_m365_event_get_last_modified_date_time (EM365Event *event)
+{
+       return e_m365_get_date_time_offset_member (event, "lastModifiedDateTime");
+}
+
+EM365Location *
+e_m365_event_get_location (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "location");
+}
+
+void
+e_m365_event_begin_location (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "location");
+}
+
+void
+e_m365_event_end_location (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+void
+e_m365_event_add_null_location (JsonBuilder *builder)
+{
+       e_m365_json_add_null_member (builder, "location");
+}
+
+JsonArray * /* EM365Location * */
+e_m365_event_get_locations (EM365Event *event)
+{
+       return e_m365_json_get_array_member (event, "locations");
+}
+
+void
+e_m365_event_begin_locations (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "locations");
+}
+
+void
+e_m365_event_end_locations (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_event_begin_locations_location (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, NULL);
+}
+
+void
+e_m365_event_end_locations_location (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+EM365OnlineMeetingInfo *
+e_m365_event_get_online_meeting_info (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "onlineMeeting");
+}
+
+EM365OnlineMeetingProviderType
+e_m365_event_get_online_meeting_provider (EM365Event *event)
+{
+       return m365_json_utils_get_json_as_enum (event, "onlineMeetingProvider",
+               meeting_provider_map, G_N_ELEMENTS (meeting_provider_map),
+               E_M365_ONLINE_MEETING_PROVIDER_NOT_SET,
+               E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN);
+}
+
+void
+e_m365_event_add_online_meeting_provider (JsonBuilder *builder,
+                                         EM365OnlineMeetingProviderType value)
+{
+       if (value == E_M365_ONLINE_MEETING_PROVIDER_NOT_SET) {
+               e_m365_json_add_null_member (builder, "onlineMeetingProvider");
+       } else {
+               m365_json_utils_add_enum_as_json (builder, "onlineMeetingProvider", value,
+                       meeting_provider_map, G_N_ELEMENTS (meeting_provider_map),
+                       E_M365_ONLINE_MEETING_PROVIDER_NOT_SET,
+                       E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN);
+       }
+}
+
+const gchar *
+e_m365_event_get_online_meeting_url (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "onlineMeetingUrl", NULL);
+}
+
+EM365Recipient *
+e_m365_event_get_organizer (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "organizer");
+}
+
+void
+e_m365_event_add_organizer (JsonBuilder *builder,
+                           const gchar *name,
+                           const gchar *address)
+{
+       e_m365_add_recipient (builder, "organizer", name, address);
+}
+
+void
+e_m365_event_add_null_organizer (JsonBuilder *builder)
+{
+       e_m365_json_add_null_member (builder, "organizer");
+}
+
+const gchar *
+e_m365_event_get_original_end_timezone (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "originalEndTimeZone", NULL);
+}
+
+time_t
+e_m365_event_get_original_start (EM365Event *event)
+{
+       return e_m365_get_date_time_offset_member (event, "originalStart");
+}
+
+const gchar *
+e_m365_event_get_original_start_timezone (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "originalStartTimeZone", NULL);
+}
+
+EM365PatternedRecurrence *
+e_m365_event_get_recurrence (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "recurrence");
+}
+
+void
+e_m365_event_begin_recurrence (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "recurrence");
+}
+
+void
+e_m365_event_end_recurrence (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+void
+e_m365_event_add_null_recurrence (JsonBuilder *builder)
+{
+       e_m365_json_add_null_member (builder, "recurrence");
+}
+
+gint
+e_m365_event_get_reminder_minutes_before_start (EM365Event *event)
+{
+       return e_m365_json_get_int_member (event, "reminderMinutesBeforeStart", -1);
+}
+
+void
+e_m365_event_add_reminder_minutes_before_start (JsonBuilder *builder,
+                                               gint value)
+{
+       e_m365_json_add_int_member (builder, "reminderMinutesBeforeStart", value);
+}
+
+gboolean
+e_m365_event_get_response_requested (EM365Event *event)
+{
+       return e_m365_json_get_boolean_member (event, "responseRequested", FALSE);
+}
+
+void
+e_m365_event_add_response_requested (JsonBuilder *builder,
+                                    gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "responseRequested", value);
+}
+
+
+EM365ResponseStatus *
+e_m365_event_get_response_status (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "responseStatus");
+}
+
+EM365SensitivityType
+e_m365_event_get_sensitivity (EM365Event *event)
+{
+       return m365_json_utils_get_json_as_enum (event, "sensitivity",
+               sensitivity_map, G_N_ELEMENTS (sensitivity_map),
+               E_M365_SENSITIVITY_NOT_SET,
+               E_M365_SENSITIVITY_UNKNOWN);
+}
+
+void
+e_m365_event_add_sensitivity (JsonBuilder *builder,
+                             EM365SensitivityType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "sensitivity", value,
+               sensitivity_map, G_N_ELEMENTS (sensitivity_map),
+               E_M365_SENSITIVITY_NOT_SET,
+               E_M365_SENSITIVITY_UNKNOWN);
+}
+
+const gchar *
+e_m365_event_get_series_master_id (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "seriesMasterId", NULL);
+}
+
+EM365FreeBusyStatusType
+e_m365_event_get_show_as (EM365Event *event)
+{
+       return m365_json_utils_get_json_as_enum (event, "showAs",
+               free_busy_status_map, G_N_ELEMENTS (free_busy_status_map),
+               E_M365_FREE_BUSY_STATUS_NOT_SET,
+               E_M365_FREE_BUSY_STATUS_UNKNOWN);
+}
+
+void
+e_m365_event_add_show_as (JsonBuilder *builder,
+                         EM365FreeBusyStatusType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "showAs", value,
+               free_busy_status_map, G_N_ELEMENTS (free_busy_status_map),
+               E_M365_FREE_BUSY_STATUS_NOT_SET,
+               E_M365_FREE_BUSY_STATUS_UNKNOWN);
+}
+
+EM365DateTimeWithZone *
+e_m365_event_get_start (EM365Event *event)
+{
+       return e_m365_json_get_object_member (event, "start");
+}
+
+void
+e_m365_event_add_start (JsonBuilder *builder,
+                       time_t date_time,
+                       const gchar *zone)
+{
+       e_m365_add_date_time (builder, "start", date_time, zone);
+}
+
+const gchar *
+e_m365_event_get_subject (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "subject", NULL);
+}
+
+void
+e_m365_event_add_subject (JsonBuilder *builder,
+                         const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "subject", value ? value : "");
+}
+
+EM365EventTypeType
+e_m365_event_get_type (EM365Event *event)
+{
+       return m365_json_utils_get_json_as_enum (event, "type",
+               event_type_map, G_N_ELEMENTS (event_type_map),
+               E_M365_EVENT_TYPE_NOT_SET,
+               E_M365_EVENT_TYPE_UNKNOWN);
+}
+
+const gchar *
+e_m365_event_get_web_link (EM365Event *event)
+{
+       return e_m365_json_get_string_member (event, "webLink", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/freebusyerror?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_free_busy_error_get_message (EM365FreeBusyError *fberror)
+{
+       return e_m365_json_get_string_member (fberror, "message", NULL);
+}
+
+const gchar *
+e_m365_free_busy_error_get_response_code (EM365FreeBusyError *fberror)
+{
+       return e_m365_json_get_string_member (fberror, "responseCode", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/scheduleitem?view=graph-rest-1.0 */
+
+EM365DateTimeWithZone *
+e_m365_schedule_item_get_end (EM365ScheduleItem *schitem)
+{
+       return e_m365_json_get_object_member (schitem, "end");
+}
+
+gboolean
+e_m365_schedule_item_get_is_private (EM365ScheduleItem *schitem)
+{
+       return e_m365_json_get_boolean_member (schitem, "isPrivate", FALSE);
+}
+
+const gchar *
+e_m365_schedule_item_get_location (EM365ScheduleItem *schitem)
+{
+       return e_m365_json_get_string_member (schitem, "location", NULL);
+}
+
+EM365DateTimeWithZone *
+e_m365_schedule_item_get_start (EM365ScheduleItem *schitem)
+{
+       return e_m365_json_get_object_member (schitem, "start");
+}
+
+EM365FreeBusyStatusType
+e_m365_schedule_item_get_status (EM365ScheduleItem *schitem)
+{
+       return m365_json_utils_get_json_as_enum (schitem, "status",
+               free_busy_status_map, G_N_ELEMENTS (free_busy_status_map),
+               E_M365_FREE_BUSY_STATUS_NOT_SET,
+               E_M365_FREE_BUSY_STATUS_UNKNOWN);
+}
+
+const gchar *
+e_m365_schedule_item_get_subject (EM365ScheduleItem *schitem)
+{
+       return e_m365_json_get_string_member (schitem, "subject", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/workinghours?view=graph-rest-1.0 */
+
+JsonArray * /* Use e_m365_array_get_day_of_week_element() to get the items */
+e_m365_working_hours_get_days_of_week (EM365WorkingHours *wrkhrs)
+{
+       return e_m365_json_get_array_member (wrkhrs, "daysOfWeek");
+}
+
+EM365TimeOfDay
+e_m365_working_hours_get_start_time (EM365WorkingHours *wrkhrs)
+{
+       return e_m365_time_of_day_get (wrkhrs, "startTime");
+}
+
+EM365TimeOfDay
+e_m365_working_hours_get_end_time (EM365WorkingHours *wrkhrs)
+{
+       return e_m365_time_of_day_get (wrkhrs, "endTime");
+}
+
+const gchar *
+e_m365_working_hours_get_time_zone_name (EM365WorkingHours *wrkhrs)
+{
+       JsonObject *value;
+
+       value = e_m365_json_get_object_member (wrkhrs, "timeZone");
+
+       if (!value)
+               return NULL;
+
+       /* https://docs.microsoft.com/en-us/graph/api/resources/timezonebase?view=graph-rest-1.0 */
+
+       return e_m365_json_get_string_member (value, "name", NULL);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/scheduleinformation?view=graph-rest-1.0 */
+
+const gchar *
+e_m365_schedule_information_get_availability_view (EM365ScheduleInformation *schinfo)
+{
+       return e_m365_json_get_string_member (schinfo, "availabilityView", NULL);
+}
+
+EM365FreeBusyError *
+e_m365_schedule_information_get_free_busy_error (EM365ScheduleInformation *schinfo)
+{
+       return e_m365_json_get_object_member (schinfo, "error");
+}
+
+const gchar *
+e_m365_schedule_information_get_schedule_id (EM365ScheduleInformation *schinfo)
+{
+       return e_m365_json_get_string_member (schinfo, "scheduleId", NULL);
+}
+
+JsonArray * /* EM365ScheduleItem * */
+e_m365_schedule_information_get_schedule_items (EM365ScheduleInformation *schinfo)
+{
+       return e_m365_json_get_array_member (schinfo, "scheduleItems");
+}
+
+EM365WorkingHours *
+e_m365_schedule_information_get_working_hours (EM365ScheduleInformation *schinfo)
+{
+       return e_m365_json_get_object_member (schinfo, "workingHours");
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/outlooktaskfolder?view=graph-rest-beta */
+
+const gchar *
+e_m365_task_folder_get_id (EM365TaskFolder *folder)
+{
+       return e_m365_json_get_string_member (folder, "id", NULL);
+}
+
+const gchar *
+e_m365_task_folder_get_change_key (EM365TaskFolder *folder)
+{
+       return e_m365_json_get_string_member (folder, "changeKey", NULL);
+}
+
+const gchar *
+e_m365_task_folder_get_parent_group_key (EM365TaskFolder *folder)
+{
+       return e_m365_json_get_string_member (folder, "parentGroupKey", NULL);
+}
+
+const gchar *
+e_m365_task_folder_get_name (EM365TaskFolder *folder)
+{
+       return e_m365_json_get_string_member (folder, "name", NULL);
+}
+
+gboolean
+e_m365_task_folder_get_is_default_folder (EM365TaskFolder *folder)
+{
+       return e_m365_json_get_boolean_member (folder, "isDefaultFolder", FALSE);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/outlooktaskgroup?view=graph-rest-beta */
+
+const gchar *
+e_m365_task_group_get_id (EM365TaskGroup *group)
+{
+       return e_m365_json_get_string_member (group, "id", NULL);
+}
+
+const gchar *
+e_m365_task_group_get_change_key (EM365TaskGroup *group)
+{
+       return e_m365_json_get_string_member (group, "changeKey", NULL);
+}
+
+const gchar *
+e_m365_task_group_get_group_key (EM365TaskGroup *group)
+{
+       return e_m365_json_get_string_member (group, "groupKey", NULL);
+}
+
+const gchar *
+e_m365_task_group_get_name (EM365TaskGroup *group)
+{
+       return e_m365_json_get_string_member (group, "name", NULL);
+}
+
+gboolean
+e_m365_task_group_get_is_default_group (EM365TaskGroup *group)
+{
+       return e_m365_json_get_boolean_member (group, "isDefaultGroup", FALSE);
+}
+
+/* https://docs.microsoft.com/en-us/graph/api/resources/outlooktask?view=graph-rest-beta */
+
+const gchar *
+e_m365_task_get_id (EM365Task *task)
+{
+       return e_m365_json_get_string_member (task, "id", NULL);
+}
+
+const gchar *
+e_m365_task_get_change_key (EM365Task *task)
+{
+       return e_m365_json_get_string_member (task, "changeKey", NULL);
+}
+
+const gchar *
+e_m365_task_get_parent_folder_id (EM365Task *task)
+{
+       return e_m365_json_get_string_member (task, "parentFolderId", NULL);
+}
+
+const gchar *
+e_m365_task_get_assigned_to (EM365Task *task)
+{
+       return e_m365_json_get_string_member (task, "assignedTo", NULL);
+}
+
+EM365ItemBody *
+e_m365_task_get_body (EM365Task *task)
+{
+       return e_m365_json_get_object_member (task, "body");
+}
+
+void
+e_m365_task_add_body (JsonBuilder *builder,
+                     EM365ItemBodyContentTypeType content_type,
+                     const gchar *content)
+{
+       e_m365_add_item_body (builder, "body", content_type, content);
+}
+
+JsonArray * /* const gchar * */
+e_m365_task_get_categories (EM365Task *task)
+{
+       return e_m365_json_get_array_member (task, "categories");
+}
+
+void
+e_m365_task_begin_categories (JsonBuilder *builder)
+{
+       e_m365_json_begin_array_member (builder, "categories");
+}
+
+void
+e_m365_task_end_categories (JsonBuilder *builder)
+{
+       e_m365_json_end_array_member (builder);
+}
+
+void
+e_m365_task_add_category (JsonBuilder *builder,
+                         const gchar *category)
+{
+       g_return_if_fail (category && *category);
+
+       json_builder_add_string_value (builder, category);
+}
+
+EM365DateTimeWithZone *
+e_m365_task_get_completed_date_time (EM365Task *task)
+{
+       return e_m365_json_get_object_member (task, "completedDateTime");
+}
+
+void
+e_m365_task_add_completed_date_time (JsonBuilder *builder,
+                                    time_t date_time,
+                                    const gchar *zone)
+{
+       e_m365_add_date_time (builder, "completedDateTime", date_time, zone);
+}
+
+time_t
+e_m365_task_get_created_date_time (EM365Task *task)
+{
+       return e_m365_get_date_time_offset_member (task, "createdDateTime");
+}
+
+EM365DateTimeWithZone *
+e_m365_task_get_due_date_time (EM365Task *task)
+{
+       return e_m365_json_get_object_member (task, "dueDateTime");
+}
+
+void
+e_m365_task_add_due_date_time (JsonBuilder *builder,
+                              time_t date_time,
+                              const gchar *zone)
+{
+       e_m365_add_date_time (builder, "dueDateTime", date_time, zone);
+}
+
+gboolean
+e_m365_task_get_has_attachments (EM365Task *task)
+{
+       return e_m365_json_get_boolean_member (task, "hasAttachments", FALSE);
+}
+
+EM365ImportanceType
+e_m365_task_get_importance (EM365Task *task)
+{
+       return m365_json_utils_get_json_as_enum (task, "importance",
+               importance_map, G_N_ELEMENTS (importance_map),
+               E_M365_IMPORTANCE_NOT_SET,
+               E_M365_IMPORTANCE_UNKNOWN);
+}
+
+void
+e_m365_task_add_importance (JsonBuilder *builder,
+                           EM365ImportanceType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "importance", value,
+               importance_map, G_N_ELEMENTS (importance_map),
+               E_M365_IMPORTANCE_NOT_SET,
+               E_M365_IMPORTANCE_NOT_SET);
+}
+
+gboolean
+e_m365_task_get_is_reminder_on (EM365Task *task)
+{
+       return e_m365_json_get_boolean_member (task, "isReminderOn", FALSE);
+}
+
+void
+e_m365_task_add_is_reminder_on (JsonBuilder *builder,
+                               gboolean value)
+{
+       e_m365_json_add_boolean_member (builder, "isReminderOn", value);
+}
+
+time_t
+e_m365_task_get_last_modified_date_time (EM365Task *task)
+{
+       return e_m365_get_date_time_offset_member (task, "lastModifiedDateTime");
+}
+
+const gchar *
+e_m365_task_get_owner (EM365Task *task)
+{
+       return e_m365_json_get_string_member (task, "owner", NULL);
+}
+
+void
+e_m365_task_add_owner (JsonBuilder *builder,
+                      const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "owner", value);
+}
+
+EM365PatternedRecurrence *
+e_m365_task_get_recurrence (EM365Task *task)
+{
+       return e_m365_json_get_object_member (task, "recurrence");
+}
+
+void
+e_m365_task_begin_recurrence (JsonBuilder *builder)
+{
+       e_m365_json_begin_object_member (builder, "recurrence");
+}
+
+void
+e_m365_task_end_recurrence (JsonBuilder *builder)
+{
+       e_m365_json_end_object_member (builder);
+}
+
+void
+e_m365_task_add_null_recurrence (JsonBuilder *builder)
+{
+       e_m365_json_add_null_member (builder, "recurrence");
+}
+
+EM365DateTimeWithZone *
+e_m365_task_get_reminder_date_time (EM365Task *task)
+{
+       return e_m365_json_get_object_member (task, "reminderDateTime");
+}
+
+void
+e_m365_task_add_reminder_date_time (JsonBuilder *builder,
+                                   time_t date_time,
+                                   const gchar *zone)
+{
+       e_m365_add_date_time (builder, "reminderDateTime", date_time, zone);
+}
+
+EM365SensitivityType
+e_m365_task_get_sensitivity (EM365Task *task)
+{
+       return m365_json_utils_get_json_as_enum (task, "sensitivity",
+               sensitivity_map, G_N_ELEMENTS (sensitivity_map),
+               E_M365_SENSITIVITY_NOT_SET,
+               E_M365_SENSITIVITY_UNKNOWN);
+}
+
+void
+e_m365_task_add_sensitivity (JsonBuilder *builder,
+                            EM365SensitivityType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "sensitivity", value,
+               sensitivity_map, G_N_ELEMENTS (sensitivity_map),
+               E_M365_SENSITIVITY_NOT_SET,
+               E_M365_SENSITIVITY_UNKNOWN);
+}
+
+EM365DateTimeWithZone *
+e_m365_task_get_start_date_time (EM365Task *task)
+{
+       return e_m365_json_get_object_member (task, "startDateTime");
+}
+
+void
+e_m365_task_add_start_date_time (JsonBuilder *builder,
+                                time_t date_time,
+                                const gchar *zone)
+{
+       e_m365_add_date_time (builder, "startDateTime", date_time, zone);
+}
+
+EM365StatusType
+e_m365_task_get_status (EM365Task *task)
+{
+       return m365_json_utils_get_json_as_enum (task, "status",
+               status_map, G_N_ELEMENTS (status_map),
+               E_M365_STATUS_NOT_SET,
+               E_M365_STATUS_UNKNOWN);
+}
+
+void
+e_m365_task_add_status (JsonBuilder *builder,
+                       EM365StatusType value)
+{
+       m365_json_utils_add_enum_as_json (builder, "status", value,
+               status_map, G_N_ELEMENTS (status_map),
+               E_M365_STATUS_NOT_SET,
+               E_M365_STATUS_UNKNOWN);
+}
+
+const gchar *
+e_m365_task_get_subject (EM365Task *task)
+{
+       return e_m365_json_get_string_member (task, "subject", NULL);
+}
+
+void
+e_m365_task_add_subject (JsonBuilder *builder,
+                        const gchar *value)
+{
+       e_m365_json_add_string_member (builder, "subject", value);
+}
diff --git a/src/Microsoft365/common/e-m365-json-utils.h b/src/Microsoft365/common/e-m365-json-utils.h
new file mode 100644
index 00000000..df5a0548
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-json-utils.h
@@ -0,0 +1,1056 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_M365_JSON_UTILS_H
+#define E_M365_JSON_UTILS_H
+
+#include <time.h>
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+/* Just for better readability */
+#define EM365Attachment                        JsonObject
+#define EM365Attendee                  JsonObject
+#define EM365Calendar                  JsonObject
+#define EM365CalendarGroup             JsonObject
+#define EM365Category                  JsonObject
+#define EM365Contact                   JsonObject
+#define EM365Date                      gint
+#define EM365DateTimeWithZone          JsonObject
+#define EM365EmailAddress              JsonObject
+#define EM365Event                     JsonObject
+#define EM365Folder                    JsonObject
+#define EM365FollowupFlag              JsonObject
+#define EM365FreeBusyError             JsonObject
+#define EM365InternetMessageHeader     JsonObject
+#define EM365ItemBody                  JsonObject
+#define EM365Location                  JsonObject
+#define EM365MailFolder                        JsonObject
+#define EM365MailMessage               JsonObject
+#define EM365OnlineMeetingInfo         JsonObject
+#define EM365OutlookGeoCoordinates     JsonObject
+#define EM365PatternedRecurrence       JsonObject
+#define EM365Phone                     JsonObject
+#define EM365PhysicalAddress           JsonObject
+#define EM365Recipient                 JsonObject
+#define EM365RecurrencePattern         JsonObject
+#define EM365RecurrenceRange           JsonObject
+#define EM365ResponseStatus            JsonObject
+#define EM365ScheduleInformation       JsonObject
+#define EM365ScheduleItem              JsonObject
+#define EM365Task                      JsonObject
+#define EM365TaskFolder                        JsonObject
+#define EM365TaskGroup                 JsonObject
+#define EM365TimeOfDay                 gint64
+#define EM365WorkingHours              JsonObject
+
+typedef enum _EM365AttachmentDataType {
+       E_M365_ATTACHMENT_DATA_TYPE_NOT_SET,
+       E_M365_ATTACHMENT_DATA_TYPE_UNKNOWN,
+       E_M365_ATTACHMENT_DATA_TYPE_FILE,
+       E_M365_ATTACHMENT_DATA_TYPE_ITEM,
+       E_M365_ATTACHMENT_DATA_TYPE_REFERENCE
+} EM365AttachmentDataType;
+
+typedef enum _EM365AttendeeType {
+       E_M365_ATTENDEE_NOT_SET,
+       E_M365_ATTENDEE_UNKNOWN,
+       E_M365_ATTENDEE_REQUIRED,
+       E_M365_ATTENDEE_OPTIONAL,
+       E_M365_ATTENDEE_RESOURCE
+} EM365AttendeeType;
+
+typedef enum _EM365CalendarColorType {
+       E_M365_CALENDAR_COLOR_NOT_SET           = -3,
+       E_M365_CALENDAR_COLOR_UNKNOWN           = -2,
+       E_M365_CALENDAR_COLOR_AUTO              = -1,
+       E_M365_CALENDAR_COLOR_LIGHT_BLUE        = 0,
+       E_M365_CALENDAR_COLOR_LIGHT_GREEN       = 1,
+       E_M365_CALENDAR_COLOR_LIGHT_ORANGE      = 2,
+       E_M365_CALENDAR_COLOR_LIGHT_GRAY        = 3,
+       E_M365_CALENDAR_COLOR_LIGHT_YELLOW      = 4,
+       E_M365_CALENDAR_COLOR_LIGHT_TEAL        = 5,
+       E_M365_CALENDAR_COLOR_LIGHT_PINK        = 6,
+       E_M365_CALENDAR_COLOR_LIGHT_BROWN       = 7,
+       E_M365_CALENDAR_COLOR_LIGHT_RED         = 8,
+       E_M365_CALENDAR_COLOR_MAX_COLOR         = 9
+} EM365CalendarColorType;
+
+typedef enum _EM365DayOfWeekType {
+       E_M365_DAY_OF_WEEK_NOT_SET,
+       E_M365_DAY_OF_WEEK_UNKNOWN,
+       E_M365_DAY_OF_WEEK_SUNDAY,
+       E_M365_DAY_OF_WEEK_MONDAY,
+       E_M365_DAY_OF_WEEK_TUESDAY,
+       E_M365_DAY_OF_WEEK_WEDNESDAY,
+       E_M365_DAY_OF_WEEK_THURSDAY,
+       E_M365_DAY_OF_WEEK_FRIDAY,
+       E_M365_DAY_OF_WEEK_SATURDAY
+} EM365DayOfWeekType;
+
+typedef enum _EM365EventTypeType {
+       E_M365_EVENT_TYPE_NOT_SET,
+       E_M365_EVENT_TYPE_UNKNOWN,
+       E_M365_EVENT_TYPE_SINGLE_INSTANCE,
+       E_M365_EVENT_TYPE_OCCURRENCE,
+       E_M365_EVENT_TYPE_EXCEPTION,
+       E_M365_EVENT_TYPE_SERIES_MASTER
+} EM365EventTypeType;
+
+typedef enum _EM365FollowupFlagStatusType {
+       E_M365_FOLLOWUP_FLAG_STATUS_NOT_SET,
+       E_M365_FOLLOWUP_FLAG_STATUS_UNKNOWN,
+       E_M365_FOLLOWUP_FLAG_STATUS_NOT_FLAGGED,
+       E_M365_FOLLOWUP_FLAG_STATUS_COMPLETE,
+       E_M365_FOLLOWUP_FLAG_STATUS_FLAGGED
+} EM365FollowupFlagStatusType;
+
+typedef enum _EM365FreeBusyStatusType {
+       E_M365_FREE_BUSY_STATUS_NOT_SET,
+       E_M365_FREE_BUSY_STATUS_UNKNOWN,
+       E_M365_FREE_BUSY_STATUS_FREE,
+       E_M365_FREE_BUSY_STATUS_TENTATIVE,
+       E_M365_FREE_BUSY_STATUS_BUSY,
+       E_M365_FREE_BUSY_STATUS_OOF,
+       E_M365_FREE_BUSY_STATUS_WORKING_ELSEWHERE
+} EM365FreeBusyStatusType;
+
+typedef enum _EM365ImportanceType {
+       E_M365_IMPORTANCE_NOT_SET,
+       E_M365_IMPORTANCE_UNKNOWN,
+       E_M365_IMPORTANCE_LOW,
+       E_M365_IMPORTANCE_NORMAL,
+       E_M365_IMPORTANCE_HIGH
+} EM365ImportanceType;
+
+typedef enum _EM365InferenceClassificationType {
+       E_M365_INFERENCE_CLASSIFICATION_NOT_SET,
+       E_M365_INFERENCE_CLASSIFICATION_UNKNOWN,
+       E_M365_INFERENCE_CLASSIFICATION_FOCUSED,
+       E_M365_INFERENCE_CLASSIFICATION_OTHER
+} EM365InferenceClassificationType;
+
+typedef enum _EM365ItemBodyContentTypeType {
+       E_M365_ITEM_BODY_CONTENT_TYPE_NOT_SET,
+       E_M365_ITEM_BODY_CONTENT_TYPE_UNKNOWN,
+       E_M365_ITEM_BODY_CONTENT_TYPE_TEXT,
+       E_M365_ITEM_BODY_CONTENT_TYPE_HTML
+} EM365ItemBodyContentTypeType;
+
+typedef enum _EM365LocationType {
+       E_M365_LOCATION_NOT_SET,
+       E_M365_LOCATION_UNKNOWN,
+       E_M365_LOCATION_DEFAULT,
+       E_M365_LOCATION_CONFERENCE_ROOM,
+       E_M365_LOCATION_HOME_ADDRESS,
+       E_M365_LOCATION_BUSINESS_ADDRESS,
+       E_M365_LOCATION_GEO_COORDINATES,
+       E_M365_LOCATION_STREET_ADDRESS,
+       E_M365_LOCATION_HOTEL,
+       E_M365_LOCATION_RESTAURANT,
+       E_M365_LOCATION_LOCAL_BUSINESS,
+       E_M365_LOCATION_POSTAL_ADDRESS
+} EM365LocationType;
+
+typedef enum _EM365OnlineMeetingProviderType {
+       E_M365_ONLINE_MEETING_PROVIDER_NOT_SET                  = -1,
+       E_M365_ONLINE_MEETING_PROVIDER_UNKNOWN                  = 0,
+       E_M365_ONLINE_MEETING_PROVIDER_SKYPE_FOR_BUSINESS       = 1 << 0,
+       E_M365_ONLINE_MEETING_PROVIDER_SKYPE_FOR_CONSUMER       = 1 << 1,
+       E_M365_ONLINE_MEETING_PROVIDER_TEAMS_FOR_BUSINESS       = 1 << 2
+} EM365OnlineMeetingProviderType;
+
+typedef enum _EM365PhoneType {
+       E_M365_PHONE_NOT_SET,
+       E_M365_PHONE_UNKNOWN,
+       E_M365_PHONE_HOME,
+       E_M365_PHONE_BUSINESS,
+       E_M365_PHONE_MOBILE,
+       E_M365_PHONE_OTHER,
+       E_M365_PHONE_ASSISTANT,
+       E_M365_PHONE_HOMEFAX,
+       E_M365_PHONE_BUSINESSFAX,
+       E_M365_PHONE_OTHERFAX,
+       E_M365_PHONE_PAGER,
+       E_M365_PHONE_RADIO
+} EM365PhoneType;
+
+typedef enum _EM365RecurrencePatternType {
+       E_M365_RECURRENCE_PATTERN_NOT_SET,
+       E_M365_RECURRENCE_PATTERN_UNKNOWN,
+       E_M365_RECURRENCE_PATTERN_DAILY,
+       E_M365_RECURRENCE_PATTERN_WEEKLY,
+       E_M365_RECURRENCE_PATTERN_ABSOLUTE_MONTHLY,
+       E_M365_RECURRENCE_PATTERN_RELATIVE_MONTHLY,
+       E_M365_RECURRENCE_PATTERN_ABSOLUTE_YEARLY,
+       E_M365_RECURRENCE_PATTERN_RELATIVE_YEARLY
+} EM365RecurrencePatternType;
+
+typedef enum _EM365RecurrenceRangeType {
+       E_M365_RECURRENCE_RANGE_NOT_SET,
+       E_M365_RECURRENCE_RANGE_UNKNOWN,
+       E_M365_RECURRENCE_RANGE_ENDDATE,
+       E_M365_RECURRENCE_RANGE_NOEND,
+       E_M365_RECURRENCE_RANGE_NUMBERED
+} EM365RecurrenceRangeType;
+
+typedef enum _EM365ResponseType {
+       E_M365_RESPONSE_NOT_SET,
+       E_M365_RESPONSE_UNKNOWN,
+       E_M365_RESPONSE_NONE,
+       E_M365_RESPONSE_ORGANIZER,
+       E_M365_RESPONSE_TENTATIVELY_ACCEPTED,
+       E_M365_RESPONSE_ACCEPTED,
+       E_M365_RESPONSE_DECLINED,
+       E_M365_RESPONSE_NOT_RESPONDED
+} EM365ResponseType;
+
+typedef enum _EM365SensitivityType {
+       E_M365_SENSITIVITY_NOT_SET,
+       E_M365_SENSITIVITY_UNKNOWN,
+       E_M365_SENSITIVITY_NORMAL,
+       E_M365_SENSITIVITY_PERSONAL,
+       E_M365_SENSITIVITY_PRIVATE,
+       E_M365_SENSITIVITY_CONFIDENTIAL
+} EM365SensitivityType;
+
+typedef enum _EM365StatusType {
+       E_M365_STATUS_NOT_SET,
+       E_M365_STATUS_UNKNOWN,
+       E_M365_STATUS_NOT_STARTED,
+       E_M365_STATUS_IN_PROGRESS,
+       E_M365_STATUS_COMPLETED,
+       E_M365_STATUS_WAITING_ON_OTHERS,
+       E_M365_STATUS_DEFERRED
+} EM365StatusType;
+
+typedef enum _EM365WeekIndexType {
+       E_M365_WEEK_INDEX_NOT_SET,
+       E_M365_WEEK_INDEX_UNKNOWN,
+       E_M365_WEEK_INDEX_FIRST,
+       E_M365_WEEK_INDEX_SECOND,
+       E_M365_WEEK_INDEX_THIRD,
+       E_M365_WEEK_INDEX_FOURTH,
+       E_M365_WEEK_INDEX_LAST
+} EM365WeekIndexType;
+
+const gchar *  e_m365_calendar_color_to_rgb            (EM365CalendarColorType color);
+EM365CalendarColorType
+               e_m365_rgb_to_calendar_color            (const gchar *rgb);
+
+JsonArray *    e_m365_json_get_array_member            (JsonObject *object,
+                                                        const gchar *member_name);
+void           e_m365_json_begin_array_member          (JsonBuilder *builder,
+                                                        const gchar *member_name);
+void           e_m365_json_end_array_member            (JsonBuilder *builder);
+gboolean       e_m365_json_get_boolean_member          (JsonObject *object,
+                                                        const gchar *member_name,
+                                                        gboolean default_value);
+void           e_m365_json_add_boolean_member          (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        gboolean value);
+gdouble                e_m365_json_get_double_member           (JsonObject *object,
+                                                        const gchar *member_name,
+                                                        gdouble default_value);
+void           e_m365_json_add_double_member           (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        gdouble value);
+gint64         e_m365_json_get_int_member              (JsonObject *object,
+                                                        const gchar *member_name,
+                                                        gint64 default_value);
+void           e_m365_json_add_int_member              (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        gint64 value);
+gboolean       e_m365_json_get_null_member             (JsonObject *object,
+                                                        const gchar *member_name,
+                                                        gboolean default_value);
+void           e_m365_json_add_null_member             (JsonBuilder *builder,
+                                                        const gchar *member_name);
+JsonObject *   e_m365_json_get_object_member           (JsonObject *object,
+                                                        const gchar *member_name);
+void           e_m365_json_begin_object_member         (JsonBuilder *builder,
+                                                        const gchar *member_name);
+void           e_m365_json_end_object_member           (JsonBuilder *builder);
+const gchar *  e_m365_json_get_string_member           (JsonObject *object,
+                                                        const gchar *member_name,
+                                                        const gchar *default_value);
+void           e_m365_json_add_string_member           (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        const gchar *value);
+void           e_m365_json_add_nonempty_string_member  (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        const gchar *value);
+void           e_m365_json_add_nonempty_or_null_string_member
+                                                       (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        const gchar *value);
+
+EM365Date      e_m365_date_get                         (JsonObject *object,
+                                                        const gchar *member_name);
+void           e_m365_add_date                         (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        EM365Date value);
+gboolean       e_m365_date_decode                      (EM365Date dt,
+                                                        gint *out_year,
+                                                        gint *out_month,
+                                                        gint *out_day);
+EM365Date      e_m365_date_encode                      (gint year,
+                                                        gint month,
+                                                        gint day);
+EM365TimeOfDay e_m365_time_of_day_get                  (JsonObject *object,
+                                                        const gchar *member_name);
+void           e_m365_add_time_of_day                  (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        EM365TimeOfDay value);
+gboolean       e_m365_time_of_day_decode               (EM365TimeOfDay tod,
+                                                        gint *out_hour,
+                                                        gint *out_minute,
+                                                        gint *out_second,
+                                                        gint *out_fraction);
+EM365TimeOfDay e_m365_time_of_day_encode               (gint hour,
+                                                        gint minute,
+                                                        gint second,
+                                                        gint fraction);
+
+time_t         e_m365_get_date_time_offset_member      (JsonObject *object,
+                                                        const gchar *member_name);
+void           e_m365_add_date_time_offset_member      (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        time_t value);
+
+time_t         e_m365_date_time_get_date_time          (EM365DateTimeWithZone *datetime);
+const gchar *  e_m365_date_time_get_time_zone          (EM365DateTimeWithZone *datetime);
+void           e_m365_add_date_time                    (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+
+gboolean       e_m365_delta_is_removed_object          (JsonObject *object);
+
+const gchar *  e_m365_category_get_display_name        (EM365Category *category);
+const gchar *  e_m365_category_get_id                  (EM365Category *category);
+const gchar *  e_m365_category_get_color               (EM365Category *category);
+
+const gchar *  e_m365_folder_get_id                    (EM365Folder *folder);
+const gchar *  e_m365_folder_get_parent_folder_id      (EM365Folder *folder);
+const gchar *  e_m365_folder_get_display_name          (EM365Folder *folder);
+
+gint32         e_m365_mail_folder_get_child_folder_count
+                                                       (EM365MailFolder *folder);
+gint32         e_m365_mail_folder_get_total_item_count (EM365MailFolder *folder);
+gint32         e_m365_mail_folder_get_unread_item_count(EM365MailFolder *folder);
+
+const gchar *  e_m365_recipient_get_name               (EM365Recipient *recipient);
+const gchar *  e_m365_recipient_get_address            (EM365Recipient *recipient);
+void           e_m365_add_recipient                    (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        const gchar *name,
+                                                        const gchar *address);
+
+const gchar *  e_m365_internet_message_header_get_name (EM365InternetMessageHeader *header);
+const gchar *  e_m365_internet_message_header_get_value(EM365InternetMessageHeader *header);
+void           e_m365_add_internet_message_header      (JsonBuilder *builder,
+                                                        const gchar *name,
+                                                        const gchar *value);
+
+EM365DateTimeWithZone *
+               e_m365_followup_flag_get_completed_date_time
+                                                       (EM365FollowupFlag *flag);
+void           e_m365_followup_flag_add_completed_date_time
+                                                       (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+EM365DateTimeWithZone *
+               e_m365_followup_flag_get_due_date_time  (EM365FollowupFlag *flag);
+void           e_m365_followup_flag_add_due_date_time  (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+EM365FollowupFlagStatusType
+               e_m365_followup_flag_get_flag_status    (EM365FollowupFlag *flag);
+void           e_m365_followup_flag_add_flag_status    (JsonBuilder *builder,
+                                                        EM365FollowupFlagStatusType status);
+EM365DateTimeWithZone *
+               e_m365_followup_flag_get_start_date_time(EM365FollowupFlag *flag);
+void           e_m365_followup_flag_add_start_date_time(JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+
+const gchar *  e_m365_item_body_get_content            (EM365ItemBody *item_body);
+EM365ItemBodyContentTypeType
+               e_m365_item_body_get_content_type       (EM365ItemBody *item_body);
+void           e_m365_add_item_body                    (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        EM365ItemBodyContentTypeType content_type,
+                                                        const gchar *content);
+
+JsonArray *    e_m365_mail_message_get_bcc_recipients  (EM365MailMessage *mail); /* EM365Recipient * */
+void           e_m365_mail_message_begin_bcc_recipients(JsonBuilder *builder);
+void           e_m365_mail_message_end_bcc_recipients  (JsonBuilder *builder);
+EM365ItemBody *        e_m365_mail_message_get_body            (EM365MailMessage *mail);
+void           e_m365_mail_message_add_body            (JsonBuilder *builder,
+                                                        EM365ItemBodyContentTypeType content_type,
+                                                        const gchar *content);
+const gchar *  e_m365_mail_message_get_body_preview    (EM365MailMessage *mail);
+JsonArray *    e_m365_mail_message_get_categories      (EM365MailMessage *mail); /* const gchar * */
+void           e_m365_mail_message_begin_categories    (JsonBuilder *builder);
+void           e_m365_mail_message_end_categories      (JsonBuilder *builder);
+void           e_m365_mail_message_add_category        (JsonBuilder *builder,
+                                                        const gchar *category);
+JsonArray *    e_m365_mail_message_get_cc_recipients   (EM365MailMessage *mail); /* EM365Recipient * */
+void           e_m365_mail_message_begin_cc_recipients (JsonBuilder *builder);
+void           e_m365_mail_message_end_cc_recipients   (JsonBuilder *builder);
+const gchar *  e_m365_mail_message_get_change_key      (EM365MailMessage *mail);
+const gchar *  e_m365_mail_message_get_conversation_id (EM365MailMessage *mail);
+JsonObject *   e_m365_mail_message_get_conversation_index
+                                                       (EM365MailMessage *mail);
+time_t         e_m365_mail_message_get_created_date_time
+                                                       (EM365MailMessage *mail);
+EM365FollowupFlag *
+               e_m365_mail_message_get_flag            (EM365MailMessage *mail);
+void           e_m365_mail_message_begin_flag          (JsonBuilder *builder);
+void           e_m365_mail_message_end_flag            (JsonBuilder *builder);
+EM365Recipient *
+               e_m365_mail_message_get_from            (EM365MailMessage *mail);
+void           e_m365_mail_message_add_from            (JsonBuilder *builder,
+                                                        const gchar *name,
+                                                        const gchar *address);
+gboolean       e_m365_mail_message_get_has_attachments (EM365MailMessage *mail);
+const gchar *  e_m365_mail_message_get_id              (EM365MailMessage *mail);
+EM365ImportanceType
+               e_m365_mail_message_get_importance      (EM365MailMessage *mail);
+void           e_m365_mail_message_add_importance      (JsonBuilder *builder,
+                                                        EM365ImportanceType importance);
+EM365InferenceClassificationType
+               e_m365_mail_message_get_inference_classification
+                                                       (EM365MailMessage *mail);
+JsonArray *    e_m365_mail_message_get_internet_message_headers
+                                                       (EM365MailMessage *mail); /* 
EM365InternetMessageHeader * */
+void           e_m365_mail_message_begin_internet_message_headers
+                                                       (JsonBuilder *builder);
+void           e_m365_mail_message_end_internet_message_headers
+                                                       (JsonBuilder *builder);
+const gchar *  e_m365_mail_message_get_internet_message_id
+                                                       (EM365MailMessage *mail);
+void           e_m365_mail_message_add_internet_message_id
+                                                       (JsonBuilder *builder,
+                                                        const gchar *message_id);
+gboolean       e_m365_mail_message_get_is_delivery_receipt_requested
+                                                       (EM365MailMessage *mail);
+void           e_m365_mail_message_add_is_delivery_receipt_requested
+                                                       (JsonBuilder *builder,
+                                                        gboolean value);
+gboolean       e_m365_mail_message_get_is_draft        (EM365MailMessage *mail);
+gboolean       e_m365_mail_message_get_is_read         (EM365MailMessage *mail);
+void           e_m365_mail_message_add_is_read         (JsonBuilder *builder,
+                                                        gboolean value);
+gboolean       e_m365_mail_message_get_is_read_receipt_requested
+                                                       (EM365MailMessage *mail);
+void           e_m365_mail_message_add_is_read_receipt_requested
+                                                       (JsonBuilder *builder,
+                                                        gboolean value);
+time_t         e_m365_mail_message_get_last_modified_date_time
+                                                       (EM365MailMessage *mail);
+const gchar *  e_m365_mail_message_get_parent_folder_id(EM365MailMessage *mail);
+time_t         e_m365_mail_message_get_received_date_time
+                                                       (EM365MailMessage *mail);
+void           e_m365_mail_message_add_received_date_time
+                                                       (JsonBuilder *builder,
+                                                        time_t value);
+JsonArray *    e_m365_mail_message_get_reply_to        (EM365MailMessage *mail); /* EM365Recipient * */
+void           e_m365_mail_message_begin_reply_to      (JsonBuilder *builder);
+void           e_m365_mail_message_end_reply_to        (JsonBuilder *builder);
+EM365Recipient *e_m365_mail_message_get_sender         (EM365MailMessage *mail);
+void           e_m365_mail_message_add_sender          (JsonBuilder *builder,
+                                                        const gchar *name,
+                                                        const gchar *address);
+time_t         e_m365_mail_message_get_sent_date_time  (EM365MailMessage *mail);
+void           e_m365_mail_message_add_sent_date_time  (JsonBuilder *builder,
+                                                        time_t value);
+const gchar *  e_m365_mail_message_get_subject         (EM365MailMessage *mail);
+void           e_m365_mail_message_add_subject         (JsonBuilder *builder,
+                                                        const gchar *subject);
+JsonArray *    e_m365_mail_message_get_to_recipients   (EM365MailMessage *mail); /* EM365Recipient * */
+void           e_m365_mail_message_begin_to_recipients (JsonBuilder *builder);
+void           e_m365_mail_message_end_to_recipients   (JsonBuilder *builder);
+EM365ItemBody *        e_m365_mail_message_get_unique_body     (EM365MailMessage *mail);
+const gchar *  e_m365_mail_message_get_web_link        (EM365MailMessage *mail);
+
+EM365AttachmentDataType
+               e_m365_attachment_get_data_type         (EM365Attachment *attachment);
+void           e_m365_attachment_begin_attachment      (JsonBuilder *builder,
+                                                        EM365AttachmentDataType data_type);
+void           e_m365_attachment_end_attachment        (JsonBuilder *builder);
+const gchar *  e_m365_attachment_get_content_type      (EM365Attachment *attachment);
+void           e_m365_attachment_add_content_type      (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_attachment_get_id                (EM365Attachment *attachment);
+gboolean       e_m365_attachment_get_is_inline         (EM365Attachment *attachment);
+void           e_m365_attachment_add_is_inline         (JsonBuilder *builder,
+                                                        gboolean value);
+time_t         e_m365_attachment_get_last_modified_date_time
+                                                       (EM365Attachment *attachment);
+void           e_m365_attachment_add_last_modified_date_time
+                                                       (JsonBuilder *builder,
+                                                        time_t value);
+const gchar *  e_m365_attachment_get_name              (EM365Attachment *attachment);
+void           e_m365_attachment_add_name              (JsonBuilder *builder,
+                                                        const gchar *value);
+gint32         e_m365_attachment_get_size              (EM365Attachment *attachment);
+void           e_m365_attachment_add_size              (JsonBuilder *builder,
+                                                        gint32 value);
+const gchar *  e_m365_file_attachment_get_content_bytes(EM365Attachment *attachment); /* base64-encoded */
+void           e_m365_file_attachment_add_content_bytes(JsonBuilder *builder,
+                                                        const gchar *base64_value);
+const gchar *  e_m365_file_attachment_get_content_id   (EM365Attachment *attachment);
+void           e_m365_file_attachment_add_content_id   (JsonBuilder *builder,
+                                                        const gchar *value);
+
+const gchar *  e_m365_email_address_get_name           (EM365EmailAddress *email);
+const gchar *  e_m365_email_address_get_address        (EM365EmailAddress *email);
+void           e_m365_add_email_address                (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        const gchar *name,
+                                                        const gchar *address);
+const gchar *  e_m365_physical_address_get_city        (EM365PhysicalAddress *address);
+const gchar *  e_m365_physical_address_get_country_or_region
+                                                       (EM365PhysicalAddress *address);
+const gchar *  e_m365_physical_address_get_postal_code (EM365PhysicalAddress *address);
+const gchar *  e_m365_physical_address_get_state       (EM365PhysicalAddress *address);
+const gchar *  e_m365_physical_address_get_street      (EM365PhysicalAddress *address);
+void           e_m365_add_physical_address             (JsonBuilder *builder,
+                                                        const gchar *member_name,
+                                                        const gchar *city,
+                                                        const gchar *country_or_region,
+                                                        const gchar *postal_code,
+                                                        const gchar *state,
+                                                        const gchar *street);
+
+const gchar *  e_m365_contact_get_id                   (EM365Contact *contact);
+const gchar *  e_m365_contact_get_parent_folder_id     (EM365Contact *contact);
+const gchar *  e_m365_contact_get_change_key           (EM365Contact *contact);
+time_t         e_m365_contact_get_created_date_time    (EM365Contact *contact);
+time_t         e_m365_contact_get_last_modified_date_time
+                                                       (EM365Contact *contact);
+const gchar *  e_m365_contact_get_assistant_name       (EM365Contact *contact);
+void           e_m365_contact_add_assistant_name       (JsonBuilder *builder,
+                                                        const gchar *value);
+time_t         e_m365_contact_get_birthday             (EM365Contact *contact);
+void           e_m365_contact_add_birthday             (JsonBuilder *builder,
+                                                        time_t value);
+EM365PhysicalAddress *
+               e_m365_contact_get_business_address     (EM365Contact *contact);
+void           e_m365_contact_add_business_address     (JsonBuilder *builder,
+                                                        const gchar *city,
+                                                        const gchar *country_or_region,
+                                                        const gchar *postal_code,
+                                                        const gchar *state,
+                                                        const gchar *street);
+const gchar *  e_m365_contact_get_business_home_page   (EM365Contact *contact);
+void           e_m365_contact_add_business_home_page   (JsonBuilder *builder,
+                                                        const gchar *value);
+JsonArray *    e_m365_contact_get_business_phones      (EM365Contact *contact); /* const gchar * */
+void           e_m365_contact_begin_business_phones    (JsonBuilder *builder);
+void           e_m365_contact_end_business_phones      (JsonBuilder *builder);
+void           e_m365_contact_add_business_phone       (JsonBuilder *builder,
+                                                        const gchar *value);
+JsonArray *    e_m365_contact_get_categories           (EM365Contact *contact); /* const gchar * */
+void           e_m365_contact_begin_categories         (JsonBuilder *builder);
+void           e_m365_contact_end_categories           (JsonBuilder *builder);
+void           e_m365_contact_add_category             (JsonBuilder *builder,
+                                                        const gchar *category);
+JsonArray *    e_m365_contact_get_children             (EM365Contact *contact); /* const gchar * */
+void           e_m365_contact_begin_children           (JsonBuilder *builder);
+void           e_m365_contact_end_children             (JsonBuilder *builder);
+void           e_m365_contact_add_child                (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_company_name         (EM365Contact *contact);
+void           e_m365_contact_add_company_name         (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_department           (EM365Contact *contact);
+void           e_m365_contact_add_department           (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_display_name         (EM365Contact *contact);
+void           e_m365_contact_add_display_name         (JsonBuilder *builder,
+                                                        const gchar *value);
+JsonArray *    e_m365_contact_get_email_addresses      (EM365Contact *contact); /* EM365EmailAddress * */
+void           e_m365_contact_begin_email_addresses    (JsonBuilder *builder);
+void           e_m365_contact_end_email_addresses      (JsonBuilder *builder);
+const gchar *  e_m365_contact_get_file_as              (EM365Contact *contact);
+void           e_m365_contact_add_file_as              (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_generation           (EM365Contact *contact);
+void           e_m365_contact_add_generation           (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_given_name           (EM365Contact *contact);
+void           e_m365_contact_add_given_name           (JsonBuilder *builder,
+                                                        const gchar *value);
+EM365PhysicalAddress *
+               e_m365_contact_get_home_address         (EM365Contact *contact);
+void           e_m365_contact_add_home_address         (JsonBuilder *builder,
+                                                        const gchar *city,
+                                                        const gchar *country_or_region,
+                                                        const gchar *postal_code,
+                                                        const gchar *state,
+                                                        const gchar *street);
+JsonArray *    e_m365_contact_get_home_phones          (EM365Contact *contact); /* const gchar * */
+void           e_m365_contact_begin_home_phones        (JsonBuilder *builder);
+void           e_m365_contact_end_home_phones          (JsonBuilder *builder);
+void           e_m365_contact_add_home_phone           (JsonBuilder *builder,
+                                                        const gchar *value);
+JsonArray *    e_m365_contact_get_im_addresses         (EM365Contact *contact); /* const gchar * */
+void           e_m365_contact_begin_im_addresses       (JsonBuilder *builder);
+void           e_m365_contact_end_im_addresses         (JsonBuilder *builder);
+void           e_m365_contact_add_im_address           (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_initials             (EM365Contact *contact);
+void           e_m365_contact_add_initials             (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_job_title            (EM365Contact *contact);
+void           e_m365_contact_add_job_title            (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_manager              (EM365Contact *contact);
+void           e_m365_contact_add_manager              (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_middle_name          (EM365Contact *contact);
+void           e_m365_contact_add_middle_name          (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_mobile_phone         (EM365Contact *contact);
+void           e_m365_contact_add_mobile_phone         (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_nick_name            (EM365Contact *contact);
+void           e_m365_contact_add_nick_name            (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_office_location      (EM365Contact *contact);
+void           e_m365_contact_add_office_location      (JsonBuilder *builder,
+                                                        const gchar *value);
+EM365PhysicalAddress *
+               e_m365_contact_get_other_address        (EM365Contact *contact);
+void           e_m365_contact_add_other_address        (JsonBuilder *builder,
+                                                        const gchar *city,
+                                                        const gchar *country_or_region,
+                                                        const gchar *postal_code,
+                                                        const gchar *state,
+                                                        const gchar *street);
+const gchar *  e_m365_contact_get_personal_notes       (EM365Contact *contact);
+void           e_m365_contact_add_personal_notes       (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_profession           (EM365Contact *contact);
+void           e_m365_contact_add_profession           (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_spouse_name          (EM365Contact *contact);
+void           e_m365_contact_add_spouse_name          (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_surname              (EM365Contact *contact);
+void           e_m365_contact_add_surname              (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_title                (EM365Contact *contact);
+void           e_m365_contact_add_title                (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_yomi_company_name    (EM365Contact *contact);
+void           e_m365_contact_add_yomi_company_name    (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_yomi_given_name      (EM365Contact *contact);
+void           e_m365_contact_add_yomi_given_name      (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_contact_get_yomi_surname         (EM365Contact *contact);
+void           e_m365_contact_add_yomi_surname         (JsonBuilder *builder,
+                                                        const gchar *value);
+
+const gchar *  e_m365_calendar_group_get_id            (EM365CalendarGroup *group);
+const gchar *  e_m365_calendar_group_get_change_key    (EM365CalendarGroup *group);
+const gchar *  e_m365_calendar_group_get_class_id      (EM365CalendarGroup *group);
+const gchar *  e_m365_calendar_group_get_name          (EM365CalendarGroup *group);
+
+const gchar *  e_m365_calendar_get_id                  (EM365Calendar *calendar);
+const gchar *  e_m365_calendar_get_change_key          (EM365Calendar *calendar);
+gboolean       e_m365_calendar_get_can_edit            (EM365Calendar *calendar);
+gboolean       e_m365_calendar_get_can_share           (EM365Calendar *calendar);
+gboolean       e_m365_calendar_get_can_view_private_items
+                                                       (EM365Calendar *calendar);
+gboolean       e_m365_calendar_get_is_removable        (EM365Calendar *calendar);
+gboolean       e_m365_calendar_get_is_tallying_responses
+                                                       (EM365Calendar *calendar);
+EM365EmailAddress *
+               e_m365_calendar_get_owner               (EM365Calendar *calendar);
+const gchar *  e_m365_calendar_get_name                (EM365Calendar *calendar);
+void           e_m365_calendar_add_name                (JsonBuilder *builder,
+                                                        const gchar *name);
+guint32                e_m365_calendar_get_allowed_online_meeting_providers /* bit-or of 
EM365OnlineMeetingProviderType */
+                                                       (EM365Calendar *calendar);
+void           e_m365_calendar_add_allowed_online_meeting_providers
+                                                       (JsonBuilder *builder,
+                                                        guint providers); /* bit-or of 
EM365OnlineMeetingProviderType */
+EM365CalendarColorType
+               e_m365_calendar_get_color               (EM365Calendar *calendar);
+void           e_m365_calendar_add_color               (JsonBuilder *builder,
+                                                        EM365CalendarColorType color);
+EM365OnlineMeetingProviderType
+               e_m365_calendar_get_default_online_meeting_provider
+                                                       (EM365Calendar *calendar);
+void           e_m365_calendar_add_default_online_meeting_provider
+                                                       (JsonBuilder *builder,
+                                                        EM365OnlineMeetingProviderType provider);
+EM365ResponseType
+               e_m365_response_status_get_response     (EM365ResponseStatus *response_status);
+time_t         e_m365_response_status_get_time         (EM365ResponseStatus *response_status);
+
+EM365ResponseStatus *
+               e_m365_attendee_get_status              (EM365Attendee *attendee);
+EM365AttendeeType
+               e_m365_attendee_get_type                (EM365Attendee *attendee);
+EM365EmailAddress *
+               e_m365_attendee_get_email_address       (EM365Attendee *attendee);
+
+gdouble                e_m365_outlook_geo_coordinates_get_accuracy
+                                                       (EM365OutlookGeoCoordinates *coords);
+void           e_m365_outlook_geo_coordinates_add_accuracy
+                                                       (JsonBuilder *builder,
+                                                        gdouble value);
+gdouble                e_m365_outlook_geo_coordinates_get_altitude
+                                                       (EM365OutlookGeoCoordinates *coords);
+void           e_m365_outlook_geo_coordinates_add_altitude
+                                                       (JsonBuilder *builder,
+                                                        gdouble value);
+gdouble                e_m365_outlook_geo_coordinates_get_altitude_accuracy
+                                                       (EM365OutlookGeoCoordinates *coords);
+void           e_m365_outlook_geo_coordinates_add_altitude_accuracy
+                                                       (JsonBuilder *builder,
+                                                        gdouble value);
+gdouble                e_m365_outlook_geo_coordinates_get_latitude
+                                                       (EM365OutlookGeoCoordinates *coords);
+void           e_m365_outlook_geo_coordinates_add_latitude
+                                                       (JsonBuilder *builder,
+                                                        gdouble value);
+gdouble                e_m365_outlook_geo_coordinates_get_longitude
+                                                       (EM365OutlookGeoCoordinates *coords);
+void           e_m365_outlook_geo_coordinates_add_longitude
+                                                       (JsonBuilder *builder,
+                                                        gdouble value);
+EM365PhysicalAddress *
+               e_m365_location_get_address             (EM365Location *location);
+void           e_m365_location_add_address             (JsonBuilder *builder,
+                                                        const gchar *city,
+                                                        const gchar *country_or_region,
+                                                        const gchar *postal_code,
+                                                        const gchar *state,
+                                                        const gchar *street);
+EM365OutlookGeoCoordinates *
+               e_m365_location_get_coordinates         (EM365Location *location);
+void           e_m365_location_begin_coordinates       (JsonBuilder *builder);
+void           e_m365_location_end_coordinates         (JsonBuilder *builder);
+const gchar *  e_m365_location_get_display_name        (EM365Location *location);
+void           e_m365_location_add_display_name        (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_location_get_email_address       (EM365Location *location);
+void           e_m365_location_add_email_address       (JsonBuilder *builder,
+                                                        const gchar *value);
+const gchar *  e_m365_location_get_uri                 (EM365Location *location);
+void           e_m365_location_add_uri                 (JsonBuilder *builder,
+                                                        const gchar *value);
+EM365LocationType
+               e_m365_location_get_type                (EM365Location *location);
+
+const gchar *  e_m365_phone_get_number                 (EM365Phone *phone);
+EM365PhoneType e_m365_phone_get_type                   (EM365Phone *phone);
+
+const gchar *  e_m365_online_meeting_info_get_conference_id
+                                                       (EM365OnlineMeetingInfo *meeting_info);
+const gchar *  e_m365_online_meeting_info_get_join_url (EM365OnlineMeetingInfo *meeting_info);
+JsonArray *    e_m365_online_meeting_info_get_phones   (EM365OnlineMeetingInfo *meeting_info); /* EM365Phone 
* */
+const gchar *  e_m365_online_meeting_info_get_quick_dial
+                                                       (EM365OnlineMeetingInfo *meeting_info);
+JsonArray *    e_m365_online_meeting_info_get_toll_free_numbers
+                                                       (EM365OnlineMeetingInfo *meeting_info); /* gchar * */
+const gchar *  e_m365_online_meeting_info_get_toll_number
+                                                       (EM365OnlineMeetingInfo *meeting_info);
+
+EM365DayOfWeekType
+               e_m365_array_get_day_of_week_element    (JsonArray *array, /* const gchar * representing 
EM365DayOfWeekType */
+                                                        guint index);
+gint           e_m365_recurrence_pattern_get_day_of_month
+                                                       (EM365RecurrencePattern *pattern);
+void           e_m365_recurrence_pattern_add_day_of_month
+                                                       (JsonBuilder *builder,
+                                                        gint value);
+JsonArray *    e_m365_recurrence_pattern_get_days_of_week
+                                                       (EM365RecurrencePattern *pattern); /* const gchar * 
representing EM365DayOfWeekType, use e_m365_array_get_day_of_week_element() */
+void           e_m365_recurrence_pattern_begin_days_of_week
+                                                       (JsonBuilder *builder);
+void           e_m365_recurrence_pattern_end_days_of_week
+                                                       (JsonBuilder *builder);
+void           e_m365_recurrence_pattern_add_day_of_week
+                                                       (JsonBuilder *builder,
+                                                        EM365DayOfWeekType value);
+EM365DayOfWeekType
+               e_m365_recurrence_pattern_get_first_day_of_week
+                                                       (EM365RecurrencePattern *pattern);
+void           e_m365_recurrence_pattern_add_first_day_of_week
+                                                       (JsonBuilder *builder,
+                                                        EM365DayOfWeekType value);
+EM365WeekIndexType
+               e_m365_recurrence_pattern_get_index     (EM365RecurrencePattern *pattern);
+void           e_m365_recurrence_pattern_add_index     (JsonBuilder *builder,
+                                                        EM365WeekIndexType value);
+gint           e_m365_recurrence_pattern_get_interval  (EM365RecurrencePattern *pattern);
+void           e_m365_recurrence_pattern_add_interval  (JsonBuilder *builder,
+                                                        gint value);
+gint           e_m365_recurrence_pattern_get_month     (EM365RecurrencePattern *pattern);
+void           e_m365_recurrence_pattern_add_month     (JsonBuilder *builder,
+                                                        gint value);
+EM365RecurrencePatternType
+               e_m365_recurrence_pattern_get_type      (EM365RecurrencePattern *pattern);
+void           e_m365_recurrence_pattern_add_type      (JsonBuilder *builder,
+                                                        EM365RecurrencePatternType value);
+
+EM365Date      e_m365_recurrence_range_get_end_date    (EM365RecurrenceRange *range);
+void           e_m365_recurrence_range_add_end_date    (JsonBuilder *builder,
+                                                        EM365Date value);
+gint           e_m365_recurrence_range_get_number_of_occurrences
+                                                       (EM365RecurrenceRange *range);
+void           e_m365_recurrence_range_add_number_of_occurrences
+                                                       (JsonBuilder *builder,
+                                                        gint value);
+const gchar *  e_m365_recurrence_range_get_recurrence_time_zone
+                                                       (EM365RecurrenceRange *range);
+void           e_m365_recurrence_range_add_recurrence_time_zone
+                                                       (JsonBuilder *builder,
+                                                        const gchar *value);
+EM365Date      e_m365_recurrence_range_get_start_date  (EM365RecurrenceRange *range);
+void           e_m365_recurrence_range_add_start_date  (JsonBuilder *builder,
+                                                        EM365Date value);
+EM365RecurrenceRangeType
+               e_m365_recurrence_range_get_type        (EM365RecurrenceRange *range);
+void           e_m365_recurrence_range_add_type        (JsonBuilder *builder,
+                                                        EM365RecurrenceRangeType value);
+
+EM365RecurrencePattern *
+               e_m365_patterned_recurrence_get_pattern (EM365PatternedRecurrence *patterned_recurrence);
+void           e_m365_patterned_recurrence_begin_pattern
+                                                       (JsonBuilder *builder);
+void           e_m365_patterned_recurrence_end_pattern
+                                                       (JsonBuilder *builder);
+EM365RecurrenceRange *
+               e_m365_patterned_recurrence_get_range   (EM365PatternedRecurrence *patterned_recurrence);
+void           e_m365_patterned_recurrence_begin_range
+                                                       (JsonBuilder *builder);
+void           e_m365_patterned_recurrence_end_range
+                                                       (JsonBuilder *builder);
+
+const gchar *  e_m365_event_get_id                     (EM365Event *event);
+const gchar *  e_m365_event_get_change_key             (EM365Event *event);
+JsonArray *    e_m365_event_get_attendees              (EM365Event *event); /* EM365Attendee * */
+void           e_m365_event_begin_attendees            (JsonBuilder *builder);
+void           e_m365_event_end_attendees              (JsonBuilder *builder);
+void           e_m365_event_add_attendee               (JsonBuilder *builder,
+                                                        EM365AttendeeType type,
+                                                        EM365ResponseType response,
+                                                        time_t response_time,
+                                                        const gchar *name,
+                                                        const gchar *address);
+void           e_m365_event_add_null_attendees         (JsonBuilder *builder);
+EM365ItemBody *        e_m365_event_get_body                   (EM365Event *event);
+void           e_m365_event_add_body                   (JsonBuilder *builder,
+                                                        EM365ItemBodyContentTypeType content_type,
+                                                        const gchar *content);
+const gchar *  e_m365_event_get_body_preview           (EM365Event *event);
+JsonArray *    e_m365_event_get_categories             (EM365Event *event); /* const gchar * */
+void           e_m365_event_begin_categories           (JsonBuilder *builder);
+void           e_m365_event_end_categories             (JsonBuilder *builder);
+void           e_m365_event_add_category               (JsonBuilder *builder,
+                                                        const gchar *category);
+time_t         e_m365_event_get_created_date_time      (EM365Event *event);
+EM365DateTimeWithZone *
+               e_m365_event_get_end                    (EM365Event *event);
+void           e_m365_event_add_end                    (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+gboolean       e_m365_event_get_has_attachments        (EM365Event *event);
+const gchar *  e_m365_event_get_ical_uid               (EM365Event *event);
+EM365ImportanceType
+               e_m365_event_get_importance             (EM365Event *event);
+void           e_m365_event_add_importance             (JsonBuilder *builder,
+                                                        EM365ImportanceType value);
+gboolean       e_m365_event_get_is_all_day             (EM365Event *event);
+void           e_m365_event_add_is_all_day             (JsonBuilder *builder,
+                                                        gboolean value);
+gboolean       e_m365_event_get_is_cancelled           (EM365Event *event);
+gboolean       e_m365_event_get_is_online_meeting      (EM365Event *event);
+void           e_m365_event_add_is_online_meeting      (JsonBuilder *builder,
+                                                        gboolean value);
+gboolean       e_m365_event_get_is_organizer           (EM365Event *event);
+gboolean       e_m365_event_get_is_reminder_on         (EM365Event *event);
+void           e_m365_event_add_is_reminder_on         (JsonBuilder *builder,
+                                                        gboolean value);
+time_t         e_m365_event_get_last_modified_date_time(EM365Event *event);
+EM365Location *        e_m365_event_get_location               (EM365Event *event);
+void           e_m365_event_begin_location             (JsonBuilder *builder);
+void           e_m365_event_end_location               (JsonBuilder *builder);
+void           e_m365_event_add_null_location          (JsonBuilder *builder);
+JsonArray *    e_m365_event_get_locations              (EM365Event *event); /* EM365Location * */
+void           e_m365_event_begin_locations            (JsonBuilder *builder);
+void           e_m365_event_end_locations              (JsonBuilder *builder);
+void           e_m365_event_begin_locations_location   (JsonBuilder *builder);
+void           e_m365_event_end_locations_location     (JsonBuilder *builder);
+EM365OnlineMeetingInfo *
+               e_m365_event_get_online_meeting_info    (EM365Event *event);
+EM365OnlineMeetingProviderType
+               e_m365_event_get_online_meeting_provider(EM365Event *event);
+void           e_m365_event_add_online_meeting_provider(JsonBuilder *builder,
+                                                        EM365OnlineMeetingProviderType value);
+const gchar *  e_m365_event_get_online_meeting_url     (EM365Event *event);
+EM365Recipient *e_m365_event_get_organizer             (EM365Event *event);
+void           e_m365_event_add_organizer              (JsonBuilder *builder,
+                                                        const gchar *name,
+                                                        const gchar *address);
+void           e_m365_event_add_null_organizer         (JsonBuilder *builder);
+const gchar *  e_m365_event_get_original_end_timezone  (EM365Event *event);
+time_t         e_m365_event_get_original_start         (EM365Event *event);
+const gchar *  e_m365_event_get_original_start_timezone(EM365Event *event);
+EM365PatternedRecurrence *
+               e_m365_event_get_recurrence             (EM365Event *event);
+void           e_m365_event_begin_recurrence           (JsonBuilder *builder);
+void           e_m365_event_end_recurrence             (JsonBuilder *builder);
+void           e_m365_event_add_null_recurrence        (JsonBuilder *builder);
+gint           e_m365_event_get_reminder_minutes_before_start
+                                                       (EM365Event *event);
+void           e_m365_event_add_reminder_minutes_before_start
+                                                       (JsonBuilder *builder,
+                                                        gint value);
+gboolean       e_m365_event_get_response_requested     (EM365Event *event);
+void           e_m365_event_add_response_requested     (JsonBuilder *builder,
+                                                        gboolean value);
+EM365ResponseStatus *
+               e_m365_event_get_response_status        (EM365Event *event);
+EM365SensitivityType
+               e_m365_event_get_sensitivity            (EM365Event *event);
+void           e_m365_event_add_sensitivity            (JsonBuilder *builder,
+                                                        EM365SensitivityType value);
+const gchar *  e_m365_event_get_series_master_id       (EM365Event *event);
+EM365FreeBusyStatusType
+               e_m365_event_get_show_as                (EM365Event *event);
+void           e_m365_event_add_show_as                (JsonBuilder *builder,
+                                                        EM365FreeBusyStatusType value);
+EM365DateTimeWithZone *
+               e_m365_event_get_start                  (EM365Event *event);
+void           e_m365_event_add_start                  (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+const gchar *  e_m365_event_get_subject                (EM365Event *event);
+void           e_m365_event_add_subject                (JsonBuilder *builder,
+                                                        const gchar *value);
+EM365EventTypeType
+               e_m365_event_get_type                   (EM365Event *event);
+const gchar *  e_m365_event_get_web_link               (EM365Event *event);
+
+const gchar *  e_m365_free_busy_error_get_message      (EM365FreeBusyError *fberror);
+const gchar *  e_m365_free_busy_error_get_response_code(EM365FreeBusyError *fberror);
+
+EM365DateTimeWithZone *
+               e_m365_schedule_item_get_end            (EM365ScheduleItem *schitem);
+gboolean       e_m365_schedule_item_get_is_private     (EM365ScheduleItem *schitem);
+const gchar *  e_m365_schedule_item_get_location       (EM365ScheduleItem *schitem);
+EM365DateTimeWithZone *
+               e_m365_schedule_item_get_start          (EM365ScheduleItem *schitem);
+EM365FreeBusyStatusType
+               e_m365_schedule_item_get_status         (EM365ScheduleItem *schitem);
+const gchar *  e_m365_schedule_item_get_subject        (EM365ScheduleItem *schitem);
+
+JsonArray *    e_m365_working_hours_get_days_of_week   (EM365WorkingHours *wrkhrs); /* Use 
e_m365_array_get_day_of_week_element() to get the items */
+EM365TimeOfDay e_m365_working_hours_get_start_time     (EM365WorkingHours *wrkhrs);
+EM365TimeOfDay e_m365_working_hours_get_end_time       (EM365WorkingHours *wrkhrs);
+const gchar *  e_m365_working_hours_get_time_zone_name (EM365WorkingHours *wrkhrs);
+
+const gchar *  e_m365_schedule_information_get_availability_view
+                                                       (EM365ScheduleInformation *schinfo);
+EM365FreeBusyError *
+               e_m365_schedule_information_get_free_busy_error
+                                                       (EM365ScheduleInformation *schinfo);
+const gchar *  e_m365_schedule_information_get_schedule_id
+                                                       (EM365ScheduleInformation *schinfo);
+JsonArray *    e_m365_schedule_information_get_schedule_items /* EM365ScheduleItem * */
+                                                       (EM365ScheduleInformation *schinfo);
+EM365WorkingHours *
+               e_m365_schedule_information_get_working_hours
+                                                       (EM365ScheduleInformation *schinfo);
+
+const gchar *  e_m365_task_folder_get_id               (EM365TaskFolder *folder);
+const gchar *  e_m365_task_folder_get_change_key       (EM365TaskFolder *folder);
+const gchar *  e_m365_task_folder_get_parent_group_key (EM365TaskFolder *folder);
+const gchar *  e_m365_task_folder_get_name             (EM365TaskFolder *folder);
+gboolean       e_m365_task_folder_get_is_default_folder(EM365TaskFolder *folder);
+
+const gchar *  e_m365_task_group_get_id                (EM365TaskGroup *group);
+const gchar *  e_m365_task_group_get_change_key        (EM365TaskGroup *group);
+const gchar *  e_m365_task_group_get_group_key         (EM365TaskGroup *group);
+const gchar *  e_m365_task_group_get_name              (EM365TaskGroup *group);
+gboolean       e_m365_task_group_get_is_default_group  (EM365TaskGroup *group);
+
+const gchar *  e_m365_task_get_id                      (EM365Task *task);
+const gchar *  e_m365_task_get_change_key              (EM365Task *task);
+const gchar *  e_m365_task_get_parent_folder_id        (EM365Task *task);
+const gchar *  e_m365_task_get_assigned_to             (EM365Task *task);
+EM365ItemBody *        e_m365_task_get_body                    (EM365Task *task);
+void           e_m365_task_add_body                    (JsonBuilder *builder,
+                                                        EM365ItemBodyContentTypeType content_type,
+                                                        const gchar *content);
+JsonArray *    e_m365_task_get_categories              (EM365Task *task); /* const gchar * */
+void           e_m365_task_begin_categories            (JsonBuilder *builder);
+void           e_m365_task_end_categories              (JsonBuilder *builder);
+void           e_m365_task_add_category                (JsonBuilder *builder,
+                                                        const gchar *category);
+EM365DateTimeWithZone *
+               e_m365_task_get_completed_date_time     (EM365Task *task);
+void           e_m365_task_add_completed_date_time     (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+time_t         e_m365_task_get_created_date_time       (EM365Task *task);
+EM365DateTimeWithZone *
+               e_m365_task_get_due_date_time           (EM365Task *task);
+void           e_m365_task_add_due_date_time           (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+gboolean       e_m365_task_get_has_attachments         (EM365Task *task);
+EM365ImportanceType
+               e_m365_task_get_importance              (EM365Task *task);
+void           e_m365_task_add_importance              (JsonBuilder *builder,
+                                                        EM365ImportanceType value);
+gboolean       e_m365_task_get_is_reminder_on          (EM365Task *task);
+void           e_m365_task_add_is_reminder_on          (JsonBuilder *builder,
+                                                        gboolean value);
+time_t         e_m365_task_get_last_modified_date_time (EM365Task *task);
+const gchar *  e_m365_task_get_owner                   (EM365Task *task);
+void           e_m365_task_add_owner                   (JsonBuilder *builder,
+                                                        const gchar *value);
+EM365PatternedRecurrence *
+               e_m365_task_get_recurrence              (EM365Task *task);
+void           e_m365_task_begin_recurrence            (JsonBuilder *builder);
+void           e_m365_task_end_recurrence              (JsonBuilder *builder);
+void           e_m365_task_add_null_recurrence         (JsonBuilder *builder);
+EM365DateTimeWithZone *
+               e_m365_task_get_reminder_date_time      (EM365Task *task);
+void           e_m365_task_add_reminder_date_time      (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+EM365SensitivityType
+               e_m365_task_get_sensitivity             (EM365Task *task);
+void           e_m365_task_add_sensitivity             (JsonBuilder *builder,
+                                                        EM365SensitivityType value);
+EM365DateTimeWithZone *
+               e_m365_task_get_start_date_time         (EM365Task *task);
+void           e_m365_task_add_start_date_time         (JsonBuilder *builder,
+                                                        time_t date_time,
+                                                        const gchar *zone);
+EM365StatusType        e_m365_task_get_status                  (EM365Task *task);
+void           e_m365_task_add_status                  (JsonBuilder *builder,
+                                                        EM365StatusType value);
+const gchar *  e_m365_task_get_subject                 (EM365Task *task);
+void           e_m365_task_add_subject                 (JsonBuilder *builder,
+                                                        const gchar *value);
+
+G_END_DECLS
+
+#endif /* E_M365_JSON_UTILS_H */
diff --git a/src/Microsoft365/common/e-m365-tz-utils.c b/src/Microsoft365/common/e-m365-tz-utils.c
new file mode 100644
index 00000000..bc0cf6bd
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-tz-utils.c
@@ -0,0 +1,184 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+
+#include "e-m365-tz-utils.h"
+
+/*
+ * A bunch of global variables used to map the ICalTimezone to MSDN[0] format.
+ * Also, some auxiliar functions to translate from one tz type to another.
+ *
+ * [0]: http://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx
+ */
+static GRecMutex tz_mutex;
+static GHashTable *ical_to_msdn = NULL;
+static GHashTable *msdn_to_ical = NULL;
+static guint tables_counter = 0;
+
+void
+e_m365_tz_utils_ref_windows_zones (void)
+{
+       const gchar *xpath_eval_exp;
+       gchar *filename = NULL;
+       xmlDocPtr doc;
+       xmlXPathContextPtr xpath_ctxt;
+       xmlXPathObjectPtr xpath_obj;
+       xmlNodeSetPtr nodes;
+       gint i, len;
+
+       g_rec_mutex_lock (&tz_mutex);
+       if (ical_to_msdn != NULL && msdn_to_ical != NULL) {
+               g_hash_table_ref (ical_to_msdn);
+               g_hash_table_ref (msdn_to_ical);
+               tables_counter++;
+
+               g_rec_mutex_unlock (&tz_mutex);
+               return;
+       }
+
+       filename = g_build_filename (M365_DATADIR, "windowsZones.xml", NULL);
+
+       doc = xmlReadFile (filename, NULL, 0);
+
+       if (doc == NULL) {
+               g_warning (G_STRLOC "Could not map %s file.", filename);
+               g_free (filename);
+
+               g_rec_mutex_unlock (&tz_mutex);
+               return;
+       }
+
+       xpath_eval_exp = "/supplementalData/windowsZones/mapTimezones/mapZone";
+
+       xpath_ctxt = xmlXPathNewContext (doc);
+       xpath_obj = xmlXPathEvalExpression (BAD_CAST xpath_eval_exp, xpath_ctxt);
+
+       if (xpath_obj == NULL) {
+               g_warning (G_STRLOC "Unable to evaluate xpath expression \"%s\".", xpath_eval_exp);
+               xmlXPathFreeContext (xpath_ctxt);
+               xmlFreeDoc (doc);
+               g_free (filename);
+
+               g_rec_mutex_unlock (&tz_mutex);
+               return;
+       }
+
+       nodes = xpath_obj->nodesetval;
+       len = nodes->nodeNr;
+
+       msdn_to_ical = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       ical_to_msdn = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+       tables_counter++;
+
+       for (i = 0; i < len; i++) {
+               xmlChar *msdn = xmlGetProp (nodes->nodeTab[i], BAD_CAST "other");
+               xmlChar *ical = xmlGetProp (nodes->nodeTab[i], BAD_CAST "type");
+               gchar **tokens;
+               gint tokens_len;
+
+               tokens = g_strsplit ((gchar *) ical, " ", 0);
+               tokens_len = g_strv_length (tokens);
+               if (tokens_len == 1) {
+                       if (!g_hash_table_lookup (msdn_to_ical, msdn))
+                               g_hash_table_insert (msdn_to_ical, g_strdup ((gchar *) msdn), g_strdup 
((gchar *) ical));
+
+                       if (!g_hash_table_lookup (ical_to_msdn, ical))
+                               g_hash_table_insert (ical_to_msdn, g_strdup ((gchar *) ical), g_strdup 
((gchar *) msdn));
+               } else {
+                       gint j;
+                       for (j = 0; j < tokens_len; j++) {
+                               if (!g_hash_table_lookup (msdn_to_ical, msdn))
+                                       g_hash_table_insert (msdn_to_ical, g_strdup ((gchar *) msdn), 
g_strdup (tokens[j]));
+
+                               if (!g_hash_table_lookup (ical_to_msdn, tokens[j]))
+                                       g_hash_table_insert (ical_to_msdn, g_strdup (tokens[j]), g_strdup 
((gchar *) msdn));
+                       }
+               }
+
+               g_strfreev (tokens);
+               xmlFree (ical);
+               xmlFree (msdn);
+       }
+
+       xmlXPathFreeObject (xpath_obj);
+       xmlXPathFreeContext (xpath_ctxt);
+       xmlFreeDoc (doc);
+       g_free (filename);
+
+       g_rec_mutex_unlock (&tz_mutex);
+}
+
+void
+e_m365_tz_utils_unref_windows_zones (void)
+{
+       g_rec_mutex_lock (&tz_mutex);
+       if (ical_to_msdn != NULL)
+               g_hash_table_unref (ical_to_msdn);
+
+       if (msdn_to_ical != NULL)
+               g_hash_table_unref (msdn_to_ical);
+
+       if (tables_counter > 0) {
+               tables_counter--;
+
+               if (tables_counter == 0) {
+                       ical_to_msdn = NULL;
+                       msdn_to_ical = NULL;
+               }
+       }
+
+       g_rec_mutex_unlock (&tz_mutex);
+}
+
+const gchar *
+e_m365_tz_utils_get_msdn_equivalent (const gchar *ical_tz_location)
+{
+       const gchar *msdn_tz_location = NULL;
+
+       if (!ical_tz_location || !*ical_tz_location)
+               return NULL;
+
+       g_rec_mutex_lock (&tz_mutex);
+       if (ical_to_msdn == NULL) {
+               g_rec_mutex_unlock (&tz_mutex);
+
+               g_warn_if_reached ();
+               return NULL;
+       }
+
+       msdn_tz_location = g_hash_table_lookup (ical_to_msdn, ical_tz_location);
+       g_rec_mutex_unlock (&tz_mutex);
+
+       return msdn_tz_location;
+}
+
+const gchar *
+e_m365_tz_utils_get_ical_equivalent (const gchar *msdn_tz_location)
+{
+       const gchar *ical_tz_location = NULL;
+
+       if (!msdn_tz_location || !*msdn_tz_location)
+               return NULL;
+
+       g_rec_mutex_lock (&tz_mutex);
+       if (msdn_to_ical == NULL) {
+               g_rec_mutex_unlock (&tz_mutex);
+
+               g_warn_if_reached ();
+               return NULL;
+       }
+
+       ical_tz_location = g_hash_table_lookup (msdn_to_ical, msdn_tz_location);
+       g_rec_mutex_unlock (&tz_mutex);
+
+       return ical_tz_location;
+}
diff --git a/src/Microsoft365/common/e-m365-tz-utils.h b/src/Microsoft365/common/e-m365-tz-utils.h
new file mode 100644
index 00000000..0aa6002c
--- /dev/null
+++ b/src/Microsoft365/common/e-m365-tz-utils.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_M365_TZ_UTILS_H
+#define E_M365_TZ_UTILS_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+void           e_m365_tz_utils_ref_windows_zones       (void);
+void           e_m365_tz_utils_unref_windows_zones     (void);
+const gchar *  e_m365_tz_utils_get_msdn_equivalent     (const gchar *ical_tz_location);
+const gchar *  e_m365_tz_utils_get_ical_equivalent     (const gchar *msdn_tz_location);
+
+G_END_DECLS
+
+#endif /* E_M365_TZ_UTILS_H */
diff --git a/src/Microsoft365/common/e-oauth2-service-microsoft365.c 
b/src/Microsoft365/common/e-oauth2-service-microsoft365.c
new file mode 100644
index 00000000..18c12f17
--- /dev/null
+++ b/src/Microsoft365/common/e-oauth2-service-microsoft365.c
@@ -0,0 +1,374 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+#include <libedataserver/libedataserver.h>
+
+#include "camel-m365-settings.h"
+
+#include "e-oauth2-service-microsoft365.h"
+
+/* https://portal.azure.com/
+   https://docs.microsoft.com/en-us/graph/auth/
+
+   https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-developers-guide
+   https://tsmatz.wordpress.com/2016/10/07/application-permission-with-v2-endpoint-and-microsoft-graph/
+*/
+
+#define MICROSOFT365_SCOPE     "Calendars.ReadWrite " \
+                               "Calendars.ReadWrite.Shared " \
+                               "Contacts.ReadWrite " \
+                               "Contacts.ReadWrite.Shared " \
+                               "Mail.ReadWrite " \
+                               "Mail.ReadWrite.Shared " \
+                               "Mail.Send " \
+                               "Mail.Send.Shared " \
+                               "MailboxSettings.Read " \
+                               /*"Notes.Create "*/ \
+                               /*"Notes.ReadWrite.All "*/ \
+                               "offline_access " \
+                               "Tasks.ReadWrite " \
+                               "Tasks.ReadWrite.Shared "
+
+struct _EOAuth2ServiceMicrosoft365Private
+{
+       GMutex string_cache_lock;
+       GHashTable *string_cache;
+};
+
+/* Forward Declarations */
+static void e_oauth2_service_microsoft365_oauth2_service_init (EOAuth2ServiceInterface *iface);
+
+G_DEFINE_DYNAMIC_TYPE_EXTENDED (EOAuth2ServiceMicrosoft365, e_oauth2_service_microsoft365, 
E_TYPE_OAUTH2_SERVICE_BASE, 0,
+       G_IMPLEMENT_INTERFACE_DYNAMIC (E_TYPE_OAUTH2_SERVICE, 
e_oauth2_service_microsoft365_oauth2_service_init)
+       G_ADD_PRIVATE_DYNAMIC (EOAuth2ServiceMicrosoft365))
+
+static const gchar *
+eos_microsoft365_cache_string (EOAuth2ServiceMicrosoft365 *oauth2_microsoft365,
+                              gchar *str) /* takes ownership of the 'str' */
+{
+       const gchar *cached_str;
+
+       g_return_val_if_fail (E_IS_OAUTH2_SERVICE_MICROSOFT365 (oauth2_microsoft365), NULL);
+
+       if (!str)
+               return NULL;
+
+       if (!*str)
+               return "";
+
+       g_mutex_lock (&oauth2_microsoft365->priv->string_cache_lock);
+
+       cached_str = g_hash_table_lookup (oauth2_microsoft365->priv->string_cache, str);
+       if (cached_str) {
+               g_free (str);
+       } else {
+               g_hash_table_insert (oauth2_microsoft365->priv->string_cache, str, str);
+               cached_str = str;
+       }
+
+       g_mutex_unlock (&oauth2_microsoft365->priv->string_cache_lock);
+
+       return cached_str;
+}
+
+static CamelM365Settings *
+eos_microsoft365_get_camel_settings (ESource *source)
+{
+       ESourceCamel *extension;
+
+       if (!source)
+               return NULL;
+
+       g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+
+       extension = e_source_get_extension (source, e_source_camel_get_extension_name ("microsoft365"));
+
+       return CAMEL_M365_SETTINGS (e_source_camel_get_settings (extension));
+}
+
+static gboolean
+eos_microsoft365_guess_can_process (EOAuth2Service *service,
+                                   const gchar *protocol,
+                                   const gchar *hostname)
+{
+       return e_oauth2_services_is_supported () &&
+               protocol && g_ascii_strcasecmp (protocol, "microsoft365") == 0;
+}
+
+static const gchar *
+eos_microsoft365_get_name (EOAuth2Service *service)
+{
+       return "Microsoft365";
+}
+
+static const gchar *
+eos_microsoft365_get_display_name (EOAuth2Service *service)
+{
+       /* Translators: This is a user-visible string, display name of an OAuth2 service. */
+       return C_("OAuth2Service", "Microsoft365");
+}
+
+static const gchar *
+eos_microsoft365_get_client_id (EOAuth2Service *service,
+                               ESource *source)
+{
+       EOAuth2ServiceMicrosoft365 *oauth2_microsoft365 = E_OAUTH2_SERVICE_MICROSOFT365 (service);
+       CamelM365Settings *m365_settings;
+
+       m365_settings = eos_microsoft365_get_camel_settings (source);
+       if (m365_settings && camel_m365_settings_get_override_oauth2 (m365_settings)) {
+               gchar *client_id = camel_m365_settings_dup_oauth2_client_id (m365_settings);
+
+               if (client_id && !*client_id) {
+                       g_free (client_id);
+                       client_id = NULL;
+               }
+
+               if (client_id)
+                       return eos_microsoft365_cache_string (oauth2_microsoft365, client_id);
+       }
+
+       return MICROSOFT365_CLIENT_ID;
+}
+
+static const gchar *
+eos_microsoft365_get_client_secret (EOAuth2Service *service,
+                                   ESource *source)
+{
+       return NULL;
+}
+
+static const gchar *
+eos_microsoft365_get_authentication_uri (EOAuth2Service *service,
+                                        ESource *source)
+{
+       EOAuth2ServiceMicrosoft365 *oauth2_microsoft365 = E_OAUTH2_SERVICE_MICROSOFT365 (service);
+       CamelM365Settings *m365_settings;
+
+       m365_settings = eos_microsoft365_get_camel_settings (source);
+       if (m365_settings && camel_m365_settings_get_override_oauth2 (m365_settings)) {
+               gchar *tenant;
+               const gchar *res;
+
+               tenant = camel_m365_settings_dup_oauth2_tenant (m365_settings);
+               if (tenant && !*tenant) {
+                       g_free (tenant);
+                       tenant = NULL;
+               }
+
+               res = eos_microsoft365_cache_string (oauth2_microsoft365,
+                       g_strdup_printf ("https://login.microsoftonline.com/%s/oauth2/v2.0/authorize";,
+                               tenant ? tenant : MICROSOFT365_TENANT));
+
+               g_free (tenant);
+
+               return res;
+       }
+
+       return "https://login.microsoftonline.com/"; MICROSOFT365_TENANT "/oauth2/v2.0/authorize";
+}
+
+static const gchar *
+eos_microsoft365_get_refresh_uri (EOAuth2Service *service,
+                                 ESource *source)
+{
+       EOAuth2ServiceMicrosoft365 *oauth2_microsoft365 = E_OAUTH2_SERVICE_MICROSOFT365 (service);
+       CamelM365Settings *m365_settings;
+
+       m365_settings = eos_microsoft365_get_camel_settings (source);
+       if (m365_settings && camel_m365_settings_get_override_oauth2 (m365_settings)) {
+               gchar *tenant;
+               const gchar *res;
+
+               tenant = camel_m365_settings_dup_oauth2_tenant (m365_settings);
+               if (tenant && !*tenant) {
+                       g_free (tenant);
+                       tenant = NULL;
+               }
+
+               res = eos_microsoft365_cache_string (oauth2_microsoft365,
+                       g_strdup_printf ("https://login.microsoftonline.com/%s/oauth2/v2.0/token";,
+                               tenant ? tenant : MICROSOFT365_TENANT));
+
+               g_free (tenant);
+
+               return res;
+       }
+
+       return "https://login.microsoftonline.com/"; MICROSOFT365_TENANT "/oauth2/v2.0/token";
+}
+
+static const gchar *
+eos_microsoft365_get_redirect_uri (EOAuth2Service *service,
+                                  ESource *source)
+{
+       EOAuth2ServiceMicrosoft365 *oauth2_microsoft365 = E_OAUTH2_SERVICE_MICROSOFT365 (service);
+       CamelM365Settings *m365_settings;
+       const gchar *res;
+
+       m365_settings = eos_microsoft365_get_camel_settings (source);
+       if (m365_settings && camel_m365_settings_get_override_oauth2 (m365_settings)) {
+               gchar *redirect_uri;
+
+               redirect_uri = camel_m365_settings_dup_oauth2_redirect_uri (m365_settings);
+
+               if (redirect_uri && !*redirect_uri) {
+                       g_free (redirect_uri);
+                       redirect_uri = NULL;
+               }
+
+               if (redirect_uri)
+                       return eos_microsoft365_cache_string (oauth2_microsoft365, redirect_uri);
+       }
+
+       res = MICROSOFT365_REDIRECT_URI;
+       if (res && *res)
+               return res;
+
+       return "https://login.microsoftonline.com/common/oauth2/nativeclient";;
+}
+
+static void
+eos_microsoft365_prepare_authentication_uri_query (EOAuth2Service *service,
+                                                  ESource *source,
+                                                  GHashTable *uri_query)
+{
+       g_return_if_fail (uri_query != NULL);
+
+       e_oauth2_service_util_set_to_form (uri_query, "response_type", "code");
+       e_oauth2_service_util_set_to_form (uri_query, "scope", MICROSOFT365_SCOPE);
+       e_oauth2_service_util_set_to_form (uri_query, "response_mode", "query");
+}
+
+static gboolean
+eos_microsoft365_extract_authorization_code (EOAuth2Service *service,
+                                            ESource *source,
+                                            const gchar *page_title,
+                                            const gchar *page_uri,
+                                            const gchar *page_content,
+                                            gchar **out_authorization_code)
+{
+       SoupURI *suri;
+       gboolean known = FALSE;
+
+       g_return_val_if_fail (out_authorization_code != NULL, FALSE);
+
+       *out_authorization_code = NULL;
+
+       if (!page_uri || !*page_uri)
+               return FALSE;
+
+       suri = soup_uri_new (page_uri);
+       if (!suri)
+               return FALSE;
+
+       if (suri->query) {
+               GHashTable *uri_query = soup_form_decode (suri->query);
+
+               if (uri_query) {
+                       const gchar *code;
+
+                       code = g_hash_table_lookup (uri_query, "code");
+
+                       if (code && *code) {
+                               *out_authorization_code = g_strdup (code);
+                               known = TRUE;
+                       } else if (g_hash_table_lookup (uri_query, "error")) {
+                               known = TRUE;
+                               if (g_strcmp0 (g_hash_table_lookup (uri_query, "error"), "access_denied") != 
0) {
+                                       const gchar *description;
+
+                                       description = g_hash_table_lookup (uri_query, "error_description");
+                                       if (description) {
+                                               g_warning ("%s: error:%s description:%s", G_STRFUNC,
+                                                       (const gchar *) g_hash_table_lookup (uri_query, 
"error"),
+                                                       description);
+                                       }
+                               }
+                       }
+
+                       g_hash_table_unref (uri_query);
+               }
+       }
+
+       soup_uri_free (suri);
+
+       return known;
+}
+
+static void
+eos_microsoft365_prepare_refresh_token_form (EOAuth2Service *service,
+                                            ESource *source,
+                                            const gchar *refresh_token,
+                                            GHashTable *form)
+{
+       g_return_if_fail (form != NULL);
+
+       e_oauth2_service_util_set_to_form (form, "scope", MICROSOFT365_SCOPE);
+       e_oauth2_service_util_set_to_form (form, "redirect_uri", e_oauth2_service_get_redirect_uri (service, 
source));
+}
+
+static void
+eos_microsoft365_finalize (GObject *object)
+{
+       EOAuth2ServiceMicrosoft365 *oauth2_microsoft365 = E_OAUTH2_SERVICE_MICROSOFT365 (object);
+
+       g_mutex_lock (&oauth2_microsoft365->priv->string_cache_lock);
+       g_hash_table_destroy (oauth2_microsoft365->priv->string_cache);
+       g_mutex_unlock (&oauth2_microsoft365->priv->string_cache_lock);
+       g_mutex_clear (&oauth2_microsoft365->priv->string_cache_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_oauth2_service_microsoft365_parent_class)->finalize (object);
+}
+
+static void
+e_oauth2_service_microsoft365_oauth2_service_init (EOAuth2ServiceInterface *iface)
+{
+       iface->guess_can_process = eos_microsoft365_guess_can_process;
+       iface->get_name = eos_microsoft365_get_name;
+       iface->get_display_name = eos_microsoft365_get_display_name;
+       iface->get_client_id = eos_microsoft365_get_client_id;
+       iface->get_client_secret = eos_microsoft365_get_client_secret;
+       iface->get_authentication_uri = eos_microsoft365_get_authentication_uri;
+       iface->get_refresh_uri = eos_microsoft365_get_refresh_uri;
+       iface->get_redirect_uri = eos_microsoft365_get_redirect_uri;
+       iface->prepare_authentication_uri_query = eos_microsoft365_prepare_authentication_uri_query;
+       iface->extract_authorization_code = eos_microsoft365_extract_authorization_code;
+       iface->prepare_refresh_token_form = eos_microsoft365_prepare_refresh_token_form;
+}
+
+static void
+e_oauth2_service_microsoft365_class_init (EOAuth2ServiceMicrosoft365Class *klass)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = eos_microsoft365_finalize;
+}
+
+static void
+e_oauth2_service_microsoft365_class_finalize (EOAuth2ServiceMicrosoft365Class *klass)
+{
+}
+
+static void
+e_oauth2_service_microsoft365_init (EOAuth2ServiceMicrosoft365 *oauth2_microsoft365)
+{
+       oauth2_microsoft365->priv = e_oauth2_service_microsoft365_get_instance_private (oauth2_microsoft365);
+
+       g_mutex_init (&oauth2_microsoft365->priv->string_cache_lock);
+       oauth2_microsoft365->priv->string_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
NULL);
+}
+
+void
+e_oauth2_service_microsoft365_type_register (GTypeModule *type_module)
+{
+       e_oauth2_service_microsoft365_register_type (type_module);
+}
diff --git a/src/Microsoft365/common/e-oauth2-service-microsoft365.h 
b/src/Microsoft365/common/e-oauth2-service-microsoft365.h
new file mode 100644
index 00000000..0e76d93f
--- /dev/null
+++ b/src/Microsoft365/common/e-oauth2-service-microsoft365.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_OAUTH2_SERVICE_MICROSOFT365_H
+#define E_OAUTH2_SERVICE_MICROSOFT365_H
+
+#include <gmodule.h>
+#include <libedataserver/libedataserver.h>
+
+/* Standard GObject macros */
+#define E_TYPE_OAUTH2_SERVICE_MICROSOFT365 \
+       (e_oauth2_service_microsoft365_get_type ())
+#define E_OAUTH2_SERVICE_MICROSOFT365(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_OAUTH2_SERVICE_MICROSOFT365, EOAuth2ServiceMicrosoft365))
+#define E_OAUTH2_SERVICE_MICROSOFT365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_OAUTH2_SERVICE_MICROSOFT365, EOAuth2ServiceMicrosoft365Class))
+#define E_IS_OAUTH2_SERVICE_MICROSOFT365(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_OAUTH2_SERVICE_MICROSOFT365))
+#define E_IS_OAUTH2_SERVICE_MICROSOFT365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_OAUTH2_SERVICE_MICROSOFT365))
+#define E_OAUTH2_SERVICE_MICROSOFT365_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_OAUTH2_SERVICE_MICROSOFT365, EOAuth2ServiceMicrosoft365Class))
+
+G_BEGIN_DECLS
+
+typedef struct _EOAuth2ServiceMicrosoft365 EOAuth2ServiceMicrosoft365;
+typedef struct _EOAuth2ServiceMicrosoft365Class EOAuth2ServiceMicrosoft365Class;
+typedef struct _EOAuth2ServiceMicrosoft365Private EOAuth2ServiceMicrosoft365Private;
+
+struct _EOAuth2ServiceMicrosoft365 {
+       EOAuth2ServiceBase parent;
+       EOAuth2ServiceMicrosoft365Private *priv;
+};
+
+struct _EOAuth2ServiceMicrosoft365Class {
+       EOAuth2ServiceBaseClass parent_class;
+};
+
+GType          e_oauth2_service_microsoft365_get_type  (void) G_GNUC_CONST;
+
+void           e_oauth2_service_microsoft365_type_register
+                                                       (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_OAUTH2_SERVICE_MICROSOFT365_H */
diff --git a/src/Microsoft365/common/e-source-m365-folder.c b/src/Microsoft365/common/e-source-m365-folder.c
new file mode 100644
index 00000000..f333d1a9
--- /dev/null
+++ b/src/Microsoft365/common/e-source-m365-folder.c
@@ -0,0 +1,287 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include "e-source-m365-folder.h"
+
+struct _ESourceM365FolderPrivate {
+       gchar *id;
+       gchar *group_id;
+       gboolean is_default;
+};
+
+enum {
+       PROP_0,
+       PROP_ID,
+       PROP_IS_DEFAULT,
+       PROP_GROUP_ID
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (ESourceM365Folder, e_source_m365_folder, E_TYPE_SOURCE_EXTENSION)
+
+static void
+source_m365_folder_set_property (GObject *object,
+                                guint property_id,
+                                const GValue *value,
+                                GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_IS_DEFAULT:
+                       e_source_m365_folder_set_is_default (
+                               E_SOURCE_M365_FOLDER (object),
+                               g_value_get_boolean (value));
+                       return;
+
+               case PROP_ID:
+                       e_source_m365_folder_set_id (
+                               E_SOURCE_M365_FOLDER (object),
+                               g_value_get_string (value));
+                       return;
+
+               case PROP_GROUP_ID:
+                       e_source_m365_folder_set_group_id (
+                               E_SOURCE_M365_FOLDER (object),
+                               g_value_get_string (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+source_m365_folder_get_property (GObject *object,
+                                guint property_id,
+                                GValue *value,
+                                GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_IS_DEFAULT:
+                       g_value_set_boolean (
+                               value,
+                               e_source_m365_folder_get_is_default (
+                               E_SOURCE_M365_FOLDER (object)));
+                       return;
+
+               case PROP_ID:
+                       g_value_take_string (
+                               value,
+                               e_source_m365_folder_dup_id (
+                               E_SOURCE_M365_FOLDER (object)));
+                       return;
+
+               case PROP_GROUP_ID:
+                       g_value_take_string (
+                               value,
+                               e_source_m365_folder_dup_group_id (
+                               E_SOURCE_M365_FOLDER (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+source_m365_folder_finalize (GObject *object)
+{
+       ESourceM365Folder *m365_folder = E_SOURCE_M365_FOLDER (object);
+
+       g_free (m365_folder->priv->id);
+       g_free (m365_folder->priv->group_id);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_source_m365_folder_parent_class)->finalize (object);
+}
+
+static void
+e_source_m365_folder_class_init (ESourceM365FolderClass *class)
+{
+       GObjectClass *object_class;
+       ESourceExtensionClass *extension_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = source_m365_folder_set_property;
+       object_class->get_property = source_m365_folder_get_property;
+       object_class->finalize = source_m365_folder_finalize;
+
+       extension_class = E_SOURCE_EXTENSION_CLASS (class);
+       extension_class->name = E_SOURCE_EXTENSION_M365_FOLDER;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_ID,
+               g_param_spec_string (
+                       "id",
+                       "ID",
+                       "The server-assigned folder ID",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS |
+                       E_SOURCE_PARAM_SETTING));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_IS_DEFAULT,
+               g_param_spec_boolean (
+                       "is-default",
+                       "Is Default",
+                       "Whether it's user's default folder (like 'contacts', which are not part of the 
contactFolders)",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS |
+                       E_SOURCE_PARAM_SETTING));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_GROUP_ID,
+               g_param_spec_string (
+                       "group-id",
+                       "Group ID",
+                       "Optional group ID, into which the folder ID belongs",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS |
+                       E_SOURCE_PARAM_SETTING));
+}
+
+static void
+e_source_m365_folder_init (ESourceM365Folder *extension)
+{
+       extension->priv = e_source_m365_folder_get_instance_private (extension);
+}
+
+void
+e_source_m365_folder_type_register (GTypeModule *type_module)
+{
+       /* We need to ensure this is registered, because it's looked up
+        * by name in e_source_get_extension(). */
+       g_type_ensure (E_TYPE_SOURCE_M365_FOLDER);
+}
+
+const gchar *
+e_source_m365_folder_get_id (ESourceM365Folder *extension)
+{
+       g_return_val_if_fail (E_IS_SOURCE_M365_FOLDER (extension), NULL);
+
+       return extension->priv->id;
+}
+
+gchar *
+e_source_m365_folder_dup_id (ESourceM365Folder *extension)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (E_IS_SOURCE_M365_FOLDER (extension), NULL);
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       protected = e_source_m365_folder_get_id (extension);
+       duplicate = g_strdup (protected);
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       return duplicate;
+}
+
+void
+e_source_m365_folder_set_id (ESourceM365Folder *extension,
+                            const gchar *id)
+{
+       g_return_if_fail (E_IS_SOURCE_M365_FOLDER (extension));
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       if (g_strcmp0 (extension->priv->id, id) == 0) {
+               e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+               return;
+       }
+
+       g_free (extension->priv->id);
+       extension->priv->id = e_util_strdup_strip (id);
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       g_object_notify (G_OBJECT (extension), "id");
+}
+
+gboolean
+e_source_m365_folder_get_is_default (ESourceM365Folder *extension)
+{
+       g_return_val_if_fail (E_IS_SOURCE_M365_FOLDER (extension), FALSE);
+
+       return extension->priv->is_default;
+}
+
+void
+e_source_m365_folder_set_is_default (ESourceM365Folder *extension,
+                                    gboolean value)
+{
+       g_return_if_fail (E_IS_SOURCE_M365_FOLDER (extension));
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       if ((extension->priv->is_default ? 1 : 0) == (value ? 1 : 0)) {
+               e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+               return;
+       }
+
+       extension->priv->is_default = value;
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       g_object_notify (G_OBJECT (extension), "is-default");
+}
+
+const gchar *
+e_source_m365_folder_get_group_id (ESourceM365Folder *extension)
+{
+       g_return_val_if_fail (E_IS_SOURCE_M365_FOLDER (extension), NULL);
+
+       return extension->priv->group_id;
+}
+
+gchar *
+e_source_m365_folder_dup_group_id (ESourceM365Folder *extension)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (E_IS_SOURCE_M365_FOLDER (extension), NULL);
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       protected = e_source_m365_folder_get_group_id (extension);
+       duplicate = g_strdup (protected);
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       return duplicate;
+}
+
+void
+e_source_m365_folder_set_group_id (ESourceM365Folder *extension,
+                                  const gchar *group_id)
+{
+       g_return_if_fail (E_IS_SOURCE_M365_FOLDER (extension));
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       if (g_strcmp0 (extension->priv->group_id, group_id) == 0) {
+               e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+               return;
+       }
+
+       g_free (extension->priv->group_id);
+       extension->priv->group_id = e_util_strdup_strip (group_id);
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       g_object_notify (G_OBJECT (extension), "group-id");
+}
diff --git a/src/Microsoft365/common/e-source-m365-folder.h b/src/Microsoft365/common/e-source-m365-folder.h
new file mode 100644
index 00000000..0f8750e8
--- /dev/null
+++ b/src/Microsoft365/common/e-source-m365-folder.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_SOURCE_M365_FOLDER_H
+#define E_SOURCE_M365_FOLDER_H
+
+#include <libedataserver/libedataserver.h>
+
+/* Standard GObject macros */
+#define E_TYPE_SOURCE_M365_FOLDER \
+       (e_source_m365_folder_get_type ())
+#define E_SOURCE_M365_FOLDER(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_SOURCE_M365_FOLDER, ESourceM365Folder))
+#define E_SOURCE_M365_FOLDER_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_SOURCE_M365_FOLDER, ESourceM365FolderClass))
+#define E_IS_SOURCE_M365_FOLDER(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_SOURCE_M365_FOLDER))
+#define E_IS_SOURCE_M365_FOLDER_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_SOURCE_M365_FOLDER))
+#define E_SOURCE_M365_FOLDER_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_SOURCE_M365_FOLDER, ESourceM365FolderClass))
+
+#define E_SOURCE_EXTENSION_M365_FOLDER "Microsoft365 Folder"
+
+G_BEGIN_DECLS
+
+typedef struct _ESourceM365Folder ESourceM365Folder;
+typedef struct _ESourceM365FolderClass ESourceM365FolderClass;
+typedef struct _ESourceM365FolderPrivate ESourceM365FolderPrivate;
+
+struct _ESourceM365Folder {
+       ESourceExtension parent;
+       ESourceM365FolderPrivate *priv;
+};
+
+struct _ESourceM365FolderClass {
+       ESourceExtensionClass parent_class;
+};
+
+GType          e_source_m365_folder_get_type   (void) G_GNUC_CONST;
+void           e_source_m365_folder_type_register
+                                               (GTypeModule *type_module);
+const gchar *  e_source_m365_folder_get_id     (ESourceM365Folder *extension);
+gchar *                e_source_m365_folder_dup_id     (ESourceM365Folder *extension);
+void           e_source_m365_folder_set_id     (ESourceM365Folder *extension,
+                                                const gchar *id);
+gboolean       e_source_m365_folder_get_is_default
+                                               (ESourceM365Folder *extension);
+void           e_source_m365_folder_set_is_default
+                                               (ESourceM365Folder *extension,
+                                                gboolean value);
+const gchar *  e_source_m365_folder_get_group_id
+                                               (ESourceM365Folder *extension);
+gchar *                e_source_m365_folder_dup_group_id
+                                               (ESourceM365Folder *extension);
+void           e_source_m365_folder_set_group_id
+                                               (ESourceM365Folder *extension,
+                                                const gchar *group_id);
+
+G_END_DECLS
+
+#endif /* E_SOURCE_M365_FOLDER_H */
diff --git a/src/Microsoft365/evolution/CMakeLists.txt b/src/Microsoft365/evolution/CMakeLists.txt
new file mode 100644
index 00000000..e239083b
--- /dev/null
+++ b/src/Microsoft365/evolution/CMakeLists.txt
@@ -0,0 +1,39 @@
+set(sources
+       e-cal-config-m365.c
+       e-cal-config-m365.h
+       e-book-config-m365.c
+       e-book-config-m365.h
+       e-mail-config-m365-backend.c
+       e-mail-config-m365-backend.h
+       module-m365-configuration.c
+)
+set(extra_deps)
+set(extra_defines)
+set(extra_cflags
+       ${EVOLUTION_MAIL_CFLAGS}
+       ${EVOLUTION_SHELL_CFLAGS}
+       ${LIBECAL_CFLAGS}
+       ${LIBEBOOK_CFLAGS}
+)
+set(extra_incdirs
+       ${EVOLUTION_MAIL_INCLUDE_DIRS}
+       ${EVOLUTION_SHELL_INCLUDE_DIRS}
+       ${LIBECAL_INCLUDE_DIRS}
+       ${LIBEBOOK_INCLUDE_DIRS}
+)
+set(extra_ldflags
+       ${EVOLUTION_MAIL_LDFLAGS}
+       ${EVOLUTION_SHELL_LDFLAGS}
+       ${LIBECAL_LDFLAGS}
+       ${LIBEBOOK_LDFLAGS}
+)
+
+add_simple_module_m365(module-microsoft365-configuration
+       sources
+       extra_deps
+       extra_defines
+       extra_cflags
+       extra_incdirs
+       extra_ldflags
+       "${evo_moduledir}"
+)
diff --git a/src/Microsoft365/evolution/e-book-config-m365.c b/src/Microsoft365/evolution/e-book-config-m365.c
new file mode 100644
index 00000000..86afae36
--- /dev/null
+++ b/src/Microsoft365/evolution/e-book-config-m365.c
@@ -0,0 +1,66 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-book-config-m365.h"
+
+G_DEFINE_DYNAMIC_TYPE (EBookConfigM365, e_book_config_m365, E_TYPE_SOURCE_CONFIG_BACKEND)
+
+static gboolean
+book_config_m365_allow_creation (ESourceConfigBackend *backend)
+{
+       return FALSE;
+}
+
+static void
+book_config_m365_insert_widgets (ESourceConfigBackend *backend,
+                                ESource *scratch_source)
+{
+       if (!scratch_source)
+               return;
+
+       e_source_config_add_refresh_interval (e_source_config_backend_get_config (backend), scratch_source);
+}
+
+static void
+e_book_config_m365_class_init (EBookConfigM365Class *class)
+{
+       EExtensionClass *extension_class;
+       ESourceConfigBackendClass *backend_class;
+
+       extension_class = E_EXTENSION_CLASS (class);
+       extension_class->extensible_type = E_TYPE_BOOK_SOURCE_CONFIG;
+
+       backend_class = E_SOURCE_CONFIG_BACKEND_CLASS (class);
+       backend_class->backend_name = "microsoft365";
+       backend_class->allow_creation = book_config_m365_allow_creation;
+       backend_class->insert_widgets = book_config_m365_insert_widgets;
+}
+
+static void
+e_book_config_m365_class_finalize (EBookConfigM365Class *class)
+{
+}
+
+static void
+e_book_config_m365_init (EBookConfigM365 *backend)
+{
+}
+
+void
+e_book_config_m365_type_register (GTypeModule *type_module)
+{
+       /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
+        *     function, so we have to wrap it with a public function in
+        *     order to register types from a separate compilation unit. */
+       e_book_config_m365_register_type (type_module);
+}
diff --git a/src/Microsoft365/evolution/e-book-config-m365.h b/src/Microsoft365/evolution/e-book-config-m365.h
new file mode 100644
index 00000000..e619282d
--- /dev/null
+++ b/src/Microsoft365/evolution/e-book-config-m365.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_BOOK_CONFIG_M365_H
+#define E_BOOK_CONFIG_M365_H
+
+#include <e-util/e-util.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_CONFIG_M365 \
+       (e_book_config_m365_get_type ())
+#define E_BOOK_CONFIG_M365(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_BOOK_CONFIG_M365, EBookConfigM365))
+#define E_BOOK_CONFIG_M365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_BOOK_CONFIG_M365, EBookConfigM365Class))
+#define E_IS_BOOK_CONFIG_M365(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_BOOK_CONFIG_M365))
+#define E_IS_BOOK_CONFIG_M365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_BOOK_CONFIG_M365))
+#define E_BOOK_CONFIG_M365_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_BOOK_CONFIG_M365, EBookConfigM365Class))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookConfigM365 EBookConfigM365;
+typedef struct _EBookConfigM365Class EBookConfigM365Class;
+typedef struct _EBookConfigM365Private EBookConfigM365Private;
+
+struct _EBookConfigM365 {
+       ESourceConfigBackend parent;
+       EBookConfigM365Private *priv;
+};
+
+struct _EBookConfigM365Class {
+       ESourceConfigBackendClass parent_class;
+};
+
+GType          e_book_config_m365_get_type     (void) G_GNUC_CONST;
+void           e_book_config_m365_type_register(GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_BOOK_CONFIG_M365_H */
diff --git a/src/Microsoft365/evolution/e-cal-config-m365.c b/src/Microsoft365/evolution/e-cal-config-m365.c
new file mode 100644
index 00000000..694bf3dc
--- /dev/null
+++ b/src/Microsoft365/evolution/e-cal-config-m365.c
@@ -0,0 +1,84 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include "e-cal-config-m365.h"
+
+G_DEFINE_DYNAMIC_TYPE (ECalConfigM365, e_cal_config_m365, E_TYPE_SOURCE_CONFIG_BACKEND)
+
+static gboolean
+cal_config_m365_allow_creation (ESourceConfigBackend *backend)
+{
+#if 0
+       ESourceConfig *config;
+       ECalSourceConfig *cal_config;
+       ECalClientSourceType source_type;
+       gboolean allow_creation = FALSE;
+
+       config = e_source_config_backend_get_config (backend);
+
+       cal_config = E_CAL_SOURCE_CONFIG (config);
+       source_type = e_cal_source_config_get_source_type (cal_config);
+
+       switch (source_type) {
+               case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
+               case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
+                       allow_creation = TRUE;
+                       break;
+
+               default:
+                       break;
+       }
+
+       return allow_creation;
+#endif
+       return FALSE;
+}
+
+static void
+cal_config_m365_insert_widgets (ESourceConfigBackend *backend,
+                               ESource *scratch_source)
+{
+       if (!scratch_source)
+               return;
+
+       e_source_config_add_refresh_interval (e_source_config_backend_get_config (backend), scratch_source);
+}
+
+static void
+e_cal_config_m365_class_init (ECalConfigM365Class *class)
+{
+       EExtensionClass *extension_class;
+       ESourceConfigBackendClass *backend_class;
+
+       extension_class = E_EXTENSION_CLASS (class);
+       extension_class->extensible_type = E_TYPE_CAL_SOURCE_CONFIG;
+
+       backend_class = E_SOURCE_CONFIG_BACKEND_CLASS (class);
+       backend_class->backend_name = "microsoft365";
+       backend_class->allow_creation = cal_config_m365_allow_creation;
+       backend_class->insert_widgets = cal_config_m365_insert_widgets;
+}
+
+static void
+e_cal_config_m365_class_finalize (ECalConfigM365Class *class)
+{
+}
+
+static void
+e_cal_config_m365_init (ECalConfigM365 *backend)
+{
+}
+
+void
+e_cal_config_m365_type_register (GTypeModule *type_module)
+{
+       /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
+        *     function, so we have to wrap it with a public function in
+        *     order to register types from a separate compilation unit. */
+       e_cal_config_m365_register_type (type_module);
+}
diff --git a/src/Microsoft365/evolution/e-cal-config-m365.h b/src/Microsoft365/evolution/e-cal-config-m365.h
new file mode 100644
index 00000000..9ac9b574
--- /dev/null
+++ b/src/Microsoft365/evolution/e-cal-config-m365.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_CAL_CONFIG_M365_H
+#define E_CAL_CONFIG_M365_H
+
+#include <e-util/e-util.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CAL_CONFIG_M365 \
+       (e_cal_config_m365_get_type ())
+#define E_CAL_CONFIG_M365(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_CAL_CONFIG_M365, ECalConfigM365))
+#define E_CAL_CONFIG_M365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_CAL_CONFIG_M365, ECalConfigM365Class))
+#define E_IS_CAL_CONFIG_M365(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_CAL_CONFIG_M365))
+#define E_IS_CAL_CONFIG_M365_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_CAL_CONFIG_M365))
+#define E_CAL_CONFIG_M365_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_CAL_CONFIG_M365, ECalConfigM365Class))
+
+G_BEGIN_DECLS
+
+typedef struct _ECalConfigM365 ECalConfigM365;
+typedef struct _ECalConfigM365Class ECalConfigM365Class;
+typedef struct _ECalConfigM365Private ECalConfigM365Private;
+
+struct _ECalConfigM365 {
+       ESourceConfigBackend parent;
+       ECalConfigM365Private *priv;
+};
+
+struct _ECalConfigM365Class {
+       ESourceConfigBackendClass parent_class;
+};
+
+GType          e_cal_config_m365_get_type      (void) G_GNUC_CONST;
+void           e_cal_config_m365_type_register (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_CAL_CONFIG_M365_H */
diff --git a/src/Microsoft365/evolution/e-mail-config-m365-backend.c 
b/src/Microsoft365/evolution/e-mail-config-m365-backend.c
new file mode 100644
index 00000000..8da11fb4
--- /dev/null
+++ b/src/Microsoft365/evolution/e-mail-config-m365-backend.c
@@ -0,0 +1,492 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include <camel/camel.h>
+#include <libebackend/libebackend.h>
+
+#include <mail/e-mail-config-auth-check.h>
+#include <mail/e-mail-config-receiving-page.h>
+
+#include "common/camel-m365-settings.h"
+#include "common/e-m365-connection.h"
+
+#include "e-mail-config-m365-backend.h"
+
+struct _EMailConfigM365BackendPrivate {
+       GtkWidget *user_entry;
+       GtkWidget *impersonate_user_entry;
+       GtkGrid *oauth2_settings_grid;
+       GtkWidget *oauth2_override_check;
+       GtkWidget *oauth2_tenant_entry;
+       GtkWidget *oauth2_client_id_entry;
+       GtkWidget *oauth2_redirect_uri_entry;
+};
+
+G_DEFINE_DYNAMIC_TYPE_EXTENDED (EMailConfigM365Backend, e_mail_config_m365_backend, 
E_TYPE_MAIL_CONFIG_SERVICE_BACKEND, 0,
+       G_ADD_PRIVATE_DYNAMIC (EMailConfigM365Backend))
+
+static ESource *
+mail_config_m365_backend_new_collection (EMailConfigServiceBackend *backend)
+{
+       EMailConfigServiceBackendClass *class;
+       ESourceBackend *extension;
+       ESource *source;
+       const gchar *extension_name;
+
+       /* This backend serves double duty.  One instance holds the
+        * mail account source, another holds the mail transport source.
+        * We can differentiate by examining the EMailConfigServicePage
+        * the backend is associated with.  We return a new collection
+        * for both the Receiving Page and Sending Page.  Although the
+        * Sending Page instance ultimately gets discarded, it's still
+        * needed to avoid creating an [Microsoft365 Backend] extension
+        * in the mail transport source. */
+
+       class = E_MAIL_CONFIG_SERVICE_BACKEND_GET_CLASS (backend);
+
+       source = e_source_new (NULL, NULL, NULL);
+       extension_name = E_SOURCE_EXTENSION_COLLECTION;
+       extension = e_source_get_extension (source, extension_name);
+       e_source_backend_set_backend_name (extension, class->backend_name);
+
+       return source;
+}
+
+static void
+mail_config_m365_backend_set_oauth2_tooltip (GtkWidget *widget,
+                                            const gchar *value,
+                                            const gchar *when_value_empty,
+                                            gchar *when_value_filled) /* takes ownership */
+{
+       g_return_if_fail (GTK_IS_WIDGET (widget));
+
+       gtk_widget_set_tooltip_text (widget, value && *value ? when_value_filled : when_value_empty);
+
+       g_free (when_value_filled);
+}
+
+static void
+mail_config_m365_backend_insert_widgets (EMailConfigServiceBackend *backend,
+                                        GtkBox *parent)
+{
+       EMailConfigM365Backend *m365_backend;
+       EMailConfigServicePage *page;
+       ESource *source;
+       ESourceExtension *extension;
+       ESourceAuthentication *auth_extension;
+       CamelSettings *settings;
+       GtkLabel *label;
+       GtkWidget *widget;
+       GtkWidget *container;
+       const gchar *extension_name;
+       const gchar *text;
+       gchar *markup;
+
+       m365_backend = E_MAIL_CONFIG_M365_BACKEND (backend);
+       page = e_mail_config_service_backend_get_page (backend);
+
+       /* This backend serves double duty.  One instance holds the
+        * mail account source, another holds the mail transport source.
+        * We can differentiate by examining the EMailConfigServicePage
+        * the backend is associated with.  This method only applies to
+        * the Receiving Page. */
+       if (!E_IS_MAIL_CONFIG_RECEIVING_PAGE (page))
+               return;
+
+       /* This needs to come _after_ the page type check so we don't
+        * introduce a backend extension in the mail transport source. */
+       settings = e_mail_config_service_backend_get_settings (backend);
+
+       text = _("Configuration");
+       markup = g_markup_printf_escaped ("<b>%s</b>", text);
+       widget = gtk_label_new (markup);
+       gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
+       gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
+       gtk_box_pack_start (GTK_BOX (parent), widget, FALSE, FALSE, 0);
+       gtk_widget_show (widget);
+       g_free (markup);
+
+       widget = gtk_grid_new ();
+       gtk_widget_set_margin_left (widget, 12);
+       gtk_grid_set_row_spacing (GTK_GRID (widget), 6);
+       gtk_grid_set_column_spacing (GTK_GRID (widget), 6);
+       gtk_box_pack_start (GTK_BOX (parent), widget, FALSE, FALSE, 0);
+       gtk_widget_show (widget);
+
+       container = widget;
+
+       widget = gtk_label_new_with_mnemonic (_("User_name:"));
+       gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5);
+       gtk_grid_attach (GTK_GRID (container), widget, 0, 0, 1, 1);
+       gtk_widget_show (widget);
+
+       label = GTK_LABEL (widget);
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_hexpand (widget, TRUE);
+       gtk_label_set_mnemonic_widget (label, widget);
+       gtk_grid_attach (GTK_GRID (container), widget, 1, 0, 2, 1);
+       m365_backend->priv->user_entry = widget;  /* do not reference */
+       gtk_widget_show (widget);
+
+       widget = gtk_check_button_new_with_mnemonic (_("Open _Mailbox of other user"));
+       gtk_grid_attach (GTK_GRID (container), widget, 1, 1, 1, 1);
+       gtk_widget_show (widget);
+
+       if (camel_m365_settings_get_use_impersonation (CAMEL_M365_SETTINGS (settings))) {
+               const gchar *impersonate_user = camel_m365_settings_get_impersonate_user (CAMEL_M365_SETTINGS 
(settings));
+
+               if (impersonate_user && !*impersonate_user) {
+                       camel_m365_settings_set_impersonate_user (CAMEL_M365_SETTINGS (settings), NULL);
+                       camel_m365_settings_set_use_impersonation (CAMEL_M365_SETTINGS (settings), FALSE);
+               }
+       }
+
+       e_binding_bind_property (
+               settings, "use-impersonation",
+               widget, "active",
+               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_hexpand (widget, TRUE);
+       gtk_grid_attach (GTK_GRID (container), widget, 1, 2, 1, 1);
+       gtk_widget_show (widget);
+       m365_backend->priv->impersonate_user_entry = widget;  /* do not reference */
+
+       e_binding_bind_object_text_property (
+               settings, "impersonate-user",
+               widget, "text",
+               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               settings, "use-impersonation",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       text = _("Authentication");
+       markup = g_markup_printf_escaped ("<b>%s</b>", text);
+       widget = gtk_label_new (markup);
+       gtk_widget_set_margin_top (widget, 6);
+       gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
+       gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
+       gtk_box_pack_start (GTK_BOX (parent), widget, FALSE, FALSE, 0);
+       gtk_widget_show (widget);
+       g_free (markup);
+
+       widget = gtk_grid_new ();
+       gtk_widget_set_margin_left (widget, 12);
+       gtk_box_pack_start (GTK_BOX (parent), widget, FALSE, FALSE, 0);
+       m365_backend->priv->oauth2_settings_grid = GTK_GRID (widget);
+
+       gtk_grid_set_column_spacing (m365_backend->priv->oauth2_settings_grid, 4);
+       gtk_grid_set_row_spacing (m365_backend->priv->oauth2_settings_grid, 4);
+
+       container = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, container, 0, 0, 2, 1);
+
+       widget = gtk_check_button_new_with_mnemonic (_("_Override Microsoft 365 OAuth2 settings"));
+       gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0);
+       m365_backend->priv->oauth2_override_check = widget;
+
+       markup = g_markup_printf_escaped ("(<a 
href=\"https://wiki.gnome.org/Apps/Evolution/EWS/OAuth2\";>%s</a>)", _("Helpā€¦"));
+       widget = gtk_label_new (markup);
+       gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
+       gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
+       gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0);
+       g_free (markup);
+
+       /* Translators: 'Tenant' here means a term used by Microsoft to identify a company or organization in 
a Microsoft 365 world.
+          You probably do not want to translate it. More for example here: 
https://powerbi.microsoft.com/en-us/blog/what-is-a-tenant/ */
+       widget = gtk_label_new_with_mnemonic (_("_Tenant:"));
+       gtk_widget_set_margin_left (widget, 12);
+       gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, widget, 0, 1, 1, 1);
+       label = GTK_LABEL (widget);
+
+       e_binding_bind_property (
+               m365_backend->priv->oauth2_override_check, "active",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_hexpand (widget, TRUE);
+       gtk_label_set_mnemonic_widget (label, widget);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, widget, 1, 1, 1, 1);
+       m365_backend->priv->oauth2_tenant_entry = widget;
+
+       e_binding_bind_property (
+               m365_backend->priv->oauth2_override_check, "active",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       mail_config_m365_backend_set_oauth2_tooltip (widget, MICROSOFT365_TENANT,
+               /* Translators: 'Tenant' here means a term used by Microsoft to identify a company or 
organization in a Microsoft 365 world. Same for 'common', it's a default URL path.
+                  You probably do not want to translate it. More for example here: 
https://powerbi.microsoft.com/en-us/blog/what-is-a-tenant/ */
+               _("Default tenant is ā€œcommonā€œ"),
+               /* Translators: 'Tenant' here means a term used by Microsoft to identify a company or 
organization in a Microsoft 365 world.
+                  You probably do not want to translate it. More for example here: 
https://powerbi.microsoft.com/en-us/blog/what-is-a-tenant/ */
+               g_strdup_printf (_("Default tenant is ā€œ%sā€"), MICROSOFT365_TENANT));
+
+       widget = gtk_label_new_with_mnemonic (_("Application I_D:"));
+       gtk_widget_set_margin_left (widget, 12);
+       gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, widget, 0, 2, 1, 1);
+       label = GTK_LABEL (widget);
+
+       e_binding_bind_property (
+               m365_backend->priv->oauth2_override_check, "active",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_hexpand (widget, TRUE);
+       gtk_label_set_mnemonic_widget (label, widget);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, widget, 1, 2, 1, 1);
+       m365_backend->priv->oauth2_client_id_entry = widget;
+
+       e_binding_bind_property (
+               m365_backend->priv->oauth2_override_check, "active",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       mail_config_m365_backend_set_oauth2_tooltip (widget, MICROSOFT365_CLIENT_ID,
+               _("There is not set any default application ID"),
+               g_strdup_printf (_("Default application ID is ā€œ%sā€"), MICROSOFT365_CLIENT_ID));
+
+       widget = gtk_label_new_with_mnemonic (_("_Redirect URI:"));
+       gtk_widget_set_margin_left (widget, 12);
+       gtk_misc_set_alignment (GTK_MISC (widget), 1.0, 0.5);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, widget, 0, 3, 1, 1);
+       label = GTK_LABEL (widget);
+
+       e_binding_bind_property (
+               m365_backend->priv->oauth2_override_check, "active",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_hexpand (widget, TRUE);
+       gtk_label_set_mnemonic_widget (label, widget);
+       gtk_grid_attach (m365_backend->priv->oauth2_settings_grid, widget, 1, 3, 1, 1);
+       m365_backend->priv->oauth2_redirect_uri_entry = widget;
+
+       e_binding_bind_property (
+               m365_backend->priv->oauth2_override_check, "active",
+               widget, "sensitive",
+               G_BINDING_SYNC_CREATE);
+
+       markup = g_strdup_printf (_("Default redirect URI is ā€œ%sā€"), 
"https://login.microsoftonline.com/common/oauth2/nativeclient";);
+       mail_config_m365_backend_set_oauth2_tooltip (widget, MICROSOFT365_REDIRECT_URI,
+               markup,
+               g_strdup_printf (_("Default redirect URI is ā€œ%sā€"), MICROSOFT365_REDIRECT_URI));
+       g_free (markup);
+
+       gtk_widget_show_all (GTK_WIDGET (m365_backend->priv->oauth2_settings_grid));
+
+       e_binding_bind_object_text_property (
+               settings, "user",
+               m365_backend->priv->user_entry, "text",
+               G_BINDING_BIDIRECTIONAL |
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               settings, "override-oauth2",
+               m365_backend->priv->oauth2_override_check, "active",
+               G_BINDING_BIDIRECTIONAL |
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_object_text_property (
+               settings, "oauth2-tenant",
+               m365_backend->priv->oauth2_tenant_entry, "text",
+               G_BINDING_BIDIRECTIONAL |
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_object_text_property (
+               settings, "oauth2-client-id",
+               m365_backend->priv->oauth2_client_id_entry, "text",
+               G_BINDING_BIDIRECTIONAL |
+               G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_object_text_property (
+               settings, "oauth2-redirect_uri",
+               m365_backend->priv->oauth2_redirect_uri_entry, "text",
+               G_BINDING_BIDIRECTIONAL |
+               G_BINDING_SYNC_CREATE);
+
+       extension_name = E_SOURCE_EXTENSION_COLLECTION;
+       source = e_mail_config_service_backend_get_collection (backend);
+       extension = e_source_get_extension (source, extension_name);
+
+       /* The collection identity is the user name. */
+       e_binding_bind_property (
+               settings, "user",
+               extension, "identity",
+               G_BINDING_BIDIRECTIONAL |
+               G_BINDING_SYNC_CREATE);
+
+       auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+       e_source_authentication_set_host (auth_extension, "graph.microsoft.com");
+       e_source_authentication_set_port (auth_extension, 442);
+       e_source_authentication_set_method (auth_extension, "Microsoft365");
+}
+
+static void
+mail_config_m365_backend_setup_defaults (EMailConfigServiceBackend *backend)
+{
+       CamelSettings *settings;
+       EMailConfigServicePage *page;
+       const gchar *email_address;
+
+       page = e_mail_config_service_backend_get_page (backend);
+
+       /* This backend serves double duty.  One instance holds the
+        * mail account source, another holds the mail transport source.
+        * We can differentiate by examining the EMailConfigServicePage
+        * the backend is associated with.  This method only applies to
+        * the Receiving Page. */
+       if (!E_IS_MAIL_CONFIG_RECEIVING_PAGE (page))
+               return;
+
+       /* This needs to come _after_ the page type check so we don't
+        * introduce a backend extension in the mail transport source. */
+       email_address = e_mail_config_service_page_get_email_address (page);
+       settings = e_mail_config_service_backend_get_settings (backend);
+
+       camel_m365_settings_set_email (CAMEL_M365_SETTINGS (settings), email_address);
+       camel_network_settings_set_user (CAMEL_NETWORK_SETTINGS (settings), email_address);
+}
+
+static gboolean
+mail_config_m365_backend_auto_configure (EMailConfigServiceBackend *backend,
+                                        EConfigLookup *config_lookup,
+                                        gint *out_priority,
+                                        gboolean *out_is_complete)
+{
+       return e_mail_config_service_backend_auto_configure_for_kind (backend, config_lookup,
+               E_CONFIG_LOOKUP_RESULT_COLLECTION, NULL,
+               e_mail_config_service_backend_get_collection (backend),
+               out_priority, out_is_complete);
+}
+
+static gboolean
+mail_config_m365_backend_check_complete (EMailConfigServiceBackend *backend)
+{
+       EMailConfigServicePage *page;
+       EMailConfigM365Backend *m365_backend;
+       CamelSettings *settings;
+       CamelNetworkSettings *network_settings;
+       const gchar *user;
+       gboolean correct, complete = TRUE;
+
+       m365_backend = E_MAIL_CONFIG_M365_BACKEND (backend);
+       page = e_mail_config_service_backend_get_page (backend);
+
+       /* This backend serves double duty.  One instance holds the
+        * mail account source, another holds the mail transport source.
+        * We can differentiate by examining the EMailConfigServicePage
+        * the backend is associated with.  This method only applies to
+        * the Receiving Page. */
+       if (!E_IS_MAIL_CONFIG_RECEIVING_PAGE (page))
+               return TRUE;
+
+       /* This needs to come _after_ the page type check so we don't
+        * introduce a backend extension in the mail transport source. */
+       settings = e_mail_config_service_backend_get_settings (backend);
+
+       network_settings = CAMEL_NETWORK_SETTINGS (settings);
+       user = camel_network_settings_get_user (network_settings);
+
+       correct = user != NULL && *user != '\0';
+       complete = complete && correct;
+
+       e_util_set_entry_issue_hint (m365_backend->priv->user_entry, correct ? NULL : _("User name cannot be 
empty"));
+
+       if (correct) {
+               CamelM365Settings *m365_settings = CAMEL_M365_SETTINGS (settings);
+               const gchar *client_id;
+
+               if (camel_m365_settings_get_override_oauth2 (m365_settings)) {
+                       client_id = camel_m365_settings_get_oauth2_client_id (m365_settings);
+               } else {
+                       client_id = MICROSOFT365_CLIENT_ID;
+               }
+
+               correct = client_id && *client_id;
+               complete = complete && correct;
+
+               e_util_set_entry_issue_hint (m365_backend->priv->oauth2_client_id_entry, correct ? NULL : 
_("Application ID cannot be empty"));
+       }
+
+       return complete;
+}
+
+static void
+mail_config_m365_backend_commit_changes (EMailConfigServiceBackend *backend)
+{
+       CamelSettings *settings;
+       CamelM365Settings *m365_settings;
+       EMailConfigServicePage *page;
+       const gchar *email_address;
+
+       page = e_mail_config_service_backend_get_page (backend);
+
+       /* This backend serves double duty.  One instance holds the
+        * mail account source, another holds the mail transport source.
+        * We can differentiate by examining the EMailConfigServicePage
+        * the backend is associated with.  This method only applies to
+        * the Receiving Page. */
+       if (!E_IS_MAIL_CONFIG_RECEIVING_PAGE (page))
+               return;
+
+       /* This needs to come _after_ the page type check so we don't
+        * introduce a backend extension in the mail transport source. */
+       settings = e_mail_config_service_backend_get_settings (backend);
+
+       email_address = e_mail_config_service_page_get_email_address (page);
+       if (email_address != NULL) {
+               m365_settings = CAMEL_M365_SETTINGS (settings);
+               camel_m365_settings_set_email (m365_settings, email_address);
+       }
+}
+
+static void
+e_mail_config_m365_backend_class_init (EMailConfigM365BackendClass *class)
+{
+       EMailConfigServiceBackendClass *backend_class;
+
+       backend_class = E_MAIL_CONFIG_SERVICE_BACKEND_CLASS (class);
+       backend_class->backend_name = "microsoft365";
+       backend_class->new_collection = mail_config_m365_backend_new_collection;
+       backend_class->insert_widgets = mail_config_m365_backend_insert_widgets;
+       backend_class->setup_defaults = mail_config_m365_backend_setup_defaults;
+       backend_class->auto_configure = mail_config_m365_backend_auto_configure;
+       backend_class->check_complete = mail_config_m365_backend_check_complete;
+       backend_class->commit_changes = mail_config_m365_backend_commit_changes;
+}
+
+static void
+e_mail_config_m365_backend_class_finalize (EMailConfigM365BackendClass *class)
+{
+}
+
+static void
+e_mail_config_m365_backend_init (EMailConfigM365Backend *backend)
+{
+       backend->priv = e_mail_config_m365_backend_get_instance_private (backend);
+}
+
+void
+e_mail_config_m365_backend_type_register (GTypeModule *type_module)
+{
+       /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
+        *     function, so we have to wrap it with a public function in
+        *     order to register types from a separate compilation unit. */
+       e_mail_config_m365_backend_register_type (type_module);
+}
diff --git a/src/Microsoft365/evolution/e-mail-config-m365-backend.h 
b/src/Microsoft365/evolution/e-mail-config-m365-backend.h
new file mode 100644
index 00000000..1727d0de
--- /dev/null
+++ b/src/Microsoft365/evolution/e-mail-config-m365-backend.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_MAIL_CONFIG_M365_BACKEND_H
+#define E_MAIL_CONFIG_M365_BACKEND_H
+
+#include <mail/e-mail-config-service-backend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_MAIL_CONFIG_M365_BACKEND \
+       (e_mail_config_m365_backend_get_type ())
+#define E_MAIL_CONFIG_M365_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_MAIL_CONFIG_M365_BACKEND, EMailConfigM365Backend))
+#define E_MAIL_CONFIG_M365_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_MAIL_CONFIG_M365_BACKEND, EMailConfigM365BackendClass))
+#define E_IS_MAIL_CONFIG_M365_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_MAIL_CONFIG_M365_BACKEND))
+#define E_IS_MAIL_CONFIG_M365_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_MAIL_CONFIG_M365_BACKEND))
+#define E_MAIL_CONFIG_M365_BACKEND_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_MAIL_CONFIG_M365_BACKEND, EMailConfigM365BackendClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EMailConfigM365Backend EMailConfigM365Backend;
+typedef struct _EMailConfigM365BackendClass EMailConfigM365BackendClass;
+typedef struct _EMailConfigM365BackendPrivate EMailConfigM365BackendPrivate;
+
+struct _EMailConfigM365Backend {
+       EMailConfigServiceBackend parent;
+       EMailConfigM365BackendPrivate *priv;
+};
+
+struct _EMailConfigM365BackendClass {
+       EMailConfigServiceBackendClass parent_class;
+};
+
+GType          e_mail_config_m365_backend_get_type
+                                               (void) G_GNUC_CONST;
+void           e_mail_config_m365_backend_type_register
+                                               (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_MAIL_CONFIG_M365_BACKEND_H */
diff --git a/src/Microsoft365/evolution/module-m365-configuration.c 
b/src/Microsoft365/evolution/module-m365-configuration.c
new file mode 100644
index 00000000..f6629f1c
--- /dev/null
+++ b/src/Microsoft365/evolution/module-m365-configuration.c
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "common/camel-sasl-xoauth2-microsoft365.h"
+#include "common/e-oauth2-service-microsoft365.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-cal-config-m365.h"
+#include "e-book-config-m365.h"
+#include "e-mail-config-m365-backend.h"
+
+/* Module Entry Points */
+void e_module_load (GTypeModule *type_module);
+void e_module_unload (GTypeModule *type_module);
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+       bindtextdomain (GETTEXT_PACKAGE, M365_LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+       camel_sasl_xoauth2_microsoft365_type_register (type_module);
+       e_oauth2_service_microsoft365_type_register (type_module);
+       e_source_m365_folder_type_register (type_module);
+
+       e_cal_config_m365_type_register (type_module);
+       e_book_config_m365_type_register (type_module);
+       e_mail_config_m365_backend_type_register (type_module);
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+}
diff --git a/src/Microsoft365/registry/CMakeLists.txt b/src/Microsoft365/registry/CMakeLists.txt
new file mode 100644
index 00000000..d7888446
--- /dev/null
+++ b/src/Microsoft365/registry/CMakeLists.txt
@@ -0,0 +1,24 @@
+set(extra_deps)
+set(sources
+       module-m365-backend.c
+       e-m365-backend.c
+       e-m365-backend.h
+       e-m365-backend-factory.c
+       e-m365-backend-factory.h
+       e-source-m365-deltas.c
+       e-source-m365-deltas.h
+)
+set(extra_defines)
+set(extra_cflags)
+set(extra_incdirs)
+set(extra_ldflags)
+
+add_simple_module_m365(module-microsoft365-backend
+       sources
+       extra_deps
+       extra_defines
+       extra_cflags
+       extra_incdirs
+       extra_ldflags
+       "${eds_moduledir}"
+)
diff --git a/src/Microsoft365/registry/e-m365-backend-factory.c 
b/src/Microsoft365/registry/e-m365-backend-factory.c
new file mode 100644
index 00000000..303e0794
--- /dev/null
+++ b/src/Microsoft365/registry/e-m365-backend-factory.c
@@ -0,0 +1,78 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include "e-m365-backend.h"
+
+#include "e-m365-backend-factory.h"
+
+G_DEFINE_DYNAMIC_TYPE (EM365BackendFactory, e_m365_backend_factory, E_TYPE_COLLECTION_BACKEND_FACTORY)
+
+static void
+m365_backend_prepare_mail_account_source (ESource *source)
+{
+       ESourceBackend *extension;
+       const gchar *extension_name;
+
+       extension_name = E_SOURCE_EXTENSION_MAIL_ACCOUNT;
+       extension = e_source_get_extension (source, extension_name);
+       e_source_backend_set_backend_name (extension, "microsoft365");
+}
+
+static void
+m365_backend_prepare_mail_transport_source (ESource *source)
+{
+       ESourceBackend *extension;
+       const gchar *extension_name;
+
+       extension_name = E_SOURCE_EXTENSION_MAIL_TRANSPORT;
+       extension = e_source_get_extension (source, extension_name);
+       e_source_backend_set_backend_name (extension, "microsoft365");
+}
+
+static void
+m365_backend_factory_prepare_mail (ECollectionBackendFactory *factory,
+                                  ESource *mail_account_source,
+                                  ESource *mail_identity_source,
+                                  ESource *mail_transport_source)
+{
+       /* Chain up to parent's method. */
+       E_COLLECTION_BACKEND_FACTORY_CLASS (e_m365_backend_factory_parent_class)->prepare_mail (factory, 
mail_account_source, mail_identity_source, mail_transport_source);
+
+       m365_backend_prepare_mail_account_source (mail_account_source);
+       m365_backend_prepare_mail_transport_source (mail_transport_source);
+}
+
+static void
+e_m365_backend_factory_class_init (EM365BackendFactoryClass *class)
+{
+       ECollectionBackendFactoryClass *factory_class;
+
+       factory_class = E_COLLECTION_BACKEND_FACTORY_CLASS (class);
+       factory_class->factory_name = "microsoft365";
+       factory_class->backend_type = E_TYPE_M365_BACKEND;
+       factory_class->prepare_mail = m365_backend_factory_prepare_mail;
+}
+
+static void
+e_m365_backend_factory_class_finalize (EM365BackendFactoryClass *class)
+{
+}
+
+static void
+e_m365_backend_factory_init (EM365BackendFactory *factory)
+{
+}
+
+void
+e_m365_backend_factory_type_register (GTypeModule *type_module)
+{
+       /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
+        *     function, so we have to wrap it with a public function in
+        *     order to register types from a separate compilation unit. */
+       e_m365_backend_factory_register_type (type_module);
+}
diff --git a/src/Microsoft365/registry/e-m365-backend-factory.h 
b/src/Microsoft365/registry/e-m365-backend-factory.h
new file mode 100644
index 00000000..4e497cce
--- /dev/null
+++ b/src/Microsoft365/registry/e-m365-backend-factory.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_M365_BACKEND_FACTORY_H
+#define E_M365_BACKEND_FACTORY_H
+
+#include <libebackend/libebackend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_M365_BACKEND_FACTORY \
+       (e_m365_backend_factory_get_type ())
+#define E_M365_BACKEND_FACTORY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_M365_BACKEND_FACTORY, EM365BackendFactory))
+#define E_M365_BACKEND_FACTORY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_M365_BACKEND_FACTORY, EM365BackendFactoryClass))
+#define E_IS_M365_BACKEND_FACTORY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_M365_BACKEND_FACTORY))
+#define E_IS_M365_BACKEND_FACTORY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_M365_BACKEND_FACTORY))
+#define E_M365_BACKEND_FACTORY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_M365_BACKEND_FACTORY, EM365BackendFactoryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EM365BackendFactory EM365BackendFactory;
+typedef struct _EM365BackendFactoryClass EM365BackendFactoryClass;
+typedef struct _EM365BackendFactoryPrivate EM365BackendFactoryPrivate;
+
+struct _EM365BackendFactory {
+       ECollectionBackendFactory parent;
+       EM365BackendFactoryPrivate *priv;
+};
+
+struct _EM365BackendFactoryClass {
+       ECollectionBackendFactoryClass parent_class;
+};
+
+GType          e_m365_backend_factory_get_type (void) G_GNUC_CONST;
+void           e_m365_backend_factory_type_register
+                                               (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_M365_BACKEND_FACTORY_H */
diff --git a/src/Microsoft365/registry/e-m365-backend.c b/src/Microsoft365/registry/e-m365-backend.c
new file mode 100644
index 00000000..e60f68fa
--- /dev/null
+++ b/src/Microsoft365/registry/e-m365-backend.c
@@ -0,0 +1,955 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "common/e-m365-connection.h"
+#include "common/e-source-m365-folder.h"
+#include "common/camel-m365-settings.h"
+
+#include "e-source-m365-deltas.h"
+
+#include "e-m365-backend.h"
+
+#define LOCK(_backend) g_mutex_lock (&_backend->priv->property_lock)
+#define UNLOCK(_backend) g_mutex_unlock (&_backend->priv->property_lock)
+
+struct _EM365BackendPrivate {
+       GMutex property_lock;
+
+       GHashTable *folder_sources; /* gchar *folder_id ~> ESource * */
+
+       gboolean need_update_folders;
+
+       gulong source_changed_id;
+};
+
+G_DEFINE_DYNAMIC_TYPE_EXTENDED (EM365Backend, e_m365_backend, E_TYPE_COLLECTION_BACKEND, 0,
+       G_ADD_PRIVATE_DYNAMIC (EM365Backend))
+
+static void
+m365_backend_claim_old_resources (ECollectionBackend *backend)
+{
+       ESourceRegistryServer *registry;
+       GList *old_resources, *iter;
+
+       g_return_if_fail (E_IS_COLLECTION_BACKEND (backend));
+
+       registry = e_collection_backend_ref_server (backend);
+       old_resources = e_collection_backend_claim_all_resources (backend);
+
+       for (iter = old_resources; iter; iter = g_list_next (iter)) {
+               ESource *source = iter->data;
+
+               e_source_registry_server_add_source (registry, source);
+       }
+
+       g_list_free_full (old_resources, g_object_unref);
+       g_clear_object (&registry);
+}
+
+static void m365_backend_populate (ECollectionBackend *backend);
+
+static void
+m365_backend_source_changed_cb (ESource *source,
+                               EM365Backend *backend)
+{
+       if (!e_source_get_enabled (source)) {
+               backend->priv->need_update_folders = TRUE;
+               return;
+       }
+
+       if (!backend->priv->need_update_folders)
+               return;
+
+       m365_backend_populate (E_COLLECTION_BACKEND (backend));
+}
+
+static void
+m365_backend_populate (ECollectionBackend *backend)
+{
+       ESource *source;
+       EM365Backend *m365_backend = E_M365_BACKEND (backend);
+
+       source = e_backend_get_source (E_BACKEND (backend));
+
+       m365_backend->priv->need_update_folders = TRUE;
+
+       if (!m365_backend->priv->source_changed_id) {
+               m365_backend->priv->source_changed_id = g_signal_connect (source, "changed",
+                       G_CALLBACK (m365_backend_source_changed_cb), m365_backend);
+       }
+
+       /* do not do anything, if account is disabled */
+       if (!e_source_get_enabled (source))
+               return;
+
+       m365_backend_claim_old_resources (backend);
+
+       if (e_backend_get_online (E_BACKEND (backend)))
+               e_backend_schedule_authenticate (E_BACKEND (backend), NULL);
+}
+
+static void
+m365_backend_update_resource (EM365Backend *m365_backend,
+                             const gchar *extension_name,
+                             const gchar *id,
+                             const gchar *group_id,
+                             const gchar *display_name,
+                             gboolean is_default,
+                             const gchar *calendar_color)
+{
+       ESource *source;
+       gboolean is_new;
+
+       LOCK (m365_backend);
+       source = g_hash_table_lookup (m365_backend->priv->folder_sources, id);
+       if (source)
+               g_object_ref (source);
+       UNLOCK (m365_backend);
+
+       is_new = !source;
+
+       if (is_new)
+               source = e_collection_backend_new_child (E_COLLECTION_BACKEND (m365_backend), id);
+
+       if (source) {
+               e_source_set_display_name (source, display_name);
+
+               if (calendar_color && g_ascii_strcasecmp (calendar_color, "auto") != 0 && (
+                   g_strcmp0 (extension_name, E_SOURCE_EXTENSION_CALENDAR) == 0 ||
+                   g_strcmp0 (extension_name, E_SOURCE_EXTENSION_TASK_LIST) == 0 ||
+                   g_strcmp0 (extension_name, E_SOURCE_EXTENSION_MEMO_LIST) == 0)) {
+                       ESourceSelectable *selectable;
+
+                       selectable = e_source_get_extension (source, extension_name);
+                       e_source_selectable_set_color (selectable, calendar_color);
+               }
+
+               if (is_new) {
+                       ESourceRegistryServer *server;
+                       gpointer extension;
+
+                       extension = e_source_get_extension (source, extension_name);
+                       e_source_backend_set_backend_name (E_SOURCE_BACKEND (extension), "microsoft365");
+
+                       /* Do not notify with too old reminders */
+                       if (g_strcmp0 (extension_name, E_SOURCE_EXTENSION_CALENDAR) == 0 ||
+                           g_strcmp0 (extension_name, E_SOURCE_EXTENSION_TASK_LIST) == 0) {
+                               ESourceAlarms *alarms;
+                               gchar *today;
+                               GTimeVal today_tv;
+                               GDate dt;
+
+                               g_date_clear (&dt, 1);
+                               g_get_current_time (&today_tv);
+                               g_date_set_time_val (&dt, &today_tv);
+
+                               /* midnight UTC */
+                               today = g_strdup_printf ("%04d-%02d-%02dT00:00:00Z", g_date_get_year (&dt), 
g_date_get_month (&dt), g_date_get_day (&dt));
+
+                               alarms = e_source_get_extension (source, E_SOURCE_EXTENSION_ALARMS);
+                               e_source_alarms_set_last_notified (alarms, today);
+
+                               g_free (today);
+                       }
+
+                       extension = e_source_get_extension (source, E_SOURCE_EXTENSION_M365_FOLDER);
+                       e_source_m365_folder_set_id (extension, id);
+                       e_source_m365_folder_set_group_id (extension, group_id);
+                       e_source_m365_folder_set_is_default (extension, is_default);
+
+                       server = e_collection_backend_ref_server (E_COLLECTION_BACKEND (m365_backend));
+
+                       e_source_registry_server_add_source (server, source);
+
+                       g_clear_object (&server);
+               }
+       }
+
+       g_clear_object (&source);
+}
+
+static void
+m365_backend_remove_resource (EM365Backend *m365_backend,
+                             const gchar *extension_name,
+                             const gchar *id) /* NULL to remove the "is-default" resource for the 
extension_name */
+{
+       ESource *existing_source = NULL;
+
+       LOCK (m365_backend);
+
+       if (id) {
+               existing_source = g_hash_table_lookup (m365_backend->priv->folder_sources, id);
+       } else {
+               GHashTableIter iter;
+               gpointer value;
+
+               g_hash_table_iter_init (&iter, m365_backend->priv->folder_sources);
+               while (g_hash_table_iter_next (&iter, NULL, &value)) {
+                       ESource *source = value;
+
+                       if (value && e_source_has_extension (source, extension_name) &&
+                           e_source_m365_folder_get_is_default (e_source_get_extension (source, 
E_SOURCE_EXTENSION_M365_FOLDER))) {
+                               existing_source = source;
+                               break;
+                       }
+               }
+       }
+
+       if (existing_source)
+               g_object_ref (existing_source);
+
+       UNLOCK (m365_backend);
+
+       if (existing_source)
+               e_source_remove_sync (existing_source, NULL, NULL);
+
+       g_clear_object (&existing_source);
+}
+
+static GHashTable * /* gchar *uid ~> NULL */
+m365_backend_get_known_folder_ids (EM365Backend *m365_backend,
+                                  const gchar *extension_name,
+                                  gboolean with_the_default)
+{
+       GHashTable *ids;
+       GHashTableIter iter;
+       gpointer value;
+
+       ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+       LOCK (m365_backend);
+
+       g_hash_table_iter_init (&iter, m365_backend->priv->folder_sources);
+
+       while (g_hash_table_iter_next (&iter, NULL, &value)) {
+               ESource *source = value;
+
+               if (source && e_source_has_extension (source, extension_name)) {
+                       ESourceM365Folder *m365_folder;
+
+                       m365_folder = e_source_get_extension (source, E_SOURCE_EXTENSION_M365_FOLDER);
+
+                       if (with_the_default || !e_source_m365_folder_get_is_default (m365_folder))
+                               g_hash_table_insert (ids, e_source_m365_folder_dup_id (m365_folder), NULL);
+               }
+       }
+
+       UNLOCK (m365_backend);
+
+       return ids;
+}
+
+static void
+m365_backend_forget_folders_hash (EM365Backend *m365_backend,
+                                 const gchar *extension_name,
+                                 GHashTable *ids) /* gchar *id ~> NULL */
+{
+       GHashTableIter iter;
+       gpointer key;
+
+       g_hash_table_iter_init (&iter, ids);
+
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               const gchar *id = key;
+
+               if (id)
+                       m365_backend_remove_resource (m365_backend, extension_name, id);
+       }
+}
+
+static void
+m365_backend_forget_folders (EM365Backend *m365_backend,
+                            const gchar *extension_name,
+                            gboolean with_the_default)
+{
+       GHashTable *ids;
+
+       ids = m365_backend_get_known_folder_ids (m365_backend, extension_name, with_the_default);
+
+       m365_backend_forget_folders_hash (m365_backend, extension_name, ids);
+
+       g_hash_table_destroy (ids);
+}
+
+static gboolean
+m365_backend_got_contact_folders_delta_cb (EM365Connection *cnc,
+                                          const GSList *results, /* JsonObject * - the returned objects from 
the server */
+                                          gpointer user_data,
+                                          GCancellable *cancellable,
+                                          GError **error)
+{
+       EM365Backend *m365_backend = user_data;
+       GSList *link;
+
+       g_return_val_if_fail (E_IS_M365_BACKEND (m365_backend), FALSE);
+
+       for (link = (GSList *) results; link; link = g_slist_next (link)) {
+               JsonObject *object = link->data;
+               const gchar *id = e_m365_folder_get_id (object);
+
+               if (!id)
+                       continue;
+
+               if (e_m365_delta_is_removed_object (object)) {
+                       m365_backend_remove_resource (m365_backend, E_SOURCE_EXTENSION_ADDRESS_BOOK, id);
+               } else {
+                       m365_backend_update_resource (m365_backend, E_SOURCE_EXTENSION_ADDRESS_BOOK,
+                             id, NULL, e_m365_folder_get_display_name (object),
+                             FALSE, NULL);
+               }
+       }
+
+       return TRUE;
+}
+
+static void
+m365_backend_sync_contact_folders_sync (EM365Backend *m365_backend,
+                                       EM365Connection *cnc,
+                                       GCancellable *cancellable)
+{
+       EM365Folder *user_contacts = NULL;
+       ESourceM365Deltas *m365_deltas;
+       gchar *old_delta_link, *new_delta_link;
+       gboolean success;
+       GError *error = NULL;
+
+       m365_deltas = e_source_get_extension (e_backend_get_source (E_BACKEND (m365_backend)), 
E_SOURCE_EXTENSION_M365_DELTAS);
+
+       if (e_m365_connection_get_contacts_folder_sync (cnc, NULL, NULL, NULL, &user_contacts, cancellable, 
&error)) {
+               const gchar *id, *display_name;
+
+               id = e_m365_folder_get_id (user_contacts);
+               display_name = e_m365_folder_get_display_name (user_contacts);
+
+               g_warn_if_fail (id != NULL);
+               g_warn_if_fail (display_name != NULL);
+
+               m365_backend_update_resource (m365_backend,
+                       E_SOURCE_EXTENSION_ADDRESS_BOOK,
+                       id, NULL, display_name, TRUE, NULL);
+
+               json_object_unref (user_contacts);
+       } else if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND) ||
+                  g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+               m365_backend_remove_resource (m365_backend, E_SOURCE_EXTENSION_ADDRESS_BOOK, NULL);
+       }
+
+       g_clear_error (&error);
+
+       new_delta_link = NULL;
+       old_delta_link = e_source_m365_deltas_dup_contacts_link (m365_deltas);
+
+       success = e_m365_connection_get_folders_delta_sync (cnc, NULL, E_M365_FOLDER_KIND_CONTACTS, NULL, 
old_delta_link, 0,
+               m365_backend_got_contact_folders_delta_cb, m365_backend, &new_delta_link, cancellable, 
&error);
+
+       if (old_delta_link && *old_delta_link && e_m365_connection_util_delta_token_failed (error)) {
+               g_clear_pointer (&old_delta_link, g_free);
+               g_clear_error (&error);
+
+               m365_backend_forget_folders (m365_backend, E_SOURCE_EXTENSION_ADDRESS_BOOK, FALSE);
+
+               success = e_m365_connection_get_folders_delta_sync (cnc, NULL, E_M365_FOLDER_KIND_CONTACTS, 
NULL, NULL, 0,
+                       m365_backend_got_contact_folders_delta_cb, m365_backend, &new_delta_link, 
cancellable, &error);
+       }
+
+       if (success)
+               e_source_m365_deltas_set_contacts_link (m365_deltas, new_delta_link);
+
+       g_clear_pointer (&old_delta_link, g_free);
+       g_clear_pointer (&new_delta_link, g_free);
+       g_clear_error (&error);
+}
+
+static void
+m365_backend_sync_calendar_folders_sync (EM365Backend *m365_backend,
+                                        EM365Connection *cnc,
+                                        GCancellable *cancellable)
+{
+       const gchar *extension_name = E_SOURCE_EXTENSION_CALENDAR;
+       GHashTable *known_ids; /* gchar *id ~> NULL */
+       gboolean success = FALSE;
+       GSList *groups = NULL, *link;
+       GError *error = NULL;
+
+       known_ids = m365_backend_get_known_folder_ids (m365_backend, extension_name, FALSE);
+
+       if (e_m365_connection_list_calendar_groups_sync (cnc, NULL, &groups, cancellable, &error) && groups) {
+               success = TRUE;
+
+               for (link = groups; link && success; link = g_slist_next (link)) {
+                       EM365CalendarGroup *group = link->data;
+                       GSList *calendars = NULL;
+
+                       if (!group)
+                               continue;
+
+                       if (e_m365_connection_list_calendars_sync (cnc, NULL, e_m365_calendar_group_get_id 
(group), NULL, &calendars, cancellable, &error)) {
+                               GSList *clink;
+
+                               for (clink = calendars; clink; clink = g_slist_next (clink)) {
+                                       EM365Calendar *calendar = clink->data;
+
+                                       if (!calendar || !e_m365_calendar_get_id (calendar))
+                                               continue;
+
+                                       m365_backend_update_resource (m365_backend, extension_name,
+                                               e_m365_calendar_get_id (calendar),
+                                               e_m365_calendar_group_get_id (group),
+                                               e_m365_calendar_get_name (calendar),
+                                               FALSE,
+                                               e_m365_calendar_color_to_rgb (e_m365_calendar_get_color 
(calendar)));
+
+                                       g_hash_table_remove (known_ids, e_m365_calendar_get_id (calendar));
+                               }
+
+                               g_slist_free_full (calendars, (GDestroyNotify) json_object_unref);
+                       } else {
+                               success = FALSE;
+                       }
+               }
+
+               g_slist_free_full (groups, (GDestroyNotify) json_object_unref);
+       }
+
+       if (success)
+               m365_backend_forget_folders_hash (m365_backend, extension_name, known_ids);
+
+       g_hash_table_destroy (known_ids);
+       g_clear_error (&error);
+}
+
+/* Tasks are in the beta stage (as of 2020-07-31) and even one can get groups of tasks, getting
+   information about a task folder in the group fails with an error NavigationNotSupported:
+   "Recursive navigation is not allowed after property 'TaskFolders' according to the entity schema."
+   The server returns similar error when trying to get a single task, which makes it unusable. */
+#if 0
+static void
+m365_backend_sync_task_folders_sync (EM365Backend *m365_backend,
+                                    EM365Connection *cnc,
+                                    GCancellable *cancellable)
+{
+       const gchar *extension_name = E_SOURCE_EXTENSION_TASK_LIST;
+       GHashTable *known_ids; /* gchar *id ~> NULL */
+       gboolean success = FALSE;
+       GSList *groups = NULL, *link;
+       GError *error = NULL;
+
+       known_ids = m365_backend_get_known_folder_ids (m365_backend, extension_name, FALSE);
+
+       if (e_m365_connection_list_task_groups_sync (cnc, NULL, &groups, cancellable, &error) && groups) {
+               success = TRUE;
+
+               for (link = groups; link && success; link = g_slist_next (link)) {
+                       EM365TaskGroup *group = link->data;
+                       GSList *task_folders = NULL;
+
+                       if (!group)
+                               continue;
+
+                       if (e_m365_connection_list_task_folders_sync (cnc, NULL, e_m365_task_group_get_id 
(group), NULL, &task_folders, cancellable, &error)) {
+                               GSList *tlink;
+
+                               for (tlink = task_folders; tlink; tlink = g_slist_next (tlink)) {
+                                       EM365TaskFolder *task_folder = tlink->data;
+
+                                       if (!task_folder || !e_m365_task_folder_get_id (task_folder))
+                                               continue;
+
+                                       m365_backend_update_resource (m365_backend, extension_name,
+                                               e_m365_task_folder_get_id (task_folder),
+                                               e_m365_task_group_get_id (group),
+                                               e_m365_task_folder_get_name (task_folder),
+                                               e_m365_task_folder_get_is_default_folder (task_folder),
+                                               NULL);
+
+                                       g_hash_table_remove (known_ids, e_m365_task_folder_get_id 
(task_folder));
+                               }
+
+                               g_slist_free_full (task_folders, (GDestroyNotify) json_object_unref);
+                       } else {
+                               success = FALSE;
+                       }
+               }
+
+               g_slist_free_full (groups, (GDestroyNotify) json_object_unref);
+       }
+
+       if (success)
+               m365_backend_forget_folders_hash (m365_backend, extension_name, known_ids);
+
+       g_hash_table_destroy (known_ids);
+       g_clear_error (&error);
+}
+#endif
+
+static void
+m365_backend_sync_folders_thread (GTask *task,
+                                 gpointer source_object,
+                                 gpointer task_data,
+                                 GCancellable *cancellable)
+{
+       EM365Backend *m365_backend = source_object;
+       EM365Connection *cnc = task_data;
+       ESourceCollection *collection_extension = NULL;
+       ESource *source;
+
+       g_return_if_fail (E_IS_M365_BACKEND (m365_backend));
+       g_return_if_fail (E_IS_M365_CONNECTION (cnc));
+
+       source = e_backend_get_source (E_BACKEND (m365_backend));
+       collection_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_COLLECTION);
+
+       if (e_source_collection_get_contacts_enabled (collection_extension)) {
+               m365_backend_sync_contact_folders_sync (m365_backend, cnc, cancellable);
+       }
+
+       if (e_source_collection_get_calendar_enabled (collection_extension)) {
+               m365_backend_sync_calendar_folders_sync (m365_backend, cnc, cancellable);
+#if 0
+               m365_backend_sync_task_folders_sync (m365_backend, cnc, cancellable);
+#endif
+       }
+}
+
+static void
+m365_backend_sync_folders (EM365Backend *m365_backend,
+                          EM365Connection *cnc,
+                          GCancellable *cancellable,
+                          GAsyncReadyCallback callback,
+                          gpointer user_data)
+{
+       GTask *task;
+
+       m365_backend->priv->need_update_folders = FALSE;
+
+       task = g_task_new (m365_backend, cancellable, callback, user_data);
+
+       g_task_set_check_cancellable (task, TRUE);
+       g_task_set_task_data (task, g_object_ref (cnc), g_object_unref);
+       g_task_run_in_thread (task, m365_backend_sync_folders_thread);
+
+       g_object_unref (task);
+}
+
+static gchar *
+m365_backend_dup_resource_id (ECollectionBackend *backend,
+                             ESource *child_source)
+{
+       ESourceM365Folder *extension;
+       const gchar *extension_name;
+
+       extension_name = E_SOURCE_EXTENSION_M365_FOLDER;
+       extension = e_source_get_extension (child_source, extension_name);
+
+       return e_source_m365_folder_dup_id (extension);
+}
+
+static void
+m365_backend_child_added (ECollectionBackend *backend,
+                         ESource *child_source)
+{
+       ESource *collection_source;
+
+       collection_source = e_backend_get_source (E_BACKEND (backend));
+
+       if (e_source_has_extension (child_source, E_SOURCE_EXTENSION_AUTHENTICATION) && (
+           e_source_has_extension (child_source, E_SOURCE_EXTENSION_MAIL_ACCOUNT) ||
+           e_source_has_extension (child_source, E_SOURCE_EXTENSION_MAIL_IDENTITY) ||
+           e_source_has_extension (child_source, E_SOURCE_EXTENSION_MAIL_TRANSPORT))) {
+               ESourceAuthentication *auth_child_extension;
+               ESourceCollection *collection_extension;
+
+               collection_extension = e_source_get_extension (collection_source, 
E_SOURCE_EXTENSION_COLLECTION);
+               auth_child_extension = e_source_get_extension (child_source, 
E_SOURCE_EXTENSION_AUTHENTICATION);
+
+               e_binding_bind_property (
+                       collection_extension, "identity",
+                       auth_child_extension, "user",
+                       G_BINDING_SYNC_CREATE);
+       }
+
+       /* We track M365 folders in a hash table by folder ID. */
+       if (e_source_has_extension (child_source, E_SOURCE_EXTENSION_M365_FOLDER)) {
+               ESourceM365Folder *extension;
+               gchar *folder_id;
+
+               extension = e_source_get_extension (child_source, E_SOURCE_EXTENSION_M365_FOLDER);
+               folder_id = e_source_m365_folder_dup_id (extension);
+
+               if (folder_id) {
+                       EM365Backend *m365_backend = E_M365_BACKEND (backend);
+
+                       LOCK (m365_backend);
+                       g_hash_table_insert (m365_backend->priv->folder_sources, folder_id, g_object_ref 
(child_source));
+                       UNLOCK (m365_backend);
+               }
+       }
+
+       /* Chain up to parent's method. */
+       E_COLLECTION_BACKEND_CLASS (e_m365_backend_parent_class)->child_added (backend, child_source);
+}
+
+static void
+m365_backend_child_removed (ECollectionBackend *backend,
+                           ESource *child_source)
+{
+       /* We track M365 folders in a hash table by folder ID. */
+       if (e_source_has_extension (child_source, E_SOURCE_EXTENSION_M365_FOLDER)) {
+               ESourceM365Folder *extension;
+               const gchar *folder_id;
+
+               extension = e_source_get_extension (child_source, E_SOURCE_EXTENSION_M365_FOLDER);
+               folder_id = e_source_m365_folder_get_id (extension);
+
+               if (folder_id) {
+                       EM365Backend *m365_backend = E_M365_BACKEND (backend);
+
+                       LOCK (m365_backend);
+                       g_hash_table_remove (m365_backend->priv->folder_sources, folder_id);
+                       UNLOCK (m365_backend);
+               }
+       }
+
+       /* Chain up to parent's method. */
+       E_COLLECTION_BACKEND_CLASS (e_m365_backend_parent_class)->child_removed (backend, child_source);
+}
+
+static gboolean
+m365_backend_create_resource_sync (ECollectionBackend *backend,
+                                  ESource *source,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+#if 0
+       EM365Connection *connection = NULL;
+       M365FolderId *out_folder_id = NULL;
+       EM365FolderType folder_type = E_M365_FOLDER_TYPE_UNKNOWN;
+       const gchar *extension_name;
+       const gchar *parent_folder_id = NULL;
+       gchar *folder_name;
+       gboolean success = FALSE;
+
+       extension_name = E_SOURCE_EXTENSION_M365_FOLDER;
+       if (e_source_has_extension (source, extension_name)) {
+               ESourceM365Folder *extension;
+
+               /* foreign and public folders are just added */
+               extension = e_source_get_extension (source, extension_name);
+               if (e_source_m365_folder_get_foreign (extension) ||
+                   e_source_m365_folder_get_public (extension))
+                       success = TRUE;
+       }
+
+       if (!success) {
+               connection = e_m365_backend_ref_connection_sync (E_M365_BACKEND (backend), NULL, NULL, NULL, 
cancellable, error);
+               if (connection == NULL)
+                       return FALSE;
+
+               extension_name = E_SOURCE_EXTENSION_ADDRESS_BOOK;
+               if (e_source_has_extension (source, extension_name)) {
+                       folder_type = E_M365_FOLDER_TYPE_CONTACTS;
+                       parent_folder_id = "contacts";
+               }
+
+               extension_name = E_SOURCE_EXTENSION_CALENDAR;
+               if (e_source_has_extension (source, extension_name)) {
+                       folder_type = E_M365_FOLDER_TYPE_CALENDAR;
+                       parent_folder_id = "calendar";
+               }
+
+               extension_name = E_SOURCE_EXTENSION_TASK_LIST;
+               if (e_source_has_extension (source, extension_name)) {
+                       folder_type = E_M365_FOLDER_TYPE_TASKS;
+                       parent_folder_id = "tasks";
+               }
+
+               /* FIXME No support for memo lists. */
+
+               if (parent_folder_id == NULL) {
+                       g_set_error (
+                               error, G_IO_ERROR,
+                               G_IO_ERROR_INVALID_ARGUMENT,
+                               _("Could not determine a suitable folder "
+                               "class for a new folder named ā€œ%sā€"),
+                               e_source_get_display_name (source));
+                       goto exit;
+               }
+
+               folder_name = e_source_dup_display_name (source);
+
+               success = e_m365_connection_create_folder_sync (
+                       connection, M365_PRIORITY_MEDIUM,
+                       parent_folder_id, TRUE,
+                       folder_name, folder_type,
+                       &out_folder_id, cancellable, error);
+
+               g_free (folder_name);
+
+               /* Sanity check */
+               g_warn_if_fail (
+                       (success && out_folder_id != NULL) ||
+                       (!success && out_folder_id == NULL));
+
+               if (out_folder_id != NULL) {
+                       ESourceM365Folder *extension;
+                       const gchar *extension_name;
+
+                       extension_name = E_SOURCE_EXTENSION_M365_FOLDER;
+                       extension = e_source_get_extension (source, extension_name);
+                       e_source_m365_folder_set_id (
+                               extension, out_folder_id->id);
+                       e_source_m365_folder_set_change_key (
+                               extension, out_folder_id->change_key);
+
+                       e_m365_folder_id_free (out_folder_id);
+               }
+       }
+
+       if (success) {
+               ESourceRegistryServer *server;
+               ESource *parent_source;
+               const gchar *cache_dir;
+               const gchar *parent_uid;
+
+               /* Configure the source as a collection member. */
+               parent_source = e_backend_get_source (E_BACKEND (backend));
+               parent_uid = e_source_get_uid (parent_source);
+               e_source_set_parent (source, parent_uid);
+
+               /* Changes should be written back to the cache directory. */
+               cache_dir = e_collection_backend_get_cache_dir (backend);
+               e_server_side_source_set_write_directory (
+                       E_SERVER_SIDE_SOURCE (source), cache_dir);
+
+               /* Set permissions for clients. */
+               e_server_side_source_set_writable (E_SERVER_SIDE_SOURCE (source), TRUE);
+               e_server_side_source_set_remote_deletable (E_SERVER_SIDE_SOURCE (source), TRUE);
+
+               server = e_collection_backend_ref_server (backend);
+               e_source_registry_server_add_source (server, source);
+               g_object_unref (server);
+       }
+
+ exit:
+       if (connection)
+               g_object_unref (connection);
+
+       return success;
+#endif
+       return FALSE;
+}
+
+static gboolean
+m365_backend_delete_resource_sync (ECollectionBackend *backend,
+                                  ESource *source,
+                                  GCancellable *cancellable,
+                                  GError **error)
+{
+#if 0
+       EM365Connection *connection;
+       ESourceM365Folder *extension;
+       const gchar *extension_name;
+       gboolean success = FALSE;
+
+       connection = e_m365_backend_ref_connection_sync (E_M365_BACKEND (backend), NULL, NULL, NULL, 
cancellable, error);
+       if (connection == NULL)
+               return FALSE;
+
+       extension_name = E_SOURCE_EXTENSION_M365_FOLDER;
+       if (!e_source_has_extension (source, extension_name)) {
+               g_set_error (
+                       error, G_IO_ERROR,
+                       G_IO_ERROR_INVALID_ARGUMENT,
+                       _("Data source ā€œ%sā€ does not represent a Microsoft 365 folder"),
+                       e_source_get_display_name (source));
+               goto exit;
+       }
+       extension = e_source_get_extension (source, extension_name);
+
+       if (e_source_m365_folder_get_foreign (extension) ||
+           e_source_m365_folder_get_public (extension)) {
+               /* do not delete foreign or public folders,
+                * just remove them from local store */
+               success = TRUE;
+       } else {
+               gchar *folder_id;
+
+               folder_id = e_source_m365_folder_dup_id (extension);
+
+               success = e_m365_connection_delete_folder_sync (
+                       connection, M365_PRIORITY_MEDIUM, folder_id,
+                       FALSE, "HardDelete", cancellable, error);
+
+               g_free (folder_id);
+       }
+
+       if (success)
+               success = e_source_remove_sync (source, cancellable, error);
+
+ exit:
+       g_object_unref (connection);
+
+       return success;
+#endif
+       return FALSE;
+}
+
+static gboolean
+m365_backend_get_destination_address (EBackend *backend,
+                                     gchar **host,
+                                     guint16 *port)
+{
+       g_return_val_if_fail (port != NULL, FALSE);
+       g_return_val_if_fail (host != NULL, FALSE);
+
+       *host = g_strdup ("graph.microsoft.com");
+       *port = 443;
+
+       return TRUE;
+}
+
+static ESourceAuthenticationResult
+m365_backend_authenticate_sync (EBackend *backend,
+                               const ENamedParameters *credentials,
+                               gchar **out_certificate_pem,
+                               GTlsCertificateFlags *out_certificate_errors,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       CamelM365Settings *m365_settings;
+       EM365Connection *cnc;
+       ESourceAuthenticationResult result;
+
+       g_return_val_if_fail (E_IS_M365_BACKEND (backend), E_SOURCE_AUTHENTICATION_ERROR);
+
+       m365_settings = camel_m365_settings_get_from_backend (backend, NULL);
+       g_return_val_if_fail (m365_settings != NULL, E_SOURCE_AUTHENTICATION_ERROR);
+
+       cnc = e_m365_connection_new (e_backend_get_source (backend), m365_settings);
+
+       result = e_m365_connection_authenticate_sync (cnc, NULL, E_M365_FOLDER_KIND_UNKNOWN, NULL, NULL, 
out_certificate_pem, out_certificate_errors, cancellable, error);
+
+       if (result == E_SOURCE_AUTHENTICATION_ACCEPTED) {
+               e_collection_backend_authenticate_children (E_COLLECTION_BACKEND (backend), credentials);
+               m365_backend_sync_folders (E_M365_BACKEND (backend), cnc, NULL, NULL, NULL);
+       } else if (result == E_SOURCE_AUTHENTICATION_REJECTED &&
+                  !e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD)) {
+               result = E_SOURCE_AUTHENTICATION_REQUIRED;
+       }
+
+       g_clear_object (&cnc);
+
+       return result;
+}
+
+static void
+m365_backend_constructed (GObject *object)
+{
+       EBackend *backend;
+       ESource *source;
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_m365_backend_parent_class)->constructed (object);
+
+       backend = E_BACKEND (object);
+       source = e_backend_get_source (backend);
+
+       e_server_side_source_set_remote_creatable (E_SERVER_SIDE_SOURCE (source), TRUE);
+
+       /* Reset the connectable, it steals data from Authentication extension,
+          where is written incorrect address */
+       e_backend_set_connectable (backend, NULL);
+
+       if (e_source_has_extension (source, E_SOURCE_EXTENSION_COLLECTION)) {
+               ESourceCollection *collection_extension;
+
+               collection_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_COLLECTION);
+               e_source_collection_set_allow_sources_rename (collection_extension, TRUE);
+       }
+}
+
+static void
+m365_backend_dispose (GObject *object)
+{
+       EM365Backend *m365_backend = E_M365_BACKEND (object);
+       ESource *source;
+
+       source = e_backend_get_source (E_BACKEND (object));
+       if (source && m365_backend->priv->source_changed_id) {
+               g_signal_handler_disconnect (source, m365_backend->priv->source_changed_id);
+               m365_backend->priv->source_changed_id = 0;
+       }
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_m365_backend_parent_class)->dispose (object);
+}
+
+static void
+m365_backend_finalize (GObject *object)
+{
+       EM365Backend *m365_backend = E_M365_BACKEND (object);
+
+       g_hash_table_destroy (m365_backend->priv->folder_sources);
+       g_mutex_clear (&m365_backend->priv->property_lock);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_m365_backend_parent_class)->finalize (object);
+}
+
+static void
+e_m365_backend_class_init (EM365BackendClass *class)
+{
+       GObjectClass *object_class;
+       EBackendClass *backend_class;
+       ECollectionBackendClass *collection_backend_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->dispose = m365_backend_dispose;
+       object_class->finalize = m365_backend_finalize;
+       object_class->constructed = m365_backend_constructed;
+
+       collection_backend_class = E_COLLECTION_BACKEND_CLASS (class);
+       collection_backend_class->populate = m365_backend_populate;
+       collection_backend_class->dup_resource_id = m365_backend_dup_resource_id;
+       collection_backend_class->child_added = m365_backend_child_added;
+       collection_backend_class->child_removed = m365_backend_child_removed;
+       collection_backend_class->create_resource_sync = m365_backend_create_resource_sync;
+       collection_backend_class->delete_resource_sync = m365_backend_delete_resource_sync;
+
+       backend_class = E_BACKEND_CLASS (class);
+       backend_class->get_destination_address = m365_backend_get_destination_address;
+       backend_class->authenticate_sync = m365_backend_authenticate_sync;
+
+       /* This generates an ESourceCamel subtype for CamelM365Settings. */
+       e_source_camel_generate_subtype ("microsoft365", CAMEL_TYPE_M365_SETTINGS);
+}
+
+static void
+e_m365_backend_class_finalize (EM365BackendClass *class)
+{
+}
+
+static void
+e_m365_backend_init (EM365Backend *backend)
+{
+       backend->priv = e_m365_backend_get_instance_private (backend);
+       backend->priv->folder_sources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 
g_object_unref);
+
+       g_mutex_init (&backend->priv->property_lock);
+}
+
+void
+e_m365_backend_type_register (GTypeModule *type_module)
+{
+       /* XXX G_DEFINE_DYNAMIC_TYPE declares a static type registration
+        *     function, so we have to wrap it with a public function in
+        *     order to register types from a separate compilation unit. */
+       e_m365_backend_register_type (type_module);
+}
diff --git a/src/Microsoft365/registry/e-m365-backend.h b/src/Microsoft365/registry/e-m365-backend.h
new file mode 100644
index 00000000..834c28c8
--- /dev/null
+++ b/src/Microsoft365/registry/e-m365-backend.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_M365_BACKEND_H
+#define E_M365_BACKEND_H
+
+#include <libebackend/libebackend.h>
+
+/* Standard GObject macros */
+#define E_TYPE_M365_BACKEND \
+       (e_m365_backend_get_type ())
+#define E_M365_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_M365_BACKEND, EM365Backend))
+#define E_M365_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_M365_BACKEND, EM365BackendClass))
+#define E_IS_M365_BACKEND(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_M365_BACKEND))
+#define E_IS_M365_BACKEND_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_M365_BACKEND))
+#define E_M365_BACKEND_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_M365_BACKEND, EM365BackendClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EM365Backend EM365Backend;
+typedef struct _EM365BackendClass EM365BackendClass;
+typedef struct _EM365BackendPrivate EM365BackendPrivate;
+
+struct _EM365Backend {
+       ECollectionBackend parent;
+       EM365BackendPrivate *priv;
+};
+
+struct _EM365BackendClass {
+       ECollectionBackendClass parent_class;
+};
+
+GType          e_m365_backend_get_type         (void) G_GNUC_CONST;
+void           e_m365_backend_type_register    (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* E_M365_BACKEND_H */
diff --git a/src/Microsoft365/registry/e-source-m365-deltas.c 
b/src/Microsoft365/registry/e-source-m365-deltas.c
new file mode 100644
index 00000000..cd3a6297
--- /dev/null
+++ b/src/Microsoft365/registry/e-source-m365-deltas.c
@@ -0,0 +1,155 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include "e-source-m365-deltas.h"
+
+struct _ESourceM365DeltasPrivate {
+       gchar *contacts_link;
+};
+
+enum {
+       PROP_0,
+       PROP_CONTACTS_LINK
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (ESourceM365Deltas, e_source_m365_deltas, E_TYPE_SOURCE_EXTENSION)
+
+static void
+source_m365_deltas_set_property (GObject *object,
+                                guint property_id,
+                                const GValue *value,
+                                GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CONTACTS_LINK:
+                       e_source_m365_deltas_set_contacts_link (
+                               E_SOURCE_M365_DELTAS (object),
+                               g_value_get_string (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+source_m365_deltas_get_property (GObject *object,
+                                guint property_id,
+                                GValue *value,
+                                GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_CONTACTS_LINK:
+                       g_value_take_string (
+                               value,
+                               e_source_m365_deltas_dup_contacts_link (
+                               E_SOURCE_M365_DELTAS (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+source_m365_deltas_finalize (GObject *object)
+{
+       ESourceM365Deltas *m365_deltas = E_SOURCE_M365_DELTAS (object);
+
+       g_free (m365_deltas->priv->contacts_link);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (e_source_m365_deltas_parent_class)->finalize (object);
+}
+
+static void
+e_source_m365_deltas_class_init (ESourceM365DeltasClass *class)
+{
+       GObjectClass *object_class;
+       ESourceExtensionClass *extension_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = source_m365_deltas_set_property;
+       object_class->get_property = source_m365_deltas_get_property;
+       object_class->finalize = source_m365_deltas_finalize;
+
+       extension_class = E_SOURCE_EXTENSION_CLASS (class);
+       extension_class->name = E_SOURCE_EXTENSION_M365_DELTAS;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_CONTACTS_LINK,
+               g_param_spec_string (
+                       "contacts-link",
+                       "Contacts Link",
+                       "The delta link for contacts",
+                       NULL,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_STATIC_STRINGS |
+                       E_SOURCE_PARAM_SETTING));
+}
+
+static void
+e_source_m365_deltas_init (ESourceM365Deltas *extension)
+{
+       extension->priv = e_source_m365_deltas_get_instance_private (extension);
+}
+
+void
+e_source_m365_deltas_type_register (GTypeModule *type_module)
+{
+       /* We need to ensure this is registered, because it's looked up
+        * by name in e_source_get_extension(). */
+       g_type_ensure (E_TYPE_SOURCE_M365_DELTAS);
+}
+
+const gchar *
+e_source_m365_deltas_get_contacts_link (ESourceM365Deltas *extension)
+{
+       g_return_val_if_fail (E_IS_SOURCE_M365_DELTAS (extension), NULL);
+
+       return extension->priv->contacts_link;
+}
+
+gchar *
+e_source_m365_deltas_dup_contacts_link (ESourceM365Deltas *extension)
+{
+       const gchar *protected;
+       gchar *duplicate;
+
+       g_return_val_if_fail (E_IS_SOURCE_M365_DELTAS (extension), NULL);
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       protected = e_source_m365_deltas_get_contacts_link (extension);
+       duplicate = g_strdup (protected);
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       return duplicate;
+}
+
+void
+e_source_m365_deltas_set_contacts_link (ESourceM365Deltas *extension,
+                                       const gchar *delta_link)
+{
+       g_return_if_fail (E_IS_SOURCE_M365_DELTAS (extension));
+
+       e_source_extension_property_lock (E_SOURCE_EXTENSION (extension));
+
+       if (g_strcmp0 (extension->priv->contacts_link, delta_link) == 0) {
+               e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+               return;
+       }
+
+       g_free (extension->priv->contacts_link);
+       extension->priv->contacts_link = e_util_strdup_strip (delta_link);
+
+       e_source_extension_property_unlock (E_SOURCE_EXTENSION (extension));
+
+       g_object_notify (G_OBJECT (extension), "contacts-link");
+}
diff --git a/src/Microsoft365/registry/e-source-m365-deltas.h 
b/src/Microsoft365/registry/e-source-m365-deltas.h
new file mode 100644
index 00000000..3cd9132e
--- /dev/null
+++ b/src/Microsoft365/registry/e-source-m365-deltas.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_SOURCE_M365_DELTAS_H
+#define E_SOURCE_M365_DELTAS_H
+
+#include <libedataserver/libedataserver.h>
+
+/* Standard GObject macros */
+#define E_TYPE_SOURCE_M365_DELTAS \
+       (e_source_m365_deltas_get_type ())
+#define E_SOURCE_M365_DELTAS(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), E_TYPE_SOURCE_M365_DELTAS, ESourceM365Deltas))
+#define E_SOURCE_M365_DELTAS_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), E_TYPE_SOURCE_M365_DELTAS, ESourceM365DeltasClass))
+#define E_IS_SOURCE_M365_DELTAS(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), E_TYPE_SOURCE_M365_DELTAS))
+#define E_IS_SOURCE_M365_DELTAS_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), E_TYPE_SOURCE_M365_DELTAS))
+#define E_SOURCE_M365_DELTAS_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), E_TYPE_SOURCE_M365_DELTAS, ESourceM365DeltasClass))
+
+#define E_SOURCE_EXTENSION_M365_DELTAS "Microsoft365 Deltas"
+
+G_BEGIN_DECLS
+
+typedef struct _ESourceM365Deltas ESourceM365Deltas;
+typedef struct _ESourceM365DeltasClass ESourceM365DeltasClass;
+typedef struct _ESourceM365DeltasPrivate ESourceM365DeltasPrivate;
+
+struct _ESourceM365Deltas {
+       ESourceExtension parent;
+       ESourceM365DeltasPrivate *priv;
+};
+
+struct _ESourceM365DeltasClass {
+       ESourceExtensionClass parent_class;
+};
+
+GType          e_source_m365_deltas_get_type   (void) G_GNUC_CONST;
+void           e_source_m365_deltas_type_register
+                                               (GTypeModule *type_module);
+const gchar *  e_source_m365_deltas_get_contacts_link
+                                               (ESourceM365Deltas *extension);
+gchar *                e_source_m365_deltas_dup_contacts_link
+                                               (ESourceM365Deltas *extension);
+void           e_source_m365_deltas_set_contacts_link
+                                               (ESourceM365Deltas *extension,
+                                                const gchar *delta_link);
+
+G_END_DECLS
+
+#endif /* E_SOURCE_M365_DELTAS_H */
diff --git a/src/Microsoft365/registry/module-m365-backend.c b/src/Microsoft365/registry/module-m365-backend.c
new file mode 100644
index 00000000..554501d2
--- /dev/null
+++ b/src/Microsoft365/registry/module-m365-backend.c
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2020 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-ews-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "common/e-oauth2-service-microsoft365.h"
+#include "common/e-source-m365-folder.h"
+
+#include "e-m365-backend.h"
+#include "e-m365-backend-factory.h"
+#include "e-source-m365-deltas.h"
+
+/* Module Entry Points */
+void e_module_load (GTypeModule *type_module);
+void e_module_unload (GTypeModule *type_module);
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+       bindtextdomain (GETTEXT_PACKAGE, M365_LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+
+       e_oauth2_service_microsoft365_type_register (type_module);
+       e_source_m365_folder_type_register (type_module);
+       e_source_m365_deltas_type_register (type_module);
+
+       if (g_strcmp0 (g_getenv ("ENABLE_M365"), "1") == 0) {
+               e_m365_backend_type_register (type_module);
+               e_m365_backend_factory_type_register (type_module);
+       }
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 50a9b020..35089dd8 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -32,8 +32,8 @@ macro(add_ews_test _name)
        target_include_directories(${_name} PUBLIC
                ${CMAKE_BINARY_DIR}
                ${CMAKE_SOURCE_DIR}
-               ${CMAKE_BINARY_DIR}/src
-               ${CMAKE_SOURCE_DIR}/src
+               ${CMAKE_BINARY_DIR}/src/EWS
+               ${CMAKE_SOURCE_DIR}/src/EWS
                ${LIBECAL_INCLUDE_DIRS}
                ${LIBEDATASERVER_INCLUDE_DIRS}
                ${UHTTPMOCK_INCLUDE_DIRS}
diff --git a/tests/ews-test-camel.c b/tests/ews-test-camel.c
index 8e9cd866..3e84ca60 100644
--- a/tests/ews-test-camel.c
+++ b/tests/ews-test-camel.c
@@ -7,9 +7,9 @@
  * SPDX-License-Identifier: LGPL-2.1-or-later
  */
 
-#include "server/e-ews-connection.h"
-#include "server/e-ews-debug.h"
-#include "server/e-ews-folder.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-debug.h"
+#include "common/e-ews-folder.h"
 
 #include "ews-test-common.h"
 
diff --git a/tests/ews-test-common.h b/tests/ews-test-common.h
index 663a3049..dd55a623 100644
--- a/tests/ews-test-common.h
+++ b/tests/ews-test-common.h
@@ -10,8 +10,8 @@
 
 #include <uhttpmock/uhm.h>
 
-#include "server/camel-ews-settings.h"
-#include "server/e-ews-connection.h"
+#include "common/camel-ews-settings.h"
+#include "common/e-ews-connection.h"
 
 G_BEGIN_DECLS
 
diff --git a/tests/ews-test-timezones.c b/tests/ews-test-timezones.c
index 76646045..8fea8280 100644
--- a/tests/ews-test-timezones.c
+++ b/tests/ews-test-timezones.c
@@ -6,8 +6,8 @@
  */
 
 #include "calendar/e-cal-backend-ews-utils.h"
-#include "server/e-ews-connection.h"
-#include "server/e-ews-debug.h"
+#include "common/e-ews-connection.h"
+#include "common/e-ews-debug.h"
 
 #include "ews-test-common.h"
 


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