diff --git a/Makefile.am b/Makefile.am
index 07f4f2d704898677182b6d0ea717371f23641f9a..6693f4649525cde8cc8f0dc75d4a56f3c6fba521 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -158,6 +158,7 @@ test_cases = \
 	tests/extensions/duplicate/errors.svtest \
 	tests/extensions/duplicate/execute.svtest \
 	tests/extensions/duplicate/execute-vnd.svtest \
+	tests/extensions/metadata/execute.svtest \
 	tests/extensions/vnd.dovecot/debug/execute.svtest \
 	tests/deprecated/notify/basic.svtest \
 	tests/deprecated/notify/mailto.svtest \
diff --git a/src/lib-sieve/plugins/metadata/tst-metadata.c b/src/lib-sieve/plugins/metadata/tst-metadata.c
index 7be3c70f4c3bf05a1ecfabc1951eaa541d7cece0..fe45fa9148762bbdec5ff48ea4d8b703149e9423 100644
--- a/src/lib-sieve/plugins/metadata/tst-metadata.c
+++ b/src/lib-sieve/plugins/metadata/tst-metadata.c
@@ -299,6 +299,8 @@ static int tst_metadata_get_annotation
 		*annotation_r = avalue.value;
 	}
 	(void)imap_metadata_transaction_commit(&imtrans, NULL, NULL);
+	if ( box != NULL )
+		mailbox_free(&box);
 	return status;
 }
 
diff --git a/src/testsuite/Makefile.am b/src/testsuite/Makefile.am
index 440979270ff2ff0dfc8d371d181b3e77f5159cb1..8bca4136a70bc55f296fb24c18067379533df929 100644
--- a/src/testsuite/Makefile.am
+++ b/src/testsuite/Makefile.am
@@ -24,7 +24,8 @@ commands = \
 	cmd-test-result.c \
 	cmd-test-message.c \
 	cmd-test-mailbox.c \
-	cmd-test-binary.c
+	cmd-test-binary.c \
+	cmd-test-imap-metadata.c
 
 tests = \
 	tst-test-script-compile.c \
diff --git a/src/testsuite/cmd-test-imap-metadata.c b/src/testsuite/cmd-test-imap-metadata.c
new file mode 100644
index 0000000000000000000000000000000000000000..0868c1dfff6885851591eb1761b10e19e73a37fd
--- /dev/null
+++ b/src/testsuite/cmd-test-imap-metadata.c
@@ -0,0 +1,176 @@
+/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "sieve-common.h"
+#include "sieve-commands.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-interpreter.h"
+#include "sieve-code.h"
+#include "sieve-binary.h"
+#include "sieve-dump.h"
+
+#include "testsuite-common.h"
+#include "testsuite-mailstore.h"
+
+/*
+ * Commands
+ */
+
+static bool cmd_test_imap_metadata_validate
+	(struct sieve_validator *valdtr, struct sieve_command *cmd);
+static bool cmd_test_imap_metadata_generate
+	(const struct sieve_codegen_env *cgenv, struct sieve_command *ctx);
+
+/* Test_mailbox_create command
+ *
+ * Syntax:
+ *   test_imap_metadata_set
+ *     <mailbox: string> <annotation: string> <value:string>
+ */
+
+const struct sieve_command_def cmd_test_imap_metadata_set = {
+	"test_imap_metadata_set",
+	SCT_COMMAND,
+	3, 0, FALSE, FALSE,
+	NULL, NULL,
+	cmd_test_imap_metadata_validate,
+	NULL,
+	cmd_test_imap_metadata_generate,
+	NULL
+};
+
+/*
+ * Operations
+ */
+
+static bool cmd_test_imap_metadata_operation_dump
+	(const struct sieve_dumptime_env *denv, sieve_size_t *address);
+static int cmd_test_imap_metadata_operation_execute
+	(const struct sieve_runtime_env *renv, sieve_size_t *address);
+
+/* Test_mailbox_create operation */
+
+const struct sieve_operation_def test_imap_metadata_set_operation = {
+	"TEST_IMAP_METADATA_SET",
+	&testsuite_extension,
+	TESTSUITE_OPERATION_TEST_IMAP_METADATA_SET,
+	cmd_test_imap_metadata_operation_dump,
+	cmd_test_imap_metadata_operation_execute
+};
+
+/*
+ * Validation
+ */
+
+static bool cmd_test_imap_metadata_validate
+(struct sieve_validator *valdtr, struct sieve_command *cmd)
+{
+	struct sieve_ast_argument *arg = cmd->first_positional;
+
+	if ( !sieve_validate_positional_argument
+		(valdtr, cmd, arg, "mailbox", 1, SAAT_STRING) )
+		return FALSE;
+
+	if (!sieve_validator_argument_activate(valdtr, cmd, arg, FALSE))
+		return FALSE;
+
+	arg = sieve_ast_argument_next(arg);
+
+	if ( !sieve_validate_positional_argument
+		(valdtr, cmd, arg, "annotation", 2, SAAT_STRING) )
+		return FALSE;
+
+	if (!sieve_validator_argument_activate(valdtr, cmd, arg, FALSE))
+		return FALSE;
+
+	arg = sieve_ast_argument_next(arg);
+
+	if ( !sieve_validate_positional_argument
+		(valdtr, cmd, arg, "value", 3, SAAT_STRING) )
+		return FALSE;
+
+	if (!sieve_validator_argument_activate(valdtr, cmd, arg, FALSE))
+		return FALSE;
+	return TRUE;
+}
+
+/*
+ * Code generation
+ */
+
+static bool cmd_test_imap_metadata_generate
+(const struct sieve_codegen_env *cgenv, struct sieve_command *cmd)
+{
+	/* Emit operation */
+	if ( sieve_command_is(cmd, cmd_test_imap_metadata_set) )
+		sieve_operation_emit
+			(cgenv->sblock, cmd->ext, &test_imap_metadata_set_operation);
+	else
+		i_unreached();
+
+ 	/* Generate arguments */
+	if ( !sieve_generate_arguments(cgenv, cmd, NULL) )
+		return FALSE;
+	return TRUE;
+}
+
+/*
+ * Code dump
+ */
+
+static bool cmd_test_imap_metadata_operation_dump
+(const struct sieve_dumptime_env *denv, sieve_size_t *address)
+{
+	sieve_code_dumpf(denv, "%s:", sieve_operation_mnemonic(denv->oprtn));
+
+	sieve_code_descend(denv);
+
+	return (sieve_opr_string_dump(denv, address, "mailbox") &&
+		sieve_opr_string_dump(denv, address, "annotation") &&
+		sieve_opr_string_dump(denv, address, "value"));
+}
+
+/*
+ * Intepretation
+ */
+
+static int cmd_test_imap_metadata_operation_execute
+(const struct sieve_runtime_env *renv, sieve_size_t *address)
+{
+	const struct sieve_operation *oprtn = renv->oprtn;
+	string_t *mailbox = NULL, *annotation = NULL, *value = NULL;
+	int ret;
+
+	/*
+	 * Read operands
+	 */
+
+	if ( (ret=sieve_opr_string_read
+		(renv, address, "mailbox", &mailbox)) <= 0 )
+		return ret;
+	if ( (ret=sieve_opr_string_read
+		(renv, address, "annotation", &annotation)) <= 0 )
+		return ret;
+	if ( (ret=sieve_opr_string_read
+		(renv, address, "value", &value)) <= 0 )
+		return ret;
+
+	/*
+	 * Perform operation
+	 */
+
+	if ( sieve_operation_is(oprtn, test_imap_metadata_set_operation) ) {
+		if ( sieve_runtime_trace_active(renv, SIEVE_TRLVL_COMMANDS) ) {
+			sieve_runtime_trace(renv, 0, "testsuite/test_imap_metadata_set command");
+			sieve_runtime_trace_descend(renv);
+			sieve_runtime_trace(renv, 0, "set annotation `%s'", str_c(mailbox));
+		}
+
+		if (testsuite_mailstore_set_imap_metadata
+			(str_c(mailbox), str_c(annotation), str_c(value)) < 0)
+			return SIEVE_EXEC_FAILURE;
+	}
+
+	return SIEVE_EXEC_OK;
+}
diff --git a/src/testsuite/ext-testsuite.c b/src/testsuite/ext-testsuite.c
index b8fd4171e971e45c9dea8c2128bff9fe54161c3a..431ffcb16b3ee77793234d8af9cbc390ecaaf5dc 100644
--- a/src/testsuite/ext-testsuite.c
+++ b/src/testsuite/ext-testsuite.c
@@ -74,7 +74,8 @@ const struct sieve_operation_def *testsuite_operations[] = {
 	&test_mailbox_create_operation,
 	&test_mailbox_delete_operation,
 	&test_binary_load_operation,
-	&test_binary_save_operation
+	&test_binary_save_operation,
+	&test_imap_metadata_set_operation
 };
 
 /*
@@ -134,6 +135,7 @@ static bool ext_testsuite_validator_load
 	sieve_validator_register_command(valdtr, ext, &cmd_test_mailbox_delete);
 	sieve_validator_register_command(valdtr, ext, &cmd_test_binary_load);
 	sieve_validator_register_command(valdtr, ext, &cmd_test_binary_save);
+	sieve_validator_register_command(valdtr, ext, &cmd_test_imap_metadata_set);
 
 	sieve_validator_register_command(valdtr, ext, &tst_test_script_compile);
 	sieve_validator_register_command(valdtr, ext, &tst_test_script_run);
diff --git a/src/testsuite/testsuite-common.h b/src/testsuite/testsuite-common.h
index 48c3ceee39efcb7a029cbc1f8ee8bca38f89c7c9..8850cdae11531dd149684a0604c71d55f5c62111 100644
--- a/src/testsuite/testsuite-common.h
+++ b/src/testsuite/testsuite-common.h
@@ -78,6 +78,7 @@ extern const struct sieve_command_def cmd_test_mailbox_create;
 extern const struct sieve_command_def cmd_test_mailbox_delete;
 extern const struct sieve_command_def cmd_test_binary_load;
 extern const struct sieve_command_def cmd_test_binary_save;
+extern const struct sieve_command_def cmd_test_imap_metadata_set;
 
 /*
  * Tests
@@ -117,6 +118,7 @@ enum testsuite_operation_code {
 	TESTSUITE_OPERATION_TEST_MAILBOX_DELETE,
 	TESTSUITE_OPERATION_TEST_BINARY_LOAD,
 	TESTSUITE_OPERATION_TEST_BINARY_SAVE,
+	TESTSUITE_OPERATION_TEST_IMAP_METADATA_SET
 };
 
 extern const struct sieve_operation_def test_operation;
@@ -141,6 +143,7 @@ extern const struct sieve_operation_def test_mailbox_create_operation;
 extern const struct sieve_operation_def test_mailbox_delete_operation;
 extern const struct sieve_operation_def test_binary_load_operation;
 extern const struct sieve_operation_def test_binary_save_operation;
+extern const struct sieve_operation_def test_imap_metadata_set_operation;
 
 /*
  * Operands
diff --git a/src/testsuite/testsuite-mailstore.c b/src/testsuite/testsuite-mailstore.c
index 7b6dd33a06810c957fdee91c5ad107eaf1f95580..a3dc2796124f8c4078d3c3a654870209cb7124f2 100644
--- a/src/testsuite/testsuite-mailstore.c
+++ b/src/testsuite/testsuite-mailstore.c
@@ -6,10 +6,13 @@
 #include "imem.h"
 #include "array.h"
 #include "strfuncs.h"
+#include "str-sanitize.h"
+#include "abspath.h"
 #include "unlink-directory.h"
 #include "env-util.h"
 #include "mail-namespace.h"
 #include "mail-storage.h"
+#include "imap-metadata.h"
 
 #include "sieve-common.h"
 #include "sieve-error.h"
@@ -34,7 +37,10 @@ static void testsuite_mailstore_close(void);
  * State
  */
 
-static char *testsuite_mailstore_tmp = NULL;
+static struct mail_user *testsuite_mailstore_user = NULL;
+
+static char *testsuite_mailstore_location = NULL;
+static char *testsuite_mailstore_attrs = NULL;
 
 static char *testsuite_mailstore_folder = NULL;
 static struct mailbox *testsuite_mailstore_box = NULL;
@@ -47,34 +53,81 @@ static struct mail *testsuite_mailstore_mail = NULL;
 
 void testsuite_mailstore_init(void)
 {
-	testsuite_mailstore_tmp = i_strconcat
-		(testsuite_tmp_dir_get(), "/mailstore", NULL);
-
-	if ( mkdir(testsuite_mailstore_tmp, 0700) < 0 ) {
+	struct mail_user *mail_user_dovecot, *mail_user;
+	struct mail_namespace *ns;
+	struct mail_namespace_settings *ns_set;
+	struct mail_storage_settings *mail_set;
+	const char *tmpdir, *error;
+
+	tmpdir = testsuite_tmp_dir_get();
+	testsuite_mailstore_location =
+		i_strconcat(tmpdir, "/mailstore", NULL);
+	testsuite_mailstore_attrs =
+		i_strconcat(tmpdir, "/mail-attrs.dict", NULL);
+
+	if ( mkdir(testsuite_mailstore_location, 0700) < 0 ) {
 		i_fatal("failed to create temporary directory '%s': %m.",
-			testsuite_mailstore_tmp);
+			testsuite_mailstore_location);
 	}
-
-	sieve_tool_init_mail_user
-		(sieve_tool, t_strconcat("maildir:", testsuite_mailstore_tmp, NULL));
+	
+	mail_user_dovecot = sieve_tool_get_mail_user(sieve_tool);
+	mail_user = mail_user_alloc("testsuite mail user",
+		mail_user_dovecot->set_info, mail_user_dovecot->unexpanded_set);
+	mail_user->autocreated = TRUE;
+	mail_user_set_home(mail_user, t_abspath(""));
+	if (mail_user_init(mail_user, &error) < 0)
+		i_fatal("Testsuite user initialization failed: %s", error);
+
+	ns_set = p_new(mail_user->pool, struct mail_namespace_settings, 1);
+	ns_set->location = testsuite_mailstore_location;
+	ns_set->separator = ".";
+
+	ns = mail_namespaces_init_empty(mail_user);
+	ns->flags |= NAMESPACE_FLAG_INBOX_USER;
+	ns->set = ns_set;
+	/* absolute paths are ok with raw storage */
+	mail_set = p_new(mail_user->pool, struct mail_storage_settings, 1);
+	*mail_set = *ns->mail_set;
+	mail_set->mail_location = p_strconcat
+		(mail_user->pool, "maildir:", testsuite_mailstore_location, NULL);
+	mail_set->mail_attribute_dict = p_strconcat
+		(mail_user->pool, "file:", testsuite_mailstore_attrs, NULL);
+	mail_set->mail_full_filesystem_access = TRUE;
+	ns->mail_set = mail_set;
+
+	if (mail_storage_create(ns, "maildir", 0, &error) < 0)
+		i_fatal("Couldn't create testsuite storage: %s", error);
+
+	testsuite_mailstore_user = mail_user;
 }
 
 void testsuite_mailstore_deinit(void)
 {
 	testsuite_mailstore_close();
 
-	if ( unlink_directory(testsuite_mailstore_tmp, TRUE) < 0 ) {
+	if ( unlink_directory(testsuite_mailstore_location, TRUE) < 0 ) {
 		i_warning("failed to remove temporary directory '%s': %m.",
-			testsuite_mailstore_tmp);
+			testsuite_mailstore_location);
 	}
 
-	i_free(testsuite_mailstore_tmp);
+	i_free(testsuite_mailstore_location);
+	i_free(testsuite_mailstore_attrs);
+	mail_user_unref(&testsuite_mailstore_user);
 }
 
 void testsuite_mailstore_reset(void)
 {
 }
 
+/*
+ * Mail user
+ */
+
+struct mail_user *testsuite_mailstore_get_user(void)
+{
+	return testsuite_mailstore_user;
+}
+
 /*
  * Mailbox Access
  */
@@ -82,7 +135,7 @@ void testsuite_mailstore_reset(void)
 bool testsuite_mailstore_mailbox_create
 (const struct sieve_runtime_env *renv ATTR_UNUSED, const char *folder)
 {
-	struct mail_user *mail_user = sieve_tool_get_mail_user(sieve_tool);
+	struct mail_user *mail_user = testsuite_mailstore_user;
 	struct mail_namespace *ns = mail_user->namespaces;
 	struct mailbox *box;
 
@@ -117,7 +170,7 @@ static struct mail *testsuite_mailstore_open(const char *folder)
 {
 	enum mailbox_flags flags =
 		MAILBOX_FLAG_SAVEONLY | MAILBOX_FLAG_POST_SESSION;
-	struct mail_user *mail_user = sieve_tool_get_mail_user(sieve_tool);
+	struct mail_user *mail_user = testsuite_mailstore_user;
 	struct mail_namespace *ns = mail_user->namespaces;
 	struct mailbox *box;
 	struct mailbox_transaction_context *t;
@@ -173,3 +226,59 @@ bool testsuite_mailstore_mail_index
 
 	return TRUE;
 }
+
+/*
+ * IMAP metadata
+ */
+
+int testsuite_mailstore_set_imap_metadata
+(const char *mailbox, const char *annotation, const char *value)
+{
+	struct imap_metadata_transaction *imtrans;
+	struct mail_attribute_value avalue;
+	struct mailbox *box;
+	enum mail_error error_code;
+	const char *error;
+	int ret;
+
+	if ( !imap_metadata_verify_entry_name(annotation, &error) ) {
+		sieve_sys_error(testsuite_sieve_instance,
+			"testsuite: imap metadata: "
+			"specified annotation name `%s' is invalid: %s",
+			str_sanitize(annotation, 256), error);
+		return -1;
+	}
+
+	if ( mailbox != NULL ) {
+		struct mail_namespace *ns;
+		ns = mail_namespace_find
+			(testsuite_mailstore_user->namespaces, mailbox);
+		box = mailbox_alloc(ns->list, mailbox, 0);
+		imtrans = imap_metadata_transaction_begin(box);
+	} else {
+		imtrans = imap_metadata_transaction_begin_server
+			(testsuite_mailstore_user);
+	}
+
+	memset(&avalue, 0, sizeof(avalue));
+	avalue.value = value;
+	if ((ret=imap_metadata_set(imtrans, annotation, &avalue)) < 0) {
+		error = imap_metadata_transaction_get_last_error
+			(imtrans, &error_code);
+		imap_metadata_transaction_rollback(&imtrans);
+	} else {
+		ret = imap_metadata_transaction_commit
+			(&imtrans, &error_code, &error);
+	}
+	if ( box != NULL )
+		mailbox_free(&box);
+
+	if ( ret < 0 ) {
+		sieve_sys_error(testsuite_sieve_instance,
+			"testsuite: imap metadata: "
+			"failed to assign annotation `%s': %s",
+			str_sanitize(annotation, 256), error);
+		return -1;
+	}
+	return 0;
+}
diff --git a/src/testsuite/testsuite-mailstore.h b/src/testsuite/testsuite-mailstore.h
index 1f2fa4051fe4dae09d3d1846564a3132a357db49..5bb5e2dd7af7cd113e2664095f3cf97d1669eb95 100644
--- a/src/testsuite/testsuite-mailstore.h
+++ b/src/testsuite/testsuite-mailstore.h
@@ -16,6 +16,11 @@ void testsuite_mailstore_init(void);
 void testsuite_mailstore_deinit(void);
 void testsuite_mailstore_reset(void);
 
+/*
+ * Mail user
+ */
+
+struct mail_user *testsuite_mailstore_get_user(void);
 
 /*
  * Mailbox Access
@@ -28,4 +33,11 @@ bool testsuite_mailstore_mail_index
 	(const struct sieve_runtime_env *renv, const char *folder,
 		unsigned int index);
 
+/*
+ * IMAP metadata
+ */
+
+int testsuite_mailstore_set_imap_metadata
+	(const char *mailbox, const char *annotation, const char *value);
+
 #endif /* __TESTSUITE_MAILSTORE */
diff --git a/src/testsuite/testsuite.c b/src/testsuite/testsuite.c
index d53eafbc80786476a2a27876545aec1cc6d9dddc..4ca2da16833e1c278aa976c0d2476121bbe63f05 100644
--- a/src/testsuite/testsuite.c
+++ b/src/testsuite/testsuite.c
@@ -176,7 +176,7 @@ int main(int argc, char **argv)
 		testsuite_message_init();
 
 		memset(&scriptenv, 0, sizeof(scriptenv));
-		scriptenv.user = sieve_tool_get_mail_user(sieve_tool);
+		scriptenv.user = testsuite_mailstore_get_user();
 		scriptenv.default_mailbox = "INBOX";
 		scriptenv.postmaster_address = "postmaster@example.com";
 		scriptenv.smtp_start = testsuite_smtp_start;
diff --git a/tests/extensions/metadata/execute.svtest b/tests/extensions/metadata/execute.svtest
new file mode 100644
index 0000000000000000000000000000000000000000..799a730c4f18d7aff52293a249e7d3006f63b3ea
--- /dev/null
+++ b/tests/extensions/metadata/execute.svtest
@@ -0,0 +1,58 @@
+require "vnd.dovecot.testsuite";
+require "mboxmetadata";
+require "fileinto";
+
+test "MetadataExists - None exist" {
+	if metadataexists "INBOX" "/private/frop" {
+		test_fail "metadataexists confirms existence of unknown annotation";
+	}
+}
+
+test_imap_metadata_set "INBOX" "/private/frop" "FROP!";
+test_imap_metadata_set "INBOX" "/private/friep" "FRIEP!";
+
+test "MetadataExists - Not all exist" {
+	if metadataexists "INBOX"
+		["/private/frop", "/private/friep", "/private/frml"] {
+		test_fail "metadataexists confirms existence of unknown annotation";
+	}
+}
+
+test_imap_metadata_set "INBOX" "/private/friep" "FRIEP!";
+test_imap_metadata_set "INBOX" "/private/frml" "FRML!";
+
+test "MetadataExists - One exists" {
+	if not metadataexists "INBOX" ["/private/frop"] {
+		test_fail "metadataexists fails to recognize annotation";
+	}
+}
+
+test "MetadataExists - All exist" {
+	if not metadataexists "INBOX"
+		["/private/frop", "/private/friep", "/private/frml"] {
+		test_fail "metadataexists fails to recognize annotations";
+	}
+}
+
+test "Metadata" {
+	if not metadata :is "INBOX" "/private/frop" "FROP!" {
+		test_fail "invalid metadata value for /private/frop";
+	}
+	if metadata :is "INBOX" "/private/frop" "Hutsefluts" {
+		test_fail "unexpected match for /private/frop";
+	}
+
+	if not metadata :is "INBOX" "/private/friep" "FRIEP!" {
+		test_fail "invalid metadata value for /private/friep";
+	}
+	if metadata :is "INBOX" "/private/friep" "Hutsefluts" {
+		test_fail "unexpected match for /private/friep";
+	}
+
+	if not metadata :is "INBOX" "/private/frml" "FRML!" {
+		test_fail "invalid metadata value for /private/frml";
+	}
+	if metadata :is "INBOX" "/private/frml" "Hutsefluts" {
+		test_fail "unexpected match for /private/frml";
+	}
+}