From 9a193393de7512ee252c8cb820a9e2be9971b748 Mon Sep 17 00:00:00 2001
From: Stephan Bosch <stephan@rename-it.nl>
Date: Wed, 12 Nov 2014 22:10:25 +0100
Subject: [PATCH] lib-sieve: Implemented the Sieve index extension (RFC 5260).

---
 Makefile.am                                   |   2 +
 configure.ac                                  |   1 +
 src/lib-sieve/Makefile.am                     |   1 +
 src/lib-sieve/plugins/Makefile.am             |   1 +
 src/lib-sieve/plugins/index/Makefile.am       |  13 +
 .../plugins/index/ext-index-common.c          |  15 +
 .../plugins/index/ext-index-common.h          |  32 +++
 src/lib-sieve/plugins/index/ext-index.c       |  69 +++++
 src/lib-sieve/plugins/index/tag-index.c       | 272 ++++++++++++++++++
 src/lib-sieve/sieve-extensions.c              |   4 +-
 tests/extensions/index/basic.svtest           |  93 ++++++
 tests/extensions/index/errors.svtest          |  20 ++
 tests/extensions/index/errors/syntax.sieve    |  20 ++
 13 files changed, 542 insertions(+), 1 deletion(-)
 create mode 100644 src/lib-sieve/plugins/index/Makefile.am
 create mode 100644 src/lib-sieve/plugins/index/ext-index-common.c
 create mode 100644 src/lib-sieve/plugins/index/ext-index-common.h
 create mode 100644 src/lib-sieve/plugins/index/ext-index.c
 create mode 100644 src/lib-sieve/plugins/index/tag-index.c
 create mode 100644 tests/extensions/index/basic.svtest
 create mode 100644 tests/extensions/index/errors.svtest
 create mode 100644 tests/extensions/index/errors/syntax.sieve

diff --git a/Makefile.am b/Makefile.am
index 87b6fded9..07f4f2d70 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -140,6 +140,8 @@ test_cases = \
 	tests/extensions/date/basic.svtest \
 	tests/extensions/date/date-parts.svtest \
 	tests/extensions/date/zones.svtest \
+	tests/extensions/index/basic.svtest \
+	tests/extensions/index/errors.svtest \
 	tests/extensions/spamvirustest/spamtest.svtest \
 	tests/extensions/spamvirustest/virustest.svtest \
 	tests/extensions/spamvirustest/spamtestplus.svtest \
diff --git a/configure.ac b/configure.ac
index a1e5cbe5c..0ccaa9403 100644
--- a/configure.ac
+++ b/configure.ac
@@ -203,6 +203,7 @@ src/lib-sieve/plugins/ihave/Makefile
 src/lib-sieve/plugins/editheader/Makefile
 src/lib-sieve/plugins/metadata/Makefile
 src/lib-sieve/plugins/duplicate/Makefile
+src/lib-sieve/plugins/index/Makefile
 src/lib-sieve/plugins/vnd.dovecot/Makefile
 src/lib-sieve/plugins/vnd.dovecot/debug/Makefile
 src/lib-sieve-tool/Makefile
diff --git a/src/lib-sieve/Makefile.am b/src/lib-sieve/Makefile.am
index aeaaa2b47..143287bbc 100644
--- a/src/lib-sieve/Makefile.am
+++ b/src/lib-sieve/Makefile.am
@@ -76,6 +76,7 @@ plugins = \
 	$(extdir)/ihave/libsieve_ext_ihave.la \
 	$(extdir)/editheader/libsieve_ext_editheader.la \
 	$(extdir)/duplicate/libsieve_ext_duplicate.la \
+	$(extdir)/index/libsieve_ext_index.la \
 	$(extdir)/vnd.dovecot/debug/libsieve_ext_debug.la \
 	$(unfinished_plugins)
 
diff --git a/src/lib-sieve/plugins/Makefile.am b/src/lib-sieve/plugins/Makefile.am
index 915ada271..ee69303f4 100644
--- a/src/lib-sieve/plugins/Makefile.am
+++ b/src/lib-sieve/plugins/Makefile.am
@@ -22,6 +22,7 @@ SUBDIRS = \
 	ihave \
 	editheader \
 	duplicate \
+	index \
 	vnd.dovecot \
 	$(UNFINISHED)
 
diff --git a/src/lib-sieve/plugins/index/Makefile.am b/src/lib-sieve/plugins/index/Makefile.am
new file mode 100644
index 000000000..434ca1c06
--- /dev/null
+++ b/src/lib-sieve/plugins/index/Makefile.am
@@ -0,0 +1,13 @@
+noinst_LTLIBRARIES = libsieve_ext_index.la
+
+AM_CPPFLAGS = \
+	-I$(srcdir)/../.. \
+	$(LIBDOVECOT_INCLUDE)
+
+libsieve_ext_index_la_SOURCES = \
+	ext-index-common.c \
+	ext-index.c \
+	tag-index.c
+
+noinst_HEADERS = \
+	ext-index-common.h
diff --git a/src/lib-sieve/plugins/index/ext-index-common.c b/src/lib-sieve/plugins/index/ext-index-common.c
new file mode 100644
index 000000000..53eb2554a
--- /dev/null
+++ b/src/lib-sieve/plugins/index/ext-index-common.c
@@ -0,0 +1,15 @@
+/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "utc-offset.h"
+#include "str.h"
+
+#include "sieve-common.h"
+#include "sieve-stringlist.h"
+#include "sieve-code.h"
+#include "sieve-interpreter.h"
+#include "sieve-message.h"
+
+#include "ext-index-common.h"
+
diff --git a/src/lib-sieve/plugins/index/ext-index-common.h b/src/lib-sieve/plugins/index/ext-index-common.h
new file mode 100644
index 000000000..ca4f484e5
--- /dev/null
+++ b/src/lib-sieve/plugins/index/ext-index-common.h
@@ -0,0 +1,32 @@
+/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
+ */
+
+#ifndef __EXT_INDEX_COMMON_H
+#define __EXT_INDEX_COMMON_H
+
+#include "sieve-common.h"
+
+#include <time.h>
+
+#define SIEVE_EXT_INDEX_HDR_OVERRIDE_SEQUENCE 100
+
+/*
+ * Tagged arguments
+ */
+
+extern const struct sieve_argument_def index_tag;
+extern const struct sieve_argument_def last_tag;
+
+/*
+ * Operands
+ */
+
+extern const struct sieve_operand_def index_operand;
+
+/*
+ * Extension
+ */
+
+extern const struct sieve_extension_def index_extension;
+
+#endif /* __EXT_INDEX_COMMON_H */
diff --git a/src/lib-sieve/plugins/index/ext-index.c b/src/lib-sieve/plugins/index/ext-index.c
new file mode 100644
index 000000000..c22253564
--- /dev/null
+++ b/src/lib-sieve/plugins/index/ext-index.c
@@ -0,0 +1,69 @@
+/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
+ */
+
+/* Extension index
+ * ------------------
+ *
+ * Authors: Stephan Bosch
+ * Specification: RFC 5260
+ * Implementation: full
+ * Status: testing
+ *
+ */
+
+#include "lib.h"
+#include "array.h"
+
+#include "sieve-common.h"
+#include "sieve-message.h"
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-binary.h"
+#include "sieve-interpreter.h"
+#include "sieve-dump.h"
+
+#include "ext-index-common.h"
+
+/*
+ * Extension
+ */
+
+static bool ext_index_validator_load
+(const struct sieve_extension *ext, struct sieve_validator *validator);
+
+const struct sieve_extension_def index_extension = {
+	.name = "index",
+	.validator_load = ext_index_validator_load,
+	SIEVE_EXT_DEFINE_OPERAND(index_operand)
+};
+
+static bool ext_index_validator_load
+(const struct sieve_extension *ext, struct sieve_validator *valdtr)
+{
+	/* Register :index and :last tags with header, address and date test commands
+	 * and we don't care whether these command are registered or even whether
+	 * these will be registered at all. The validator handles either situation
+	 * gracefully.
+	 */
+	sieve_validator_register_external_tag
+		(valdtr, "header", ext, &index_tag, SIEVE_OPT_MESSAGE_OVERRIDE);
+	sieve_validator_register_external_tag
+		(valdtr, "header", ext, &last_tag, 0);
+
+	sieve_validator_register_external_tag
+		(valdtr, "address", ext, &index_tag, SIEVE_OPT_MESSAGE_OVERRIDE);
+	sieve_validator_register_external_tag
+		(valdtr, "address", ext, &last_tag, 0);
+
+	sieve_validator_register_external_tag
+		(valdtr, "date", ext, &index_tag, SIEVE_OPT_MESSAGE_OVERRIDE);
+	sieve_validator_register_external_tag
+		(valdtr, "date", ext, &last_tag, 0);
+
+	return TRUE;
+}
+
+
diff --git a/src/lib-sieve/plugins/index/tag-index.c b/src/lib-sieve/plugins/index/tag-index.c
new file mode 100644
index 000000000..56657a403
--- /dev/null
+++ b/src/lib-sieve/plugins/index/tag-index.c
@@ -0,0 +1,272 @@
+/* Copyright (c) 2002-2014 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "mail-storage.h"
+#include "mail-namespace.h"
+
+#include "sieve-common.h"
+#include "sieve-stringlist.h"
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+#include "sieve-binary.h"
+#include "sieve-code.h"
+#include "sieve-message.h"
+#include "sieve-result.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+
+#include "ext-index-common.h"
+
+/*
+ * Tagged argument
+ */
+
+static bool tag_index_validate
+	(struct sieve_validator *valdtr, struct sieve_ast_argument **arg,
+		struct sieve_command *cmd);
+static bool tag_index_generate
+	(const struct sieve_codegen_env *cgenv, struct sieve_ast_argument *arg,
+    struct sieve_command *context);
+
+const struct sieve_argument_def index_tag = {
+	"index",
+	NULL,
+	tag_index_validate,
+	NULL, NULL,
+	tag_index_generate
+};
+
+static bool tag_last_validate
+	(struct sieve_validator *valdtr, struct sieve_ast_argument **arg,
+		struct sieve_command *cmd);
+
+const struct sieve_argument_def last_tag = {
+	"last",
+	NULL,
+	tag_last_validate,
+	NULL, NULL, NULL
+};
+
+
+/*
+ * Header override
+ */
+
+static bool svmo_index_dump_context
+	(const struct sieve_message_override *svmo,
+		const struct sieve_dumptime_env *denv, sieve_size_t *address);
+static int svmo_index_read_context
+	(const struct sieve_message_override *svmo,
+		const struct sieve_runtime_env *renv, sieve_size_t *address,
+		void **ho_context);
+static int svmo_index_header_override
+	(const struct sieve_message_override *svmo,
+		const struct sieve_runtime_env *renv,
+		struct sieve_stringlist **headers);
+
+const struct sieve_message_override_def index_header_override = {
+	SIEVE_OBJECT("index", &index_operand, 0),
+	.sequence = SIEVE_EXT_INDEX_HDR_OVERRIDE_SEQUENCE,
+	.dump_context = svmo_index_dump_context,
+	.read_context = svmo_index_read_context,
+	.header_override = svmo_index_header_override
+};
+
+/*
+ * Operand
+ */
+
+static const struct sieve_extension_objects ext_header_overrides =
+	SIEVE_EXT_DEFINE_MESSAGE_OVERRIDE(index_header_override);
+
+const struct sieve_operand_def index_operand = {
+	"index operand",
+	&index_extension,
+	0,
+	&sieve_message_override_operand_class,
+	&ext_header_overrides
+};
+
+/*
+ * Tag data
+ */
+
+struct tag_index_data {
+	sieve_number_t fieldno;
+	unsigned int last:1;
+};
+
+/*
+ * Tag validation
+ */
+
+static bool tag_index_validate
+(struct sieve_validator *valdtr ATTR_UNUSED,
+	struct sieve_ast_argument **arg, struct sieve_command *cmd)
+{
+	struct sieve_ast_argument *tag = *arg;
+	struct tag_index_data *data;
+
+	/* Skip the tag itself */
+	*arg = sieve_ast_argument_next(*arg);
+
+	/* Check syntax:
+	 *   ":index" <fieldno: number>
+	 */
+	if ( !sieve_validate_tag_parameter
+		(valdtr, cmd, tag, *arg, NULL, 0, SAAT_NUMBER, FALSE) ) {
+		return FALSE;
+	}
+
+	if (tag->argument->data == NULL) {
+		data = p_new(sieve_command_pool(cmd), struct tag_index_data, 1);
+		tag->argument->data = (void *)data;
+	} else {
+		data = (struct tag_index_data *)tag->argument->data;
+	}
+
+	data->fieldno = sieve_ast_argument_number(*arg);
+
+	/* Detach parameter */
+	*arg = sieve_ast_arguments_detach(*arg,1);
+	return TRUE;
+}
+
+static bool tag_last_validate
+(struct sieve_validator *valdtr ATTR_UNUSED,
+	struct sieve_ast_argument **arg, struct sieve_command *cmd)
+{
+	struct sieve_ast_argument *index_arg;
+	struct tag_index_data *data;
+
+	index_arg = sieve_command_find_argument(cmd, &index_tag);
+	if (index_arg == NULL) {
+		sieve_argument_validate_error(valdtr, *arg,
+			"the :last tag for the %s %s cannot be specified "
+			"without the :index tag",
+			sieve_command_identifier(cmd), sieve_command_type_name(cmd));
+		return FALSE;
+	}
+
+	/* Set :last flag */
+	if (index_arg->argument->data == NULL) {
+		data = p_new(sieve_command_pool(cmd), struct tag_index_data, 1);
+		index_arg->argument->data = (void*)data;
+	} else {
+		data = (struct tag_index_data *)index_arg->argument->data;
+	}
+	data->last = TRUE;
+
+	/* Detach */
+	*arg = sieve_ast_arguments_detach(*arg,1);
+	return TRUE;
+}
+
+/*
+ * Code generation
+ */
+
+static bool tag_index_generate
+(const struct sieve_codegen_env *cgenv, struct sieve_ast_argument *arg,
+	struct sieve_command *cmd ATTR_UNUSED)
+{
+	struct tag_index_data *data =
+		(struct tag_index_data *)arg->argument->data;
+
+	if ( sieve_ast_argument_type(arg) != SAAT_TAG )
+		return FALSE;
+
+	sieve_opr_message_override_emit
+		(cgenv->sblock, arg->argument->ext, &index_header_override);
+
+	(void)sieve_binary_emit_integer
+		(cgenv->sblock, data->fieldno);
+	(void)sieve_binary_emit_byte
+		(cgenv->sblock, ( data->last ? 1 : 0 ));
+
+	return TRUE;
+}
+
+/*
+ * Header override implementation
+ */
+
+/* Context data */
+
+struct svmo_index_context {
+	unsigned int fieldno;
+	unsigned int last:1;
+};
+
+/* Context coding */
+
+static bool svmo_index_dump_context
+(const struct sieve_message_override *svmo ATTR_UNUSED,
+	const struct sieve_dumptime_env *denv, sieve_size_t *address)
+{
+	sieve_number_t fieldno = 0;
+	unsigned int last;
+
+	if ( !sieve_binary_read_integer(denv->sblock, address, &fieldno) )
+		return FALSE;
+
+	sieve_code_dumpf(denv, "fieldno: %llu",
+		(unsigned long long) fieldno);
+
+	if ( !sieve_binary_read_byte(denv->sblock, address, &last) )
+		return FALSE;
+
+	if (last)
+		sieve_code_dumpf(denv, "last");
+	return TRUE;
+}
+
+static int svmo_index_read_context
+(const struct sieve_message_override *svmo ATTR_UNUSED,
+	const struct sieve_runtime_env *renv, sieve_size_t *address,
+	void **ho_context)
+{
+	pool_t pool = sieve_result_pool(renv->result);
+	struct svmo_index_context *ctx;
+	sieve_number_t fieldno;
+	unsigned int last = 0;
+
+	if ( !sieve_binary_read_integer(renv->sblock, address, &fieldno) ) {
+		sieve_runtime_trace_error(renv, "fieldno: invalid number");
+		return SIEVE_EXEC_BIN_CORRUPT;
+	}
+
+	if ( !sieve_binary_read_byte(renv->sblock, address, &last) ) {
+		sieve_runtime_trace_error(renv, "last: invalid byte");
+		return SIEVE_EXEC_BIN_CORRUPT;
+	}
+
+	ctx = p_new(pool, struct svmo_index_context, 1);
+	ctx->fieldno = fieldno;
+	ctx->last = (last == 0 ? FALSE : TRUE);
+
+	*ho_context = (void *) ctx;
+
+	return SIEVE_EXEC_OK;
+}
+
+/* Override */
+
+static int svmo_index_header_override
+(const struct sieve_message_override *svmo,
+	const struct sieve_runtime_env *renv,
+	struct sieve_stringlist **headers)
+{
+	struct svmo_index_context *ctx =
+		(struct svmo_index_context *)svmo->context;
+
+	sieve_runtime_trace(renv, SIEVE_TRLVL_MATCHING,
+		"header index override: only returning index %d%s",
+		ctx->fieldno, ( ctx->last ? " (from last)" : "" ));
+
+	*headers = sieve_index_stringlist_create(renv, *headers,
+		(int)ctx->fieldno * ( ctx->last ? -1 : 1 ));
+	return SIEVE_EXEC_OK;
+}
+
diff --git a/src/lib-sieve/sieve-extensions.c b/src/lib-sieve/sieve-extensions.c
index 03183c3fb..ff887afa4 100644
--- a/src/lib-sieve/sieve-extensions.c
+++ b/src/lib-sieve/sieve-extensions.c
@@ -103,6 +103,7 @@ extern const struct sieve_extension_def enotify_extension;
 extern const struct sieve_extension_def environment_extension;
 extern const struct sieve_extension_def mailbox_extension;
 extern const struct sieve_extension_def date_extension;
+extern const struct sieve_extension_def index_extension;
 extern const struct sieve_extension_def ihave_extension;
 extern const struct sieve_extension_def duplicate_extension;
 
@@ -117,7 +118,8 @@ const struct sieve_extension_def *sieve_core_extensions[] = {
 	&relational_extension, &regex_extension, &imap4flags_extension,
 	&copy_extension, &include_extension, &body_extension,
 	&variables_extension, &enotify_extension, &environment_extension,
-	&mailbox_extension, &date_extension, &ihave_extension, &duplicate_extension
+	&mailbox_extension, &date_extension, &index_extension, &ihave_extension,
+	&duplicate_extension
 };
 
 const unsigned int sieve_core_extensions_count =
diff --git a/tests/extensions/index/basic.svtest b/tests/extensions/index/basic.svtest
new file mode 100644
index 000000000..070602251
--- /dev/null
+++ b/tests/extensions/index/basic.svtest
@@ -0,0 +1,93 @@
+require "vnd.dovecot.testsuite";
+require "index";
+require "date";
+require "variables";
+require "subaddress";
+
+test_set "message" text:
+To: first@friep.example.com
+X-A: First
+Received: from mx.example.com (127.0.0.13) by mx.example.org
+ (127.0.0.12) with Macrosoft SMTP Server (TLS) id 1.2.3.4;
+ Wed, 12 Nov 2014 18:18:31 +0100
+To: second@friep.example.com
+From: stephan@example.org
+Received: from mx.example.com (127.0.0.13) by mx.example.org
+ (127.0.0.12) with Macrosoft SMTP Server (TLS) id 1.2.3.4;
+ Wed, 12 Nov 2014 18:18:30 +0100
+X-A: Second
+To: third@friep.example.com
+X-A: Third
+Received: from mx.example.com (127.0.0.13) by mx.example.org
+ (127.0.0.12) with Macrosoft SMTP Server (TLS) id 1.2.3.4;
+ Wed, 12 Nov 2014 18:18:29 +0100
+Subject: Frop!
+X-A: Fourth
+To: fourth@friep.example.com
+Received: from mx.example.com (127.0.0.13) by mx.example.org
+ (127.0.0.12) with Macrosoft SMTP Server (TLS) id 1.2.3.4;
+ Wed, 12 Nov 2014 18:18:28 +0100
+
+Frop
+.
+;
+
+test "Header :index" {
+	if not header :index 3 "x-a" "Third" {
+		test_fail "wrong header retrieved";
+	}
+
+	if header :index 3 "x-a" ["First", "Second", "Fourth"] {
+		test_fail "other header retrieved";
+	}
+}
+
+test "Header :index :last" {
+	if not header :index 3 :last "x-a" "Second" {
+		test_fail "wrong header retrieved";
+	}
+
+	if header :index 3 :last "x-a" ["First", "Third", "Fourth"] {
+		test_fail "other header retrieved";
+	}
+}
+
+test "Address :index" {
+	if not address :localpart :index 2 "to" "second" {
+		test_fail "wrong header retrieved";
+	}
+
+	if address :localpart :index 2 "to" ["first", "third", "fourth"] {
+		test_fail "other header retrieved";
+	}
+}
+
+test "Address :index :last" {
+	if not address :localpart :index 2 :last "to" "third" {
+		test_fail "wrong header retrieved";
+	}
+
+	if address :localpart :index 2 :last "to" ["first", "second", "fourth"] {
+		test_fail "other header retrieved";
+	}
+}
+
+test "Date :index" {
+	if not date :index 1 "received" "second" "31" {
+		test_fail "wrong header retrieved";
+	}
+
+	if date :index 1 "received" "second" ["30", "29", "28"] {
+		test_fail "other header retrieved";
+	}
+}
+
+test "Date :index :last" {
+	if not date :index 1 :last "received" "second" "28"{
+		test_fail "wrong header retrieved";
+	}
+
+	if date :index 1 :last "received" "second" ["31", "30", "29"] {
+		test_fail "other header retrieved";
+	}
+}
diff --git a/tests/extensions/index/errors.svtest b/tests/extensions/index/errors.svtest
new file mode 100644
index 000000000..b975fd4f6
--- /dev/null
+++ b/tests/extensions/index/errors.svtest
@@ -0,0 +1,20 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Invalid syntax
+ */
+
+test "Invalid Syntax" {
+        if test_script_compile "errors/syntax.sieve" {
+                test_fail "compile should have failed";
+        }
+
+        if not test_error :count "eq" :comparator "i;ascii-numeric" "5" {
+                test_fail "wrong number of errors reported";
+        }
+}
+
+
diff --git a/tests/extensions/index/errors/syntax.sieve b/tests/extensions/index/errors/syntax.sieve
new file mode 100644
index 000000000..afa31e3cb
--- /dev/null
+++ b/tests/extensions/index/errors/syntax.sieve
@@ -0,0 +1,20 @@
+require "date";
+require "index";
+
+# Not an error
+if header :last :index 2 "to" "ok" { }
+
+# Not an error
+if header :index 444 :last "to" "ok" { }
+
+# 1: missing argument
+if header :index "to" "ok" {}
+
+# 2: missing argument
+if header :index :last "to" "ok" {}
+
+# 3: erroneous string argument
+if header :index "frop" "to" "ok" {}
+
+# 4: last without index
+if header :last "to" "ok" {}
-- 
GitLab