diff --git a/src/lib-sieve/sieve-code.c b/src/lib-sieve/sieve-code.c
index b4b74d9640d5df39690a550cab2cd80204a34f34..3d0f6d4ab1aba98bf02815da456c38602c8b3343 100644
--- a/src/lib-sieve/sieve-code.c
+++ b/src/lib-sieve/sieve-code.c
@@ -492,13 +492,17 @@ bool sieve_opr_string_dump_data
 {
 	const struct sieve_opr_string_interface *intf;
 	
-	if ( !sieve_operand_is_string(operand) ) 
+	if ( !sieve_operand_is_string(operand) ) {
+		sieve_code_dumpf(denv, "ERROR: INVALID STRING OPERAND %s", operand->name);
 		return FALSE;
+	}
 		
 	intf = (const struct sieve_opr_string_interface *) operand->interface; 
 	
-	if ( intf->dump == NULL ) 
+	if ( intf->dump == NULL ) {
+		sieve_code_dumpf(denv, "ERROR: DUMP STRING OPERAND");
 		return FALSE;
+	}
 
 	return intf->dump(denv, address, field_name);  
 }
@@ -511,6 +515,11 @@ bool sieve_opr_string_dump
 	
 	sieve_code_mark(denv);
 	operand = sieve_operand_read(denv->sbin, address);
+	
+	if ( operand == NULL ) {
+		sieve_code_dumpf(denv, "ERROR: INVALID OPERAND");
+		return FALSE;
+	}
 
 	return sieve_opr_string_dump_data(denv, operand, address, field_name);
 }
@@ -790,7 +799,8 @@ static bool opr_catenated_string_dump
 
 	sieve_code_descend(denv);
 	for ( i = 0; i < (unsigned int) elements; i++ ) {
-		sieve_opr_string_dump(denv, address, NULL);
+		if ( !sieve_opr_string_dump(denv, address, NULL) )
+			return FALSE;
 	}
 	sieve_code_ascend(denv);
 	
diff --git a/src/testsuite/Makefile.am b/src/testsuite/Makefile.am
index 61ebb99c33ef188146abdd4a99a075b1421dd42c..976cb5676565d7b9cdb41f8ad522acdbfac26d5a 100644
--- a/src/testsuite/Makefile.am
+++ b/src/testsuite/Makefile.am
@@ -47,6 +47,8 @@ tests = \
 testsuite_SOURCES = \
 	testsuite-common.c \
 	testsuite-objects.c \
+	testsuite-substitutions.c \
+	testsuite-arguments.c \
 	testsuite-result.c \
 	$(commands) \
 	$(tests) \
@@ -56,5 +58,7 @@ testsuite_SOURCES = \
 noinst_HEADERS = \
 	testsuite-common.h \
 	testsuite-objects.h \
+	testsuite-substitutions.h \
+	testsuite-arguments.h \
 	testsuite-result.h
 
diff --git a/src/testsuite/ext-testsuite.c b/src/testsuite/ext-testsuite.c
index 47c20f037d5c599efe271e9d5d7f8b4a98dfc693..f62cb19478bf3dba21f6a8287ab57f4aa0117cd7 100644
--- a/src/testsuite/ext-testsuite.c
+++ b/src/testsuite/ext-testsuite.c
@@ -45,6 +45,7 @@
 #include "sieve-result.h"
 
 #include "testsuite-common.h"
+#include "testsuite-arguments.h"
 
 /* 
  * Operations 
@@ -65,8 +66,10 @@ const struct sieve_operation *testsuite_operations[] = {
  * Operands 
  */
 
-const struct sieve_operand *testsuite_operands[] =
-    { &testsuite_object_operand };
+const struct sieve_operand *testsuite_operands[] = { 
+	&testsuite_object_operand,
+	&testsuite_substitution_operand
+};
     
 /* 
  * Extension
@@ -92,7 +95,7 @@ const struct sieve_extension testsuite_extension = {
 	ext_testsuite_binary_load, 
 	NULL, NULL,
 	SIEVE_EXT_DEFINE_OPERATIONS(testsuite_operations),
-	SIEVE_EXT_DEFINE_OPERAND(testsuite_object_operand)
+	SIEVE_EXT_DEFINE_OPERANDS(testsuite_operands)
 };
 
 /* Extension implementation */
@@ -108,6 +111,9 @@ static bool ext_testsuite_validator_load(struct sieve_validator *valdtr)
 	sieve_validator_register_command(valdtr, &tst_test_error);
 	sieve_validator_register_command(valdtr, &tst_test_result);	
 
+	sieve_validator_argument_override(valdtr, SAT_VAR_STRING,
+		&testsuite_string_argument);
+
 	return testsuite_validator_context_initialize(valdtr);
 }
 
diff --git a/src/testsuite/testsuite-arguments.c b/src/testsuite/testsuite-arguments.c
new file mode 100644
index 0000000000000000000000000000000000000000..c86424babcf15d29232ddb23cd5667a8556ce874
--- /dev/null
+++ b/src/testsuite/testsuite-arguments.c
@@ -0,0 +1,192 @@
+/* Copyright (c) 2002-2008 Dovecot Sieve authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "array.h"
+
+#include "sieve-common.h"
+#include "sieve-ast.h"
+#include "sieve-commands.h"
+#include "sieve-code.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-dump.h"
+
+#include "testsuite-common.h"
+#include "testsuite-substitutions.h"
+#include "testsuite-arguments.h"
+
+#include <ctype.h>
+
+/* 
+ * Testsuite string argument 
+ */
+
+static bool arg_testsuite_string_validate
+	(struct sieve_validator *validator, struct sieve_ast_argument **arg, 
+		struct sieve_command_context *context);
+
+const struct sieve_argument testsuite_string_argument = { 
+	"@testsuite-string", 
+	NULL, NULL,
+	arg_testsuite_string_validate, 
+	NULL, 
+	sieve_arg_catenated_string_generate,
+};
+
+static bool arg_testsuite_string_validate
+(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, 
+	struct sieve_command_context *cmd)
+{
+	enum { ST_NONE, ST_OPEN, ST_SUBSTITUTION, ST_PARAM, ST_CLOSE } state = 
+		ST_NONE;
+	pool_t pool = sieve_ast_pool((*arg)->ast);
+	struct sieve_arg_catenated_string *catstr = NULL;
+	string_t *str = sieve_ast_argument_str(*arg);
+	const char *p, *strstart, *substart = NULL;
+	const char *strval = (const char *) str_data(str);
+	const char *strend = strval + str_len(str);
+	bool result = TRUE;
+	string_t *subs_name = t_str_new(256);
+	string_t *subs_param = t_str_new(256);
+	
+	T_BEGIN {
+		/* Initialize substitution structure */
+	
+		p = strval;
+		strstart = p;
+		while ( result && p < strend ) {
+			switch ( state ) {
+
+			/* Nothing found yet */
+			case ST_NONE:
+				if ( *p == '%' ) {
+					substart = p;
+					state = ST_OPEN;
+					str_truncate(subs_name, 0);
+					str_truncate(subs_param, 0);
+				}
+				p++;
+				break;
+
+			/* Got '%' */
+			case ST_OPEN:
+				if ( *p == '{' ) {
+					state = ST_SUBSTITUTION;
+					p++;
+				} else 
+					state = ST_NONE;
+				break;
+
+			/* Got '%{' */ 
+			case ST_SUBSTITUTION:
+				state = ST_PARAM;	
+
+				while ( *p != '}' && *p != ':' ) {
+					if ( !i_isalnum(*p) ) {
+						state = ST_NONE;
+						break;
+					}	
+					str_append_c(subs_name, *p);
+					p++;
+				}
+				break;
+				
+			/* Got '%{name' */
+			case ST_PARAM:
+				if ( *p == ':' ) {
+					p++;
+					while ( *p != '}' ) {
+						str_append_c(subs_param, *p);
+						p++;
+					}
+				}
+				state = ST_CLOSE;
+				break;
+
+			/* Finished parsing param, expecting '}' */
+			case ST_CLOSE:
+				if ( *p == '}' ) {				
+					struct sieve_ast_argument *strarg;
+				
+					/* We now know that the substitution is valid */	
+					
+					if ( catstr == NULL ) {
+						catstr = sieve_arg_catenated_string_create(*arg);
+					}
+				
+					/* Add the substring that is before the substitution to the 
+					 * variable-string AST.
+					 */
+					if ( substart > strstart ) {
+						string_t *newstr = str_new(pool, substart - strstart);
+						str_append_n(newstr, strstart, substart - strstart); 
+						
+						strarg = sieve_ast_argument_string_create_raw
+							((*arg)->ast, newstr, (*arg)->source_line);
+						sieve_arg_catenated_string_add_element(catstr, strarg);
+					
+						/* Give other substitution extensions a chance to do their work */
+						if ( !sieve_validator_argument_activate_super
+							(valdtr, cmd, strarg, FALSE) ) {
+							result = FALSE;
+							break;
+						}
+					}
+				
+					strarg = testsuite_substitution_argument_create
+						(valdtr, (*arg)->ast, (*arg)->source_line, str_c(subs_name), 
+							str_c(subs_param));
+					
+					if ( strarg != NULL )
+						sieve_arg_catenated_string_add_element(catstr, strarg);
+					else {
+						sieve_argument_validate_error(valdtr, *arg, 
+							"unknown testsuite substitution type '%s'", str_c(subs_name));
+					}
+
+					strstart = p + 1;
+					substart = strstart;
+
+					p++;	
+				}
+		
+				/* Finished, reset for the next substitution */	
+				state = ST_NONE;
+			}
+		}
+	} T_END;
+
+	/* Bail out early if substitution is invalid */
+	if ( !result ) return FALSE;
+	
+	/* Check whether any substitutions were found */
+	if ( catstr == NULL ) {
+		/* No substitutions in this string, pass it on to any other substution
+		 * extension.
+		 */
+		return sieve_validator_argument_activate_super(valdtr, cmd, *arg, TRUE);
+	}
+	
+	/* Add the final substring that comes after the last substitution to the 
+	 * variable-string AST.
+	 */
+	if ( strend > strstart ) {
+		struct sieve_ast_argument *strarg;
+		string_t *newstr = str_new(pool, strend - strstart);
+		str_append_n(newstr, strstart, strend - strstart); 
+
+		strarg = sieve_ast_argument_string_create_raw
+			((*arg)->ast, newstr, (*arg)->source_line);
+		sieve_arg_catenated_string_add_element(catstr, strarg);
+			
+		/* Give other substitution extensions a chance to do their work */	
+		if ( !sieve_validator_argument_activate_super
+			(valdtr, cmd, strarg, FALSE) )
+			return FALSE;
+	}	
+	
+	return TRUE;
+}
diff --git a/src/testsuite/testsuite-arguments.h b/src/testsuite/testsuite-arguments.h
new file mode 100644
index 0000000000000000000000000000000000000000..5c268f1dd598b542a27bcf73b688b2e5b9f7fcb6
--- /dev/null
+++ b/src/testsuite/testsuite-arguments.h
@@ -0,0 +1,6 @@
+#ifndef __TESTSUITE_ARGUMENTS_H
+#define __TESTSUITE_ARGUMENTS_H
+
+extern const struct sieve_argument testsuite_string_argument;
+
+#endif
diff --git a/src/testsuite/testsuite-common.h b/src/testsuite/testsuite-common.h
index 7e6300d75ed0979b93e5ff5def1109eb419225c6..a0c09e0e850df548ccbbe5f9a740b4d858d7ee34 100644
--- a/src/testsuite/testsuite-common.h
+++ b/src/testsuite/testsuite-common.h
@@ -94,9 +94,11 @@ extern const struct sieve_operation test_result_operation;
  */
 
 extern const struct sieve_operand testsuite_object_operand;
+extern const struct sieve_operand testsuite_substitution_operand;
 
 enum testsuite_operand_code {
-	TESTSUITE_OPERAND_OBJECT
+	TESTSUITE_OPERAND_OBJECT,
+	TESTSUITE_OPERAND_SUBSTITUTION
 };
 
 /* 
diff --git a/src/testsuite/testsuite-substitutions.c b/src/testsuite/testsuite-substitutions.c
new file mode 100644
index 0000000000000000000000000000000000000000..44a6ffd1c98e68119556439de201a5e1ee32303c
--- /dev/null
+++ b/src/testsuite/testsuite-substitutions.c
@@ -0,0 +1,274 @@
+#include "lib.h"
+
+#include "sieve.h"
+#include "sieve-code.h"
+#include "sieve-commands.h"
+#include "sieve-binary.h"
+#include "sieve-generator.h"
+#include "sieve-interpreter.h"
+#include "sieve-dump.h"
+
+#include "testsuite-common.h"
+#include "testsuite-substitutions.h"
+
+/*
+ * Forward declarations
+ */
+ 
+void testsuite_opr_substitution_emit
+	(struct sieve_binary *sbin, const struct testsuite_substitution *tsub,
+		const char *param);
+			
+/*
+ * Testsuite substitutions
+ */
+ 
+/* FIXME: make this extendible */
+
+enum {
+	TESTSUITE_SUBSTITUTION_FILE,
+	TESTSUITE_SUBSTITUTION_MAILBOX,
+	TESTSUITE_SUBSTITUTION_SMTPOUT
+};
+
+static const struct testsuite_substitution testsuite_file_substitution;
+static const struct testsuite_substitution testsuite_mailbox_substitution;
+static const struct testsuite_substitution testsuite_smtpout_substitution;
+
+static const struct testsuite_substitution *substitutions[] = {
+	&testsuite_file_substitution,
+	&testsuite_mailbox_substitution,
+	&testsuite_smtpout_substitution
+};
+
+static const unsigned int substitutions_count = N_ELEMENTS(substitutions);
+ 
+static inline const struct testsuite_substitution *testsuite_substitution_get
+(unsigned int code)
+{
+	if ( code > substitutions_count )
+		return NULL;
+	
+	return substitutions[code];
+}
+
+const struct testsuite_substitution *testsuite_substitution_find
+(const char *identifier)
+{
+	unsigned int i; 
+	
+	for ( i = 0; i < substitutions_count; i++ ) {
+		if ( strcasecmp(substitutions[i]->object.identifier, identifier) == 0 )
+			return substitutions[i];
+	}
+	
+	return NULL;
+}
+
+/*
+ * Substitution argument
+ */
+ 
+static bool arg_testsuite_substitution_generate
+	(const struct sieve_codegen_env *cgenv, struct sieve_ast_argument *arg, 
+		struct sieve_command_context *context);
+
+struct _testsuite_substitution_context {
+	const struct testsuite_substitution *tsub;
+	const char *param;
+};
+
+const struct sieve_argument testsuite_substitution_argument = { 
+	"@testsuite-substitution", 
+	NULL, NULL, NULL, NULL,
+	arg_testsuite_substitution_generate 
+};
+
+struct sieve_ast_argument *testsuite_substitution_argument_create
+(struct sieve_validator *validator ATTR_UNUSED, struct sieve_ast *ast, 
+	unsigned int source_line, const char *substitution, const char *param)
+{
+	const struct testsuite_substitution *tsub;
+	struct _testsuite_substitution_context *tsctx;
+	struct sieve_ast_argument *arg;
+	pool_t pool;
+	
+	tsub = testsuite_substitution_find(substitution);
+	if ( tsub == NULL ) 
+		return NULL;
+	
+	arg = sieve_ast_argument_create(ast, source_line);
+	arg->type = SAAT_STRING;
+	arg->argument = &testsuite_substitution_argument;
+
+	pool = sieve_ast_pool(ast);
+	tsctx = p_new(pool, struct _testsuite_substitution_context, 1);
+	tsctx->tsub = tsub;
+	tsctx->param = p_strdup(pool, param);
+	arg->context = (void *) tsctx;
+	
+	return arg;
+}
+
+static bool arg_testsuite_substitution_generate
+(const struct sieve_codegen_env *cgenv, struct sieve_ast_argument *arg, 
+	struct sieve_command_context *context ATTR_UNUSED)
+{
+	struct _testsuite_substitution_context *tsctx =  
+		(struct _testsuite_substitution_context *) arg->context;
+	
+	testsuite_opr_substitution_emit(cgenv->sbin, tsctx->tsub, tsctx->param);
+
+	return TRUE;
+}
+
+/*
+ * Substitution operand
+ */
+
+static bool opr_substitution_dump
+	(const struct sieve_dumptime_env *denv, sieve_size_t *address, 
+		const char *field_name);
+static bool opr_substitution_read_value
+	(const struct sieve_runtime_env *renv, sieve_size_t *address, string_t **str);
+	
+const struct sieve_opr_string_interface testsuite_substitution_interface = { 
+	opr_substitution_dump,
+	opr_substitution_read_value
+};
+		
+const struct sieve_operand testsuite_substitution_operand = { 
+	"test-substitution", 
+	&testsuite_extension, 
+	TESTSUITE_OPERAND_SUBSTITUTION,
+	&string_class,
+	&testsuite_substitution_interface
+};
+
+void testsuite_opr_substitution_emit
+(struct sieve_binary *sbin, const struct testsuite_substitution *tsub,
+	const char *param) 
+{
+	/* Default variable storage */
+	(void) sieve_operand_emit_code(sbin, &testsuite_substitution_operand);
+	(void) sieve_binary_emit_unsigned(sbin, tsub->object.code);
+	(void) sieve_binary_emit_cstring(sbin, param);
+}
+
+static bool opr_substitution_dump
+(const struct sieve_dumptime_env *denv, sieve_size_t *address,
+	const char *field_name) 
+{
+	unsigned int code = 0;
+	const struct testsuite_substitution *tsub;
+	string_t *param; 
+
+	if ( !sieve_binary_read_unsigned(denv->sbin, address, &code) )
+		return FALSE;
+		
+	tsub = testsuite_substitution_get(code);
+	if ( tsub == NULL )
+		return FALSE;	
+			
+	if ( !sieve_binary_read_string(denv->sbin, address, &param) )
+		return FALSE;
+	
+	if ( field_name != NULL ) 
+		sieve_code_dumpf(denv, "%s: TEST_SUBS %%{%s:%s}", 
+			field_name, tsub->object.identifier, str_c(param));
+	else
+		sieve_code_dumpf(denv, "TEST_SUBS %%{%s:%s}", 
+			tsub->object.identifier, str_c(param));
+	return TRUE;
+}
+
+static bool opr_substitution_read_value
+(const struct sieve_runtime_env *renv, sieve_size_t *address, string_t **str)
+{ 
+	const struct testsuite_substitution *tsub;
+	unsigned int code = 0;
+	string_t *param;
+	
+	if ( !sieve_binary_read_unsigned(renv->sbin, address, &code) )
+		return FALSE;
+		
+	tsub = testsuite_substitution_get(code);
+	if ( tsub == NULL )
+		return FALSE;	
+
+	/* Parameter str can be NULL if we are requested to only skip and not 
+	 * actually read the argument.
+	 */	
+	if ( str == NULL ) 
+		return sieve_binary_read_string(renv->sbin, address, NULL);
+	
+	if ( !sieve_binary_read_string(renv->sbin, address, &param) )
+		return FALSE;
+				
+	return tsub->get_value(str_c(param), str);
+}
+
+/*
+ * Testsuite substitution definitions
+ */
+ 
+static bool testsuite_file_substitution_get_value
+	(const char *param, string_t **result); 
+static bool testsuite_mailbox_substitution_get_value
+	(const char *param, string_t **result); 
+static bool testsuite_smtpout_substitution_get_value
+	(const char *param, string_t **result); 
+ 
+static const struct testsuite_substitution testsuite_file_substitution = {
+	SIEVE_OBJECT(
+		"file", 
+		&testsuite_substitution_operand, 
+		TESTSUITE_SUBSTITUTION_FILE
+	),
+	testsuite_file_substitution_get_value
+};
+
+static const struct testsuite_substitution testsuite_mailbox_substitution = {
+	SIEVE_OBJECT(
+		"mailbox", 
+		&testsuite_substitution_operand, 
+		TESTSUITE_SUBSTITUTION_MAILBOX
+	),
+	testsuite_mailbox_substitution_get_value
+};
+
+static const struct testsuite_substitution testsuite_smtpout_substitution = {
+	SIEVE_OBJECT(
+		"smtpout",
+		&testsuite_substitution_operand,
+		TESTSUITE_SUBSTITUTION_SMTPOUT
+	),
+	testsuite_smtpout_substitution_get_value
+};
+ 
+static bool testsuite_file_substitution_get_value
+	(const char *param, string_t **result)
+{
+	*result = t_str_new(256);
+
+	str_printfa(*result, "[FILE: %s]", param);
+	return TRUE;
+}
+
+static bool testsuite_mailbox_substitution_get_value
+	(const char *param, string_t **result)
+{
+	*result = t_str_new(256);
+
+	str_printfa(*result, "[MAILBOX: %s]", param);
+	return TRUE;
+}
+
+static bool testsuite_smtpout_substitution_get_value
+	(const char *param, string_t **result) 
+{
+	*result = t_str_new(256);
+
+	str_printfa(*result, "[SMTPOUT: %s]", param);
+	return TRUE;
+} 
diff --git a/src/testsuite/testsuite-substitutions.h b/src/testsuite/testsuite-substitutions.h
new file mode 100644
index 0000000000000000000000000000000000000000..9fd0718271ee821a8b9bcccdbb2705243869c9d8
--- /dev/null
+++ b/src/testsuite/testsuite-substitutions.h
@@ -0,0 +1,20 @@
+#ifndef __TESTSUITE_SUBSTITUTIONS_H
+#define __TESTSUITE_SUBSTITUTIONS_H
+
+#include "sieve-common.h"
+#include "sieve-objects.h"
+
+struct testsuite_substitution {
+	struct sieve_object object;
+	
+	bool (*get_value)(const char *param, string_t **result);
+};
+
+const struct testsuite_substitution *testsuite_substitution_find
+	(const char *identifier);
+
+struct sieve_ast_argument *testsuite_substitution_argument_create
+	(struct sieve_validator *validator, struct sieve_ast *ast, 
+		unsigned int source_line, const char *substitution, const char *param);
+
+#endif /* __TESTSUITE_SUBSTITUTIONS_H */
diff --git a/tests/testsuite.svtest b/tests/testsuite.svtest
index 5cf884ed1f6ee128db0891afd62a6f99d901120c..9e4823ccb9914b1f18984ff66a9f88cdf3995fcc 100644
--- a/tests/testsuite.svtest
+++ b/tests/testsuite.svtest
@@ -1,6 +1,8 @@
 require "vnd.dovecot.testsuite";
 require "envelope";
 
+/* Test message environment */
+
 test "Message Environment" {
 	test_set "message" text:
 From: sirius@rename-it.nl
@@ -31,6 +33,8 @@ Friep!
 	keep;
 }
 
+/* Test envelope environment */
+
 test "Envelope Environment" {
 	test_set "envelope.from" "stephan@hutsefluts.nl";
 
@@ -68,3 +72,9 @@ test "Envelope Environment" {
         test_fail "envelope.auth data not reset properly (2).";
     }
 }
+
+/* Test substitutions */
+
+test "Substitutions" {
+	test_fail "file substitution failed: %{file:frop}.";
+}