aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexander Sulfrian <alexander@sulfrian.net>2009-06-04 03:22:53 +0200
committerAlexander Sulfrian <alexander@sulfrian.net>2009-06-04 03:22:53 +0200
commit26730e6192cd6fa218a4a900612d6eb8236ebeff (patch)
treedd5e4ef5b911e283bec203c905090bb9eefac95e
parentdb3c7e32a49b5fe6ddba18eab16e9258c26bfcf1 (diff)
parent07ed21cfec312813f3dca9ccd30e40431909a091 (diff)
downloaddovecot-antispam-26730e6192cd6fa218a4a900612d6eb8236ebeff.tar.gz
dovecot-antispam-26730e6192cd6fa218a4a900612d6eb8236ebeff.tar.xz
dovecot-antispam-26730e6192cd6fa218a4a900612d6eb8236ebeff.zip
Merge branch 'master' of http://git.sipsolutions.net/dovecot-antispam
-rw-r--r--Makefile2
-rw-r--r--antispam-plugin.h2
-rw-r--r--antispam-storage-1.1.c3
-rw-r--r--antispam-storage-1.2.c488
-rw-r--r--antispam.717
-rw-r--r--defconfig2
-rw-r--r--dovecot-version.c2
-rw-r--r--mailtrain.c2
-rw-r--r--spool2dir.c268
9 files changed, 781 insertions, 5 deletions
diff --git a/Makefile b/Makefile
index bcffd34..237bceb 100644
--- a/Makefile
+++ b/Makefile
@@ -89,7 +89,7 @@ clean:
rm -f *.so *.o *~ dovecot-version dovecot-version.h antispam-version.h
install: all
- install -o $(USER) -g $(GROUP) -m 0755 $(LIBRARY_NAME) $(INSTALLDIR)/
+ install -o $(USER) -g $(GROUP) -m 0755 $(LIBRARY_NAME) $(DESTDIR)$(INSTALLDIR)/
verify_config:
@if [ ! -r $(CONFIG) ]; then \
diff --git a/antispam-plugin.h b/antispam-plugin.h
index 468d43e..df59e88 100644
--- a/antispam-plugin.h
+++ b/antispam-plugin.h
@@ -84,7 +84,7 @@ extern bool need_folder_hook;
* Dovecot version compat code
*/
-#if DOVECOT_VERSION_CODE(1, 1) == DOVECOT_VERSION
+#if DOVECOT_VERSION_CODE(1, 1) == DOVECOT_VERSION || DOVECOT_VERSION_CODE(1, 2) == DOVECOT_VERSION
#define __attr_unused__ ATTR_UNUSED
#define ME(err) MAIL_ERROR_ ##err,
#define PLUGIN_ID uint32_t PLUGIN_FUNCTION(id) = 0
diff --git a/antispam-storage-1.1.c b/antispam-storage-1.1.c
index 348ae9d..80c80e3 100644
--- a/antispam-storage-1.1.c
+++ b/antispam-storage-1.1.c
@@ -167,7 +167,8 @@ static int antispam_save_init(struct mailbox_transaction_context *t,
timezone_offset, from_envelope,
input, dest_mail, ctx_r);
- (*ctx_r)->dest_mail = dest_mail;
+ if (ret >= 0)
+ (*ctx_r)->dest_mail = dest_mail;
return ret;
}
diff --git a/antispam-storage-1.2.c b/antispam-storage-1.2.c
new file mode 100644
index 0000000..54b8f7b
--- /dev/null
+++ b/antispam-storage-1.2.c
@@ -0,0 +1,488 @@
+/*
+ * Storage implementation for antispam plugin
+ * Copyright 2007-2008 Johannes Berg <johannes@sipsolutions.net>
+ * Copyright 2009 Jonas Maurus <jonas@maurus.net>
+ *
+ * Derived from Quota plugin:
+ * Copyright (C) 2005 Timo Sirainen
+ */
+
+#include <sys/stat.h>
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "mail-search.h"
+#include "mail-index.h"
+#include "mailbox-list-private.h"
+#include "mail-storage-private.h"
+
+#include "antispam-plugin.h"
+
+#define ANTISPAM_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, antispam_storage_module)
+#define ANTISPAM_MAIL_CONTEXT(obj) \
+ MODULE_CONTEXT(obj, antispam_mail_module)
+
+static MODULE_CONTEXT_DEFINE_INIT(antispam_storage_module,
+ &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(antispam_mail_module,
+ &mail_module_register);
+
+
+enum mailbox_move_type {
+ MMT_APPEND,
+ MMT_UNINTERESTING,
+ MMT_TO_CLEAN,
+ MMT_TO_SPAM,
+};
+
+struct antispam_internal_context {
+ union mailbox_transaction_module_context module_ctx;
+ struct antispam_transaction_context *backendctx;
+ struct mail *mail;
+};
+
+static enum classification move_to_class(enum mailbox_move_type tp)
+{
+ switch (tp) {
+ case MMT_TO_CLEAN:
+ return CLASS_NOTSPAM;
+ case MMT_TO_SPAM:
+ return CLASS_SPAM;
+ default:
+ i_assert(0);
+ }
+}
+
+struct antispam_mailbox {
+ union mailbox_module_context module_ctx;
+
+ enum mailbox_move_type movetype;
+
+ /* used to check if copy was implemented with save */
+ unsigned int save_hack:1;
+};
+
+static uint32_t antispam_storage_module_id = 0;
+static bool antispam_storage_module_id_set = FALSE;
+
+static int
+antispam_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+ struct mailbox_transaction_context *t = ctx->transaction;
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box);
+ struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t);
+ int ret;
+ bool src_trash, dst_trash;
+
+ if (!ctx->dest_mail) {
+ /* always need mail */
+ if (!ast->mail)
+ ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY,
+ NULL);
+ ctx->dest_mail = ast->mail;
+ }
+
+ i_assert(mail->box);
+
+ asbox->save_hack = FALSE;
+ asbox->movetype = MMT_UNINTERESTING;
+
+ if (mailbox_is_unsure(t->box)) {
+ mail_storage_set_error(t->box->storage, MAIL_ERROR_NOTPOSSIBLE,
+ "Cannot copy to unsure folder");
+ return -1;
+ }
+
+ src_trash = mailbox_is_trash(mail->box);
+ dst_trash = mailbox_is_trash(t->box);
+
+ debug_verbose("mail copy: from trash: %d, to trash: %d\n",
+ src_trash, dst_trash);
+
+ if (!src_trash && !dst_trash) {
+ bool src_spam = mailbox_is_spam(mail->box);
+ bool dst_spam = mailbox_is_spam(t->box);
+ bool src_unsu = mailbox_is_unsure(mail->box);
+
+ debug_verbose("mail copy: src spam: %d, dst spam: %d,"
+ " src unsure: %d\n",
+ src_spam, dst_spam, src_unsu);
+
+ if ((src_spam || src_unsu) && !dst_spam)
+ asbox->movetype = MMT_TO_CLEAN;
+ else if ((!src_spam || src_unsu) && dst_spam)
+ asbox->movetype = MMT_TO_SPAM;
+ }
+
+ if (asbox->module_ctx.super.copy(ctx, mail) < 0)
+ return -1;
+
+ /*
+ * If copying used saving internally, we already have treated the mail
+ */
+ if (asbox->save_hack || asbox->movetype == MMT_UNINTERESTING)
+ ret = 0;
+ else
+ ret = backend_handle_mail(t, ast->backendctx, ctx->dest_mail,
+ move_to_class(asbox->movetype));
+
+ /*
+ * Both save_hack and movetype are only valid within a copy operation,
+ * i.e. they are now invalid. Because, in theory, another operation
+ * could be done after mailbox_open(), we need to reset the movetype
+ * variable here. save_hack doesn't need to be reset because it is
+ * only ever set within the save function and tested within this copy
+ * function after being reset at the beginning of the copy, movetype
+ * however is tested within the save_finish() function and a subsequent
+ * save to the mailbox should not invoke the backend.
+ */
+ asbox->movetype = MMT_APPEND;
+ return ret;
+}
+
+static int antispam_save_begin(struct mail_save_context *ctx,
+ struct istream *input)
+{
+ struct mailbox_transaction_context *t = ctx->transaction;
+ struct antispam_internal_context *ast = ANTISPAM_CONTEXT(t);
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(t->box);
+ int ret;
+
+ if (!ctx->dest_mail) {
+ if (!ast->mail)
+ ast->mail = mail_alloc(t, MAIL_FETCH_STREAM_HEADER |
+ MAIL_FETCH_STREAM_BODY,
+ NULL);
+ ctx->dest_mail = ast->mail;
+ }
+ ret = asbox->module_ctx.super.save_begin(ctx, input);
+
+ return ret;
+}
+
+static int antispam_save_finish(struct mail_save_context *ctx)
+{
+ struct antispam_mailbox *asbox =
+ ANTISPAM_CONTEXT(ctx->transaction->box);
+ struct antispam_internal_context *ast =
+ ANTISPAM_CONTEXT(ctx->transaction);
+ struct mail *dest_mail;
+ int ret;
+
+ if (asbox->module_ctx.super.save_finish(ctx) < 0)
+ return -1;
+
+ dest_mail = ctx->dest_mail ? : ast->mail;
+
+ asbox->save_hack = TRUE;
+
+ ret = 0;
+
+ switch (asbox->movetype) {
+ case MMT_UNINTERESTING:
+ break;
+ case MMT_APPEND:
+ /* Disallow APPENDs to UNSURE folders. */
+ if (mailbox_is_unsure(dest_mail->box)) {
+ ret = -1;
+ mail_storage_set_error(dest_mail->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Cannot APPEND to an UNSURE folder.");
+ break;
+ } else if (mailbox_is_spam(dest_mail->box)) {
+ /*
+ * The client is APPENDing a message to a SPAM folder
+ * so we try to train the backend on it. For most of
+ * the backends, that can only succeed if the message
+ * contains appropriate information.
+ *
+ * This happens especially when offlineimap is used and
+ * the user moved a message to the SPAM folder while
+ * offline---offlineimap cannot reproduce the COPY but
+ * rather APPENDs the moved message on the next sync.
+ *
+ * This could be a bad if the spam headers were not
+ * generated on our server, but since the user can
+ * always APPEND to another folder and then COPY to a
+ * SPAM folder backends need to be prepared for cases
+ * like this anyway. With dspam, for example, the worst
+ * that can happen is that the APPEND fails with a
+ * training error from dspam.
+ *
+ * Unfortunately, we cannot handle the cases where
+ * (1) the user moved a message from one folder that
+ * contains SPAM to another folder containing SPAM
+ * (2) the user moved a message out of the SPAM folder
+ * (3) the user recovered a message from trash
+ *
+ * Because of these limitations, this behaviour needs
+ * to be enabled with an option.
+ */
+ if (!antispam_can_append_to_spam) {
+ ret = -1;
+ mail_storage_set_error(
+ dest_mail->box->storage,
+ MAIL_ERROR_NOTPOSSIBLE,
+ "Cannot APPEND to a SPAM folder.");
+ break;
+ }
+ asbox->movetype = MMT_TO_SPAM;
+ /* fall through to default case to invoke backend */
+ } else {
+ /* neither UNSURE nor SPAM, regular folder */
+ break;
+ }
+ /* fall through */
+ default:
+ ret = backend_handle_mail(ctx->transaction, ast->backendctx,
+ dest_mail,
+ move_to_class(asbox->movetype));
+ }
+
+ return ret;
+}
+
+static struct antispam_transaction_context *
+antispam_transaction_begin(struct mailbox *box)
+{
+ struct antispam_transaction_context *ast;
+
+ ast = backend_start(box);
+ i_assert(ast != NULL);
+
+ return ast;
+}
+
+static void
+antispam_transaction_rollback(struct antispam_transaction_context **_ast)
+{
+ struct antispam_transaction_context *ast = *_ast;
+
+ backend_rollback(ast);
+ *_ast = NULL;
+}
+
+static int
+antispam_transaction_commit(struct mailbox_transaction_context *ctx,
+ struct antispam_transaction_context **_ast)
+{
+ struct antispam_transaction_context *ast = *_ast;
+ int ret;
+
+ ret = backend_commit(ctx, ast);
+ *_ast = NULL;
+ return ret;
+}
+
+static void
+antispam_mail_update_keywords(struct mail *mail,
+ enum modify_type modify_type,
+ struct mail_keywords *keywords)
+{
+ struct mail_private *pmail = (struct mail_private *)mail;
+ union mail_module_context *amail = ANTISPAM_MAIL_CONTEXT(pmail);
+ unsigned int i, numkwds;
+ const ARRAY_TYPE(keywords) *idxkwd = mail_index_get_keywords(keywords->index);
+ const char *const *keyword_names = array_get(idxkwd, &numkwds);
+ const char *const *orig_keywords;
+ bool previous_spam_keyword, now_spam_keyword;
+
+ switch (modify_type) {
+ case MODIFY_ADD:
+ debug("adding keyword(s)\n");
+ break;
+ case MODIFY_REMOVE:
+ debug("removing keyword(s)\n");
+ break;
+ case MODIFY_REPLACE:
+ debug("replacing keyword(s)\n");
+ break;
+ default:
+ i_assert(0);
+ }
+
+ orig_keywords = pmail->v.get_keywords(mail);
+ if (orig_keywords) {
+ debug("original keyword list:\n");
+ while (*orig_keywords) {
+ debug(" * %s\n", *orig_keywords);
+ if (keyword_is_spam(*orig_keywords))
+ previous_spam_keyword = TRUE;
+ orig_keywords++;
+ }
+ }
+
+ debug("keyword list:\n");
+
+ for (i = 0; i < keywords->count; i++) {
+ unsigned int idx = keywords->idx[i];
+
+ i_assert(idx < numkwds);
+
+ debug(" * %s\n", keyword_names[idx]);
+
+ switch (modify_type) {
+ case MODIFY_ADD:
+ case MODIFY_REPLACE:
+ if (keyword_is_spam(keyword_names[idx]))
+ now_spam_keyword = TRUE;
+ break;
+ case MODIFY_REMOVE:
+ if (keyword_is_spam(keyword_names[idx]))
+ now_spam_keyword = FALSE;
+ break;
+ default:
+ i_assert(0);
+ }
+ }
+
+ amail->super.update_keywords(mail, modify_type, keywords);
+
+ debug("previous-spam, now-spam: %d, %d\n",
+ previous_spam_keyword, now_spam_keyword);
+
+ if (previous_spam_keyword != now_spam_keyword) {
+ /*
+ * Call backend here.
+ *
+ * TODO: It is not clear how to roll back the
+ * keyword change if the backend fails.
+ */
+ }
+}
+
+static struct mailbox_transaction_context *
+antispam_mailbox_transaction_begin(struct mailbox *box,
+ enum mailbox_transaction_flags flags)
+{
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(box);
+ struct mailbox_transaction_context *t;
+ struct antispam_transaction_context *ast;
+ struct antispam_internal_context *aic;
+
+ t = asbox->module_ctx.super.transaction_begin(box, flags);
+ aic = i_new(struct antispam_internal_context, 1);
+ ast = antispam_transaction_begin(box);
+ aic->backendctx = ast;
+
+ MODULE_CONTEXT_SET(t, antispam_storage_module, aic);
+ return t;
+}
+
+static int
+antispam_mailbox_transaction_commit(struct mailbox_transaction_context *ctx,
+ uint32_t *uid_validity_r,
+ uint32_t *first_saved_uid_r,
+ uint32_t *last_saved_uid_r)
+{
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box);
+ struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx);
+
+ if (antispam_transaction_commit(ctx, &ast->backendctx) < 0) {
+ if (ast->mail)
+ mail_free(&ast->mail);
+
+ asbox->module_ctx.super.transaction_rollback(ctx);
+ return -1;
+ }
+
+ if (ast->mail)
+ mail_free(&ast->mail);
+
+ return asbox->module_ctx.super.transaction_commit(ctx, uid_validity_r,
+ first_saved_uid_r,
+ last_saved_uid_r);
+}
+
+static void
+antispam_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx)
+{
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box);
+ struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx);
+
+ if (ast->mail)
+ mail_free(&ast->mail);
+
+ asbox->module_ctx.super.transaction_rollback(ctx);
+
+ antispam_transaction_rollback(&ast->backendctx);
+}
+
+static struct mail *
+antispam_mailbox_mail_alloc(struct mailbox_transaction_context *ctx,
+ enum mail_fetch_field wanted_fields,
+ struct mailbox_header_lookup_ctx *wanted_headers)
+{
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box);
+ union mail_module_context *amail;
+ struct mail *_mail;
+ struct mail_private *mail;
+
+ _mail = asbox->module_ctx.super.
+ mail_alloc(ctx, wanted_fields, wanted_headers);
+ mail = (struct mail_private *)_mail;
+
+ amail = p_new(mail->pool, union mail_module_context, 1);
+ amail->super = mail->v;
+
+ mail->v.update_keywords = antispam_mail_update_keywords;
+ MODULE_CONTEXT_SET_SELF(mail, antispam_mail_module, amail);
+ return _mail;
+}
+
+static struct mailbox *antispam_mailbox_open(struct mail_storage *storage,
+ const char *name,
+ struct istream *input,
+ enum mailbox_open_flags flags)
+{
+ union mail_storage_module_context *as_storage = ANTISPAM_CONTEXT(storage);
+ struct mailbox *box;
+ struct antispam_mailbox *asbox;
+
+ box = as_storage->super.mailbox_open(storage, name, input, flags);
+ if (box == NULL)
+ return NULL;
+
+ asbox = p_new(box->pool, struct antispam_mailbox, 1);
+ asbox->module_ctx.super = box->v;
+ asbox->save_hack = FALSE;
+ asbox->movetype = MMT_APPEND;
+
+ if (need_folder_hook) {
+ /* override save_init to override want_mail, we need that */
+ box->v.save_begin = antispam_save_begin;
+ box->v.save_finish = antispam_save_finish;
+ box->v.transaction_begin = antispam_mailbox_transaction_begin;
+ box->v.transaction_commit = antispam_mailbox_transaction_commit;
+ box->v.transaction_rollback = antispam_mailbox_transaction_rollback;
+ box->v.copy = antispam_copy;
+ }
+
+ if (need_keyword_hook)
+ box->v.mail_alloc = antispam_mailbox_mail_alloc;
+
+ MODULE_CONTEXT_SET(box, antispam_storage_module, asbox);
+ return box;
+}
+
+void antispam_mail_storage_created(struct mail_storage *storage)
+{
+ union mail_storage_module_context *as_storage;
+
+ if (antispam_next_hook_mail_storage_created != NULL)
+ antispam_next_hook_mail_storage_created(storage);
+
+ as_storage = p_new(storage->pool, union mail_storage_module_context, 1);
+ as_storage->super = storage->v;
+ storage->v.mailbox_open = antispam_mailbox_open;
+
+ if (!antispam_storage_module_id_set) {
+ antispam_storage_module_id = PLUGIN_FUNCTION(id);
+ antispam_storage_module_id_set = TRUE;
+ }
+
+ MODULE_CONTEXT_SET_SELF(storage, antispam_storage_module, as_storage);
+}
diff --git a/antispam.7 b/antispam.7
index efec9cd..daef414 100644
--- a/antispam.7
+++ b/antispam.7
@@ -94,6 +94,12 @@ into a spam folder and other mail regularly.
Has the same drawbacks as the dspam-exec approach.
+.SS spool2dir backend (general)
+
+This backend spools the message into a file. No further processing
+is performed. You need to write an extra daemon that picks up the
+spooled files and trains the spam filter as appropriate. You can,
+for example, use incron to pick up new emails.
.SH CONFIGURATION
@@ -194,6 +200,17 @@ plugin {
# NOTE: you need to set the signature for this backend
antispam_signature = X-CRM114-CacheID
+
+ #===================
+ # spool2dir plugin
+
+ # spam/not-spam spool2dir drop (default unset which will give errors)
+ # The first %%lu is replaced by the current time.
+ # The second %%lu is replaced by a counter to generate unique names.
+ # These two tokens MUST be present in the template! However
+ # you can insert any C-style modifier as shown.
+ # antispam_spool2dir_spam = /tmp/spamspool/%%020lu-%u-%%05lus
+ # antispam_spool2dir_notspam = /tmp/spamspool/%%020lu-%u-%%05luh
}
.fi
diff --git a/defconfig b/defconfig
index ef22e81..3bbda18 100644
--- a/defconfig
+++ b/defconfig
@@ -14,10 +14,12 @@
# signature-log - signature logging using dovecot's dict API
# mailtrain - send mail to special addresses for training
# crm114-exec - direct crm114 training by calling mailreaver.crm
+# spool2dir - spool trained emails to a directory
#BACKEND=dspam-exec
#BACKEND=signature-log
#BACKEND=mailtrain
#BACKEND=crm114-exec
+#BACKEND=spool2dir
# Dovecot build/header directory
# Building the plugin requires configured dovecot sources or having
diff --git a/dovecot-version.c b/dovecot-version.c
index 82772a9..fbb7a88 100644
--- a/dovecot-version.c
+++ b/dovecot-version.c
@@ -8,7 +8,7 @@ int main(int argc, char **argv)
char *v = PACKAGE_STRING, *e;
int maj = 0, min = 0;
- if (strncmp(v, "dovecot ", 8))
+ if (strncmp(v, "dovecot ", 8) && strncmp(v, "Dovecot ", 8))
return 1;
/* skip "dovecot " */
diff --git a/mailtrain.c b/mailtrain.c
index 7156805..fe2868d 100644
--- a/mailtrain.c
+++ b/mailtrain.c
@@ -288,7 +288,7 @@ int backend_handle_mail(struct mailbox_transaction_context *t,
if (memcmp("From ", beginning, 5) == 0) {
i_stream_read_next_line(mailstream);
} else {
- if (o_stream_send_str(outstream, "From ") < 0) {
+ if (o_stream_send(outstream, beginning, 5) != 5) {
ret = -1;
mail_storage_set_error(t->box->storage,
ME(NOTPOSSIBLE)
diff --git a/spool2dir.c b/spool2dir.c
new file mode 100644
index 0000000..06bd307
--- /dev/null
+++ b/spool2dir.c
@@ -0,0 +1,268 @@
+/*
+ * mailing backend for dovecot antispam plugin
+ *
+ * Copyright (C) 2008 Steffen Kaiser <skdovecot@smail.inf.fh-brs.de>
+ * this backend "spool2dir" bases on "mailtrain" backend of
+ * Copyright (C) 2007 Johannes Berg <johannes@sipsolutions.net>
+ *
+ * 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
+ */
+
+
+/*
+ * spool2dir antispam backend / plugin
+ *
+ * Any modification of SPAM status is recorded into a directory.
+ *
+ * Configuration
+ *
+ * Via settings similiar to the other antispam backends
+ * antispam_spool2dir_spam :- filename tempate for SPAM messages
+ * antispam_spool2dir_notsam :- filename tempate for HAM messages
+ *
+ * The templates _must_ provide two arguments:
+ * 1. %%lu - the current unix time (lowercase L, lowercase U)
+ * 2. %%lu - a counter to create different temporary files
+ * Note: The %-sign must be given two times to protect agains
+ * the expansion by Dovecot itself. You can put any legal
+ * format modification character of C's printf() function between
+ * '%%' and 'lu'.
+ *
+ * e.g.:
+ * antispam_spool2dir_spam = /tmp/spamspool/%%020lu-%%05lu-%u-S
+ * antispam_spool2dir_ham = /tmp/spamspool/%%020lu-%%05lu-%u-H
+ *
+ * This example will spool the messages into the directory
+ * /tmp/spamspool. The individual files start with 20 digits,
+ * followe by a dash, 5 digits, the current username and S or H,
+ * indicating Spam or Ham messages.
+ * The first %%lu placeholder is replace by the current unix time,
+ * the second %%lu with the counter. That way, if the same user
+ * trains the same message twice, the filename indicates the order
+ * in which it was done. So if the message was trained as SPAM first,
+ * as HAM later, HAM superceeds SPAM.
+ *
+ * Operation
+ *
+ * When the antispam plugin identifies detects a SPAM status change,
+ * e.g. moving/copying a message from any antispam_spam folder into
+ * a folder _not_ listed in antispam_spam or antispam_trash, this
+ * backend spools the complete message into antispam_mail_basedir.
+ * If there is an error copying _all_ messages around, old spools
+ * are kept, but the current one is deleted. For instance, if the
+ * user is copying 15 messages, but only 10 succeed, the 10 would
+ * be usually deleted. In this backend there is no rollback of
+ * successfully spooled message, only the failed message is
+ * deleted.
+ *
+ * Possible usage models
+ *
+ * A)
+ * I use spool2dir for training the Bayes database as follows:
+ *
+ * Every 10 seconds a service invokes the training program, unless
+ * it already runs.
+ *
+ * The training progran reads the content of the spool directory, sorts
+ * the filenames alphanumerically, waits two seconds to allow any current
+ * spool2dir processes to finish currently open files.
+ * Then one message at a time is read and identified, if it contains
+ * local modifications, e.g. user-visible SPAM reports, which are removed.
+ * Furthermore, reports of untrustworthy people are discarded.
+ * This process continues until either all messages are processed or
+ * the next message would have another SPAM report type (HAM or SPAM). The
+ * file names of the messages processed til now are passed to the Bayes
+ * trainer to be processed within one run. Then those messages are removed.
+ *
+ * B)
+ *
+ * An Inotify server watches the spamspool directory and passes the messages
+ * to spamd. No need for the filenames to indicate the order anymore, unless
+ * the inotify server is not fast enough.
+ */
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <errno.h>
+
+#include "lib.h"
+#include "mail-storage-private.h"
+#include "ostream.h"
+#include "istream.h"
+
+#include "antispam-plugin.h"
+
+static const char *spamspool = NULL;
+static const char * hamspool = NULL;
+
+struct antispam_transaction_context {
+ int count;
+};
+
+
+void backend_rollback(struct antispam_transaction_context *ast)
+{
+ i_free(ast);
+}
+
+int backend_commit(struct mailbox_transaction_context *ctx __attr_unused__,
+ struct antispam_transaction_context *ast)
+{
+ i_free(ast);
+
+ return 0;
+}
+
+int backend_handle_mail(struct mailbox_transaction_context *t,
+ struct antispam_transaction_context *ast,
+ struct mail *mail, enum classification wanted)
+{
+ struct istream *mailstream;
+ struct ostream *outstream;
+ int ret;
+ const char *dest, *buf;
+ const unsigned char *beginning;
+ size_t size;
+ int fd;
+
+ i_assert(ast);
+
+ switch (wanted) {
+ case CLASS_SPAM:
+ dest = spamspool;
+ break;
+ case CLASS_NOTSPAM:
+ dest = hamspool;
+ break;
+ default: /* cannot handle this */
+ return -1;
+ }
+
+ if(!dest) {
+ mail_storage_set_error(t->box->storage,
+ ME(NOTPOSSIBLE)
+ "antispam plugin / spool2dir backend not configured");
+ return -1;
+ }
+
+ mailstream = get_mail_stream(mail);
+ if (!mailstream) {
+ mail_storage_set_error(t->box->storage,
+ ME(EXPUNGED)
+ "Failed to get mail contents");
+ return -1;
+ }
+
+ t_push();
+
+ /* atomically create a _new_ file */
+ while (ast->count <= 9999) {
+ buf = t_strdup_printf(dest, (long)time(0), (long)++ast->count);
+ fd = open(buf, O_CREAT | O_EXCL | O_WRONLY, 0600);
+ if(fd >= 0 || errno != EEXIST)
+ break;
+ /* current filename in buf already exists, zap it */
+ t_pop();
+ t_push();
+ /* buf is invalid now ! */
+ }
+ if (fd < 0) {
+ debug("spool2dir backend: Failed to create spool file %s: %s\n",
+ dest, strerror(errno));
+ mail_storage_set_error(t->box->storage,
+ ME(NOTPOSSIBLE)
+ "Failed to create spool file");
+ goto out;
+ }
+
+ /* buf still points to allocated memory, because fd >= 0 */
+ outstream = o_stream_create_from_fd(fd, t->box->pool);
+ if (!outstream) {
+ mail_storage_set_error(t->box->storage,
+ ME(NOTPOSSIBLE)
+ "Failed to stream spool file");
+ goto out_close;
+ }
+
+ if (i_stream_read_data(mailstream, &beginning, &size, 5) < 0 ||
+ size < 5) {
+ ret = -1;
+ mail_storage_set_error(t->box->storage,
+ ME(NOTPOSSIBLE)
+ "Failed to read mail beginning");
+ goto failed_to_copy;
+ }
+
+ /* "From "? skip line */
+ if (memcmp("From ", beginning, 5) == 0) {
+ i_stream_read_next_line(mailstream);
+ } else {
+ if (o_stream_send(outstream, beginning, 5) != 5) {
+ ret = -1;
+ mail_storage_set_error(t->box->storage,
+ ME(NOTPOSSIBLE)
+ "Failed to write line to temp");
+ goto failed_to_copy;
+ }
+ }
+
+ if (o_stream_send_istream(outstream, mailstream) < 0) {
+ ret = -1;
+ mail_storage_set_error(t->box->storage,
+ ME(NOTPOSSIBLE)
+ "Failed to copy to spool file");
+ goto failed_to_copy;
+ }
+
+ ret = 0;
+
+ failed_to_copy:
+ o_stream_destroy(&outstream);
+ out_close:
+ close(fd);
+ out:
+ if (ret)
+ unlink(buf);
+
+ t_pop();
+
+ return ret;
+}
+
+void backend_init(pool_t pool __attr_unused__)
+{
+ if((spamspool = get_setting("SPOOL2DIR_SPAM")))
+ debug("spool2dir spamspool %s\n", spamspool);
+
+ if((hamspool = get_setting("SPOOL2DIR_NOTSPAM")))
+ debug("spool2dir hamspool %s\n", hamspool);
+}
+
+struct antispam_transaction_context *
+ backend_start(struct mailbox *box __attr_unused__)
+{
+ struct antispam_transaction_context *ast;
+
+ ast = i_new(struct antispam_transaction_context, 1);
+
+ ast->count = 0;
+
+ return ast;
+}
+
+
+void backend_exit(void)
+{
+}