diff options
Diffstat (limited to 'pigeonhole/src/lib-sieve/plugins/enotify/mailto/ntfy-mailto.c')
-rw-r--r-- | pigeonhole/src/lib-sieve/plugins/enotify/mailto/ntfy-mailto.c | 794 |
1 files changed, 794 insertions, 0 deletions
diff --git a/pigeonhole/src/lib-sieve/plugins/enotify/mailto/ntfy-mailto.c b/pigeonhole/src/lib-sieve/plugins/enotify/mailto/ntfy-mailto.c new file mode 100644 index 0000000..4e104d1 --- /dev/null +++ b/pigeonhole/src/lib-sieve/plugins/enotify/mailto/ntfy-mailto.c @@ -0,0 +1,794 @@ +/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file + */ + +/* Notify method mailto + * -------------------- + * + * Authors: Stephan Bosch + * Specification: RFC 5436 + * Implementation: full + * Status: testing + * + */ + +/* FIXME: URI syntax conforms to something somewhere in between RFC 2368 and + * draft-duerst-mailto-bis-05.txt. Should fully migrate to new specification + * when it matures. This requires modifications to the address parser (no + * whitespace allowed within the address itself) and UTF-8 support will be + * required in the URL. + */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "ioloop.h" +#include "str-sanitize.h" +#include "ostream.h" +#include "message-date.h" +#include "mail-storage.h" + +#include "sieve-common.h" +#include "sieve-address.h" +#include "sieve-address-source.h" +#include "sieve-message.h" +#include "sieve-smtp.h" +#include "sieve-settings.h" + +#include "sieve-ext-enotify.h" + +#include "rfc2822.h" + +#include "uri-mailto.h" + +/* + * Configuration + */ + +#define NTFY_MAILTO_MAX_RECIPIENTS 8 +#define NTFY_MAILTO_MAX_HEADERS 16 +#define NTFY_MAILTO_MAX_SUBJECT 256 + +/* + * Mailto notification configuration + */ + +struct ntfy_mailto_config { + pool_t pool; + struct sieve_address_source envelope_from; +}; + +/* + * Mailto notification method + */ + +static bool ntfy_mailto_load + (const struct sieve_enotify_method *nmth, void **context); +static void ntfy_mailto_unload + (const struct sieve_enotify_method *nmth); + +static bool ntfy_mailto_compile_check_uri + (const struct sieve_enotify_env *nenv, const char *uri, const char *uri_body); +static bool ntfy_mailto_compile_check_from + (const struct sieve_enotify_env *nenv, string_t *from); + +static const char *ntfy_mailto_runtime_get_notify_capability + (const struct sieve_enotify_env *nenv, const char *uri, const char *uri_body, + const char *capability); +static bool ntfy_mailto_runtime_check_uri + (const struct sieve_enotify_env *nenv, const char *uri, const char *uri_body); +static bool ntfy_mailto_runtime_check_operands + (const struct sieve_enotify_env *nenv, const char *uri,const char *uri_body, + string_t *message, string_t *from, pool_t context_pool, + void **method_context); + +static int ntfy_mailto_action_check_duplicates + (const struct sieve_enotify_env *nenv, + const struct sieve_enotify_action *nact, + const struct sieve_enotify_action *nact_other); + +static void ntfy_mailto_action_print + (const struct sieve_enotify_print_env *penv, + const struct sieve_enotify_action *nact); + +static int ntfy_mailto_action_execute + (const struct sieve_enotify_exec_env *nenv, + const struct sieve_enotify_action *nact); + +const struct sieve_enotify_method_def mailto_notify = { + "mailto", + ntfy_mailto_load, + ntfy_mailto_unload, + ntfy_mailto_compile_check_uri, + NULL, + ntfy_mailto_compile_check_from, + NULL, + ntfy_mailto_runtime_check_uri, + ntfy_mailto_runtime_get_notify_capability, + ntfy_mailto_runtime_check_operands, + NULL, + ntfy_mailto_action_check_duplicates, + ntfy_mailto_action_print, + ntfy_mailto_action_execute +}; + +/* + * Reserved and unique headers + */ + +static const char *_reserved_headers[] = { + "auto-submitted", + "received", + "message-id", + "data", + "bcc", + "in-reply-to", + "references", + "resent-date", + "resent-from", + "resent-sender", + "resent-to", + "resent-cc", + "resent-bcc", + "resent-msg-id", + "from", + "sender", + NULL +}; + +static const char *_unique_headers[] = { + "reply-to", + NULL +}; + +/* + * Method context data + */ + +struct ntfy_mailto_context { + struct uri_mailto *uri; + const struct smtp_address *from_address; +}; + +/* + * Method registration + */ + +static bool ntfy_mailto_load +(const struct sieve_enotify_method *nmth, void **context) +{ + struct sieve_instance *svinst = nmth->svinst; + struct ntfy_mailto_config *config; + pool_t pool; + + if ( *context != NULL ) { + ntfy_mailto_unload(nmth); + } + + pool = pool_alloconly_create("ntfy_mailto_config", 256); + config = p_new(pool, struct ntfy_mailto_config, 1); + config->pool = pool; + + (void)sieve_address_source_parse_from_setting (svinst, + config->pool, "sieve_notify_mailto_envelope_from", + &config->envelope_from); + + *context = (void *) config; + + return TRUE; +} + +static void ntfy_mailto_unload +(const struct sieve_enotify_method *nmth) +{ + struct ntfy_mailto_config *config = + (struct ntfy_mailto_config *) nmth->context; + + pool_unref(&config->pool); +} + +/* + * URI parsing + */ + +struct ntfy_mailto_uri_env { + const struct sieve_enotify_env *nenv; + + struct event *event; + + struct uri_mailto_log uri_log; +}; + +static void ATTR_FORMAT(5, 0) +ntfy_mailto_uri_logv(void *context, enum log_type log_type, + const char *csrc_filename, unsigned int csrc_linenum, + const char *fmt, va_list args) +{ + struct ntfy_mailto_uri_env *nmuenv = context; + const struct sieve_enotify_env *nenv = nmuenv->nenv; + + sieve_event_logv(nenv->svinst, nenv->ehandler, nmuenv->event, + log_type, csrc_filename, csrc_linenum, + nenv->location, 0, fmt, args); +} + +static void +ntfy_mailto_uri_env_init(struct ntfy_mailto_uri_env *nmuenv, + const struct sieve_enotify_env *nenv) +{ + i_zero(nmuenv); + nmuenv->nenv = nenv; + nmuenv->event = event_create(nenv->event); + event_set_append_log_prefix(nmuenv->event, "mailto URI: "); + + nmuenv->uri_log.context = nmuenv; + nmuenv->uri_log.logv = ntfy_mailto_uri_logv; +} + +static void +ntfy_mailto_uri_env_deinit(struct ntfy_mailto_uri_env *nmuenv) +{ + event_unref(&nmuenv->event); +} + +/* + * Validation + */ + + +static bool ntfy_mailto_compile_check_uri +(const struct sieve_enotify_env *nenv, const char *uri ATTR_UNUSED, + const char *uri_body) +{ + struct ntfy_mailto_uri_env nmuenv; + bool result; + + ntfy_mailto_uri_env_init(&nmuenv, nenv); + result = uri_mailto_validate( + uri_body, _reserved_headers, _unique_headers, + NTFY_MAILTO_MAX_RECIPIENTS, NTFY_MAILTO_MAX_HEADERS, + &nmuenv.uri_log); + ntfy_mailto_uri_env_deinit(&nmuenv); + + return result; +} + +static bool ntfy_mailto_compile_check_from +(const struct sieve_enotify_env *nenv, string_t *from) +{ + const char *error; + bool result = FALSE; + + T_BEGIN { + result = sieve_address_validate_str(from, &error); + if ( !result ) { + sieve_enotify_error(nenv, + "specified :from address '%s' is invalid for " + "the mailto method: %s", + str_sanitize(str_c(from), 128), error); + } + } T_END; + + return result; +} + +/* + * Runtime + */ + +struct ntfy_mailto_runtime_env { + const struct sieve_enotify_env *nenv; + + struct event *event; +}; + +static const char *ntfy_mailto_runtime_get_notify_capability +(const struct sieve_enotify_env *nenv ATTR_UNUSED, const char *uri ATTR_UNUSED, + const char *uri_body, const char *capability) +{ + if ( !uri_mailto_validate(uri_body, _reserved_headers, _unique_headers, + NTFY_MAILTO_MAX_RECIPIENTS, NTFY_MAILTO_MAX_HEADERS, NULL) ) { + return NULL; + } + + if ( strcasecmp(capability, "online") == 0 ) + return "maybe"; + + return NULL; +} + +static bool ntfy_mailto_runtime_check_uri +(const struct sieve_enotify_env *nenv ATTR_UNUSED, const char *uri ATTR_UNUSED, + const char *uri_body) +{ + return uri_mailto_validate + (uri_body, _reserved_headers, _unique_headers, + NTFY_MAILTO_MAX_RECIPIENTS, NTFY_MAILTO_MAX_HEADERS, NULL); +} + +static bool ntfy_mailto_runtime_check_operands +(const struct sieve_enotify_env *nenv, const char *uri ATTR_UNUSED, + const char *uri_body, string_t *message ATTR_UNUSED, string_t *from, + pool_t context_pool, void **method_context) +{ + struct ntfy_mailto_context *mtctx; + struct uri_mailto *parsed_uri; + const struct smtp_address *address; + struct ntfy_mailto_uri_env nmuenv; + const char *error; + + /* Need to create context before validation to have arrays present */ + mtctx = p_new(context_pool, struct ntfy_mailto_context, 1); + + /* Validate :from */ + if ( from != NULL ) { + T_BEGIN { + address = sieve_address_parse_str(from, &error); + if ( address == NULL ) { + sieve_enotify_error(nenv, + "specified :from address '%s' is invalid for " + "the mailto method: %s", + str_sanitize(str_c(from), 128), error); + } else + mtctx->from_address = + smtp_address_clone(context_pool, address); + } T_END; + + if ( address == NULL ) return FALSE; + } + + ntfy_mailto_uri_env_init(&nmuenv, nenv); + parsed_uri = uri_mailto_parse(uri_body, context_pool, + _reserved_headers, _unique_headers, + NTFY_MAILTO_MAX_RECIPIENTS, + NTFY_MAILTO_MAX_HEADERS, + &nmuenv.uri_log); + ntfy_mailto_uri_env_deinit(&nmuenv); + + if (parsed_uri == NULL) + return FALSE; + + mtctx->uri = parsed_uri; + *method_context = (void *) mtctx; + return TRUE; +} + +/* + * Action duplicates + */ + +static int ntfy_mailto_action_check_duplicates +(const struct sieve_enotify_env *nenv ATTR_UNUSED, + const struct sieve_enotify_action *nact, + const struct sieve_enotify_action *nact_other) +{ + struct ntfy_mailto_context *mtctx = + (struct ntfy_mailto_context *) nact->method_context; + struct ntfy_mailto_context *mtctx_other = + (struct ntfy_mailto_context *) nact_other->method_context; + const struct uri_mailto_recipient *new_rcpts, *old_rcpts; + unsigned int new_count, old_count, i, j; + unsigned int del_start = 0, del_len = 0; + + new_rcpts = array_get(&mtctx->uri->recipients, &new_count); + old_rcpts = array_get(&mtctx_other->uri->recipients, &old_count); + + for ( i = 0; i < new_count; i++ ) { + for ( j = 0; j < old_count; j++ ) { + if ( smtp_address_equals + (new_rcpts[i].address, old_rcpts[j].address) ) + break; + } + + if ( j == old_count ) { + /* Not duplicate */ + if ( del_len > 0 ) { + /* Perform pending deletion */ + array_delete(&mtctx->uri->recipients, del_start, del_len); + + /* Make sure the loop integrity is maintained */ + i -= del_len; + new_rcpts = array_get(&mtctx->uri->recipients, &new_count); + } + del_len = 0; + } else { + /* Mark deletion */ + if ( del_len == 0 ) + del_start = i; + del_len++; + } + } + + /* Perform pending deletion */ + if ( del_len > 0 ) { + array_delete(&mtctx->uri->recipients, del_start, del_len); + } + + return ( array_count(&mtctx->uri->recipients) > 0 ? 0 : 1 ); +} + +/* + * Action printing + */ + +static void ntfy_mailto_action_print +(const struct sieve_enotify_print_env *penv, + const struct sieve_enotify_action *nact) +{ + unsigned int count, i; + const struct uri_mailto_recipient *recipients; + const struct uri_mailto_header_field *headers; + struct ntfy_mailto_context *mtctx = + (struct ntfy_mailto_context *) nact->method_context; + + /* Print main method parameters */ + + sieve_enotify_method_printf + (penv, " => importance : %llu\n", + (unsigned long long)nact->importance); + + if ( nact->message != NULL ) + sieve_enotify_method_printf + (penv, " => subject : %s\n", nact->message); + else if ( mtctx->uri->subject != NULL ) + sieve_enotify_method_printf + (penv, " => subject : %s\n", mtctx->uri->subject); + + if ( nact->from != NULL ) + sieve_enotify_method_printf + (penv, " => from : %s\n", nact->from); + + /* Print mailto: recipients */ + + sieve_enotify_method_printf(penv, " => recipients :\n" ); + + recipients = array_get(&mtctx->uri->recipients, &count); + if ( count == 0 ) { + sieve_enotify_method_printf(penv, " NONE, action has no effect\n"); + } else { + for ( i = 0; i < count; i++ ) { + if ( recipients[i].carbon_copy ) + sieve_enotify_method_printf + (penv, " + Cc: %s\n", recipients[i].full); + else + sieve_enotify_method_printf + (penv, " + To: %s\n", recipients[i].full); + } + } + + /* Print accepted headers for notification message */ + + headers = array_get(&mtctx->uri->headers, &count); + if ( count > 0 ) { + sieve_enotify_method_printf(penv, " => headers :\n" ); + for ( i = 0; i < count; i++ ) { + sieve_enotify_method_printf(penv, " + %s: %s\n", + headers[i].name, headers[i].body); + } + } + + /* Print body for notification message */ + + if ( mtctx->uri->body != NULL ) + sieve_enotify_method_printf + (penv, " => body : \n--\n%s\n--\n", mtctx->uri->body); + + /* Finish output with an empty line */ + + sieve_enotify_method_printf(penv, "\n"); +} + +/* + * Action execution + */ + +static bool _contains_8bit(const char *msg) +{ + const unsigned char *s = (const unsigned char *)msg; + + for (; *s != '\0'; s++) { + if ((*s & 0x80) != 0) + return TRUE; + } + + return FALSE; +} + +static int ntfy_mailto_send +(const struct sieve_enotify_exec_env *nenv, + const struct sieve_enotify_action *nact, + const struct smtp_address *owner_email) +{ + struct sieve_instance *svinst = nenv->svinst; + const struct sieve_message_data *msgdata = nenv->msgdata; + const struct sieve_script_env *senv = nenv->scriptenv; + struct ntfy_mailto_context *mtctx = + (struct ntfy_mailto_context *) nact->method_context; + struct ntfy_mailto_config *mth_config = + (struct ntfy_mailto_config *)nenv->method->context; + struct sieve_address_source env_from = + mth_config->envelope_from; + const char *from = NULL; + const struct smtp_address *from_smtp = NULL; + const char *subject = mtctx->uri->subject; + const char *body = mtctx->uri->body; + string_t *to, *cc, *all; + const struct uri_mailto_recipient *recipients; + const struct uri_mailto_header_field *headers; + struct sieve_smtp_context *sctx; + struct ostream *output; + string_t *msg; + unsigned int count, i, hcount, h; + const char *outmsgid, *error; + int ret; + + /* Get recipients */ + recipients = array_get(&mtctx->uri->recipients, &count); + if ( count == 0 ) { + sieve_enotify_warning(nenv, + "notify mailto uri specifies no recipients; action has no effect"); + return 0; + } + + /* Just to be sure */ + if ( !sieve_smtp_available(senv) ) { + sieve_enotify_global_warning(nenv, + "notify mailto method has no means to send mail"); + return 0; + } + + /* Determine which sender to use + + From RFC 5436, Section 2.3: + + The ":from" tag overrides the default sender of the notification + message. "Sender", here, refers to the value used in the [RFC5322] + "From" header. Implementations MAY also use this value in the + [RFC5321] "MAIL FROM" command (the "envelope sender"), or they may + prefer to establish a mailbox that receives bounces from notification + messages. + */ + if ( (nenv->flags & SIEVE_EXECUTE_FLAG_NO_ENVELOPE) == 0 ) { + from_smtp = sieve_message_get_sender(nenv->msgctx); + if ( from_smtp == NULL ) { + /* "<>" */ + i_zero(&env_from); + env_from.type = SIEVE_ADDRESS_SOURCE_EXPLICIT; + } + } + from = nact->from; + if ( (ret=sieve_address_source_get_address(&env_from, svinst, + senv, nenv->msgctx, nenv->flags, &from_smtp)) < 0 ) { + from_smtp = NULL; + } else if ( ret == 0 ) { + if ( mtctx->from_address != NULL ) + from_smtp = mtctx->from_address; + else if ( svinst->user_email != NULL ) + from_smtp = svinst->user_email; + else { + from_smtp = sieve_get_postmaster_smtp(senv); + if (from == NULL) + from = sieve_get_postmaster_address(senv); + } + } + + /* Determine message from address */ + if ( from == NULL ) { + if ( from_smtp == NULL ) + from = sieve_get_postmaster_address(senv); + else { + from = t_strdup_printf("<%s>", + smtp_address_encode(from_smtp)); + } + } + + /* Determine subject */ + if ( nact->message != NULL ) { + /* FIXME: handle UTF-8 */ + subject = str_sanitize(nact->message, NTFY_MAILTO_MAX_SUBJECT); + } else if ( subject == NULL ) { + const char *const *hsubject; + + /* Fetch subject from original message */ + if ( mail_get_headers_utf8 + (msgdata->mail, "subject", &hsubject) > 0 ) + subject = str_sanitize(t_strdup_printf("Notification: %s", hsubject[0]), + NTFY_MAILTO_MAX_SUBJECT); + else + subject = "Notification: (no subject)"; + } + + /* Compose To and Cc headers */ + to = NULL; + cc = NULL; + all = t_str_new(256); + for ( i = 0; i < count; i++ ) { + if ( recipients[i].carbon_copy ) { + if ( cc == NULL ) { + cc = t_str_new(256); + str_append(cc, recipients[i].full); + } else { + str_append(cc, ", "); + str_append(cc, recipients[i].full); + } + } else { + if ( to == NULL ) { + to = t_str_new(256); + str_append(to, recipients[i].full); + } else { + str_append(to, ", "); + str_append(to, recipients[i].full); + } + } + if ( i < 3) { + if ( i > 0 ) + str_append(all, ", "); + str_append(all, + smtp_address_encode_path(recipients[i].address)); + } else if (i == 3) { + str_printfa(all, ", ... (%u total)", count); + } + } + + msg = t_str_new(512); + outmsgid = sieve_message_get_new_id(svinst); + + rfc2822_header_write(msg, "X-Sieve", SIEVE_IMPLEMENTATION); + rfc2822_header_write(msg, "Message-ID", outmsgid); + rfc2822_header_write(msg, "Date", message_date_create(ioloop_time)); + rfc2822_header_utf8_printf(msg, "Subject", "%s", subject); + + rfc2822_header_write_address(msg, "From", from); + + if ( to != NULL ) + rfc2822_header_write_address(msg, "To", str_c(to)); + if ( cc != NULL ) + rfc2822_header_write_address(msg, "Cc", str_c(cc)); + + rfc2822_header_printf(msg, "Auto-Submitted", + "auto-notified; owner-email=\"%s\"", + smtp_address_encode(owner_email)); + rfc2822_header_write(msg, "Precedence", "bulk"); + + /* Set importance */ + switch ( nact->importance ) { + case 1: + rfc2822_header_write(msg, "X-Priority", "1 (Highest)"); + rfc2822_header_write(msg, "Importance", "High"); + break; + case 3: + rfc2822_header_write(msg, "X-Priority", "5 (Lowest)"); + rfc2822_header_write(msg, "Importance", "Low"); + break; + case 2: + default: + rfc2822_header_write(msg, "X-Priority", "3 (Normal)"); + rfc2822_header_write(msg, "Importance", "Normal"); + break; + } + + /* Add custom headers */ + + headers = array_get(&mtctx->uri->headers, &hcount); + for ( h = 0; h < hcount; h++ ) { + const char *name = rfc2822_header_field_name_sanitize(headers[h].name); + + rfc2822_header_write(msg, name, headers[h].body); + } + + /* Generate message body */ + + rfc2822_header_write(msg, "MIME-Version", "1.0"); + if ( body != NULL ) { + if (_contains_8bit(body)) { + rfc2822_header_write + (msg, "Content-Type", "text/plain; charset=utf-8"); + rfc2822_header_write(msg, "Content-Transfer-Encoding", "8bit"); + } else { + rfc2822_header_write + (msg, "Content-Type", "text/plain; charset=us-ascii"); + rfc2822_header_write(msg, "Content-Transfer-Encoding", "7bit"); + } + str_printfa(msg, "\r\n%s\r\n", body); + + } else { + rfc2822_header_write + (msg, "Content-Type", "text/plain; charset=US-ASCII"); + rfc2822_header_write(msg, "Content-Transfer-Encoding", "7bit"); + + str_append(msg, "\r\nNotification of new message.\r\n"); + } + + sctx = sieve_smtp_start(senv, from_smtp); + + /* Send message to all recipients */ + for ( i = 0; i < count; i++ ) + sieve_smtp_add_rcpt(sctx, recipients[i].address); + + output = sieve_smtp_send(sctx); + o_stream_nsend(output, str_data(msg), str_len(msg)); + + if ( (ret=sieve_smtp_finish(sctx, &error)) <= 0 ) { + if (ret < 0) { + sieve_enotify_global_error(nenv, + "failed to send mail notification to %s: %s (temporary failure)", + str_c(all), str_sanitize(error, 512)); + } else { + sieve_enotify_global_log_error(nenv, + "failed to send mail notification to %s: %s (permanent failure)", + str_c(all), str_sanitize(error, 512)); + } + } else { + struct event_passthrough *e = + sieve_enotify_create_finish_event(nenv)-> + add_str("notify_target", str_c(all)); + + sieve_enotify_event_log(nenv, e->event(), + "sent mail notification to %s", + str_c(all)); + } + + return 0; +} + +static int ntfy_mailto_action_execute +(const struct sieve_enotify_exec_env *nenv, + const struct sieve_enotify_action *nact) +{ + struct sieve_instance *svinst = nenv->svinst; + const struct sieve_script_env *senv = nenv->scriptenv; + struct mail *mail = nenv->msgdata->mail; + const struct smtp_address *owner_email; + const char *const *hdsp; + int ret; + + owner_email = svinst->user_email; + if ( owner_email == NULL && + (nenv->flags & SIEVE_EXECUTE_FLAG_NO_ENVELOPE) == 0 ) + owner_email = sieve_message_get_final_recipient(nenv->msgctx); + if ( owner_email == NULL ) { + owner_email = sieve_get_postmaster_smtp(senv); + } + i_assert( owner_email != NULL ); + + /* Is the message an automatic reply ? */ + if ( (ret=mail_get_headers(mail, "auto-submitted", &hdsp)) < 0 ) { + sieve_enotify_critical(nenv, + "mailto notification: " + "failed to read `auto-submitted' header field", + "mailto notification: " + "failed to read `auto-submitted' header field: %s", + mailbox_get_last_internal_error(mail->box, NULL)); + return -1; + } + + /* Theoretically multiple headers could exist, so lets make sure */ + if ( ret > 0 ) { + while ( *hdsp != NULL ) { + if ( strcasecmp(*hdsp, "no") != 0 ) { + const struct smtp_address *sender = NULL; + const char *from; + + if ( (nenv->flags & SIEVE_EXECUTE_FLAG_NO_ENVELOPE) == 0 ) + sender = sieve_message_get_sender(nenv->msgctx); + from = (sender == NULL ? "" : t_strdup_printf + (" from <%s>", smtp_address_encode(sender))); + + sieve_enotify_global_info(nenv, + "not sending notification " + "for auto-submitted message%s", from); + return 0; + } + hdsp++; + } + } + + T_BEGIN { + ret = ntfy_mailto_send(nenv, nact, owner_email); + } T_END; + + return ret; +} + + + + |