1 /*
   2   dspam plugin for dovecot
   3 
   4   Copyright (C) 2004-2006  Johannes Berg <johannes@sipsolutions.net>
   5                      2006  Frank Cusack
   6 
   7   This program is free software; you can redistribute it and/or modify
   8   it under the terms of the GNU General Public License Version 2 as
   9   published by the Free Software Foundation.
  10 
  11   This program is distributed in the hope that it will be useful,
  12   but WITHOUT ANY WARRANTY; without even the implied warranty of
  13   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14   GNU General Public License for more details.
  15 
  16   You should have received a copy of the GNU General Public License
  17   along with this program; if not, write to the Free Software
  18   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
  19 
  20   based on the original framework http://www.dovecot.org/patches/1.0/copy_plugin.c
  21 
  22   Please see http://johannes.sipsolutions.net/wiki/Projects/dovecot-dspam-integration
  23   for more information on this code.
  24 
  25   To compile:
  26   make "plugins" directory right beside "src" in the dovecot source tree,
  27   copy this into there and run
  28 
  29   cc -fPIC -shared -Wall \
  30     -I../src/ \
  31     -I../src/lib \
  32     -I.. \
  33     -I../src/lib-storage \
  34     -I../src/lib-mail \
  35     -I../src/lib-imap \
  36     -I../src/imap/ \
  37     -DHAVE_CONFIG_H \
  38     -DDSPAM=\"/path/to/dspam\" \
  39     dspam.c -o lib_dspam.so
  40 
  41   (if you leave out -DDSPAM=... then /usr/bin/dspam is taken as default)
  42 
  43   Install the plugin in the usual dovecot module location.
  44 */
  45 
  46 /*
  47  * If you need to ignore a trash folder, define a trash folder
  48  * name as follows, or alternatively give -DIGNORE_TRASH_NAME=\"Trash\" on
  49  * the cc command line.
  50  */
  51 /*#define IGNORE_TRASH_NAME "Trash"*/
  52 
  53 #include "common.h"
  54 #include "str.h"
  55 #include "strfuncs.h"
  56 #include "commands.h"
  57 #include "imap-search.h"
  58 #include "lib-storage/mail-storage.h"
  59 #include "lib/mempool.h"
  60 #include "mail-storage.h"
  61 #include <unistd.h>
  62 #include <sys/wait.h>
  63 #include <stdlib.h>
  64 #include <stdio.h>
  65 #include <errno.h>
  66 #include <fcntl.h>
  67 #ifdef DEBUG
  68 #include <syslog.h>
  69 #endif
  70 
  71 #define SIGHEADERLINE "X-DSPAM-Signature"
  72 #define MAXSIGLEN 100
  73 
  74 #ifndef DSPAM
  75 #define DSPAM "/usr/bin/dspam"
  76 #endif /* DSPAM */
  77 
  78 static int
  79 call_dspam(const char* signature, int is_spam)
  80 {
  81     pid_t pid;
  82     int s;
  83     char class_arg[16+2];
  84     char sign_arg[MAXSIGLEN+2];
  85     int pipes[2];
  86 
  87     s = snprintf(sign_arg, 101, "--signature=%s", signature);
  88     if ( s > MAXSIGLEN || s <= 0) return -1;
  89 
  90     snprintf(class_arg, 17, "--class=%s", is_spam ? "spam" : "innocent");
  91 
  92     pipe(pipes); /* for dspam stderr */
  93 
  94     pid = fork();
  95     if (pid < 0) return -1;
  96 
  97     if (pid) {
  98         int status;
  99         /* well. dspam doesn't report an error if it has an error,
 100            but instead only prints stuff to stderr. Usually, it
 101            won't print anything, so we treat it having output as
 102            an error condition */
 103 
 104         char buf[1024];
 105         int readsize;
 106         close(pipes[1]);
 107 
 108         do {
 109             readsize = read(pipes[0], buf, 1024);
 110             if (readsize < 0) {
 111                 readsize = -1;
 112                 if (errno == EINTR) readsize = -2;
 113             }
 114         } while (readsize == -2);
 115 
 116         if (readsize != 0) {
 117             close(pipes[0]);
 118             return -1;
 119         }
 120 
 121         waitpid (pid, &status, 0);
 122         if (!WIFEXITED(status)) {
 123             close(pipes[0]);
 124             return -1;
 125         }
 126 
 127         readsize = read(pipes[0], buf, 1024);
 128         if (readsize != 0) {
 129             close(pipes[0]);
 130             return -1;
 131         }
 132 
 133         close(pipes[0]);
 134         return WEXITSTATUS(status);
 135     } else {
 136         int fd = open("/dev/null", O_RDONLY);
 137         close(0); close(1); close(2);
 138         /* see above */
 139         close(pipes[0]);
 140 
 141         if (dup2(pipes[1], 2) != 2) {
 142             exit(1);
 143         }
 144         if (dup2(pipes[1], 1) != 1) {
 145             exit(1);
 146         }
 147         close(pipes[1]);
 148 
 149         if (dup2(fd, 0) != 0) {
 150             exit(1);
 151         }
 152         close(fd);
 153 
 154 #ifdef DEBUG
 155         syslog(LOG_INFO, DSPAM " --source=error --stdout %s %s", class_arg, sign_arg);
 156 #endif
 157         execl (DSPAM, DSPAM, "--source=error", "--stdout", class_arg, sign_arg, NULL);
 158         exit(127); /* fall through if dspam can't be found */
 159         return -1; /* never executed */
 160     }
 161 }
 162 
 163 struct dspam_signature_list {
 164     struct dspam_signature_list * next;
 165     char * sig;
 166 };
 167 typedef struct dspam_signature_list * siglist_t;
 168 
 169 static siglist_t list_append(pool_t pool, siglist_t * list) {
 170     siglist_t l = *list;
 171     siglist_t p = NULL;
 172     siglist_t n;
 173     
 174     while (l != NULL) {
 175     p = l;
 176     l = l->next;
 177     }
 178     n = p_malloc (pool, sizeof(struct dspam_signature_list));
 179     n->next = NULL;
 180     n->sig = NULL;
 181     if (p == NULL) {
 182     *list = n;
 183     } else {
 184     p->next = n;
 185     }
 186     return n;
 187 }
 188 
 189 static int
 190 fetch_and_copy_reclassified (struct mailbox_transaction_context *t, 
 191                              struct mailbox *srcbox,
 192                              struct mail_search_arg *search_args,
 193                              int is_spam,
 194                              int *enh_error)
 195 {
 196     struct mail_search_context *search_ctx;
 197     struct mailbox_transaction_context *src_trans;
 198     struct mail_keywords *keywords;
 199     const char *const *keywords_list;
 200     struct mail *mail;
 201     int ret;
 202 
 203     const char* signature;
 204     struct dspam_signature_list * siglist = NULL;
 205     pool_t listpool = pool_alloconly_create("dspam-siglist-pool", 1024);
 206     
 207     *enh_error = 0;
 208 
 209     src_trans = mailbox_transaction_begin(srcbox, 0);
 210     search_ctx = mailbox_search_init(src_trans, NULL, search_args, NULL);
 211     
 212     mail = mail_alloc(src_trans, MAIL_FETCH_STREAM_HEADER |
 213                           MAIL_FETCH_STREAM_BODY, NULL);
 214         ret = 1;
 215         while (mailbox_search_next(search_ctx, mail) > 0 && ret > 0) {
 216                 if (mail->expunged) {
 217                         ret = 0;
 218                         break;
 219                 }
 220 
 221         signature = mail_get_first_header(mail, SIGHEADERLINE);
 222         if (is_empty_str(signature)) {
 223             ret = -1;
 224             *enh_error = -2;
 225             break;
 226         }
 227         list_append(listpool, &siglist)->sig = p_strdup(listpool, signature);
 228 
 229         keywords_list = mail_get_keywords(mail);
 230         keywords = strarray_length(keywords_list) == 0 ? NULL :
 231             mailbox_keywords_create(t, keywords_list);
 232         if (mailbox_copy(t, mail, mail_get_flags(mail),
 233                 keywords, NULL) < 0)
 234             ret = -1;
 235         mailbox_keywords_free(t, &keywords);
 236         }
 237         mail_free(&mail);
 238 
 239         if (mailbox_search_deinit(&search_ctx) < 0)
 240                 ret = -1;
 241 
 242     /* got all signatures now, walk them passing to dspam */
 243     while (siglist) {
 244         if ((*enh_error = call_dspam (siglist->sig, is_spam))) {
 245         ret = -1;
 246         break;
 247         }
 248         siglist = siglist->next;
 249     }
 250     
 251     pool_unref (listpool);
 252 
 253     if (*enh_error) {
 254         mailbox_transaction_rollback(&src_trans);
 255     } else {
 256             if (mailbox_transaction_commit(&src_trans, 0) < 0)
 257                     ret = -1;
 258     }
 259 
 260     return ret;
 261 }
 262 
 263 static bool cmd_append_spam_plugin(struct client_command_context *cmd)
 264 {
 265     const char *mailbox;
 266     struct mail_storage *storage;
 267     struct mailbox *box;
 268 
 269     /* <mailbox> */
 270     if (!client_read_string_args(cmd, 1, &mailbox))
 271         return FALSE;
 272     
 273     storage = client_find_storage(cmd, &mailbox);
 274     if (storage == NULL)
 275         return FALSE;
 276     /* TODO: is this really the best way to handle this? maybe more logic could be provided */
 277     box = mailbox_open(storage, mailbox, NULL, MAILBOX_OPEN_FAST | MAILBOX_OPEN_KEEP_RECENT);
 278     if (box != NULL)
 279     {
 280     
 281     
 282         if (mailbox_equals(box, storage, "SPAM"))
 283         {
 284             mailbox_close(&box);
 285             return cmd_sync (cmd, 0, 0, "NO Cannot APPEND to SPAM box, sorry.");
 286         }
 287         
 288         mailbox_close(&box);
 289     }
 290     
 291     return cmd_append (cmd);
 292 }
 293 
 294 static bool cmd_copy_spam_plugin(struct client_command_context *cmd)
 295 {
 296     struct client *client = cmd->client;
 297     struct mail_storage *storage;
 298     struct mailbox *destbox;
 299     struct mailbox_transaction_context *t;
 300     struct mail_search_arg *search_arg;
 301     const char *messageset, *mailbox;
 302     enum mailbox_sync_flags sync_flags = 0;
 303     int ret;
 304     int spam_folder = 0;
 305     int enh_error = 0, is_spam;
 306 #ifdef IGNORE_TRASH_NAME
 307     int is_trash;
 308     int trash_folder = 0;
 309 #endif
 310     struct mailbox *box;
 311     
 312     /* <message set> <mailbox> */
 313     if (!client_read_string_args(cmd, 2, &messageset, &mailbox))
 314         return FALSE;
 315 
 316     if (!client_verify_open_mailbox(cmd))
 317         return TRUE;
 318 
 319     storage = client_find_storage(cmd, &mailbox);
 320     if (storage == NULL)
 321         return FALSE;
 322     box = mailbox_open(storage, mailbox, NULL, MAILBOX_OPEN_FAST | MAILBOX_OPEN_KEEP_RECENT);
 323     if (!box) {
 324         client_send_storage_error(cmd, storage);
 325         return TRUE;
 326     }
 327 
 328     is_spam = mailbox_equals(box, storage, "SPAM");
 329     spam_folder = is_spam || mailbox_equals(cmd->client->mailbox, storage, "SPAM");
 330 #ifdef IGNORE_TRASH_NAME
 331     is_trash = mailbox_equals(box, storage, IGNORE_TRASH_NAME);
 332     trash_folder = is_trash || mailbox_equals(cmd->client->mailbox, storage, IGNORE_TRASH_NAME);
 333 #endif
 334 
 335     mailbox_close(&box);
 336 
 337     /* only act on spam */
 338     if (!spam_folder)
 339         return cmd_copy(cmd);
 340 #ifdef IGNORE_TRASH_NAME
 341     /* ignore any mail going into or out of trash
 342      * This means users can circumvent re-classification
 343      * by moving into trash and then out again...
 344      * All in all, it may be a better idea to not use
 345      * a Trash folder at all :) */
 346     if (trash_folder)
 347         return cmd_copy(cmd);
 348 #endif
 349 
 350     /* otherwise, do (almost) everything the copy would have done */
 351     /* open the destination mailbox */
 352     if (!client_verify_mailbox_name(cmd, mailbox, TRUE, FALSE))
 353         return TRUE;
 354     
 355     search_arg = imap_search_get_arg(cmd, messageset, cmd->uid);
 356     if (search_arg == NULL)
 357         return TRUE;
 358 
 359     storage = client_find_storage(cmd, &mailbox);
 360     if (storage == NULL)
 361         return TRUE;
 362 
 363     if (mailbox_equals(client->mailbox, storage, mailbox))
 364         destbox = client->mailbox;
 365     else {
 366         destbox = mailbox_open(storage, mailbox, NULL,
 367                                MAILBOX_OPEN_FAST | 
 368                                MAILBOX_OPEN_KEEP_RECENT);
 369         if (destbox == NULL) {
 370             client_send_storage_error(cmd, storage);
 371             return TRUE;
 372         }
 373     }
 374     
 375     t = mailbox_transaction_begin(destbox,
 376                                   MAILBOX_TRANSACTION_FLAG_EXTERNAL);
 377     ret = fetch_and_copy_reclassified(t, client->mailbox, search_arg, is_spam, &enh_error);
 378 
 379     if (ret <= 0)
 380         mailbox_transaction_rollback(&t);
 381     else {
 382         if (mailbox_transaction_commit(&t, 0) < 0)
 383             ret = -1;
 384     }
 385 
 386     if (destbox != client->mailbox) {
 387         sync_flags |= MAILBOX_SYNC_FLAG_FAST;
 388         mailbox_close(&destbox);
 389     }
 390 
 391     if (ret > 0)
 392         return cmd_sync(cmd, sync_flags, 0, "OK Copy completed.");
 393     else if (ret == 0) {
 394         /* some messages were expunged, sync them */
 395         return cmd_sync(cmd, 0, 0,
 396             "NO Some of the requested messages no longer exist.");
 397     } else {
 398         switch (enh_error) {
 399         case -2:
 400             return cmd_sync(cmd, 0, 0, "NO Some messages did not have " SIGHEADERLINE " header line");
 401             break;
 402         case -3:
 403             return cmd_sync(cmd, 0, 0, "NO Failed to call dspam");
 404             break;
 405         case 0:
 406             client_send_storage_error(cmd, storage);
 407             return TRUE;
 408             break;
 409         default:
 410             return cmd_sync(cmd, 0, 0, "NO dspam failed");
 411             break;
 412         }
 413     }
 414     
 415     return TRUE;
 416 }
 417 
 418 void dspam_init(void)
 419 {
 420     command_unregister("COPY");
 421     command_unregister("APPEND");
 422     command_unregister("UID COPY");
 423     /* i_strdup() here is a kludge to avoid crashing in commands_deinit()
 424      * since modules are unloaded before it's called, this "COPY" string
 425      * would otherwise point to nonexisting memory. */
 426     command_register(i_strdup("COPY"), cmd_copy_spam_plugin);
 427     command_register(i_strdup("UID COPY"), cmd_copy_spam_plugin);
 428     command_register(i_strdup("APPEND"), cmd_append_spam_plugin);
 429 }
 430 
 431 void dspam_deinit(void)
 432 {
 433 }