summaryrefslogtreecommitdiffstats
path: root/src/plugins/push-notification/push-notification-driver-ox.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
commit0441d265f2bb9da249c7abf333f0f771fadb4ab5 (patch)
tree3f3789daa2f6db22da6e55e92bee0062a7d613fe /src/plugins/push-notification/push-notification-driver-ox.c
parentInitial commit. (diff)
downloaddovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.tar.xz
dovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.zip
Adding upstream version 1:2.3.21+dfsg1.upstream/1%2.3.21+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/plugins/push-notification/push-notification-driver-ox.c')
-rw-r--r--src/plugins/push-notification/push-notification-driver-ox.c470
1 files changed, 470 insertions, 0 deletions
diff --git a/src/plugins/push-notification/push-notification-driver-ox.c b/src/plugins/push-notification/push-notification-driver-ox.c
new file mode 100644
index 0000000..728cce9
--- /dev/null
+++ b/src/plugins/push-notification/push-notification-driver-ox.c
@@ -0,0 +1,470 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash.h"
+#include "http-client.h"
+#include "http-url.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "settings-parser.h"
+#include "json-parser.h"
+#include "mailbox-attribute.h"
+#include "mail-storage-private.h"
+#include "str.h"
+#include "strescape.h"
+#include "iostream-ssl.h"
+
+#include "push-notification-plugin.h"
+#include "push-notification-drivers.h"
+#include "push-notification-event-messagenew.h"
+#include "push-notification-events.h"
+#include "push-notification-txn-msg.h"
+
+#define OX_METADATA_KEY \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER \
+ "vendor/vendor.dovecot/http-notify"
+
+/* Default values. */
+static const char *const default_events[] = { "MessageNew", NULL };
+static const char *const default_mboxes[] = { "INBOX", NULL };
+#define DEFAULT_CACHE_LIFETIME_SECS 60
+#define DEFAULT_TIMEOUT_MSECS 2000
+#define DEFAULT_RETRY_COUNT 1
+
+/* This is data that is shared by all plugin users. */
+struct push_notification_driver_ox_global {
+ struct http_client *http_client;
+ int refcount;
+};
+static struct push_notification_driver_ox_global *ox_global = NULL;
+
+/* This is data specific to an OX driver. */
+struct push_notification_driver_ox_config {
+ struct http_url *http_url;
+ struct event *event;
+ unsigned int cached_ox_metadata_lifetime_secs;
+ bool use_unsafe_username;
+ unsigned int http_max_retries;
+ unsigned int http_timeout_msecs;
+
+ char *cached_ox_metadata;
+ time_t cached_ox_metadata_timestamp;
+};
+
+/* This is data specific to an OX driver transaction. */
+struct push_notification_driver_ox_txn {
+ const char *unsafe_user;
+};
+
+static void
+push_notification_driver_ox_init_global(
+ struct mail_user *user,
+ struct push_notification_driver_ox_config *config)
+{
+ struct http_client_settings http_set;
+ struct ssl_iostream_settings ssl_set;
+
+ if (ox_global->http_client == NULL) {
+ /* This is going to use the first user's settings, but these are
+ unlikely to change between users so it shouldn't matter much.
+ */
+ i_zero(&http_set);
+ http_set.debug = user->mail_debug;
+ http_set.max_attempts = config->http_max_retries+1;
+ http_set.request_timeout_msecs = config->http_timeout_msecs;
+ http_set.event_parent = user->event;
+ mail_user_init_ssl_client_settings(user, &ssl_set);
+ http_set.ssl = &ssl_set;
+
+ ox_global->http_client = http_client_init(&http_set);
+ }
+}
+
+static int
+push_notification_driver_ox_init(struct push_notification_driver_config *config,
+ struct mail_user *user, pool_t pool,
+ void **context, const char **error_r)
+{
+ struct push_notification_driver_ox_config *dconfig;
+ const char *error, *tmp;
+
+ /* Valid config keys: cache_lifetime, url */
+ tmp = hash_table_lookup(config->config, (const char *)"url");
+ if (tmp == NULL) {
+ *error_r = "Driver requires the url parameter";
+ return -1;
+ }
+
+ dconfig = p_new(pool, struct push_notification_driver_ox_config, 1);
+ dconfig->event = event_create(user->event);
+ event_add_category(dconfig->event, &event_category_push_notification);
+ event_set_append_log_prefix(dconfig->event, "push-notification-ox: ");
+
+ if (http_url_parse(tmp, NULL, HTTP_URL_ALLOW_USERINFO_PART, pool,
+ &dconfig->http_url, &error) < 0) {
+ event_unref(&dconfig->event);
+ *error_r = t_strdup_printf("Failed to parse OX REST URL %s: %s",
+ tmp, error);
+ return -1;
+ }
+ dconfig->use_unsafe_username =
+ hash_table_lookup(config->config,
+ (const char *)"user_from_metadata") != NULL;
+
+ e_debug(dconfig->event, "Using URL %s", tmp);
+
+ tmp = hash_table_lookup(config->config, (const char *)"cache_lifetime");
+ if (tmp == NULL) {
+ dconfig->cached_ox_metadata_lifetime_secs =
+ DEFAULT_CACHE_LIFETIME_SECS;
+ } else if (settings_get_time(
+ tmp, &dconfig->cached_ox_metadata_lifetime_secs, &error) < 0) {
+ event_unref(&dconfig->event);
+ *error_r = t_strdup_printf(
+ "Failed to parse OX cache_lifetime %s: %s", tmp, error);
+ return -1;
+ }
+
+ tmp = hash_table_lookup(config->config, (const char *)"max_retries");
+ if ((tmp == NULL) ||
+ (str_to_uint(tmp, &dconfig->http_max_retries) < 0)) {
+ dconfig->http_max_retries = DEFAULT_RETRY_COUNT;
+ }
+ tmp = hash_table_lookup(config->config, (const char *)"timeout_msecs");
+ if ((tmp == NULL) ||
+ (str_to_uint(tmp, &dconfig->http_timeout_msecs) < 0)) {
+ dconfig->http_timeout_msecs = DEFAULT_TIMEOUT_MSECS;
+ }
+
+ e_debug(dconfig->event, "Using cache lifetime: %u",
+ dconfig->cached_ox_metadata_lifetime_secs);
+
+ if (ox_global == NULL) {
+ ox_global = i_new(struct push_notification_driver_ox_global, 1);
+ ox_global->refcount = 0;
+ }
+
+ ++ox_global->refcount;
+ *context = dconfig;
+
+ return 0;
+}
+
+static const char *
+push_notification_driver_ox_get_metadata(
+ struct push_notification_driver_txn *dtxn)
+{
+ struct push_notification_driver_ox_config *dconfig =
+ dtxn->duser->context;
+ struct mail_attribute_value attr;
+ struct mailbox *inbox;
+ struct mail_namespace *ns;
+ bool success = FALSE, use_existing_txn = FALSE;
+ int ret;
+
+ if ((dconfig->cached_ox_metadata != NULL) &&
+ ((dconfig->cached_ox_metadata_timestamp +
+ (time_t)dconfig->cached_ox_metadata_lifetime_secs) >
+ ioloop_time)) {
+ return dconfig->cached_ox_metadata;
+ }
+
+ /* Get canonical INBOX, where private server-level metadata is stored.
+ * See imap/cmd-getmetadata.c */
+ if ((dtxn->ptxn->t != NULL) && dtxn->ptxn->mbox->inbox_user) {
+ inbox = dtxn->ptxn->mbox;
+ use_existing_txn = TRUE;
+ } else {
+ ns = mail_namespace_find_inbox(dtxn->ptxn->muser->namespaces);
+ inbox = mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_READONLY);
+ }
+
+ ret = mailbox_attribute_get(inbox, MAIL_ATTRIBUTE_TYPE_PRIVATE,
+ OX_METADATA_KEY, &attr);
+ if (ret < 0) {
+ e_error(dconfig->event,
+ "Skipped because unable to get attribute: %s",
+ mailbox_get_last_internal_error(inbox, NULL));
+ } else if (ret == 0) {
+ e_debug(dconfig->event,
+ "Skipped because not active "
+ "(/private/"OX_METADATA_KEY" METADATA not set)");
+ } else {
+ success = TRUE;
+ }
+
+ if (!use_existing_txn)
+ mailbox_free(&inbox);
+ if (!success)
+ return NULL;
+
+ i_free(dconfig->cached_ox_metadata);
+ dconfig->cached_ox_metadata = i_strdup(attr.value);
+ dconfig->cached_ox_metadata_timestamp = ioloop_time;
+
+ return dconfig->cached_ox_metadata;
+}
+
+static bool
+push_notification_driver_ox_begin_txn(struct push_notification_driver_txn *dtxn)
+{
+ const char *const *args;
+ struct push_notification_event_messagenew_config *config;
+ const char *key, *mbox_curr, *md_value, *value;
+ bool mbox_found = FALSE;
+ struct push_notification_driver_ox_txn *txn;
+ struct push_notification_driver_ox_config *dconfig =
+ dtxn->duser->context;
+
+ md_value = push_notification_driver_ox_get_metadata(dtxn);
+ if (md_value == NULL)
+ return FALSE;
+
+ /* Unused keys: events, expire, folder */
+ /* TODO: To be implemented later(?) */
+ const char *const *events = default_events;
+ time_t expire = INT_MAX;
+ const char *const *mboxes = default_mboxes;
+
+ if (expire < ioloop_time) {
+ e_debug(dconfig->event, "Skipped due to expiration (%ld < %ld)",
+ (long)expire, (long)ioloop_time);
+ return FALSE;
+ }
+
+ mbox_curr = mailbox_get_vname(dtxn->ptxn->mbox);
+ for (; *mboxes != NULL; mboxes++) {
+ if (strcmp(mbox_curr, *mboxes) == 0) {
+ mbox_found = TRUE;
+ break;
+ }
+ }
+
+ if (mbox_found == FALSE) {
+ e_debug(dconfig->event,
+ "Skipped because %s is not a watched mailbox",
+ mbox_curr);
+ return FALSE;
+ }
+
+ txn = p_new(dtxn->ptxn->pool,
+ struct push_notification_driver_ox_txn, 1);
+
+ /* Valid keys: user */
+ args = t_strsplit_tabescaped(md_value);
+ for (; *args != NULL; args++) {
+ key = *args;
+
+ value = strchr(key, '=');
+ if (value != NULL) {
+ key = t_strdup_until(key, value++);
+ if (strcmp(key, "user") == 0) {
+ txn->unsafe_user =
+ p_strdup(dtxn->ptxn->pool, value);
+ }
+ }
+ }
+
+ if (txn->unsafe_user == NULL) {
+ e_error(dconfig->event, "No user provided in config");
+ return FALSE;
+ }
+
+ e_debug(dconfig->event, "User (%s)", txn->unsafe_user);
+
+ for (; *events != NULL; events++) {
+ if (strcmp(*events, "MessageNew") == 0) {
+ config = p_new(
+ dtxn->ptxn->pool,
+ struct push_notification_event_messagenew_config, 1);
+ config->flags = PUSH_NOTIFICATION_MESSAGE_HDR_FROM |
+ PUSH_NOTIFICATION_MESSAGE_HDR_SUBJECT |
+ PUSH_NOTIFICATION_MESSAGE_BODY_SNIPPET;
+ push_notification_event_init(
+ dtxn, "MessageNew", config);
+ e_debug(dconfig->event, "Handling MessageNew event");
+ }
+ }
+
+ dtxn->context = txn;
+
+ return TRUE;
+}
+
+static void
+push_notification_driver_ox_http_callback(
+ const struct http_response *response,
+ struct push_notification_driver_ox_config *dconfig)
+{
+ switch (response->status / 100) {
+ case 2:
+ // Success.
+ e_debug(dconfig->event, "Notification sent successfully: %s",
+ http_response_get_message(response));
+ break;
+
+ default:
+ // Error.
+ e_error(dconfig->event, "Error when sending notification: %s",
+ http_response_get_message(response));
+ break;
+ }
+}
+
+/* Callback needed for i_stream_add_destroy_callback() in
+ push_notification_driver_ox_process_msg. */
+static void str_free_i(string_t *str)
+{
+ str_free(&str);
+}
+
+static int
+push_notification_driver_ox_get_mailbox_status(
+ struct push_notification_driver_txn *dtxn,
+ struct mailbox_status *r_box_status)
+{
+ struct push_notification_driver_ox_config *dconfig =
+ dtxn->duser->context;
+ /* The already opened mailbox. We cannot use or sync it, because we are
+ within a save transaction. */
+ struct mailbox *mbox = dtxn->ptxn->mbox;
+ struct mailbox *box;
+ int ret;
+
+ /* Open and sync new instance of the same mailbox to get most recent
+ status */
+ box = mailbox_alloc(mailbox_get_namespace(mbox)->list,
+ mailbox_get_name(mbox), MAILBOX_FLAG_READONLY);
+ if (mailbox_sync(box, 0) < 0) {
+ e_error(dconfig->event, "mailbox_sync(%s) failed: %s",
+ mailbox_get_vname(mbox),
+ mailbox_get_last_internal_error(box, NULL));
+ ret = -1;
+ } else {
+ /* only 'unseen' is needed at the moment */
+ mailbox_get_open_status(box, STATUS_UNSEEN, r_box_status);
+ e_debug(dconfig->event,
+ "Got status of mailbox '%s': (unseen: %u)",
+ mailbox_get_vname(box), r_box_status->unseen);
+ ret = 0;
+ }
+
+ mailbox_free(&box);
+ return ret;
+}
+
+
+static void
+push_notification_driver_ox_process_msg(
+ struct push_notification_driver_txn *dtxn,
+ struct push_notification_txn_msg *msg)
+{
+ struct push_notification_driver_ox_config *dconfig =
+ (struct push_notification_driver_ox_config *)
+ dtxn->duser->context;
+ struct http_client_request *http_req;
+ struct push_notification_event_messagenew_data *messagenew;
+ struct istream *payload;
+ string_t *str;
+ struct push_notification_driver_ox_txn *txn =
+ (struct push_notification_driver_ox_txn *)dtxn->context;
+ struct mail_user *user = dtxn->ptxn->muser;
+ struct mailbox_status box_status;
+ bool status_success = TRUE;
+
+ if (push_notification_driver_ox_get_mailbox_status(
+ dtxn, &box_status) < 0) {
+ status_success = FALSE;
+ }
+
+ messagenew = push_notification_txn_msg_get_eventdata(msg, "MessageNew");
+ if (messagenew == NULL)
+ return;
+
+ push_notification_driver_ox_init_global(user, dconfig);
+
+ http_req = http_client_request_url(
+ ox_global->http_client, "PUT", dconfig->http_url,
+ push_notification_driver_ox_http_callback, dconfig);
+ http_client_request_set_event(http_req, dtxn->ptxn->event);
+ http_client_request_add_header(http_req, "Content-Type",
+ "application/json; charset=utf-8");
+
+ str = str_new(default_pool, 256);
+ str_append(str, "{\"user\":\"");
+ json_append_escaped(str, dconfig->use_unsafe_username ?
+ txn->unsafe_user : user->username);
+ str_append(str, "\",\"event\":\"messageNew\",\"folder\":\"");
+ json_append_escaped(str, msg->mailbox);
+ str_printfa(str, "\",\"imap-uidvalidity\":%u,\"imap-uid\":%u",
+ msg->uid_validity, msg->uid);
+ if (messagenew->from != NULL) {
+ str_append(str, ",\"from\":\"");
+ json_append_escaped(str, messagenew->from);
+ str_append(str, "\"");
+ }
+ if (messagenew->subject != NULL) {
+ str_append(str, ",\"subject\":\"");
+ json_append_escaped(str, messagenew->subject);
+ str_append(str, "\"");
+ }
+ if (messagenew->snippet != NULL) {
+ str_append(str, ",\"snippet\":\"");
+ json_append_escaped(str, messagenew->snippet);
+ str_append(str, "\"");
+ }
+ if (status_success) {
+ str_printfa(str, ",\"unseen\":%u", box_status.unseen);
+ }
+ str_append(str, "}");
+
+ e_debug(dconfig->event, "Sending notification: %s", str_c(str));
+
+ payload = i_stream_create_from_data(str_data(str), str_len(str));
+ i_stream_add_destroy_callback(payload, str_free_i, str);
+ http_client_request_set_payload(http_req, payload, FALSE);
+
+ http_client_request_submit(http_req);
+ i_stream_unref(&payload);
+}
+
+static void
+push_notification_driver_ox_deinit(
+ struct push_notification_driver_user *duser ATTR_UNUSED)
+{
+ struct push_notification_driver_ox_config *dconfig = duser->context;
+
+ i_free(dconfig->cached_ox_metadata);
+ if (ox_global != NULL) {
+ if (ox_global->http_client != NULL)
+ http_client_wait(ox_global->http_client);
+ i_assert(ox_global->refcount > 0);
+ --ox_global->refcount;
+ }
+ event_unref(&dconfig->event);
+}
+
+static void push_notification_driver_ox_cleanup(void)
+{
+ if ((ox_global != NULL) && (ox_global->refcount <= 0)) {
+ if (ox_global->http_client != NULL) {
+ http_client_deinit(&ox_global->http_client);
+ }
+ i_free_and_null(ox_global);
+ }
+}
+
+/* Driver definition */
+
+extern struct push_notification_driver push_notification_driver_ox;
+
+struct push_notification_driver push_notification_driver_ox = {
+ .name = "ox",
+ .v = {
+ .init = push_notification_driver_ox_init,
+ .begin_txn = push_notification_driver_ox_begin_txn,
+ .process_msg = push_notification_driver_ox_process_msg,
+ .deinit = push_notification_driver_ox_deinit,
+ .cleanup = push_notification_driver_ox_cleanup,
+ },
+};