test code

/*
  dspam plugin for dovecot

  based on the original framework http://www.dovecot.org/patches/1.0/copy_plugin.c

  Author: Johannes Berg <johannes@sipsolutions.net>

  Please see https://johannes.sipsolutions.net/wiki/Projects/dovecot-dspam-integration
  for more information on this code.

  NOTE: You shouldn't be using this code. It has a bug (it doesn't rollback the
        transaction if dspam fails) and the dovecot version it is for is outdated.

  To compile:
  make "plugins" directory right beside "src" in the dovecot source tree,
  copy this into there and run

  gcc -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 spam_plugin.c -o spam_plugin.so

  Install the plugin in the usual dovecot module location.

  Copyright (C) 2004-2006  Johannes Berg

  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
 */

#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 <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>

#define SIGHEADERLINE "X-DSPAM-Signature"
#define MAXSIGLEN 100

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);

                execl ("/usr/bin/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(siglist_t));
    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 *mail;
        const char* signature;
        string_t *dest_str;
        int ret;
        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, FALSE);
        search_ctx = mailbox_search_init(src_trans, NULL, search_args, NULL,
                                         MAIL_FETCH_STREAM_HEADER |
                                         MAIL_FETCH_STREAM_BODY, NULL);
        if (search_ctx == NULL) {
                mailbox_transaction_rollback(src_trans);
                return -1;
        }

        dest_str = t_str_new(128);

        ret = 1;
        while ((mail = mailbox_search_next(search_ctx)) != NULL) {
                if (mail->expunged) {
                        ret = 0;
                        break;
                }

                signature = mail->get_header(mail, SIGHEADERLINE);
                if is_empty_str(signature) {
                        ret = -1;
                        *enh_error = -2;
                        break;
                }
                list_append(listpool, &siglist)->sig = p_strdup(listpool, signature);

                if (mailbox_copy(t, mail, NULL) < 0) {
                        ret = -1;
                        break;
                }
        }

        /* 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 (mailbox_search_deinit(search_ctx) < 0)
                ret = -1;

        if (mailbox_transaction_commit(src_trans, 0) < 0)
                ret = -1;

        return ret;
}

static int cmd_append_spam_plugin(struct client *client)
{
        const char *mailbox;

        /* <mailbox> */
        if (!client_read_string_args(client, 1, &mailbox))
                return FALSE;

        /* TODO: is this really the best way to handle this? maybe more logic could be provided */
        if (mailbox_name_equals(mailbox, "SPAM"))
        {
                return cmd_sync (client, 0, "NO Cannot APPEND to SPAM box, sorry.");
        }

        return cmd_append (client);
}

static int cmd_copy_spam_plugin(struct client *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;

        /* <message set> <mailbox> */
        if (!client_read_string_args(client, 2, &messageset, &mailbox))
                return FALSE;

        /* otherwise, do (almost) everything the copy would have done */
        if (!client_verify_open_mailbox(client))
                return TRUE;

        spam_folder = mailbox_name_equals(mailbox_get_name(client->mailbox), "SPAM")
                   || mailbox_name_equals(mailbox, "SPAM");

        /* only act on spam */
        if ( !spam_folder ) {
                return cmd_copy(client); /* do nothing but copying */
        }
        /* open the destination mailbox */
        if (!client_verify_mailbox_name(client, mailbox, TRUE, FALSE))
                return TRUE;

        search_arg = imap_search_get_arg(client, messageset, client->cmd_uid);
        if (search_arg == NULL)
                return TRUE;

        storage = client_find_storage(client, &mailbox);
        if (storage == NULL)
                return TRUE;

        if (mailbox_name_equals(mailbox_get_name(client->mailbox), mailbox))
                destbox = client->mailbox;
        else {
                destbox = mailbox_open(storage, mailbox, MAILBOX_OPEN_FAST |
                                       MAILBOX_OPEN_KEEP_RECENT);
                if (destbox == NULL) {
                        client_send_storage_error(client, storage);
                        return TRUE;
                }
        }

        t = mailbox_transaction_begin(destbox, FALSE);

        ret = fetch_and_copy_reclassified(t, client->mailbox, search_arg, mailbox_name_equals(mailbox, "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(client, sync_flags, "OK Copy completed.");
        } else if (ret == 0) {
                /* some messages were expunged, sync them */
                return cmd_sync(client, 0,
                        "NO Some of the requested messages no longer exist.");
        } else {
                switch (enh_error) {
                case -2:
                        return cmd_sync(client, 0, "NO Some messages did not have " SIGHEADERLINE " header line");
                        break;
                case -3:
                        return cmd_sync(client, 0, "NO Failed to call dspam");
                        break;
                case 0:
                        client_send_storage_error(client, storage);
                        return TRUE;
                        break;
                default:
                        return cmd_sync(client, 0, "NO dspam failed");
                        break;
                }
        }

        return TRUE;
}

void spam_plugin_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 spam_plugin_deinit(void)
{
}