diff --git a/TODO b/TODO
index be9da82b34c3e0d7a44222a69e35181163229da9..bd184ffe402651016ba1a3b73826efeb76a94834 100644
--- a/TODO
+++ b/TODO
@@ -88,11 +88,9 @@ Low priority items:
   within Dovecot)
 * Warn during compile if using non-existent folders.
 
-* Implement IMAP plugin for IMAPSieve support:
-	- Requires some sort of Sieve transaction support.
-	- This may include support for manually running a script on a set of messages
-	  through IMAP (no specification for something like this is available; we will
-	  have to provide our own)
+* Implement support for manually running a script on a set of messages through
+  IMAP (no specification for something like this is available; we will have to
+  provide our own)
 * Variables extension: implement compile time evaluation of constant values
 	- Detect assignment of too large constant values to variables at compile
 	  time.
diff --git a/configure.ac b/configure.ac
index 821cad90e7360d8e36de12db0bd1f32e90715c74..fcd3854bb1eb1473df03c5fec7030abe826b018c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -223,6 +223,7 @@ src/plugins/Makefile
 src/plugins/doveadm-sieve/Makefile
 src/plugins/lda-sieve/Makefile
 src/plugins/sieve-extprograms/Makefile
+src/plugins/imapsieve/Makefile
 src/plugins/settings/Makefile
 src/sieve-tools/Makefile
 src/managesieve/Makefile
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index a124d59ed807061a229c0854bfd071a13e7e7f7b..58a3997eef88ddac8c578a61ae3e0c1ca584fa58 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -2,4 +2,5 @@ SUBDIRS = \
 	doveadm-sieve \
 	lda-sieve \
 	sieve-extprograms \
+	imapsieve \
 	settings
diff --git a/src/plugins/imapsieve/Makefile.am b/src/plugins/imapsieve/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..1fea23e5e49fb1325c1d9061ec74d33ec2cdfb31
--- /dev/null
+++ b/src/plugins/imapsieve/Makefile.am
@@ -0,0 +1,40 @@
+imap_moduledir = $(dovecot_moduledir)
+sieve_plugindir = $(dovecot_moduledir)/sieve
+
+imap_module_LTLIBRARIES = lib95_imap_sieve_plugin.la
+sieve_plugin_LTLIBRARIES = lib90_sieve_imapsieve_plugin.la
+
+lib95_imap_sieve_plugin_la_LDFLAGS = -module -avoid-version
+lib90_sieve_imapsieve_plugin_la_LDFLAGS = -module -avoid-version
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib-sieve \
+	-I$(top_srcdir)/src/lib-sieve/util \
+	-I$(top_srcdir)/src/lib-sieve/plugins/environment \
+	$(LIBDOVECOT_IMAP_INCLUDE) \
+	$(LIBDOVECOT_LDA_INCLUDE) \
+	$(LIBDOVECOT_INCLUDE) \
+	-DPKG_RUNDIR=\""$(rundir)"\"
+
+lib95_imap_sieve_plugin_la_SOURCES = \
+	ext-imapsieve.c \
+	ext-imapsieve-environment.c \
+	imap-sieve.c \
+	imap-sieve-storage.c \
+	imap-sieve-plugin.c
+lib95_imap_sieve_plugin_la_LIBADD = \
+	$(top_builddir)/src/lib-sieve/libdovecot-sieve.la
+
+lib90_sieve_imapsieve_plugin_la_SOURCES = \
+	ext-imapsieve.c \
+	sieve-imapsieve-plugin.c
+lib90_sieve_imapsieve_plugin_la_CPPFLAGS = \
+	${AM_CPPFLAGS} \
+	-D__IMAPSIEVE_DUMMY
+
+noinst_HEADERS = \
+	ext-imapsieve-common.h \
+	imap-sieve.h \
+	imap-sieve-storage.h \
+	imap-sieve-plugin.h \
+	sieve-imapsieve-plugin.h
diff --git a/src/plugins/imapsieve/ext-imapsieve-common.h b/src/plugins/imapsieve/ext-imapsieve-common.h
new file mode 100644
index 0000000000000000000000000000000000000000..8fa9717532fd220966d7711e704c42c30cd7bc29
--- /dev/null
+++ b/src/plugins/imapsieve/ext-imapsieve-common.h
@@ -0,0 +1,26 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef __EXT_IMAPSIEVE_COMMON_H
+#define __EXT_IMAPSIEVE_COMMON_H
+
+#include "sieve-extensions.h"
+
+#include "imap-sieve.h"
+
+/*
+ * Extensions
+ */
+
+extern const struct sieve_extension_def imapsieve_extension;
+extern const struct sieve_extension_def imapsieve_extension_dummy;
+
+/*
+ * Environment items
+ */
+
+void ext_imapsieve_environment_items_register
+	(const struct sieve_extension *ext,
+		const struct sieve_runtime_env *renv);
+
+#endif /* __EXT_IMAPSIEVE_COMMON_H */
diff --git a/src/plugins/imapsieve/ext-imapsieve-environment.c b/src/plugins/imapsieve/ext-imapsieve-environment.c
new file mode 100644
index 0000000000000000000000000000000000000000..7339720a70c8cd10c7994cbe84cd34c6ae4b7a3c
--- /dev/null
+++ b/src/plugins/imapsieve/ext-imapsieve-environment.c
@@ -0,0 +1,131 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-storage.h"
+
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-runtime.h"
+
+#include "sieve-ext-environment.h"
+
+#include "ext-imapsieve-common.h"
+
+/*
+ * Environment items
+ */
+
+/* imap.user */
+
+static const char *envit_imap_user_get_value
+(const struct sieve_runtime_env *renv)
+{
+	const struct sieve_script_env *senv = renv->scriptenv;
+
+	return senv->user->username;
+}
+
+const struct sieve_environment_item imap_user_env_item = {
+	.name = "imap.user",
+	.get_value = envit_imap_user_get_value
+};
+
+/* imap.email */
+
+static const char *envit_imap_email_get_value
+(const struct sieve_runtime_env *renv)
+{
+	struct sieve_instance *svinst = renv->svinst;
+	const struct sieve_script_env *senv = renv->scriptenv;
+	const char *username = senv->user->username;
+
+	// FIXME: explicit configuration
+	if ( strchr(username, '@') != 0 )
+		return username;
+	if ( svinst->domainname != NULL )
+		return t_strconcat(username, "@", svinst->domainname, NULL);
+	return NULL;
+}
+
+const struct sieve_environment_item imap_email_env_item = {
+	.name = "imap.email",
+	.get_value = envit_imap_email_get_value
+};
+
+/* imap.cause */
+
+static const char *envit_imap_cause_get_value
+(const struct sieve_runtime_env *renv)
+{
+	const struct sieve_script_env *senv = renv->scriptenv;
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+
+	return isctx->event.cause;
+}
+
+const struct sieve_environment_item imap_cause_env_item = {
+	.name = "imap.cause",
+	.get_value = envit_imap_cause_get_value
+};
+
+/* imap.mailbox */
+
+static const char *envit_imap_mailbox_get_value
+(const struct sieve_runtime_env *renv)
+{
+	const struct sieve_script_env *senv = renv->scriptenv;
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+
+	return mailbox_get_vname(isctx->event.mailbox);
+}
+
+const struct sieve_environment_item imap_mailbox_env_item = {
+	.name = "imap.mailbox",
+	.get_value = envit_imap_mailbox_get_value
+};
+
+
+/* imap.changedflags */
+
+static const char *envit_imap_changedflags_get_value
+(const struct sieve_runtime_env *renv)
+{
+	const struct sieve_script_env *senv = renv->scriptenv;
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+
+	return isctx->event.changed_flags;
+}
+
+const struct sieve_environment_item imap_changedflags_env_item = {
+	.name = "imap.changedflags",
+	.get_value = envit_imap_changedflags_get_value
+};
+
+/*
+ * Register
+ */
+
+void ext_imapsieve_environment_items_register
+(const struct sieve_extension *ext, const struct sieve_runtime_env *renv)
+{
+	const struct sieve_extension *env_ext =
+		(const struct sieve_extension *) ext->context;
+
+	sieve_environment_item_register
+		(env_ext, renv->interp, &imap_user_env_item);
+	sieve_environment_item_register
+		(env_ext, renv->interp, &imap_email_env_item);
+	sieve_environment_item_register
+		(env_ext, renv->interp, &imap_cause_env_item);
+	sieve_environment_item_register
+		(env_ext, renv->interp, &imap_mailbox_env_item);
+	sieve_environment_item_register
+		(env_ext, renv->interp, &imap_changedflags_env_item);
+}
diff --git a/src/plugins/imapsieve/ext-imapsieve.c b/src/plugins/imapsieve/ext-imapsieve.c
new file mode 100644
index 0000000000000000000000000000000000000000..d153fb6e69ced481fd06c77108d088a54cc91d57
--- /dev/null
+++ b/src/plugins/imapsieve/ext-imapsieve.c
@@ -0,0 +1,110 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+/* Extension imapsieve
+ * -------------------
+ *
+ * Authors: Stephan Bosch
+ * Specification: rfc6785
+ * Implementation: full
+ * Status: experimental
+ *
+ */
+
+#include "lib.h"
+
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+#include "sieve-binary.h"
+
+#include "sieve-validator.h"
+#include "sieve-interpreter.h"
+
+#include "sieve-ext-environment.h"
+
+#include "ext-imapsieve-common.h"
+
+/*
+ * Extension
+ */
+
+static bool ext_imapsieve_load
+	(const struct sieve_extension *ext, void **context);
+static bool ext_imapsieve_interpreter_load
+	(const struct sieve_extension *ext,
+		const struct sieve_runtime_env *renv,
+		sieve_size_t *address ATTR_UNUSED);
+
+#ifdef __IMAPSIEVE_DUMMY
+const struct sieve_extension_def imapsieve_extension_dummy = {
+#else
+const struct sieve_extension_def imapsieve_extension = {
+#endif
+	.name = "imapsieve",
+	.load = ext_imapsieve_load,
+	.interpreter_load = ext_imapsieve_interpreter_load
+};
+
+/*
+ * Context
+ */
+
+static bool ext_imapsieve_load
+(const struct sieve_extension *ext, void **context)
+{
+	*context = (void*)
+		sieve_ext_environment_require_extension(ext->svinst);
+	return TRUE;
+}
+
+/*
+ * Interpreter
+ */
+
+static int ext_imapsieve_interpreter_run
+	(const struct sieve_extension *this_ext,
+		const struct sieve_runtime_env *renv,
+		void *context, bool deferred);
+
+const struct sieve_interpreter_extension
+imapsieve_interpreter_extension = {
+#ifdef __IMAPSIEVE_DUMMY
+	.ext_def = &imapsieve_extension_dummy,
+#else
+	.ext_def = &imapsieve_extension,
+#endif
+	.run = ext_imapsieve_interpreter_run
+};
+
+static bool ext_imapsieve_interpreter_load
+(const struct sieve_extension *ext ATTR_UNUSED,
+	const struct sieve_runtime_env *renv,
+	sieve_size_t *address ATTR_UNUSED)
+{
+	sieve_interpreter_extension_register(renv->interp,
+		ext, &imapsieve_interpreter_extension, NULL);
+	return TRUE;
+}
+
+#ifdef __IMAPSIEVE_DUMMY
+static int ext_imapsieve_interpreter_run
+(const struct sieve_extension *ext ATTR_UNUSED,
+	const struct sieve_runtime_env *renv,
+	void *context ATTR_UNUSED, bool deferred)
+{
+	if ( !deferred ) {
+		sieve_runtime_error(renv, NULL,
+			"the imapsieve extension cannot be used outside IMAP");
+	}
+	return SIEVE_EXEC_FAILURE;
+}
+#else
+static int ext_imapsieve_interpreter_run
+(const struct sieve_extension *ext ATTR_UNUSED,
+	const struct sieve_runtime_env *renv,
+	void *context ATTR_UNUSED, bool deferred ATTR_UNUSED)
+{
+	ext_imapsieve_environment_items_register(ext, renv);
+	return SIEVE_EXEC_OK;
+}
+#endif
diff --git a/src/plugins/imapsieve/imap-sieve-plugin.c b/src/plugins/imapsieve/imap-sieve-plugin.c
new file mode 100644
index 0000000000000000000000000000000000000000..c81de23266be01e1886836e29508e69519e28fc3
--- /dev/null
+++ b/src/plugins/imapsieve/imap-sieve-plugin.c
@@ -0,0 +1,60 @@
+/* Copyright (c) 2016 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+
+#include "imap-sieve.h"
+#include "imap-sieve-storage.h"
+
+#include "imap-sieve-plugin.h"
+
+static struct module *imap_sieve_module;
+static imap_client_created_func_t *next_hook_client_created;
+
+/*
+ * Client
+ */
+
+static void imap_sieve_client_created(struct client **clientp)
+{
+	struct client *client = *clientp;
+	struct mail_user *user = client->user;
+	const char *url = NULL;
+
+	if (mail_user_is_plugin_loaded(user, imap_sieve_module)) {
+		url = mail_user_plugin_getenv(user, "imapsieve_url");
+		// FIXME: parse the URL and report error if it is bad
+		if (url != NULL && strncasecmp(url, "sieve:", 6) == 0) {
+			str_append(client->capability_string, " IMAPSIEVE=");
+			str_append(client->capability_string, url);
+		} else {
+			url = NULL;
+		}
+
+		imap_sieve_storage_client_created(client, (url != NULL));
+	}
+
+	if (next_hook_client_created != NULL)
+		next_hook_client_created(clientp);
+}
+
+/*
+ * Plugin
+ */
+
+const char *imap_sieve_plugin_version = DOVECOT_ABI_VERSION;
+const char imap_sieve_plugin_binary_dependency[] = "imap";
+
+void imap_sieve_plugin_init(struct module *module)
+{
+	imap_sieve_module = module;
+	next_hook_client_created =
+		imap_client_created_hook_set(imap_sieve_client_created);
+	imap_sieve_storage_init(module);
+}
+
+void imap_sieve_plugin_deinit(void)
+{
+	imap_sieve_storage_deinit();
+	imap_client_created_hook_set(next_hook_client_created);
+}
diff --git a/src/plugins/imapsieve/imap-sieve-plugin.h b/src/plugins/imapsieve/imap-sieve-plugin.h
new file mode 100644
index 0000000000000000000000000000000000000000..46b6549915b581e2553ea59907ee6fa0ad49256a
--- /dev/null
+++ b/src/plugins/imapsieve/imap-sieve-plugin.h
@@ -0,0 +1,14 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef IMAP_SIEVE_PLUGIN_H
+#define IMAP_SIEVE_PLUGIN_H
+
+struct module;
+
+extern const char imap_sieve_plugin_binary_dependency[];
+
+void imap_sieve_plugin_init(struct module *module);
+void imap_sieve_plugin_deinit(void);
+
+#endif
diff --git a/src/plugins/imapsieve/imap-sieve-storage.c b/src/plugins/imapsieve/imap-sieve-storage.c
new file mode 100644
index 0000000000000000000000000000000000000000..40c410879afba8aee9d196dab83531e7760cea2b
--- /dev/null
+++ b/src/plugins/imapsieve/imap-sieve-storage.c
@@ -0,0 +1,1159 @@
+/* Copyright (c) 2016 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "array.h"
+#include "hash.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "module-context.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+#include "mailbox-attribute.h"
+#include "mailbox-list-private.h"
+#include "imap-match.h"
+#include "imap-util.h"
+
+#include "strtrim.h"
+
+#include "imap-sieve.h"
+#include "imap-sieve-storage.h"
+
+#define MAILBOX_ATTRIBUTE_IMAPSIEVE_SCRIPT "imapsieve/script"
+#define MAIL_SERVER_ATTRIBUTE_IMAPSIEVE_SCRIPT "imapsieve/script"
+
+#define IMAP_SIEVE_USER_CONTEXT(obj) \
+	MODULE_CONTEXT(obj, imap_sieve_user_module)
+#define IMAP_SIEVE_CONTEXT(obj) \
+	MODULE_CONTEXT(obj, imap_sieve_storage_module)
+#define IMAP_SIEVE_MAIL_CONTEXT(obj) \
+	MODULE_CONTEXT(obj, imap_sieve_mail_module)
+
+struct imap_sieve_mailbox_rule;
+struct imap_sieve_user;
+struct imap_sieve_mailbox_event;
+struct imap_sieve_mailbox_transaction;
+struct imap_sieve_mail;
+
+enum imap_sieve_command {
+	IMAP_SIEVE_CMD_NONE = 0,
+	IMAP_SIEVE_CMD_APPEND,
+	IMAP_SIEVE_CMD_COPY,
+	IMAP_SIEVE_CMD_MOVE,
+	IMAP_SIEVE_CMD_STORE,
+	IMAP_SIEVE_CMD_OTHER
+};
+
+ARRAY_DEFINE_TYPE(imap_sieve_mailbox_rule,
+	struct imap_sieve_mailbox_rule *);
+ARRAY_DEFINE_TYPE(imap_sieve_mailbox_event,
+	struct imap_sieve_mailbox_event);
+
+HASH_TABLE_DEFINE_TYPE(imap_sieve_mailbox_rule,
+	struct imap_sieve_mailbox_rule *,
+	struct imap_sieve_mailbox_rule *);
+
+struct imap_sieve_mailbox_rule {
+	unsigned int index;
+	const char *mailbox;
+	const char *from;
+	const char *const *causes;
+	const char *before, *after;
+};
+
+struct imap_sieve_user {
+	pool_t pool;
+
+	union mail_user_module_context module_ctx;
+	struct client *client;
+	struct imap_sieve *isieve;
+
+	enum imap_sieve_command cur_cmd;
+
+	HASH_TABLE_TYPE(imap_sieve_mailbox_rule) mbox_rules;
+	ARRAY_TYPE(imap_sieve_mailbox_rule) mbox_patterns;
+
+	unsigned int sieve_active:1;
+	unsigned int user_script:1;
+};
+
+struct imap_sieve_mailbox_event {
+	uint32_t mail_uid;
+	unsigned int save_seq;
+
+	const char *changed_flags;
+};
+
+struct imap_sieve_mailbox_transaction {
+	pool_t pool;
+
+	union mailbox_transaction_module_context module_ctx;
+	struct mail *tmp_mail;
+	struct mailbox *src_box;
+
+	ARRAY_TYPE(imap_sieve_mailbox_event) events;
+};
+
+struct imap_sieve_mail {
+	union mail_module_context module_ctx;
+
+	string_t *flags;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(imap_sieve_user_module,
+				  &mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(imap_sieve_storage_module,
+				  &mail_storage_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(imap_sieve_mail_module,
+				  &mail_module_register);
+
+static void
+imap_sieve_mailbox_rules_get(struct mail_user *user,
+	struct mailbox *dst_box, struct mailbox *src_box,
+	const char *cause,
+	ARRAY_TYPE(imap_sieve_mailbox_rule) *rules);
+
+/*
+ * Logging
+ */
+
+static inline void
+imap_sieve_debug(struct mail_user *user,
+	const char *format, ...) ATTR_FORMAT(2, 3);
+static inline void
+imap_sieve_debug(struct mail_user *user,
+	const char *format, ...)
+{
+	va_list args;
+
+	if (user->mail_debug) {
+		va_start(args, format);
+		i_debug("imapsieve: %s",
+			t_strdup_vprintf(format, args));
+		va_end(args);
+	}
+}
+
+static inline void
+imap_sieve_warning(struct mail_user *user ATTR_UNUSED,
+	const char *format, ...) ATTR_FORMAT(2, 3);
+static inline void
+imap_sieve_warning(struct mail_user *user ATTR_UNUSED,
+	const char *format, ...)
+{
+	va_list args;
+
+	va_start(args, format);
+	i_warning("imapsieve: %s",
+		t_strdup_vprintf(format, args));
+	va_end(args);
+}
+
+static inline void
+imap_sieve_mailbox_debug(struct mailbox *box,
+	const char *format, ...) ATTR_FORMAT(2, 3);
+static inline void
+imap_sieve_mailbox_debug(struct mailbox *box,
+	const char *format, ...)
+{
+	va_list args;
+
+	if (box->storage->user->mail_debug) {
+		va_start(args, format);
+		i_debug("imapsieve: mailbox %s: %s",
+			mailbox_get_vname(box),
+			t_strdup_vprintf(format, args));
+		va_end(args);
+	}
+}
+
+static inline void
+imap_sieve_mailbox_error(struct mailbox *box,
+	const char *format, ...) ATTR_FORMAT(2, 3);
+static inline void
+imap_sieve_mailbox_error(struct mailbox *box,
+	const char *format, ...)
+{
+	va_list args;
+
+	va_start(args, format);
+	i_error("imapsieve: mailbox %s: %s",
+		mailbox_get_vname(box),
+		t_strdup_vprintf(format, args));
+	va_end(args);
+}
+
+/*
+ * Events
+ */
+
+static int imap_sieve_mailbox_get_script_real
+(struct mailbox *box, struct mailbox_transaction_context *t,
+	const char **script_name_r)
+{
+	struct mail_user *user = box->storage->user;
+	struct mail_attribute_value value;
+	int ret;
+
+	*script_name_r = NULL;
+
+	/* get the name of the Sieve script from mailbox METADATA */
+	if ((ret=mailbox_attribute_get(t, MAIL_ATTRIBUTE_TYPE_SHARED,
+			MAILBOX_ATTRIBUTE_IMAPSIEVE_SCRIPT, &value)) < 0) {
+		imap_sieve_mailbox_error(t->box,
+			"Failed to read /shared/"
+			MAILBOX_ATTRIBUTE_IMAPSIEVE_SCRIPT" "
+			"mailbox attribute"); // FIXME: details?
+		return -1;
+	}
+
+	if (ret > 0) {
+		imap_sieve_mailbox_debug(t->box,
+			"Mailbox attribute /shared/"
+			MAILBOX_ATTRIBUTE_IMAPSIEVE_SCRIPT" "
+			"points to Sieve script `%s'", value.value);
+
+	/* if not found, get the name of the Sieve script from
+	   server METADATA */
+	} else {
+		struct mail_namespace *ns;
+		struct mailbox *box;
+		struct mailbox_transaction_context *ibt;
+
+		imap_sieve_mailbox_debug(t->box,
+			"Mailbox attribute /shared/"
+			MAILBOX_ATTRIBUTE_IMAPSIEVE_SCRIPT" "
+			"not found");
+
+		ns = mail_namespace_find_inbox(user->namespaces);
+		box = mailbox_alloc(ns->list, "INBOX",
+			MAILBOX_FLAG_READONLY);
+		if ((ret=mailbox_open(box)) >= 0) {
+			ibt = mailbox_transaction_begin
+				(box, MAILBOX_TRANSACTION_FLAG_EXTERNAL);
+			ret = mailbox_attribute_get(ibt,
+				MAIL_ATTRIBUTE_TYPE_SHARED,
+				MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER
+				MAILBOX_ATTRIBUTE_IMAPSIEVE_SCRIPT, &value);
+			mailbox_transaction_rollback(&ibt);
+		}
+		mailbox_free(&box);
+
+		if (ret <= 0) {
+			if (ret < 0) {
+				imap_sieve_mailbox_error(t->box,
+					"Failed to read /shared/"
+					MAIL_SERVER_ATTRIBUTE_IMAPSIEVE_SCRIPT" "
+					"server attribute"); // FIXME: details?
+			} else if (ret == 0) {
+				imap_sieve_mailbox_debug(t->box,
+					"Server attribute /shared/"
+					MAIL_SERVER_ATTRIBUTE_IMAPSIEVE_SCRIPT" "
+					"not found");
+			}
+			return ret;
+		}
+
+		imap_sieve_mailbox_debug(t->box,
+			"Server attribute /shared/"
+			MAIL_SERVER_ATTRIBUTE_IMAPSIEVE_SCRIPT" "
+			"points to Sieve script `%s'", value.value);
+	}
+
+	*script_name_r = value.value;
+	return 1;
+}
+
+static int imap_sieve_mailbox_get_script
+(struct mailbox *box, const char **script_name_r)
+{
+	struct mailbox_transaction_context *t;
+	int ret;
+
+	t = mailbox_transaction_begin(box, 0);
+	ret = imap_sieve_mailbox_get_script_real
+		(box, t, script_name_r);
+	mailbox_transaction_rollback(&t);
+	return ret;
+}
+
+static void imap_sieve_add_mailbox_event
+(struct mailbox_transaction_context *t,
+	struct mail *mail, struct mailbox *src_box,
+	const char *changed_flags)
+{
+	struct imap_sieve_mailbox_transaction *ismt = IMAP_SIEVE_CONTEXT(t);
+	struct imap_sieve_mailbox_event *event;
+
+	i_assert(ismt->src_box == NULL || ismt->src_box == src_box);
+	ismt->src_box = src_box;
+
+	if (!array_is_created(&ismt->events))
+		i_array_init(&ismt->events, 64);
+
+	event = array_append_space(&ismt->events);
+	event->save_seq = t->save_count;
+	event->mail_uid = mail->uid;
+	event->changed_flags = p_strdup(ismt->pool, changed_flags);
+}
+
+/*
+ * Mail
+ */
+
+static void
+imap_sieve_mail_update_flags(struct mail *_mail,
+	enum modify_type modify_type, enum mail_flags flags)
+{
+	struct mail_private *mail = (struct mail_private *)_mail;
+	struct imap_sieve_mail *ismail = IMAP_SIEVE_MAIL_CONTEXT(mail);
+	enum mail_flags old_flags, new_flags, changed_flags;
+
+	old_flags = mail_get_flags(_mail);
+	ismail->module_ctx.super.update_flags(_mail, modify_type, flags);
+	new_flags = mail_get_flags(_mail);
+
+	changed_flags = old_flags ^ new_flags;
+	if (changed_flags == 0)
+		return;
+
+	if (ismail->flags == NULL)
+		ismail->flags = str_new(default_pool, 64);
+	imap_write_flags(ismail->flags, changed_flags, NULL);
+}
+
+static void
+imap_sieve_mail_update_keywords(struct mail *_mail,
+	enum modify_type modify_type, struct mail_keywords *keywords)
+{
+	struct mail_private *mail = (struct mail_private *)_mail;
+	struct mail_user *user = _mail->box->storage->user;
+	struct imap_sieve_mail *ismail = IMAP_SIEVE_MAIL_CONTEXT(mail);
+	const char *const *old_keywords, *const *new_keywords;
+	unsigned int i, j;
+
+	old_keywords = mail_get_keywords(_mail);
+	ismail->module_ctx.super.update_keywords(_mail, modify_type, keywords);
+	new_keywords = mail_get_keywords(_mail);
+
+	if (ismail->flags == NULL)
+		ismail->flags = str_new(default_pool, 64);
+
+	imap_sieve_debug(user, "Mail set keywords");
+
+	/* Removed flags */
+	for (i = 0; old_keywords[i] != NULL; i++) {
+		for (j = 0; new_keywords[j] != NULL; j++) {
+			if (strcmp(old_keywords[i], new_keywords[j]) == 0)
+				break;
+		}
+		if (new_keywords[j] == NULL) {
+			if (str_len(ismail->flags) > 0)
+				str_append_c(ismail->flags, ' ');
+			str_append(ismail->flags, old_keywords[i]);
+		}
+	}
+
+	/* Added flags */
+	for (i = 0; new_keywords[i] != NULL; i++) {
+		for (j = 0; old_keywords[j] != NULL; j++) {
+			if (strcmp(new_keywords[i], old_keywords[j]) == 0)
+				break;
+		}
+		if (old_keywords[j] == NULL) {
+			if (str_len(ismail->flags) > 0)
+				str_append_c(ismail->flags, ' ');
+			str_append(ismail->flags, new_keywords[i]);
+		}
+	}
+}
+
+static void imap_sieve_mail_close(struct mail *_mail)
+{
+	struct mail_private *mail = (struct mail_private *)_mail;
+	struct mailbox_transaction_context *t = _mail->transaction;
+	struct imap_sieve_mail *ismail = IMAP_SIEVE_MAIL_CONTEXT(mail);
+
+	if (ismail->flags != NULL && str_len(ismail->flags) > 0) {
+		if (!_mail->expunged) {
+			imap_sieve_mailbox_debug(_mail->box,
+				"FLAG event (changed flags: %s)",
+				str_c(ismail->flags));
+
+			imap_sieve_add_mailbox_event(t,
+				_mail, _mail->box, str_c(ismail->flags));
+		}
+		str_truncate(ismail->flags, 0);
+	}
+
+	ismail->module_ctx.super.close(_mail);
+}
+
+static void imap_sieve_mail_free(struct mail *_mail)
+{
+	struct mail_private *mail = (struct mail_private *)_mail;
+	struct imap_sieve_mail *ismail = IMAP_SIEVE_MAIL_CONTEXT(mail);
+	string_t *flags = ismail->flags;
+
+	ismail->module_ctx.super.free(_mail);
+
+	if (flags != NULL)
+		str_free(&flags);
+}
+
+static void imap_sieve_mail_allocated(struct mail *_mail)
+{
+	struct mail_private *mail = (struct mail_private *)_mail;
+	struct imap_sieve_mailbox_transaction *ismt =
+		IMAP_SIEVE_CONTEXT(_mail->transaction);
+	struct mail_vfuncs *v = mail->vlast;
+	struct imap_sieve_mail *ismail;
+
+	if (ismt == NULL)
+		return;
+
+	ismail = p_new(mail->pool, struct imap_sieve_mail, 1);
+	ismail->module_ctx.super = *v;
+	mail->vlast = &ismail->module_ctx.super;
+
+	v->close = imap_sieve_mail_close;
+	v->free = imap_sieve_mail_free;
+	v->update_flags = imap_sieve_mail_update_flags;
+	v->update_keywords = imap_sieve_mail_update_keywords;
+	MODULE_CONTEXT_SET(mail, imap_sieve_mail_module, ismail);
+}
+
+/*
+ * Save/copy
+ */
+
+static int
+imap_sieve_mailbox_copy(struct mail_save_context *ctx, struct mail *mail)
+{
+	struct mailbox_transaction_context *t = ctx->transaction;
+	struct mail_storage *storage = t->box->storage;
+	struct mail_user *user = storage->user;
+	struct imap_sieve_user *isuser =
+		IMAP_SIEVE_USER_CONTEXT(user);
+	union mailbox_module_context *lbox =
+		IMAP_SIEVE_CONTEXT(t->box);
+	struct imap_sieve_mailbox_transaction *ismt =
+		IMAP_SIEVE_CONTEXT(t);
+
+	if (ismt != NULL) {
+		if (ctx->dest_mail == NULL) {
+			/* Dest mail is required for our purposes */
+			if (ismt->tmp_mail == NULL) {
+				ismt->tmp_mail = mail_alloc(t,
+					MAIL_FETCH_STREAM_HEADER |
+				  MAIL_FETCH_STREAM_BODY, NULL);
+			}
+			ctx->dest_mail = ismt->tmp_mail;
+		}
+	}
+
+	if (lbox->super.copy(ctx, mail) < 0)
+		return -1;
+
+	if (ismt != NULL && !ctx->dest_mail->expunged &&
+		(isuser->cur_cmd == IMAP_SIEVE_CMD_COPY ||
+			isuser->cur_cmd == IMAP_SIEVE_CMD_MOVE)) {
+		imap_sieve_mailbox_debug(t->box, "%s event",
+			(isuser->cur_cmd == IMAP_SIEVE_CMD_COPY ?
+				"COPY" : "MOVE"));
+		imap_sieve_add_mailbox_event
+			(t, ctx->dest_mail, mail->box, NULL);
+	}
+
+	return 0;
+}
+
+static int
+imap_sieve_mailbox_save_begin(struct mail_save_context *ctx,
+	struct istream *input)
+{
+	struct imap_sieve_mailbox_transaction *ismt =
+		IMAP_SIEVE_CONTEXT(ctx->transaction);
+	union mailbox_module_context *lbox =
+		IMAP_SIEVE_CONTEXT(ctx->transaction->box);
+
+	if (ismt != NULL) {
+		if (ctx->dest_mail == NULL) {
+			/* Dest mail is required for our purposes */
+			if (ismt->tmp_mail == NULL) {
+				ismt->tmp_mail = mail_alloc(ctx->transaction,
+					MAIL_FETCH_STREAM_HEADER |
+				  MAIL_FETCH_STREAM_BODY, NULL);
+			}
+			ctx->dest_mail = ismt->tmp_mail;
+		}
+	}
+	return lbox->super.save_begin(ctx, input);
+}
+
+static int
+imap_sieve_mailbox_save_finish(struct mail_save_context *ctx)
+{
+	struct mailbox_transaction_context *t = ctx->transaction;
+	struct mailbox *box = t->box;
+	struct imap_sieve_mailbox_transaction *ismt = IMAP_SIEVE_CONTEXT(t);
+	union mailbox_module_context *lbox = IMAP_SIEVE_CONTEXT(box);
+	struct mail_user *user = box->storage->user;
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+	struct mail *dest_mail = ctx->copying_via_save ? NULL : ctx->dest_mail;
+
+	if (lbox->super.save_finish(ctx) < 0)
+		return -1;
+
+	if (ismt != NULL && dest_mail != NULL &&
+		!dest_mail->expunged &&
+		isuser->cur_cmd == IMAP_SIEVE_CMD_APPEND) {
+
+		imap_sieve_mailbox_debug(t->box, "APPEND event");
+		imap_sieve_add_mailbox_event(t, dest_mail, box, NULL);
+	}
+	return 0;
+}
+
+/*
+ * Mailbox
+ */
+
+static struct mailbox_transaction_context *
+imap_sieve_mailbox_transaction_begin(struct mailbox *box,
+			 enum mailbox_transaction_flags flags)
+{
+	union mailbox_module_context *lbox = IMAP_SIEVE_CONTEXT(box);
+	struct mail_user *user = box->storage->user;
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+	struct mailbox_transaction_context *t;
+	struct imap_sieve_mailbox_transaction *ismt;
+	pool_t pool;
+
+	/* commence parent transaction */
+	t = lbox->super.transaction_begin(box, flags);
+
+	if (isuser == NULL || isuser->sieve_active)
+		return t;
+
+	pool = pool_alloconly_create("imap_sieve_mailbox_transaction", 1024);
+	ismt = p_new(pool, struct imap_sieve_mailbox_transaction, 1);
+	ismt->pool = pool;
+	MODULE_CONTEXT_SET(t, imap_sieve_storage_module, ismt);
+
+	return t;
+}
+
+static void
+imap_sieve_mailbox_transaction_free
+(struct imap_sieve_mailbox_transaction *ismt)
+{
+	i_assert(ismt->tmp_mail == NULL);
+	if (array_is_created(&ismt->events))
+		array_free(&ismt->events);
+	pool_unref(&ismt->pool);
+}
+
+static int
+imap_sieve_mailbox_transaction_run(
+	struct imap_sieve_mailbox_transaction *ismt,
+	struct mailbox *box,
+	struct mail_transaction_commit_changes *changes)
+{
+	static const char *wanted_headers[] = {
+		"From", "To", "Message-ID", "Subject", "Return-Path",
+		NULL
+	};
+	struct mailbox *src_box = ismt->src_box;
+	struct mail_user *user = box->storage->user;
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+	const struct imap_sieve_mailbox_event *mevent;
+	struct mailbox_header_lookup_ctx *headers_ctx;
+	struct mailbox_transaction_context *st;
+	struct mailbox *sbox;
+	struct imap_sieve_run *isrun;
+	struct seq_range_iter siter;
+	const char *cause, *script_name = NULL;
+	bool can_discard;
+	struct mail *mail;
+	int ret;
+
+	if (ismt == NULL || !array_is_created(&ismt->events)) {
+		/* Nothing to do */
+		return 0;
+	}
+
+	/* Get user script for this mailbox */
+	if (isuser->user_script && imap_sieve_mailbox_get_script
+		(box, &script_name) < 0) {
+		return 0; // FIXME: some errors may warrant -1
+	}
+
+	/* Make sure IMAPSIEVE is initialized for this user */
+	if (isuser->isieve == NULL) {
+		isuser->isieve = imap_sieve_init
+			(user, isuser->client->lda_set);
+	}
+
+	/* Get synchronized view on the mailbox */
+	sbox = mailbox_alloc(box->list, box->vname, 0);
+	if (mailbox_sync(sbox, 0) < 0) {
+		mailbox_free(&sbox);
+		return -1;
+	}
+
+	can_discard = FALSE;
+	switch (isuser->cur_cmd) {
+	case IMAP_SIEVE_CMD_APPEND:
+		cause = "APPEND";
+		can_discard = TRUE;
+		break;
+	case IMAP_SIEVE_CMD_COPY:
+	case IMAP_SIEVE_CMD_MOVE:
+		cause = "COPY";
+		can_discard = TRUE;
+		break;
+	case IMAP_SIEVE_CMD_STORE:
+	case IMAP_SIEVE_CMD_OTHER:
+		cause = "FLAG";
+		break;
+	default:
+		i_unreached();
+	}
+
+	/* Initialize execution */
+	T_BEGIN {
+		ARRAY_TYPE(imap_sieve_mailbox_rule) mbrules;
+		ARRAY_TYPE(const_string) scripts_before, scripts_after;
+		struct imap_sieve_mailbox_rule *const *rule_idx;
+
+		/* Find matching rules */
+		t_array_init(&mbrules, 16);
+		imap_sieve_mailbox_rules_get
+			(user, box, src_box, cause, &mbrules);
+
+		/* Apply all matched rules */
+		t_array_init(&scripts_before, 8);
+		t_array_init(&scripts_after, 8);
+		array_foreach(&mbrules, rule_idx) {
+			struct imap_sieve_mailbox_rule *rule = *rule_idx;
+
+			if (rule->before != NULL)
+				array_append(&scripts_before, &rule->before, 1);
+			if (rule->after != NULL)
+				array_append(&scripts_after, &rule->after, 1);
+		}
+		array_append_space(&scripts_before);
+		array_append_space(&scripts_after);
+
+		/* Initialize */
+		ret = imap_sieve_run_init
+			(isuser->isieve, box, cause, script_name,
+				array_idx(&scripts_before, 0),
+				array_idx(&scripts_after, 0), &isrun);
+	} T_END;
+
+	if (ret <= 0) {
+		// FIXME: temp fail should be handled properly
+		return 0;
+	}
+
+	/* Create transaction for event messages */
+	st = mailbox_transaction_begin(sbox, 0);
+	headers_ctx = mailbox_header_lookup_init(sbox, wanted_headers);
+	mail = mail_alloc(st, 0, headers_ctx);
+	mailbox_header_lookup_unref(&headers_ctx);
+
+	/* Iterate through all events */
+	seq_range_array_iter_init(&siter, &changes->saved_uids);
+	array_foreach(&ismt->events, mevent) {
+		uint32_t uid;
+
+		/* Determine UID for saved message */
+		if (mevent->mail_uid > 0 ||
+			!seq_range_array_iter_nth(&siter, mevent->save_seq, &uid))
+			uid = mevent->mail_uid;
+
+		/* Select event message */
+		if (!mail_set_uid(mail, uid)) {
+			imap_sieve_mailbox_error(sbox,
+				"Failed to find message for Sieve event (UID=%llu)",
+				(unsigned long long)uid);
+			continue;
+		}
+
+		i_assert(!mail->expunged);
+
+		/* Run scripts for this mail */
+		ret = imap_sieve_run_mail
+			(isrun, mail, mevent->changed_flags);
+
+		/* Handle the result */
+		if (ret < 0) {
+			/* Sieve error; keep */
+		} else if (ret > 0 && can_discard) {
+			/* Discard */
+			mail_update_flags(mail, MODIFY_ADD, MAIL_DELETED);
+		}
+	}
+
+	/* Cleanup */
+	mail_free(&mail);
+	ret = mailbox_transaction_commit(&st);
+	imap_sieve_run_deinit(&isrun);
+	mailbox_free(&sbox);
+	return ret;
+}
+
+static int
+imap_sieve_mailbox_transaction_commit(
+	struct mailbox_transaction_context *t,
+	struct mail_transaction_commit_changes *changes_r)
+{
+	struct mailbox *box = t->box;
+	struct mail_user *user = box->storage->user;
+	struct imap_sieve_mailbox_transaction *ismt = IMAP_SIEVE_CONTEXT(t);
+	union mailbox_module_context *lbox = IMAP_SIEVE_CONTEXT(t->box);
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+	int ret = 0;
+
+	if (ismt != NULL && ismt->tmp_mail != NULL)
+		mail_free(&ismt->tmp_mail);
+
+	if ((lbox->super.transaction_commit(t, changes_r)) < 0)
+		ret = -1;
+	else {
+		isuser->sieve_active = TRUE;
+		if (imap_sieve_mailbox_transaction_run
+			(ismt, box, changes_r) < 0)
+			ret = -1;
+		isuser->sieve_active = FALSE;
+	}
+
+	if (ismt != NULL)
+		imap_sieve_mailbox_transaction_free(ismt);
+	return ret;
+}
+
+static void
+imap_sieve_mailbox_transaction_rollback(
+	struct mailbox_transaction_context *t)
+{
+	struct imap_sieve_mailbox_transaction *ismt = IMAP_SIEVE_CONTEXT(t);
+	union mailbox_module_context *lbox = IMAP_SIEVE_CONTEXT(t->box);
+
+	if (ismt != NULL && ismt->tmp_mail != NULL)
+		mail_free(&ismt->tmp_mail);
+
+	lbox->super.transaction_rollback(t);
+
+	if (ismt != NULL)
+		imap_sieve_mailbox_transaction_free(ismt);
+}
+
+static void imap_sieve_mailbox_allocated(struct mailbox *box)
+{
+	struct mail_user *user = box->storage->user;
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+	struct mailbox_vfuncs *v = box->vlast;
+	union mailbox_module_context *lbox;
+
+	if (isuser->sieve_active ||
+		(box->flags & MAILBOX_FLAG_READONLY) != 0)
+		return;
+
+	lbox = p_new(box->pool, union mailbox_module_context, 1);
+	lbox->super = *v;
+	box->vlast = &lbox->super;
+
+	v->copy = imap_sieve_mailbox_copy;
+	v->save_begin = imap_sieve_mailbox_save_begin;
+	v->save_finish = imap_sieve_mailbox_save_finish;
+	v->transaction_begin = imap_sieve_mailbox_transaction_begin;
+	v->transaction_commit = imap_sieve_mailbox_transaction_commit;
+	v->transaction_rollback = imap_sieve_mailbox_transaction_rollback;
+	MODULE_CONTEXT_SET_SELF(box, imap_sieve_storage_module, lbox);
+}
+
+/*
+ * Mailbox rules
+ */
+
+static unsigned int imap_sieve_mailbox_rule_hash
+(const struct imap_sieve_mailbox_rule *rule)
+{
+	unsigned int hash = str_hash(rule->mailbox);
+
+	if (rule->from != NULL)
+		hash += str_hash(rule->from);
+	return hash;
+}
+
+static int imap_sieve_mailbox_rule_cmp
+(const struct imap_sieve_mailbox_rule *rule1,
+	const struct imap_sieve_mailbox_rule *rule2)
+{
+	int ret;
+
+	if ((ret=strcmp(rule1->mailbox, rule2->mailbox)) != 0)
+		return ret;
+	return null_strcmp(rule1->from, rule2->from);
+}
+
+static bool rule_pattern_has_wildcards(const char *pattern)
+{
+	for (; *pattern != '\0'; pattern++) {
+		if (*pattern == '%' || *pattern == '*')
+			return TRUE;
+	}
+	return FALSE;
+}
+
+static void
+imap_sieve_mailbox_rules_init(struct mail_user *user)
+{
+	struct imap_sieve_user *isuser = IMAP_SIEVE_USER_CONTEXT(user);
+	string_t *identifier = t_str_new(256);
+	unsigned int i = 0;
+	size_t prefix_len;
+
+	str_append(identifier, "imapsieve_mailbox");
+	prefix_len = str_len(identifier);
+
+	for (i = 1; ; i++) {
+		struct imap_sieve_mailbox_rule *mbrule;
+		const char *setval;
+		size_t id_len;
+
+		str_truncate(identifier, prefix_len);
+		str_printfa(identifier, "%u", i);
+		id_len = str_len(identifier);
+
+		str_append(identifier, "_name");
+		setval = mail_user_plugin_getenv
+			(user, str_c(identifier));
+		if (setval == NULL || *setval == '\0')
+			break;
+
+		mbrule = p_new(user->pool,
+			struct imap_sieve_mailbox_rule, 1);
+		mbrule->index = i;
+		mbrule->mailbox = ph_p_str_trim(user->pool, setval, "\t ");
+
+		str_truncate(identifier, id_len);
+		str_append(identifier, "_from");
+		setval = mail_user_plugin_getenv(user, str_c(identifier));
+		if (setval != NULL && *setval != '\0') {
+			mbrule->from = ph_p_str_trim(user->pool, setval, "\t ");
+			if (strcmp(mbrule->from, "*") == 0)
+				mbrule->from = NULL;
+		}
+
+		if ((strcmp(mbrule->mailbox, "*") == 0 ||
+				!rule_pattern_has_wildcards(mbrule->mailbox)) &&
+			(mbrule->from == NULL ||
+				!rule_pattern_has_wildcards(mbrule->from)) &&
+			hash_table_lookup(isuser->mbox_rules, mbrule) != NULL) {
+			imap_sieve_warning(user,
+				"Duplicate static mailbox rule [%u] for mailbox `%s' "
+				"(skipped)", i, mbrule->mailbox);
+			continue;
+		}
+
+		str_truncate(identifier, id_len);
+		str_append(identifier, "_causes");
+		setval = mail_user_plugin_getenv(user, str_c(identifier));
+		if (setval != NULL && *setval != '\0') {
+			const char *const *cause;
+
+			mbrule->causes = (const char *const *)
+				p_strsplit_spaces(user->pool, setval, " \t,");
+
+			for (cause = mbrule->causes; *cause != NULL; cause++) {
+				if (!imap_sieve_event_cause_valid(*cause))
+					break;
+			}
+			if (*cause != NULL) {
+				imap_sieve_warning(user,
+					"Static mailbox rule [%u] has invalid event cause `%s' "
+					"(skipped)", i, *cause);
+				continue;
+			}
+		}
+
+		str_truncate(identifier, id_len);
+		str_append(identifier, "_before");
+		setval = mail_user_plugin_getenv(user, str_c(identifier));
+		mbrule->before = p_strdup_empty(user->pool, setval);
+
+		str_truncate(identifier, id_len);
+		str_append(identifier, "_after");
+		setval = mail_user_plugin_getenv(user, str_c(identifier));
+		mbrule->after = p_strdup_empty(user->pool, setval);
+
+		if (user->mail_debug) {
+			imap_sieve_debug(user, "Static mailbox rule [%u]: "
+				"mailbox=`%s' from=`%s' causes=(%s) => "
+				"before=%s after=%s",
+				mbrule->index, mbrule->mailbox,
+				(mbrule->from == NULL ? "*" : mbrule->from),
+				t_strarray_join(mbrule->causes, " "),
+				(mbrule->before == NULL ? "(none)" :
+					t_strconcat("`", mbrule->before, "'", NULL)),
+				(mbrule->after == NULL ? "(none)" :
+					t_strconcat("`", mbrule->after, "'", NULL)));
+		}
+
+		if ((strcmp(mbrule->mailbox, "*") == 0 ||
+				!rule_pattern_has_wildcards(mbrule->mailbox)) &&
+			(mbrule->from == NULL ||
+				!rule_pattern_has_wildcards(mbrule->from))) {
+			hash_table_insert(isuser->mbox_rules, mbrule, mbrule);
+		} else {
+			array_append(&isuser->mbox_patterns, &mbrule, 1);
+		}
+	}
+
+	if (i == 0)
+		imap_sieve_debug(user, "No static mailbox rules");
+}
+
+static bool
+imap_sieve_mailbox_rule_match_cause
+(struct imap_sieve_mailbox_rule *rule, const char *cause)
+{
+	const char *const *cp;
+
+	if (rule->causes == NULL || *rule->causes == '\0')
+		return TRUE;
+
+	for (cp = rule->causes; *cp != NULL; cp++) {
+		if (strcasecmp(cause, *cp) == 0)
+			return TRUE;
+	}
+	return FALSE;
+}
+
+static void
+imap_sieve_mailbox_rules_match_patterns(struct mail_user *user,
+	struct mailbox *dst_box, struct mailbox *src_box,
+	const char *cause,
+	ARRAY_TYPE(imap_sieve_mailbox_rule) *rules)
+{
+	struct imap_sieve_user *isuser = IMAP_SIEVE_USER_CONTEXT(user);
+	struct imap_sieve_mailbox_rule *const *rule_idx;
+	struct mail_namespace *dst_ns, *src_ns;
+
+	if (array_count(&isuser->mbox_patterns) == 0)
+		return;
+
+	dst_ns = mailbox_get_namespace(dst_box);
+	src_ns = (src_box == NULL ? NULL :
+		mailbox_get_namespace(src_box));
+
+	array_foreach(&isuser->mbox_patterns, rule_idx) {
+		struct imap_sieve_mailbox_rule *rule = *rule_idx;
+		struct imap_match_glob *glob;
+
+		if (src_ns == NULL && rule->from != NULL)
+			continue;
+		if (!imap_sieve_mailbox_rule_match_cause(rule, cause))
+			continue;
+
+		if (strcmp(rule->mailbox, "*") != 0) {
+			glob = imap_match_init(pool_datastack_create(),
+				rule->mailbox, TRUE, mail_namespace_get_sep(dst_ns));
+			if (imap_match(glob, mailbox_get_vname(dst_box))
+				!= IMAP_MATCH_YES)
+				continue;
+		}
+		if (rule->from != NULL) {
+			glob = imap_match_init(pool_datastack_create(),
+				rule->from, TRUE, mail_namespace_get_sep(src_ns));
+			if (imap_match(glob, mailbox_get_vname(src_box))
+				!= IMAP_MATCH_YES)
+				continue;
+		}
+
+		imap_sieve_debug(user,
+			"Matched static mailbox rule [%u]",
+			rule->index);
+		array_append(rules, &rule, 1);
+	}
+}
+
+static void
+imap_sieve_mailbox_rules_match(struct mail_user *user,
+	const char *dst_box, const char *src_box,
+	const char *cause,
+	ARRAY_TYPE(imap_sieve_mailbox_rule) *rules)
+{
+	struct imap_sieve_user *isuser = IMAP_SIEVE_USER_CONTEXT(user);
+	struct imap_sieve_mailbox_rule lookup_rule;
+	struct imap_sieve_mailbox_rule *rule;
+
+	memset(&lookup_rule, 0, sizeof(lookup_rule));
+	lookup_rule.mailbox = dst_box;
+	lookup_rule.from = src_box;
+	rule = hash_table_lookup(isuser->mbox_rules, &lookup_rule);
+
+	if (rule != NULL &&
+		imap_sieve_mailbox_rule_match_cause(rule, cause)) {
+		struct imap_sieve_mailbox_rule *const *rule_idx;
+		unsigned int insert_idx = 0;
+
+		/* Insert sorted by rule index */
+		array_foreach(rules, rule_idx) {
+			if (rule->index < (*rule_idx)->index) {
+				insert_idx = array_foreach_idx(rules, rule_idx);
+				break;
+			}
+		}
+		array_insert(rules, insert_idx, &rule, 1);
+
+		imap_sieve_debug(user,
+			"Matched static mailbox rule [%u]",
+			rule->index);
+	}
+}
+
+static void
+imap_sieve_mailbox_rules_get(struct mail_user *user,
+	struct mailbox *dst_box, struct mailbox *src_box,
+	const char *cause,
+	ARRAY_TYPE(imap_sieve_mailbox_rule) *rules)
+{
+	const char *dst_name, *src_name;
+
+	imap_sieve_mailbox_rules_match_patterns
+		(user, dst_box, src_box, cause, rules);
+
+	dst_name = mailbox_get_vname(dst_box);
+	src_name = (src_box == NULL ? NULL :
+		mailbox_get_vname(src_box));
+
+	imap_sieve_mailbox_rules_match
+		(user, dst_name, src_name, cause, rules);
+	imap_sieve_mailbox_rules_match
+		(user, "*", src_name, cause, rules);
+	if (src_name != NULL) {
+		imap_sieve_mailbox_rules_match
+			(user, dst_name, NULL, cause, rules);
+		imap_sieve_mailbox_rules_match
+			(user, "*", NULL, cause, rules);
+	}
+}
+
+/*
+ * User
+ */
+
+static void imap_sieve_user_deinit(struct mail_user *user)
+{
+	struct imap_sieve_user *isuser = IMAP_SIEVE_USER_CONTEXT(user);
+
+	if (isuser->isieve != NULL)
+		imap_sieve_deinit(&isuser->isieve);
+
+	hash_table_destroy(&isuser->mbox_rules);
+	array_free(&isuser->mbox_patterns);
+
+	isuser->module_ctx.super.deinit(user);
+}
+
+static void imap_sieve_user_created(struct mail_user *user)
+{
+	struct imap_sieve_user *isuser;
+	struct mail_user_vfuncs *v = user->vlast;
+
+	isuser = p_new(user->pool, struct imap_sieve_user, 1);
+	isuser->module_ctx.super = *v;
+	user->vlast = &isuser->module_ctx.super;
+	v->deinit = imap_sieve_user_deinit;
+	MODULE_CONTEXT_SET(user, imap_sieve_user_module, isuser);
+
+	hash_table_create(&isuser->mbox_rules, default_pool, 0,
+		imap_sieve_mailbox_rule_hash, imap_sieve_mailbox_rule_cmp);
+	i_array_init(&isuser->mbox_patterns, 8);
+
+	imap_sieve_mailbox_rules_init(user);
+}
+
+/*
+ * Hooks
+ */
+
+static struct mail_storage_hooks imap_sieve_mail_storage_hooks = {
+	.mail_user_created = imap_sieve_user_created,
+	.mailbox_allocated = imap_sieve_mailbox_allocated,
+	.mail_allocated = imap_sieve_mail_allocated
+};
+
+/*
+ * Commands
+ */
+
+static void imap_sieve_command_pre(struct client_command_context *cmd)
+{
+	struct client *client = cmd->client;
+	struct mail_user *user = client->user;
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+
+	if (isuser == NULL)
+		return;
+
+	if (strcasecmp(cmd->name, "APPEND") == 0) {
+		isuser->cur_cmd = IMAP_SIEVE_CMD_APPEND;
+	} else 	if (strcasecmp(cmd->name, "COPY") == 0 ||
+		strcasecmp(cmd->name, "UID COPY") == 0) {
+		isuser->cur_cmd = IMAP_SIEVE_CMD_COPY;
+	} else 	if (strcasecmp(cmd->name, "MOVE") == 0 ||
+		strcasecmp(cmd->name, "UID MOVE") == 0) {
+		isuser->cur_cmd = IMAP_SIEVE_CMD_MOVE;
+	} else 	if (strcasecmp(cmd->name, "STORE") == 0 ||
+		strcasecmp(cmd->name, "UID STORE") == 0) {
+		isuser->cur_cmd = IMAP_SIEVE_CMD_STORE;
+	} else {
+		isuser->cur_cmd = IMAP_SIEVE_CMD_OTHER;
+	}
+}
+
+static void imap_sieve_command_post(struct client_command_context *cmd)
+{
+	struct client *client = cmd->client;
+	struct mail_user *user = client->user;
+	struct imap_sieve_user *isuser = 	IMAP_SIEVE_USER_CONTEXT(user);
+
+	if (isuser == NULL)
+		return;
+	isuser->cur_cmd = IMAP_SIEVE_CMD_NONE;
+}
+
+/*
+ * Client
+ */
+
+void imap_sieve_storage_client_created(struct client *client,
+	bool user_script)
+{
+	struct imap_sieve_user *isuser = IMAP_SIEVE_USER_CONTEXT(client->user);
+
+	isuser->client = client;
+	isuser->user_script = user_script;
+}
+
+/*
+ *
+ */
+
+void imap_sieve_storage_init(struct module *module)
+{
+	command_hook_register(imap_sieve_command_pre, imap_sieve_command_post);
+	mail_storage_hooks_add(module, &imap_sieve_mail_storage_hooks);
+}
+
+void imap_sieve_storage_deinit(void)
+{
+	mail_storage_hooks_remove(&imap_sieve_mail_storage_hooks);
+	command_hook_unregister(imap_sieve_command_pre, imap_sieve_command_post);
+}
diff --git a/src/plugins/imapsieve/imap-sieve-storage.h b/src/plugins/imapsieve/imap-sieve-storage.h
new file mode 100644
index 0000000000000000000000000000000000000000..0921e06949fb111a32b291f598eaaa6a659411f4
--- /dev/null
+++ b/src/plugins/imapsieve/imap-sieve-storage.h
@@ -0,0 +1,13 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef __IMAP_SIEVE_STORAGE_H
+#define __IMAP_SIEVE_STORAGE_H
+
+void imap_sieve_storage_init(struct module *module);
+void imap_sieve_storage_deinit(void);
+
+void imap_sieve_storage_client_created(struct client *client,
+	bool user_script);
+
+#endif
diff --git a/src/plugins/imapsieve/imap-sieve.c b/src/plugins/imapsieve/imap-sieve.c
new file mode 100644
index 0000000000000000000000000000000000000000..5904aab903e19b94dc646cb0afc25927ce5a0671
--- /dev/null
+++ b/src/plugins/imapsieve/imap-sieve.c
@@ -0,0 +1,755 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "home-expand.h"
+#include "mail-storage.h"
+#include "mail-user.h"
+#include "lda-settings.h"
+#include "mail-deliver.h"
+#include "duplicate.h"
+#include "smtp-client.h"
+
+#include "sieve.h"
+#include "sieve-script.h"
+#include "sieve-storage.h"
+
+#include "ext-imapsieve-common.h"
+
+#include "imap-sieve.h"
+
+/*
+ * Configuration
+ */
+
+#define IMAP_SIEVE_MAX_USER_ERRORS 30
+
+/*
+ * IMAP Sieve
+ */
+
+struct imap_sieve {
+	pool_t pool;
+	struct mail_user *user;
+	const struct lda_settings *lda_set;
+	const char *home_dir;
+
+	struct sieve_instance *svinst;
+	struct sieve_storage *storage;
+	const struct sieve_extension *ext_imapsieve;
+
+	struct duplicate_context *dup_ctx;
+
+	struct sieve_error_handler *master_ehandler;
+};
+
+static const char *
+mail_sieve_get_setting(void *context, const char *identifier)
+{
+	struct imap_sieve *isieve = (struct imap_sieve *)context;
+
+	return mail_user_plugin_getenv(isieve->user, identifier);
+}
+
+static const struct sieve_callbacks mail_sieve_callbacks = {
+	NULL,
+	mail_sieve_get_setting
+};
+
+
+struct imap_sieve *imap_sieve_init(struct mail_user *user,
+	const struct lda_settings *lda_set)
+{
+	struct sieve_environment svenv;
+	struct imap_sieve *isieve;
+	bool debug = user->mail_debug;
+	pool_t pool;
+
+	pool = pool_alloconly_create("imap_sieve", 256);
+	isieve = p_new(pool, struct imap_sieve, 1);
+	isieve->pool = pool;
+	isieve->user = user;
+	isieve->lda_set = lda_set;
+
+	isieve->dup_ctx = duplicate_init(user);
+
+	memset(&svenv, 0, sizeof(svenv));
+	svenv.username = user->username;
+	(void)mail_user_get_home(user, &svenv.home_dir);
+	svenv.hostname = lda_set->hostname;
+	svenv.base_dir = user->set->base_dir;
+	svenv.flags = SIEVE_FLAG_HOME_RELATIVE;
+	svenv.location = SIEVE_ENV_LOCATION_MS;
+	svenv.delivery_phase = SIEVE_DELIVERY_PHASE_POST;
+
+	isieve->home_dir = p_strdup(pool, svenv.home_dir);
+
+	isieve->svinst = sieve_init
+		(&svenv, &mail_sieve_callbacks, isieve, debug);
+
+	isieve->ext_imapsieve = sieve_extension_replace
+		(isieve->svinst, &imapsieve_extension, TRUE);
+
+	isieve->master_ehandler = sieve_master_ehandler_create
+		(isieve->svinst, NULL, 0); // FIXME: prefix?
+	sieve_system_ehandler_set(isieve->master_ehandler);
+	sieve_error_handler_accept_infolog(isieve->master_ehandler, TRUE);
+	sieve_error_handler_accept_debuglog(isieve->master_ehandler, debug);
+
+	return isieve;
+}
+
+void imap_sieve_deinit(struct imap_sieve **_isieve)
+{
+	struct imap_sieve *isieve = *_isieve;
+
+	*_isieve = NULL;
+
+	sieve_error_handler_unref(&isieve->master_ehandler);
+
+	if (isieve->storage != NULL)
+		sieve_storage_unref(&isieve->storage);
+	sieve_extension_unregister(isieve->ext_imapsieve);
+	sieve_deinit(&isieve->svinst);
+
+	duplicate_deinit(&isieve->dup_ctx);
+
+	pool_unref(&isieve->pool);
+}
+
+static int
+imap_sieve_get_storage(struct imap_sieve *isieve,
+	struct sieve_storage **storage_r)
+{
+	enum sieve_storage_flags storage_flags = 0;
+	enum sieve_error error;
+
+	if (isieve->storage != NULL) {
+		*storage_r = isieve->storage;
+		return 1;
+	}
+
+	// FIXME: limit interval between retries
+
+	isieve->storage = sieve_storage_create_main
+		(isieve->svinst, isieve->user, storage_flags, &error);
+	if (isieve->storage == NULL) {
+		if (error == SIEVE_ERROR_TEMP_FAILURE)
+			return -1;
+		return 0;
+	}
+	*storage_r = isieve->storage;
+	return 1;
+}
+
+/*
+ * Mail transmission
+ */
+
+static void *imap_sieve_smtp_start
+(const struct sieve_script_env *senv, const char *return_path)
+{
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+
+	return (void *)smtp_client_init
+		(isctx->isieve->lda_set, return_path);
+}
+
+static void imap_sieve_smtp_add_rcpt
+(const struct sieve_script_env *senv ATTR_UNUSED, void *handle,
+	const char *address)
+{
+	struct smtp_client *smtp_client = (struct smtp_client *) handle;
+
+	smtp_client_add_rcpt(smtp_client, address);
+}
+
+static struct ostream *imap_sieve_smtp_send
+(const struct sieve_script_env *senv ATTR_UNUSED, void *handle)
+{
+	struct smtp_client *smtp_client = (struct smtp_client *) handle;
+
+	return smtp_client_send(smtp_client);
+}
+
+static int imap_sieve_smtp_finish
+(const struct sieve_script_env *senv ATTR_UNUSED, void *handle,
+	const char **error_r)
+{
+	struct smtp_client *smtp_client = (struct smtp_client *) handle;
+
+	return smtp_client_deinit_timeout
+		(smtp_client, LDA_SUBMISSION_TIMEOUT_SECS, error_r);
+}
+
+/*
+ * Duplicate checking
+ */
+
+static int imap_sieve_duplicate_check
+(const struct sieve_script_env *senv, const void *id,
+	size_t id_size)
+{
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+
+	return duplicate_check(isctx->isieve->dup_ctx,
+		id, id_size, senv->user->username);
+}
+
+static void imap_sieve_duplicate_mark
+(const struct sieve_script_env *senv, const void *id,
+	size_t id_size, time_t time)
+{
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+
+	duplicate_mark(isctx->isieve->dup_ctx,
+		id, id_size, senv->user->username, time);
+}
+
+static void imap_sieve_duplicate_flush
+(const struct sieve_script_env *senv)
+{
+	struct imap_sieve_context *isctx =
+		(struct imap_sieve_context *)senv->script_context;
+	duplicate_flush(isctx->isieve->dup_ctx);
+}
+
+/*
+ * IMAP Sieve run
+ */
+
+struct imap_sieve_run_script {
+	struct sieve_script *script;
+	struct sieve_binary *binary;
+
+	/* Compile failed once; don't try again for this transaction */
+	unsigned int compile_failed:1;
+	/* Binary corrupt after recompile; don't recompile again */
+	unsigned int binary_corrupt:1;
+};
+
+struct imap_sieve_run {
+	pool_t pool;
+	struct imap_sieve *isieve;
+	struct mailbox *mailbox;
+	char *cause;
+
+	struct sieve_error_handler *user_ehandler;
+	char *userlog;
+
+	struct sieve_script *user_script;
+	struct imap_sieve_run_script *scripts;
+	unsigned int scripts_count;
+};
+
+static void
+imap_sieve_run_init_user_log(
+	struct imap_sieve_run *isrun)
+{
+	struct imap_sieve *isieve = isrun->isieve;
+	struct sieve_instance *svinst = isieve->svinst;
+	const char *log_path;
+
+	/* Determine user log file path */ // FIXME: code shared with LDA
+	if ( (log_path=mail_user_plugin_getenv
+		(isieve->user, "sieve_user_log")) == NULL ) {
+		const char *path;
+
+		if ( isrun->user_script == NULL ||
+			(path=sieve_file_script_get_path
+				(isrun->user_script)) == NULL ) {
+			/* Default */
+			if ( isieve->home_dir != NULL ) {
+				log_path = t_strconcat
+					(isieve->home_dir, "/.dovecot.sieve.log", NULL);
+			}
+		} else {
+			/* Use script file as a basis (legacy behavior) */
+			log_path = t_strconcat(path, ".log", NULL);
+		}
+	} else {
+		if ( isieve->home_dir != NULL ) {
+			/* Expand home dir if necessary */
+			if ( log_path[0] == '~' ) {
+				log_path = home_expand_tilde
+					(log_path, isieve->home_dir);
+			} else if ( log_path[0] != '/' ) {
+				log_path = t_strconcat
+					(isieve->home_dir, "/", log_path, NULL);
+			}
+		}
+	}
+
+	/* Initialize user error handler */
+	if ( log_path != NULL ) {
+		isrun->userlog = p_strdup(isrun->pool, log_path);
+		isrun->user_ehandler = sieve_logfile_ehandler_create
+			(svinst, log_path, IMAP_SIEVE_MAX_USER_ERRORS);
+	}
+}
+
+int imap_sieve_run_init(struct imap_sieve *isieve,
+	struct mailbox *mailbox, const char *cause,
+	const char *script_name,
+	const char *const *scripts_before,
+	const char *const *scripts_after,
+	struct imap_sieve_run **isrun_r)
+{
+	struct sieve_instance *svinst = isieve->svinst;
+	struct imap_sieve_run *isrun;
+	struct sieve_storage *storage;
+	struct imap_sieve_run_script *scripts;
+	struct sieve_script *user_script;
+	const char *const *sp;
+	enum sieve_error error;
+	pool_t pool;
+	unsigned int max_len, count;
+	int ret;
+
+	/* Determine how many scripts we may run for this event */
+	max_len = 0;
+	if (scripts_before != NULL)
+		max_len += str_array_length(scripts_before);
+	if (script_name != NULL)
+		max_len++;
+	if (scripts_after != NULL)
+		max_len += str_array_length(scripts_after);
+	if (max_len == 0)
+		return 0;
+
+	/* Get storage for user script */
+	storage = NULL;
+	if ((ret=imap_sieve_get_storage(isieve, &storage)) < 0)
+		return ret;
+
+	/* Open all scripts */
+	count = 0;
+	pool = pool_alloconly_create("imap_sieve_run", 256);
+	scripts = p_new(pool, struct imap_sieve_run_script, max_len);
+
+	/* Admin scripts before user script */
+	if (scripts_before != NULL) {
+		for (sp = scripts_before; *sp != NULL; sp++) {
+			i_assert(count < max_len);
+			scripts[count].script = sieve_script_create_open
+				(svinst, *sp, NULL, &error);
+			if (scripts[count].script != NULL)
+				count++;
+			else if (error == SIEVE_ERROR_TEMP_FAILURE)
+				return -1;
+		}
+	}
+
+	/* The user script */
+	user_script = NULL;
+	if (storage != NULL && script_name != NULL &&
+		*script_name != '\0') {
+		i_assert(count < max_len);
+		scripts[count].script = sieve_storage_open_script
+			(storage, script_name, &error);
+		if (scripts[count].script != NULL) {
+			user_script = scripts[count].script;
+			count++;
+		} else if (error == SIEVE_ERROR_TEMP_FAILURE) {
+			return -1;
+		}
+	}
+
+	/* Admin scripts after user script */
+	if (scripts_after != NULL) {
+		for (sp = scripts_after; *sp != NULL; sp++) {
+			i_assert(count < max_len);
+			scripts[count].script = sieve_script_create_open
+				(svinst, *sp, NULL, &error);
+			if (scripts[count].script != NULL)
+				count++;
+			else if (error == SIEVE_ERROR_TEMP_FAILURE)
+				return -1;
+		}
+	}
+
+	if (count == 0) {
+		/* None of the scripts could be opened */
+		pool_unref(&pool);
+		return 0;
+	}
+
+	/* Initialize */
+	isrun = p_new(pool, struct imap_sieve_run, 1);
+	isrun->pool = pool;
+	isrun->isieve = isieve;
+	isrun->mailbox = mailbox;
+	isrun->cause = p_strdup(pool, cause);
+	isrun->user_script = user_script;
+	isrun->scripts = scripts;
+	isrun->scripts_count = count;
+
+	imap_sieve_run_init_user_log(isrun);
+
+	*isrun_r = isrun;
+	return 1;
+}
+
+void imap_sieve_run_deinit(struct imap_sieve_run **_isrun)
+{
+	struct imap_sieve_run *isrun = *_isrun;
+	unsigned int i;
+
+	*_isrun = NULL;
+
+	for (i = 0; i < isrun->scripts_count; i++) {
+		if (isrun->scripts[i].binary != NULL)
+			sieve_close(&isrun->scripts[i].binary);
+		if (isrun->scripts[i].script != NULL)
+			sieve_script_unref(&isrun->scripts[i].script);
+	}
+	if (isrun->user_ehandler != NULL)
+		sieve_error_handler_unref(&isrun->user_ehandler);
+
+	pool_unref(&isrun->pool);
+}
+
+static struct sieve_binary *
+imap_sieve_run_open_script(
+	struct imap_sieve_run *isrun,
+	struct sieve_script *script,
+	enum sieve_compile_flags cpflags,
+	bool recompile, enum sieve_error *error_r)
+{
+	struct imap_sieve *isieve = isrun->isieve;
+	struct sieve_instance *svinst = isieve->svinst;
+	struct sieve_error_handler *ehandler = isrun->user_ehandler;
+	struct sieve_binary *sbin;
+	const char *compile_name = "compile";
+	bool debug = isieve->user->mail_debug;
+
+	if ( recompile ) {
+		/* Warn */
+		sieve_sys_warning(svinst,
+			"Encountered corrupt binary: re-compiling script %s",
+			sieve_script_location(script));
+		compile_name = "re-compile";
+	} else 	if ( debug ) {
+		sieve_sys_debug(svinst,
+			"Loading script %s", sieve_script_location(script));
+	}
+
+	if ( script == isrun->user_script )
+		ehandler = isrun->user_ehandler;
+	else
+		ehandler = isieve->master_ehandler;
+	sieve_error_handler_reset(ehandler);
+
+	/* Load or compile the sieve script */
+	if ( recompile ) {
+		sbin = sieve_compile_script
+			(script, ehandler, cpflags, error_r);
+	} else {
+		sbin = sieve_open_script
+			(script, ehandler, cpflags, error_r);
+	}
+
+	/* Handle error */
+	if ( sbin == NULL ) {
+		switch ( *error_r ) {
+		/* Script not found */
+		case SIEVE_ERROR_NOT_FOUND:
+			if ( debug ) {
+				sieve_sys_debug(svinst, "Script `%s' is missing for %s",
+					sieve_script_location(script), compile_name);
+			}
+			break;
+		/* Temporary failure */
+		case SIEVE_ERROR_TEMP_FAILURE:
+			sieve_sys_error(svinst,
+				"Failed to open script `%s' for %s (temporary failure)",
+				sieve_script_location(script), compile_name);
+			break;
+		/* Compile failed */
+		case SIEVE_ERROR_NOT_VALID:
+			if (script == isrun->user_script && isrun->userlog != NULL ) {
+				sieve_sys_info(svinst,
+					"Failed to %s script `%s' "
+					"(view user logfile `%s' for more information)",
+					compile_name, sieve_script_location(script),
+					isrun->userlog);
+				break;
+			}
+			sieve_sys_error(svinst,	"Failed to %s script `%s'",
+				compile_name, sieve_script_location(script));
+			break;
+		/* Something else */
+		default:
+			sieve_sys_error(svinst,	"Failed to open script `%s' for %s",
+				sieve_script_location(script), compile_name);
+			break;
+		}
+
+		return NULL;
+	}
+
+	if (!recompile)
+		(void)sieve_save(sbin, FALSE, NULL);
+	return sbin;
+}
+
+static int imap_sieve_handle_exec_status
+(struct imap_sieve_run *isrun,
+	struct sieve_script *script, int status, bool keep,
+	struct sieve_exec_status *estatus)
+	ATTR_NULL(2)
+{
+	struct imap_sieve *isieve = isrun->isieve;
+	struct sieve_instance *svinst = isieve->svinst;
+	const char *userlog_notice = "";
+	sieve_sys_error_func_t error_func, user_error_func;
+	enum mail_error mail_error = MAIL_ERROR_NONE;
+	int ret = -1;
+
+	error_func = user_error_func = sieve_sys_error;
+
+	if ( estatus != NULL && estatus->last_storage != NULL &&
+		estatus->store_failed) {
+		mail_storage_get_last_error(estatus->last_storage, &mail_error);
+
+		/* Don't bother administrator too much with benign errors */
+		if ( mail_error == MAIL_ERROR_NOQUOTA ) {
+			error_func = sieve_sys_info;
+			user_error_func = sieve_sys_info;
+		}
+	}
+
+	if ( script == isrun->user_script && isrun->userlog != NULL ) {
+		userlog_notice = t_strdup_printf
+			(" (user logfile %s may reveal additional details)",
+				isrun->userlog);
+		user_error_func = sieve_sys_info;
+	}
+
+	switch ( status ) {
+	case SIEVE_EXEC_FAILURE:
+		user_error_func(svinst,
+			"Execution of script %s failed%s",
+			sieve_script_location(script), userlog_notice);
+		ret = 0;
+		break;
+	case SIEVE_EXEC_TEMP_FAILURE:
+		error_func(svinst,
+			"Execution of script %s was aborted "
+			"due to temporary failure%s",
+			sieve_script_location(script), userlog_notice);
+		ret = -1;
+		break;
+	case SIEVE_EXEC_BIN_CORRUPT:
+		sieve_sys_error(svinst,
+			"!!BUG!!: Binary compiled from %s is still corrupt; "
+			"bailing out and reverting to default action",
+			sieve_script_location(script));
+		ret = 0;
+		break;
+	case SIEVE_EXEC_KEEP_FAILED:
+		error_func(svinst,
+			"Execution of script %s failed "
+			"with unsuccessful implicit keep%s",
+			sieve_script_location(script), userlog_notice);
+		ret = 0;
+		break;
+	case SIEVE_EXEC_OK:
+		ret = (keep ? 0 : 1);
+		break;
+	}
+
+	return ret;
+}
+
+static int imap_sieve_run_scripts
+(struct imap_sieve_run *isrun,
+	const struct sieve_message_data *msgdata,
+	const struct sieve_script_env *scriptenv)
+{
+	struct imap_sieve *isieve = isrun->isieve;
+	struct sieve_instance *svinst = isieve->svinst;
+	struct imap_sieve_run_script *scripts = isrun->scripts;
+	unsigned int count = isrun->scripts_count;
+	struct sieve_multiscript *mscript;
+	struct sieve_error_handler *ehandler;
+	struct sieve_script *last_script = NULL;
+	bool user_script = FALSE, more = TRUE, compile_error = FALSE;
+	bool debug = isieve->user->mail_debug, keep = TRUE;
+	enum sieve_compile_flags cpflags;
+	enum sieve_execute_flags exflags;
+
+	enum sieve_error error;
+	unsigned int i;
+	int ret;
+
+	/* Start execution */
+	mscript = sieve_multiscript_start_execute
+		(svinst, msgdata, scriptenv);
+
+	/* Execute scripts */
+	for ( i = 0; i < count && more; i++ ) {
+		struct sieve_script *script = scripts[i].script;
+		struct sieve_binary *sbin = scripts[i].binary;
+
+		cpflags = 0;
+		exflags = SIEVE_EXECUTE_FLAG_DEFER_KEEP |
+			SIEVE_EXECUTE_FLAG_NO_ENVELOPE;
+
+		user_script = ( script == isrun->user_script );
+		last_script = script;
+
+		if ( user_script ) {
+			cpflags |= SIEVE_COMPILE_FLAG_NOGLOBAL;
+			exflags |= SIEVE_EXECUTE_FLAG_NOGLOBAL;
+			ehandler = isrun->user_ehandler;
+		} else {
+			cpflags |= SIEVE_COMPILE_FLAG_NO_ENVELOPE;
+			ehandler = isieve->master_ehandler;
+		}
+
+		/* Open */
+		if (sbin == NULL) {
+			if ( debug ) {
+				sieve_sys_debug(svinst,
+					"Opening script %d of %d from `%s'",
+					i+1, count, sieve_script_location(script));
+			}
+
+			/* Already known to fail */
+			if (scripts[i].compile_failed) {
+				compile_error = TRUE;
+				break;
+			}
+
+			/* Try to open/compile binary */
+			scripts[i].binary = sbin = imap_sieve_run_open_script
+				(isrun, script, cpflags, FALSE, &error);
+			if ( sbin == NULL ) {
+				scripts[i].compile_failed = TRUE;
+				compile_error = TRUE;
+				break;
+			}
+		}
+
+		/* Execute */
+		if ( debug ) {
+			sieve_sys_debug(svinst,
+				"Executing script from `%s'",
+				sieve_get_source(sbin));
+		}
+		more = sieve_multiscript_run(mscript,
+			sbin, ehandler, ehandler, exflags);
+
+		if ( !more ) {
+			if ( !scripts[i].binary_corrupt &&
+				sieve_multiscript_status(mscript)
+					== SIEVE_EXEC_BIN_CORRUPT &&
+				sieve_is_loaded(sbin) ) {
+
+				/* Close corrupt script */
+				sieve_close(&sbin);
+
+				/* Recompile */
+				scripts[i].binary = sbin = imap_sieve_run_open_script
+					(isrun, script, cpflags, FALSE, &error);
+				if ( sbin == NULL ) {
+					scripts[i].compile_failed = TRUE;
+					compile_error = TRUE;
+					break;
+				}
+
+				/* Execute again */
+				more = sieve_multiscript_run(mscript, sbin,
+					ehandler, ehandler, exflags);
+
+				/* Save new version */
+
+				if ( sieve_multiscript_status(mscript)
+					== SIEVE_EXEC_BIN_CORRUPT )
+					scripts[i].binary_corrupt = TRUE;
+				else if ( more )
+					(void)sieve_save(sbin, FALSE, NULL);
+			}
+		}
+	}
+
+	/* Finish execution */
+	exflags = SIEVE_EXECUTE_FLAG_DEFER_KEEP |
+		SIEVE_EXECUTE_FLAG_NO_ENVELOPE;
+	ehandler = (isrun->user_ehandler != NULL ?
+		isrun->user_ehandler : isieve->master_ehandler);
+	if ( compile_error && error == SIEVE_ERROR_TEMP_FAILURE ) {
+		ret = sieve_multiscript_tempfail
+			(&mscript, ehandler, exflags);
+	} else {
+		ret = sieve_multiscript_finish
+			(&mscript, ehandler, exflags, &keep);
+	}
+
+	/* Don't log additional messages about compile failure */
+	if ( compile_error && ret == SIEVE_EXEC_FAILURE ) {
+		sieve_sys_info(svinst,
+			"Aborted script execution sequence "
+			"with successful implicit keep");
+		return 1;
+	}
+
+	return imap_sieve_handle_exec_status
+		(isrun, last_script, ret, keep, scriptenv->exec_status);
+}
+
+int imap_sieve_run_mail
+(struct imap_sieve_run *isrun, struct mail *mail,
+	const char *changed_flags)
+{
+	struct imap_sieve *isieve = isrun->isieve;
+	const struct lda_settings *lda_set = isieve->lda_set;
+	struct sieve_message_data msgdata;
+	struct sieve_script_env scriptenv;
+	struct sieve_exec_status estatus;
+	struct imap_sieve_context context;
+	int ret;
+
+	memset(&context, 0, sizeof(context));
+	context.event.mailbox = isrun->mailbox;
+	context.event.cause = isrun->cause;
+	context.event.changed_flags = changed_flags;
+	context.isieve = isieve;
+
+	T_BEGIN {
+		/* Collect necessary message data */
+
+		memset(&msgdata, 0, sizeof(msgdata));
+		msgdata.mail = mail;
+		msgdata.auth_user = isieve->user->username;
+		(void)mail_get_first_header
+			(msgdata.mail, "Message-ID", &msgdata.id);
+
+		/* Compose script execution environment */
+
+		memset(&scriptenv, 0, sizeof(scriptenv));
+		memset(&estatus, 0, sizeof(estatus));
+		scriptenv.default_mailbox = mailbox_get_vname(isrun->mailbox);
+		scriptenv.user = isieve->user;
+		scriptenv.postmaster_address = lda_set->postmaster_address;
+		scriptenv.smtp_start = imap_sieve_smtp_start;
+		scriptenv.smtp_add_rcpt = imap_sieve_smtp_add_rcpt;
+		scriptenv.smtp_send = imap_sieve_smtp_send;
+		scriptenv.smtp_finish = imap_sieve_smtp_finish;
+		scriptenv.duplicate_mark = imap_sieve_duplicate_mark;
+		scriptenv.duplicate_check = imap_sieve_duplicate_check;
+		scriptenv.duplicate_flush = imap_sieve_duplicate_flush;
+		scriptenv.exec_status = &estatus;
+		scriptenv.script_context = (void *)&context;
+
+		/* Execute script(s) */
+
+		ret = imap_sieve_run_scripts(isrun, &msgdata, &scriptenv);
+	} T_END;
+
+	return ret;
+}
diff --git a/src/plugins/imapsieve/imap-sieve.h b/src/plugins/imapsieve/imap-sieve.h
new file mode 100644
index 0000000000000000000000000000000000000000..c1057cb0528cd749eca6a938414780010cd4ded8
--- /dev/null
+++ b/src/plugins/imapsieve/imap-sieve.h
@@ -0,0 +1,63 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef __IMAP_SIEVE_H
+#define __IMAP_SIEVE_H
+
+struct lda_settings;
+
+/*
+ * IMAP event
+ */
+
+struct imap_sieve_event {
+	struct mailbox *mailbox;
+	const char *cause;
+	const char *changed_flags;
+};
+
+struct imap_sieve_context {
+	struct imap_sieve_event event;
+
+	struct imap_sieve *isieve;
+};
+
+static inline bool
+imap_sieve_event_cause_valid(const char *cause)
+{
+	return (strcasecmp(cause, "APPEND") == 0 ||
+		strcasecmp(cause, "COPY") == 0 ||
+		strcasecmp(cause, "FLAG") == 0);
+}
+
+/*
+ * IMAP Sieve
+ */
+
+struct imap_sieve;
+
+struct imap_sieve *imap_sieve_init(struct mail_user *user,
+	const struct lda_settings *lda_set);
+void imap_sieve_deinit(struct imap_sieve **_isieve);
+
+/*
+ * IMAP Sieve run
+ */
+
+struct imap_sieve_run;
+
+int imap_sieve_run_init(struct imap_sieve *isieve,
+	struct mailbox *mailbox, const char *cause,
+	const char *script_name,
+	const char *const *scripts_before,
+	const char *const *scripts_after,
+	struct imap_sieve_run **isrun_r)
+	ATTR_NULL(4, 5, 6);
+
+int imap_sieve_run_mail
+(struct imap_sieve_run *isrun, struct mail *mail,
+	const char *changed_flags);
+
+void imap_sieve_run_deinit(struct imap_sieve_run **_isrun);
+
+#endif
diff --git a/src/plugins/imapsieve/sieve-imapsieve-plugin.c b/src/plugins/imapsieve/sieve-imapsieve-plugin.c
new file mode 100644
index 0000000000000000000000000000000000000000..9547160f356a07211f987e58b9648486903746b4
--- /dev/null
+++ b/src/plugins/imapsieve/sieve-imapsieve-plugin.c
@@ -0,0 +1,56 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "sieve-common.h"
+#include "sieve-error.h"
+#include "sieve-extensions.h"
+
+#include "ext-imapsieve-common.h"
+
+#include "sieve-imapsieve-plugin.h"
+
+/*
+ * Sieve plugin interface
+ */
+
+const char *sieve_imapsieve_plugin_version = PIGEONHOLE_ABI_VERSION;
+
+void sieve_imapsieve_plugin_load
+(struct sieve_instance *svinst, void **context)
+{
+	const struct sieve_extension *ext;
+
+	ext = sieve_extension_register
+		(svinst, &imapsieve_extension_dummy, TRUE);
+
+	if ( svinst->debug ) {
+		sieve_sys_debug(svinst,
+			"Sieve imapsieve plugin for %s version %s loaded",
+			PIGEONHOLE_NAME, PIGEONHOLE_VERSION_FULL);
+	}
+
+	*context = (void *)ext;
+}
+
+void sieve_imapsieve_plugin_unload
+(struct sieve_instance *svinst ATTR_UNUSED, void *context)
+{
+	const struct sieve_extension *ext =
+		(const struct sieve_extension *)context;
+
+	sieve_extension_unregister(ext);
+}
+
+/*
+ * Module interface
+ */
+
+void sieve_imapsieve_plugin_init(void)
+{
+	/* Nothing */
+}
+
+void sieve_imapsieve_plugin_deinit(void)
+{
+	/* Nothing */
+}
diff --git a/src/plugins/imapsieve/sieve-imapsieve-plugin.h b/src/plugins/imapsieve/sieve-imapsieve-plugin.h
new file mode 100644
index 0000000000000000000000000000000000000000..971c38a2b0f30f8f7273dba5644ccf58a3d97726
--- /dev/null
+++ b/src/plugins/imapsieve/sieve-imapsieve-plugin.h
@@ -0,0 +1,23 @@
+/* Copyright (c) 2016 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef __SIEVE_IMAPSIEVE_PLUGIN_H
+#define __SIEVE_IMAPSIEVE_PLUGIN_H
+
+/*
+ * Plugin interface
+ */
+
+void sieve_imapsieve_plugin_load
+	(struct sieve_instance *svinst, void **context);
+void sieve_imapsieve_plugin_unload
+	(struct sieve_instance *svinst, void *context);
+
+/*
+ * Module interface
+ */
+
+void sieve_imapsieve_plugin_init(void);
+void sieve_imapsieve_plugin_deinit(void);
+
+#endif /* __SIEVE_IMAPSIEVE_PLUGIN_H */