diff options
Diffstat (limited to '')
-rw-r--r-- | src/plugins/push-notification/push-notification-driver-ox.c | 470 |
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, + }, +}; |