[evolution-ews/wip/mcrha/office365: 19/50] Implement batch request and lookup for some default folders
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [evolution-ews/wip/mcrha/office365: 19/50] Implement batch request and lookup for some default folders
- Date: Mon, 3 Aug 2020 15:24:07 +0000 (UTC)
commit 6a250f83de3260c2fce7c2a4ca2dfdeb12d3ccab
Author: Milan Crha <mcrha redhat com>
Date: Wed Jun 17 16:20:11 2020 +0200
Implement batch request and lookup for some default folders
src/Office365/camel/camel-o365-store-summary.c | 43 +-
src/Office365/camel/camel-o365-store-summary.h | 8 +
src/Office365/camel/camel-o365-store.c | 118 ++++-
src/Office365/common/e-o365-connection.c | 689 +++++++++++++++++++------
src/Office365/common/e-o365-connection.h | 30 ++
5 files changed, 736 insertions(+), 152 deletions(-)
---
diff --git a/src/Office365/camel/camel-o365-store-summary.c b/src/Office365/camel/camel-o365-store-summary.c
index 367af65b..05ab38da 100644
--- a/src/Office365/camel/camel-o365-store-summary.c
+++ b/src/Office365/camel/camel-o365-store-summary.c
@@ -64,6 +64,7 @@ o365_store_summary_encode_folder_name (const gchar *display_name)
return g_string_free (encoded, FALSE);
}
+#if 0
static gchar *
o365_store_summary_decode_folder_name (gchar *pathpart)
{
@@ -95,6 +96,7 @@ o365_store_summary_decode_folder_name (gchar *pathpart)
return pathpart;
}
+#endif
static void
camel_o365_store_summary_migrate_data_locked (CamelO365StoreSummary *store_summary,
@@ -404,6 +406,8 @@ camel_o365_store_summary_rebuild_hashes (CamelO365StoreSummary *store_summary)
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);
@@ -501,7 +505,7 @@ camel_o365_store_summary_set_folder (CamelO365StoreSummary *store_summary,
gboolean is_foreign,
gboolean is_public)
{
- gboolean changed;
+ gboolean changed = FALSE;
g_return_if_fail (CAMEL_IS_O365_STORE_SUMMARY (store_summary));
g_return_if_fail (id != NULL);
@@ -509,6 +513,8 @@ camel_o365_store_summary_set_folder (CamelO365StoreSummary *store_summary,
LOCK (store_summary);
+ camel_o365_store_summary_update_folder (store_summary, with_hashes_update, id, parent_id,
display_name, total_count, unread_count, -1);
+
camel_o365_store_summary_set_folder_parent_id (store_summary, id, parent_id);
camel_o365_store_summary_set_folder_total_count (store_summary, id, total_count);
camel_o365_store_summary_set_folder_unread_count (store_summary, id, unread_count);
@@ -538,6 +544,41 @@ camel_o365_store_summary_set_folder (CamelO365StoreSummary *store_summary,
UNLOCK (store_summary);
}
+void
+camel_o365_store_summary_update_folder (CamelO365StoreSummary *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_O365_STORE_SUMMARY (store_summary));
+ g_return_if_fail (id != NULL);
+ g_return_if_fail (display_name != NULL);
+
+ LOCK (store_summary);
+
+ camel_o365_store_summary_set_folder_parent_id (store_summary, id, parent_id);
+ camel_o365_store_summary_set_folder_total_count (store_summary, id, total_count);
+ camel_o365_store_summary_set_folder_unread_count (store_summary, id, unread_count);
+
+ if (children_count != -1) {
+ guint32 flags = camel_o365_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_o365_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_o365_store_summary_set_folder_display_name (store_summary, id, display_name,
with_hashes_update);
+
+ UNLOCK (store_summary);
+}
+
gboolean
camel_o365_store_summary_get_folder (CamelO365StoreSummary *store_summary,
const gchar *id,
diff --git a/src/Office365/camel/camel-o365-store-summary.h b/src/Office365/camel/camel-o365-store-summary.h
index 48cd4570..b07cc493 100644
--- a/src/Office365/camel/camel-o365-store-summary.h
+++ b/src/Office365/camel/camel-o365-store-summary.h
@@ -86,6 +86,14 @@ void camel_o365_store_summary_set_folder (CamelO365StoreSummary *store_summary,
EO365FolderKind kind,
gboolean is_foreign,
gboolean is_public);
+void camel_o365_store_summary_update_folder (CamelO365StoreSummary *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_o365_store_summary_get_folder (CamelO365StoreSummary *store_summary,
const gchar *id,
gchar **out_full_name,
diff --git a/src/Office365/camel/camel-o365-store.c b/src/Office365/camel/camel-o365-store.c
index bb17054b..70d966b8 100644
--- a/src/Office365/camel/camel-o365-store.c
+++ b/src/Office365/camel/camel-o365-store.c
@@ -36,6 +36,7 @@ struct _CamelO365StorePrivate {
gchar *storage_path;
CamelO365StoreSummary *summary;
EO365Connection *cnc;
+ GHashTable *default_folders;
};
static void camel_o365_store_initable_init (GInitableIface *iface);
@@ -148,13 +149,115 @@ o365_store_get_name (CamelService *service,
gchar *name;
if (brief)
- name = g_strdup (_("Office365 server"));
+ name = g_strdup (_("Office 365 server"));
else
- name = g_strdup (_("Mail receive via Microsoft Office365"));
+ name = g_strdup (_("Mail receive via Microsoft Office 365"));
return name;
}
+static gboolean
+o365_store_read_default_folders (CamelO365Store *o365_store,
+ EO365Connection *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_O365_STORE (o365_store), FALSE);
+ g_return_val_if_fail (E_IS_O365_CONNECTION (cnc), FALSE);
+
+ LOCK (o365_store);
+
+ if (g_hash_table_size (o365_store->priv->default_folders)) {
+ UNLOCK (o365_store);
+ return TRUE;
+ }
+
+ UNLOCK (o365_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_o365_connection_construct_uri (cnc, TRUE, NULL, E_O365_API_V1_0, NULL,
+ "mailFolders",
+ 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_o365_connection_batch_request_sync (cnc, E_O365_API_V1_0, requests, cancellable, error);
+
+ if (success) {
+ g_warn_if_fail (requests->len == G_N_ELEMENTS (default_folders));
+
+ LOCK (o365_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_o365_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_o365_json_get_string_member (object, "id", NULL);
+
+ if (id && *id) {
+ g_hash_table_insert (o365_store->priv->default_folders,
g_strdup (id),
+ GUINT_TO_POINTER (default_folders[ii].flags));
+ }
+ }
+ }
+
+ if (node)
+ json_node_unref (node);
+ }
+
+ UNLOCK (o365_store);
+ }
+
+ g_ptr_array_unref (requests);
+
+ return success;
+}
+
static EO365Connection *
o365_store_ref_connection (CamelO365Store *o365_store)
{
@@ -247,9 +350,11 @@ o365_store_authenticate_sync (CamelService *service,
GError **error)
{
CamelAuthenticationResult result;
+ CamelO365Store *o365_store;
EO365Connection *cnc;
- cnc = o365_store_ref_connection (CAMEL_O365_STORE (service));
+ o365_store = CAMEL_O365_STORE (service);
+ cnc = o365_store_ref_connection (o365_store);
if (!cnc)
return CAMEL_AUTHENTICATION_ERROR;
@@ -262,6 +367,8 @@ o365_store_authenticate_sync (CamelService *service,
break;
case E_SOURCE_AUTHENTICATION_ACCEPTED:
result = CAMEL_AUTHENTICATION_ACCEPTED;
+
+ o365_store_read_default_folders (o365_store, cnc, cancellable, NULL);
break;
case E_SOURCE_AUTHENTICATION_REJECTED:
case E_SOURCE_AUTHENTICATION_REQUIRED:
@@ -401,6 +508,8 @@ camel_o365_got_folders_delta_cb (EO365Connection *cnc,
flags = e_o365_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->o365_store->priv->default_folders, id));
+
camel_o365_store_summary_set_folder (fdd->o365_store->priv->summary, FALSE, id,
e_o365_mail_folder_get_parent_folder_id (object),
e_o365_mail_folder_get_display_name (object),
@@ -619,6 +728,8 @@ o365_store_finalize (GObject *object)
o365_store = CAMEL_O365_STORE (object);
g_rec_mutex_clear (&o365_store->priv->property_lock);
+ g_hash_table_destroy (o365_store->priv->default_folders);
+ g_free (o365_store->priv->storage_path);
/* Chain up to parent's method. */
G_OBJECT_CLASS (camel_o365_store_parent_class)->finalize (object);
@@ -697,4 +808,5 @@ camel_o365_store_init (CamelO365Store *o365_store)
o365_store->priv = camel_o365_store_get_instance_private (o365_store);
g_rec_mutex_init (&o365_store->priv->property_lock);
+ o365_store->priv->default_folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
}
diff --git a/src/Office365/common/e-o365-connection.c b/src/Office365/common/e-o365-connection.c
index 1f6db867..c30ba256 100644
--- a/src/Office365/common/e-o365-connection.c
+++ b/src/Office365/common/e-o365-connection.c
@@ -28,14 +28,11 @@
#include "e-o365-connection.h"
-typedef enum {
- E_O365_API_V1_0,
- E_O365_API_BETA
-} EO365ApiVersion;
-
#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_O365_DATA "X-EVO-O365-DATA"
+
struct _EO365ConnectionPrivate {
GRecMutex property_lock;
@@ -1052,6 +1049,78 @@ typedef gboolean (* EO365ResponseFunc) (EO365Connection *cnc,
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_o365_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_O365_DATA);
+
+ if (message_json_object) {
+ *out_node = json_node_init_object (json_node_new (JSON_NODE_OBJECT), message_json_object);
+
+ success = !o365_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 = !o365_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
o365_connection_send_request_sync (EO365Connection *cnc,
SoupMessage *message,
@@ -1061,7 +1130,7 @@ o365_connection_send_request_sync (EO365Connection *cnc,
GError **error)
{
SoupSession *soup_session;
- gint need_retry_seconds = 30;
+ gint need_retry_seconds = 5;
gboolean success = FALSE, need_retry = TRUE;
g_return_val_if_fail (E_IS_O365_CONNECTION (cnc), FALSE);
@@ -1197,26 +1266,13 @@ o365_connection_send_request_sync (EO365Connection *cnc,
success = FALSE;
} else if (success) {
- JsonParser *json_parser = NULL;
- const gchar *content_type;
-
- content_type = message->response_headers ?
soup_message_headers_get_content_type (message->response_headers, NULL) : NULL;
+ JsonNode *node = NULL;
- if (content_type && g_ascii_strcasecmp (content_type, "application/json") ==
0) {
- json_parser = json_parser_new_immutable ();
-
- success = json_parser_load_from_stream (json_parser, input_stream,
cancellable, error);
-
- if (success && error && !*error)
- success = !o365_connection_extract_error
(json_parser_get_root (json_parser), message->status_code, error);
- }
+ success = e_o365_connection_json_node_from_message (message, input_stream,
&node, cancellable, error);
if (success) {
- JsonNode *node;
gchar *next_link = NULL;
- node = json_parser ? json_parser_get_root (json_parser) : NULL;
-
success = func (cnc, message, input_stream, node, func_user_data,
&next_link, cancellable, error);
if (success && next_link && *next_link) {
@@ -1246,7 +1302,8 @@ o365_connection_send_request_sync (EO365Connection *cnc,
}
}
- g_clear_object (&json_parser);
+ if (node)
+ json_node_unref (node);
}
g_clear_object (&input_stream);
@@ -1274,135 +1331,13 @@ o365_connection_send_request_sync (EO365Connection *cnc,
return success;
}
-/* Expects pair of parameters 'name', 'value'; if value is NULL, the parameter is skipped; the last
parameter name should be NULL */
-static gchar *
-e_o365_construct_uri (EO365Connection *cnc,
- gboolean include_user,
- const gchar *user_override,
- EO365ApiVersion api_version,
- const gchar *api_part, /* NULL for 'users', empty string to skip */
- const gchar *resource,
- const gchar *path,
- ...) G_GNUC_NULL_TERMINATED;
-
-static gchar *
-e_o365_construct_uri (EO365Connection *cnc,
- gboolean include_user,
- const gchar *user_override,
- EO365ApiVersion api_version,
- const gchar *api_part,
- const gchar *resource,
- const gchar *path,
- ...)
-{
- va_list args;
- const gchar *name, *value;
- gboolean first_param = TRUE;
- GString *uri;
-
- g_return_val_if_fail (E_IS_O365_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_O365_API_V1_0:
- g_string_append_c (uri, '/');
- g_string_append (uri, "v1.0");
- break;
- case E_O365_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 (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);
- }
- }
-
- name = va_arg (args, const gchar *);
- }
-
- va_end (args);
-
- return g_string_free (uri, FALSE);
-}
-
typedef struct _EO365ResponseData {
EO365ConnectionCallFunc 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 */
} EO365ResponseData;
static gboolean
@@ -1508,7 +1443,7 @@ e_o365_connection_authenticate_sync (EO365Connection *cnc,
g_return_val_if_fail (E_IS_O365_CONNECTION (cnc), result);
/* Just pick an inexpensive operation */
- uri = e_o365_construct_uri (cnc, TRUE, NULL, E_O365_API_V1_0, NULL,
+ uri = e_o365_connection_construct_uri (cnc, TRUE, NULL, E_O365_API_V1_0, NULL,
"mailFolders",
NULL,
"$select", "displayName",
@@ -1594,6 +1529,464 @@ e_o365_connection_disconnect_sync (EO365Connection *cnc,
return TRUE;
}
+/* Expects NULL-terminated pair of parameters 'name', 'value'; if 'value' is NULL, the parameter is skipped
*/
+gchar *
+e_o365_connection_construct_uri (EO365Connection *cnc,
+ gboolean include_user,
+ const gchar *user_override,
+ EO365ApiVersion api_version,
+ const gchar *api_part,
+ const gchar *resource,
+ const gchar *path,
+ ...)
+{
+ va_list args;
+ const gchar *name, *value;
+ gboolean first_param = TRUE;
+ GString *uri;
+
+ g_return_val_if_fail (E_IS_O365_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_O365_API_V1_0:
+ g_string_append_c (uri, '/');
+ g_string_append (uri, "v1.0");
+ break;
+ case E_O365_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 (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);
+ }
+ }
+
+ name = va_arg (args, const gchar *);
+ }
+
+ va_end (args);
+
+ return g_string_free (uri, FALSE);
+}
+
+static void
+e_o365_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_o365_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_o365_json_get_int_member (object, "status", SOUP_STATUS_MALFORMED);
+
+ subobject = e_o365_json_get_object_member (object, "headers");
+
+ if (subobject)
+ json_object_foreach_member (subobject, e_o365_fill_message_headers_cb, message);
+
+ subobject = e_o365_json_get_object_member (object, "body");
+
+ if (subobject)
+ g_object_set_data_full (G_OBJECT (message), X_EVO_O365_DATA, json_object_ref (subobject),
(GDestroyNotify) json_object_unref);
+}
+
+static gboolean
+e_o365_read_batch_response_cb (EO365Connection *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_o365_json_get_string_member (object, "@odata.nextLink", NULL));
+
+ responses = e_o365_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_o365_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_o365_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_o365_connection_batch_request_internal_sync (EO365Connection *cnc,
+ EO365ApiVersion api_version,
+ GPtrArray *requests, /* SoupMessage * */
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupMessage *message;
+ JsonBuilder *builder;
+ JsonGenerator *generator;
+ JsonNode *node;
+ gboolean success;
+ gchar *uri, buff[128], *data;
+ gsize data_length = 0;
+ guint ii;
+
+ g_return_val_if_fail (E_IS_O365_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_O365_BATCH_MAX_REQUESTS, FALSE);
+
+ uri = e_o365_connection_construct_uri (cnc, FALSE, NULL, api_version, "",
+ "$batch", NULL, NULL);
+
+ message = soup_message_new (SOUP_METHOD_POST, uri);
+
+ if (!message) {
+ g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Malformed URI: ā%sā"), uri);
+ g_free (uri);
+
+ return FALSE;
+ }
+
+ g_free (uri);
+
+ builder = json_builder_new_immutable ();
+
+ json_builder_begin_object (builder);
+
+ json_builder_set_member_name (builder, "requests");
+ json_builder_begin_array (builder);
+
+ for (ii = 0; ii < requests->len; ii++) {
+ SoupMessageHeadersIter iter;
+ SoupMessage *submessage;
+ SoupURI *suri;
+ gboolean has_headers = FALSE;
+ const gchar *hdr_name, *hdr_value, *use_uri;
+
+ 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);
+
+ json_builder_begin_object (builder);
+
+ json_builder_set_member_name (builder, "id");
+ json_builder_add_string_value (builder, buff);
+
+ json_builder_set_member_name (builder, "method");
+ json_builder_add_string_value (builder, submessage->method);
+
+ json_builder_set_member_name (builder, "url");
+ json_builder_add_string_value (builder, 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) {
+ if (!has_headers) {
+ has_headers = TRUE;
+
+ json_builder_set_member_name (builder, "headers");
+ json_builder_begin_object (builder);
+ }
+
+ json_builder_set_member_name (builder, hdr_name);
+ json_builder_add_string_value (builder, hdr_value);
+ }
+ }
+
+ if (has_headers)
+ json_builder_end_object (builder);
+
+ if (submessage->request_body) {
+ SoupBuffer *sbuffer;
+
+ sbuffer = soup_message_body_flatten (submessage->request_body);
+
+ if (sbuffer && sbuffer->length > 0) {
+ json_builder_set_member_name (builder, "body");
+ json_builder_add_string_value (builder, sbuffer->data);
+ }
+
+ if (sbuffer)
+ soup_buffer_free (sbuffer);
+ }
+
+ json_builder_end_object (builder);
+ }
+
+ json_builder_end_array (builder);
+ json_builder_end_object (builder);
+
+ 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_append (message->request_headers, "Content-Type", "application/json");
+ soup_message_headers_append (message->request_headers, "Accept", "application/json");
+
+ if (data)
+ soup_message_body_append_take (message->request_body, (guchar *) data, data_length);
+
+ json_node_unref (node);
+ g_object_unref (builder);
+ g_object_unref (generator);
+
+ success = o365_connection_send_request_sync (cnc, message, e_o365_read_batch_response_cb, 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_o365_connection_batch_request_sync (EO365Connection *cnc,
+ EO365ApiVersion 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_O365_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_O365_BATCH_MAX_REQUESTS, FALSE);
+
+ use_requests = requests;
+
+ while (need_retry) {
+ need_retry = FALSE;
+
+ success = e_o365_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 EO365ConnectionCallFunc function, it only
copies items of 'results' into 'user_data', which is supposed
to be a pointer to a GSList *. */
@@ -1635,7 +2028,7 @@ e_o365_connection_list_folders_sync (EO365Connection *cnc,
g_return_val_if_fail (E_IS_O365_CONNECTION (cnc), FALSE);
g_return_val_if_fail (out_folders != NULL, FALSE);
- uri = e_o365_construct_uri (cnc, TRUE, user_override, E_O365_API_V1_0, NULL,
+ uri = e_o365_connection_construct_uri (cnc, TRUE, user_override, E_O365_API_V1_0, NULL,
"mailFolders",
from_path,
"$select", select,
@@ -1689,7 +2082,7 @@ e_o365_connection_get_mail_folders_delta_sync (EO365Connection *cnc,
if (!message) {
gchar *uri;
- uri = e_o365_construct_uri (cnc, TRUE, user_override, E_O365_API_V1_0, NULL,
+ uri = e_o365_connection_construct_uri (cnc, TRUE, user_override, E_O365_API_V1_0, NULL,
"mailFolders",
"delta",
"$select", select,
diff --git a/src/Office365/common/e-o365-connection.h b/src/Office365/common/e-o365-connection.h
index aeba478f..df6a8ae9 100644
--- a/src/Office365/common/e-o365-connection.h
+++ b/src/Office365/common/e-o365-connection.h
@@ -21,11 +21,16 @@
#include <glib-object.h>
#include <libebackend/libebackend.h>
+#include <json-glib/json-glib.h>
#include <libsoup/soup.h>
#include "camel-o365-settings.h"
#include "e-o365-enums.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_O365_BATCH_MAX_REQUESTS 20
+
/* Standard GObject macros */
#define E_TYPE_O365_CONNECTION \
(e_o365_connection_get_type ())
@@ -47,6 +52,11 @@
G_BEGIN_DECLS
+typedef enum {
+ E_O365_API_V1_0,
+ E_O365_API_BETA
+} EO365ApiVersion;
+
typedef struct _EO365Connection EO365Connection;
typedef struct _EO365ConnectionClass EO365ConnectionClass;
typedef struct _EO365ConnectionPrivate EO365ConnectionPrivate;
@@ -114,6 +124,26 @@ gboolean e_o365_connection_disconnect_sync
(EO365Connection *cnc,
GCancellable *cancellable,
GError **error);
+gchar * e_o365_connection_construct_uri (EO365Connection *cnc,
+ gboolean include_user,
+ const gchar *user_override,
+ EO365ApiVersion api_version,
+ const gchar *api_part, /* NULL for 'users', empty string to
skip */
+ const gchar *resource,
+ const gchar *path,
+ ...) G_GNUC_NULL_TERMINATED;
+gboolean e_o365_connection_json_node_from_message
+ (SoupMessage *message,
+ GInputStream *input_stream,
+ JsonNode **out_node,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_o365_connection_batch_request_sync
+ (EO365Connection *cnc,
+ EO365ApiVersion api_version,
+ GPtrArray *requests, /* SoupMessage * */
+ GCancellable *cancellable,
+ GError **error);
gboolean e_o365_connection_call_gather_into_slist
(EO365Connection *cnc,
const GSList *results, /* JsonObject * - the returned
objects from the server */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]