diff --git a/TODO b/TODO index b24c5bbb0073ec58937a4d670754c82bce0693e4..687c112b324e58cdd97d05d3dea23fb6f7330fa3 100644 --- a/TODO +++ b/TODO @@ -2,8 +2,6 @@ Current activities: * Build a sieve tool to filter an entire existing mailbox through a Sieve script. -* Finish the ereject extension - - Make reject/ereject use the LDA reject interface when available Next (in order of descending priority/precedence): @@ -24,6 +22,7 @@ Next (in order of descending priority/precedence): * Implement index extension * Update include extension to latest draft (v05 currently): - Implement required ManageSieve behavior (pending IETF discussion) +* Finish the ereject extension * Vacation extension improvements: - Implement configurable sender exclusion list. - Implement mechanism for implicitly including an account's aliases in the diff --git a/doc/man/sieve-filter.1.in b/doc/man/sieve-filter.1.in index e2f74c101fe2819d82a479461ee5a70b564f633e..6400c79e3575a609ebb5b4f1b8d62f61a3db48bb 100644 --- a/doc/man/sieve-filter.1.in +++ b/doc/man/sieve-filter.1.in @@ -26,7 +26,7 @@ However, there are occasions when it is desirable to filter messages that are already stored in a mailbox, for instance when a bug in a Sieve script caused many messages to be delivered incorrectly. Using the sieve\-filter tool it is possible to apply a Sieve script on all messages in a particular mailbox, making -it possible to delete messages, to store them in a different folder and to +it possible to delete messages, to store them in a different mailbox and to change the assigned IMAP flags and keywords. Attempts to send messages to the outside world are ignored by default for obvious reasons, but, using the proper command line options, it is possible to capture outgoing mail as well. @@ -35,7 +35,10 @@ If no options are specified, the sieve\-filter command runs in a simulation mode in which it only prints what would be performed, without actually doing anything. Use the \fB\-e\fP option to activate true script execution. Also, the source mailbox is opened read\-only by default, so that the source mailbox -remains unchanged. Use the \fB\-W\fP to allow changes in the source mailbox. +remains unchanged. Use the \fB\-W\fP option to allow changes in the source mailbox. +And even with the \fB\-W\fP option enabled, messages in the source mailbox are +only potentially modified and not (re)moved, unless a \fIsource\-action\fP +argument other than \fBkeep\fP is specified. .SS CAUTION Although this is a very useful tool, it can also be very destructive when used improperly. A small bug in your Sieve script in combination with the wrong @@ -73,17 +76,28 @@ Using this option the sieve\-filter command becomes active and performs the requested actions. .TP .BI \-m\ default\-mailbox -The mailbox where the keep action stores messages. This is \(dqINBOX\(dq -by default. -.TP -.BI \-q\ output\-mailbox +The mailbox where the (implicit) \fBkeep\fP Sieve action stores messages. This is +equal to the source mailbox by default. +.TP +.B \-M +Enable move mode. This will cause all messages that were succesfully stored +somewhere else to be expunged from the source mailbox, regardless of what the +\fIsource\-action\fP is (refer to Arguments section below). However, if the +Sieve filter decides to keep the message in the source mailbox, it is left +there and not affected by this option. +.IP +Note that some Sieve actions, such as \fBredirect\fP, don't store the message +anywhere and are thus \- with respect to the fate of the message in the source +mailbox \- treated as if a \fBdiscard\fP action were executed. +.TP +.BI \-q\ output\-mailbox\ \fR[not\ implemented\ yet]\fP Store outgoing e\-mail into the indicated \fIoutput\-mailbox\fP. By default, the sieve\-filter command ignores Sieve actions such as redirect, reject, vacation and notify, but using this option outgoing messages can be appended to the indicated mailbox. This option has no effect in simulation mode. Flags of redirected messages are not preserved. .TP -.BI \-Q\ mail\-command +.BI \-Q\ mail\-command\ \fR[not\ implemented\ yet]\fP Send outgoing e\-mail (e.g. as produced by redirect, reject and vacation) through the specified program. By default, the sieve\-filter command ignores Sieve actions such as redirect, reject, vacation and notify, but using this @@ -91,7 +105,7 @@ option outgoing messages can be fed to the \fBstdin\fP of an external shell command. This option has no effect in simulation mode. Unless you really know what you are doing, \fBDO NOT USE THIS TO FEED MAIL TO SENDMAIL!\fP. .TP -.BI \-s\ script\-file +.BI \-s\ script\-file\ \fR[not\ implemented\ yet]\fP Specify additional scripts to be executed before the main script. Multiple \fB\-s\fP arguments are allowed and the specified scripts are executed sequentially in the order specified at the command @@ -133,21 +147,18 @@ into a new binary. The name of the source mailbox. .TP .I source\-action -Specifies what needs to be done with messages in the source mailbox once -processed by the Sieve script. By default, the sieve\-filter command does not -remove the messages from the source mailbox. In particular this means that the -keep and fileinto actions will cause the messages to be copied. To alter this -behavior, the \fIsource\-action\fP parameter of the \fB\-R\fP option accepts -one of the following values: +Specifies what is done with messages in the source mailbox once processed by the +Sieve script. The \fIsource\-action\fP parameter accepts one of the following values: .RS 7 .TP .BR keep\ (default) -Keep processed messages in source folder. If \fB\-W\fR is specified and the -source mailbox is the destination of a keep or fileinto action, flags may be -changed by the Sieve script, but messages are never duplicated there. +Keep processed messages in source mailbox. When the filter decides to store the message +in the source mailbox, it is never duplicated there. However, in that case, the +IMAP flags of the original message can be modified by the Sieve interpreter. .TP -.BI move\ folder -Move processed messages to the indicated \fIfolder\fP. +.BI move\ mailbox +Move processed messages to the indicated \fImailbox\fP. This is for instance useful +to move messages to a Trash mailbox. .TP .B delete Flag processed messages as \\DELETED. @@ -155,9 +166,21 @@ Flag processed messages as \\DELETED. .B expunge Expunge processed messages, meaning that these are removed irreversibly when the tool finishes filtering. -.PP -Note that the chosen \fIsource\-action\fP only has an effect on the source -mailbox when the \fB\-W\fP option is specified as well. +.RE +.IP +The \fBsource\-action\fP is normally applied to all messages in the source mailbox, +but there are a few exceptions: +.RS 7 +.IP \(bu +When the \fB\-W\fP option is not specified, the source mailbox is immutable and +the specified \fIsource\-action\fP has no effect. +.IP \(bu +When the \fB-M\fR option (move mode) is active, all messages that were +successfully moved to another mailbox are expunged irrespective of the specified +\fIsource\-action\fP. +.IP \(bu +If the filter decides to keep the message in the source mailbox, it is left there +and not affected by the \fIsource\-action\fP. .RE .\"------------------------------------------------------------------------ diff --git a/src/sieve-tools/sieve-filter.c b/src/sieve-tools/sieve-filter.c index 4d359f06ca8216c539fc8f7582dfc874eccabef9..e1fec7328a10c6f878867d3a2c2eda3bb79ccb31 100644 --- a/src/sieve-tools/sieve-filter.c +++ b/src/sieve-tools/sieve-filter.c @@ -6,6 +6,7 @@ #include "ioloop.h" #include "env-util.h" #include "str.h" +#include "str-sanitize.h" #include "ostream.h" #include "array.h" #include "mail-namespace.h" @@ -49,30 +50,46 @@ enum sieve_filter_source_action { SIEVE_FILTER_SACT_EXPUNGE /* Expunge discarded messages */ }; -struct sieve_filter_context { +struct sieve_filter_data { enum sieve_filter_source_action source_action; - const char *move_mailbox; + struct mailbox *move_mailbox; struct sieve_script_env *senv; struct sieve_binary *main_sbin; struct sieve_error_handler *ehandler; + + unsigned int execute:1; + unsigned int source_write:1; + unsigned int move_mode:1; +}; + +struct sieve_filter_context { + const struct sieve_filter_data *data; + + struct mailbox_transaction_context *move_trans; + + struct ostream *teststream; }; static int filter_message (struct sieve_filter_context *sfctx, struct mail *mail) { - struct sieve_error_handler *ehandler = sfctx->ehandler; + struct sieve_error_handler *ehandler = sfctx->data->ehandler; + struct sieve_script_env *senv = sfctx->data->senv; struct sieve_exec_status estatus; struct sieve_binary *sbin; struct sieve_message_data msgdata; const char *recipient, *sender; + bool execute = sfctx->data->execute; + bool source_write = sfctx->data->source_write; + bool move_mode = sfctx->data->move_mode; int ret; sieve_tool_get_envelope_data(mail, &recipient, &sender); /* Initialize execution status */ memset(&estatus, 0, sizeof(estatus)); - sfctx->senv->exec_status = &estatus; + senv->exec_status = &estatus; /* Collect necessary message data */ memset(&msgdata, 0, sizeof(msgdata)); @@ -80,44 +97,91 @@ static int filter_message msgdata.return_path = sender; msgdata.orig_envelope_to = recipient; msgdata.final_envelope_to = recipient; - msgdata.auth_user = sfctx->senv->username; + msgdata.auth_user = senv->username; (void)mail_get_first_header(mail, "Message-ID", &msgdata.id); /* Single script */ - sbin = sfctx->main_sbin; + sbin = sfctx->data->main_sbin; /* Execute script */ - ret = sieve_execute(sbin, &msgdata, sfctx->senv, ehandler, NULL); + if ( execute ) { + ret = sieve_execute(sbin, &msgdata, senv, ehandler, NULL); + } else { + ret = sieve_test + (sbin, &msgdata, senv, ehandler, sfctx->teststream, NULL); + } /* Handle message in source folder */ - if ( ret > 0 && !estatus.keep_original ) { - switch ( sfctx->source_action ) { - /* Leave it there */ - case SIEVE_FILTER_SACT_KEEP: - sieve_info(ehandler, NULL, "message left in source mailbox"); - break; - /* Move message to Trash folder */ - case SIEVE_FILTER_SACT_MOVE: - sieve_info(ehandler, NULL, - "message in source mailbox moved to mailbox '%s'", sfctx->move_mailbox); - break; - /* Flag message as \DELETED */ - case SIEVE_FILTER_SACT_DELETE: - sieve_info(ehandler, NULL, "message flagged as deleted in source mailbox"); - mail_update_flags(mail, MODIFY_ADD, MAIL_DELETED); - break; - /* Expunge the message immediately */ - case SIEVE_FILTER_SACT_EXPUNGE: - sieve_info(ehandler, NULL, "message expunged from source mailbox"); - mail_expunge(mail); - break; - /* Unknown */ - default: - i_unreached(); - break; + if ( ret > 0 ) { + struct mailbox *move_box = sfctx->data->move_mailbox; + enum sieve_filter_source_action source_action = + sfctx->data->source_action; + + if ( !source_write ) { + /* Do nothing */ + + } else if ( estatus.keep_original ) { + sieve_info(ehandler, NULL, "message kept in source mailbox"); + + } else if ( move_mode && estatus.message_saved ) { + sieve_info(ehandler, NULL, + "message expunged from source mailbox upon successful move"); + + if ( execute ) + mail_expunge(mail); + + } else { + switch ( source_action ) { + /* Leave it there */ + case SIEVE_FILTER_SACT_KEEP: + sieve_info(ehandler, NULL, "message left in source mailbox"); + break; + /* Move message to indicated folder */ + case SIEVE_FILTER_SACT_MOVE: + sieve_info(ehandler, NULL, + "message in source mailbox moved to mailbox '%s'", + mailbox_get_name(move_box)); + + if ( execute && move_box != NULL ) { + struct mailbox_transaction_context *t = sfctx->move_trans; + struct mail_save_context *save_ctx; + + save_ctx = mailbox_save_alloc(t); + + if ( mailbox_copy(&save_ctx, mail) < 0 ) { + enum mail_error error; + const char *errstr; + + errstr = mail_storage_get_last_error + (mailbox_get_storage(move_box), &error); + + sieve_error(ehandler, NULL, + "failed to move message to mailbox %s: %s", + mailbox_get_name(move_box), errstr); + return -1; + } + + mail_expunge(mail); + } + break; + /* Flag message as \DELETED */ + case SIEVE_FILTER_SACT_DELETE: + sieve_info(ehandler, NULL, "message flagged as deleted in source mailbox"); + if ( execute ) + mail_update_flags(mail, MODIFY_ADD, MAIL_DELETED); + break; + /* Expunge the message immediately */ + case SIEVE_FILTER_SACT_EXPUNGE: + sieve_info(ehandler, NULL, "message expunged from source mailbox"); + if ( execute ) + mail_expunge(mail); + break; + /* Unknown */ + default: + i_unreached(); + break; + } } - } else { - sieve_info(ehandler, NULL, "message left in source mailbox"); } return ret; @@ -139,32 +203,51 @@ static void mail_search_build_add_flags } static int filter_mailbox -(struct sieve_filter_context *sfctx, struct mailbox *src_box) +(const struct sieve_filter_data *sfdata, struct mailbox *src_box) { - struct sieve_error_handler *ehandler = sfctx->ehandler; + struct sieve_filter_context sfctx; + struct mailbox *move_box = sfdata->move_mailbox; + struct sieve_error_handler *ehandler = sfdata->ehandler; struct mail_search_args *search_args; struct mailbox_transaction_context *t; struct mail_search_context *search_ctx; struct mail *mail; int ret = 1; - /* Sync mailbox */ + /* Sync source mailbox */ if ( mailbox_sync(src_box, MAILBOX_SYNC_FLAG_FULL_READ) < 0 ) { + sieve_error(ehandler, NULL, "failed to sync source mailbox"); return -1; } + /* Initialize */ + + memset(&sfctx, 0, sizeof(sfctx)); + sfctx.data = sfdata; + + /* Create test stream */ + if ( !sfdata->execute ) + sfctx.teststream = o_stream_create_fd(1, 0, FALSE); + + /* Start move mailbox transaction */ + + if ( move_box != NULL ) { + sfctx.move_trans = mailbox_transaction_begin + (move_box, MAILBOX_TRANSACTION_FLAG_EXTERNAL); + } + /* Search non-deleted messages in the source folder */ search_args = mail_search_build_init(); mail_search_build_add_flags(search_args, MAIL_DELETED, TRUE); - /* Iterate through all requested messages */ - t = mailbox_transaction_begin(src_box, 0); search_ctx = mailbox_search_init(t, search_args, NULL); mail_search_args_unref(&search_args); + /* Iterate through all requested messages */ + mail = mail_alloc(t, 0, NULL); while ( ret > 0 && mailbox_search_next(search_ctx, mail) > 0 ) { const char *subject, *date; @@ -186,9 +269,10 @@ static int filter_mailbox subject = ""; sieve_info(ehandler, NULL, - "filtering: [%s; %"PRIuUOFF_T" bytes] %s", date, size, subject); + "filtering: [%s; %"PRIuUOFF_T" bytes] %s", date, size, + str_sanitize(subject, 40)); - ret = filter_message(sfctx, mail); + ret = filter_message(&sfctx, mail); } mail_free(&mail); @@ -198,19 +282,36 @@ static int filter_mailbox ret = -1; } + if ( sfctx.move_trans != NULL ) { + if ( ret < 0 ) { + mailbox_transaction_rollback(&sfctx.move_trans); + } else { + if ( mailbox_transaction_commit(&sfctx.move_trans) < 0 ) { + ret = -1; + } + } + } + if ( ret < 0 ) { mailbox_transaction_rollback(&t); - return -1; } else { if ( mailbox_transaction_commit(&t) < 0 ) { - return -1; + ret = -1; } } + if ( sfctx.teststream != NULL ) + o_stream_destroy(&sfctx.teststream); + + if ( ret < 0 ) return ret; + /* Sync mailbox */ - if ( mailbox_sync(src_box, MAILBOX_SYNC_FLAG_FULL_WRITE) < 0 ) { - return -1; + if ( sfdata->execute ) { + if ( mailbox_sync(src_box, MAILBOX_SYNC_FLAG_FULL_WRITE) < 0 ) { + sieve_error(ehandler, NULL, "failed to sync source mailbox"); + return -1; + } } return ret; @@ -235,30 +336,29 @@ int main(int argc, char **argv) struct sieve_instance *svinst; ARRAY_TYPE (const_string) scriptfiles; const char *scriptfile, *src_mailbox, *dst_mailbox, *move_mailbox; - struct sieve_filter_context sfctx; + struct sieve_filter_data sfdata; enum sieve_filter_source_action source_action = SIEVE_FILTER_SACT_KEEP; struct mail_user *mail_user; struct sieve_binary *main_sbin; struct sieve_script_env scriptenv; struct sieve_error_handler *ehandler; - bool force_compile = FALSE, execute = FALSE, source_write = FALSE; + bool force_compile, execute, source_write, move_mode; struct mail_namespace *ns; - struct mailbox *src_box; + struct mailbox *src_box = NULL, *move_box = NULL; enum mailbox_flags open_flags = MAILBOX_FLAG_KEEP_RECENT | MAILBOX_FLAG_IGNORE_ACLS; enum mail_error error; int c; sieve_tool = sieve_tool_init("sieve-filter", &argc, &argv, - "R:m:s:x:P:CD", FALSE); + "m:s:x:P:q:Q:DCeWM", FALSE); t_array_init(&scriptfiles, 16); /* Parse arguments */ scriptfile = NULL; - src_mailbox = dst_mailbox = "INBOX"; - move_mailbox = "Trash"; - force_compile = FALSE; + src_mailbox = dst_mailbox = move_mailbox = NULL; + force_compile = execute = source_write = move_mode = FALSE; while ((c = sieve_tool_getopt(sieve_tool)) > 0) { switch (c) { case 'm': @@ -278,22 +378,39 @@ int main(int argc, char **argv) "The -s argument is currently NOT IMPLEMENTED"); } break; + case 'q': + i_fatal_status(EX_USAGE, + "The -q argument is currently NOT IMPLEMENTED"); + break; + case 'Q': + i_fatal_status(EX_USAGE, + "The -Q argument is currently NOT IMPLEMENTED"); + break; case 'e': + /* execution mode */ execute = TRUE; break; case 'C': + /* force script compile */ force_compile = TRUE; break; + case 'M': + /* move mode */ + move_mode = TRUE; + break; case 'W': + /* enable source mailbox write */ source_write = TRUE; break; default: + /* unrecognized option */ print_help(); i_fatal_status(EX_USAGE, "Unknown argument: %c", c); break; } } - + + /* Script file argument */ if ( optind < argc ) { scriptfile = t_strdup(argv[optind++]); } else { @@ -301,6 +418,7 @@ int main(int argc, char **argv) i_fatal_status(EX_USAGE, "Missing <script-file> argument"); } + /* Source mailbox argument */ if ( optind < argc ) { src_mailbox = t_strdup(argv[optind++]); } else { @@ -308,6 +426,7 @@ int main(int argc, char **argv) i_fatal_status(EX_USAGE, "Missing <source-mailbox> argument"); } + /* Source action argument */ if ( optind < argc ) { const char *srcact = argv[optind++]; @@ -317,6 +436,11 @@ int main(int argc, char **argv) source_action = SIEVE_FILTER_SACT_MOVE; if ( optind < argc ) { move_mailbox = t_strdup(argv[optind++]); + } else { + print_help(); + i_fatal_status(EX_USAGE, + "Invalid <source-action> argument: " + "the `move' action requires mailbox argument"); } } else if ( strcmp(srcact, "flag") == 0 ) { source_action = SIEVE_FILTER_SACT_DELETE; @@ -333,6 +457,11 @@ int main(int argc, char **argv) i_fatal_status(EX_USAGE, "Unknown argument: %s", argv[optind]); } + if ( dst_mailbox == NULL ) { + dst_mailbox = src_mailbox; + } + + /* Finish tool initialization */ svinst = sieve_tool_init_finish(sieve_tool); /* Register Sieve debug extension */ @@ -355,24 +484,45 @@ int main(int argc, char **argv) /* Initialize mail user */ mail_user = sieve_tool_get_mail_user(sieve_tool); - /* Find namespace for source mailbox */ + /* Open the source mailbox */ + src_mailbox = mailbox_name_to_mutf7(src_mailbox); ns = mail_namespace_find(mail_user->namespaces, &src_mailbox); if ( ns == NULL ) i_fatal("Unknown namespace for source mailbox '%s'", src_mailbox); - /* Open the source mailbox */ - if ( !source_write ) + if ( !source_write || !execute ) open_flags |= MAILBOX_FLAG_READONLY; + src_box = mailbox_alloc(ns->list, src_mailbox, open_flags); - if ( mailbox_open(src_box) < 0 ) { - i_fatal("Couldn't open mailbox '%s': %s", + if ( mailbox_open(src_box) < 0 ) { + i_fatal("Couldn't open source mailbox '%s': %s", src_mailbox, mail_storage_get_last_error(ns->storage, &error)); - } + } + + /* Open move box if necessary */ + + if ( execute && source_action == SIEVE_FILTER_SACT_MOVE && + move_mailbox != NULL ) { + move_mailbox = mailbox_name_to_mutf7(move_mailbox); + ns = mail_namespace_find(mail_user->namespaces, &move_mailbox); + if ( ns == NULL ) + i_fatal("Unknown namespace for mailbox '%s'", move_mailbox); + + move_box = mailbox_alloc(ns->list, move_mailbox, open_flags); + if ( mailbox_open(move_box) < 0 ) { + i_fatal("Couldn't open mailbox '%s': %s", + move_mailbox, mail_storage_get_last_error(ns->storage, &error)); + } + + if ( mailbox_backends_equal(src_box, move_box) ) { + i_fatal("Source mailbox and mailbox for move action are identical."); + } + } /* Compose script environment */ memset(&scriptenv, 0, sizeof(scriptenv)); - scriptenv.mailbox_autocreate = TRUE; + scriptenv.mailbox_autocreate = FALSE; scriptenv.default_mailbox = dst_mailbox; scriptenv.user = mail_user; scriptenv.username = sieve_tool_get_username(sieve_tool); @@ -382,20 +532,27 @@ int main(int argc, char **argv) scriptenv.smtp_close = NULL; /* Compose filter context */ - memset(&sfctx, 0, sizeof(sfctx)); - sfctx.senv = &scriptenv; - sfctx.source_action = source_action; - sfctx.move_mailbox = move_mailbox; - sfctx.main_sbin = main_sbin; - sfctx.ehandler = ehandler; + memset(&sfdata, 0, sizeof(sfdata)); + sfdata.senv = &scriptenv; + sfdata.source_action = source_action; + sfdata.move_mailbox = move_box; + sfdata.main_sbin = main_sbin; + sfdata.ehandler = ehandler; + sfdata.execute = execute; + sfdata.source_write = source_write; + sfdata.move_mode = move_mode; /* Apply Sieve filter to all messages found */ - (void) filter_mailbox(&sfctx, src_box); + (void) filter_mailbox(&sfdata, src_box); - /* Close the mailbox */ + /* Close the source mailbox */ if ( src_box != NULL ) mailbox_free(&src_box); + /* Close the move mailbox */ + if ( move_box != NULL ) + mailbox_free(&move_box); + /* Cleanup error handler */ sieve_error_handler_unref(&ehandler); diff --git a/src/sieve-tools/sieve-test.c b/src/sieve-tools/sieve-test.c index 747399b7c102872b47cb1b6f683ccaef269ee422..f6b1379462297739e09ff7ac89c41db6726e2c6f 100644 --- a/src/sieve-tools/sieve-test.c +++ b/src/sieve-tools/sieve-test.c @@ -156,6 +156,7 @@ int main(int argc, char **argv) /* trace file */ tracefile = optarg; break; + /* trace options */ case 'T': sieve_tool_parse_trace_option(&tr_config, optarg); break; @@ -172,13 +173,16 @@ int main(int argc, char **argv) array_append(&scriptfiles, &file, 1); } break; + /* execution mode */ case 'e': execute = TRUE; break; + /* force script compile */ case 'C': force_compile = TRUE; break; default: + /* unrecognized option */ print_help(); i_fatal_status(EX_USAGE, "Unknown argument: %c", c); break;