/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file */ #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 "rfc2822.h" #include "sieve-common.h" #include "sieve-stringlist.h" #include "sieve-code.h" #include "sieve-extensions.h" #include "sieve-commands.h" #include "sieve-actions.h" #include "sieve-validator.h" #include "sieve-generator.h" #include "sieve-interpreter.h" #include "sieve-dump.h" #include "sieve-result.h" #include "sieve-address.h" #include "sieve-message.h" #include "sieve-smtp.h" #include "ext-notify-common.h" #include "ext-notify-limits.h" #include /* Notify command (DEPRECATED) * * Syntax: * notify [":method" string] [":id" string] [":options" string-list] * [<":low" / ":normal" / ":high">] ["message:" string] * */ static bool cmd_notify_registered(struct sieve_validator *valdtr, const struct sieve_extension *ext, struct sieve_command_registration *cmd_reg); static bool cmd_notify_pre_validate(struct sieve_validator *valdtr, struct sieve_command *cmd); static bool cmd_notify_validate(struct sieve_validator *valdtr, struct sieve_command *cmd); static bool cmd_notify_generate(const struct sieve_codegen_env *cgenv, struct sieve_command *ctx); const struct sieve_command_def cmd_notify_old = { .identifier = "notify", .type = SCT_COMMAND, .positional_args = 0, .subtests = 0, .block_allowed = FALSE, .block_required = FALSE, .registered = cmd_notify_registered, .pre_validate = cmd_notify_pre_validate, .validate = cmd_notify_validate, .generate = cmd_notify_generate }; /* * Tagged arguments */ /* Forward declarations */ static bool cmd_notify_validate_string_tag(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, struct sieve_command *cmd); static bool cmd_notify_validate_stringlist_tag(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, struct sieve_command *cmd); /* Argument objects */ static const struct sieve_argument_def notify_method_tag = { .identifier = "method", .validate = cmd_notify_validate_string_tag }; static const struct sieve_argument_def notify_options_tag = { .identifier = "options", .validate = cmd_notify_validate_stringlist_tag }; static const struct sieve_argument_def notify_id_tag = { .identifier = "id", .validate = cmd_notify_validate_string_tag }; static const struct sieve_argument_def notify_message_tag = { .identifier = "message", .validate = cmd_notify_validate_string_tag }; /* * Notify operation */ static bool cmd_notify_operation_dump(const struct sieve_dumptime_env *denv, sieve_size_t *address); static int cmd_notify_operation_execute(const struct sieve_runtime_env *renv, sieve_size_t *address); const struct sieve_operation_def notify_old_operation = { .mnemonic = "NOTIFY", .ext_def = ¬ify_extension, .code = EXT_NOTIFY_OPERATION_NOTIFY, .dump = cmd_notify_operation_dump, .execute = cmd_notify_operation_execute }; /* Codes for optional operands */ enum cmd_notify_optional { OPT_END, OPT_MESSAGE, OPT_IMPORTANCE, OPT_OPTIONS, OPT_ID }; /* * Notify action */ /* Forward declarations */ static int act_notify_check_duplicate(const struct sieve_runtime_env *renv, const struct sieve_action *act, const struct sieve_action *act_other); static void act_notify_print(const struct sieve_action *action, const struct sieve_result_print_env *rpenv, bool *keep); static int act_notify_commit(const struct sieve_action_exec_env *aenv, void *tr_context); /* Action object */ const struct sieve_action_def act_notify_old = { .name = "notify", .check_duplicate = act_notify_check_duplicate, .print = act_notify_print, .commit = act_notify_commit }; /* * Command validation context */ struct cmd_notify_context_data { struct sieve_ast_argument *id; struct sieve_ast_argument *method; struct sieve_ast_argument *options; struct sieve_ast_argument *message; }; /* * Tag validation */ static bool cmd_notify_validate_string_tag(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, struct sieve_command *cmd) { struct sieve_ast_argument *tag = *arg; struct cmd_notify_context_data *ctx_data = (struct cmd_notify_context_data *)cmd->data; /* Detach the tag itself */ *arg = sieve_ast_arguments_detach(*arg, 1); /* Check syntax: * :id * :method * :message */ if (!sieve_validate_tag_parameter(valdtr, cmd, tag, *arg, NULL, 0, SAAT_STRING, FALSE)) return FALSE; if (sieve_argument_is(tag, notify_method_tag)) { ctx_data->method = *arg; /* Removed */ *arg = sieve_ast_arguments_detach(*arg, 1); } else if (sieve_argument_is(tag, notify_id_tag)) { ctx_data->id = *arg; /* Skip parameter */ *arg = sieve_ast_argument_next(*arg); } else if (sieve_argument_is(tag, notify_message_tag)) { ctx_data->message = *arg; /* Skip parameter */ *arg = sieve_ast_argument_next(*arg); } return TRUE; } static bool cmd_notify_validate_stringlist_tag(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, struct sieve_command *cmd) { struct sieve_ast_argument *tag = *arg; struct cmd_notify_context_data *ctx_data = (struct cmd_notify_context_data *)cmd->data; /* Detach the tag itself */ *arg = sieve_ast_arguments_detach(*arg,1); /* Check syntax: * :options string-list */ if (!sieve_validate_tag_parameter(valdtr, cmd, tag, *arg, NULL, 0, SAAT_STRING_LIST, FALSE)) return FALSE; /* Assign context */ ctx_data->options = *arg; /* Skip parameter */ *arg = sieve_ast_argument_next(*arg); return TRUE; } /* * Command registration */ static bool cmd_notify_registered(struct sieve_validator *valdtr, const struct sieve_extension *ext, struct sieve_command_registration *cmd_reg) { sieve_validator_register_tag(valdtr, cmd_reg, ext, ¬ify_method_tag, 0); sieve_validator_register_tag(valdtr, cmd_reg, ext, ¬ify_id_tag, OPT_ID); sieve_validator_register_tag(valdtr, cmd_reg, ext, ¬ify_message_tag, OPT_MESSAGE); sieve_validator_register_tag(valdtr, cmd_reg, ext, ¬ify_options_tag, OPT_OPTIONS); ext_notify_register_importance_tags(valdtr, cmd_reg, ext, OPT_IMPORTANCE); return TRUE; } /* * Command validation */ static bool cmd_notify_pre_validate(struct sieve_validator *valdtr ATTR_UNUSED, struct sieve_command *cmd) { struct cmd_notify_context_data *ctx_data; /* Create context */ ctx_data = p_new(sieve_command_pool(cmd), struct cmd_notify_context_data, 1); cmd->data = ctx_data; return TRUE; } static int cmd_notify_address_validate(void *context, struct sieve_ast_argument *arg) { struct sieve_validator *valdtr = (struct sieve_validator *)context; if (sieve_argument_is_string_literal(arg)) { string_t *address = sieve_ast_argument_str(arg); const char *error; int result; T_BEGIN { result = (sieve_address_validate_str(address, &error) ? 1 : -1); if (result <= 0) { sieve_argument_validate_error( valdtr, arg, "specified :options address '%s' is invalid for " "the mailto notify method: %s", str_sanitize(str_c(address), 128), error); } } T_END; return result; } return 1; } static bool cmd_notify_validate(struct sieve_validator *valdtr, struct sieve_command *cmd) { struct cmd_notify_context_data *ctx_data = (struct cmd_notify_context_data *)cmd->data; /* Check :method argument */ if (ctx_data->method != NULL) { const char *method = sieve_ast_argument_strc(ctx_data->method); if (strcasecmp(method, "mailto") != 0) { sieve_command_validate_error( valdtr, cmd, "the notify command of the deprecated notify extension " "only supports the 'mailto' notification method"); return FALSE; } } /* Check :options argument */ if (ctx_data->options != NULL) { struct sieve_ast_argument *option = ctx_data->options; /* Parse and check options */ if (sieve_ast_stringlist_map( &option, (void *)valdtr, cmd_notify_address_validate) <= 0) { return FALSE; } } else { sieve_command_validate_warning( valdtr, cmd, "no :options (and hence recipients) specified for the notify command"); } return TRUE; } /* * Code generation */ static bool cmd_notify_generate(const struct sieve_codegen_env *cgenv, struct sieve_command *cmd) { sieve_operation_emit(cgenv->sblock, cmd->ext, ¬ify_old_operation); /* Generate arguments */ return sieve_generate_arguments(cgenv, cmd, NULL); } /* * Code dump */ static bool cmd_notify_operation_dump(const struct sieve_dumptime_env *denv, sieve_size_t *address) { int opt_code = 0; sieve_code_dumpf(denv, "NOTIFY"); sieve_code_descend(denv); /* Dump optional operands */ for (;;) { int opt; bool opok = TRUE; if ((opt = sieve_opr_optional_dump(denv, address, &opt_code)) < 0) return FALSE; if (opt == 0) break; switch (opt_code) { case OPT_IMPORTANCE: opok = sieve_opr_number_dump(denv, address, "importance"); break; case OPT_ID: opok = sieve_opr_string_dump(denv, address, "id"); break; case OPT_OPTIONS: opok = sieve_opr_stringlist_dump(denv, address, "options"); break; case OPT_MESSAGE: opok = sieve_opr_string_dump(denv, address, "message"); break; default: return FALSE; } if (!opok) return FALSE; } return TRUE; } /* * Code execution */ static int cmd_notify_operation_execute(const struct sieve_runtime_env *renv, sieve_size_t *address) { const struct sieve_extension *this_ext = renv->oprtn->ext; struct ext_notify_action *act; pool_t pool; int opt_code = 0; sieve_number_t importance = 1; struct sieve_stringlist *options = NULL; string_t *message = NULL, *id = NULL; int ret = 0; /* * Read operands */ /* Optional operands */ for (;;) { int opt; if ((opt = sieve_opr_optional_read(renv, address, &opt_code)) < 0) return SIEVE_EXEC_BIN_CORRUPT; if (opt == 0) break; switch (opt_code) { case OPT_IMPORTANCE: ret = sieve_opr_number_read(renv, address, "importance", &importance); break; case OPT_ID: ret = sieve_opr_string_read(renv, address, "id", &id); break; case OPT_MESSAGE: ret = sieve_opr_string_read(renv, address, "from", &message); break; case OPT_OPTIONS: ret = sieve_opr_stringlist_read(renv, address, "options", &options); break; default: sieve_runtime_trace_error( renv, "unknown optional operand"); return SIEVE_EXEC_BIN_CORRUPT; } if (ret <= 0) return ret; } /* * Perform operation */ /* Enforce 0 < importance < 4 (just to be sure) */ if (importance < 1) importance = 1; else if (importance > 3) importance = 3; /* Trace */ sieve_runtime_trace(renv, SIEVE_TRLVL_ACTIONS, "notify action"); /* Compose action */ if (options != NULL) { string_t *raw_address; string_t *out_message; pool = sieve_result_pool(renv->result); act = p_new(pool, struct ext_notify_action, 1); if (id != NULL) act->id = p_strdup(pool, str_c(id)); act->importance = importance; /* Process message */ out_message = t_str_new(1024); if ((ret = ext_notify_construct_message( renv, (message == NULL ? NULL : str_c(message)), out_message)) <= 0) return ret; act->message = p_strdup(pool, str_c(out_message)); /* Normalize and verify all :options addresses */ sieve_stringlist_reset(options); p_array_init(&act->recipients, pool, 4); raw_address = NULL; while ((ret = sieve_stringlist_next_item( options, &raw_address)) > 0) { const char *error = NULL; const struct smtp_address *address; /* Add if valid address */ address = sieve_address_parse_str(raw_address, &error); if (address != NULL) { const struct ext_notify_recipient *rcpts; unsigned int rcpt_count, i; /* Prevent duplicates */ rcpts = array_get(&act->recipients, &rcpt_count); for (i = 0; i < rcpt_count; i++) { if (smtp_address_equals(rcpts[i].address, address)) break; } /* Add only if unique */ if (i != rcpt_count) { sieve_runtime_warning( renv, NULL, "duplicate recipient '%s' specified in the :options argument of " "the deprecated notify command", str_sanitize(str_c(raw_address), 128)); } else if (array_count(&act->recipients) >= EXT_NOTIFY_MAX_RECIPIENTS) { sieve_runtime_warning(renv, NULL, "more than the maximum %u recipients are specified " "for the deprecated notify command; " "the rest is discarded", EXT_NOTIFY_MAX_RECIPIENTS); break; } else { struct ext_notify_recipient recipient; recipient.full = p_strdup(pool, str_c(raw_address)); recipient.address = smtp_address_clone(pool, address); array_append(&act->recipients, &recipient, 1); } } else { sieve_runtime_error( renv, NULL, "specified :options address '%s' is invalid for " "the deprecated notify command: %s", str_sanitize(str_c(raw_address), 128), error); return SIEVE_EXEC_FAILURE; } } if (ret < 0) { sieve_runtime_trace_error( renv, "invalid options stringlist"); return SIEVE_EXEC_BIN_CORRUPT; } if (sieve_result_add_action(renv, this_ext, "notify", &act_notify_old, NULL, (void *)act, 0, FALSE) < 0) return SIEVE_EXEC_FAILURE; } return SIEVE_EXEC_OK; } /* * Action */ /* Runtime verification */ static int act_notify_check_duplicate(const struct sieve_runtime_env *renv ATTR_UNUSED, const struct sieve_action *act ATTR_UNUSED, const struct sieve_action *act_other ATTR_UNUSED) { struct ext_notify_action *new_nact, *old_nact; const struct ext_notify_recipient *new_rcpts; const struct ext_notify_recipient *old_rcpts; unsigned int new_count, old_count, i, j; unsigned int del_start = 0, del_len = 0; if (act->context == NULL || act_other->context == NULL) return 0; new_nact = (struct ext_notify_action *)act->context; old_nact = (struct ext_notify_action *)act_other->context; new_rcpts = array_get(&new_nact->recipients, &new_count); old_rcpts = array_get(&old_nact->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(&new_nact->recipients, del_start, del_len); /* Make sure the loop integrity is maintained */ i -= del_len; new_rcpts = array_get(&new_nact->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(&new_nact->recipients, del_start, del_len); return (array_count(&new_nact->recipients) > 0 ? 0 : 1); } /* Result printing */ static void act_notify_print(const struct sieve_action *action, const struct sieve_result_print_env *rpenv, bool *keep ATTR_UNUSED) { const struct ext_notify_action *act = (const struct ext_notify_action *)action->context; const struct ext_notify_recipient *recipients; unsigned int count, i; sieve_result_action_printf( rpenv, "send (deprecated) notification with method 'mailto':"); /* Print main method parameters */ sieve_result_printf(rpenv, " => importance : %llu\n", (unsigned long long)act->importance); if (act->message != NULL) { sieve_result_printf( rpenv, " => message : %s\n", act->message); } if (act->id != NULL) { sieve_result_printf( rpenv, " => id : %s \n", act->id); } /* Print mailto: recipients */ sieve_result_printf(rpenv, " => recipients :\n"); recipients = array_get(&act->recipients, &count); if (count == 0) { sieve_result_printf( rpenv, " NONE, action has no effect\n"); } else { for (i = 0; i < count; i++) { sieve_result_printf( rpenv, " + To: %s\n", recipients[i].full); } } /* Finish output with an empty line */ sieve_result_printf(rpenv, "\n"); } /* Result 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 bool act_notify_send(const struct sieve_action_exec_env *aenv, const struct ext_notify_action *act) { const struct sieve_execute_env *eenv = aenv->exec_env; const struct sieve_script_env *senv = eenv->scriptenv; const struct ext_notify_recipient *recipients; struct sieve_smtp_context *sctx; unsigned int count, i; struct ostream *output; string_t *msg, *to, *all; const char *outmsgid, *error; int ret; /* Get recipients */ recipients = array_get(&act->recipients, &count); if (count == 0) { sieve_result_warning( aenv, "notify action specifies no recipients; " "action has no effect"); return TRUE; } /* Just to be sure */ if (!sieve_smtp_available(senv)) { sieve_result_global_warning( aenv, "notify action has no means to send mail"); return TRUE; } /* Compose common headers */ msg = t_str_new(512); rfc2822_header_write(msg, "X-Sieve", SIEVE_IMPLEMENTATION); rfc2822_header_write(msg, "Date", message_date_create(ioloop_time)); /* Set importance */ switch (act->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; } rfc2822_header_write(msg, "From", sieve_get_postmaster_address(senv)); rfc2822_header_write(msg, "Subject", "[SIEVE] New mail notification"); rfc2822_header_write(msg, "Auto-Submitted", "auto-generated (notify)"); rfc2822_header_write(msg, "Precedence", "bulk"); rfc2822_header_write(msg, "MIME-Version", "1.0"); if (contains_8bit(act->message)) { 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"); } outmsgid = sieve_message_get_new_id(eenv->svinst); rfc2822_header_write(msg, "Message-ID", outmsgid); if ((eenv->flags & SIEVE_EXECUTE_FLAG_NO_ENVELOPE) == 0 && sieve_message_get_sender(aenv->msgctx) != NULL) { sctx = sieve_smtp_start(senv, sieve_get_postmaster_smtp(senv)); } else { sctx = sieve_smtp_start(senv, NULL); } /* Add all recipients (and compose To header field) */ to = t_str_new(128); all = t_str_new(256); for (i = 0; i < count; i++) { sieve_smtp_add_rcpt(sctx, recipients[i].address); if (i > 0) 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); } } rfc2822_header_write_address(msg, "To", str_c(to)); /* Generate message body */ str_printfa(msg, "\r\n%s\r\n", act->message); 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_result_global_error( aenv, "failed to send mail notification to %s: " "%s (temporary failure)", str_c(all), str_sanitize(error, 512)); } else { sieve_result_global_log_error( aenv, "failed to send mail notification to %s: " "%s (permanent failure)", str_c(all), str_sanitize(error, 512)); } } else { struct event_passthrough *e = sieve_action_create_finish_event(aenv)-> add_str("notify_target", str_c(all)); sieve_result_event_log(aenv, e->event(), "sent mail notification to %s", str_c(all)); } return TRUE; } static int act_notify_commit(const struct sieve_action_exec_env *aenv, void *tr_context ATTR_UNUSED) { const struct sieve_execute_env *eenv = aenv->exec_env; struct mail *mail = eenv->msgdata->mail; const struct ext_notify_action *act = (const struct ext_notify_action *)aenv->action->context; const char *const *hdsp; bool result; int ret; /* Is the message an automatic reply ? */ if ((ret = mail_get_headers(mail, "auto-submitted", &hdsp)) < 0) { return sieve_result_mail_error( aenv, mail, "failed to read `auto-submitted' header field"); } /* 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 ((eenv->flags & SIEVE_EXECUTE_FLAG_NO_ENVELOPE) == 0) sender = sieve_message_get_sender(aenv->msgctx); from = (sender == NULL ? "" : t_strdup_printf(" from <%s>", smtp_address_encode(sender))); sieve_result_global_log( aenv, "not sending notification for auto-submitted message%s", from); return SIEVE_EXEC_OK; } hdsp++; } } T_BEGIN { result = act_notify_send(aenv, act); } T_END; if (!result) return SIEVE_EXEC_FAILURE; eenv->exec_status->significant_action_executed = TRUE; return SIEVE_EXEC_OK; }