diff options
-rw-r--r-- | plugin.c | 449 |
1 files changed, 449 insertions, 0 deletions
diff --git a/plugin.c b/plugin.c new file mode 100644 index 0000000..ddff171 --- /dev/null +++ b/plugin.c @@ -0,0 +1,449 @@ +#format CPLUSPLUS +/* + dspam plugin for dovecot + + Copyright (C) 2004-2006 Johannes Berg <johannes@sipsolutions.net> + 2006 Frank Cusack + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License Version 2 as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + based on the original framework http://www.dovecot.org/patches/1.0/copy_plugin.c + + Please see http://johannes.sipsolutions.net/wiki/Projects/dovecot-dspam-integration + for more information on this code. + + To compile: + make "plugins" directory right beside "src" in the dovecot source tree, + copy this into there and run + + cc -fPIC -shared -Wall \ + -I../src/ \ + -I../src/lib \ + -I.. \ + -I../src/lib-storage \ + -I../src/lib-mail \ + -I../src/lib-imap \ + -I../src/imap/ \ + -DHAVE_CONFIG_H \ + -DDSPAM=\"/path/to/dspam\" \ + dspam.c -o lib_dspam.so + + (if you leave out -DDSPAM=... then /usr/bin/dspam is taken as default) + + Install the plugin in the usual dovecot module location. +*/ + +/* + * If you need to ignore a trash folder, define a trash folder + * name as follows, or alternatively give -DIGNORE_TRASH_NAME=\"Trash\" on + * the cc command line. + */ +/*#define IGNORE_TRASH_NAME "Trash"*/ + +#include "common.h" +#include "str.h" +#include "strfuncs.h" +#include "commands.h" +#include "imap-search.h" +#include "lib-storage/mail-storage.h" +#include "lib/mempool.h" +#include "mail-storage.h" +#include <unistd.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <fcntl.h> +#ifdef DEBUG +#include <syslog.h> +#endif + +#define SIGHEADERLINE "X-DSPAM-Signature" +#define MAXSIGLEN 100 + +#ifndef DSPAM +#define DSPAM "/usr/bin/dspam" +#endif /* DSPAM */ + +static int call_dspam(const char *signature, int is_spam) +{ + pid_t pid; + int s; + char class_arg[16 + 2]; + char sign_arg[MAXSIGLEN + 2]; + int pipes[2]; + + s = snprintf(sign_arg, 101, "--signature=%s", signature); + if (s > MAXSIGLEN || s <= 0) + return -1; + + snprintf(class_arg, 17, "--class=%s", is_spam ? "spam" : "innocent"); + + pipe(pipes); /* for dspam stderr */ + + pid = fork(); + if (pid < 0) + return -1; + + if (pid) { + int status; + /* well. dspam doesn't report an error if it has an error, + but instead only prints stuff to stderr. Usually, it + won't print anything, so we treat it having output as + an error condition */ + + char buf[1024]; + int readsize; + close(pipes[1]); + + do { + readsize = read(pipes[0], buf, 1024); + if (readsize < 0) { + readsize = -1; + if (errno == EINTR) + readsize = -2; + } + } while (readsize == -2); + + if (readsize != 0) { + close(pipes[0]); + return -1; + } + + waitpid(pid, &status, 0); + if (!WIFEXITED(status)) { + close(pipes[0]); + return -1; + } + + readsize = read(pipes[0], buf, 1024); + if (readsize != 0) { + close(pipes[0]); + return -1; + } + + close(pipes[0]); + return WEXITSTATUS(status); + } else { + int fd = open("/dev/null", O_RDONLY); + close(0); + close(1); + close(2); + /* see above */ + close(pipes[0]); + + if (dup2(pipes[1], 2) != 2) { + exit(1); + } + if (dup2(pipes[1], 1) != 1) { + exit(1); + } + close(pipes[1]); + + if (dup2(fd, 0) != 0) { + exit(1); + } + close(fd); + +#ifdef DEBUG + syslog(LOG_INFO, DSPAM " --source=error --stdout %s %s", + class_arg, sign_arg); +#endif + execl(DSPAM, DSPAM, "--source=error", "--stdout", class_arg, + sign_arg, NULL); + exit(127); /* fall through if dspam can't be found */ + return -1; /* never executed */ + } +} + +struct dspam_signature_list { + struct dspam_signature_list *next; + char *sig; +}; +typedef struct dspam_signature_list *siglist_t; + +static siglist_t list_append(pool_t pool, siglist_t * list) +{ + siglist_t l = *list; + siglist_t p = NULL; + siglist_t n; + + while (l != NULL) { + p = l; + l = l->next; + } + n = p_malloc(pool, sizeof(struct dspam_signature_list)); + n->next = NULL; + n->sig = NULL; + if (p == NULL) { + *list = n; + } else { + p->next = n; + } + return n; +} + +static int +fetch_and_copy_reclassified(struct mailbox_transaction_context *t, + struct mailbox *srcbox, + struct mail_search_arg *search_args, + int is_spam, int *enh_error) +{ + struct mail_search_context *search_ctx; + struct mailbox_transaction_context *src_trans; + struct mail_keywords *keywords; + const char *const *keywords_list; + struct mail *mail; + int ret; + + const char *signature; + struct dspam_signature_list *siglist = NULL; + pool_t listpool = pool_alloconly_create("dspam-siglist-pool", 1024); + + *enh_error = 0; + + src_trans = mailbox_transaction_begin(srcbox, 0); + search_ctx = mailbox_search_init(src_trans, NULL, search_args, NULL); + + mail = mail_alloc(src_trans, MAIL_FETCH_STREAM_HEADER | + MAIL_FETCH_STREAM_BODY, NULL); + ret = 1; + while (mailbox_search_next(search_ctx, mail) > 0 && ret > 0) { + if (mail->expunged) { + ret = 0; + break; + } + + signature = mail_get_first_header(mail, SIGHEADERLINE); + if (is_empty_str(signature)) { + ret = -1; + *enh_error = -2; + break; + } + list_append(listpool, &siglist)->sig = + p_strdup(listpool, signature); + + keywords_list = mail_get_keywords(mail); + keywords = strarray_length(keywords_list) == 0 ? NULL : + mailbox_keywords_create(t, keywords_list); + if (mailbox_copy(t, mail, mail_get_flags(mail), + keywords, NULL) < 0) + ret = -1; + mailbox_keywords_free(t, &keywords); + } + mail_free(&mail); + + if (mailbox_search_deinit(&search_ctx) < 0) + ret = -1; + + /* got all signatures now, walk them passing to dspam */ + while (siglist) { + if ((*enh_error = call_dspam(siglist->sig, is_spam))) { + ret = -1; + break; + } + siglist = siglist->next; + } + + pool_unref(listpool); + + if (*enh_error) { + mailbox_transaction_rollback(&src_trans); + } else { + if (mailbox_transaction_commit(&src_trans, 0) < 0) + ret = -1; + } + + return ret; +} + +static bool cmd_append_spam_plugin(struct client_command_context *cmd) +{ + const char *mailbox; + struct mail_storage *storage; + struct mailbox *box; + + /* <mailbox> */ + if (!client_read_string_args(cmd, 1, &mailbox)) + return FALSE; + + storage = client_find_storage(cmd, &mailbox); + if (storage == NULL) + return FALSE; + /* TODO: is this really the best way to handle this? maybe more logic could be provided */ + box = + mailbox_open(storage, mailbox, NULL, + MAILBOX_OPEN_FAST | MAILBOX_OPEN_KEEP_RECENT); + if (box != NULL) { + + if (mailbox_equals(box, storage, "SPAM")) { + mailbox_close(&box); + return cmd_sync(cmd, 0, 0, + "NO Cannot APPEND to SPAM box, sorry."); + } + + mailbox_close(&box); + } + + return cmd_append(cmd); +} + +static bool cmd_copy_spam_plugin(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct mail_storage *storage; + struct mailbox *destbox; + struct mailbox_transaction_context *t; + struct mail_search_arg *search_arg; + const char *messageset, *mailbox; + enum mailbox_sync_flags sync_flags = 0; + int ret; + int spam_folder = 0; + int enh_error = 0, is_spam; +#ifdef IGNORE_TRASH_NAME + int is_trash; + int trash_folder = 0; +#endif + struct mailbox *box; + + /* <message set> <mailbox> */ + if (!client_read_string_args(cmd, 2, &messageset, &mailbox)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + storage = client_find_storage(cmd, &mailbox); + if (storage == NULL) + return FALSE; + box = + mailbox_open(storage, mailbox, NULL, + MAILBOX_OPEN_FAST | MAILBOX_OPEN_KEEP_RECENT); + if (!box) { + client_send_storage_error(cmd, storage); + return TRUE; + } + + is_spam = mailbox_equals(box, storage, "SPAM"); + spam_folder = is_spam + || mailbox_equals(cmd->client->mailbox, storage, "SPAM"); +#ifdef IGNORE_TRASH_NAME + is_trash = mailbox_equals(box, storage, IGNORE_TRASH_NAME); + trash_folder = is_trash + || mailbox_equals(cmd->client->mailbox, storage, IGNORE_TRASH_NAME); +#endif + + mailbox_close(&box); + + /* only act on spam */ + if (!spam_folder) + return cmd_copy(cmd); +#ifdef IGNORE_TRASH_NAME + /* ignore any mail going into or out of trash + * This means users can circumvent re-classification + * by moving into trash and then out again... + * All in all, it may be a better idea to not use + * a Trash folder at all :) */ + if (trash_folder) + return cmd_copy(cmd); +#endif + + /* otherwise, do (almost) everything the copy would have done */ + /* open the destination mailbox */ + if (!client_verify_mailbox_name(cmd, mailbox, TRUE, FALSE)) + return TRUE; + + search_arg = imap_search_get_arg(cmd, messageset, cmd->uid); + if (search_arg == NULL) + return TRUE; + + storage = client_find_storage(cmd, &mailbox); + if (storage == NULL) + return TRUE; + + if (mailbox_equals(client->mailbox, storage, mailbox)) + destbox = client->mailbox; + else { + destbox = mailbox_open(storage, mailbox, NULL, + MAILBOX_OPEN_FAST | + MAILBOX_OPEN_KEEP_RECENT); + if (destbox == NULL) { + client_send_storage_error(cmd, storage); + return TRUE; + } + } + + t = mailbox_transaction_begin(destbox, + MAILBOX_TRANSACTION_FLAG_EXTERNAL); + ret = + fetch_and_copy_reclassified(t, client->mailbox, search_arg, is_spam, + &enh_error); + + if (ret <= 0) + mailbox_transaction_rollback(&t); + else { + if (mailbox_transaction_commit(&t, 0) < 0) + ret = -1; + } + + if (destbox != client->mailbox) { + sync_flags |= MAILBOX_SYNC_FLAG_FAST; + mailbox_close(&destbox); + } + + if (ret > 0) + return cmd_sync(cmd, sync_flags, 0, "OK Copy completed."); + else if (ret == 0) { + /* some messages were expunged, sync them */ + return cmd_sync(cmd, 0, 0, + "NO Some of the requested messages no longer exist."); + } else { + switch (enh_error) { + case -2: + return cmd_sync(cmd, 0, 0, + "NO Some messages did not have " + SIGHEADERLINE " header line"); + break; + case -3: + return cmd_sync(cmd, 0, 0, "NO Failed to call dspam"); + break; + case 0: + client_send_storage_error(cmd, storage); + return TRUE; + break; + default: + return cmd_sync(cmd, 0, 0, "NO dspam failed"); + break; + } + } + + return TRUE; +} + +void dspam_init(void) +{ + command_unregister("COPY"); + command_unregister("APPEND"); + command_unregister("UID COPY"); + /* i_strdup() here is a kludge to avoid crashing in commands_deinit() + * since modules are unloaded before it's called, this "COPY" string + * would otherwise point to nonexisting memory. */ + command_register(i_strdup("COPY"), cmd_copy_spam_plugin); + command_register(i_strdup("UID COPY"), cmd_copy_spam_plugin); + command_register(i_strdup("APPEND"), cmd_append_spam_plugin); +} + +void dspam_deinit(void) +{ +} |