diff --git a/Makefile.am b/Makefile.am index a761fde6a42fe6314cc84e58207acadb6a872d52..832e2806ed2ffdaa06c9e7c4f711ab33eeea0079 100644 --- a/Makefile.am +++ b/Makefile.am @@ -24,6 +24,7 @@ if BUILD_ENOTIFY ENOTIFY_TESTS = \ tests/extensions/enotify/basic.svtest \ tests/extensions/enotify/encodeurl.svtest \ + tests/extensions/enotify/errors.svtest \ tests/extensions/enotify/execute.svtest endif @@ -77,6 +78,7 @@ test_cases = \ tests/extensions/subaddress/rfc.svtest \ tests/extensions/vacation/errors.svtest \ tests/extensions/vacation/execute.svtest \ + tests/extensions/vacation/references.svtest \ $(ENOTIFY_TESTS) if HAVE_DOVECOT_LIBS diff --git a/TODO b/TODO index b6b343cff3c5140f3026566ddffcf49716ed7ee8..d7e3b3b3812c0dacf7ef0ce977a42bec9766f279 100644 --- a/TODO +++ b/TODO @@ -35,8 +35,6 @@ Next (in order of descending priority/precedence): any vacation response that is generated. UTF-8 characters can be used in the string argument; implementations MUST convert the string to [RFC2047] encoded words if and only if non-ASCII characters are present. - - Vacation: properly implement construction of a References header from - the original message. - Body: contains various issues that need to be resolved for standards compliance. Body test support currently matches but barely exceeds the original CMU Sieve implentation in terms of standards compliance. diff --git a/configure.in b/configure.in index 225094d01cc1a9d2a2c444180b71fa1f43682ce3..57bcea0342920e4fb926bdb86b305cc59ccc5d69 100644 --- a/configure.in +++ b/configure.in @@ -7,8 +7,10 @@ AC_CONFIG_SRCDIR([src]) # real config header ourselves. AC_CONFIG_HEADERS([dummy-config.h dsieve-config.h]) -AC_DEFINE(SIEVE_NAME, [PACKAGE_NAME], [Define to the full name of this Sieve implementation.]) -AC_DEFINE(SIEVE_VERSION, [PACKAGE_VERSION], [Define to the version of this Sieve implementation.]) +AC_DEFINE_UNQUOTED(SIEVE_NAME, "$PACKAGE_NAME", + [Define to the full name of this Sieve implementation.]) +AC_DEFINE_UNQUOTED(SIEVE_VERSION, "$PACKAGE_VERSION", + [Define to the version of this Sieve implementation.]) AM_INIT_AUTOMAKE(no-define) diff --git a/src/lib-sieve/cmd-redirect.c b/src/lib-sieve/cmd-redirect.c index b5dc36cf12224485d55896b9df431d8f8f76ab31..1d1a63ffac3525c006b77e019869f1a8a2bec394 100644 --- a/src/lib-sieve/cmd-redirect.c +++ b/src/lib-sieve/cmd-redirect.c @@ -7,6 +7,8 @@ #include "istream.h" #include "istream-header-filter.h" +#include "rfc2822.h" + #include "sieve-common.h" #include "sieve-limits.h" #include "sieve-address.h" @@ -267,7 +269,7 @@ static void act_redirect_print static bool act_redirect_send (const struct sieve_action_exec_env *aenv, struct act_redirect_context *ctx) { - static const char *hide_headers[] = { "Return-Path" }; + static const char *hide_headers[] = { "Return-Path", "X-Sieve" }; const struct sieve_message_data *msgdata = aenv->msgdata; const struct sieve_script_env *senv = aenv->scriptenv; @@ -295,6 +297,9 @@ static bool act_redirect_send (input, HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR, hide_headers, N_ELEMENTS(hide_headers), null_header_filter_callback, NULL); + /* Prepend sieve version header (should not affect signatures) */ + rfc2822_header_field_write(f, "X-Sieve", SIEVE_IMPLEMENTATION); + /* Pipe the message to the outgoing SMTP transport */ while ((ret = i_stream_read_data(input, &data, &size, 0)) > 0) { if (fwrite(data, size, 1, f) == 0) diff --git a/src/lib-sieve/ext-reject.c b/src/lib-sieve/ext-reject.c index 15d0aef2ea3bdf45c7b386efcba87434b2d4596b..6bac6c003907158b4ce3dacb6c77f91c2d009d69 100644 --- a/src/lib-sieve/ext-reject.c +++ b/src/lib-sieve/ext-reject.c @@ -20,6 +20,8 @@ #include "istream.h" #include "istream-header-filter.h" +#include "rfc2822.h" + #include "sieve-common.h" #include "sieve-extensions.h" #include "sieve-commands.h" @@ -125,7 +127,7 @@ static int act_reject_check_duplicate const char *location1, const char *location2); int act_reject_check_conflict (const struct sieve_runtime_env *renv, const struct sieve_action *action, - const struct sieve_action *other_action, void *context, + const struct sieve_action *other_action, void *context, const char *location1, const char *location2); static void act_reject_print (const struct sieve_action *action, const struct sieve_result_print_env *rpenv, @@ -176,10 +178,10 @@ static bool cmd_reject_generate sieve_operation_emit_code(cgenv->sbin, &reject_operation); /* Emit line number */ - sieve_code_source_line_emit(cgenv->sbin, sieve_command_source_line(ctx)); + sieve_code_source_line_emit(cgenv->sbin, sieve_command_source_line(ctx)); /* Generate arguments */ - return sieve_generate_arguments(cgenv, ctx, NULL); + return sieve_generate_arguments(cgenv, ctx, NULL); } /* @@ -194,11 +196,11 @@ static bool ext_reject_operation_dump sieve_code_descend(denv); /* Source line */ - if ( !sieve_code_source_line_dump(denv, address) ) - return FALSE; + if ( !sieve_code_source_line_dump(denv, address) ) + return FALSE; if ( !sieve_code_dumper_print_optional_operands(denv, address) ) - return FALSE; + return FALSE; return sieve_opr_string_dump(denv, address, "reason"); @@ -220,9 +222,9 @@ static int ext_reject_operation_execute int ret; /* Source line */ - if ( !sieve_code_source_line_read(renv, address, &source_line) ) { + if ( !sieve_code_source_line_read(renv, address, &source_line) ) { sieve_runtime_trace_error(renv, "invalid source line"); - return SIEVE_EXEC_BIN_CORRUPT; + return SIEVE_EXEC_BIN_CORRUPT; } /* Optional operands (side effects) */ @@ -328,18 +330,21 @@ static bool act_reject_send new_msgid = sieve_get_new_message_id(senv); boundary = t_strdup_printf("%s/%s", my_pid, senv->hostname); - fprintf(f, "Message-ID: %s\r\n", new_msgid); - fprintf(f, "Date: %s\r\n", message_date_create(ioloop_time)); - fprintf(f, "From: Mail Delivery Subsystem <%s>\r\n", + rfc2822_header_field_write(f, "X-Sieve", SIEVE_IMPLEMENTATION); + rfc2822_header_field_write(f, "Message-ID", new_msgid); + rfc2822_header_field_write(f, "Date", message_date_create(ioloop_time)); + rfc2822_header_field_printf(f, "From", "Mail Delivery Subsystem <%s>", senv->postmaster_address); - fprintf(f, "To: <%s>\r\n", msgdata->return_path); - fprintf(f, "MIME-Version: 1.0\r\n"); - fprintf(f, "Content-Type: " - "multipart/report; report-type=disposition-notification;\r\n" - "\tboundary=\"%s\"\r\n", boundary); - fprintf(f, "Subject: Automatically rejected mail\r\n"); - fprintf(f, "Auto-Submitted: auto-replied (rejected)\r\n"); - fprintf(f, "Precedence: bulk\r\n"); + rfc2822_header_field_printf(f, "To", "<%s>", msgdata->return_path); + rfc2822_header_field_write(f, "Subject", "Automatically rejected mail"); + rfc2822_header_field_write(f, "Auto-Submitted", "auto-replied (rejected)"); + rfc2822_header_field_write(f, "Precedence", "bulk"); + + rfc2822_header_field_write(f, "MIME-Version", "1.0"); + rfc2822_header_field_printf(f, "Content-Type", + "multipart/report; report-type=disposition-notification;\n" + "boundary=\"%s\"", boundary); + fprintf(f, "\r\nThis is a MIME-encapsulated message\r\n\r\n"); /* Human readable status report */ diff --git a/src/lib-sieve/plugins/enotify/cmd-notify.c b/src/lib-sieve/plugins/enotify/cmd-notify.c index ed38e46ebac1358014ffb6d900bcdfb18ec87f4a..89ac8c6b14d3002eb8d160e12e0dd248400c061e 100644 --- a/src/lib-sieve/plugins/enotify/cmd-notify.c +++ b/src/lib-sieve/plugins/enotify/cmd-notify.c @@ -160,17 +160,6 @@ const struct sieve_action act_notify = { NULL }; -/* Action context information */ - -struct act_notify_context { - const char *method; - - sieve_number_t importance; - const char *message; - const char *from; - const char *const *options; -}; - /* * Tag validation */ @@ -388,12 +377,14 @@ static int cmd_notify_operation_execute const struct sieve_runtime_env *renv, sieve_size_t *address) { struct sieve_side_effects_list *slist = NULL; - struct act_notify_context *act; + struct sieve_enotify_context *act; + void *method_context; pool_t pool; int opt_code = 1; sieve_number_t importance = 1; struct sieve_coded_stringlist *options = NULL; - string_t *method, *message = NULL, *from = NULL; + const struct sieve_enotify_method *method; + string_t *method_uri, *message = NULL, *from = NULL; unsigned int source_line; /* @@ -456,7 +447,7 @@ static int cmd_notify_operation_execute } /* Reason operand */ - if ( !sieve_opr_string_read(renv, address, &method) ) { + if ( !sieve_opr_string_read(renv, address, &method_uri) ) { sieve_runtime_trace_error(renv, "invalid method operand"); return SIEVE_EXEC_BIN_CORRUPT; } @@ -467,21 +458,31 @@ static int cmd_notify_operation_execute sieve_runtime_trace(renv, "NOTIFY action"); - /* Add notify action to the result */ - - pool = sieve_result_pool(renv->result); - act = p_new(pool, struct act_notify_context, 1); - act->method = p_strdup(pool, str_c(method)); - act->importance = importance; - if ( message != NULL ) - act->message = p_strdup(pool, str_c(message)); - if ( from != NULL ) - act->from = p_strdup(pool, str_c(from)); - if ( options != NULL ) - sieve_coded_stringlist_read_all(options, pool, &(act->options)); + /* Check operands */ + + if ( (method=ext_enotify_runtime_check_operands + (renv, source_line, str_c(method_uri), + message == NULL ? NULL : str_c(message), + from == NULL ? NULL : str_c(from), + &method_context)) != NULL ) { + /* Add notify action to the result */ + + pool = sieve_result_pool(renv->result); + act = p_new(pool, struct sieve_enotify_context, 1); + act->method = method; + act->method_context = method_context; + act->importance = importance; + if ( message != NULL ) + act->message = p_strdup(pool, str_c(message)); + if ( from != NULL ) + act->from = p_strdup(pool, str_c(from)); - return ( sieve_result_add_action - (renv, &act_notify, slist, source_line, (void *) act, 0) >= 0 ); + return ( sieve_result_add_action + (renv, &act_notify, slist, source_line, (void *) act, 0) >= 0 ); + } + + /* Erroneous notify action is no reason to kill the script */ + return SIEVE_EXEC_OK; } /* @@ -507,17 +508,14 @@ static void act_notify_print const struct sieve_result_print_env *rpenv, void *context, bool *keep ATTR_UNUSED) { - struct act_notify_context *ctx = (struct act_notify_context *) context; - - sieve_result_action_printf( rpenv, "send notification with method %s:", - ctx->method); - sieve_result_printf(rpenv, " => importance : %d\n", ctx->importance); - if ( ctx->message != NULL ) - sieve_result_printf(rpenv, " => message: \n%s\n", ctx->message); - if ( ctx->from != NULL ) - sieve_result_printf(rpenv, " => from : %s\n", ctx->from); + const struct sieve_enotify_context *nctx = + (const struct sieve_enotify_context *) context; + + sieve_result_action_printf + ( rpenv, "send notification with method '%s:':", nctx->method->identifier); - /* FIXME: list options */ + if ( nctx->method->action_print != NULL ) + nctx->method->action_print(rpenv, nctx); } /* Result execution */ @@ -527,7 +525,13 @@ static bool act_notify_commit const struct sieve_action_exec_env *aenv, void *tr_context, bool *keep ATTR_UNUSED) { - return FALSE; + const struct sieve_enotify_context *nctx = + (const struct sieve_enotify_context *) tr_context; + + if ( nctx->method->action_execute != NULL ) + return nctx->method->action_execute(aenv, nctx); + + return TRUE; } diff --git a/src/lib-sieve/plugins/enotify/ext-enotify-common.c b/src/lib-sieve/plugins/enotify/ext-enotify-common.c index edf5b5895d67a72b8cc05eb57c0d904b09c823fa..d991e21d3a3de9f3e68307fba5c03bbb55c1fc67 100644 --- a/src/lib-sieve/plugins/enotify/ext-enotify-common.c +++ b/src/lib-sieve/plugins/enotify/ext-enotify-common.c @@ -10,6 +10,7 @@ #include "sieve-ast.h" #include "sieve-commands.h" #include "sieve-validator.h" +#include "sieve-interpreter.h" #include "ext-enotify-limits.h" #include "ext-enotify-common.h" @@ -156,3 +157,38 @@ bool ext_enotify_uri_validate return TRUE; } +const struct sieve_enotify_method *ext_enotify_runtime_check_operands +(const struct sieve_runtime_env *renv, unsigned int source_line, + const char *method_uri, const char *message, const char *from, void **context) +{ + const char *uri = method_uri; + const char *scheme; + const struct sieve_enotify_method *method; + + if ( (scheme=ext_enotify_uri_scheme_parse(&uri)) == NULL ) { + sieve_runtime_error + (renv, sieve_error_script_location(renv->script, source_line), + "invalid scheme part for method URI '%s'", + str_sanitize(method_uri, 80)); + return NULL; + } + + if ( (method=ext_enotify_method_find(scheme)) == NULL ) { + sieve_runtime_error + (renv, sieve_error_script_location(renv->script, source_line), + "invalid notify method '%s'", scheme); + return NULL; + } + + if ( method->runtime_check_operands != NULL ) { + if ( method->runtime_check_operands + (renv, source_line, method_uri, uri, message, from, context) ) + return method; + + return NULL; + } + + *context = NULL; + return method; +} + diff --git a/src/lib-sieve/plugins/enotify/ext-enotify-common.h b/src/lib-sieve/plugins/enotify/ext-enotify-common.h index 8353188b41197cd954dd3b3115c5f22158510ebb..0ec6b3d1997e8c837c1c2f5c737c4fe7126f74bb 100644 --- a/src/lib-sieve/plugins/enotify/ext-enotify-common.h +++ b/src/lib-sieve/plugins/enotify/ext-enotify-common.h @@ -67,10 +67,19 @@ const struct sieve_enotify_method *ext_enotify_method_find (const char *identifier); /* - * URI validation + * Validation */ bool ext_enotify_uri_validate (struct sieve_validator *valdtr, struct sieve_ast_argument *arg); +/* + * Runtime + */ + +const struct sieve_enotify_method *ext_enotify_runtime_check_operands + (const struct sieve_runtime_env *renv, unsigned int source_line, + const char *method_uri, const char *message, const char *from, + void **context); + #endif /* __EXT_ENOTIFY_COMMON_H */ diff --git a/src/lib-sieve/plugins/enotify/ntfy-mailto.c b/src/lib-sieve/plugins/enotify/ntfy-mailto.c index 7ce3873b56a9157c0aa56580b92e665140ba9f34..07df45a5145fcf4df4694722b7593420ef857cef 100644 --- a/src/lib-sieve/plugins/enotify/ntfy-mailto.c +++ b/src/lib-sieve/plugins/enotify/ntfy-mailto.c @@ -14,6 +14,7 @@ #include "sieve-ast.h" #include "sieve-commands.h" #include "sieve-validator.h" +#include "sieve-interpreter.h" #include "sieve-actions.h" #include "sieve-result.h" @@ -24,6 +25,19 @@ */ #define NTFY_MAILTO_MAX_RECIPIENTS 4 +#define NTFY_MAILTO_MAX_HEADERS 16 + +/* + * Types + */ + +struct ntfy_mailto_header_field { + const char *name; + const char *body; +}; + +ARRAY_DEFINE_TYPE(recipients, const char *); +ARRAY_DEFINE_TYPE(headers, struct ntfy_mailto_header_field); /* * Mailto notification method @@ -32,16 +46,62 @@ static bool ntfy_mailto_validate_uri (struct sieve_validator *valdtr, struct sieve_ast_argument *arg, const char *uri_body); -static bool ntfy_mailto_execute +static bool ntfy_mailto_runtime_check_operands + (const struct sieve_runtime_env *renv, unsigned int source_line, + const char *uri, const char *uri_body, const char *message, + const char *from, void **context); +static void ntfy_mailto_action_print + (const struct sieve_result_print_env *rpenv, + const struct sieve_enotify_context *nctx); +static bool ntfy_mailto_action_execute (const struct sieve_action_exec_env *aenv, const struct sieve_enotify_context *nctx); const struct sieve_enotify_method mailto_notify = { "mailto", ntfy_mailto_validate_uri, - ntfy_mailto_execute + ntfy_mailto_runtime_check_operands, + ntfy_mailto_action_print, + ntfy_mailto_action_execute +}; + +/* + * Method context data + */ + +struct ntfy_mailto_context { + ARRAY_TYPE(recipients) recipients; + ARRAY_TYPE(headers) headers; + const char *subject; + const char *body; }; +/* + * Reserved headers + */ + +static const char *_reserved_headers[] = { + "auto-submitted", + "received", + "message-id", + "data", + NULL +}; + +static inline bool _ntfy_mailto_header_allowed(const char *field_name) +{ + const char **rhdr = _reserved_headers; + + /* Check whether it is reserved */ + while ( *rhdr != NULL ) { + if ( strcasecmp(field_name, *rhdr) == 0 ) + return FALSE; + rhdr++; + } + + return TRUE; +} + /* * Mailto URI parsing */ @@ -49,7 +109,7 @@ const struct sieve_enotify_method mailto_notify = { /* FIXME: much of this implementation will be common to other URI schemes. This * should be merged into a common implementation. */ - + static inline int _decode_hex_digit(char digit) { switch ( digit ) { @@ -61,7 +121,7 @@ static inline int _decode_hex_digit(char digit) return (int) digit - 'a' + 0x0a; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': - return (int) digit - 'a' + 0x0A; + return (int) digit - 'A' + 0x0A; } return -1; @@ -85,17 +145,24 @@ static bool _parse_hex_value(const char **in, char *out) return (*out != '\0'); } +static inline pool_t array_get_pool_i(struct array *array) +{ + return buffer_get_pool(array->buffer); +} +#define array_get_pool(array) \ + array_get_pool_i(&(array)->arr) + static bool _uri_parse_recipients -(const char **uri_p, const char *const **recipients_r, const char **error_r) +(const char **uri_p, ARRAY_TYPE(recipients) *recipients_r, const char **error_r) { - ARRAY_DEFINE(recipients, const char *); string_t *to = t_str_new(128); const char *recipient; const char *p = *uri_p; + pool_t pool = NULL; if ( recipients_r != NULL ) - t_array_init(&recipients, NTFY_MAILTO_MAX_RECIPIENTS); - + pool = array_get_pool(recipients_r); + while ( *p != '\0' && *p != '?' ) { if ( *p == '%' ) { /* % encoded character */ @@ -114,11 +181,13 @@ static bool _uri_parse_recipients recipient = str_c(to); /* Verify recipient */ + + // FIXME .... /* Add recipient to the list */ if ( recipients_r != NULL ) { - recipient = t_strdup(recipient); - array_append(&recipients, &recipient, 1); + recipient = p_strdup(pool, recipient); + array_append(recipients_r, &recipient, 1); } /* Reset for next recipient */ @@ -141,40 +210,34 @@ static bool _uri_parse_recipients /* Verify recipient */ - // .... + // FIXME .... if ( recipients_r != NULL ) { /* Add recipient to the list */ - recipient = t_strdup(recipient); - array_append(&recipients, &recipient, 1); - - /* Return recipients */ - (void)array_append_space(&recipients); - *recipients_r = array_idx(&recipients, 0); + recipient = p_strdup(pool, recipient); + array_append(recipients_r, &recipient, 1); } *uri_p = p; return TRUE; } -struct _header_field { - const char *name; - const char *body; -}; - static bool _uri_parse_headers -(const char **uri_p, struct _header_field *const *headers_r, - const char **error_r) +(const char **uri_p, ARRAY_TYPE(headers) *headers_r, const char **body, + const char **subject, const char **error_r) { - ARRAY_DEFINE(headers, struct _header_field); string_t *field = t_str_new(128); const char *p = *uri_p; + pool_t pool = NULL; if ( headers_r != NULL ) - t_array_init(&headers, NTFY_MAILTO_MAX_RECIPIENTS); - + pool = array_get_pool(headers_r); + while ( *p != '\0' ) { - struct _header_field *hdrf; + enum { _HNAME_GENERIC, _HNAME_SUBJECT, _HNAME_BODY } hname_type = + _HNAME_GENERIC; + struct ntfy_mailto_header_field *hdrf = NULL; + const char *field_name; /* Parse field name */ while ( *p != '\0' && *p != '=' ) { @@ -184,6 +247,7 @@ static bool _uri_parse_headers if ( ch == '%' ) { /* Encoded, parse 2-digit hex value */ if ( !_parse_hex_value(&p, &ch) ) { + printf("F: %s\n", p); *error_r = "invalid % encoding"; return FALSE; } @@ -192,11 +256,28 @@ static bool _uri_parse_headers } if ( *p != '\0' ) p++; - if ( headers_r != NULL ) { - hdrf = array_append_space(&headers); - hdrf->name = t_strdup(str_c(field)); + /* Verify field name */ + if ( !rfc2822_header_field_name_verify(str_c(field), str_len(field)) ) { + *error_r = "invalid header field name"; + return FALSE; + } + + /* Add new header field to array and assign its name */ + field_name = str_c(field); + if ( strcasecmp(field_name, "subject") == 0 ) + hname_type = _HNAME_SUBJECT; + else if ( strcasecmp(field_name, "body") == 0 ) + hname_type = _HNAME_BODY; + else if ( _ntfy_mailto_header_allowed(field_name) ) { + if ( headers_r != NULL ) { + hdrf = array_append_space(headers_r); + hdrf->name = p_strdup(pool, field_name); + } + } else { + hdrf = NULL; } + /* Reset for field body */ str_truncate(field, 0); /* Parse field body */ @@ -207,6 +288,7 @@ static bool _uri_parse_headers if ( ch == '%' ) { /* Encoded, parse 2-digit hex value */ if ( !_parse_hex_value(&p, &ch) ) { + printf("F: %s\n", p); *error_r = "invalid % encoding"; return FALSE; } @@ -215,10 +297,31 @@ static bool _uri_parse_headers } if ( *p != '\0' ) p++; + /* Verify field body */ + + // FIXME .... + + /* Assign field body */ + if ( headers_r != NULL ) { - hdrf->body = t_strdup(str_c(field)); - str_truncate(field, 0); + switch ( hname_type ) { + case _HNAME_SUBJECT: + if ( subject != NULL ) + *subject = p_strdup(pool, str_c(field)); + break; + case _HNAME_BODY: + if ( subject != NULL ) + *body = p_strdup(pool, str_c(field)); + break; + case _HNAME_GENERIC: + if ( hdrf != NULL ) + hdrf->body = p_strdup(pool, str_c(field)); + break; + } } + + /* Reset for next name */ + str_truncate(field, 0); } /* Skip '&' */ @@ -229,8 +332,9 @@ static bool _uri_parse_headers } static bool ntfy_mailto_parse_uri -(const char *uri_body, const char *const **recipients_r, - struct _header_field *const *headers_r, const char **error_r) +(const char *uri_body, ARRAY_TYPE(recipients) *recipients_r, + ARRAY_TYPE(headers) *headers_r, const char **body, const char **subject, + const char **error_r) { const char *p = uri_body; @@ -260,7 +364,7 @@ static bool ntfy_mailto_parse_uri while ( *p != '\0' ) { /* Extract hfield item by searching for '&' and decoding '%' items */ - if ( !_uri_parse_headers(&p, headers_r, error_r) ) + if ( !_uri_parse_headers(&p, headers_r, body, subject, error_r) ) return FALSE; } @@ -277,7 +381,7 @@ static bool ntfy_mailto_validate_uri { const char *error; - if ( !ntfy_mailto_parse_uri(uri_body, NULL, NULL, &error) ) { + if ( !ntfy_mailto_parse_uri(uri_body, NULL, NULL, NULL, NULL, &error) ) { sieve_argument_validate_error(valdtr, arg, "invalid mailto URI '%s': %s", str_sanitize(sieve_ast_argument_strc(arg), 80), error); @@ -288,7 +392,81 @@ static bool ntfy_mailto_validate_uri } /* - * Execution + * Runtime + */ + +static bool ntfy_mailto_runtime_check_operands +(const struct sieve_runtime_env *renv, unsigned int source_line, + const char *mailto_uri, const char *uri_body, + const char *message ATTR_UNUSED, const char *from ATTR_UNUSED, + void **context) +{ + pool_t pool; + struct ntfy_mailto_context *nctx; + const char *error; + + /* Need to create context before validation to have arrays present */ + pool = sieve_result_pool(renv->result); + nctx = p_new(pool, struct ntfy_mailto_context, 1); + p_array_init(&nctx->recipients, pool, NTFY_MAILTO_MAX_RECIPIENTS); + p_array_init(&nctx->headers, pool, NTFY_MAILTO_MAX_HEADERS); + + if ( !ntfy_mailto_parse_uri + (uri_body, &nctx->recipients, &nctx->headers, &nctx->body, &nctx->subject, + &error) ) { + sieve_runtime_error + (renv, sieve_error_script_location(renv->script, source_line), + "invalid mailto URI '%s': %s", str_sanitize(mailto_uri, 80), error); + return FALSE; + } + + *context = (void *) nctx; + + return TRUE; +} + +/* + * Action printing + */ + +static void ntfy_mailto_action_print +(const struct sieve_result_print_env *rpenv, + const struct sieve_enotify_context *nctx) +{ + unsigned int count, i; + const char *const *recipients; + const struct ntfy_mailto_header_field *headers; + struct ntfy_mailto_context *mtctx = + (struct ntfy_mailto_context *) nctx->method_context; + + sieve_result_printf(rpenv, " => importance : %d\n", nctx->importance); + if ( nctx->message != NULL ) + sieve_result_printf(rpenv, " => subject : %s\n", nctx->message); + else if ( mtctx->subject != NULL ) + sieve_result_printf(rpenv, " => subject : %s\n", mtctx->subject); + if ( nctx->from != NULL ) + sieve_result_printf(rpenv, " => from : %s\n", nctx->from); + + sieve_result_printf(rpenv, " => recipients :\n" ); + recipients = array_get(&mtctx->recipients, &count); + for ( i = 0; i < count; i++ ) { + sieve_result_printf(rpenv, " + %s\n", recipients[i]); + } + + sieve_result_printf(rpenv, " => headers :\n" ); + headers = array_get(&mtctx->headers, &count); + for ( i = 0; i < count; i++ ) { + sieve_result_printf(rpenv, " + %s: %s\n", + headers[i].name, headers[i].body); + } + + if ( mtctx->body != NULL ) + sieve_result_printf(rpenv, " => body : \n--\n%s\n--\n\n", + mtctx->body); +} + +/* + * Action execution */ static bool _contains_8bit(const char *msg) @@ -302,18 +480,24 @@ static bool _contains_8bit(const char *msg) return FALSE; } - -static bool ntfy_mailto_execute + +static bool ntfy_mailto_action_execute (const struct sieve_action_exec_env *aenv, const struct sieve_enotify_context *nctx) { const struct sieve_message_data *msgdata = aenv->msgdata; const struct sieve_script_env *senv = aenv->scriptenv; + struct ntfy_mailto_context *mtctx = + (struct ntfy_mailto_context *) nctx->method_context; + const char *from = NULL; + const char *subject = mtctx->subject; + const char *body = mtctx->body; + const char *const *recipients; void *smtp_handle; + unsigned int count, i; FILE *f; const char *outmsgid; - const char *recipient = "BOGUS"; - + /* Just to be sure */ if ( senv->smtp_open == NULL || senv->smtp_close == NULL ) { sieve_result_warning(aenv, @@ -321,52 +505,106 @@ static bool ntfy_mailto_execute return FALSE; } - smtp_handle = senv->smtp_open(msgdata->return_path, NULL, &f); - outmsgid = sieve_get_new_message_id(senv); - - fprintf(f, "Message-ID: %s\r\n", outmsgid); - fprintf(f, "Date: %s\r\n", message_date_create(ioloop_time)); - fprintf(f, "X-Sieve: %s\r\n", SIEVE_IMPLEMENTATION); - - switch ( nctx->importance ) { - case 1: - fprintf(f, "X-Priority: 1 (Highest)\r\n"); - fprintf(f, "Importance: High\r\n"); - break; - case 3: - fprintf(f, "X-Priority: 5 (Lowest)\r\n"); - fprintf(f, "Importance: Low\r\n"); - break; - case 2: - default: - fprintf(f, "X-Priority: 3 (Normal)\r\n"); - fprintf(f, "Importance: Normal\r\n"); - break; + /* Determine from address */ + if ( msgdata->return_path != NULL ) { + if ( nctx->from == NULL ) + from = senv->postmaster_address; + else + /* FIXME: validate */ + from = nctx->from; } - - fprintf(f, "From: Postmaster <%s>\r\n", senv->postmaster_address); - fprintf(f, "To: <%s>\r\n", recipient); - fprintf(f, "Subject: [SIEVE] New mail notification\r\n"); - fprintf(f, "Auto-Submitted: auto-generated (notify)\r\n"); - fprintf(f, "Precedence: bulk\r\n"); - if (_contains_8bit(nctx->message)) { - fprintf(f, "MIME-Version: 1.0\r\n"); - fprintf(f, "Content-Type: text/plain; charset=UTF-8\r\n"); - fprintf(f, "Content-Transfer-Encoding: 8bit\r\n"); + /* Determine subject */ + if ( nctx->message != NULL ) { + /* FIXME: handle UTF-8 */ + subject = str_sanitize(nctx->message, 256); + } else if ( subject == NULL ) { + const char *const *hsubject; + + /* Fetch subject from original message */ + if ( mail_get_headers_utf8 + (aenv->msgdata->mail, "subject", &hsubject) >= 0 ) + subject = t_strdup_printf("Notification: %s", hsubject[0]); + else + subject = "Notification: (no subject)"; } - fprintf(f, "\r\n"); - fprintf(f, "%s\r\n", nctx->message); + + /* Send message to all recipients */ + recipients = array_get(&mtctx->recipients, &count); + for ( i = 0; i < count; i++ ) { + const struct ntfy_mailto_header_field *headers; + unsigned int h, hcount; + + smtp_handle = senv->smtp_open(recipients[i], from, &f); + outmsgid = sieve_get_new_message_id(senv); + + rfc2822_header_field_write(f, "X-Sieve", SIEVE_IMPLEMENTATION); + rfc2822_header_field_write(f, "Message-ID", outmsgid); + rfc2822_header_field_write(f, "Date", message_date_create(ioloop_time)); + rfc2822_header_field_printf(f, "From", "<%s>", from); + rfc2822_header_field_printf(f, "To", "<%s>", recipients[i]); + rfc2822_header_field_write(f, "Subject", subject); + + rfc2822_header_field_write(f, "Auto-Submitted", "auto-notified"); + rfc2822_header_field_write(f, "Precedence", "bulk"); + + /* Set importance */ + switch ( nctx->importance ) { + case 1: + rfc2822_header_field_write(f, "X-Priority", "1 (Highest)"); + rfc2822_header_field_write(f, "Importance", "High"); + break; + case 3: + rfc2822_header_field_write(f, "X-Priority", "5 (Lowest)"); + rfc2822_header_field_write(f, "Importance", "Low"); + break; + case 2: + default: + rfc2822_header_field_write(f, "X-Priority", "3 (Normal)"); + rfc2822_header_field_write(f, "Importance", "Normal"); + break; + } + + /* Add custom headers */ + + /* FIXME: ignore from and auto-submitted and recognize body, subject, to and + * cc. + */ + headers = array_get(&mtctx->headers, &hcount); + for ( h = 0; h < hcount; h++ ) { + const char *name = rfc2822_header_field_name_sanitize(headers[h].name); + + rfc2822_header_field_write(f, name, headers[h].body); + } + + /* Generate message body */ + if ( body != NULL ) { + if (_contains_8bit(body)) { + rfc2822_header_field_write(f, "MIME-Version", "1.0"); + rfc2822_header_field_write + (f, "Content-Type", "text/plain; charset=UTF-8"); + rfc2822_header_field_write(f, "Content-Transfer-Encoding", "8bit"); + } + + fprintf(f, "\r\n"); + fprintf(f, "%s\r\n", body); + + } else { + fprintf(f, "\r\n"); + fprintf(f, "Notification of new message.\r\n"); + } - if ( senv->smtp_close(smtp_handle) ) { - sieve_result_log(aenv, - "sent mail notification to <%s>", str_sanitize(recipient, 80)); - } else { - sieve_result_error(aenv, - "failed to send mail notification to <%s> " - "(refer to system log for more information)", - str_sanitize(recipient, 80)); + if ( senv->smtp_close(smtp_handle) ) { + sieve_result_log(aenv, + "sent mail notification to <%s>", str_sanitize(recipients[i], 80)); + } else { + sieve_result_error(aenv, + "failed to send mail notification to <%s> " + "(refer to system log for more information)", + str_sanitize(recipients[i], 80)); + } } return TRUE; } + diff --git a/src/lib-sieve/plugins/enotify/sieve-ext-enotify.h b/src/lib-sieve/plugins/enotify/sieve-ext-enotify.h index e635e9144ad6f0008a652a8da3fbc3341d095451..5dbed7d7f07d1919b5dba43f5bbb81d117b1f342 100644 --- a/src/lib-sieve/plugins/enotify/sieve-ext-enotify.h +++ b/src/lib-sieve/plugins/enotify/sieve-ext-enotify.h @@ -8,14 +8,11 @@ #include "sieve-common.h" -struct sieve_enotify_context { - const char *method_uri; - - sieve_number_t importance; - const char *message; - const char *from; - const char *const *options; -}; +/* + * Forward declarations + */ + +struct sieve_enotify_context; /* * Notify methods @@ -28,14 +25,39 @@ struct sieve_enotify_method { bool (*validate_uri) (struct sieve_validator *valdtr, struct sieve_ast_argument *arg, const char *uri_body); + + /* Runtime */ + bool (*runtime_check_operands) + (const struct sieve_runtime_env *renv, unsigned int source_line, + const char *uri, const char *uri_body, const char *message, + const char *from, void **context); + + /* Action print */ + void (*action_print) + (const struct sieve_result_print_env *rpenv, + const struct sieve_enotify_context *nctx); - /* Execution */ - bool (*execute) + /* Action execution */ + bool (*action_execute) (const struct sieve_action_exec_env *aenv, const struct sieve_enotify_context *nctx); }; void sieve_enotify_method_register(const struct sieve_enotify_method *method); +/* + * Action context + */ + +struct sieve_enotify_context { + const struct sieve_enotify_method *method; + void *method_context; + + sieve_number_t importance; + const char *message; + const char *from; +}; + + #endif /* __SIEVE_EXT_ENOTIFY_H */ diff --git a/src/lib-sieve/plugins/vacation/cmd-vacation.c b/src/lib-sieve/plugins/vacation/cmd-vacation.c index d1b1dc5daafea68847f01ce340a970258129f5c7..55a92020c6815bde2e3ff0173ce3ed430f1bfcf4 100644 --- a/src/lib-sieve/plugins/vacation/cmd-vacation.c +++ b/src/lib-sieve/plugins/vacation/cmd-vacation.c @@ -2,6 +2,8 @@ */ #include "lib.h" +#include "str.h" +#include "strfuncs.h" #include "md5.h" #include "hostpid.h" #include "str-sanitize.h" @@ -9,6 +11,8 @@ #include "message-date.h" #include "ioloop.h" +#include "rfc2822.h" + #include "sieve-common.h" #include "sieve-code.h" #include "sieve-address.h" @@ -796,6 +800,8 @@ static bool act_vacation_send void *smtp_handle; FILE *f; const char *outmsgid; + const char *const *headers; + int references; /* Check smpt functions just to be sure */ @@ -811,31 +817,40 @@ static bool act_vacation_send /* Produce a proper reply */ - fprintf(f, "Message-ID: %s\r\n", outmsgid); - fprintf(f, "Date: %s\r\n", message_date_create(ioloop_time)); + rfc2822_header_field_write(f, "Message-ID", outmsgid); + rfc2822_header_field_write(f, "Date", message_date_create(ioloop_time)); if ( ctx->from != NULL && *(ctx->from) != '\0' ) - fprintf(f, "From: <%s>\r\n", ctx->from); + rfc2822_header_field_printf(f, "From", "<%s>", ctx->from); else - fprintf(f, "From: <%s>\r\n", msgdata->to_address); + rfc2822_header_field_printf(f, "From", "<%s>", msgdata->to_address); - fprintf(f, "To: <%s>\r\n", msgdata->return_path); - fprintf(f, "Subject: %s\r\n", str_sanitize(ctx->subject, 80)); - + rfc2822_header_field_printf(f, "To", "<%s>", msgdata->return_path); + rfc2822_header_field_printf(f, "Subject", "%s", + str_sanitize(ctx->subject, 256)); + + references = mail_get_headers_utf8 + (aenv->msgdata->mail, "references", &headers); + if ( msgdata->id != NULL ) { - fprintf(f, "In-Reply-To: %s\r\n", msgdata->id); - - /* FIXME: Update References header */ + rfc2822_header_field_write(f, "In-Reply-To", msgdata->id); + + if ( references >= 0 ) + rfc2822_header_field_write + (f, "References", t_strconcat(headers[0], " ", msgdata->id, NULL)); + else + rfc2822_header_field_write(f, "References", msgdata->id); + } else if ( references > 0 ) { + rfc2822_header_field_write(f, "References", headers[0]); } - fprintf(f, "Auto-Submitted: auto-replied (vacation)\r\n"); - - fprintf(f, "X-Sieve: %s\r\n", SIEVE_IMPLEMENTATION); - - fprintf(f, "Precedence: bulk\r\n"); - fprintf(f, "MIME-Version: 1.0\r\n"); + + rfc2822_header_field_write(f, "Auto-Submitted", "auto-replied (vacation)"); + rfc2822_header_field_write(f, "X-Sieve", SIEVE_IMPLEMENTATION); + rfc2822_header_field_write(f, "Precedence", "bulk"); + rfc2822_header_field_write(f, "MIME-Version", "1.0"); if ( !ctx->mime ) { - fprintf(f, "Content-Type: text/plain; charset=utf-8\r\n"); - fprintf(f, "Content-Transfer-Encoding: 8bit\r\n"); + rfc2822_header_field_write(f, "Content-Type", "text/plain; charset=utf-8"); + rfc2822_header_field_write(f, "Content-Transfer-Encoding", "8bit"); fprintf(f, "\r\n"); } diff --git a/src/lib-sieve/rfc2822.c b/src/lib-sieve/rfc2822.c index 329432a6c550687e781b0b13ec8b046197dc53ef..6aed0c81522200703082833962d0b7a3ec5c77cd 100644 --- a/src/lib-sieve/rfc2822.c +++ b/src/lib-sieve/rfc2822.c @@ -2,9 +2,13 @@ */ #include "lib.h" +#include "str.h" #include "rfc2822.h" +#include <stdio.h> +#include <ctype.h> + /* NOTE: much of the functionality implemented here should eventually appear * somewhere in Dovecot itself. */ @@ -31,3 +35,117 @@ bool rfc2822_header_field_name_verify return TRUE; } +/* + * + */ + +const char *rfc2822_header_field_name_sanitize(const char *name) +{ + char *result = t_strdup_noconst(name); + char *p; + + /* Make the whole name lower case ... */ + result = str_lcase(result); + + /* ... except for the first letter and those that follow '-' */ + p = result; + *p = i_toupper(*p); + while ( *p != '\0' ) { + if ( *p == '-' ) { + p++; + + if ( *p != '\0' ) + *p = i_toupper(*p); + + continue; + } + + p++; + } + + return result; +} + +/* + * Message construction + */ + +/* FIXME: This should be collected into a Dovecot API for composing internet + * mail messages. These functions now use FILE * output streams, but this should + * be changed to proper dovecot streams. + */ + +void rfc2822_header_field_write +(FILE *f, const char *name, const char *body) +{ + static const unsigned int max_line = 80; + + const char *bp = body; /* Pointer */ + const char *sp = body; /* Start pointer */ + const char *wp = NULL; /* Whitespace pointer */ + const char *nlp = NULL; /* New-line pointer */ + unsigned int len = strlen(name); + + /* Write header field name first */ + fwrite(name, len, 1, f); + fwrite(": ", 2, 1, f); + + /* Add field body; fold it if necessary and account for existing folding */ + len += 2; + while ( *bp != '\0' ) { + while ( *bp != '\0' && nlp == NULL && (wp == NULL || len < max_line) ) { + if ( *bp == ' ' || *bp == '\t' ) { + wp = bp; + } else if ( *bp == '\r' || *bp == '\n' ) { + nlp = bp; + break; + } + + bp++; len++; + } + + if ( *bp == '\0' ) break; + + /* Existing newline ? */ + if ( nlp != NULL ) { + /* Replace any sort of newline with proper CRLF */ + while ( *bp == '\r' || *bp == '\n' ) + bp++; + + fwrite(sp, nlp-sp, 1, f); + + if ( *bp != '\0' && *bp != ' ' && *bp != '\t' ) + fwrite("\r\n\t", 3, 1, f); + else + fwrite("\r\n", 2, 1, f); + + sp = bp; + } else { + /* Insert newline at last whitespace within the max_line limit */ + fwrite(sp, wp-sp, 1, f); + fwrite("\r\n", 2, 1, f); + sp = wp; + } + + len = bp - sp; + wp = NULL; + nlp = NULL; + } + + fwrite(sp, bp-sp, 1, f); + fwrite("\r\n", 2, 1, f); +} + +void rfc2822_header_field_printf +(FILE *f, const char *name, const char *body_fmt, ...) +{ + string_t *body = t_str_new(256); + va_list args; + + va_start(args, body_fmt); + str_vprintfa(body, body_fmt, args); + va_end(args); + + rfc2822_header_field_write(f, name, str_c(body)); +} + diff --git a/src/lib-sieve/rfc2822.h b/src/lib-sieve/rfc2822.h index 3748c950f0902ffe70d4974a546c9ccc7df149ae..fc85ac2a67ab7f97a49a63103867fcf21ca7561f 100644 --- a/src/lib-sieve/rfc2822.h +++ b/src/lib-sieve/rfc2822.h @@ -6,7 +6,29 @@ #include "lib.h" +#include <stdio.h> + +/* + * Verification + */ + bool rfc2822_header_field_name_verify (const char *field_name, unsigned int len); +/* + * + */ + +const char *rfc2822_header_field_name_sanitize(const char *name); + +/* + * Message composition + */ + +void rfc2822_header_field_write + (FILE *f, const char *name, const char *body); + +void rfc2822_header_field_printf + (FILE *f, const char *name, const char *body_fmt, ...) ATTR_FORMAT(3, 4); + #endif /* __RFC2822_H */ diff --git a/src/sieve-tools/sieve-exec.c b/src/sieve-tools/sieve-exec.c index cc66524767ee7816e38ac63acac5abe4b23e9a32..4741016288ca8eddc5adbbdc424a5ffc6944189a 100644 --- a/src/sieve-tools/sieve-exec.c +++ b/src/sieve-tools/sieve-exec.c @@ -29,7 +29,7 @@ static void *sieve_smtp_open(const char *destination, const char *return_path, FILE **file_r) { - i_info("sending mesage from <%s> to <%s>:", + i_info("sending message from <%s> to <%s>:", return_path == NULL || *return_path == '\0' ? "" : return_path, destination); printf("\nSTART MESSAGE:\n"); diff --git a/tests/extensions/enotify/errors.svtest b/tests/extensions/enotify/errors.svtest new file mode 100644 index 0000000000000000000000000000000000000000..40fb6d1eb72ca05d256a9f60baa8afeb7b31554b --- /dev/null +++ b/tests/extensions/enotify/errors.svtest @@ -0,0 +1,26 @@ +require "vnd.dovecot.testsuite"; +require "comparator-i;ascii-numeric"; +require "relational"; + +require "enotify"; + + +test "Invalid URL (FIXME: count only)" { + if test_compile "errors/url.sieve" { + test_fail "compile should have failed"; + } + + if not test_error :count "eq" :comparator "i;ascii-numeric" "2" { + test_fail "wrong number of errors reported"; + } +} + +test "Invalid mailto URL (FIXME: count only)" { + if test_compile "errors/url-mailto.sieve" { + test_fail "compile should have failed"; + } + + if not test_error :count "eq" :comparator "i;ascii-numeric" "2" { + test_fail "wrong number of errors reported"; + } +} diff --git a/tests/extensions/enotify/errors/url-mailto.sieve b/tests/extensions/enotify/errors/url-mailto.sieve new file mode 100644 index 0000000000000000000000000000000000000000..5a4d32f6c40bbf0276fbacd5bfeb0f4cb59f8f5b --- /dev/null +++ b/tests/extensions/enotify/errors/url-mailto.sieve @@ -0,0 +1,5 @@ +require "enotify"; + +# 1: Invalid header name +notify "mailto:stephan@rename-it.nl?header:=frop"; + diff --git a/tests/extensions/enotify/errors/url.sieve b/tests/extensions/enotify/errors/url.sieve new file mode 100644 index 0000000000000000000000000000000000000000..06e86d764f6d384ce86443addb3580132d7b6c23 --- /dev/null +++ b/tests/extensions/enotify/errors/url.sieve @@ -0,0 +1,5 @@ +require "enotify"; + +# 1: Invalid url scheme +notify "snailto:stephan@rename-it.nl"; + diff --git a/tests/extensions/vacation/references.sieve b/tests/extensions/vacation/references.sieve new file mode 100644 index 0000000000000000000000000000000000000000..77658f2fa0b0e4f50ffacb6c8ab3b43e2054c701 --- /dev/null +++ b/tests/extensions/vacation/references.sieve @@ -0,0 +1,4 @@ +require "vacation"; + +vacation "I am on vacation."; +discard; diff --git a/tests/extensions/vacation/references.svtest b/tests/extensions/vacation/references.svtest new file mode 100644 index 0000000000000000000000000000000000000000..f67b1e69643c9c86056d6a35897fc9f2cfd30182 --- /dev/null +++ b/tests/extensions/vacation/references.svtest @@ -0,0 +1,18 @@ +require "vnd.dovecot.testsuite"; +require "vacation"; + +test_set "message" text: +From: stephan@rename-it.nl +Subject: frop +References: <1234@local.machine.example> <3456@example.net> + <435444@ttms.com> <4223@froop.nl> <m345444444@message-id.exp> +Message-ID: <432df324@rename-it.nl> +To: nico@vestingbar.nl + +Frop +. +; + +test "References" { + vacation "I am not in today!"; +}