+diff -ru a/dovecot-antispam-2.0+20120225/antispam-plugin.c b/dovecot-antispam-2.0+20120225/antispam-plugin.c
+--- a/dovecot-antispam-2.0+20120225/antispam-plugin.c 2012-02-24 20:22:48.000000000 +0100
++++ b/dovecot-antispam-2.0+20120225/antispam-plugin.c 2013-07-29 07:14:51.962245023 +0200
+@@ -246,6 +246,21 @@
+
+ return FALSE;
+ }
++bool keyword_is_ham(const struct antispam_config *cfg, const char *keyword)
++{
++ char **k = cfg->ham_keywords;
++
++ if (!cfg->ham_keywords)
++ return FALSE;
++
++ while (*k) {
++ if (strcmp(*k, keyword) == 0)
++ return TRUE;
++ k++;
++ }
++
++ return FALSE;
++}
+
+ static int parse_folder_setting(const struct antispam_config *cfg,
+ const char *setting, char ***strings,
+@@ -335,6 +350,17 @@
+ iter++;
+ }
+ }
++ tmp = getenv("HAM_KEYWORDS", getenv_data);
++ if (tmp)
++ cfg->ham_keywords = p_strsplit(cfg->mem_pool, tmp, ";");
++
++ if (cfg->ham_keywords) {
++ iter = cfg->ham_keywords;
++ while (*iter) {
++ debug(&cfg->dbgcfg, "\"%s\" is ham keyword\n", *iter);
++ iter++;
++ }
++ }
+
+ tmp = getenv("BACKEND", getenv_data);
+ if (tmp) {
+diff -ru a/dovecot-antispam-2.0+20120225/antispam-plugin.h b/dovecot-antispam-2.0+20120225/antispam-plugin.h
+--- a/dovecot-antispam-2.0+20120225/antispam-plugin.h 2013-07-31 12:45:33.000000000 +0200
++++ b/dovecot-antispam-2.0+20120225/antispam-plugin.h 2013-07-30 05:22:21.248730740 +0200
+@@ -27,6 +27,7 @@
+ struct antispam_transaction_context;
+
+ enum classification {
++ CLASS_UNSURE,
+ CLASS_NOTSPAM,
+ CLASS_SPAM,
+ };
+@@ -72,6 +73,7 @@
+ char **unsure_folders[3]; // = { NULL, NULL, NULL };
+
+ char **spam_keywords;
++ char **ham_keywords;
+
+ const char *signature_hdr;
+
+@@ -178,6 +180,7 @@
+ bool mailbox_is_trash(const struct antispam_config *cfg, struct mailbox *box);
+ bool mailbox_is_unsure(const struct antispam_config *cfg, struct mailbox *box);
+ bool keyword_is_spam(const struct antispam_config *cfg, const char *keyword);
++bool keyword_is_ham(const struct antispam_config *cfg, const char *keyword);
+
+ struct antispam_config *
+ antispam_setup_config(const char *(getenv)(const char *env, void *data),
+diff -ru a/dovecot-antispam-2.0+20120225/antispam-storage-2.0.c b/dovecot-antispam-2.0+20120225/antispam-storage-2.0.c
+--- a/dovecot-antispam-2.0+20120225/antispam-storage-2.0.c 2012-02-24 20:22:48.000000000 +0100
++++ b/dovecot-antispam-2.0+20120225/antispam-storage-2.0.c 2013-07-31 14:52:43.277922438 +0200
+@@ -310,21 +310,9 @@
+ 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(&amail->cfg->dbgcfg, "adding keyword(s)\n");
+- break;
+- case MODIFY_REMOVE:
+- debug(&amail->cfg->dbgcfg, "removing keyword(s)\n");
+- break;
+- case MODIFY_REPLACE:
+- debug(&amail->cfg->dbgcfg, "replacing keyword(s)\n");
+- break;
+- default:
+- i_assert(0);
+- }
++ bool previous_spam_keyword = FALSE, previous_ham_keyword = FALSE;
++ enum classification spam_class = CLASS_UNSURE;
++ struct antispam_transaction_context *ast;
+
+ orig_keywords = pmail->v.get_keywords(mail);
+ if (orig_keywords) {
+@@ -333,6 +321,8 @@
+ debug(&amail->cfg->dbgcfg, " * %s\n", *orig_keywords);
+ if (keyword_is_spam(amail->cfg, *orig_keywords))
+ previous_spam_keyword = TRUE;
++ if (keyword_is_ham(amail->cfg, *orig_keywords))
++ previous_ham_keyword = TRUE;
+ orig_keywords++;
+ }
+ }
+@@ -350,29 +340,47 @@
+ case MODIFY_ADD:
+ case MODIFY_REPLACE:
+ if (keyword_is_spam(amail->cfg, keyword_names[idx]))
+- now_spam_keyword = TRUE;
++ spam_class = CLASS_SPAM;
++ if (keyword_is_ham(amail->cfg, keyword_names[idx]))
++ spam_class = CLASS_NOTSPAM;
+ break;
+ case MODIFY_REMOVE:
+ if (keyword_is_spam(amail->cfg, keyword_names[idx]))
+- now_spam_keyword = FALSE;
++ spam_class = CLASS_NOTSPAM;
++ if (keyword_is_ham(amail->cfg, keyword_names[idx]))
++ spam_class = CLASS_SPAM;
+ break;
+ default:
+ i_assert(0);
+ }
+ }
+-
++
++ debug(&amail->cfg->dbgcfg, "spam_class=%d previous_spam_keyword=%d previous_ham_keyword=%d\n",
++ spam_class, previous_spam_keyword, previous_ham_keyword);
++
+ amail->module_ctx.super.update_keywords(mail, modify_type, keywords);
+-
+- debug(&amail->cfg->dbgcfg, "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.
++
++ if (previous_spam_keyword && previous_ham_keyword) {
++ /* NOTE: avoid to call the backend two times for the same change
++ * once when adding one spam or ham keyword
++ * and once when removing the other.
++ */
++ debug(&amail->cfg->dbgcfg, "does not run backend a second time\n");
++ return;
++ }
++
++ if (spam_class != CLASS_UNSURE) {
++ /* NOTE: on error, backend uses mail_storage_set_error()
++ * which is checked in commit() where a rollback() can be done.
+ */
++ debug(&amail->cfg->dbgcfg, "call backend->start()\n");
++ ast = amail->cfg->backend->start(amail->cfg, mail->transaction->box);
++ i_assert(ast != NULL);
++ debug(&amail->cfg->dbgcfg, "call backend->handle_mail()\n");
++ if (!amail->cfg->backend->handle_mail(amail->cfg, mail->transaction, ast, mail, spam_class)) {
++ debug(&amail->cfg->dbgcfg, "call backend->commit()\n");
++ amail->cfg->backend->commit(amail->cfg, mail->transaction, ast);
++ }
+ }
+ }
+
+@@ -401,7 +409,9 @@
+ struct antispam_mailbox *asbox = ANTISPAM_CONTEXT(ctx->box);
+ struct antispam_internal_context *ast = ANTISPAM_CONTEXT(ctx);
+
+- if (antispam_transaction_commit(asbox->cfg, ctx, &ast->backendctx) < 0) {
++ if ((ctx->box->storage->error != MAIL_ERROR_NONE)
++ || antispam_transaction_commit(asbox->cfg, ctx, &ast->backendctx) < 0) {
++ debug(&asbox->cfg->dbgcfg, "transaction rollback\n");
+ if (ast->mail)
+ mail_free(&ast->mail);
+
+diff -ru a/dovecot-antispam-2.0+20120225/crm114-exec.c b/dovecot-antispam-2.0+20120225/crm114-exec.c
+--- a/dovecot-antispam-2.0+20120225/crm114-exec.c 2012-02-24 20:22:48.000000000 +0100
++++ b/dovecot-antispam-2.0+20120225/crm114-exec.c 2013-07-30 06:00:16.266596502 +0200
+@@ -43,6 +43,8 @@
+ case CLASS_SPAM:
+ class_arg = "--spam";
+ break;
++ default:
++ i_assert(0);
+ }
+
+ /*
+diff -ru a/dovecot-antispam-2.0+20120225/dspam-exec.c b/dovecot-antispam-2.0+20120225/dspam-exec.c
+--- a/dovecot-antispam-2.0+20120225/dspam-exec.c 2012-02-24 20:22:48.000000000 +0100
++++ b/dovecot-antispam-2.0+20120225/dspam-exec.c 2013-07-31 13:28:39.217464532 +0200
+@@ -45,6 +45,8 @@
+ case CLASS_SPAM:
+ class_arg = t_strconcat("--class=", "spam", NULL);
+ break;
++ default:
++ i_assert(0);
+ }
+
+ /*
+diff -ru a/dovecot-antispam-2.0+20120225/pipe.c b/dovecot-antispam-2.0+20120225/pipe.c
+--- a/dovecot-antispam-2.0+20120225/pipe.c 2012-02-24 20:22:48.000000000 +0100
++++ b/dovecot-antispam-2.0+20120225/pipe.c 2013-07-31 14:16:12.164255967 +0200
+@@ -48,6 +48,8 @@
+ dest = cfg->pipe.ham_args;
+ dest_num = cfg->pipe.spam_args_num;
+ break;
++ default:
++ i_assert(0);
+ }
+
+ if (!dest)