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 }