summaryrefslogtreecommitdiffstats
path: root/pigeonhole/tests/extensions
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /pigeonhole/tests/extensions
parentInitial commit. (diff)
downloaddovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.tar.xz
dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'pigeonhole/tests/extensions')
-rw-r--r--pigeonhole/tests/extensions/body/basic.svtest97
-rw-r--r--pigeonhole/tests/extensions/body/content.svtest332
-rw-r--r--pigeonhole/tests/extensions/body/errors.svtest19
-rw-r--r--pigeonhole/tests/extensions/body/errors/syntax.sieve38
-rw-r--r--pigeonhole/tests/extensions/body/match-values.svtest55
-rw-r--r--pigeonhole/tests/extensions/body/raw.svtest85
-rw-r--r--pigeonhole/tests/extensions/body/text.svtest225
-rw-r--r--pigeonhole/tests/extensions/date/basic.svtest73
-rw-r--r--pigeonhole/tests/extensions/date/date-parts.svtest120
-rw-r--r--pigeonhole/tests/extensions/date/zones.svtest76
-rw-r--r--pigeonhole/tests/extensions/duplicate/errors.svtest54
-rw-r--r--pigeonhole/tests/extensions/duplicate/errors/conflict-vnd.sieve4
-rw-r--r--pigeonhole/tests/extensions/duplicate/errors/conflict.sieve4
-rw-r--r--pigeonhole/tests/extensions/duplicate/errors/syntax-vnd.sieve19
-rw-r--r--pigeonhole/tests/extensions/duplicate/errors/syntax.sieve54
-rw-r--r--pigeonhole/tests/extensions/duplicate/execute-vnd.svtest20
-rw-r--r--pigeonhole/tests/extensions/duplicate/execute.svtest41
-rw-r--r--pigeonhole/tests/extensions/editheader/addheader.svtest833
-rw-r--r--pigeonhole/tests/extensions/editheader/alternating.svtest181
-rw-r--r--pigeonhole/tests/extensions/editheader/deleteheader.svtest1115
-rw-r--r--pigeonhole/tests/extensions/editheader/errors.svtest164
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/command-syntax.sieve42
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/field-name-runtime.sieve6
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/field-name.sieve19
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/field-value.sieve15
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/runtime-error.sieve6
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/size-limit-runtime.sieve46
-rw-r--r--pigeonhole/tests/extensions/editheader/errors/size-limit.sieve43
-rw-r--r--pigeonhole/tests/extensions/editheader/execute.svtest57
-rw-r--r--pigeonhole/tests/extensions/editheader/execute/multiscript-after.sieve4
-rw-r--r--pigeonhole/tests/extensions/editheader/execute/multiscript-before.sieve4
-rw-r--r--pigeonhole/tests/extensions/editheader/execute/multiscript-personal.sieve4
-rw-r--r--pigeonhole/tests/extensions/editheader/protected.svtest173
-rw-r--r--pigeonhole/tests/extensions/editheader/utf8.svtest97
-rw-r--r--pigeonhole/tests/extensions/encoded-character.svtest180
-rw-r--r--pigeonhole/tests/extensions/enotify/basic.svtest15
-rw-r--r--pigeonhole/tests/extensions/enotify/encodeurl.svtest359
-rw-r--r--pigeonhole/tests/extensions/enotify/errors.svtest45
-rw-r--r--pigeonhole/tests/extensions/enotify/errors/from-mailto.sieve7
-rw-r--r--pigeonhole/tests/extensions/enotify/errors/options.sieve18
-rw-r--r--pigeonhole/tests/extensions/enotify/errors/uri-mailto.sieve20
-rw-r--r--pigeonhole/tests/extensions/enotify/errors/uri.sieve5
-rw-r--r--pigeonhole/tests/extensions/enotify/execute.svtest99
-rw-r--r--pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex1.sieve26
-rw-r--r--pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex2.sieve22
-rw-r--r--pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex3.sieve31
-rw-r--r--pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex5.sieve11
-rw-r--r--pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex6.sieve5
-rw-r--r--pigeonhole/tests/extensions/enotify/execute/duplicates.sieve4
-rw-r--r--pigeonhole/tests/extensions/enotify/mailto.svtest541
-rw-r--r--pigeonhole/tests/extensions/enotify/notify_method_capability.svtest12
-rw-r--r--pigeonhole/tests/extensions/enotify/valid_notify_method.svtest31
-rw-r--r--pigeonhole/tests/extensions/envelope.svtest244
-rw-r--r--pigeonhole/tests/extensions/environment/basic.svtest33
-rw-r--r--pigeonhole/tests/extensions/environment/rfc.svtest28
-rw-r--r--pigeonhole/tests/extensions/ihave/errors.svtest19
-rw-r--r--pigeonhole/tests/extensions/ihave/errors/error.sieve3
-rw-r--r--pigeonhole/tests/extensions/ihave/execute.svtest23
-rw-r--r--pigeonhole/tests/extensions/ihave/execute/ihave.sieve7
-rw-r--r--pigeonhole/tests/extensions/ihave/restrictions.svtest14
-rw-r--r--pigeonhole/tests/extensions/imap4flags/basic.svtest332
-rw-r--r--pigeonhole/tests/extensions/imap4flags/execute.svtest68
-rw-r--r--pigeonhole/tests/extensions/imap4flags/execute/flags-side-effect.sieve18
-rw-r--r--pigeonhole/tests/extensions/imap4flags/flagstore.svtest146
-rw-r--r--pigeonhole/tests/extensions/imap4flags/flagstring.svtest82
-rw-r--r--pigeonhole/tests/extensions/imap4flags/hasflag.svtest91
-rw-r--r--pigeonhole/tests/extensions/imap4flags/multiscript.svtest55
-rw-r--r--pigeonhole/tests/extensions/imap4flags/multiscript/fileinto.sieve4
-rw-r--r--pigeonhole/tests/extensions/imap4flags/multiscript/group-spam.sieve14
-rw-r--r--pigeonhole/tests/extensions/imap4flags/multiscript/sent-store.sieve7
-rw-r--r--pigeonhole/tests/extensions/imap4flags/multiscript/setflag.sieve3
-rw-r--r--pigeonhole/tests/extensions/imap4flags/multiscript/spam.sieve8
-rw-r--r--pigeonhole/tests/extensions/include/errors.svtest149
-rw-r--r--pigeonhole/tests/extensions/include/errors/action-conflicts.sieve4
-rw-r--r--pigeonhole/tests/extensions/include/errors/circular-1.sieve5
-rw-r--r--pigeonhole/tests/extensions/include/errors/circular-2.sieve5
-rw-r--r--pigeonhole/tests/extensions/include/errors/circular-3.sieve5
-rw-r--r--pigeonhole/tests/extensions/include/errors/depth-limit.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/errors/generic.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/errors/global-namespace.sieve13
-rw-r--r--pigeonhole/tests/extensions/include/errors/include-limit.sieve6
-rw-r--r--pigeonhole/tests/extensions/include/errors/scriptname.sieve25
-rw-r--r--pigeonhole/tests/extensions/include/errors/variables-inactive.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/errors/variables.sieve23
-rw-r--r--pigeonhole/tests/extensions/include/execute.svtest68
-rw-r--r--pigeonhole/tests/extensions/include/execute/actions-fileinto.sieve5
-rw-r--r--pigeonhole/tests/extensions/include/execute/namespace.sieve26
-rw-r--r--pigeonhole/tests/extensions/include/execute/optional.sieve5
-rw-r--r--pigeonhole/tests/extensions/include/included-global/namespace.dict4
-rw-r--r--pigeonhole/tests/extensions/include/included-global/namespace.sieve4
-rw-r--r--pigeonhole/tests/extensions/include/included-global/rfc-ex1-spam_tests.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/included/action-fileinto.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/action-reject.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/actions-fileinto1.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/actions-fileinto2.sieve4
-rw-r--r--pigeonhole/tests/extensions/include/included/actions-fileinto3.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/circular-one.sieve5
-rw-r--r--pigeonhole/tests/extensions/include/included/circular-three-2.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/circular-three-3.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/circular-three.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/included/circular-two-2.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/circular-two.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/included/depth-limit-1.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/depth-limit-2.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/depth-limit-3.sieve1
-rw-r--r--pigeonhole/tests/extensions/include/included/namespace.dict4
-rw-r--r--pigeonhole/tests/extensions/include/included/namespace.sieve4
-rw-r--r--pigeonhole/tests/extensions/include/included/once-1.sieve9
-rw-r--r--pigeonhole/tests/extensions/include/included/once-2.sieve12
-rw-r--r--pigeonhole/tests/extensions/include/included/once-3.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/once-4.sieve3
-rw-r--r--pigeonhole/tests/extensions/include/included/optional-1.sieve9
-rw-r--r--pigeonhole/tests/extensions/include/included/optional-2.sieve9
-rw-r--r--pigeonhole/tests/extensions/include/included/rfc-ex1-always_allow.sieve8
-rw-r--r--pigeonhole/tests/extensions/include/included/rfc-ex1-mailing_lists.sieve10
-rw-r--r--pigeonhole/tests/extensions/include/included/rfc-ex1-spam_tests.sieve10
-rw-r--r--pigeonhole/tests/extensions/include/included/rfc-ex2-spam_filter_script.sieve8
-rw-r--r--pigeonhole/tests/extensions/include/included/twice-1.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/included/twice-2.sieve8
-rw-r--r--pigeonhole/tests/extensions/include/included/variables-included1.sieve7
-rw-r--r--pigeonhole/tests/extensions/include/included/variables-included2.sieve6
-rw-r--r--pigeonhole/tests/extensions/include/included/variables-included3.sieve8
-rw-r--r--pigeonhole/tests/extensions/include/once.svtest24
-rw-r--r--pigeonhole/tests/extensions/include/optional.svtest40
-rw-r--r--pigeonhole/tests/extensions/include/rfc-ex1-default.sieve6
-rw-r--r--pigeonhole/tests/extensions/include/rfc-ex2-default.sieve21
-rw-r--r--pigeonhole/tests/extensions/include/rfc.svtest13
-rw-r--r--pigeonhole/tests/extensions/include/twice.svtest20
-rw-r--r--pigeonhole/tests/extensions/include/variables.svtest29
-rw-r--r--pigeonhole/tests/extensions/index/basic.svtest93
-rw-r--r--pigeonhole/tests/extensions/index/errors.svtest20
-rw-r--r--pigeonhole/tests/extensions/index/errors/syntax.sieve26
-rw-r--r--pigeonhole/tests/extensions/mailbox/errors.svtest40
-rw-r--r--pigeonhole/tests/extensions/mailbox/errors/mailboxexists-bad-utf8.sieve9
-rw-r--r--pigeonhole/tests/extensions/mailbox/errors/syntax.sieve41
-rw-r--r--pigeonhole/tests/extensions/mailbox/execute.svtest80
-rw-r--r--pigeonhole/tests/extensions/metadata/errors.svtest56
-rw-r--r--pigeonhole/tests/extensions/metadata/errors/metadata-bad-utf8.sieve9
-rw-r--r--pigeonhole/tests/extensions/metadata/errors/metadataexists-bad-utf8.sieve9
-rw-r--r--pigeonhole/tests/extensions/metadata/errors/syntax.sieve53
-rw-r--r--pigeonhole/tests/extensions/metadata/execute.svtest145
-rw-r--r--pigeonhole/tests/extensions/mime/address.svtest281
-rw-r--r--pigeonhole/tests/extensions/mime/calendar-example.svtest129
-rw-r--r--pigeonhole/tests/extensions/mime/content-header.svtest161
-rw-r--r--pigeonhole/tests/extensions/mime/errors.svtest162
-rw-r--r--pigeonhole/tests/extensions/mime/errors/address-mime-tag.sieve38
-rw-r--r--pigeonhole/tests/extensions/mime/errors/break.sieve157
-rw-r--r--pigeonhole/tests/extensions/mime/errors/exists-mime-tag.sieve43
-rw-r--r--pigeonhole/tests/extensions/mime/errors/extracttext-nofep.sieve4
-rw-r--r--pigeonhole/tests/extensions/mime/errors/extracttext-novar.sieve6
-rw-r--r--pigeonhole/tests/extensions/mime/errors/extracttext.sieve42
-rw-r--r--pigeonhole/tests/extensions/mime/errors/foreverypart.sieve45
-rw-r--r--pigeonhole/tests/extensions/mime/errors/header-mime-tag.sieve100
-rw-r--r--pigeonhole/tests/extensions/mime/errors/limits-include.sieve6
-rw-r--r--pigeonhole/tests/extensions/mime/errors/limits.sieve13
-rw-r--r--pigeonhole/tests/extensions/mime/execute.svtest82
-rw-r--r--pigeonhole/tests/extensions/mime/execute/foreverypart.sieve14
-rw-r--r--pigeonhole/tests/extensions/mime/execute/mime.sieve69
-rw-r--r--pigeonhole/tests/extensions/mime/exists.svtest237
-rw-r--r--pigeonhole/tests/extensions/mime/extracttext.svtest143
-rw-r--r--pigeonhole/tests/extensions/mime/foreverypart.svtest178
-rw-r--r--pigeonhole/tests/extensions/mime/header.svtest444
-rw-r--r--pigeonhole/tests/extensions/mime/included/include-foreverypart.sieve44
-rw-r--r--pigeonhole/tests/extensions/mime/included/include-loop-2.sieve6
-rw-r--r--pigeonhole/tests/extensions/mime/included/include-loop-3.sieve6
-rw-r--r--pigeonhole/tests/extensions/mime/included/include-loop-4.sieve6
-rw-r--r--pigeonhole/tests/extensions/mime/included/include-loop-5.sieve9
-rw-r--r--pigeonhole/tests/extensions/regex/basic.svtest51
-rw-r--r--pigeonhole/tests/extensions/regex/errors.svtest29
-rw-r--r--pigeonhole/tests/extensions/regex/errors/compile.sieve25
-rw-r--r--pigeonhole/tests/extensions/regex/errors/runtime.sieve9
-rw-r--r--pigeonhole/tests/extensions/regex/match-values.svtest72
-rw-r--r--pigeonhole/tests/extensions/reject/execute.svtest34
-rw-r--r--pigeonhole/tests/extensions/reject/execute/basic.sieve8
-rw-r--r--pigeonhole/tests/extensions/reject/smtp.svtest56
-rw-r--r--pigeonhole/tests/extensions/relational/basic.svtest178
-rw-r--r--pigeonhole/tests/extensions/relational/comparators.svtest258
-rw-r--r--pigeonhole/tests/extensions/relational/errors.svtest33
-rw-r--r--pigeonhole/tests/extensions/relational/errors/syntax.sieve8
-rw-r--r--pigeonhole/tests/extensions/relational/errors/validation.sieve11
-rw-r--r--pigeonhole/tests/extensions/relational/rfc.svtest71
-rw-r--r--pigeonhole/tests/extensions/spamvirustest/errors.svtest15
-rw-r--r--pigeonhole/tests/extensions/spamvirustest/errors/syntax-errors.sieve19
-rw-r--r--pigeonhole/tests/extensions/spamvirustest/spamtest.svtest276
-rw-r--r--pigeonhole/tests/extensions/spamvirustest/spamtestplus.svtest136
-rw-r--r--pigeonhole/tests/extensions/spamvirustest/virustest.svtest143
-rw-r--r--pigeonhole/tests/extensions/special-use/errors.svtest38
-rw-r--r--pigeonhole/tests/extensions/special-use/errors/specialuse_exists-bad-utf8.sieve9
-rw-r--r--pigeonhole/tests/extensions/special-use/errors/syntax.sieve38
-rw-r--r--pigeonhole/tests/extensions/special-use/execute.svtest54
-rw-r--r--pigeonhole/tests/extensions/subaddress/basic.svtest111
-rw-r--r--pigeonhole/tests/extensions/subaddress/config.svtest85
-rw-r--r--pigeonhole/tests/extensions/subaddress/rfc.svtest59
-rw-r--r--pigeonhole/tests/extensions/vacation/errors.svtest19
-rw-r--r--pigeonhole/tests/extensions/vacation/errors/conflict-reject.sieve5
-rw-r--r--pigeonhole/tests/extensions/vacation/execute.svtest73
-rw-r--r--pigeonhole/tests/extensions/vacation/execute/action.sieve4
-rw-r--r--pigeonhole/tests/extensions/vacation/execute/no-handle.sieve10
-rw-r--r--pigeonhole/tests/extensions/vacation/execute/seconds.sieve4
-rw-r--r--pigeonhole/tests/extensions/vacation/message.svtest752
-rw-r--r--pigeonhole/tests/extensions/vacation/references.sieve4
-rw-r--r--pigeonhole/tests/extensions/vacation/reply.svtest536
-rw-r--r--pigeonhole/tests/extensions/vacation/smtp.svtest199
-rw-r--r--pigeonhole/tests/extensions/vacation/utf-8.svtest168
-rw-r--r--pigeonhole/tests/extensions/variables/basic.svtest223
-rw-r--r--pigeonhole/tests/extensions/variables/errors.svtest34
-rw-r--r--pigeonhole/tests/extensions/variables/errors/limits.sieve287
-rw-r--r--pigeonhole/tests/extensions/variables/errors/namespace.sieve8
-rw-r--r--pigeonhole/tests/extensions/variables/errors/set.sieve19
-rw-r--r--pigeonhole/tests/extensions/variables/limits.svtest435
-rw-r--r--pigeonhole/tests/extensions/variables/match.svtest365
-rw-r--r--pigeonhole/tests/extensions/variables/modifiers.svtest160
-rw-r--r--pigeonhole/tests/extensions/variables/quoting.svtest36
-rw-r--r--pigeonhole/tests/extensions/variables/regex.svtest35
-rw-r--r--pigeonhole/tests/extensions/variables/string.svtest37
-rw-r--r--pigeonhole/tests/extensions/vnd.dovecot/debug/execute.svtest6
-rw-r--r--pigeonhole/tests/extensions/vnd.dovecot/environment/basic.svtest29
-rw-r--r--pigeonhole/tests/extensions/vnd.dovecot/environment/variables.svtest18
-rw-r--r--pigeonhole/tests/extensions/vnd.dovecot/report/errors.svtest13
-rw-r--r--pigeonhole/tests/extensions/vnd.dovecot/report/errors/syntax.sieve28
-rw-r--r--pigeonhole/tests/extensions/vnd.dovecot/report/execute.svtest269
221 files changed, 16063 insertions, 0 deletions
diff --git a/pigeonhole/tests/extensions/body/basic.svtest b/pigeonhole/tests/extensions/body/basic.svtest
new file mode 100644
index 0000000..0b2bffc
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/basic.svtest
@@ -0,0 +1,97 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+require "body";
+
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Subject: Test message.
+
+Test!
+
+.
+;
+
+/* Empty line
+ *
+ * RFC 5173:
+ * 'The body test matches content in the body of an email message, that
+ * is, anything following the first empty line after the header. (The
+ * empty line itself, if present, is not considered to be part of the
+ * body.)'
+ */
+test "The empty line" {
+
+ if not body :raw :is text:
+Test!
+
+.
+ {
+ test_fail "invalid message body extracted (1)";
+ }
+
+ if body :raw :is text:
+
+Test!
+
+.
+ {
+ test_fail "invalid message body extracted (2)";
+ }
+
+ if body :raw :is "Test"
+ {
+ test_fail "body test matches nonsense (3)";
+ }
+}
+
+/* Default comparator and match type
+ *
+ * RFC 5173:
+ * 'The COMPARATOR and MATCH-TYPE keyword parameters are defined in
+ * [SIEVE]. As specified in Sections 2.7.1 and 2.7.3 of [SIEVE], the
+ * default COMPARATOR is "i;ascii-casemap" and the default MATCH-TYPE is
+ * ":is".'
+ */
+
+test "Defaults" {
+ if anyof ( body :raw "Test", body :raw "*Test*" ) {
+ test_fail "default match type is not :is as is required";
+ }
+
+ if allof( not body :raw :contains "tesT", body :raw :contains "Test" ) {
+ test_fail "default comparator is not i;ascii-casemap as is required";
+ }
+}
+
+/* No body
+ *
+ * RFC 5173:
+ * 'If a message consists of a header only, not followed by an empty line,
+ * then that set is empty and all "body" tests return false, including
+ * those that test for an empty string. (This is similar to how the
+ * "header" test always fails when the named header fields aren't present.)'
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Subject: No body is here!
+.
+;
+
+test "No body" {
+ if body :raw :contains "" {
+ test_fail "matched against non-existent body (:contains \"\")";
+ }
+
+ if body :raw :is "" {
+ test_fail "matched against non-existent body (:is \"\")";
+ }
+
+ if body :raw :matches "*" {
+ test_fail "matched against non-existent body (:matches \"*\")";
+ }
+}
diff --git a/pigeonhole/tests/extensions/body/content.svtest b/pigeonhole/tests/extensions/body/content.svtest
new file mode 100644
index 0000000..2eb3837
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/content.svtest
@@ -0,0 +1,332 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+require "body";
+
+/*
+ *
+ */
+
+test_set "message" text:
+From: justin@example.com
+To: carl@example.nl
+Subject: Frop
+Content-Type: multipart/mixed; boundary=donkey
+
+This is a multi-part message in MIME format.
+
+--donkey
+Content-Type: text/plain
+
+Plain Text
+
+--donkey
+Content-Type: text/stupid
+
+Stupid Text
+
+--donkey
+Content-Type: text/plain/stupid
+
+Plain Stupid Text
+
+--donkey--
+.
+;
+
+/*
+ * RFC5173, Section 5.2:
+ * If an individual content type begins or ends with a '/' (slash) or
+ * contains multiple slashes, then it matches no content types.
+ * ...
+ */
+
+test "Basic Match" {
+ if not body :content "text/plain" :matches "Plain Text*" {
+ test_fail "failed to match (1)";
+ }
+
+ if not body :content "text/plain" :contains "" {
+ test_fail "failed to match (2)";
+ }
+
+ if not body :content "text/stupid" :contains "" {
+ test_fail "failed to match (3)";
+ }
+}
+
+test "Begin Slash" {
+ if body :content "/plain" :contains "" {
+ test_fail "matched :content \"/plain\"";
+ }
+}
+
+test "End Slash" {
+ if body :content "text/" :contains "" {
+ test_fail "matched :content \"text/\"";
+ }
+}
+
+test "Double Slash" {
+ if body :content "text/plain/stupid" :contains "" {
+ test_fail "matched :content \"text/plain/stupid\"";
+ }
+}
+
+/*
+ *
+ */
+
+test_set "message" text:
+From: justin@example.com
+To: carl@example.nl
+Subject: Frop
+Content-Type: multipart/mixed; boundary=limit
+
+This is a multi-part message in MIME format.
+
+--limit
+Content-Type: text/plain
+
+This is a text message.
+
+--limit
+Content-Type: text/html
+
+<html><body>This is HTML</body></html>
+
+--limit
+Content-Type: application/sieve
+
+keep;
+
+--limit--
+.
+;
+
+/* RFC5173, Section 5.2:
+ * ...
+ * Otherwise, if it contains a slash, then it specifies a full
+ * <type>/<subtype> pair, and matches only that specific content type.
+ * If it is the empty string, all MIME content types are matched.
+ * Otherwise, it specifies a <type> only, and any subtype of that type
+ * matches it.
+ */
+
+test "Full Content Type" {
+ if not body :content "text/plain" :matches "This is a text message.*" {
+ test_fail "failed to match text/plain content";
+ }
+
+ if body :content "text/plain" :matches "<html><body>This is HTML</body></html>*" {
+ test_fail "erroneously matched text/html content";
+ }
+
+ if not body :content "text/html" :matches "<html><body>This is HTML</body></html>*" {
+ test_fail "failed to match text/html content";
+ }
+
+ if body :content "text/html" :matches "This is a text message.*" {
+ test_fail "erroneously matched text/plain content";
+ }
+
+ if body :content "text/html" :matches "This is HTML*" {
+ test_fail "body :content test matched plain text";
+ }
+}
+
+test "Empty Content Type" {
+ if not body :content "" :matches "This is a text message.*" {
+ test_fail "failed to match text/plain content";
+ }
+
+ if not body :content "" :matches "<html><body>This is HTML</body></html>*" {
+ test_fail "failed to match text/html content";
+ }
+
+ if not body :content "" :matches "keep;*" {
+ test_fail "failed to match application/sieve content";
+ }
+
+ if body :content "" :matches "*blurdybloop*" {
+ test_fail "body :content \"\" test matches nonsense";
+ }
+}
+
+test "Main Content Type" {
+ if not body :content "text" :matches "This is a text message.*" {
+ test_fail "failed to match text/plain content";
+ }
+
+ if not body :content "text" :matches "<html><body>This is HTML</body></html>*" {
+ test_fail "failed to match text/html content";
+ }
+
+ if body :content "text" :matches "keep;*" {
+ test_fail "erroneously matched application/sieve content";
+ }
+}
+
+/*
+ *
+ */
+
+test_set "message" text:
+From: Whomever <whoever@example.com>
+To: Someone <someone@example.com>
+Date: Sat, 10 Oct 2009 00:30:04 +0200
+Subject: whatever
+Content-Type: multipart/mixed; boundary=outer
+
+This is a multi-part message in MIME format.
+
+--outer
+Content-Type: multipart/alternative; boundary=inner
+
+This is a nested multi-part message in MIME format.
+
+--inner
+Content-Type: text/plain; charset="us-ascii"
+
+Hello
+
+--inner
+Content-Type: text/html; charset="us-ascii"
+
+<html><body>Hello</body></html>
+
+--inner--
+
+This is the end of the inner MIME multipart.
+
+--outer
+Content-Type: message/rfc822
+
+From: Someone Else
+Subject: Hello, this is an elaborate request for you to finally say hello
+ already!
+
+Please say Hello
+
+--outer--
+
+This is the end of the outer MIME multipart.
+.
+;
+
+/* RFC5173, Section 5.2:
+ *
+ * The search for MIME parts matching the :content specification is
+ * recursive and automatically descends into multipart and
+ * message/rfc822 MIME parts. All MIME parts with matching types are
+ * searched for the key strings. The test returns true if any
+ * combination of a searched MIME part and key-list argument match.
+ */
+
+test "Nested Search" {
+ if not body :content "text/plain" :matches "Hello*" {
+ test_fail "failed to match text/plain content";
+ }
+
+ if body :content "text/plain" :matches "<html><body>Hello</body></html>*" {
+ test_fail "erroneously matched text/html content";
+ }
+
+ if not body :content "text/html" :matches "<html><body>Hello</body></html>*" {
+ test_fail "failed to match text/html content";
+ }
+
+ if body :content "text/html" :matches "Hello*" {
+ test_fail "erroneously matched text/plain content";
+ }
+
+ if not body :content "text" :contains "html" {
+ test_fail "failed match text content (1)";
+ }
+
+ if not body :content "text" :contains "hello" {
+ test_fail "failed match text content (2)";
+ }
+
+ if not body :content "text/plain" :contains "please say hello" {
+ test_fail "failed match nested message content as text/plain";
+ }
+
+ if not body :content "text" :contains "please say hello" {
+ test_fail "failed match nested message content as text/*";
+ }
+
+ if not body :content "text" :count "eq" :comparator "i;ascii-numeric" "3" {
+ test_fail "matched wrong number of \"text/*\" body parts";
+ }
+}
+
+/* RFC5173, Section 5.2:
+ *
+ * If the :content specification matches a multipart MIME part, only the
+ * prologue and epilogue sections of the part will be searched for the
+ * key strings, treating the entire prologue and the entire epilogue as
+ * separate strings; the contents of nested parts are only searched if
+ * their respective types match the :content specification.
+ *
+ */
+
+test "Multipart Content" {
+ if not body :content "multipart" :contains
+ "This is a multi-part message in MIME format" {
+ test_fail "missed first multipart body part";
+ }
+
+ if not body :content "multipart" :contains
+ "This is a nested multi-part message in MIME format" {
+ test_fail "missed second multipart body part";
+ }
+
+ if not body :content "multipart" :contains
+ "This is the end of the inner MIME multipart" {
+ test_fail "missed third multipart body part";
+ }
+
+ if not body :content "multipart" :contains
+ "This is the end of the outer MIME multipart." {
+ test_fail "missed fourth multipart body part";
+ }
+
+ if body :content "multipart" :contains "--inner" {
+ test_fail "inner boundary is part of match";
+ }
+
+ if body :content "multipart" :contains "--outer" {
+ test_fail "outer boundary is part of match";
+ }
+}
+
+/* RFC5173, Section 5.2:
+ *
+ * If the :content specification matches a message/rfc822 MIME part,
+ * only the header of the nested message will be searched for the key
+ * strings, treating the header as a single string; the contents of the
+ * nested message body parts are only searched if their content type
+ * matches the :content specification.
+ */
+
+test "Content-Type: message/rfc822" {
+ if not body :content "message/rfc822" :contains
+ "From: Someone Else" {
+ test_fail "missed raw message/rfc822 from header";
+ }
+
+ if not body :content "message/rfc822" :is text:
+From: Someone Else
+Subject: Hello, this is an elaborate request for you to finally say hello
+ already!
+.
+ {
+ test_fail "header content does not match exactly";
+ }
+}
+
+
+
+
diff --git a/pigeonhole/tests/extensions/body/errors.svtest b/pigeonhole/tests/extensions/body/errors.svtest
new file mode 100644
index 0000000..8db5657
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/errors.svtest
@@ -0,0 +1,19 @@
+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" "12" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/body/errors/syntax.sieve b/pigeonhole/tests/extensions/body/errors/syntax.sieve
new file mode 100644
index 0000000..8adf0ef
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/errors/syntax.sieve
@@ -0,0 +1,38 @@
+require "body";
+
+# 1: No key list
+if body { }
+
+# 2: Number
+if body 3 { }
+
+# OK: String
+if body "frop" { }
+
+# 3: To many arguments
+if body "frop" "friep" { }
+
+# 4: Unknown tag
+if body :frop { }
+
+# 5: Unknown tag with valid key
+if body :friep "frop" { }
+
+# 6: Content without argument
+if body :content { }
+
+# 7: Content without key argument
+if body :content "frop" { }
+
+# 8: Content with number argument
+if body :content 3 "frop" { }
+
+# 9: Content with unknown tag
+if body :content :frml "frop" { }
+
+# 10: Content with known tag
+if body :content :contains "frop" { }
+
+# 11: Duplicate transform
+if body :content "frop" :raw "frop" { }
+
diff --git a/pigeonhole/tests/extensions/body/match-values.svtest b/pigeonhole/tests/extensions/body/match-values.svtest
new file mode 100644
index 0000000..55d5535
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/match-values.svtest
@@ -0,0 +1,55 @@
+require "vnd.dovecot.testsuite";
+
+require "body";
+require "variables";
+
+test_set "message" text:
+From: stephan@example.org
+To: s.bosch@twente.example.net
+Subject: Body test
+
+The big bad body test.
+.
+;
+
+# Test whether body test ignores match values
+test "Match values disabled" {
+ if not body :raw :matches "The * bad * test*" {
+ test_fail "should have matched";
+ }
+
+ if anyof (
+ string :is "${1}" "big",
+ string :is "${2}" "body",
+ not string :is "${0}" "",
+ not string :is "${1}" "",
+ not string :is "${2}" "") {
+ test_fail "match values not disabled";
+ }
+}
+
+test "Match values re-enabled" {
+ if not header :matches "from" "*@*" {
+ test_fail "should have matched";
+ }
+
+ if anyof (
+ not string :is "${0}" "stephan@example.org",
+ not string :is "${1}" "stephan",
+ not string :is "${2}" "example.org" ) {
+ test_fail "match values not re-enabled properly.";
+ }
+}
+
+test "Match values retained" {
+ if not body :raw :matches "The * bad * test*" {
+ test_fail "should have matched";
+ }
+
+ if anyof (
+ not string :is "${0}" "stephan@example.org",
+ not string :is "${1}" "stephan",
+ not string :is "${2}" "example.org" ) {
+ test_fail "match values not retained after body test.";
+ }
+}
diff --git a/pigeonhole/tests/extensions/body/raw.svtest b/pigeonhole/tests/extensions/body/raw.svtest
new file mode 100644
index 0000000..d3404b9
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/raw.svtest
@@ -0,0 +1,85 @@
+require "vnd.dovecot.testsuite";
+require "body";
+
+test_set "message" text:
+From: Whomever <whoever@example.com>
+To: Someone <someone@example.com>
+Date: Sat, 10 Oct 2009 00:30:04 +0200
+Subject: whatever
+Content-Type: multipart/mixed; boundary=outer
+
+This is a multi-part message in MIME format.
+
+--outer
+Content-Type: multipart/alternative; boundary=inner
+
+This is a nested multi-part message in MIME format.
+
+--inner
+Content-Type: text/plain; charset="us-ascii"
+
+Hello
+
+--inner
+Content-Type: text/html; charset="us-ascii"
+
+<html><body>Hello</body></html>
+
+--inner--
+
+This is the end of the inner MIME multipart.
+
+--outer
+Content-Type: message/rfc822
+
+From: Someone Else
+Subject: hello request
+
+Please say Hello
+
+--outer--
+
+This is the end of the outer MIME multipart.
+.
+;
+
+/*
+ *
+ * RFC 5173:
+ * The ":raw" transform matches against the entire undecoded body of a
+ * message as a single item.
+ *
+ * If the specified body-transform is ":raw", the [MIME] structure of
+ * the body is irrelevant. The implementation MUST NOT remove any
+ * transfer encoding from the message, MUST NOT refuse to filter
+ * messages with syntactic errors (unless the environment it is part of
+ * rejects them outright), and MUST treat multipart boundaries or the
+ * MIME headers of enclosed body parts as part of the content being
+ * matched against, instead of MIME structures to interpret.
+ */
+
+test "Multipart Boundaries" {
+ if not body :raw :contains "--inner" {
+ test_fail "Raw body does not contain '--inner'";
+ }
+
+ if not body :raw :contains "--outer" {
+ test_fail "Raw body does not contain '--outer'";
+ }
+}
+
+test "Multipart Headers" {
+ if not body :raw :contains "boundary=inner" {
+ test_fail "Raw body does not contain 'boundary=inner'";
+ }
+
+ if not body :raw :contains "rfc822" {
+ test_fail "Raw body does not contain 'rfc822'";
+ }
+}
+
+test "Multipart Content" {
+ if not body :raw :contains "<html><body>Hello</body></html>" {
+ test_fail "Raw body does not contain '<html><body>Hello</body></html>'";
+ }
+}
diff --git a/pigeonhole/tests/extensions/body/text.svtest b/pigeonhole/tests/extensions/body/text.svtest
new file mode 100644
index 0000000..2dc6a03
--- /dev/null
+++ b/pigeonhole/tests/extensions/body/text.svtest
@@ -0,0 +1,225 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+require "body";
+
+/*
+ *
+ */
+
+test_set "message" text:
+From: justin@example.com
+To: carl@example.nl
+Subject: Frop
+Content-Type: multipart/mixed; boundary=donkey
+
+This is a multi-part message in MIME format.
+
+--donkey
+Content-Type: text/plain
+
+Plain Text
+
+--donkey
+Content-Type: text/stupid
+
+Stupid Text
+
+--donkey
+Content-Type: text/plain/stupid
+
+Plain Stupid Text
+
+--donkey--
+.
+;
+
+test "Basic Match" {
+ if not body :text :contains "Plain Text" {
+ test_fail "failed to match (1)";
+ }
+
+ if not body :text :contains "Stupid Text" {
+ test_fail "failed to match (2)";
+ }
+}
+
+test "Double Slash" {
+ if body :text :contains "Plain Stupid Text" {
+ test_fail "matched \"text/plain/stupid\"";
+ }
+}
+
+/*
+ *
+ */
+
+test_set "message" text:
+From: justin@example.com
+To: carl@example.nl
+Subject: Frop
+Content-Type: multipart/mixed; boundary=limit
+
+This is a multi-part message in MIME format.
+
+--limit
+Content-Type: text/plain
+
+This is a text message.
+
+--limit
+Content-Type: text/html
+
+<html><body>This is HTML</body></html>
+
+--limit
+Content-Type: application/sieve
+
+keep;
+
+--limit--
+.
+;
+
+test "Full Content Type" {
+ if not body :text :contains "This is a text message" {
+ test_fail "failed to match text/plain content";
+ }
+
+ if not body :text :contains "This is HTML" {
+ test_fail "failed to match text/html content";
+ }
+
+ if body :text :contains "<html>" {
+ test_fail "erroneously matched text/html markup";
+ }
+
+ if body :text :contains "keep;" {
+ test_fail "body :text test matched non-text content";
+ }
+}
+
+/*
+ *
+ */
+
+test_set "message" text:
+From: Whomever <whoever@example.com>
+To: Someone <someone@example.com>
+Date: Sat, 10 Oct 2009 00:30:04 +0200
+Subject: whatever
+Content-Type: multipart/mixed; boundary=outer
+
+This is a multi-part message in MIME format.
+
+--outer
+Content-Type: multipart/alternative; boundary=inner
+
+This is a nested multi-part message in MIME format.
+
+--inner
+Content-Type: text/plain; charset="us-ascii"
+
+Hello
+
+--inner
+Content-Type: text/html; charset="us-ascii"
+
+<html><body>HTML Hello</body></html>
+
+--inner
+Content-Type: application/xhtml+xml; charset="us-ascii"
+
+<html><body>XHTML Hello</body></html>
+
+--inner--
+
+This is the end of the inner MIME multipart.
+
+--outer
+Content-Type: message/rfc822
+
+From: Someone Else
+Subject: Hello, this is an elaborate request for you to finally say hello
+ already!
+
+Please say Hello
+
+--outer--
+
+This is the end of the outer MIME multipart.
+.
+;
+
+/* RFC5173, Section 5.2:
+ *
+ * The search for MIME parts matching the :content specification is
+ * recursive and automatically descends into multipart and
+ * message/rfc822 MIME parts. All MIME parts with matching types are
+ * searched for the key strings. The test returns true if any
+ * combination of a searched MIME part and key-list argument match.
+ */
+
+test "Nested Search" {
+ if not body :text :contains "Hello" {
+ test_fail "failed to match text/plain content";
+ }
+ if not body :text :contains "HTML Hello" {
+ test_fail "failed to match text/html content";
+ }
+ if not body :text :contains "XHTML Hello" {
+ test_fail "failed to match application/xhtml+xml content";
+ }
+ if body :text :contains ["<html>", "body"] {
+ test_fail "erroneously matched text/html markup";
+ }
+ if not body :text :contains "Please say Hello" {
+ test_fail "failed to match message/rfc822 body";
+ }
+ if body :text :contains "MIME" {
+ test_fail "erroneously matched multipart prologue/epilogue text";
+ }
+}
+
+/*
+ * Broken/Empty parts
+ */
+
+test_set "message" text:
+From: Whomever <whoever@example.com>
+To: Someone <someone@example.com>
+Date: Sat, 10 Oct 2009 00:30:04 +0200
+Subject: whatever
+Content-Type: multipart/mixed; boundary=outer
+
+This is a multi-part message in MIME format.
+
+--outer
+Content-Type: text/html
+
+--outer
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: multipart/related
+Content-Disposition: inline
+
+<html><body>Please say Hello</body></html>
+
+--outer--
+
+This is the end of the outer MIME multipart.
+.
+;
+
+test "Nested Search" {
+ if body :text :contains "Hello" {
+ test_fail "Cannot match empty/broken part";
+ }
+ if body :text :contains ["<html>", "body"] {
+ test_fail "erroneously matched text/html markup";
+ }
+ if body :text :contains "MIME" {
+ test_fail "erroneously matched multipart prologue/epilogue text";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/date/basic.svtest b/pigeonhole/tests/extensions/date/basic.svtest
new file mode 100644
index 0000000..5d0b33f
--- /dev/null
+++ b/pigeonhole/tests/extensions/date/basic.svtest
@@ -0,0 +1,73 @@
+require "vnd.dovecot.testsuite";
+require "date";
+require "variables";
+require "relational";
+
+test_set "message" text:
+From: stephan@example.org
+To: sirius@friep.example.com
+Subject: Frop!
+Date: Mon, 20 Jul 2009 21:44:43 +0300
+Delivery-Date: Mon, 22 Jul 2009 23:30:14 +0300
+Invalid-Date: Moo, 34 Juul 3060 25:30:42 +6600
+Wanna date?
+.
+;
+
+test "Defaults" {
+ if not date :originalzone "date" "std11" "mon, 20 jul 2009 21:44:43 +0300" {
+ test_fail "default comparator is not i;ascii-casemap";
+ }
+
+ if anyof ( date "date" "std11" "Mon", date "date" "std11" "*") {
+ test_fail "default match type appears to be :contains or :matches";
+ }
+}
+
+test "Count" {
+ if not date :count "eq" "date" "date" "1" {
+ test_fail "count of existing date header field is not 1";
+ }
+
+ if not date :count "eq" "resent-date" "date" "0" {
+ test_fail "count of non-existent date header field is not 0";
+ }
+}
+
+test "Invalid" {
+ if date :matches "invalid-date" "std11" "*" {
+ test_fail "matched invalid date: ${0}";
+ }
+}
+
+test "Comparison" {
+ if not date :originalzone :is "delivery-date" "date" "2009-07-22" {
+ if date :originalzone :matches "delivery-date" "date" "*" { set "date" "${1}"; }
+ test_fail "date is invalid: ${date}";
+ }
+ if not date :originalzone :value "ge" "delivery-date" "date" "2009-07-22" {
+ test_fail "date comparison ge failed equal";
+ }
+
+ if not date :originalzone :value "ge" "delivery-date" "date" "2009-07-21" {
+ test_fail "date comparison ge failed greater";
+ }
+
+ if anyof (not date :originalzone :value "ge" "delivery-date" "date" "2009-06-22",
+ not date :originalzone :value "ge" "date" "date" "2006-07-22" ) {
+ test_fail "date comparison ge failed much greater";
+ }
+
+ if not date :originalzone :value "le" "delivery-date" "date" "2009-07-22" {
+ test_fail "date comparison le failed equal";
+ }
+
+ if not date :originalzone :value "le" "delivery-date" "date" "2009-07-23" {
+ test_fail "date comparison le failed less";
+ }
+
+ if anyof (not date :originalzone :value "le" "delivery-date" "date" "2009-09-22",
+ not date :originalzone :value "le" "date" "date" "2012-07-22" ) {
+ test_fail "date comparison ge failed much less";
+ }
+}
diff --git a/pigeonhole/tests/extensions/date/date-parts.svtest b/pigeonhole/tests/extensions/date/date-parts.svtest
new file mode 100644
index 0000000..edc565c
--- /dev/null
+++ b/pigeonhole/tests/extensions/date/date-parts.svtest
@@ -0,0 +1,120 @@
+require "vnd.dovecot.testsuite";
+require "date";
+require "variables";
+
+test_set "message" text:
+From: stephan@example.org
+To: sirius@friep.example.com
+Subject: Frop!
+Date: Mon, 20 Jul 2009 21:44:43 +0300
+Delivery-Date: Mon, 22 Jul 2009 23:30:14 +0300
+
+Wanna date?
+.
+;
+
+/* "year" => the year, "0000" .. "9999". */
+test "Year" {
+ if not date :originalzone "date" "year" "2009" {
+ test_fail "failed to extract year part";
+ }
+}
+
+/* "month" => the month, "01" .. "12". */
+test "Month" {
+ if not date :originalzone "date" "month" "07" {
+ test_fail "failed to extract month part";
+ }
+}
+
+/* "day" => the day, "01" .. "31". */
+test "Day" {
+ if not date :originalzone "date" "day" "20" {
+ test_fail "failed to extract day part";
+ }
+}
+
+/* "date" => the date in "yyyy-mm-dd" format. */
+test "Date" {
+ if not date :originalzone "date" "date" "2009-07-20" {
+ test_fail "failed to extract date part";
+ }
+}
+
+/* "julian" => the Modified Julian Day, that is, the date
+ expressed as an integer number of days since
+ 00:00 UTC on November 17, 1858 (using the Gregorian
+ calendar). This corresponds to the regular
+ Julian Day minus 2400000.5. */
+test "Julian" {
+ if not date :originalzone "date" "julian" "55032" {
+ if date :matches :originalzone "date" "julian" "*" { }
+ test_fail "failed to extract julian part: ${0}";
+ }
+ if not date :originalzone "delivery-date" "julian" "55034" {
+ if date :matches :originalzone "delivery-date" "julian" "*" { }
+ test_fail "failed to extract julian part: ${0}";
+ }
+}
+
+/* "hour" => the hour, "00" .. "23". */
+test "Hour" {
+ if not date :originalzone "date" "hour" "21" {
+ test_fail "failed to extract hour part";
+ }
+}
+
+/* "minute" => the minute, "00" .. "59". */
+test "Minute" {
+ if not date :originalzone "date" "minute" "44" {
+ test_fail "failed to extract minute part";
+ }
+}
+
+/* "second" => the second, "00" .. "60". */
+test "Second" {
+ if not date :originalzone "date" "second" "43" {
+ test_fail "failed to extract second part";
+ }
+}
+
+/* "time" => the time in "hh:mm:ss" format. */
+test "Time" {
+ if not date :originalzone "date" "time" "21:44:43" {
+ test_fail "failed to extract time part";
+ }
+}
+
+/* "iso8601" => the date and time in restricted ISO 8601 format. */
+test "ISO8601" {
+ if not date :originalzone "date" "iso8601" "2009-07-20T21:44:43+03:00" {
+ test_fail "failed to extract iso8601 part";
+ }
+}
+
+/* "std11" => the date and time in a format appropriate
+ for use in a Date: header field [RFC2822]. */
+test "STD11" {
+ if not date :originalzone "date" "std11" "Mon, 20 Jul 2009 21:44:43 +0300" {
+ test_fail "failed to extract std11 part";
+ }
+}
+
+/* "zone" => the time zone in use. */
+test "zone" {
+ if not date :originalzone "date" "zone" "+0300" {
+ test_fail "failed to extract zone part";
+ }
+
+ if not date :zone "+0200" "date" "zone" "+0200" {
+ test_fail "failed to extract zone part";
+ }
+}
+
+/* "weekday" => the day of the week expressed as an integer between
+ "0" and "6". "0" is Sunday, "1" is Monday, etc. */
+test "Weekday" {
+ if not date :originalzone "date" "weekday" "1" {
+ test_fail "failed to extract weekday part";
+ }
+}
diff --git a/pigeonhole/tests/extensions/date/zones.svtest b/pigeonhole/tests/extensions/date/zones.svtest
new file mode 100644
index 0000000..77adb77
--- /dev/null
+++ b/pigeonhole/tests/extensions/date/zones.svtest
@@ -0,0 +1,76 @@
+require "vnd.dovecot.testsuite";
+require "date";
+require "variables";
+
+/* Extract local timezone first */
+test "Local-Zone" {
+ if not currentdate :matches "zone" "*" {
+ test_fail "matches '*' failed for zone part.";
+ }
+ set "local_zone" "${0}";
+}
+
+/* FIXME: using variables somehow fails here */
+if string "${local_zone}" "+0200" {
+test_set "message" text:
+From: stephan@example.org
+To: sirius@friep.example.com
+Subject: Frop!
+Date: Mon, 20 Jul 2009 21:44:43 +0300
+Delivery-Date: Mon, 23 Jul 2009 05:30:14 +0800
+
+Wanna date?
+.
+;
+} else {
+test_set "message" text:
+From: stephan@example.org
+To: sirius@friep.example.com
+Subject: Frop!
+Date: Mon, 20 Jul 2009 21:44:43 +0300
+Delivery-Date: Mon, 22 Jul 2009 23:30:14 +0200
+
+Wanna date?
+.
+;
+}
+
+test "Specified Zone" {
+ if not date :zone "+0200" "date" "zone" "+0200" {
+ if date :matches :zone "+0200" "date" "zone" "*" {}
+ test_fail "zone is incorrect: ${0}";
+ }
+
+ if not date :zone "+0200" "date" "time" "20:44:43" {
+ test_fail "zone is not applied";
+ }
+}
+
+test "Original Zone" {
+ if not date :originalzone "date" "zone" "+0300" {
+ if date :matches :originalzone "date" "zone" "*" {}
+ test_fail "zone is incorrect: ${0}";
+ }
+
+ if not date :originalzone "date" "time" "21:44:43" {
+ test_fail "time should be left untouched";
+ }
+}
+
+test "Local Zone Shift" {
+ if anyof (
+ allof (
+ string "${local_zone}" "+0200",
+ date "delivery-date" "iso8601" "2009-07-23T05:30:14+08:00"),
+ allof (
+ not string "${local_zone}" "+0200",
+ date "delivery-date" "iso8601" "2009-07-22T23:30:14+02:00")) {
+
+ if date :matches "delivery-date" "iso8601" "*"
+ { set "a" "${0}"; }
+ if date :originalzone :matches "delivery-date" "iso8601" "*"
+ { set "b" "${0}"; }
+
+ test_fail "time not shifted to local zone: ${b} => ${a}";
+ }
+}
diff --git a/pigeonhole/tests/extensions/duplicate/errors.svtest b/pigeonhole/tests/extensions/duplicate/errors.svtest
new file mode 100644
index 0000000..108a0f0
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/errors.svtest
@@ -0,0 +1,54 @@
+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" "17" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Invalid Syntax (vnd)" {
+ if test_script_compile "errors/syntax-vnd.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";
+ }
+}
+
+/*
+ * Extension conflict
+ */
+
+test "Extension conflict" {
+ if test_script_compile "errors/conflict.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Extension conflict (vnd first)" {
+ if test_script_compile "errors/conflict-vnd.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/duplicate/errors/conflict-vnd.sieve b/pigeonhole/tests/extensions/duplicate/errors/conflict-vnd.sieve
new file mode 100644
index 0000000..1c133df
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/errors/conflict-vnd.sieve
@@ -0,0 +1,4 @@
+require "vnd.dovecot.duplicate";
+require "duplicate";
+
+if duplicate { keep; }
diff --git a/pigeonhole/tests/extensions/duplicate/errors/conflict.sieve b/pigeonhole/tests/extensions/duplicate/errors/conflict.sieve
new file mode 100644
index 0000000..aa9b038
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/errors/conflict.sieve
@@ -0,0 +1,4 @@
+require "duplicate";
+require "vnd.dovecot.duplicate";
+
+if duplicate { keep; }
diff --git a/pigeonhole/tests/extensions/duplicate/errors/syntax-vnd.sieve b/pigeonhole/tests/extensions/duplicate/errors/syntax-vnd.sieve
new file mode 100644
index 0000000..f62aa2c
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/errors/syntax-vnd.sieve
@@ -0,0 +1,19 @@
+require "vnd.dovecot.duplicate";
+
+# Used as a command
+duplicate;
+
+# Used with no argument (not an error)
+if duplicate {}
+
+# Used with string argument
+if duplicate "frop" { }
+
+# Used with numer argument
+if duplicate 23423 { }
+
+# Used with numer argument
+if duplicate ["frop"] { }
+
+
+
diff --git a/pigeonhole/tests/extensions/duplicate/errors/syntax.sieve b/pigeonhole/tests/extensions/duplicate/errors/syntax.sieve
new file mode 100644
index 0000000..a561cfb
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/errors/syntax.sieve
@@ -0,0 +1,54 @@
+require "duplicate";
+
+# Used as a command
+duplicate;
+
+# Used with no argument (not an error)
+if duplicate {}
+
+# Used with string argument
+if duplicate "frop" { }
+
+# Used with numner argument
+if duplicate 23423 { }
+
+# Used with numer argument
+if duplicate ["frop"] { }
+
+# Used with unknown tag
+if duplicate :test "frop" { }
+
+# Bad :header parameter
+if duplicate :header 23 {}
+
+# Bad :uniqueid parameter
+if duplicate :uniqueid 23 {}
+
+# Bad :handle parameter
+if duplicate :handle ["a", "b", "c"] {}
+
+# Bad seconds parameter
+if duplicate :seconds "a" {}
+
+# Missing :header parameter
+if duplicate :header {}
+
+# Missing :uniqueid parameter
+if duplicate :uniqueid {}
+
+# Missing :handle parameter
+if duplicate :handle {}
+
+# Missing seconds parameter
+if duplicate :seconds {}
+
+# :last with a parameter
+if duplicate :last "frop" {}
+
+# :last as :seconds parameter
+if duplicate :seconds :last {}
+
+# Conflicting tags
+if duplicate :header "X-Frop" :uniqueid "FROP!" { }
+
+
diff --git a/pigeonhole/tests/extensions/duplicate/execute-vnd.svtest b/pigeonhole/tests/extensions/duplicate/execute-vnd.svtest
new file mode 100644
index 0000000..386550f
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/execute-vnd.svtest
@@ -0,0 +1,20 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.duplicate";
+
+test "Run" {
+ if duplicate {
+ test_fail "test erroneously reported a duplicate";
+ }
+
+ if duplicate :handle "handle" {
+ test_fail "test with name erroneously reported a duplicate";
+ }
+
+ if duplicate {
+ test_fail "test erroneously reported a duplicate";
+ }
+
+ if duplicate :handle "handle" {
+ test_fail "test with name erroneously reported a duplicate";
+ }
+}
diff --git a/pigeonhole/tests/extensions/duplicate/execute.svtest b/pigeonhole/tests/extensions/duplicate/execute.svtest
new file mode 100644
index 0000000..9e060ff
--- /dev/null
+++ b/pigeonhole/tests/extensions/duplicate/execute.svtest
@@ -0,0 +1,41 @@
+require "vnd.dovecot.testsuite";
+require "duplicate";
+
+# Simple execution tests; no duplicate verification can be tested yet.
+test "Run" {
+ if duplicate {
+ test_fail "test erroneously reported a duplicate";
+ }
+
+ if duplicate :handle "handle" {
+ test_fail "test with :handle erroneously reported a duplicate";
+ }
+
+ if duplicate {
+ test_fail "test erroneously reported a duplicate";
+ }
+
+ if duplicate :handle "handle" {
+ test_fail "test with :handle erroneously reported a duplicate";
+ }
+
+ if duplicate :header "X-frop" {
+ test_fail "test with :header erroneously reported a duplicate";
+ }
+
+ if duplicate :uniqueid "FROP!" {
+ test_fail "test with :uniqueid erroneously reported a duplicate";
+ }
+
+ if duplicate :seconds 90 {
+ test_fail "test with :seconds erroneously reported a duplicate";
+ }
+
+ if duplicate :seconds 90 :last {
+ test_fail "test with :seconds :last erroneously reported a duplicate";
+ }
+
+ if duplicate :last {
+ test_fail "test with :seconds :last erroneously reported a duplicate";
+ }
+}
diff --git a/pigeonhole/tests/extensions/editheader/addheader.svtest b/pigeonhole/tests/extensions/editheader/addheader.svtest
new file mode 100644
index 0000000..426b43d
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/addheader.svtest
@@ -0,0 +1,833 @@
+require "vnd.dovecot.testsuite";
+require "encoded-character";
+require "variables";
+require "fileinto";
+require "mailbox";
+require "body";
+
+require "editheader";
+
+set "message" text:
+From: stephan@example.com
+To: timo@example.com
+Subject: Frop!
+
+Frop!
+
+.
+;
+
+test_set "message" "${message}";
+test "Addheader - first" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader "X-Some-Header" "Header content";
+
+ if not size :over 76 {
+ test_fail "mail is not larger";
+ }
+
+ if size :over 107 {
+ test_fail "mail is too large";
+ }
+
+ if size :under 107 {
+ test_fail "mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not added";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder1";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder1" 0 {
+ test_fail "message not stored";
+ }
+
+ if not size :over 76 {
+ test_fail "stored mail is not larger";
+ }
+
+ if size :over 107 {
+ test_fail "stored mail is too large";
+ }
+
+ if size :under 100 {
+ test_fail "stored mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not in stored mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in stored mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not in redirected mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in redirected mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - first (two)" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader "X-Some-Header" "Header content";
+ addheader "X-Some-Other-Header" "More header content";
+
+ if not size :over 76 {
+ test_fail "mail is not larger";
+ }
+
+ if size :over 149 {
+ test_fail "mail is too large";
+ }
+
+ if size :under 149 {
+ test_fail "mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header #1 not added";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added #1";
+ }
+
+ if not exists "x-some-other-header" {
+ test_fail "header #2 not added";
+ }
+
+ if not header :is "x-some-other-header" "More header content" {
+ test_fail "wrong content added #2";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder2";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder2" 0 {
+ test_fail "message not stored";
+ }
+
+ if not size :over 76 {
+ test_fail "stored mail is not larger";
+ }
+
+ if size :over 149 {
+ test_fail "stored mail is too large";
+ }
+
+ if size :under 100 {
+ test_fail "stored mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header #1 not in stored mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content #1 in stored mail ";
+ }
+
+ if not exists "x-some-other-header" {
+ test_fail "header #2 not in stored mail";
+ }
+
+ if not header :is "x-some-other-header" "More header content" {
+ test_fail "wrong content #2 in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not in redirected mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in redirected mail ";
+ }
+
+ if not exists "x-some-other-header" {
+ test_fail "header #2 not in redirected mail";
+ }
+
+ if not header :is "x-some-other-header" "More header content" {
+ test_fail "wrong content #2 in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - last" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader :last "X-Some-Header" "Header content";
+
+ if not size :over 76 {
+ test_fail "mail is not larger";
+ }
+
+ if size :over 107 {
+ test_fail "mail is too large";
+ }
+
+ if size :under 107 {
+ test_fail "mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not added";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder3";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder3" 0 {
+ test_fail "message not stored";
+ }
+
+ if not size :over 76 {
+ test_fail "stored mail is not larger";
+ }
+
+ if size :over 107 {
+ test_fail "stored mail is too large";
+ }
+
+ if size :under 100 {
+ test_fail "stored mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not in stored mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in stored mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not in redirected mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in redirected mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - last (two)" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader :last "X-Some-Header" "Header content";
+ addheader "X-Some-Other-Header" "More header content";
+
+ if not size :over 76 {
+ test_fail "mail is not larger";
+ }
+
+ if size :over 149 {
+ test_fail "mail is too large";
+ }
+
+ if size :under 149 {
+ test_fail "mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header #1 not added";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added #1";
+ }
+
+ if not exists "x-some-other-header" {
+ test_fail "header #2 not added";
+ }
+
+ if not header :is "x-some-other-header" "More header content" {
+ test_fail "wrong content added #2";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder4";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder4" 0 {
+ test_fail "message not stored";
+ }
+
+ if not size :over 76 {
+ test_fail "stored mail is not larger";
+ }
+
+ if size :over 149 {
+ test_fail "stored mail is too large";
+ }
+
+ if size :under 100 {
+ test_fail "stored mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header #1 not in stored mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content #1 in stored mail";
+ }
+
+ if not exists "x-some-other-header" {
+ test_fail "header #2 not in stored mail";
+ }
+
+ if not header :is "x-some-other-header" "More header content" {
+ test_fail "wrong content #2 in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header #1 not in redirected mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content #1 in redirected mail ";
+ }
+
+ if not exists "x-some-other-header" {
+ test_fail "header #2 not in redirected mail";
+ }
+
+ if not header :is "x-some-other-header" "More header content" {
+ test_fail "wrong content #2 in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - framed" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader "X-Some-Header-first" "Header content first";
+ addheader :last "X-Some-Header-last" "Header content last";
+
+ if not size :over 76 {
+ test_fail "mail is not larger";
+ }
+
+ if size :over 160 {
+ test_fail "mail is too large";
+ }
+
+ if size :under 160 {
+ test_fail "mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not added";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not added";
+ }
+
+ if not header :is "x-some-header-first" "Header content first" {
+ test_fail "wrong first content added";
+ }
+
+ if not header :is "x-some-header-last" "Header content last" {
+ test_fail "wrong last content added";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder5";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder5" 0 {
+ test_fail "message not stored";
+ }
+
+ if not size :over 76 {
+ test_fail "stored mail is not larger";
+ }
+
+ if size :over 160 {
+ test_fail "stored mail is too large";
+ }
+
+ if size :under 152 {
+ test_fail "stored mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not in stored mail";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not in stored mail";
+ }
+
+ if not header :is "x-some-header-first" "Header content first" {
+ test_fail "wrong first header content in stored mail ";
+ }
+
+ if not header :is "x-some-header-last" "Header content last" {
+ test_fail "wrong last header content in stored mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not in redirected mail";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not in redirected mail";
+ }
+
+ if not header :is "x-some-header-first" "Header content first" {
+ test_fail "wrong first header content in redirected mail ";
+ }
+
+ if not header :is "x-some-header-last" "Header content last" {
+ test_fail "wrong last header content in redirected mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+/*
+ * Addheader - folded
+ */
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - folded" {
+ set "before"
+ "This is very long header content, folded to fit inside multiple header lines. This may cause problems, so that is why it is tested here.";
+ set "after"
+ "This is somewhat longer header content, folded to fit inside multiple header lines. This may cause problems, so that is why it is tested here.";
+
+ addheader :last "X-Some-Header-first" "${before}";
+ addheader :last "X-Some-Header-last" "${after}";
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not added";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not added";
+ }
+
+ if not header :is "x-some-header-first" "${before}" {
+ test_fail "wrong first content added";
+ }
+
+ if not header :is "x-some-header-last" "${after}" {
+ test_fail "wrong last content added";
+ }
+
+ redirect "frop@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not in redirected mail";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not in redirected mail";
+ }
+
+ if not header :is "x-some-header-first" "${before}" {
+ test_fail "wrong first header content in redirected mail ";
+ }
+
+ if not header :is "x-some-header-last" "${after}" {
+ test_fail "wrong last header content in redirected mail ";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+/*
+ * Addheader - newlines
+ */
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - newlines" {
+ set "before" text:
+This is very long header content
+ containing newlines. This may
+ cause some problems, so that
+ is why it is tested here.
+.
+;
+
+ set "after" text:
+This is somewhat longer header content
+ containing newlines. This may
+ cause some problems, so that
+ is why it is tested here.
+.
+;
+
+ set "before_out"
+ "This is very long header content containing newlines. This may cause some problems, so that is why it is tested here.";
+
+ set "after_out"
+ "This is somewhat longer header content containing newlines. This may cause some problems, so that is why it is tested here.";
+
+ addheader "X-Some-Header-first" "${before}";
+ addheader :last "X-Some-Header-last" "${after}";
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not added";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not added";
+ }
+
+ if not header :is "x-some-header-first" "${before_out}" {
+ if header :matches "x-some-header-first" "*" {}
+ test_fail "wrong first content added: `${0}`";
+ }
+
+ if not header :is "x-some-header-last" "${after_out}" {
+ if header :matches "x-some-header-last" "*" {}
+ test_fail "wrong last content added: `${0}`";
+ }
+
+ redirect "frop@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not exists "x-some-header-first" {
+ test_fail "first header not in redirected mail";
+ }
+
+ if not exists "x-some-header-last" {
+ test_fail "last header not in redirected mail";
+ }
+
+ if not header :is "x-some-header-first" "${before_out}" {
+ if header :matches "x-some-header-first" "*" {}
+ test_fail "wrong first header content in redirected mail: `${0}`";
+ }
+
+ if not header :is "x-some-header-last" "${after_out}" {
+ if header :matches "x-some-header-last" "*" {}
+ test_fail "wrong last header content in redirected mail: `${0}`";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Addheader - implicit keep" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader "X-Some-Header" "Header content";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "INBOX" 0 {
+ test_fail "message not stored";
+ }
+
+ if not size :over 76 {
+ test_fail "stored mail is not larger";
+ }
+
+ if size :over 107 {
+ test_fail "stored mail is too large";
+ }
+
+ if size :under 100 {
+ test_fail "stored mail is too small";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored message";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not added to stored message";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added to stored message";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+}
+
+test_set "message" "${message}";
+test "Addheader - UTF 8" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader "X-Some-Header" "Это тест!";
+ fileinto :create "folder6";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder6" 0 {
+ test_fail "message not stored";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not added to stored message";
+ }
+
+ if not header :is "x-some-header" "Это тест!" {
+ if header :matches "x-some-header" "*" {}
+ test_fail "Bel character not retained: `${0}`";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+}
+
+test_result_reset;
+
+test_set "message" "${message}";
+test "Addheader - devious characters" {
+ if size :over 76 {
+ test_fail "original message is longer than 76 bytes?!";
+ }
+
+ addheader "X-Some-Header" "Ring my ${hex:07}!";
+ fileinto :create "folder7";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder7" 0 {
+ test_fail "message not stored";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "header not added to stored message";
+ }
+
+ if header :is "x-some-header" "Ring my !" {
+ if header :matches "x-some-header" "*" {}
+ test_fail "Bel character not retained: `${0}`";
+ }
+
+ if not header :is "x-some-header" "Ring my ${hex:07}!" {
+ if header :matches "x-some-header" "*" {}
+ test_fail "Incorrect header value: `${0}`";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+}
diff --git a/pigeonhole/tests/extensions/editheader/alternating.svtest b/pigeonhole/tests/extensions/editheader/alternating.svtest
new file mode 100644
index 0000000..44d459c
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/alternating.svtest
@@ -0,0 +1,181 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "fileinto";
+require "mailbox";
+require "body";
+
+require "editheader";
+
+set "message" text:
+From: stephan@example.com
+To: timo@example.com
+Subject: Frop!
+
+Frop!
+
+.
+;
+
+
+test_set "message" "${message}";
+test "Alternating - add; delete" {
+ addheader "X-Some-Header" "Header content";
+
+ if not exists "x-some-header" {
+ test_fail "header not added";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added";
+ }
+
+ redirect "frop@example.com";
+
+ deleteheader "X-Some-Header";
+
+ if exists "x-some-header" {
+ test_fail "header not deleted";
+ }
+
+ fileinto :create "folder1";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ /* redirected message */
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "added header not in redirected mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in redirected mail ";
+ }
+
+ /* stored message message */
+
+ if not test_message :folder "folder1" 0 {
+ test_fail "message not stored";
+ }
+
+ if exists "x-some-header" {
+ test_fail "added header still present stored mail";
+ }
+}
+
+test_result_reset;
+
+test_set "message" "${message}";
+test "Alternating - delete; add" {
+ deleteheader "Subject";
+
+ if exists "subject" {
+ test_fail "header not deleted";
+ }
+
+ redirect "frop@example.com";
+
+ addheader "Subject" "Friep!";
+
+ if not exists "subject" {
+ test_fail "header not added";
+ }
+
+ if not header :is "subject" "Friep!" {
+ test_fail "wrong content added";
+ }
+
+ fileinto :create "folder2";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ /* redirected message */
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if exists "subject" {
+ test_fail "deleted header still present redirected mail";
+ }
+
+ /* stored message message */
+
+ if not test_message :folder "folder2" 0 {
+ test_fail "message not stored";
+ }
+
+ if not exists "subject" {
+ test_fail "added header not in stored mail";
+ }
+
+ if not header :is "subject" "Friep!" {
+ test_fail "wrong content in redirected mail ";
+ }
+}
+
+test_result_reset;
+
+test_set "message" "${message}";
+test "Alternating - add :last; delete any" {
+ addheader :last "X-Some-Header" "Header content";
+
+ if not exists "x-some-header" {
+ test_fail "header not added";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content added";
+ }
+
+ redirect "frop@example.com";
+
+ deleteheader "X-Some-Other-Header";
+
+ if not exists "x-some-header" {
+ test_fail "header somehow deleted";
+ }
+
+ fileinto :create "folder3";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ /* redirected message */
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "added header not in redirected mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in redirected mail ";
+ }
+
+ /* stored message message */
+
+ if not test_message :folder "folder3" 0 {
+ test_fail "message not stored";
+ }
+
+ if not exists "x-some-header" {
+ test_fail "added header lost in stored mail";
+ }
+
+ if not header :is "x-some-header" "Header content" {
+ test_fail "wrong content in stored mail ";
+ }
+
+}
+
diff --git a/pigeonhole/tests/extensions/editheader/deleteheader.svtest b/pigeonhole/tests/extensions/editheader/deleteheader.svtest
new file mode 100644
index 0000000..8b9d3ad
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/deleteheader.svtest
@@ -0,0 +1,1115 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "fileinto";
+require "mailbox";
+require "body";
+
+require "editheader";
+
+set "message" text:
+X-A: Onzinnige informatie
+X-B: kun je maar beter
+X-C: niet via e-mail versturen
+From: stephan@example.com
+X-D: en daarom is het nuttig
+To: timo@example.com
+Subject: Frop!
+X-A: dit terstond te verwijderen,
+X-B: omdat dit anders
+X-C: alleen maar schijfruimte verspilt.
+
+Frop!
+
+.
+;
+
+test_set "message" "${message}";
+test "Deleteheader - nonexistent" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader "X-Z";
+
+ if size :under 288 {
+ test_fail "message is shorter than original";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder1";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder1" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in redirected mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_set "message" "${message}";
+test "Deleteheader - nonexistent (match)" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader :matches "X-Z" "*frop*";
+
+ if size :under 288 {
+ test_fail "message is shorter than original";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder1b";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder1b" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in redirected mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Deleteheader - one" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader "X-D";
+
+ if not size :under 288 {
+ test_fail "edited message is not shorter";
+ }
+
+ if size :over 258 {
+ test_fail "edited message is too long";
+ }
+
+ if size :under 258 {
+ test_fail "edited message is too short";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained";
+ }
+
+ if exists "X-D" {
+ test_fail "X-D header not deleted";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder2";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder2" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in stored mail";
+ }
+
+ if exists "X-D" {
+ test_fail "X-D header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in redirected mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in redirected mail";
+ }
+
+ if exists "X-D" {
+ test_fail "X-D header not deleted in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Deleteheader - two (first)" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader "X-A";
+
+ if not size :under 288 {
+ test_fail "edited message is not shorter";
+ }
+
+ if size :over 226 {
+ test_fail "edited message is too long";
+ }
+
+ if size :under 226 {
+ test_fail "edited message is too short";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained";
+ }
+
+ if exists "X-A" {
+ test_fail "X-A header not deleted";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder3";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder3" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in stored mail";
+ }
+
+ if exists "X-A" {
+ test_fail "X-A header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in redirected mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in redirected mail";
+ }
+
+ if exists "X-A" {
+ test_fail "X-A header not deleted in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Deleteheader - two (last)" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader "X-C";
+
+ if not size :under 288 {
+ test_fail "edited message is not shorter";
+ }
+
+ if size :over 215 {
+ test_fail "edited message is too long";
+ }
+
+ if size :under 215 {
+ test_fail "edited message is too short";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A header not retained";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained";
+ }
+
+ if exists "X-C" {
+ test_fail "X-C header not deleted";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder4";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder4" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in stored mail";
+ }
+
+ if exists "X-C" {
+ test_fail "X-C header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in redirected mail";
+ }
+
+ if exists "X-C" {
+ test_fail "X-C header not deleted in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Deleteheader - :index" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader :index 1 "X-A";
+ deleteheader :index 2 "X-C";
+
+ if not size :under 288 {
+ test_fail "edited message is not shorter";
+ }
+
+ if size :over 220 {
+ test_fail "edited message is too long";
+ }
+
+ if size :under 220 {
+ test_fail "edited message is too short";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A (2) header not retained";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C (1) header not retained";
+ }
+
+ if header :is "X-A" "Onzinnige informatie" {
+ test_fail "original X-A (1) header not deleted";
+ }
+
+ if header :is "X-C" "alleen maar schijfruimte verspilt." {
+ test_fail "original X-C (2) header not deleted";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder5";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder5" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A (2) header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C (1) header not retained in stored mail";
+ }
+
+ if header :is "X-A" "Onzinnige informatie" {
+ test_fail "original X-A (1) header not deleted in stored mail";
+ }
+
+ if header :is "X-C" "alleen maar schijfruimte verspilt." {
+ test_fail "original X-C (2) header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if not header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A (2) header not retained redirected mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-B (1) header not retained redirected mail";
+ }
+
+ if header :is "X-A" "Onzinnige informatie" {
+ test_fail "original X-A (1) header not deleted redirected mail";
+ }
+
+ if header :is "X-C" "alleen maar schijfruimte verspilt." {
+ test_fail "original X-B (2) header not deleted redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Deleteheader - :index :last" {
+ if size :over 288 {
+ test_fail "original message is longer than 288 bytes?!";
+ }
+
+ if size :under 288 {
+ test_fail "original message is shorter than 288 bytes?!";
+ }
+
+ deleteheader :index 1 :last "X-A";
+ deleteheader :last :index 2 "X-C";
+
+ if size :over 221 {
+ test_fail "edited message is too long";
+ }
+
+ if size :under 221 {
+ test_fail "edited message is too short";
+ }
+
+ if not size :under 288 {
+ test_fail "edited message is not shorter";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained";
+ }
+
+ if not header :is "X-A" "Onzinnige informatie" {
+ test_fail "original X-A (1) header not retained";
+ }
+
+ if not header :is "X-C" "alleen maar schijfruimte verspilt." {
+ test_fail "original X-C (2) header not retained";
+ }
+
+ if header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A (2) header not deleted";
+ }
+
+ if header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C (1) header not deleted";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder6";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder6" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-A" "Onzinnige informatie" {
+ test_fail "original X-A (1) header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "alleen maar schijfruimte verspilt." {
+ test_fail "original X-C (2) header not retained in stored mail";
+ }
+
+ if header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A (2) header not deleted in stored mail";
+ }
+
+ if header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C (1) header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in redirected mail";
+ }
+
+ if header :is "X-A" "dit terstond te verwijderen," {
+ test_fail "original X-A (2) header not deleted redirected mail";
+ }
+
+ if header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-B (1) header not deleted redirected mail";
+ }
+
+ if not header :is "X-A" "Onzinnige informatie" {
+ test_fail "original X-A (1) header not retained redirected mail";
+ }
+
+ if not header :is "X-C" "alleen maar schijfruimte verspilt." {
+ test_fail "original X-B (2) header not retained redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message}";
+test "Deleteheader - implicit keep" {
+ deleteheader "X-D";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "INBOX" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "subject" "Frop!" {
+ test_fail "original subject header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "omdat dit anders" {
+ test_fail "original X-B header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "niet via e-mail versturen" {
+ test_fail "original X-C header not retained in stored mail";
+ }
+
+ if exists "X-D" {
+ test_fail "X-D header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+}
+
+/*
+ *
+ */
+
+test_result_reset;
+
+test_set "message" text:
+X-A: Dit is een klein verhaaltje
+X-B: om te testen of de correcte
+X-C: informatie wordt herkend en
+X-D: verwijderd. Zo valt goed te
+X-A: zien dat het allemaal werkt
+X-B: zoals het bedoeld is. Alles
+X-C: wordt in een keer getest op
+X-D: een wijze die efficient die
+X-A: problemen naar voren brengt
+X-B: die bij dit nieuwe deel van
+X-C: de programmatuur naar voren
+X-D: kunnen komen. Zo werkt het!
+
+Frop!
+.
+;
+
+test "Deleteheader - :matches" {
+ if size :over 417 {
+ test_fail "original message is longer than 417 bytes?!";
+ }
+
+ if size :under 417 {
+ test_fail "original message is shorter than 417 bytes?!";
+ }
+
+ deleteheader :matches "X-A" "*klein*";
+ deleteheader :matches "X-B" "*bedoeld*";
+ deleteheader :matches "X-C" "*programmatuur*";
+ deleteheader :contains "X-D" ["verwijderd", "!"];
+
+ if not size :under 417 {
+ test_fail "edited message is not shorter";
+ }
+
+ if size :over 247 {
+ test_fail "edited message is too long";
+ }
+
+ if size :under 247 {
+ test_fail "edited message is too short";
+ }
+
+ if not header :is "X-A" "zien dat het allemaal werkt" {
+ test_fail "original X-A (2) header not retained";
+ }
+
+ if not header :is "X-A" "problemen naar voren brengt" {
+ test_fail "original X-A (3) header not retained";
+ }
+
+ if not header :is "X-B" "om te testen of de correcte" {
+ test_fail "original X-B (1) header not retained";
+ }
+
+ if not header :is "X-B" "die bij dit nieuwe deel van" {
+ test_fail "original X-B (3) header not retained";
+ }
+
+ if not header :is "X-C" "informatie wordt herkend en" {
+ test_fail "original X-C (1) header not retained";
+ }
+
+ if not header :is "X-C" "wordt in een keer getest op" {
+ test_fail "original X-C (2) header not retained";
+ }
+
+ if not header :is "X-D" "een wijze die efficient die" {
+ test_fail "original X-C (2) header not retained";
+ }
+
+ if header :is "X-A" "Dit is een klein verhaaltje" {
+ test_fail "original X-A (1) header not deleted";
+ }
+
+ if header :is "X-B" "zoals het bedoeld is. Alles" {
+ test_fail "original X-B (2) header not deleted";
+ }
+
+ if header :is "X-C" "de programmatuur naar voren" {
+ test_fail "original X-C (3) header not deleted";
+ }
+
+ if header :is "X-D" "verwijderd. Zo valt goed te" {
+ test_fail "original X-C (1) header not deleted";
+ }
+
+ if header :is "X-D" "kunnen komen. Zo werkt het!" {
+ test_fail "original X-C (3) header not deleted";
+ }
+
+ redirect "frop@example.com";
+ fileinto :create "folder7";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :folder "folder7" 0 {
+ test_fail "message not stored";
+ }
+
+ if not header :is "X-A" "zien dat het allemaal werkt" {
+ test_fail "original X-A (2) header not retained in stored mail";
+ }
+
+ if not header :is "X-A" "problemen naar voren brengt" {
+ test_fail "original X-A (3) header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "om te testen of de correcte" {
+ test_fail "original X-B (1) header not retained in stored mail";
+ }
+
+ if not header :is "X-B" "die bij dit nieuwe deel van" {
+ test_fail "original X-B (3) header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "informatie wordt herkend en" {
+ test_fail "original X-C (1) header not retained in stored mail";
+ }
+
+ if not header :is "X-C" "wordt in een keer getest op" {
+ test_fail "original X-C (2) header not retained in stored mail";
+ }
+
+ if not header :is "X-D" "een wijze die efficient die" {
+ test_fail "original X-C (2) header not retained in stored mail";
+ }
+
+ if header :is "X-A" "Dit is een klein verhaaltje" {
+ test_fail "original X-A (1) header not deleted in stored mail";
+ }
+
+ if header :is "X-B" "zoals het bedoeld is. Alles" {
+ test_fail "original X-B (2) header not deleted in stored mail";
+ }
+
+ if header :is "X-C" "de programmatuur naar voren" {
+ test_fail "original X-C (3) header not deleted in stored mail";
+ }
+
+ if header :is "X-D" "verwijderd. Zo valt goed te" {
+ test_fail "original X-C (1) header not deleted in stored mail";
+ }
+
+ if header :is "X-D" "kunnen komen. Zo werkt het!" {
+ test_fail "original X-C (3) header not deleted in stored mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in stored mail";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not header :is "X-A" "zien dat het allemaal werkt" {
+ test_fail "original X-A (2) header not retained in redirected mail";
+ }
+
+ if not header :is "X-A" "problemen naar voren brengt" {
+ test_fail "original X-A (3) header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "om te testen of de correcte" {
+ test_fail "original X-B (1) header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "die bij dit nieuwe deel van" {
+ test_fail "original X-B (3) header not retained in redirected mail";
+ }
+
+ if not header :is "X-C" "informatie wordt herkend en" {
+ test_fail "original X-C (1) header not retained in redirected mail";
+ }
+
+ if not header :is "X-C" "wordt in een keer getest op" {
+ test_fail "original X-C (2) header not retained in redirected mail";
+ }
+
+ if not header :is "X-D" "een wijze die efficient die" {
+ test_fail "original X-C (2) header not retained in redirected mail";
+ }
+
+ if header :is "X-A" "Dit is een klein verhaaltje" {
+ test_fail "original X-A (1) header not deleted in redirected mail";
+ }
+
+ if header :is "X-B" "zoals het bedoeld is. Alles" {
+ test_fail "original X-B (2) header not deleted in redirected mail";
+ }
+
+ if header :is "X-C" "de programmatuur naar voren" {
+ test_fail "original X-C (3) header not deleted in redirected mail";
+ }
+
+ if header :is "X-D" "verwijderd. Zo valt goed te" {
+ test_fail "original X-C (1) header not deleted in redirected mail";
+ }
+
+ if header :is "X-D" "kunnen komen. Zo werkt het!" {
+ test_fail "original X-C (3) header not deleted in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+
+/*
+ *
+ */
+
+set "message2" text:
+X-A: Long folded header to test removal of folded
+ headers from a message. This is the top header.
+X-B: First intermittent unfolded header
+X-A: Long folded header to test removal of folded
+ headers from a message. This is the middle header.
+X-B: Second intermittent unfolded header
+X-A: Long folded header to test removal of folded
+ headers from a message. This is the bottom header,
+ which concludes the header of this message.
+
+Frop!
+.
+;
+
+test_result_reset;
+test_set "message" "${message2}";
+test "Deleteheader - folded" {
+ deleteheader "X-A";
+
+ if exists "X-A" {
+ test_fail "original X-A (1) header not deleted";
+ }
+
+ if not header :is "X-B" "First intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained";
+ }
+
+ if not header :is "X-B" "Second intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+
+ redirect "frop@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if exists "X-A" {
+ test_fail "original X-A (1) header not deleted in redirected mail";
+ }
+
+ if not header :is "X-B" "First intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "Second intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+test_result_reset;
+test_set "message" "${message2}";
+test "Deleteheader - folded (match)" {
+ deleteheader :matches "X-A" "*header*";
+
+ if exists "X-A" {
+ test_fail "original X-A (1) header not deleted";
+ }
+
+ if not header :is "X-B" "First intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained";
+ }
+
+ if not header :is "X-B" "Second intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+
+ redirect "frop@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if exists "X-A" {
+ test_fail "original X-A (1) header not deleted in redirected mail";
+ }
+
+ if not header :is "X-B" "First intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained in redirected mail";
+ }
+
+ if not header :is "X-B" "Second intermittent unfolded header" {
+ test_fail "original X-B (2) header not retained in redirected mail";
+ }
+
+ if not body :matches "Frop!*" {
+ test_fail "body not retained in redirected mail";
+ }
+}
+
+
+/*
+ * TEST: Ignoring whitespace
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+Subject: Help
+X-A: Text
+X-B: Text
+
+Text
+.
+;
+
+test "Ignoring whitespace" {
+ deleteheader :is "subject" "Help";
+ deleteheader :is "x-a" "Text";
+ deleteheader :is "x-b" "Text";
+
+ if exists "subject" {
+ test_fail "subject header not deleted";
+ }
+
+ if exists "x-a" {
+ test_fail "x-a header not deleted";
+ }
+
+ if exists "x-b" {
+ test_fail "x-b header not deleted";
+ }
+}
+
+/*
+ * TEST: Interaction with body test
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+Subject: Hoppa
+
+Text
+.
+;
+
+test "Interaction with body test" {
+ addheader "X-Frop" "frop";
+
+ if body "!TEST!" {}
+
+ deleteheader "subject";
+
+ if exists "subject" {
+ test_fail "subject header not deleted";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/editheader/errors.svtest b/pigeonhole/tests/extensions/editheader/errors.svtest
new file mode 100644
index 0000000..1d1f24d
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors.svtest
@@ -0,0 +1,164 @@
+require "vnd.dovecot.testsuite";
+require "comparator-i;ascii-numeric";
+require "relational";
+require "variables";
+
+require "editheader";
+
+test "Invalid field-name" {
+ if test_script_compile "errors/field-name.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";
+ }
+
+ if not test_error :index 1 :matches "*field name*X-field:*invalid*" {
+ test_fail "wrong error reported (1)";
+ }
+
+ if not test_error :index 2 :matches "*field name*X field*invalid*" {
+ test_fail "wrong error reported (2)";
+ }
+
+ if not test_error :index 3 :matches "*field name*X-field:*invalid*" {
+ test_fail "wrong error reported (3)";
+ }
+
+ if not test_error :index 4 :matches "*field name*X field*invalid*" {
+ test_fail "wrong error reported (4)";
+ }
+}
+
+test "Invalid field-name at runtime " {
+ if not test_script_compile "errors/field-name-runtime.sieve" {
+ test_fail "compile failed";
+ }
+
+ if test_script_run {
+ test_fail "run should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of errors reported";
+ }
+
+ if not test_error :matches "*field name*X-field:*invalid*" {
+ test_fail "wrong error reported";
+ }
+}
+
+test "Invalid field value" {
+ if test_script_compile "errors/field-value.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+
+ if not test_error :index 1 :matches "*value*Woah*invalid*" {
+ test_fail "wrong error reported (1): ${0}";
+ }
+}
+
+test "Command syntax (FIXME: count only)" {
+ if test_script_compile "errors/command-syntax.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "10" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * TEST - Size limit
+ */
+
+test "Size limit" {
+ if not test_script_compile "errors/size-limit.sieve" {
+ test_fail "compile should have succeeded";
+ }
+
+ test_config_set "sieve_editheader_max_header_size" "1024";
+ test_config_reload :extension "editheader";
+
+ if test_script_compile "errors/size-limit.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+
+/*
+ * TEST - Size limit at runtime
+ */
+
+test_config_set "sieve_editheader_max_header_size" "";
+test_config_reload :extension "editheader";
+
+test "Size limit at runtime" {
+ if not test_script_compile "errors/size-limit-runtime.sieve" {
+ test_fail "compile should have succeeded";
+ }
+
+ if not test_script_run {
+ test_fail "run failed";
+ }
+
+ test_config_set "sieve_editheader_max_header_size" "1024";
+ test_config_reload :extension "editheader";
+
+ if not test_script_compile "errors/size-limit-runtime.sieve" {
+ test_fail "compile should have succeeded";
+ }
+
+ if test_script_run {
+ test_fail "run should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * TEST - Implicit keep at runtime error
+ */
+
+test_set "message" text:
+From: stephan@example.com
+To: tss@example.com
+Subject: Frop
+
+Frop!
+.
+;
+
+test "Implicit keep at runtime error" {
+ if not test_script_compile "errors/runtime-error.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "run failed";
+ }
+
+ if test_result_execute {
+ test_fail "result execution should have failed";
+ }
+
+ if not test_message :folder "INBOX" 0 {
+ test_fail "message not stored (no implicit keep)";
+ }
+
+ if exists "X-Frop" {
+ test_fail "implicit keep message has editheader changes";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/editheader/errors/command-syntax.sieve b/pigeonhole/tests/extensions/editheader/errors/command-syntax.sieve
new file mode 100644
index 0000000..8543e6d
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/command-syntax.sieve
@@ -0,0 +1,42 @@
+require "editheader";
+
+/* "addheader" [":last"] <field-name: string> <value: string>
+ */
+
+# 1: missing field name and value
+addheader;
+
+# 2: missing value
+addheader "x-frop";
+
+# 3: value not a string; number
+addheader "x-frop" 2;
+
+# 4: value not a string; list
+addheader "x-frop" ["frop"];
+
+# 5: strange tag
+addheader :tag "x-frop" "frop";
+
+/* "deleteheader" [":index" <fieldno: number> [":last"]]
+ * [COMPARATOR] [MATCH-TYPE]
+ * <field-name: string>
+ * [<value-patterns: string-list>]
+ */
+
+# 6: missing field name
+deleteheader;
+
+# 7: :last tag without index
+deleteheader :last "x-frop";
+
+# 8: :index tag with string argument
+deleteheader :index "frop" "x-frop";
+
+# OK: match type without value patterns
+deleteheader :matches "x-frop";
+
+# 9: value patterns not a string(list)
+deleteheader "x-frop" 1;
+
+
diff --git a/pigeonhole/tests/extensions/editheader/errors/field-name-runtime.sieve b/pigeonhole/tests/extensions/editheader/errors/field-name-runtime.sieve
new file mode 100644
index 0000000..3f34461
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/field-name-runtime.sieve
@@ -0,0 +1,6 @@
+require "editheader";
+require "variables";
+
+set "header" "X-field:";
+
+addheader "${header}" "Frop";
diff --git a/pigeonhole/tests/extensions/editheader/errors/field-name.sieve b/pigeonhole/tests/extensions/editheader/errors/field-name.sieve
new file mode 100644
index 0000000..469bfc8
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/field-name.sieve
@@ -0,0 +1,19 @@
+require "editheader";
+
+# Ok
+addheader "X-field" "Frop";
+
+# Invalid ':'
+addheader "X-field:" "Frop";
+
+# Invalid ' '
+addheader "X field" "Frop";
+
+# Ok
+deleteheader "X-field";
+
+# Invalid ':'
+deleteheader "X-field:";
+
+# Invalid ' '
+deleteheader "X field";
diff --git a/pigeonhole/tests/extensions/editheader/errors/field-value.sieve b/pigeonhole/tests/extensions/editheader/errors/field-value.sieve
new file mode 100644
index 0000000..c9f4eab
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/field-value.sieve
@@ -0,0 +1,15 @@
+require "editheader";
+require "encoded-character";
+
+# Ok
+addheader "X-field" "Frop";
+
+# Ok
+addheader "X-field" "Frop
+Frml";
+
+# Invalid 'BELL'; but not an error
+addheader "X-field" "Yeah${hex:07}!";
+
+# Invalid 'NUL'
+addheader "X-field" "Woah${hex:00}!";
diff --git a/pigeonhole/tests/extensions/editheader/errors/runtime-error.sieve b/pigeonhole/tests/extensions/editheader/errors/runtime-error.sieve
new file mode 100644
index 0000000..b308d74
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/runtime-error.sieve
@@ -0,0 +1,6 @@
+require "editheader";
+require "fileinto";
+
+addheader "X-Frop" "Friep";
+
+fileinto "Rediculous.non-existent.folder";
diff --git a/pigeonhole/tests/extensions/editheader/errors/size-limit-runtime.sieve b/pigeonhole/tests/extensions/editheader/errors/size-limit-runtime.sieve
new file mode 100644
index 0000000..73a1437
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/size-limit-runtime.sieve
@@ -0,0 +1,46 @@
+require "editheader";
+require "variables";
+
+set "blob" text:
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+.
+;
+
+addheader "x-frop" "${blob}";
diff --git a/pigeonhole/tests/extensions/editheader/errors/size-limit.sieve b/pigeonhole/tests/extensions/editheader/errors/size-limit.sieve
new file mode 100644
index 0000000..598f5f9
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/errors/size-limit.sieve
@@ -0,0 +1,43 @@
+require "editheader";
+
+addheader "x-frop" text:
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+.
+;
diff --git a/pigeonhole/tests/extensions/editheader/execute.svtest b/pigeonhole/tests/extensions/editheader/execute.svtest
new file mode 100644
index 0000000..e65cc5d
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/execute.svtest
@@ -0,0 +1,57 @@
+require "vnd.dovecot.testsuite";
+require "include";
+require "variables";
+require "editheader";
+
+/*
+ * Testsuite self-test
+ */
+
+set "message" ".";
+addheader "X-Some-Header" "Header content";
+test_result_reset;
+test_set "message" "${message}";
+
+/*
+ * Multi script
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: idiot@example.com
+To: idiot@example.org
+Subject: Frop!
+
+Frop.
+.
+;
+
+test_result_reset;
+test "Multi script" {
+ if not test_multiscript [
+ "execute/multiscript-before.sieve",
+ "execute/multiscript-personal.sieve",
+ "execute/multiscript-after.sieve"
+ ] {
+ test_fail "failed to run all scripts";
+ }
+
+ test_message :folder "INBOX" 0;
+
+ if not header "subject" "Frop!" {
+ test_fail "keep not executed.";
+ }
+
+ if not header "X-Before" "before" {
+ test_fail "No X-Before header";
+ }
+
+ if not header "X-Personal" "personal" {
+ test_fail "No X-Personal header";
+ }
+
+ if not header "X-After" "after" {
+ test_fail "No X-After header";
+ }
+}
diff --git a/pigeonhole/tests/extensions/editheader/execute/multiscript-after.sieve b/pigeonhole/tests/extensions/editheader/execute/multiscript-after.sieve
new file mode 100644
index 0000000..f11f02d
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/execute/multiscript-after.sieve
@@ -0,0 +1,4 @@
+require "editheader";
+
+addheader "X-After" "after";
+
diff --git a/pigeonhole/tests/extensions/editheader/execute/multiscript-before.sieve b/pigeonhole/tests/extensions/editheader/execute/multiscript-before.sieve
new file mode 100644
index 0000000..5c8a988
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/execute/multiscript-before.sieve
@@ -0,0 +1,4 @@
+require "editheader";
+
+addheader "X-Before" "before";
+
diff --git a/pigeonhole/tests/extensions/editheader/execute/multiscript-personal.sieve b/pigeonhole/tests/extensions/editheader/execute/multiscript-personal.sieve
new file mode 100644
index 0000000..92e82ac
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/execute/multiscript-personal.sieve
@@ -0,0 +1,4 @@
+require "editheader";
+
+addheader "X-Personal" "personal";
+
diff --git a/pigeonhole/tests/extensions/editheader/protected.svtest b/pigeonhole/tests/extensions/editheader/protected.svtest
new file mode 100644
index 0000000..148a9c8
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/protected.svtest
@@ -0,0 +1,173 @@
+require "vnd.dovecot.testsuite";
+
+require "variables";
+require "encoded-character";
+require "editheader";
+
+set "message" text:
+Received: by example.com (Postfix, from userid 202)
+ id 32A131WFW23QWE4; Mon, 21 Nov 2011 05:25:26 +0200 (EET)
+Delivery-date: Mon, 21 Nov 2011 04:26:04 +0100
+Auto-Submitted: yes
+X-Friep: frop 3
+Subject: Frop!
+From: stephan@example.com
+To: tss@example.com
+
+Frop!
+.
+;
+
+test_set "message" "${message}";
+test "Default protected" {
+ if not exists "received" {
+ test_fail "received header did not exist in the first place";
+ }
+
+ if not exists "auto-submitted" {
+ test_fail "auto-submitted header did not exist in the first place";
+ }
+
+ deleteheader "received";
+ deleteheader "auto-submitted";
+ deleteheader "subject";
+
+ if not exists "received" {
+ test_fail "protected received header was deleted";
+ }
+
+ if not exists "auto-submitted" {
+ test_fail "protected auto-submitted header was deleted";
+ }
+
+ if exists "subject" {
+ test_fail "subject header cannot be protected, but it was not deleted";
+ }
+}
+
+test_config_set "sieve_editheader_protected" "subject delivery-date x-frop";
+test_config_reload :extension "editheader";
+
+test_set "message" "${message}";
+test "Configured protected" {
+ if not exists "delivery-date" {
+ test_fail "received header did not exist in the first place";
+ }
+
+ if not exists "subject" {
+ test_fail "received header did not exist in the first place";
+ }
+
+ if exists "x-frop" {
+ test_fail "x-frop header already present";
+ }
+
+ deleteheader "delivery-date";
+ deleteheader "subject";
+ addheader "x-frop" "Frop!";
+
+ if not exists "delivery-date" {
+ test_fail "protected delivery-date header was deleted";
+ }
+
+ if exists "subject" {
+ test_fail "subject header cannot be protected, but it was not deleted";
+ }
+
+ if exists "x-frop" {
+ test_fail "protected x-frop header was added";
+ }
+}
+
+test_config_set "sieve_editheader_protected" "";
+test_config_set "sieve_editheader_forbid_add" "subject x-frop";
+test_config_set "sieve_editheader_forbid_delete" "subject x-friep";
+test_config_reload :extension "editheader";
+
+test_set "message" "${message}";
+test "Configured forbid_add/forbid_delete" {
+ if not exists "delivery-date" {
+ test_fail "received header did not exist in the first place";
+ }
+
+ if not exists "subject" {
+ test_fail "received header did not exist in the first place";
+ }
+
+ if not exists "x-friep" {
+ test_fail "x-friep header did not exist in the first place";
+ }
+
+ if exists "x-frop" {
+ test_fail "x-frop header already present";
+ }
+
+ deleteheader "delivery-date";
+ deleteheader "subject";
+ deleteheader "x-friep";
+
+ if exists "delivery-date" {
+ test_fail "unprotected delivery-date header was not deleted";
+ }
+
+ if exists "subject" {
+ test_fail "subject header cannot be protected, but it was not deleted";
+ }
+
+ if not exists "x-friep" {
+ test_fail "protected x-friep header was deleted";
+ }
+
+ addheader "delivery-date" "Yesterday";
+ addheader "subject" "Fropfrop!";
+ addheader "x-frop" "Frop!";
+ addheader "received" text:
+by sieve.example.com (My little Sieve script)
+id 3jhl22khhf23f; Mon, 24 Aug 2015 04:11:54 -0600;
+.
+;
+ addheader "auto-submitted" "no way";
+
+ if not header "delivery-date" "Yesterday" {
+ test_fail "unprotected delivery-date header was not added";
+ }
+
+ if not header "subject" "Fropfrop!" {
+ test_fail "subject header cannot be protected, but it was not added";
+ }
+
+ if exists "x-frop" {
+ test_fail "protected x-frop header was added";
+ }
+
+ if not header :contains "received" "sieve.example.com" {
+ test_fail "received header was not added";
+ }
+
+ if not header "auto-submitted" "no way" {
+ test_fail "autosubmitted header was not added";
+ }
+}
+
+/*
+ * TEST - Bad header configuration
+ */
+
+test_config_set "sieve_editheader_protected" "${unicode:1F4A9} delivery-date";
+test_config_reload :extension "editheader";
+
+test_set "message" "${message}";
+test "Bad header configuration" {
+ if not exists "delivery-date" {
+ test_fail "delivery-date header did not exist in the first place";
+ }
+
+ deleteheader "delivery-date";
+
+ if not exists "delivery-date" {
+ test_fail "protected delivery-date header was deleted";
+ }
+}
+
+test_config_set "sieve_editheader_protected" "";
+test_config_reload :extension "editheader";
diff --git a/pigeonhole/tests/extensions/editheader/utf8.svtest b/pigeonhole/tests/extensions/editheader/utf8.svtest
new file mode 100644
index 0000000..159a71c
--- /dev/null
+++ b/pigeonhole/tests/extensions/editheader/utf8.svtest
@@ -0,0 +1,97 @@
+require "vnd.dovecot.testsuite";
+
+require "encoded-character";
+require "variables";
+require "editheader";
+
+test_set "message" text:
+Subject: Frop!
+From: stephan@example.com
+To: stephan@example.com
+
+Frop!
+.
+;
+
+test "UTF8 - add; get" {
+ set "comment" "Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test";
+
+ addheader "Comment" "${comment}";
+
+ if not exists "comment" {
+ test_fail "header not added";
+ }
+
+ if not header :is "comment" "${comment}" {
+ test_fail "wrong content added/retrieved";
+ }
+
+ redirect "frop@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ /* redirected message */
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not exists "comment" {
+ test_fail "header not added in redirected mail";
+ }
+
+ if not header :is "comment" "${comment}" {
+ test_fail "wrong content added/retrieved from redirected mail";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+Subject: Frop!
+Comment: Ein =?utf-8?q?unerh=C3=B6rt_gro=C3=9Fer?= Test
+X-Spam: no
+From: stephan@example.com
+To: stephan@example.com
+
+Frop!
+.
+;
+
+test "UTF8 - existing; delete other; get" {
+ set "comment" "Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test";
+
+ deleteheader "x-spam";
+
+ if not exists "comment" {
+ test_fail "header not present";
+ }
+
+ if not header :is "comment" "${comment}" {
+ test_fail "wrong content retrieved";
+ }
+
+ redirect "frop@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ /* redirected message */
+
+ if not test_message :smtp 0 {
+ test_fail "message not redirected";
+ }
+
+ if not exists "comment" {
+ test_fail "header not present in redirected mail";
+ }
+
+ if not header :is "comment" "${comment}" {
+ test_fail "wrong content retrieved from redirected mail";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/encoded-character.svtest b/pigeonhole/tests/extensions/encoded-character.svtest
new file mode 100644
index 0000000..150d812
--- /dev/null
+++ b/pigeonhole/tests/extensions/encoded-character.svtest
@@ -0,0 +1,180 @@
+require "vnd.dovecot.testsuite";
+
+require "encoded-character";
+require "variables";
+
+test "HEX equality one" {
+ if not string "${hex:42}" "B" {
+ test_fail "failed to match the string 'B'";
+ }
+
+ if string "${hex:42}" "b" {
+ test_fail "matched nonsense";
+ }
+
+ if string "${hex:42}" "" {
+ test_fail "substitution failed";
+ }
+}
+
+test "HEX equality one middle" {
+ if not string " ${hex:42} " " B " {
+ test_fail "failed to match the string ' B '";
+ }
+
+ if string " ${hex:42} " " b " {
+ test_fail "matched nonsense";
+ }
+
+ if string " ${hex:42} " " " {
+ test_fail "substitution failed";
+ }
+}
+
+test "HEX equality one begin" {
+ if not string "${hex:42} " "B " {
+ test_fail "failed to match the string 'B '";
+ }
+
+ if string "${hex:42} " " b" {
+ test_fail "matched nonsense";
+ }
+
+ if string "${hex:42} " " " {
+ test_fail "substitution failed";
+ }
+}
+
+test "HEX equality one end" {
+ if not string " ${hex:42}" " B" {
+ test_fail "failed to match the string ' B'";
+ }
+
+ if string " ${hex:42}" " b " {
+ test_fail "matched nonsense";
+ }
+
+ if string " ${hex:42}" " " {
+ test_fail "substitution failed";
+ }
+}
+
+test "HEX equality two triple" {
+ if not string "${hex:42 61 64}${hex: 61 73 73}" "Badass" {
+ test_fail "failed to match the string 'Badass'";
+ }
+
+ if string "${hex:42 61 64}${hex: 61 73 73}" "Sadass" {
+ test_fail "matched nonsense";
+ }
+
+ if string "${hex:42 61 64}${hex: 61 73 73}" "" {
+ test_fail "substitution failed";
+ }
+}
+
+test "HEX equality braindead" {
+ if not string "${hex:42 72 61 69 6E 64 65 61 64}" "Braindead" {
+ test_fail "failed to match the string 'Braindead'";
+ }
+
+ if string "${hex:42 72 61 69 6E 64 65 61 64}" "Brian Nut" {
+ test_fail "matched nonsense";
+ }
+}
+
+test "Syntax errors" {
+ if anyof( not string "$" "${hex:24}", not string "$ " "${hex:24} ", not string " $" " ${hex:24}" ) {
+ test_fail "loose $ handled inappropriately";
+ }
+
+ if anyof( not string "${" "${hex:24}{", not string "a${" "a${hex:24}{", not string "${a" "${hex:24}{a" ) {
+ test_fail "loose ${ handled inappropriately";
+ }
+
+ if anyof( not string "${}" "${hex:24}{}", not string "b${}" "b${hex:24}{}", not string "${}b" "${hex:24}{}b" ) {
+ test_fail "entirely missing content handled inappropriately";
+ }
+
+ if not string "${:}" "${hex:24}{:}" {
+ test_fail "missing content handled inappropriately";
+ }
+
+ if not string "${hex:}" "${hex:24}{hex:}" {
+ test_fail "missing hex content handled inappropriately";
+ }
+
+ if not string "${unicode:}" "${hex:24}{unicode:}" {
+ test_fail "missing unicode content handled inappropriately";
+ }
+
+ if not string "${hex:sss}" "${hex:24}{hex:sss}" {
+ test_fail "erroneous hex content handled inappropriately";
+ }
+
+ if not string "${unicode:ttt}" "${hex:24}{unicode:ttt}" {
+ test_fail "erroneous unicode content handled inappropriately";
+ }
+
+ if not string "${hex:aa aa" "${hex:24}{hex:aa aa" {
+ test_fail "unterminated hex content handled inappropriately";
+ }
+
+ if not string "${unicode: aaaa aaaa" "${hex:24}{unicode: aaaa aaaa" {
+ test_fail "unterminated unicode content handled inappropriately";
+ }
+}
+
+/*
+ * RFC Examples
+ */
+
+test "RFC Examples" {
+ if not string "$${hex:40}" "$@" {
+ test_fail "failed RFC example 1";
+ }
+
+ if not string "${hex: 40 }" "@" {
+ test_fail "failed RFC example 2";
+ }
+
+ if not string "${HEX: 40}" "@" {
+ test_fail "failed RFC example 3";
+ }
+
+ if not string "${hex:40" "${hex:40" {
+ test_fail "failed RFC example 4";
+ }
+
+ if not string "${hex:400}" "${hex:400}" {
+ test_fail "failed RFC example 5";
+ }
+
+ if not string "${hex:4${hex:30}}" "${hex: 24}{hex:40}" {
+ test_fail "failed RFC example 6";
+ }
+
+ if not string "${unicode:40}" "@" {
+ test_fail "failed RFC example 7";
+ }
+
+ if not string "${ unicode:40}" "${ unicode:40}" {
+ test_fail "failed RFC example 8";
+ }
+
+ if not string "${UNICODE:40}" "@" {
+ test_fail "failed RFC example 9";
+ }
+
+ if not string "${UnICoDE:0000040}" "@" {
+ test_fail "failed RFC example 10";
+ }
+
+ if not string "${Unicode:40}" "@" {
+ test_fail "failed RFC example 11";
+ }
+
+ if not string "${Unicode:Cool}" "${Unicode:Cool}" {
+ test_fail "failed RFC example 12";
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/basic.svtest b/pigeonhole/tests/extensions/enotify/basic.svtest
new file mode 100644
index 0000000..2a03aee
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/basic.svtest
@@ -0,0 +1,15 @@
+require "vnd.dovecot.testsuite";
+require "enotify";
+
+test "Execute" {
+ /* Test to catch runtime segfaults */
+ if valid_notify_method
+ "mailto:stephan@example.com" {
+
+ /* Test to catch runtime segfaults */
+ notify
+ :message "This is probably very important"
+ :importance "1"
+ "mailto:stephan@example.com%2cstephan@example.org?subject=Important%20message%20received";
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/encodeurl.svtest b/pigeonhole/tests/extensions/enotify/encodeurl.svtest
new file mode 100644
index 0000000..d334dd3
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/encodeurl.svtest
@@ -0,0 +1,359 @@
+require "vnd.dovecot.testsuite";
+require "encoded-character";
+require "variables";
+require "enotify";
+
+/*
+ * :encodeurl simple
+ */
+
+test ":encodeurl simple" {
+ set :encodeurl "url_data" "\\frop\\&fruts/^@";
+
+ if not string :is :comparator "i;octet" "${url_data}" "%5Cfrop%5C%26fruts%2F%5E%40" {
+ test_fail "url data encoded incorrectly '${url_data}'";
+ }
+}
+
+/*
+ * :encodeurl variable size limit
+ */
+
+test_config_set "sieve_variables_max_variable_size" "4000";
+test_config_reload :extension "variables";
+
+set "a" text:
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+@@@@@@@@@@@@@@@@@@@@@@@
+.
+;
+
+test ":encodeurl variable size limit" {
+ set :length "alen" "${a}";
+
+ if not string "${alen}" "4000" {
+ test_fail "variable 'a' not 4000 bytes long (${alen})";
+ }
+
+ set :encodeurl "b" "${a}";
+ set :length "blen" "${b}";
+
+ if not string "${blen}" "3999" {
+ test_fail "variable 'b' not 3999 bytes long (${blen})";
+ }
+
+ set :encodeurl "c" "0${a}";
+ set :length "clen" "${c}";
+
+ if not string "${clen}" "4000" {
+ test_fail "variable 'c' not 4000 bytes long (${clen})";
+ }
+
+ set "cmt" "%40%40%40%40%40%40%40%40%40%40%40%40";
+ set "cmt" "${cmt}%40%40%40%40%40%40%40%40%40%40%40%0D%0A";
+ set "cmh" "${cmt}${cmt}${cmt}${cmt}";
+ set "cm" "${cmh}${cmh}${cmh}${cmh}${cmh}${cmh}${cmh}${cmh}${cmh}${cmh}";
+ set "cm" "${cm}${cmh}${cmh}${cmh}";
+ set "cm" "0${cm}${cmt}%40%40%40%40%40%40%40%40";
+
+ if not string :is "${c}" "${cm}" {
+ test_fail "variable 'c' has unexpected value";
+ }
+
+ set :encodeurl "d" "00${a}";
+ set :length "dlen" "${d}";
+
+ if not string "${dlen}" "3998" {
+ test_fail "variable 'd' not 3998 bytes long (${dlen})";
+ }
+}
+
+/*
+ * :encodeurl variable size limit UTF-8
+ */
+
+test_config_set "sieve_variables_max_variable_size" "4000";
+test_config_reload :extension "variables";
+
+set "a" text:
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}${unicode:4e03}
+.
+;
+
+test ":encodeurl variable size limit UTF-8" {
+ set :length "alen" "${a}";
+
+ if not string "${alen}" "546" {
+ test_fail "variable 'a' not 549 characters long (${alen})";
+ }
+
+ set :encodeurl "b" "${a}";
+ set :length "blen" "${b}";
+
+ if not string "${blen}" "3978" {
+ test_fail "variable 'b' not 3978 bytes long (${blen})";
+ }
+
+ set :encodeurl "c" "${a}${unicode:4e00}${unicode:4e00}";
+ set :length "clen" "${c}";
+
+ if not string "${clen}" "3996" {
+ test_fail "variable 'c' not 3996 bytes long (${clen})";
+ }
+
+ set :encodeurl "d" "${a}${unicode:4e00}${unicode:4e00}${unicode:4e00}";
+ set :length "dlen" "${d}";
+
+ if not string "${dlen}" "3996" {
+ test_fail "variable 'd' not 3996 bytes long (${dlen})";
+ }
+
+ set :encodeurl "e" "0000${a}${unicode:4e00}${unicode:4e00}";
+ set :length "elen" "${e}";
+
+ if not string "${elen}" "4000" {
+ test_fail "variable 'e' not 4000 bytes long (${elen})";
+ }
+
+ set :encodeurl "f" "00000${a}${unicode:4e00}${unicode:4e00}";
+ set :length "flen" "${f}";
+
+ if not string "${flen}" "3992" {
+ test_fail "variable 'f' not 3992 bytes long (${flen})";
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/errors.svtest b/pigeonhole/tests/extensions/enotify/errors.svtest
new file mode 100644
index 0000000..5af36df
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/errors.svtest
@@ -0,0 +1,45 @@
+require "vnd.dovecot.testsuite";
+require "comparator-i;ascii-numeric";
+require "relational";
+
+require "enotify";
+
+test "Invalid URI (FIXME: count only)" {
+ if test_script_compile "errors/uri.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Invalid mailto URI (FIXME: count only)" {
+ if test_script_compile "errors/uri-mailto.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "7" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Invalid mailto :from address (FIXME: count only)" {
+ if test_script_compile "errors/from-mailto.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Invalid :options argument (FIXME: count only)" {
+ if test_script_compile "errors/options.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "6" {
+ test_fail "wrong number of errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/errors/from-mailto.sieve b/pigeonhole/tests/extensions/enotify/errors/from-mailto.sieve
new file mode 100644
index 0000000..d519256
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/errors/from-mailto.sieve
@@ -0,0 +1,7 @@
+require "enotify";
+
+# 1: Invalid from address
+notify :from "stephan#example.org" "mailto:stephan@example.com";
+
+# 2: Empty from address
+notify :from "" "mailto:stephan@example.com";
diff --git a/pigeonhole/tests/extensions/enotify/errors/options.sieve b/pigeonhole/tests/extensions/enotify/errors/options.sieve
new file mode 100644
index 0000000..58d2265
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/errors/options.sieve
@@ -0,0 +1,18 @@
+require "enotify";
+
+# 1: empty option
+notify :options "" "mailto:stephan@example.org";
+
+# 2: invalid option name syntax
+notify :options "frop" "mailto:stephan@example.org";
+
+# 3: invalid option name syntax
+notify :options "_frop=" "mailto:stephan@example.org";
+
+# 4: invalid option name syntax
+notify :options "=frop" "mailto:stephan@example.org";
+
+# 5: invalid value
+notify :options "frop=frml
+frop" "mailto:stephan@example.org";
+
diff --git a/pigeonhole/tests/extensions/enotify/errors/uri-mailto.sieve b/pigeonhole/tests/extensions/enotify/errors/uri-mailto.sieve
new file mode 100644
index 0000000..2aced86
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/errors/uri-mailto.sieve
@@ -0,0 +1,20 @@
+require "enotify";
+
+# 1: Invalid character in to part
+notify "mailto:stephan@example.org;?header=frop";
+
+# 2: Invalid character in hname
+notify "mailto:stephan@example.org?header<=frop";
+
+# 3: Invalid character in hvalue
+notify "mailto:stephan@example.org?header=fr>op";
+
+# 4: Invalid header name
+notify "mailto:stephan@example.org?header:=frop";
+
+# 5: Invalid recipient
+notify "mailto:stephan%23example.org";
+
+# 6: Invalid to header recipient
+notify "mailto:stephan@example.org?to=nico%23frop.example.org";
+
diff --git a/pigeonhole/tests/extensions/enotify/errors/uri.sieve b/pigeonhole/tests/extensions/enotify/errors/uri.sieve
new file mode 100644
index 0000000..13ead81
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/errors/uri.sieve
@@ -0,0 +1,5 @@
+require "enotify";
+
+# 1: Invalid url scheme
+notify "snailto:stephan@example.org";
+
diff --git a/pigeonhole/tests/extensions/enotify/execute.svtest b/pigeonhole/tests/extensions/enotify/execute.svtest
new file mode 100644
index 0000000..cd6486d
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute.svtest
@@ -0,0 +1,99 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+
+
+/*
+ * Execution testing (currently just meant to trigger any segfaults)
+ */
+
+test "RFC Example 1" {
+ if not test_script_compile "execute/draft-rfc-ex1.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+test "RFC Example 2" {
+ if not test_script_compile "execute/draft-rfc-ex2.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+/* tel: not supported
+test "RFC Example 3" {
+ if not test_script_compile "execute/draft-rfc-ex3.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+*/
+
+/* tel: and xmmp: not supported
+test "RFC Example 5" {
+ if not test_script_compile "execute/draft-rfc-ex5.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+*/
+
+test "RFC Example 6" {
+ if not test_script_compile "execute/draft-rfc-ex6.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+test "Duplicate recipients" {
+ if not test_script_compile "execute/duplicates.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if test_result_action :count "ne" "2" {
+ test_fail "second notify action was discarded entirely";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex1.sieve b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex1.sieve
new file mode 100644
index 0000000..6747d7b
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex1.sieve
@@ -0,0 +1,26 @@
+require ["enotify", "fileinto", "variables"];
+
+if header :contains "from" "boss@example.org" {
+ notify :importance "1"
+ :message "This is probably very important"
+ "mailto:alm@example.com";
+ # Don't send any further notifications
+ stop;
+}
+
+if header :contains "to" "sievemailinglist@example.org" {
+ # :matches is used to get the value of the Subject header
+ if header :matches "Subject" "*" {
+ set "subject" "${1}";
+ }
+
+ # :matches is used to get the value of the From header
+ if header :matches "From" "*" {
+ set "from" "${1}";
+ }
+
+ notify :importance "3"
+ :message "[SIEVE] ${from}: ${subject}"
+ "mailto:alm@example.com";
+ fileinto "INBOX.sieve";
+}
diff --git a/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex2.sieve b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex2.sieve
new file mode 100644
index 0000000..a5c6a26
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex2.sieve
@@ -0,0 +1,22 @@
+require ["enotify", "fileinto", "variables", "envelope"];
+
+if header :matches "from" "*@*.example.org" {
+ # :matches is used to get the MAIL FROM address
+ if envelope :all :matches "from" "*" {
+ set "env_from" " [really: ${1}]";
+ }
+
+ # :matches is used to get the value of the Subject header
+ if header :matches "Subject" "*" {
+ set "subject" "${1}";
+ }
+
+ # :matches is used to get the address from the From header
+ if address :matches :all "from" "*" {
+ set "from_addr" "${1}";
+ }
+
+ notify :message "${from_addr}${env_from}: ${subject}"
+ "mailto:alm@example.com";
+}
+
diff --git a/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex3.sieve b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex3.sieve
new file mode 100644
index 0000000..a7b4a64
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex3.sieve
@@ -0,0 +1,31 @@
+require ["enotify", "variables"];
+
+set "notif_method"
+ "xmpp:tim@example.com?message;subject=SIEVE;body=You%20got%20mail";
+
+if header :contains "subject" "Your dog" {
+ set "notif_method" "tel:+14085551212";
+}
+
+if header :contains "to" "sievemailinglist@example.org" {
+ set "notif_method" "";
+}
+
+if not string :is "${notif_method}" "" {
+ notify "${notif_method}";
+}
+
+if header :contains "from" "boss@example.org" {
+ # :matches is used to get the value of the Subject header
+ if header :matches "Subject" "*" {
+ set "subject" "${1}";
+ }
+
+ # don't need high importance notification for
+ # a 'for your information'
+ if not header :contains "subject" "FYI:" {
+ notify :importance "1" :message "BOSS: ${subject}"
+ "tel:+14085551212";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex5.sieve b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex5.sieve
new file mode 100644
index 0000000..c6b7dc6
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex5.sieve
@@ -0,0 +1,11 @@
+require ["enotify"];
+
+if notify_method_capability
+ "xmpp:tim@example.com?message;subject=SIEVE"
+ "Online"
+ "yes" {
+ notify :importance "1" :message "You got mail"
+ "xmpp:tim@example.com?message;subject=SIEVE";
+} else {
+ notify :message "You got mail" "tel:+14085551212";
+}
diff --git a/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex6.sieve b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex6.sieve
new file mode 100644
index 0000000..6a65c64
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute/draft-rfc-ex6.sieve
@@ -0,0 +1,5 @@
+require ["enotify", "variables"];
+
+set :encodeurl "body_param" "Safe body&evil=evilbody";
+
+notify "mailto:tim@example.com?body=${body_param}";
diff --git a/pigeonhole/tests/extensions/enotify/execute/duplicates.sieve b/pigeonhole/tests/extensions/enotify/execute/duplicates.sieve
new file mode 100644
index 0000000..17f2388
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/execute/duplicates.sieve
@@ -0,0 +1,4 @@
+require "enotify";
+
+notify :message "Incoming stupidity." "mailto:stephan@example.org%2cstephan@friep.example.com%2cidiot@example.org";
+notify :message "There it is." "mailto:tss@example.net%2cstephan@example.org%2cidiot@example.org%2cnico@frop.example.org%2cstephan@friep.example.com";
diff --git a/pigeonhole/tests/extensions/enotify/mailto.svtest b/pigeonhole/tests/extensions/enotify/mailto.svtest
new file mode 100644
index 0000000..68d8daa
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/mailto.svtest
@@ -0,0 +1,541 @@
+require "vnd.dovecot.testsuite";
+require "enotify";
+require "relational";
+require "envelope";
+require "variables";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Simple test
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Simple" {
+ notify "mailto:stephan@example.org";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not header :matches "Auto-Submitted" "auto-notified*" {
+ test_fail "auto-submitted header set inappropriately";
+ }
+
+ if not exists "X-Sieve" {
+ test_fail "x-sieve header missing from outgoing message";
+ }
+
+ if anyof (
+ not header :matches "x-priority" "3 *",
+ not header "importance" "normal") {
+
+ test_fail "default priority is not normal";
+ }
+}
+
+/*
+ * Multiple recipients
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Multiple recipients" {
+ notify "mailto:timo@example.com%2cstephan@dovecot.example.net?cc=postmaster@frop.example.org&subject=Frop%20received";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "timo@example.com" {
+ test_fail "first To address missing";
+ }
+
+ if not address :is "to" "stephan@dovecot.example.net" {
+ test_fail "second To address missing";
+ }
+
+ if not address :is "cc" "postmaster@frop.example.org" {
+ test_fail "first Cc address missing";
+ }
+
+ if not address :count "eq" :comparator "i;ascii-numeric" "to" "2" {
+ test_fail "too many recipients in To header";
+ }
+
+ if not address :count "eq" :comparator "i;ascii-numeric" "cc" "1" {
+ test_fail "too many recipients in Cc header";
+ }
+
+ if not header "subject" "Frop received" {
+ test_fail "subject header set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not header :matches "Auto-Submitted" "auto-notified*" {
+ test_fail "auto-submitted header not found for second message";
+ }
+
+ test_message :smtp 2;
+
+ if not header :matches "Auto-Submitted" "auto-notified*" {
+ test_fail "auto-submitted header not found for third message";
+ }
+}
+
+/*
+ * Duplicate recipients
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Duplicate recipients" {
+ notify "mailto:timo@example.com%2cstephan@dovecot.example.net?cc=stephan@dovecot.example.net";
+ notify "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if address "Cc" "stephan@dovecot.example.net" {
+ test_fail "duplicate recipient not removed from first message";
+ }
+
+ test_message :smtp 1;
+
+ if address "Cc" "timo@example.com" {
+ test_fail "duplicate recipient not removed from second message";
+ }
+}
+
+
+/*
+ * Notifying on automated messages
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Auto-submitted: auto-notify
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Notifying on automated messages" {
+ notify "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "notified of auto-submitted message";
+ }
+}
+
+/*
+ * Envelope
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_result_reset;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "bertus@frop.example.org";
+
+test "Envelope" {
+ notify "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope :localpart :is "from" "postmaster" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "stephan@example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not envelope :localpart :is "from" "postmaster" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "timo@example.com" {
+ test_fail "envelope sender set incorrectly";
+ }
+}
+
+/*
+ * Envelope :from
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "bertus@frop.example.org";
+
+test_result_reset;
+
+test "Envelope :from" {
+ notify :from "nico@frop.example.org"
+ "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope :is "from" "nico@frop.example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "stephan@example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not envelope :is "from" "nico@frop.example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "timo@example.com" {
+ test_fail "envelope sender set incorrectly";
+ }
+}
+
+/*
+ * Envelope <>
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_set "envelope.from" "<>";
+test_set "envelope.to" "bertus@frop.example.org";
+
+test_result_reset;
+
+test "Envelope <>" {
+ notify :from "nico@frop.example.org"
+ "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "stephan@example.org" {
+ test_fail "envelope recipient set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "timo@example.com" {
+ test_fail "envelope recipient set incorrectly";
+ }
+}
+
+/*
+ * Envelope config - sender
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "bertus@frop.example.org";
+
+test_config_set "sieve_notify_mailto_envelope_from"
+ "sender";
+test_config_reload :extension "enotify";
+test_result_reset;
+
+test "Envelope config - sender" {
+ notify :from "nico@frop.example.org"
+ "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "from" "nico@frop.example.org" {
+ test_fail "from set incorrectly";
+ }
+
+ if not envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "stephan@example.org" {
+ test_fail "envelope recipient set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not header :is "from" "nico@frop.example.org" {
+ test_fail "from set incorrectly";
+ }
+
+ if not envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "timo@example.com" {
+ test_fail "envelope recipient set incorrectly";
+ }
+}
+
+/*
+ * Envelope config - recipient
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "bertus@frop.example.org";
+
+test_config_set "sieve_notify_mailto_envelope_from"
+ "recipient";
+test_config_reload :extension "enotify";
+test_result_reset;
+
+test "Envelope config - recipient" {
+ notify :from "nico@frop.example.org"
+ "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "from" "nico@frop.example.org" {
+ test_fail "from set incorrectly";
+ }
+
+ if not envelope :is "from" "bertus@frop.example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "stephan@example.org" {
+ test_fail "envelope recipient set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not header :is "from" "nico@frop.example.org" {
+ test_fail "from set incorrectly";
+ }
+
+ if not envelope :is "from" "bertus@frop.example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "timo@example.com" {
+ test_fail "envelope recipient set incorrectly";
+ }
+}
+
+/*
+ * Envelope config - user_email
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "bertus@frop.example.org";
+
+test_config_set "sieve_notify_mailto_envelope_from"
+ "user_email";
+test_config_set "sieve_user_email" "b.wortel@example.org";
+test_config_reload;
+test_config_reload :extension "enotify";
+test_result_reset;
+
+test "Envelope config - user_email" {
+ notify :from "nico@frop.example.org"
+ "mailto:stephan@example.org?cc=timo@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "from" "nico@frop.example.org" {
+ test_fail "from set incorrectly";
+ }
+
+ if not envelope :is "from" "b.wortel@example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "stephan@example.org" {
+ test_fail "envelope recipient set incorrectly";
+ }
+
+ test_message :smtp 1;
+
+ if not header :is "from" "nico@frop.example.org" {
+ test_fail "from set incorrectly";
+ }
+
+ if not envelope :is "from" "b.wortel@example.org" {
+ test_fail "envelope sender set incorrectly";
+ }
+
+ if not envelope :is "to" "timo@example.com" {
+ test_fail "envelope recipient set incorrectly";
+ }
+}
+
+/*
+ * UTF-8 addresses
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "UTF-8 address" {
+ set "to" "=?utf-8?q?G=C3=BCnther?= M. Karotte <g.m.karotte@example.com>";
+ set "cc" "Dieter T. =?utf-8?q?Stoppelr=C3=BCbe?= <d.t.stoppelruebe@example.com>";
+
+ set :encodeurl "to_enc" "${to}";
+ set :encodeurl "cc_enc" "${cc}";
+
+ notify "mailto:?to=${to_enc}&cc=${cc_enc}";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ set "expected" "Günther M. Karotte <g.m.karotte@example.com>";
+ if not header :is "to" "${expected}" {
+ if header :matches "to" "*" { set "decoded" "${1}"; }
+
+ test_fail text:
+to header is not encoded/decoded properly:
+expected: ${expected}
+decoded: ${decoded}
+.
+;
+ }
+
+ set "expected" "Dieter T. Stoppelrübe <d.t.stoppelruebe@example.com>";
+ if not header :is "cc" "${expected}" {
+ if header :matches "cc" "*" { set "decoded" "${1}"; }
+
+ test_fail text:
+to header is not encoded/decoded properly:
+expected: ${expected}
+decoded: ${decoded}
+.
+;
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/notify_method_capability.svtest b/pigeonhole/tests/extensions/enotify/notify_method_capability.svtest
new file mode 100644
index 0000000..0d13477
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/notify_method_capability.svtest
@@ -0,0 +1,12 @@
+require "vnd.dovecot.testsuite";
+require "enotify";
+
+test "Mailto" {
+ if not notify_method_capability :is "mailto:stephan@example.org" "online" "maybe" {
+ test_fail "test should have matched";
+ }
+
+ if notify_method_capability :is "mailto:stephan@example.org" "online" "yes" {
+ test_fail "test should not have matched";
+ }
+}
diff --git a/pigeonhole/tests/extensions/enotify/valid_notify_method.svtest b/pigeonhole/tests/extensions/enotify/valid_notify_method.svtest
new file mode 100644
index 0000000..35255d6
--- /dev/null
+++ b/pigeonhole/tests/extensions/enotify/valid_notify_method.svtest
@@ -0,0 +1,31 @@
+require "vnd.dovecot.testsuite";
+
+require "enotify";
+
+test "Mailto: invalid header name" {
+ if valid_notify_method
+ "mailto:stephan@example.org?header:=frop" {
+ test_fail "invalid uri accepted";
+ }
+}
+
+test "Mailto: invalid recipient" {
+ if valid_notify_method
+ "mailto:stephan%23example.org" {
+ test_fail "invalid uri accepted";
+ }
+}
+
+test "Mailto: invalid to header recipient" {
+ if valid_notify_method
+ "mailto:stephan@example.org?to=nico%23frop.example.org" {
+ test_fail "invalid uri accepted";
+ }
+}
+
+test "Mailto: valid URI" {
+ if not valid_notify_method
+ "mailto:stephan@example.org" {
+ test_fail "valid uri denied";
+ }
+}
diff --git a/pigeonhole/tests/extensions/envelope.svtest b/pigeonhole/tests/extensions/envelope.svtest
new file mode 100644
index 0000000..9cf3b8b
--- /dev/null
+++ b/pigeonhole/tests/extensions/envelope.svtest
@@ -0,0 +1,244 @@
+require "vnd.dovecot.testsuite";
+
+require "envelope";
+
+/*
+ * Empty envelope addresses
+ */
+
+/* RFC 5228, Section 5.4: The null reverse-path is matched against as the empty
+ * string, regardless of the ADDRESS-PART argument specified.
+ */
+
+test "Envelope - from empty" {
+ /* Return_path: "" */
+
+ test_set "envelope.from" "";
+
+ if not envelope :all :is "from" "" {
+ test_fail "failed to (:all :is)-match a \"\" return path";
+ }
+
+ if not envelope :all :contains "from" "" {
+ test_fail "failed to (:all :contains)-match a \"\" return path";
+ }
+
+ if not envelope :domain :is "from" "" {
+ test_fail "failed to (:domain :is)-match a \"\" return path";
+ }
+
+ if not envelope :domain :contains "from" "" {
+ test_fail "failed to (:domain :contains)-match a \"\" return path";
+ }
+
+ /* Return path: <> */
+
+ test_set "envelope.from" "<>";
+
+ if not envelope :all :is "from" "" {
+ test_fail "failed to (:all :is)-match a <> return path";
+ }
+
+ if not envelope :all :contains "from" "" {
+ test_fail "failed to (:all :contains)-match a <> return path";
+ }
+
+ if not envelope :domain :is "from" "" {
+ test_fail "failed to (:domain :is)-match a <> return path";
+ }
+
+ if not envelope :domain :contains "from" "" {
+ test_fail "failed to (:domain :contains)-match a <> return path";
+ }
+
+ if envelope :all :is "from" "nico@frop.example.org" {
+ test_fail "envelope test matches nonsense";
+ }
+}
+
+/*
+ * Invalid envelope addresses
+ */
+
+test "Envelope - invalid paths" {
+ /* Return_path: "hutsefluts" */
+
+ test_set "envelope.from" "hutsefluts@";
+ test_set "envelope.to" "knurft@";
+
+ if envelope :all :is "from" "hutsefluts@" {
+ test_fail ":all address part matched syntactically incorrect reverse path";
+ }
+ if envelope :all :is "to" "knurft@" {
+ test_fail ":all address part matched syntactically incorrect forward path";
+ }
+}
+
+/*
+ * Syntax errors
+ */
+
+test "Envelope - syntax errors" {
+ /* Control */
+ test_set "envelope.from" "<stephan@example.org>";
+ if not envelope :all :is "from" "stephan@example.org" {
+ test_fail "correct control test failed";
+ }
+
+ # Duplicate <
+ test_set "envelope.from" "<<stephan@example.org>";
+ if envelope :all :is "from" "stephan@example.org" {
+ test_fail "failed to recognize syntax error (1)";
+ }
+
+ # Spurious >
+ test_set "envelope.from" "stephan@example.org>";
+ if envelope :all :is "from" "stephan@example.org" {
+ test_fail "failed to recognize syntax error (2)";
+ }
+
+ # Missing >
+ test_set "envelope.from" "<stephan@example.org";
+ if envelope :all :is "from" "stephan@example.org" {
+ test_fail "failed to recognize syntax error (3)";
+ }
+
+ # No @
+ test_set "envelope.from" "<stephan example.org>";
+ if envelope :domain :contains "from" "example" {
+ test_fail "failed to recognize syntax error (4)";
+ }
+
+ # Duplicate @
+ test_set "envelope.from" "<stephan@@example.org>";
+ if envelope :domain :contains "from" "example" {
+ test_fail "failed to recognize syntax error (5)";
+ }
+}
+
+/*
+ * Ignoring source routes
+ */
+
+test "Envelope - source route" {
+ /* Single */
+ test_set "envelope.from" "<@cola.example.org:stephan@example.org>";
+ if not envelope :localpart :is "from" "stephan" {
+ test_fail "parsing path with source route (single) failed";
+ }
+
+ /* Dual */
+ test_set "envelope.from" "<@cola.example.org,@mx.utwente.nl:stephan@example.org>";
+ if not envelope :localpart :is "from" "stephan" {
+ test_fail "parsing path with source route (dual) failed";
+ }
+
+ /* Multiple */
+ test_set "envelope.from" "<@cola.example.org,@mx.utwente.nl,@smtp.example.net:stephan@example.org>";
+ if not envelope :localpart :is "from" "stephan" {
+ test_fail "parsing path with source route (multiple) failed";
+ }
+}
+
+test "Envelope - source route errors" {
+ test_set "envelope.to" "<cola.example.org:stephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (1)";
+ }
+
+ test_set "envelope.to" "<@.example.org:stephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (2)";
+ }
+
+ test_set "envelope.to" "<@cola..nl:stephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (3)";
+ }
+
+ test_set "envelope.to" "<@cola.example.orgstephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (4)";
+ }
+
+ test_set "envelope.to" "<@cola.example.org@mx.utwente.nl:stephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (5)";
+ }
+
+ test_set "envelope.to" "<@cola.example.org,mx.utwente.nl:stephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (6)";
+ }
+
+ test_set "envelope.to" "<@cola.example.org,@mx.utwente.nl,stephan@example.org>";
+ if envelope :domain :contains "to" "" {
+ test_fail "parsing syntactically incorrect path should have failed (7)";
+ }
+}
+
+test "Envelope - local part only" {
+ test_set "envelope.to" "<MAILER-DAEMON>";
+ if not envelope :is "to" "MAILER-DAEMON" {
+ test_fail "failed to parse local_part only path";
+ }
+
+ test_set "envelope.to" "MAILER-DAEMON@";
+ if envelope :is "to" "MAILER-DAEMON" {
+ test_fail "parsing syntactically incorrect path with missing domain";
+ }
+
+ test_set "envelope.to" "<MAILER-DAEMON>";
+ if not envelope :is "to" "MAILER-DAEMON" {
+ test_fail "failed to parse local_part only path with angle brackets";
+ }
+}
+
+test "Envelope - Japanese localpart" {
+ test_set "envelope.to" ".japanese@example.com";
+ if not envelope :localpart :is "to" ".japanese" {
+ test_fail "failed to parse japanese local_part (1)";
+ }
+
+ test_set "envelope.to" "japanese.@example.com";
+ if not envelope :localpart :is "to" "japanese." {
+ test_fail "failed to parse japanese local_part (2)";
+ }
+
+ test_set "envelope.to" "japanese...localpart@example.com";
+ if not envelope :localpart :is "to" "japanese...localpart" {
+ test_fail "failed to parse japanese local_part (3)";
+ }
+
+ test_set "envelope.to" "..japanese...localpart..@example.com";
+ if not envelope :localpart :is "to" "..japanese...localpart.." {
+ test_fail "failed to parse japanese local_part (4)";
+ }
+}
+
+test "Envelope - Non-standard hostnames" {
+ test_set "envelope.to" "japanese@_example.com";
+ if not envelope :domain :is "to" "_example.com" {
+ test_fail "failed to parse non-standard domain (1)";
+ }
+
+ test_set "envelope.to" "japanese@ex_ample.com";
+ if not envelope :domain :is "to" "ex_ample.com" {
+ test_fail "failed to parse non-standard domain (2)";
+ }
+
+ test_set "envelope.to" "japanese@example_.com";
+ if not envelope :domain :is "to" "example_.com" {
+ test_fail "failed to parse non-standard domain (3)";
+ }
+
+ test_set "envelope.to" "japanese@-example.com";
+ if not envelope :domain :is "to" "-example.com" {
+ test_fail "failed to parse non-standard domain (4)";
+ }
+
+ test_set "envelope.to" "japanese@example-.com";
+ if not envelope :domain :is "to" "example-.com" {
+ test_fail "failed to parse non-standard domain (5)";
+ }
+}
diff --git a/pigeonhole/tests/extensions/environment/basic.svtest b/pigeonhole/tests/extensions/environment/basic.svtest
new file mode 100644
index 0000000..bb0beb4
--- /dev/null
+++ b/pigeonhole/tests/extensions/environment/basic.svtest
@@ -0,0 +1,33 @@
+require "vnd.dovecot.testsuite";
+require "environment";
+require "variables";
+
+test "Name" {
+ if not environment :contains "name" "pigeonhole" {
+ if environment :matches "name" "*" { set "env_name" "${1}"; }
+
+ test_fail "name environment returned invalid value(1): ${env_name}";
+ }
+
+ if not environment :contains "name" "sieve" {
+ if environment :matches "name" "*" { set "env_name" "${1}"; }
+
+ test_fail "name environment returned invalid value(2): ${env_name}";
+ }
+
+ if environment :contains "name" "cyrus" {
+ test_fail "something is definitely wrong here";
+ }
+
+ if not environment :is :comparator "i;octet" "name" "Pigeonhole Sieve" {
+ test_fail "name environment does not match exactly with what is expected";
+ }
+}
+
+test "Location" {
+ if not environment "location" "MS" {
+ test_fail "wrong testsuite environment location";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/environment/rfc.svtest b/pigeonhole/tests/extensions/environment/rfc.svtest
new file mode 100644
index 0000000..c3177ae
--- /dev/null
+++ b/pigeonhole/tests/extensions/environment/rfc.svtest
@@ -0,0 +1,28 @@
+require "vnd.dovecot.testsuite";
+require "environment";
+require "relational";
+
+test "Non-existent" {
+ if environment :contains "nonsense" "" {
+ test_fail "matched unknown environment item";
+ }
+}
+
+test "Exists" {
+ if not environment :contains "version" "" {
+ test_fail "failed to match known environment item";
+ }
+}
+
+test "Count" {
+ if anyof (
+ environment :count "eq" "nonsense" "0",
+ environment :count "eq" "nonsense" "1"
+ ) {
+ test_fail "count should not match unknown environment item";
+ }
+
+ if not environment :count "eq" "location" "1" {
+ test_fail "count of non-empty environment should be 1";
+ }
+}
diff --git a/pigeonhole/tests/extensions/ihave/errors.svtest b/pigeonhole/tests/extensions/ihave/errors.svtest
new file mode 100644
index 0000000..c6b9750
--- /dev/null
+++ b/pigeonhole/tests/extensions/ihave/errors.svtest
@@ -0,0 +1,19 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test "Error command" {
+ if not test_script_compile "errors/error.sieve" {
+ test_fail "compile failed";
+ }
+
+ if test_script_run {
+ test_fail "execution should have failed";
+ }
+
+ if test_error :count "gt" :comparator "i;ascii-numeric" "1" {
+ test_fail "too many runtime errors reported";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/ihave/errors/error.sieve b/pigeonhole/tests/extensions/ihave/errors/error.sieve
new file mode 100644
index 0000000..8da0fe7
--- /dev/null
+++ b/pigeonhole/tests/extensions/ihave/errors/error.sieve
@@ -0,0 +1,3 @@
+require "ihave";
+
+error "Something failed.";
diff --git a/pigeonhole/tests/extensions/ihave/execute.svtest b/pigeonhole/tests/extensions/ihave/execute.svtest
new file mode 100644
index 0000000..701d817
--- /dev/null
+++ b/pigeonhole/tests/extensions/ihave/execute.svtest
@@ -0,0 +1,23 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * Execution testing (currently just meant to trigger any segfaults)
+ */
+
+test "Basic" {
+ if not test_script_compile "execute/ihave.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+
+ test_binary_save "ihave-basic";
+ test_binary_load "ihave-basic";
+}
+
diff --git a/pigeonhole/tests/extensions/ihave/execute/ihave.sieve b/pigeonhole/tests/extensions/ihave/execute/ihave.sieve
new file mode 100644
index 0000000..0fe84c8
--- /dev/null
+++ b/pigeonhole/tests/extensions/ihave/execute/ihave.sieve
@@ -0,0 +1,7 @@
+require "ihave";
+
+if ihave "nonsense-extension" {
+ nonsense_command "Frop!";
+}
+
+redirect "frop@example.com";
diff --git a/pigeonhole/tests/extensions/ihave/restrictions.svtest b/pigeonhole/tests/extensions/ihave/restrictions.svtest
new file mode 100644
index 0000000..5dba126
--- /dev/null
+++ b/pigeonhole/tests/extensions/ihave/restrictions.svtest
@@ -0,0 +1,14 @@
+require "vnd.dovecot.testsuite";
+require "ihave";
+
+test "Restricted: encoded-character" {
+ if ihave "encoded-character" {
+ test_fail "encoded-character extension is incompatible with ihave";
+ }
+}
+
+test "Restricted: variables" {
+ if ihave "variables" {
+ test_fail "variables extension is incompatible with ihave";
+ }
+}
diff --git a/pigeonhole/tests/extensions/imap4flags/basic.svtest b/pigeonhole/tests/extensions/imap4flags/basic.svtest
new file mode 100644
index 0000000..d6af444
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/basic.svtest
@@ -0,0 +1,332 @@
+require "vnd.dovecot.testsuite";
+
+require "imap4flags";
+require "relational";
+require "variables";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Basic functionality tests
+ */
+
+test "Hasflag empty" {
+ if hasflag "\\Seen" {
+ test_fail "hasflag sees initial \\seen flag were there should be none";
+ }
+ if hasflag "\\draft" {
+ test_fail "hasflag sees initial \\draft flag were there should be none";
+ }
+ if hasflag "\\recent" {
+ test_fail "hasflag sees initial \\recent flag were there should be none";
+ }
+ if hasflag "\\flagged" {
+ test_fail "hasflag sees initial \\flagged flag were there should be none";
+ }
+ if hasflag "\\answered" {
+ test_fail "hasflag sees initial \\answered flag were there should be none";
+ }
+ if hasflag "\\deleted" {
+ test_fail "hasflag sees initial \\deleted flag were there should be none";
+ }
+
+ if hasflag :comparator "i;ascii-numeric" :count "ge" "1" {
+ test_fail "hasflag sees initial flags were there should be none";
+ }
+}
+
+test "Setflag; Hasflag one" {
+ setflag "\\seen";
+
+ if not hasflag "\\Seen" {
+ test_fail "flag not set of hasflag fails to see it";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "1" {
+ test_fail "flag not set of hasflag fails to see it";
+ }
+
+ if hasflag "$Nonsense" {
+ test_fail "hasflag sees other flag that the one set";
+ }
+}
+
+test "Hasflag; duplicates" {
+ set "Flags" "A B C D E F A B C D E F";
+
+ if hasflag :comparator "i;ascii-numeric" :count "gt" "Flags" "6" {
+ test_fail "hasflag must ignore duplicates";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "Flags" "6" {
+ test_fail "hasflag :count gives strange results";
+ }
+}
+
+test "Flag operations" {
+ setflag "A";
+
+ if not hasflag "A" {
+ test_fail "hasflag misses set flag";
+ }
+
+ if hasflag :comparator "i;ascii-numeric" :count "gt" "1" {
+ test_fail "hasflag sees more than one flag";
+ }
+
+ addflag "B";
+
+ if not hasflag "B" {
+ test_fail "flag \"B\" not added";
+ }
+
+ if not hasflag "A" {
+ test_fail "flag \"A\" not retained";
+ }
+
+ if hasflag :comparator "i;ascii-numeric" :count "gt" "2" {
+ test_fail "hasflag sees more than two flags";
+ }
+
+ addflag ["C", "D", "E F"];
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "6" {
+ test_fail "hasflag sees more than two flags";
+ }
+
+ removeflag ["D"];
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "5" {
+ test_fail "hasflag sees more than two flags";
+ }
+
+ if hasflag "D" {
+ test_fail "removed flag still present";
+ }
+
+ set "var" "G";
+ addflag "${var}";
+
+ if not hasflag "G" {
+ test_fail "flag \"G\" not added";
+ }
+
+ if not hasflag "A" {
+ test_fail "flag \"A\" not retained";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "6" {
+ test_fail "hasflag sees something other than six flags";
+ }
+}
+
+test "Variable flag operations" {
+ setflag "frop" "A";
+
+ if not hasflag "frop" "A" {
+ test_fail "hasflag misses set flag";
+ }
+
+ if hasflag :comparator "i;ascii-numeric" :count "gt" "frop" "1" {
+ test_fail "hasflag sees more than one flag";
+ }
+
+ addflag "frop" "B";
+
+ if not hasflag "frop" "B" {
+ test_fail "flag \"B\" not added";
+ }
+
+ if not hasflag "frop" "A" {
+ test_fail "flag \"A\" not retained";
+ }
+
+ if hasflag :comparator "i;ascii-numeric" :count "gt" "frop" "2" {
+ test_fail "hasflag sees more than two flags";
+ }
+
+ addflag "frop" ["C", "D", "E F"];
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "frop" "6" {
+ test_fail "hasflag sees something other than six flags";
+ }
+
+ removeflag "frop" ["D"];
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "frop" "5" {
+ test_fail "hasflag sees something other than five flags";
+ }
+
+ if hasflag "frop" "D" {
+ test_fail "removed flag still present";
+ }
+
+ set "var" "G";
+ addflag "frop" "${var}";
+
+ if not hasflag "frop" "G" {
+ test_fail "flag \"G\" not added";
+ }
+
+ if not hasflag "frop" "A" {
+ test_fail "flag \"A\" not retained";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "frop" "6" {
+ test_fail "hasflag sees something other than six flags";
+ }
+}
+
+test "Setflag; string list" {
+ setflag ["A B", "C D"];
+
+ if not hasflag "A" {
+ test_fail "hasflag misses A flag";
+ }
+
+ if not hasflag "B" {
+ test_fail "hasflag misses B flag";
+ }
+
+ if not hasflag "C" {
+ test_fail "hasflag misses C flag";
+ }
+
+ if not hasflag "D" {
+ test_fail "hasflag misses D flag";
+ }
+
+ if hasflag :comparator "i;ascii-numeric" :count "ne" "4" {
+ test_fail "hasflag sees incorrect number of flags";
+ }
+}
+
+test "Removal: one" {
+ setflag "\\seen";
+
+ if not hasflag "\\seen" {
+ test_fail "hasflag misses set flag";
+ }
+
+ removeflag "\\seen";
+
+ if hasflag "\\seen" {
+ test_fail "flag not removed";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "0" {
+ test_fail "flags are still set";
+ }
+}
+
+test "Removal: first" {
+ setflag "$frop \\seen";
+
+ if not allof ( hasflag "\\seen", hasflag "$frop" ) {
+ test_fail "hasflag misses set flags";
+ }
+
+ removeflag "$frop";
+
+ if not hasflag "\\seen" {
+ test_fail "wrong flag removed";
+ }
+
+ if hasflag "$frop" {
+ test_fail "flag not removed";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "1" {
+ test_fail "more than one flag remains set";
+ }
+}
+
+test "Removal: last" {
+ setflag "\\seen $friep";
+
+ if not allof ( hasflag "\\seen", hasflag "$friep" ) {
+ test_fail "hasflag misses set flags";
+ }
+
+ removeflag "$friep";
+
+ if not hasflag "\\seen" {
+ test_fail "wrong flag removed";
+ }
+
+ if hasflag "$friep" {
+ test_fail "flag not removed";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "1" {
+ test_fail "more than one flag remains set";
+ }
+}
+
+test "Removal: middle" {
+ setflag "\\seen $friep \\flagged";
+
+ if not allof ( hasflag "\\flagged", hasflag "\\seen", hasflag "$friep" ) {
+ test_fail "hasflag misses set flags";
+ }
+
+ removeflag "$friep";
+
+ if not allof ( hasflag "\\seen", hasflag "\\flagged" ) {
+ test_fail "wrong flag removed";
+ }
+
+ if hasflag "$friep" {
+ test_fail "flag not removed";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "2" {
+ test_fail "more than two flags remain set";
+ }
+}
+
+test "Removal: duplicates" {
+ setflag "\\seen $friep $friep \\flagged $friep";
+
+ if not allof ( hasflag "\\flagged", hasflag "\\seen", hasflag "$friep" ) {
+ test_fail "hasflag misses set flags";
+ }
+
+ removeflag "$friep";
+
+ if not allof ( hasflag "\\seen", hasflag "\\flagged" ) {
+ test_fail "wrong flag removed";
+ }
+
+ if hasflag "$friep" {
+ test_fail "flag not removed";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "2" {
+ test_fail "more than two flags remain set";
+ }
+}
+
+test "Removal: whitespace" {
+ setflag " \\seen $friep $friep \\flagged $friep ";
+
+ if not allof ( hasflag "\\flagged", hasflag "\\seen", hasflag "$friep" ) {
+ test_fail "hasflag misses set flags";
+ }
+
+ removeflag "$friep";
+
+ if not allof ( hasflag "\\seen", hasflag "\\flagged" ) {
+ test_fail "wrong flag removed";
+ }
+
+ if hasflag "$friep" {
+ test_fail "flag not removed";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "2" {
+ test_fail "more than two flags remain set";
+ }
+}
+
+
+
diff --git a/pigeonhole/tests/extensions/imap4flags/execute.svtest b/pigeonhole/tests/extensions/imap4flags/execute.svtest
new file mode 100644
index 0000000..1ee1906
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/execute.svtest
@@ -0,0 +1,68 @@
+require "vnd.dovecot.testsuite";
+require "imap4flags";
+require "relational";
+
+
+/*
+ * Execution testing
+ */
+
+test_mailbox_create "INBOX.Junk";
+test_mailbox_create "INBOX.Nonsense";
+
+test "Flags Side Effect" {
+ if not test_script_compile "execute/flags-side-effect.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+
+ test_result_reset;
+
+ if not test_message :folder "INBOX.Junk" 0 {
+ test_fail "message not stored in INBOX.Junk";
+ }
+
+ if not hasflag :count "eq" "1" {
+ test_fail "invalid number of flags for message in INBOX.Junk";
+ }
+
+ if not hasflag :is "NONSENSE" {
+ test_fail "invalid flag set for message in INBOX.Junk";
+ }
+
+ test_result_reset;
+
+ if not test_message :folder "INBOX" 0 {
+ test_fail "message not stored in INBOX";
+ }
+
+ if not hasflag :count "eq" "1" {
+ test_fail "invalid number of flags for message in INBOX";
+ }
+
+ if not hasflag :is "\\seen" {
+ test_fail "invalid flag set for message in INBOX";
+ }
+
+ test_result_reset;
+
+ if not test_message :folder "INBOX.Nonsense" 0 {
+ test_fail "message not stored in INBOX.Nonsense";
+ }
+
+ if not hasflag :count "eq" "1" {
+ test_fail "invalid number of flags for message in Inbox.Nonsense";
+ }
+
+ if not hasflag :is "IMPLICIT" {
+ test_fail "invalid flag set for message in Inbox.Nonsene";
+ }
+
+}
diff --git a/pigeonhole/tests/extensions/imap4flags/execute/flags-side-effect.sieve b/pigeonhole/tests/extensions/imap4flags/execute/flags-side-effect.sieve
new file mode 100644
index 0000000..17de0ad
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/execute/flags-side-effect.sieve
@@ -0,0 +1,18 @@
+require "imap4flags";
+require "fileinto";
+
+/*
+ * When keep/fileinto is used multiple times in a script and duplicate
+ * message elimination is performed, the last flag list value MUST win.
+ */
+
+setflag "IMPLICIT";
+
+fileinto :flags "\\Seen \\Draft" "INBOX.Junk";
+fileinto :flags "NONSENSE" "INBOX.Junk";
+
+keep;
+keep :flags "\\Seen";
+
+fileinto :flags "\\Seen" "Inbox.Nonsense";
+fileinto "Inbox.Nonsense";
diff --git a/pigeonhole/tests/extensions/imap4flags/flagstore.svtest b/pigeonhole/tests/extensions/imap4flags/flagstore.svtest
new file mode 100644
index 0000000..bf11402
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/flagstore.svtest
@@ -0,0 +1,146 @@
+require "vnd.dovecot.testsuite";
+require "fileinto";
+require "imap4flags";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "mailbox";
+
+test_set "message" text:
+From: Henry von Flockenstoffen <henry@example.com>
+To: Dieter von Ausburg <dieter@example.com>
+Subject: Test message.
+
+Test message.
+.
+;
+
+test "Basic" {
+ if hasflag :comparator "i;ascii-numeric" :count "ge" "1" {
+ test_fail "some flags or keywords are already set";
+ }
+
+ setflag "$label1 \\answered";
+
+ fileinto :create "Uninteresting";
+
+ if not test_result_execute {
+ test_fail "failed to execute first result";
+ }
+
+ test_result_reset;
+
+ setflag "\\draft \\seen Junk";
+
+ fileinto "Uninteresting";
+
+ if not test_result_execute {
+ test_fail "failed to execute second result";
+ }
+
+ test_result_reset;
+
+ fileinto :flags "\\flagged" "Uninteresting";
+
+ if not test_result_execute {
+ test_fail "failed to execute third result";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Uninteresting" 0;
+
+ if not hasflag "$label1 \\answered" {
+ test_fail "flags not stored for first message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "2" {
+ test_fail "invalid number of flags set for first message";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Uninteresting" 1;
+
+ if not hasflag "\\draft \\seen Junk" {
+ test_fail "flags not stored for second message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "3" {
+ test_fail "invalid number of flags set for second message";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Uninteresting" 2;
+
+ if not hasflag "\\flagged" {
+ test_fail "flags not stored for third message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "1" {
+ test_fail "invalid number of flags set for third message";
+ }
+}
+
+test_result_reset;
+test_set "message" text:
+From: Henry von Flockenstoffen <henry@example.com>
+To: Dieter von Ausburg <dieter@example.com>
+Subject: Test message.
+
+Test message.
+.
+;
+
+test "Flag changes between stores" {
+ if hasflag :comparator "i;ascii-numeric" :count "ge" "1" {
+ test_fail "some flags or keywords are already set";
+ }
+
+ setflag "$label1 \\answered";
+ fileinto :create "FolderA";
+
+ setflag "$label2";
+ fileinto :create "FolderB";
+
+ fileinto :create :flags "\\seen \\draft \\flagged" "FolderC";
+
+ if not test_result_execute {
+ test_fail "failed to execute first result";
+ }
+
+ test_result_reset;
+ test_message :folder "FolderA" 0;
+
+ if not hasflag "\\answered $label1" {
+ test_fail "flags not stored for first message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "2" {
+ test_fail "invalid number of flags set for first message";
+ }
+
+ test_result_reset;
+ test_message :folder "FolderB" 0;
+
+ if not hasflag "$label2" {
+ test_fail "flag not stored for second message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "1" {
+ test_fail "invalid number of flags set for second message";
+ }
+
+ test_result_reset;
+ test_message :folder "FolderC" 0;
+
+ if not hasflag "\\seen \\flagged \\draft" {
+ test_fail "flags not stored for third message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "3" {
+ test_fail "invalid number of flags set for third message";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/imap4flags/flagstring.svtest b/pigeonhole/tests/extensions/imap4flags/flagstring.svtest
new file mode 100644
index 0000000..23b6b34
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/flagstring.svtest
@@ -0,0 +1,82 @@
+require "vnd.dovecot.testsuite";
+require "imap4flags";
+require "variables";
+
+test "Duplicates: setflag" {
+ setflag "flags" "\\seen \\seen";
+
+ if not string "${flags}" "\\seen" {
+ test_fail "duplicate \\seen flag item not removed (1)";
+ }
+
+ setflag "flags" "\\seen $frop \\seen";
+
+ if not string "${flags}" "\\seen $frop" {
+ test_fail "duplicate \\seen flag item not removed (2)";
+ }
+
+ setflag "flags" "\\seen $frop $frop \\seen";
+
+ if not string "${flags}" "\\seen $frop" {
+ test_fail "duplicate \\seen flag item not removed (3)";
+ }
+
+ setflag "flags" "$frop \\seen $frop \\seen";
+
+ if not string "${flags}" "$frop \\seen" {
+ test_fail "duplicate \\seen flag item not removed (4)";
+ }
+
+ setflag "flags" "$frop \\seen \\seen \\seen \\seen $frop $frop $frop \\seen";
+
+ if not string "${flags}" "$frop \\seen" {
+ test_fail "duplicate \\seen flag item not removed (5)";
+ }
+}
+
+test "Duplicates: addflag" {
+ setflag "flags" "";
+ addflag "flags" "\\seen \\seen";
+
+ if not string "${flags}" "\\seen" {
+ test_fail "duplicate \\seen flag item not removed (1)";
+ }
+
+ setflag "flags" "";
+ addflag "flags" "\\seen $frop \\seen";
+
+ if not string "${flags}" "\\seen $frop" {
+ test_fail "duplicate \\seen flag item not removed (2)";
+ }
+
+ setflag "flags" "";
+ addflag "flags" "\\seen $frop $frop \\seen";
+
+ if not string "${flags}" "\\seen $frop" {
+ test_fail "duplicate \\seen flag item not removed (3)";
+ }
+
+ setflag "flags" "";
+ addflag "flags" "$frop \\seen $frop \\seen";
+
+ if not string "${flags}" "$frop \\seen" {
+ test_fail "duplicate \\seen flag item not removed (4)";
+ }
+
+ setflag "flags" "";
+ addflag "flags" "$frop \\seen \\seen \\seen \\seen $frop $frop $frop \\seen";
+
+ if not string "${flags}" "$frop \\seen" {
+ test_fail "duplicate \\seen flag item not removed (5)";
+ }
+
+ setflag "flags" "$frop \\seen";
+ addflag "flags" "\\seen \\seen \\seen $frop $frop $frop \\seen";
+
+ if not string "${flags}" "$frop \\seen" {
+ test_fail "duplicate \\seen flag item not removed (6)";
+ }
+}
+
+
+
diff --git a/pigeonhole/tests/extensions/imap4flags/hasflag.svtest b/pigeonhole/tests/extensions/imap4flags/hasflag.svtest
new file mode 100644
index 0000000..1088190
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/hasflag.svtest
@@ -0,0 +1,91 @@
+require "vnd.dovecot.testsuite";
+
+require "imap4flags";
+require "relational";
+require "variables";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Generic tests
+ */
+
+test "Ignoring \"\"" {
+ setflag "";
+
+ if hasflag "" {
+ test_fail "hasflag fails to ignore empty string";
+ }
+}
+
+/*
+ * Variables
+ */
+
+test "Multiple variables" {
+ setflag "A" "Aflag";
+ setflag "B" "Bflag";
+ setflag "C" "Cflag";
+
+ if not hasflag ["a", "b", "c"] ["Bflag"] {
+ test_fail "hasflag failed to match multiple flags variables";
+ }
+}
+
+/*
+ * RFC examples
+ */
+
+test "RFC hasflag example - :is" {
+ setflag "A B";
+
+ if not hasflag ["b","A"] {
+ test_fail "list representation did not match";
+ }
+
+ if not hasflag :is "b A" {
+ test_fail "string representation did not match";
+ }
+}
+
+test "RFC hasflag example - :contains variable" {
+ set "MyVar" "NonJunk Junk gnus-forward $Forwarded NotJunk JunkRecorded $Junk $NotJunk";
+
+ if not hasflag :contains "MyVar" "Junk" {
+ test_fail "failed true example 1";
+ }
+
+ if not hasflag :contains "MyVar" "forward" {
+ test_fail "failed true example 2";
+ }
+
+ if not hasflag :contains "MyVar" ["label", "forward"] {
+ test_fail "failed true example 3";
+ }
+
+ if not hasflag :contains "MyVar" ["junk", "forward"] {
+ test_fail "failed true example 4";
+ }
+
+ if not hasflag :contains "MyVar" "junk forward" {
+ test_fail "failed true example 4 (rewrite 1)";
+ }
+
+ if not hasflag :contains "MyVar" "forward junk" {
+ test_fail "failed true example 4 (rewrite 2)";
+ }
+
+ if hasflag :contains "MyVar" "label" {
+ test_fail "failed false example 1";
+ }
+
+ if hasflag :contains "MyVar" ["label1", "label2"] {
+ test_fail "failed false example 2";
+ }
+}
+
+test "RFC hasflag example - :count variable" {
+ set "MyFlags" "A B";
+ if not hasflag :count "ge" :comparator "i;ascii-numeric" "MyFlags" "2" {
+ test_fail "failed count \"ge\" comparison";
+ }
+}
diff --git a/pigeonhole/tests/extensions/imap4flags/multiscript.svtest b/pigeonhole/tests/extensions/imap4flags/multiscript.svtest
new file mode 100644
index 0000000..5080eda
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/multiscript.svtest
@@ -0,0 +1,55 @@
+require "vnd.dovecot.testsuite";
+require "imap4flags";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "mailbox";
+require "fileinto";
+
+test "Segfault Trigger 1" {
+
+ if not test_multiscript [
+ "multiscript/group-spam.sieve",
+ "multiscript/spam.sieve",
+ "multiscript/sent-store.sieve"]
+ {
+ test_fail "failed multiscript execution";
+ }
+}
+
+test_set "message" text:
+From: Henry von Flockenstoffen <henry@example.com>
+To: Dieter von Ausburg <dieter@example.com>
+Subject: Test message.
+
+Test message.
+.
+;
+
+test "Internal Flags" {
+ if hasflag :comparator "i;ascii-numeric" :count "ge" "1" {
+ test_fail "some flags or keywords are already set";
+ }
+
+ if not test_multiscript [
+ "multiscript/setflag.sieve",
+ "multiscript/fileinto.sieve"]
+ {
+ test_fail "failed multiscript execution";
+ }
+
+ test_result_reset;
+ test_message :folder "folder" 0;
+
+ if not hasflag "\\answered" {
+ test_fail "\\answered flag not stored for message";
+ }
+
+ if not hasflag "$label1" {
+ test_fail "$label1 keyword not stored for message";
+ }
+
+ if not hasflag :comparator "i;ascii-numeric" :count "eq" "2" {
+ test_fail "invalid number of flags set for message";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/imap4flags/multiscript/fileinto.sieve b/pigeonhole/tests/extensions/imap4flags/multiscript/fileinto.sieve
new file mode 100644
index 0000000..94892a5
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/multiscript/fileinto.sieve
@@ -0,0 +1,4 @@
+require "fileinto";
+require "mailbox";
+
+fileinto :create "folder";
diff --git a/pigeonhole/tests/extensions/imap4flags/multiscript/group-spam.sieve b/pigeonhole/tests/extensions/imap4flags/multiscript/group-spam.sieve
new file mode 100644
index 0000000..92ea3b9
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/multiscript/group-spam.sieve
@@ -0,0 +1,14 @@
+require ["fileinto", "variables", "envelope"];
+
+if header :contains "X-Group-Mail" ["Yes", "YES", "1"] {
+ if header :contains "X-Spam-Flag" ["Yes", "YES", "1"] {
+ if envelope :matches :localpart "to" "*" {
+ fileinto "group/${1}/SPAM"; stop;
+ }
+ }
+ if address :is ["To"] "sales@florist.ru" {
+ fileinto "group/info/Orders";
+ }
+ stop;
+}
+keep;
diff --git a/pigeonhole/tests/extensions/imap4flags/multiscript/sent-store.sieve b/pigeonhole/tests/extensions/imap4flags/multiscript/sent-store.sieve
new file mode 100644
index 0000000..cb21daa
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/multiscript/sent-store.sieve
@@ -0,0 +1,7 @@
+require ["imap4flags"];
+
+if header :contains "X-Set-Seen" ["Yes", "YES", "1"] {
+ setflag "\\Seen";
+}
+
+keep;
diff --git a/pigeonhole/tests/extensions/imap4flags/multiscript/setflag.sieve b/pigeonhole/tests/extensions/imap4flags/multiscript/setflag.sieve
new file mode 100644
index 0000000..c992d19
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/multiscript/setflag.sieve
@@ -0,0 +1,3 @@
+require "imap4flags";
+
+setflag "$label1 \\answered";
diff --git a/pigeonhole/tests/extensions/imap4flags/multiscript/spam.sieve b/pigeonhole/tests/extensions/imap4flags/multiscript/spam.sieve
new file mode 100644
index 0000000..9e1b6c3
--- /dev/null
+++ b/pigeonhole/tests/extensions/imap4flags/multiscript/spam.sieve
@@ -0,0 +1,8 @@
+require ["fileinto"];
+
+if header :contains "X-Spam-Flag" ["Yes", "YES", "1"] {
+ fileinto "SPAM";
+}
+keep;
+
+
diff --git a/pigeonhole/tests/extensions/include/errors.svtest b/pigeonhole/tests/extensions/include/errors.svtest
new file mode 100644
index 0000000..6f8b1cc
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors.svtest
@@ -0,0 +1,149 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Generic include errors
+ */
+
+test "Generic" {
+ if test_script_compile "errors/generic.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Circular - direct" {
+ if test_script_compile "errors/circular-1.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Circular - one intermittent" {
+ if test_script_compile "errors/circular-2.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "4" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Circular - two intermittent" {
+ if test_script_compile "errors/circular-3.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";
+ }
+}
+
+/*
+ * Using global without variables required
+ */
+
+test "Variables inactive" {
+ if test_script_compile "errors/variables-inactive.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Generic variables errors
+ */
+
+test "Variables" {
+ if test_script_compile "errors/variables.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Global variable namespace
+ */
+
+test "Global Namespace" {
+ if test_script_compile "errors/global-namespace.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "4" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Invalid script names
+ */
+
+test "Invalid Script Names" {
+ if test_script_compile "errors/scriptname.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "8" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/* Include limit */
+
+test "Include limit" {
+ test_config_set "sieve_include_max_includes" "3";
+ test_config_reload :extension "include";
+
+ if test_script_compile "errors/include-limit.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+
+ test_config_set "sieve_include_max_includes" "255";
+ test_config_reload :extension "include";
+
+ if not test_script_compile "errors/include-limit.sieve" {
+ test_fail "compile should have succeeded";
+ }
+}
+
+/* Depth limit */
+
+test "Depth limit" {
+ test_config_set "sieve_include_max_nesting_depth" "2";
+ test_config_reload :extension "include";
+
+ if test_script_compile "errors/depth-limit.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "4" {
+ test_fail "wrong number of errors reported";
+ }
+
+ test_config_set "sieve_include_max_nesting_depth" "10";
+ test_config_reload :extension "include";
+
+ if not test_script_compile "errors/depth-limit.sieve" {
+ test_fail "compile should have succeeded";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/include/errors/action-conflicts.sieve b/pigeonhole/tests/extensions/include/errors/action-conflicts.sieve
new file mode 100644
index 0000000..ddeb42c
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/action-conflicts.sieve
@@ -0,0 +1,4 @@
+require "include";
+
+include "action-fileinto";
+include "action-reject";
diff --git a/pigeonhole/tests/extensions/include/errors/circular-1.sieve b/pigeonhole/tests/extensions/include/errors/circular-1.sieve
new file mode 100644
index 0000000..22b6f87
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/circular-1.sieve
@@ -0,0 +1,5 @@
+require "include";
+
+discard;
+
+include "circular-one";
diff --git a/pigeonhole/tests/extensions/include/errors/circular-2.sieve b/pigeonhole/tests/extensions/include/errors/circular-2.sieve
new file mode 100644
index 0000000..0cfa375
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/circular-2.sieve
@@ -0,0 +1,5 @@
+require "include";
+
+discard;
+
+include "circular-two";
diff --git a/pigeonhole/tests/extensions/include/errors/circular-3.sieve b/pigeonhole/tests/extensions/include/errors/circular-3.sieve
new file mode 100644
index 0000000..1ad95b6
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/circular-3.sieve
@@ -0,0 +1,5 @@
+require "include";
+
+discard;
+
+include "circular-three";
diff --git a/pigeonhole/tests/extensions/include/errors/depth-limit.sieve b/pigeonhole/tests/extensions/include/errors/depth-limit.sieve
new file mode 100644
index 0000000..93291b6
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/depth-limit.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include :personal "depth-limit-1";
diff --git a/pigeonhole/tests/extensions/include/errors/generic.sieve b/pigeonhole/tests/extensions/include/errors/generic.sieve
new file mode 100644
index 0000000..66eba18
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/generic.sieve
@@ -0,0 +1,7 @@
+require "include";
+
+# Non-existent sieve script
+include "frop.sieve";
+
+# Use of / in script names
+include "../frop.sieve";
diff --git a/pigeonhole/tests/extensions/include/errors/global-namespace.sieve b/pigeonhole/tests/extensions/include/errors/global-namespace.sieve
new file mode 100644
index 0000000..3827b60
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/global-namespace.sieve
@@ -0,0 +1,13 @@
+require "variables";
+require "include";
+
+# Invalid namespace
+set "globl.var" "frop";
+
+# Sub-namespace
+set "global.env.0" "12";
+
+# Invalid variable name
+set "global.12" "porf";
+
+
diff --git a/pigeonhole/tests/extensions/include/errors/include-limit.sieve b/pigeonhole/tests/extensions/include/errors/include-limit.sieve
new file mode 100644
index 0000000..f6689dd
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/include-limit.sieve
@@ -0,0 +1,6 @@
+require "include";
+
+include "rfc-ex1-always_allow";
+include "rfc-ex2-spam_filter_script";
+include "rfc-ex1-mailing_lists";
+include "rfc-ex1-spam_tests";
diff --git a/pigeonhole/tests/extensions/include/errors/scriptname.sieve b/pigeonhole/tests/extensions/include/errors/scriptname.sieve
new file mode 100644
index 0000000..9a10c3d
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/scriptname.sieve
@@ -0,0 +1,25 @@
+require "variables";
+require "include";
+require "encoded-character";
+
+# Slash
+include "../frop";
+
+# More slashes
+include "../../james/sieve/vacation";
+
+# 0000-001F; [CONTROL CHARACTERS]
+include "idiotic${unicode: 001a}";
+
+# 007F; DELETE
+include "idiotic${unicode: 007f}";
+
+# 0080-009F; [CONTROL CHARACTERS]
+include "idiotic${unicode: 0085}";
+
+# 2028; LINE SEPARATOR
+include "idiotic${unicode: 2028}";
+
+# 2029; PARAGRAPH SEPARATOR
+include "idiotic${unicode: 2029}";
+
diff --git a/pigeonhole/tests/extensions/include/errors/variables-inactive.sieve b/pigeonhole/tests/extensions/include/errors/variables-inactive.sieve
new file mode 100644
index 0000000..06e0df1
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/variables-inactive.sieve
@@ -0,0 +1,7 @@
+require "include";
+require "fileinto";
+
+global "friep";
+global "frop";
+
+fileinto "Frop";
diff --git a/pigeonhole/tests/extensions/include/errors/variables.sieve b/pigeonhole/tests/extensions/include/errors/variables.sieve
new file mode 100644
index 0000000..eac99f8
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/errors/variables.sieve
@@ -0,0 +1,23 @@
+require "include";
+require "variables";
+
+# Duplicate global declaration (not an error)
+global "frml";
+global "frml";
+
+keep;
+
+# Global after command not being require or global (not an error)
+global "friep";
+
+# DEPRECATED: import/export after command not being require or import/export
+export "friep";
+import "friep";
+
+# Marking local variable as global
+set "frutsels" "frop";
+global "frutsels";
+set "frutsels" "frop";
+
+
+
diff --git a/pigeonhole/tests/extensions/include/execute.svtest b/pigeonhole/tests/extensions/include/execute.svtest
new file mode 100644
index 0000000..734ac66
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/execute.svtest
@@ -0,0 +1,68 @@
+require "vnd.dovecot.testsuite";
+require "include";
+require "variables";
+
+test_set "message" text:
+From: idiot@example.com
+To: idiot@example.org
+Subject: Frop!
+
+Frop.
+.
+;
+
+test "Actions Fileinto" {
+ test_mailbox_create "aaaa";
+ test_mailbox_create "bbbb";
+
+ if not test_script_compile "execute/actions-fileinto.sieve" {
+ test_fail "failed to compile sieve script";
+ }
+
+ test_binary_save "actions-fileinto";
+ test_binary_load "actions-fileinto";
+
+ if not test_script_run {
+ test_fail "failed to execute sieve script";
+ }
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ test_message :folder "aaaa" 0;
+
+ if not header "subject" "Frop!" {
+ test_fail "fileinto \"aaaa\" not executed.";
+ }
+
+ test_message :folder "bbbb" 0;
+
+ if not header "subject" "Frop!" {
+ test_fail "fileinto \"bbbb\" not executed.";
+ }
+}
+
+test "Namespace - file" {
+ if not test_script_compile "execute/namespace.sieve" {
+ test_fail "failed to compile sub-test";
+ }
+
+ if not test_script_run {
+ test_fail "failed to execute sub-test";
+ }
+}
+
+test "Namespace - dict" {
+ test_config_set "sieve" "dict:file:${tst.path}/included/namespace.dict";
+ test_config_set "sieve_global" "dict:file:${tst.path}/included-global/namespace.dict";
+ test_config_reload :extension "include";
+
+ if not test_script_compile "execute/namespace.sieve" {
+ test_fail "failed to compile sub-test";
+ }
+
+ if not test_script_run {
+ test_fail "failed to execute sub-test";
+ }
+}
diff --git a/pigeonhole/tests/extensions/include/execute/actions-fileinto.sieve b/pigeonhole/tests/extensions/include/execute/actions-fileinto.sieve
new file mode 100644
index 0000000..b0b8157
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/execute/actions-fileinto.sieve
@@ -0,0 +1,5 @@
+require "include";
+
+include "actions-fileinto1";
+include "actions-fileinto2";
+include "actions-fileinto3";
diff --git a/pigeonhole/tests/extensions/include/execute/namespace.sieve b/pigeonhole/tests/extensions/include/execute/namespace.sieve
new file mode 100644
index 0000000..cbe41a2
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/execute/namespace.sieve
@@ -0,0 +1,26 @@
+require "vnd.dovecot.testsuite";
+require "include";
+require "variables";
+
+set "global.a" "none";
+include :personal "namespace";
+
+if string "${global.a}" "none" {
+ test_fail "personal script not executed";
+}
+
+if not string "${global.a}" "personal" {
+ test_fail "executed global instead of personal script: ${global.a}";
+}
+
+set "global.a" "none";
+include :global "namespace";
+
+if string "{global.a}" "none" {
+ test_fail "global script not executed";
+}
+
+if not string "${global.a}" "global" {
+ test_fail "executed personal instead of global script: ${global.a}";
+}
+
diff --git a/pigeonhole/tests/extensions/include/execute/optional.sieve b/pigeonhole/tests/extensions/include/execute/optional.sieve
new file mode 100644
index 0000000..a6ad479
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/execute/optional.sieve
@@ -0,0 +1,5 @@
+require "include";
+
+include :optional "optional-1";
+include :optional "optional-2";
+include :optional "optional-3";
diff --git a/pigeonhole/tests/extensions/include/included-global/namespace.dict b/pigeonhole/tests/extensions/include/included-global/namespace.dict
new file mode 100644
index 0000000..8f52fd3
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included-global/namespace.dict
@@ -0,0 +1,4 @@
+priv/sieve/name/namespace
+1
+priv/sieve/data/1
+require ["variables", "include"]; set "global.a" "global";
diff --git a/pigeonhole/tests/extensions/include/included-global/namespace.sieve b/pigeonhole/tests/extensions/include/included-global/namespace.sieve
new file mode 100644
index 0000000..d11c2f1
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included-global/namespace.sieve
@@ -0,0 +1,4 @@
+require "include";
+require "variables";
+
+set "global.a" "global";
diff --git a/pigeonhole/tests/extensions/include/included-global/rfc-ex1-spam_tests.sieve b/pigeonhole/tests/extensions/include/included-global/rfc-ex1-spam_tests.sieve
new file mode 100644
index 0000000..340ceaf
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included-global/rfc-ex1-spam_tests.sieve
@@ -0,0 +1,7 @@
+require ["reject"];
+
+if anyof (header :contains "Subject" "$$",
+ header :contains "Subject" "Make money")
+{
+ reject "Not wanted";
+}
diff --git a/pigeonhole/tests/extensions/include/included/action-fileinto.sieve b/pigeonhole/tests/extensions/include/included/action-fileinto.sieve
new file mode 100644
index 0000000..9aafb95
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/action-fileinto.sieve
@@ -0,0 +1,3 @@
+require "fileinto";
+
+fileinto "frop";
diff --git a/pigeonhole/tests/extensions/include/included/action-reject.sieve b/pigeonhole/tests/extensions/include/included/action-reject.sieve
new file mode 100644
index 0000000..6e7b0b0
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/action-reject.sieve
@@ -0,0 +1,3 @@
+require "reject";
+
+reject "Ik heb geen zin in die rommel.";
diff --git a/pigeonhole/tests/extensions/include/included/actions-fileinto1.sieve b/pigeonhole/tests/extensions/include/included/actions-fileinto1.sieve
new file mode 100644
index 0000000..d4c5031
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/actions-fileinto1.sieve
@@ -0,0 +1,3 @@
+require "fileinto";
+
+fileinto "aaaa";
diff --git a/pigeonhole/tests/extensions/include/included/actions-fileinto2.sieve b/pigeonhole/tests/extensions/include/included/actions-fileinto2.sieve
new file mode 100644
index 0000000..f73da0d
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/actions-fileinto2.sieve
@@ -0,0 +1,4 @@
+require "fileinto";
+
+fileinto "bbbb";
+
diff --git a/pigeonhole/tests/extensions/include/included/actions-fileinto3.sieve b/pigeonhole/tests/extensions/include/included/actions-fileinto3.sieve
new file mode 100644
index 0000000..d4c5031
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/actions-fileinto3.sieve
@@ -0,0 +1,3 @@
+require "fileinto";
+
+fileinto "aaaa";
diff --git a/pigeonhole/tests/extensions/include/included/circular-one.sieve b/pigeonhole/tests/extensions/include/included/circular-one.sieve
new file mode 100644
index 0000000..2d60606
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/circular-one.sieve
@@ -0,0 +1,5 @@
+require "include";
+
+keep;
+
+include "circular-one";
diff --git a/pigeonhole/tests/extensions/include/included/circular-three-2.sieve b/pigeonhole/tests/extensions/include/included/circular-three-2.sieve
new file mode 100644
index 0000000..5199f21
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/circular-three-2.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include "circular-three-3";
diff --git a/pigeonhole/tests/extensions/include/included/circular-three-3.sieve b/pigeonhole/tests/extensions/include/included/circular-three-3.sieve
new file mode 100644
index 0000000..4c062cd
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/circular-three-3.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include "circular-three.sieve";
diff --git a/pigeonhole/tests/extensions/include/included/circular-three.sieve b/pigeonhole/tests/extensions/include/included/circular-three.sieve
new file mode 100644
index 0000000..13be546
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/circular-three.sieve
@@ -0,0 +1,7 @@
+require "include";
+
+keep;
+
+include "circular-three-2";
+
+
diff --git a/pigeonhole/tests/extensions/include/included/circular-two-2.sieve b/pigeonhole/tests/extensions/include/included/circular-two-2.sieve
new file mode 100644
index 0000000..d529214
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/circular-two-2.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include "circular-two.sieve";
diff --git a/pigeonhole/tests/extensions/include/included/circular-two.sieve b/pigeonhole/tests/extensions/include/included/circular-two.sieve
new file mode 100644
index 0000000..8a879cb
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/circular-two.sieve
@@ -0,0 +1,7 @@
+require "include";
+
+keep;
+
+include "circular-two-2";
+
+
diff --git a/pigeonhole/tests/extensions/include/included/depth-limit-1.sieve b/pigeonhole/tests/extensions/include/included/depth-limit-1.sieve
new file mode 100644
index 0000000..ce5571f
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/depth-limit-1.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include :personal "depth-limit-2";
diff --git a/pigeonhole/tests/extensions/include/included/depth-limit-2.sieve b/pigeonhole/tests/extensions/include/included/depth-limit-2.sieve
new file mode 100644
index 0000000..79c55e0
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/depth-limit-2.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include :personal "depth-limit-3";
diff --git a/pigeonhole/tests/extensions/include/included/depth-limit-3.sieve b/pigeonhole/tests/extensions/include/included/depth-limit-3.sieve
new file mode 100644
index 0000000..6203a21
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/depth-limit-3.sieve
@@ -0,0 +1 @@
+keep;
diff --git a/pigeonhole/tests/extensions/include/included/namespace.dict b/pigeonhole/tests/extensions/include/included/namespace.dict
new file mode 100644
index 0000000..35d7aaa
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/namespace.dict
@@ -0,0 +1,4 @@
+priv/sieve/name/namespace
+1
+priv/sieve/data/1
+require ["variables", "include"]; set "global.a" "personal";
diff --git a/pigeonhole/tests/extensions/include/included/namespace.sieve b/pigeonhole/tests/extensions/include/included/namespace.sieve
new file mode 100644
index 0000000..3f5738f
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/namespace.sieve
@@ -0,0 +1,4 @@
+require "include";
+require "variables";
+
+set "global.a" "personal";
diff --git a/pigeonhole/tests/extensions/include/included/once-1.sieve b/pigeonhole/tests/extensions/include/included/once-1.sieve
new file mode 100644
index 0000000..288d141
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/once-1.sieve
@@ -0,0 +1,9 @@
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "${result} ONE";
+
+return;
+
diff --git a/pigeonhole/tests/extensions/include/included/once-2.sieve b/pigeonhole/tests/extensions/include/included/once-2.sieve
new file mode 100644
index 0000000..abf29e5
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/once-2.sieve
@@ -0,0 +1,12 @@
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "${result} TWO";
+
+keep;
+
+include :once "once-1";
+
+return;
diff --git a/pigeonhole/tests/extensions/include/included/once-3.sieve b/pigeonhole/tests/extensions/include/included/once-3.sieve
new file mode 100644
index 0000000..739651e
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/once-3.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include "once-4";
diff --git a/pigeonhole/tests/extensions/include/included/once-4.sieve b/pigeonhole/tests/extensions/include/included/once-4.sieve
new file mode 100644
index 0000000..9cc1a47
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/once-4.sieve
@@ -0,0 +1,3 @@
+require "include";
+
+include :once "once-3";
diff --git a/pigeonhole/tests/extensions/include/included/optional-1.sieve b/pigeonhole/tests/extensions/include/included/optional-1.sieve
new file mode 100644
index 0000000..288d141
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/optional-1.sieve
@@ -0,0 +1,9 @@
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "${result} ONE";
+
+return;
+
diff --git a/pigeonhole/tests/extensions/include/included/optional-2.sieve b/pigeonhole/tests/extensions/include/included/optional-2.sieve
new file mode 100644
index 0000000..11920f5
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/optional-2.sieve
@@ -0,0 +1,9 @@
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "${result} TWO";
+
+keep;
+
diff --git a/pigeonhole/tests/extensions/include/included/rfc-ex1-always_allow.sieve b/pigeonhole/tests/extensions/include/included/rfc-ex1-always_allow.sieve
new file mode 100644
index 0000000..6dc8ddc
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/rfc-ex1-always_allow.sieve
@@ -0,0 +1,8 @@
+if header :is "From" "boss@example.com"
+{
+ keep;
+}
+elsif header :is "From" "ceo@example.com"
+{
+ keep;
+}
diff --git a/pigeonhole/tests/extensions/include/included/rfc-ex1-mailing_lists.sieve b/pigeonhole/tests/extensions/include/included/rfc-ex1-mailing_lists.sieve
new file mode 100644
index 0000000..d020972
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/rfc-ex1-mailing_lists.sieve
@@ -0,0 +1,10 @@
+require ["fileinto"];
+
+if header :is "Sender" "owner-ietf-mta-filters@imc.example.com"
+{
+ fileinto "lists.sieve";
+}
+elsif header :is "Sender" "owner-ietf-imapext@imc.example.com"
+{
+ fileinto "lists.imapext";
+}
diff --git a/pigeonhole/tests/extensions/include/included/rfc-ex1-spam_tests.sieve b/pigeonhole/tests/extensions/include/included/rfc-ex1-spam_tests.sieve
new file mode 100644
index 0000000..7916064
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/rfc-ex1-spam_tests.sieve
@@ -0,0 +1,10 @@
+require ["reject"];
+
+if header :contains "Subject" "XXXX"
+{
+ reject "Not wanted";
+}
+elsif header :is "From" "money@example.com"
+{
+ reject "Not wanted";
+}
diff --git a/pigeonhole/tests/extensions/include/included/rfc-ex2-spam_filter_script.sieve b/pigeonhole/tests/extensions/include/included/rfc-ex2-spam_filter_script.sieve
new file mode 100644
index 0000000..01ab984
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/rfc-ex2-spam_filter_script.sieve
@@ -0,0 +1,8 @@
+require ["variables", "include"];
+global ["test", "test_mailbox"];
+
+if header :contains "Subject" "${test}"
+{
+ set "test_mailbox" "spam-${test}";
+}
+
diff --git a/pigeonhole/tests/extensions/include/included/twice-1.sieve b/pigeonhole/tests/extensions/include/included/twice-1.sieve
new file mode 100644
index 0000000..a770a3b
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/twice-1.sieve
@@ -0,0 +1,7 @@
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "${result} TWO";
+
diff --git a/pigeonhole/tests/extensions/include/included/twice-2.sieve b/pigeonhole/tests/extensions/include/included/twice-2.sieve
new file mode 100644
index 0000000..eff9429
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/twice-2.sieve
@@ -0,0 +1,8 @@
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "${result} THREE";
+
+include "twice-1";
diff --git a/pigeonhole/tests/extensions/include/included/variables-included1.sieve b/pigeonhole/tests/extensions/include/included/variables-included1.sieve
new file mode 100644
index 0000000..5f6cb2f
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/variables-included1.sieve
@@ -0,0 +1,7 @@
+require "include";
+require "variables";
+
+global ["value1"];
+global ["result1"];
+
+set "result1" "${value1} ${global.value2}";
diff --git a/pigeonhole/tests/extensions/include/included/variables-included2.sieve b/pigeonhole/tests/extensions/include/included/variables-included2.sieve
new file mode 100644
index 0000000..135e03b
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/variables-included2.sieve
@@ -0,0 +1,6 @@
+require "include";
+require "variables";
+
+global ["value3", "value4"];
+
+set "global.result2" "${value3} ${value4}";
diff --git a/pigeonhole/tests/extensions/include/included/variables-included3.sieve b/pigeonhole/tests/extensions/include/included/variables-included3.sieve
new file mode 100644
index 0000000..51bb786
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/included/variables-included3.sieve
@@ -0,0 +1,8 @@
+require "include";
+require "variables";
+
+global "result1";
+global "result2";
+global "result";
+
+set "result" "${result1} ${result2}";
diff --git a/pigeonhole/tests/extensions/include/once.svtest b/pigeonhole/tests/extensions/include/once.svtest
new file mode 100644
index 0000000..3395c6b
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/once.svtest
@@ -0,0 +1,24 @@
+require "vnd.dovecot.testsuite";
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "";
+
+test "Included Once" {
+ include "once-1";
+ include "once-2";
+
+ if string "${result}" " ONE TWO ONE" {
+ test_fail "duplicate included :once script";
+ }
+
+ if not string "${result}" " ONE TWO" {
+ test_fail "unexpected result value: ${result}";
+ }
+}
+
+test "Included Once recursive" {
+ include "once-3";
+}
diff --git a/pigeonhole/tests/extensions/include/optional.svtest b/pigeonhole/tests/extensions/include/optional.svtest
new file mode 100644
index 0000000..345f830
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/optional.svtest
@@ -0,0 +1,40 @@
+require "vnd.dovecot.testsuite";
+require "include";
+require "variables";
+
+global "result";
+set "result" "";
+
+test "Included Optional" {
+ include :optional "optional-1";
+ include :optional "optional-2";
+
+ if not string "${result}" " ONE TWO" {
+ test_fail "unexpected result value: ${result}";
+ }
+
+ # missing
+ include :optional "optional-3";
+
+ if not string "${result}" " ONE TWO" {
+ test_fail "unexpected result value after missing script: ${result}";
+ }
+}
+
+
+test "Included Optional - Binary" {
+ if not test_script_compile "execute/optional.sieve" {
+ test_fail "failed to compile sieve script";
+ }
+
+ test_binary_save "optional";
+ test_binary_load "optional";
+
+ if not test_script_run {
+ test_fail "failed to execute sieve script";
+ }
+
+ if not string "${result}" " ONE TWO" {
+ test_fail "unexpected result value: ${result}";
+ }
+}
diff --git a/pigeonhole/tests/extensions/include/rfc-ex1-default.sieve b/pigeonhole/tests/extensions/include/rfc-ex1-default.sieve
new file mode 100644
index 0000000..5a8cb52
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/rfc-ex1-default.sieve
@@ -0,0 +1,6 @@
+require ["include"];
+
+include :personal "rfc-ex1-always_allow";
+include :global "rfc-ex1-spam_tests";
+include :personal "rfc-ex1-spam_tests";
+include :personal "rfc-ex1-mailing_lists";
diff --git a/pigeonhole/tests/extensions/include/rfc-ex2-default.sieve b/pigeonhole/tests/extensions/include/rfc-ex2-default.sieve
new file mode 100644
index 0000000..8b1bf4d
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/rfc-ex2-default.sieve
@@ -0,0 +1,21 @@
+require ["variables", "include", "relational", "fileinto"];
+global "test";
+global "test_mailbox";
+
+# The included script may contain repetitive code that is
+# effectively a subroutine that can be factored out.
+set "test" "$$";
+include "rfc-ex2-spam_filter_script";
+
+set "test" "Make money";
+include "rfc-ex2-spam_filter_script";
+
+# Message will be filed according to the test that matched last.
+if string :count "eq" "${test_mailbox}" "1"
+{
+ fileinto "INBOX${test_mailbox}";
+ stop;
+}
+
+# If nothing matched, the message is implicitly kept.
+
diff --git a/pigeonhole/tests/extensions/include/rfc.svtest b/pigeonhole/tests/extensions/include/rfc.svtest
new file mode 100644
index 0000000..00908ac
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/rfc.svtest
@@ -0,0 +1,13 @@
+require "vnd.dovecot.testsuite";
+
+test "RFC example 1" {
+ if not test_script_compile "rfc-ex1-default.sieve" {
+ test_fail "failed to compile sieve script";
+ }
+}
+
+test "RFC example 2" {
+ if not test_script_compile "rfc-ex2-default.sieve" {
+ test_fail "failed to compile sieve script";
+ }
+}
diff --git a/pigeonhole/tests/extensions/include/twice.svtest b/pigeonhole/tests/extensions/include/twice.svtest
new file mode 100644
index 0000000..5cd5da2
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/twice.svtest
@@ -0,0 +1,20 @@
+require "vnd.dovecot.testsuite";
+require "include";
+require "variables";
+
+global "result";
+
+set "result" "ONE";
+
+test "Twice included" {
+ include "twice-1";
+ include "twice-2";
+
+ if string "${result}" "ONE TWO THREE" {
+ test_fail "duplicate include failed";
+ }
+
+ if not string "${result}" "ONE TWO THREE TWO" {
+ test_fail "unexpected result: ${result}";
+ }
+}
diff --git a/pigeonhole/tests/extensions/include/variables.svtest b/pigeonhole/tests/extensions/include/variables.svtest
new file mode 100644
index 0000000..5c4f8d8
--- /dev/null
+++ b/pigeonhole/tests/extensions/include/variables.svtest
@@ -0,0 +1,29 @@
+require "vnd.dovecot.testsuite";
+
+require "include";
+require "variables";
+
+global ["value1", "value2"];
+set "value1" "Works";
+set "value2" "fine.";
+
+global ["value3", "value4"];
+set "value3" "Yeah";
+set "value4" "it does.";
+
+include "variables-included1";
+include "variables-included2";
+include "variables-included3";
+
+global "result";
+
+test "Basic" {
+ if not string :is "${result}" "Works fine. Yeah it does." {
+ test_fail "invalid result: ${result}";
+ }
+
+ if string :is "${result}" "nonsense" {
+ test_fail "string test succeeds inappropriately";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/index/basic.svtest b/pigeonhole/tests/extensions/index/basic.svtest
new file mode 100644
index 0000000..0706022
--- /dev/null
+++ b/pigeonhole/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/pigeonhole/tests/extensions/index/errors.svtest b/pigeonhole/tests/extensions/index/errors.svtest
new file mode 100644
index 0000000..4bfe2dc
--- /dev/null
+++ b/pigeonhole/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" "7" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/index/errors/syntax.sieve b/pigeonhole/tests/extensions/index/errors/syntax.sieve
new file mode 100644
index 0000000..ee19d88
--- /dev/null
+++ b/pigeonhole/tests/extensions/index/errors/syntax.sieve
@@ -0,0 +1,26 @@
+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" {}
+
+# 5: index 0
+if header :index 0 "to" "ok" {}
+
+# 6: index 0 last
+if header :index 0 :last "to" "ok" {}
diff --git a/pigeonhole/tests/extensions/mailbox/errors.svtest b/pigeonhole/tests/extensions/mailbox/errors.svtest
new file mode 100644
index 0000000..0821f52
--- /dev/null
+++ b/pigeonhole/tests/extensions/mailbox/errors.svtest
@@ -0,0 +1,40 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "vnd.dovecot.debug";
+
+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";
+ }
+
+ # FIXME: check warnings
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "12" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Mailboxexists - bad UTF-8 in mailbox name
+ */
+
+test "Mailboxexists - bad UTF-8 in mailbox name" {
+ if not test_script_compile "errors/mailboxexists-bad-utf8.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execution failed";
+ }
+
+ # FIXME: check warnings
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "0" {
+ test_fail "wrong number of runtime errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/mailbox/errors/mailboxexists-bad-utf8.sieve b/pigeonhole/tests/extensions/mailbox/errors/mailboxexists-bad-utf8.sieve
new file mode 100644
index 0000000..e68e00e
--- /dev/null
+++ b/pigeonhole/tests/extensions/mailbox/errors/mailboxexists-bad-utf8.sieve
@@ -0,0 +1,9 @@
+require "mailbox";
+require "variables";
+require "encoded-character";
+
+set "mailbox" "${hex:ff}rop";
+if mailboxexists "${mailbox}" {
+ keep;
+}
+
diff --git a/pigeonhole/tests/extensions/mailbox/errors/syntax.sieve b/pigeonhole/tests/extensions/mailbox/errors/syntax.sieve
new file mode 100644
index 0000000..727a6e8
--- /dev/null
+++ b/pigeonhole/tests/extensions/mailbox/errors/syntax.sieve
@@ -0,0 +1,41 @@
+require "mailbox";
+require "fileinto";
+require "encoded-character";
+
+# 1
+if mailboxexists {}
+# 2
+if mailboxexists 3423 {}
+# 3
+if mailboxexists :frop {}
+# 4
+if mailboxexists 24234 "\\Sent" {}
+# 5
+if mailboxexists "frop" 32234 {}
+# 6
+if mailboxexists "frop" :friep {}
+
+if mailboxexists "frop" {}
+if mailboxexists ["frop", "friep"] {}
+
+# W:1
+if mailboxexists "${hex:ff}rop" {}
+# W:2
+if mailboxexists ["frop", "${hex:ff}riep"] {}
+
+# 7
+if mailboxexists "frop" ["frop"] {}
+
+# 8
+fileinto :create 343 "frop";
+# 9
+fileinto :create :frop "frop";
+# 10
+fileinto :create 234234;
+
+fileinto :create "frop";
+
+# 11
+fileinto :create "${hex:ff}rop";
+
+
diff --git a/pigeonhole/tests/extensions/mailbox/execute.svtest b/pigeonhole/tests/extensions/mailbox/execute.svtest
new file mode 100644
index 0000000..cba3034
--- /dev/null
+++ b/pigeonhole/tests/extensions/mailbox/execute.svtest
@@ -0,0 +1,80 @@
+require "vnd.dovecot.testsuite";
+require "mailbox";
+require "fileinto";
+
+test "MailboxExists - None exist" {
+ if mailboxexists "frop" {
+ test_fail "mailboxexists confirms existance of unknown folder";
+ }
+}
+
+test_mailbox_create "frop";
+test_mailbox_create "friep";
+
+test "MailboxExists - Not all exist" {
+ if mailboxexists ["frop", "friep", "frml"] {
+ test_fail "mailboxexists confirms existance of unknown folder";
+ }
+}
+
+test_mailbox_create "frml";
+
+test "MailboxExists - One exists" {
+ if not mailboxexists ["frop"] {
+ test_fail "mailboxexists fails to recognize folder";
+ }
+}
+
+test "MailboxExists - All exist" {
+ if not mailboxexists ["frop", "friep", "frml"] {
+ test_fail "mailboxexists fails to recognize folders";
+ }
+}
+
+test ":Create" {
+ if mailboxexists "created" {
+ test_fail "mailbox exists already";
+ }
+
+ test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop 1
+
+Frop!
+.
+ ;
+
+ fileinto :create "created";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ if not mailboxexists "created" {
+ test_fail "mailbox somehow not created";
+ }
+
+ test_result_reset;
+
+ test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop 2
+
+Frop!
+.
+ ;
+
+ fileinto "created";
+
+ if not test_result_execute {
+ test_fail "execution of result failed second time";
+ }
+
+ test_message :folder "created" 0;
+
+ if not header :is "subject" "Frop 1" {
+ test_fail "incorrect message read back from mail store";
+ }
+}
diff --git a/pigeonhole/tests/extensions/metadata/errors.svtest b/pigeonhole/tests/extensions/metadata/errors.svtest
new file mode 100644
index 0000000..3602484
--- /dev/null
+++ b/pigeonhole/tests/extensions/metadata/errors.svtest
@@ -0,0 +1,56 @@
+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" "27" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Metadataexists - bad UTF-8 in mailbox name
+ */
+
+test "Metadataexists - bad UTF-8 in mailbox name" {
+ if not test_script_compile "errors/metadataexists-bad-utf8.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execution failed";
+ }
+
+ # FIXME: check warnings
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "0" {
+ test_fail "wrong number of runtime errors reported";
+ }
+}
+
+/*
+ * Metadata - bad UTF-8 in mailbox name
+ */
+
+test "Metadata - bad UTF-8 in mailbox name" {
+ if not test_script_compile "errors/metadata-bad-utf8.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execution failed";
+ }
+
+ # FIXME: check warnings
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "0" {
+ test_fail "wrong number of runtime errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/metadata/errors/metadata-bad-utf8.sieve b/pigeonhole/tests/extensions/metadata/errors/metadata-bad-utf8.sieve
new file mode 100644
index 0000000..fd093a3
--- /dev/null
+++ b/pigeonhole/tests/extensions/metadata/errors/metadata-bad-utf8.sieve
@@ -0,0 +1,9 @@
+require "mboxmetadata";
+require "variables";
+require "encoded-character";
+
+set "mailbox" "${hex:ff}rop";
+if metadata "${mailbox}" "/private/frop" "friep" {
+ keep;
+}
+
diff --git a/pigeonhole/tests/extensions/metadata/errors/metadataexists-bad-utf8.sieve b/pigeonhole/tests/extensions/metadata/errors/metadataexists-bad-utf8.sieve
new file mode 100644
index 0000000..dbb5023
--- /dev/null
+++ b/pigeonhole/tests/extensions/metadata/errors/metadataexists-bad-utf8.sieve
@@ -0,0 +1,9 @@
+require "mboxmetadata";
+require "variables";
+require "encoded-character";
+
+set "mailbox" "${hex:ff}rop";
+if metadataexists "${mailbox}" ["/private/frop", "/shared/friep"] {
+ keep;
+}
+
diff --git a/pigeonhole/tests/extensions/metadata/errors/syntax.sieve b/pigeonhole/tests/extensions/metadata/errors/syntax.sieve
new file mode 100644
index 0000000..c719d94
--- /dev/null
+++ b/pigeonhole/tests/extensions/metadata/errors/syntax.sieve
@@ -0,0 +1,53 @@
+require "mboxmetadata";
+require "servermetadata";
+require "encoded-character";
+
+# 1-4: Used as a command
+metadata;
+metadataexists;
+servermetadata;
+servermetadataexists;
+
+# 5-8: Used with no argument
+if metadata {}
+if metadataexists {}
+if servermetadata {}
+if servermetadataexists {}
+
+# 9-11: Used with one string argument
+if metadata "frop" { }
+if servermetadata "frop" { }
+if metadataexists "frop" { }
+
+# 12-15: Used with one number argument
+if metadata 13123123 { }
+if servermetadata 123123 { }
+if metadataexists 123123 { }
+if servermetadataexists 123123 {}
+
+# 16-18: Used with one string list argument
+if metadata ["frop"] { }
+if servermetadata ["frop"] { }
+if metadataexists ["frop"] { }
+
+# 19-22: Used with unknown tag
+if metadata :frop "frop" { }
+if servermetadata :frop "frop" { }
+if metadataexists :frop "frop" { }
+if servermetadataexists :frop "frop" {}
+
+# 23-26: Invalid arguments
+if metadata "/private/frop" "friep" {}
+if servermetadata "INBOX" "/private/frop" "friep" {}
+if metadataexists 23 "/private/frop" {}
+if servermetadataexists "INBOX" "/private/frop" {}
+
+# W1-W4: Invalid annotations
+if metadata "INBOX" "frop" "friep" {}
+if servermetadata "frop" "friep" {}
+if metadataexists "INBOX" ["/private/frop", "/friep"] { }
+if servermetadataexists ["/private/frop", "/friep", "/private/friep"] { }
+
+# W5-W6: Invalid mailbox name
+if metadata "${hex:ff}rop" "/private/frop" "friep" {}
+if metadataexists "${hex:ff}rop" ["/private/frop", "/shared/friep"] { }
diff --git a/pigeonhole/tests/extensions/metadata/execute.svtest b/pigeonhole/tests/extensions/metadata/execute.svtest
new file mode 100644
index 0000000..32aac82
--- /dev/null
+++ b/pigeonhole/tests/extensions/metadata/execute.svtest
@@ -0,0 +1,145 @@
+require "vnd.dovecot.testsuite";
+require "mboxmetadata";
+require "servermetadata";
+require "fileinto";
+
+test "MetadataExists - None exist" {
+ if metadataexists "INBOX" "/private/frop" {
+ test_fail "metadataexists confirms existence of unknown annotation";
+ }
+}
+
+test_imap_metadata_set :mailbox "INBOX" "/private/frop" "FROP!";
+test_imap_metadata_set :mailbox "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 :mailbox "INBOX" "/private/friep" "FRIEP!";
+test_imap_metadata_set :mailbox "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 "MetadataExists - Invalid" {
+ if metadataexists "INBOX"
+ ["/shared/frop", "/friep", "/private/frml"] {
+ test_fail "metadataexists accepted invalid annotation name";
+ }
+}
+
+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";
+ }
+}
+
+test "Metadata - Invalid" {
+ if metadata :contains "INBOX" "/frop" "" {
+ test_fail "erroneously found a value for \"/frop\"";
+ }
+}
+
+test "ServermetadataExists - None exist" {
+ if servermetadataexists "/private/frop" {
+ test_fail "servermetadataexists confirms existence of unknown annotation";
+ }
+}
+
+# currently not possible to test servermetadata
+if false {
+
+test_imap_metadata_set "/private/frop" "FROP!";
+test_imap_metadata_set "/private/friep" "FRIEP!";
+
+test "ServermetadataExists - Not all exist" {
+ if servermetadataexists
+ ["/private/frop", "/private/friep", "/private/frml"] {
+ test_fail "metadataexists confirms existence of unknown annotation";
+ }
+}
+
+test_imap_metadata_set "/private/friep" "FRIEP!";
+test_imap_metadata_set "/private/frml" "FRML!";
+
+test "ServermetadataExists - One exists" {
+ if not servermetadataexists ["/private/frop"] {
+ test_fail "servermetadataexists fails to recognize annotation";
+ }
+}
+
+test "ServermetadataExists - All exist" {
+ if not servermetadataexists
+ ["/private/frop", "/private/friep", "/private/frml"] {
+ test_fail "servermetadataexists fails to recognize annotations";
+ }
+}
+
+test "ServermetadataExists - Invalid" {
+ if servermetadataexists
+ ["frop", "/private/friep", "/private/frml"] {
+ test_fail "servermetadataexists accepted invalid annotation name";
+ }
+}
+
+test "Servermetadata" {
+ if not servermetadata :is "/private/frop" "FROP!" {
+ test_fail "invalid servermetadata value for /private/frop";
+ }
+ if servermetadata :is "/private/frop" "Hutsefluts" {
+ test_fail "unexpected match for /private/frop";
+ }
+
+ if not servermetadata :is "/private/friep" "FRIEP!" {
+ test_fail "invalid servermetadata value for /private/friep";
+ }
+ if servermetadata :is "/private/friep" "Hutsefluts" {
+ test_fail "unexpected match for /private/friep";
+ }
+
+ if not servermetadata :is "/private/frml" "FRML!" {
+ test_fail "invalid servermetadata value for /private/frml";
+ }
+ if servermetadata :is "/private/frml" "Hutsefluts" {
+ test_fail "unexpected match for /private/frml";
+ }
+}
+
+test "Servermetadata - Invalid" {
+ if servermetadata :contains "/frop" "" {
+ test_fail "erroneously found a value for \"/frop\"";
+ }
+}
+
+} #disabled
diff --git a/pigeonhole/tests/extensions/mime/address.svtest b/pigeonhole/tests/extensions/mime/address.svtest
new file mode 100644
index 0000000..1607450
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/address.svtest
@@ -0,0 +1,281 @@
+require "vnd.dovecot.testsuite";
+require "mime";
+require "foreverypart";
+
+/*
+ * Basic functionionality
+ */
+
+test_set "message" text:
+From: stephan@example.com
+To: nico@nl.example.com, harry@de.example.com
+cc: Timo <tss(no spam)@fi.iki>
+Subject: Frobnitzm
+
+Test.
+.
+;
+
+test "Basic functionality" {
+ /* Must match */
+ if not address :mime :anychild :contains ["to", "from"] "harry" {
+ test_fail "failed to match address (1)";
+ }
+
+ if not address :mime :anychild :contains ["to", "from"] "de.example" {
+ test_fail "failed to match address (2)";
+ }
+
+ if not address :mime :anychild :matches "to" "*@*.example.com" {
+ test_fail "failed to match address (3)";
+ }
+
+ if not address :mime :anychild :is "to" "harry@de.example.com" {
+ test_fail "failed to match address (4)";
+ }
+
+ /* Must not match */
+ if address :mime :anychild :is ["to", "from"] "nonsense@example.com" {
+ test_fail "matches erroneous address";
+ }
+
+ /* Match first key */
+ if not address :mime :anychild :contains ["to"] ["nico", "fred", "henk"] {
+ test_fail "failed to match first key";
+ }
+
+ /* Match second key */
+ if not address :mime :anychild :contains ["to"] ["fred", "nico", "henk"] {
+ test_fail "failed to match second key";
+ }
+
+ /* Match last key */
+ if not address :mime :anychild :contains ["to"] ["fred", "henk", "nico"] {
+ test_fail "failed to match last key";
+ }
+
+ /* First header */
+ if not address :mime :anychild :contains
+ ["to", "from"] ["fred", "nico", "henk"] {
+ test_fail "failed to match first header";
+ }
+
+ /* Second header */
+ if not address :mime :anychild :contains
+ ["from", "to"] ["fred", "nico", "henk"] {
+ test_fail "failed to match second header";
+ }
+
+ /* Comment */
+ if not address :mime :anychild :is "cc" "tss@fi.iki" {
+ test_fail "failed to ignore comment in address";
+ }
+}
+
+/*
+ * Basic functionionality - foreverypart
+ */
+
+test "Basic functionality - foreverypart" {
+ foreverypart {
+ /* Must match */
+ if not address :mime :anychild :contains ["to", "from"] "harry" {
+ test_fail "failed to match address (1)";
+ }
+
+ if not address :mime :anychild :contains ["to", "from"] "de.example" {
+ test_fail "failed to match address (2)";
+ }
+
+ if not address :mime :anychild :matches "to" "*@*.example.com" {
+ test_fail "failed to match address (3)";
+ }
+
+ if not address :mime :anychild :is "to" "harry@de.example.com" {
+ test_fail "failed to match address (4)";
+ }
+
+ /* Must not match */
+ if address :mime :anychild :is ["to", "from"] "nonsense@example.com" {
+ test_fail "matches erroneous address";
+ }
+
+ /* Match first key */
+ if not address :mime :anychild :contains ["to"] ["nico", "fred", "henk"] {
+ test_fail "failed to match first key";
+ }
+
+ /* Match second key */
+ if not address :mime :anychild :contains ["to"] ["fred", "nico", "henk"] {
+ test_fail "failed to match second key";
+ }
+
+ /* Match last key */
+ if not address :mime :anychild :contains ["to"] ["fred", "henk", "nico"] {
+ test_fail "failed to match last key";
+ }
+
+ /* First header */
+ if not address :mime :anychild :contains
+ ["to", "from"] ["fred", "nico", "henk"] {
+ test_fail "failed to match first header";
+ }
+
+ /* Second header */
+ if not address :mime :anychild :contains
+ ["from", "to"] ["fred", "nico", "henk"] {
+ test_fail "failed to match second header";
+ }
+
+ /* Comment */
+ if not address :mime :anychild :is "cc" "tss@fi.iki" {
+ test_fail "failed to ignore comment in address";
+ }
+ }
+}
+
+/*
+ * Address headers
+ */
+
+test_set "message" text:
+From: stephan@friep.frop
+To: henk@tukkerland.ex
+CC: ivo@boer.ex
+Bcc: joop@hooibaal.ex
+Sender: s.bosch@friep.frop
+Resent-From: ivo@boer.ex
+Resent-To: idioot@dombo.ex
+Subject: Berichtje
+
+Test.
+.
+;
+
+test "Address headers" {
+ if not address :mime :anychild "from" "stephan@friep.frop" {
+ test_fail "from header not recognized";
+ }
+
+ if not address :mime :anychild "to" "henk@tukkerland.ex" {
+ test_fail "to header not recognized";
+ }
+
+ if not address :mime :anychild "cc" "ivo@boer.ex" {
+ test_fail "cc header not recognized";
+ }
+
+ if not address :mime :anychild "bcc" "joop@hooibaal.ex" {
+ test_fail "bcc header not recognized";
+ }
+
+ if not address :mime :anychild "sender" "s.bosch@friep.frop" {
+ test_fail "sender header not recognized";
+ }
+
+ if not address :mime :anychild "resent-from" "ivo@boer.ex" {
+ test_fail "resent-from header not recognized";
+ }
+
+ if not address :mime :anychild "resent-to" "idioot@dombo.ex" {
+ test_fail "resent-to header not recognized";
+ }
+}
+
+/*
+ * Address headers - foreverypart
+ */
+
+test "Address headers - foreverypart" {
+ foreverypart {
+ if not address :mime :anychild "from" "stephan@friep.frop" {
+ test_fail "from header not recognized";
+ }
+
+ if not address :mime :anychild "to" "henk@tukkerland.ex" {
+ test_fail "to header not recognized";
+ }
+
+ if not address :mime :anychild "cc" "ivo@boer.ex" {
+ test_fail "cc header not recognized";
+ }
+
+ if not address :mime :anychild "bcc" "joop@hooibaal.ex" {
+ test_fail "bcc header not recognized";
+ }
+
+ if not address :mime :anychild "sender" "s.bosch@friep.frop" {
+ test_fail "sender header not recognized";
+ }
+
+ if not address :mime :anychild "resent-from" "ivo@boer.ex" {
+ test_fail "resent-from header not recognized";
+ }
+
+ if not address :mime :anychild "resent-to" "idioot@dombo.ex" {
+ test_fail "resent-to header not recognized";
+ }
+ }
+}
+
+/*
+ * Multipart anychild
+ */
+
+test_set "message" text:
+From: Hendrik <hendrik@example.com>
+To: Harrie <harrie@example.com>
+Date: Sat, 11 Oct 2010 00:31:44 +0200
+Subject: Harrie is een prutser
+Content-Type: multipart/mixed; boundary=AA
+CC: AA@example.com
+
+This is a multi-part message in MIME format.
+--AA
+Content-Type: multipart/mixed; boundary=BB
+CC: BB@example.com
+
+This is a multi-part message in MIME format.
+--BB
+Content-Type: text/plain; charset="us-ascii"
+CC: CC@example.com
+
+Hello
+
+--BB
+Content-Type: text/plain; charset="us-ascii"
+CC: DD@example.com
+
+Hello again
+
+--BB--
+This is the end of MIME multipart.
+
+--AA
+Content-Type: text/plain; charset="us-ascii"
+CC: EE@example.com
+
+And again
+
+--AA--
+This is the end of MIME multipart.
+.
+;
+
+test "Multipart anychild" {
+ if not address :mime :anychild :localpart "Cc" "AA" {
+ test_fail "AA Cc repient does not exist";
+ }
+ if not address :mime :anychild :localpart "Cc" "BB" {
+ test_fail "BB Cc repient does not exist";
+ }
+ if not address :mime :anychild :localpart "Cc" "CC" {
+ test_fail "CC Cc repient does not exist";
+ }
+ if not address :mime :anychild :localpart "Cc" "DD" {
+ test_fail "DD Cc repient does not exist";
+ }
+ if not address :mime :anychild :localpart "Cc" "EE" {
+ test_fail "EE Cc repient does not exist";
+ }
+}
diff --git a/pigeonhole/tests/extensions/mime/calendar-example.svtest b/pigeonhole/tests/extensions/mime/calendar-example.svtest
new file mode 100644
index 0000000..745e6e6
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/calendar-example.svtest
@@ -0,0 +1,129 @@
+require "vnd.dovecot.testsuite";
+require "mime";
+require "foreverypart";
+require "editheader";
+require "relational";
+require "variables";
+
+# Example from RFC 6047, Section 2.5:
+test_set "message" text:
+From: user1@example.com
+To: user2@example.com
+Subject: Phone Conference
+Mime-Version: 1.0
+Date: Wed, 07 May 2008 21:30:25 +0400
+Message-ID: <4821E731.5040506@laptop1.example.com>
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+PRODID:-//Example/ExampleCalendarClient//EN
+METHOD:REQUEST
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER:mailto:user1@example.com
+ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:user1@example.com
+ATTENDEE;RSVP=YES;CUTYPE=INDIVIDUAL:mailto:user2@example.com
+DTSTAMP:20080507T170000Z
+DTSTART:20080701T160000Z
+DTEND:20080701T163000Z
+SUMMARY:Phone call to discuss your last visit
+DESCRIPTION:=D1=82=D1=8B =D0=BA=D0=B0=D0=BA - =D0=B4=D0=BE=D0=
+ =B2=D0=BE=D0=BB=D0=B5=D0=BD =D0=BF=D0=BE=D0=B5=D0=B7=D0=B4=D0=BA=D0
+ =BE=D0=B9?
+UID:calsvr.example.com-8739701987387998
+SEQUENCE:0
+STATUS:TENTATIVE
+END:VEVENT
+END:VCALENDAR
+.
+;
+
+test "Calendar only" {
+ foreverypart {
+ if allof(
+ header :mime :count "eq" "Content-Type" "1",
+ header :mime :contenttype "Content-Type" "text/calendar",
+ header :mime :param "method" :matches "Content-Type" "*",
+ header :mime :param "charset" :is "Content-Type" "UTF-8" ) {
+ addheader "X-ICAL" "${1}";
+ break;
+ }
+ }
+
+ if not header "x-ical" "request" {
+ test_fail "Failed to parse message correctly";
+ }
+}
+
+# Modified example
+test_set "message" text:
+From: user1@example.com
+To: user2@example.com
+Subject: Phone Conference
+Mime-Version: 1.0
+Date: Wed, 07 May 2008 21:30:25 +0400
+Message-ID: <4821E731.5040506@laptop1.example.com>
+Content-Type: multipart/mixed; boundary=AA
+
+This is a multi-part message in MIME format.
+
+--AA
+Content-Type: text/plain
+
+Hello,
+
+I'd like to discuss your last visit. A tentative meeting schedule is
+attached.
+
+Regards,
+
+User1
+
+--AA
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+PRODID:-//Example/ExampleCalendarClient//EN
+METHOD:REQUEST
+VERSION:2.0
+BEGIN:VEVENT
+ORGANIZER:mailto:user1@example.com
+ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:user1@example.com
+ATTENDEE;RSVP=YES;CUTYPE=INDIVIDUAL:mailto:user2@example.com
+DTSTAMP:20080507T170000Z
+DTSTART:20080701T160000Z
+DTEND:20080701T163000Z
+SUMMARY:Phone call to discuss your last visit
+DESCRIPTION:=D1=82=D1=8B =D0=BA=D0=B0=D0=BA - =D0=B4=D0=BE=D0=
+ =B2=D0=BE=D0=BB=D0=B5=D0=BD =D0=BF=D0=BE=D0=B5=D0=B7=D0=B4=D0=BA=D0
+ =BE=D0=B9?
+UID:calsvr.example.com-8739701987387998
+SEQUENCE:0
+STATUS:TENTATIVE
+END:VEVENT
+END:VCALENDAR
+
+--AA--
+.
+;
+
+test "Multipart message" {
+ foreverypart {
+ if allof(
+ header :mime :count "eq" "Content-Type" "1",
+ header :mime :contenttype "Content-Type" "text/calendar",
+ header :mime :param "method" :matches "Content-Type" "*",
+ header :mime :param "charset" :is "Content-Type" "UTF-8" ) {
+ addheader "X-ICAL" "${1}";
+ break;
+ }
+ }
+
+ if not header "x-ical" "request" {
+ test_fail "Failed to parse message correctly";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/content-header.svtest b/pigeonhole/tests/extensions/mime/content-header.svtest
new file mode 100644
index 0000000..9686e35
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/content-header.svtest
@@ -0,0 +1,161 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "mime";
+
+test_set "message" text:
+From: stephan@example.com
+To: timo@example.com
+Subject: Frop
+Content-Type: text/plain
+
+Frop
+.
+;
+
+test "Simple Content-Type :type" {
+ if not header :mime :type "content-type" "text" {
+ test_fail "wrong type extracted";
+ }
+}
+
+test "Simple Content-Type :subype" {
+ if not header :mime :subtype "content-type" "plain" {
+ test_fail "wrong subtype extracted";
+ }
+}
+
+test "Simple Content-Type :contenttype" {
+ if not header :mime :contenttype "content-type" "text/plain" {
+ test_fail "wrong contenttype extracted";
+ }
+}
+
+test_set "message" text:
+From: stephan@example.com
+To: timo@example.com
+Subject: Frop
+Content-Type: text/calendar; method=request; charset=UTF-8;
+
+Frop
+.
+;
+
+test "Advanced Content-Type :type" {
+ if not header :mime :type "content-type" "text" {
+ test_fail "wrong type extracted";
+ }
+}
+
+test "Advanced Content-Type :subype" {
+ if not header :mime :subtype "content-type" "calendar" {
+ test_fail "wrong subtype extracted";
+ }
+}
+
+test "Advanced Content-Type :contenttype" {
+ if not header :mime :contenttype "content-type" "text/calendar" {
+ test_fail "wrong contenttype extracted";
+ }
+}
+
+test "Advanced Content-Type :param" {
+ if not header :mime :param "method" "content-type" "request" {
+ test_fail "wrong method param extracted";
+ }
+
+ if not header :mime :param "charset" "content-type" "UTF-8" {
+ test_fail "wrong charset param extracted";
+ }
+
+ if not header :mime :param ["method", "charset"]
+ "content-type" "request" {
+ test_fail "wrong method param extracted";
+ }
+
+ if not header :mime :param ["method", "charset"]
+ "content-type" "UTF-8" {
+ test_fail "wrong charset param extracted";
+ }
+
+ if not header :count "eq" :mime :param ["method", "charset"]
+ "content-type" "2" {
+ test_fail "wrong number of parameters";
+ }
+}
+
+test_set "message" text:
+From: stephan@example.com
+To: timo@example.com
+Subject: Frop
+Content-Type: application/x-stuff;
+ title*0*=us-ascii'en'This%20is%20even%20more%20;
+ title*1*=%2A%2A%2Afun%2A%2A%2A%20;
+ title*2="isn't it!"
+
+Frop
+.
+;
+
+test "Encoded Content-Type :param" {
+ if not header :mime :param "title" "content-type"
+ "This is even more ***fun*** isn't it!" {
+ test_fail "wrong method param extracted";
+ }
+}
+
+test_set "message" text:
+From: stephan@example.com
+To: timo@example.com
+Subject: Frop
+Content-Type: image/png
+Content-Disposition: inline; filename="frop.exe"; title="Frop!"
+
+Frop
+.
+;
+
+test "Content-Disposition :type" {
+ if not header :mime :type "content-disposition" "inline" {
+ test_fail "wrong type extracted";
+ }
+}
+
+test "Content-Disposition :subype" {
+ if not header :mime :subtype "content-disposition" "" {
+ test_fail "wrong subtype extracted";
+ }
+}
+
+test "Content-Disposition :contenttype" {
+ if not header :mime :contenttype "content-disposition" "inline" {
+ test_fail "wrong contenttype extracted";
+ }
+}
+
+test "Content-Disposition :param" {
+ if not header :mime :param "filename" "content-disposition" "frop.exe" {
+ test_fail "wrong filename param extracted";
+ }
+
+ if not header :mime :param "title" "content-disposition" "Frop!" {
+ test_fail "wrong title param extracted";
+ }
+
+ if not header :mime :param ["filename", "title"]
+ "content-disposition" "frop.exe" {
+ test_fail "wrong filename param extracted";
+ }
+
+ if not header :mime :param ["filename", "title"]
+ "content-disposition" "Frop!" {
+ test_fail "wrong title param extracted";
+ }
+
+ if not header :count "eq" :mime :param ["filename", "title"]
+ "content-disposition" "2" {
+ test_fail "wrong number of parameters";
+ }
+
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/errors.svtest b/pigeonhole/tests/extensions/mime/errors.svtest
new file mode 100644
index 0000000..b3b858e
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors.svtest
@@ -0,0 +1,162 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test "Foreverypart command" {
+ if test_script_compile "errors/foreverypart.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "12" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Break command" {
+ if test_script_compile "errors/break.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "21" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Header test with :mime tag" {
+ if test_script_compile "errors/header-mime-tag.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "10" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Address test with :mime tag" {
+ if test_script_compile "errors/address-mime-tag.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "6" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Exists test with :mime tag" {
+ if test_script_compile "errors/exists-mime-tag.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "6" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Limits" {
+ if test_script_compile "errors/limits.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "2" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test_set "message" text:
+From: Whomever <whoever@example.com>
+To: Someone <someone@example.com>
+Date: Sat, 10 Oct 2009 00:30:04 +0200
+Subject: whatever
+Content-Type: multipart/mixed; boundary=AA
+
+This is a multi-part message in MIME format.
+
+--AA
+Content-Type: multipart/alternative; boundary=BB
+
+This is a multi-part message in MIME format.
+
+--BB
+Content-Type: multipart/alternative; boundary=CC
+
+This is a multi-part message in MIME format.
+
+--CC
+Content-Type: multipart/alternative; boundary=DD
+
+This is a multi-part message in MIME format.
+
+--DD
+Content-Type: multipart/alternative; boundary=EE
+
+This is a nested multi-part message in MIME format.
+
+--EE
+Content-Type: text/plain; charset="us-ascii"
+
+Hello
+
+--EE--
+
+This is the end of the inner MIME multipart.
+
+--DD--
+
+This is the end of the MIME multipart.
+
+--CC--
+
+This is the end of the MIME multipart.
+
+--BB--
+
+This is the end of the MIME multipart.
+
+--AA--
+
+This is the end of the MIME multipart.
+.
+;
+
+test "Limits - include" {
+ if not test_script_compile "errors/limits-include.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if test_script_run {
+ test_fail "script run should have failed";
+ }
+}
+
+test "Extracttext" {
+ if test_script_compile "errors/extracttext.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "11" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Extracttext - without variables" {
+ if test_script_compile "errors/extracttext-novar.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "2" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+test "Extracttext - without foreverypart" {
+ if test_script_compile "errors/extracttext-nofep.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" :comparator "i;ascii-numeric" "2" {
+ test_fail "incorrect number of compile errors reported";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/errors/address-mime-tag.sieve b/pigeonhole/tests/extensions/mime/errors/address-mime-tag.sieve
new file mode 100644
index 0000000..7adb7bc
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/address-mime-tag.sieve
@@ -0,0 +1,38 @@
+require "mime";
+
+## Address
+
+# No error
+if address :contains :mime "To" "frop@example.com" {
+ discard;
+}
+
+# No error
+if address :anychild :contains :mime "To" "frop@example.com" {
+ discard;
+}
+
+# 1: Bare anychild option
+if address :anychild "To" "frop@example.com" {
+ discard;
+}
+
+# 2: Inappropriate option
+if address :mime :anychild :type "To" "frop@example.com" {
+ discard;
+}
+
+# 3: Inappropriate option
+if address :mime :anychild :subtype "To" "frop@example.com" {
+ discard;
+}
+
+# 4: Inappropriate option
+if address :mime :anychild :contenttype "To" "frop@example.com" {
+ discard;
+}
+
+# 5: Inappropriate option
+if address :mime :anychild :param "frop" "To" "frop@example.com" {
+ discard;
+}
diff --git a/pigeonhole/tests/extensions/mime/errors/break.sieve b/pigeonhole/tests/extensions/mime/errors/break.sieve
new file mode 100644
index 0000000..1858673
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/break.sieve
@@ -0,0 +1,157 @@
+require "foreverypart";
+
+foreverypart :name "frop" {
+ # 1: Spurious tag
+ break :tag;
+
+ # 2: Spurious tests
+ break true;
+
+ # 3: Spurious tests
+ break anyof(true, false);
+
+ # 4: Bare string
+ break "frop";
+
+ # 5: Bare string-list
+ break ["frop", "friep"];
+
+ # 6: Several bad arguments
+ break 13 ["frop", "friep"];
+
+ # 7: Spurious additional tag
+ break :name "frop" :friep;
+
+ # 8: Spurious additional string
+ break :name "frop" "friep";
+
+ # 9: Bad name
+ break :name 13;
+
+ # 10: Bad name
+ break :name ["frop", "friep"];
+
+ # No error
+ break;
+
+ # No error
+ break :name "frop";
+
+ # No error
+ if exists "frop" {
+ break;
+ }
+
+ # No error
+ if exists "frop" {
+ break :name "frop";
+ }
+
+ # No error
+ foreverypart {
+ break :name "frop";
+ }
+
+ # No error
+ foreverypart :name "friep" {
+ break :name "frop";
+ }
+
+ # No error
+ foreverypart :name "friep" {
+ break :name "friep";
+ }
+
+ # No error
+ foreverypart :name "friep" {
+ break;
+ }
+
+ # No error
+ foreverypart {
+ if exists "frop" {
+ break :name "frop";
+ }
+ }
+
+ # No error
+ foreverypart :name "friep" {
+ if exists "frop" {
+ break :name "frop";
+ }
+ }
+
+ # No error
+ foreverypart :name "friep" {
+ if exists "frop" {
+ break :name "friep";
+ }
+ }
+
+ # No error
+ foreverypart :name "friep" {
+ if exists "frop" {
+ break;
+ }
+ }
+}
+
+# 11: Outside loop
+break;
+
+# 12: Outside loop
+if exists "frop" {
+ break;
+}
+
+# 13: Outside loop
+break :name "frop";
+
+# 14: Outside loop
+if exists "frop" {
+ break :name "frop";
+}
+
+# 15: Bad name
+foreverypart {
+ break :name "frop";
+}
+
+# 16: Bad name
+foreverypart {
+ if exists "frop" {
+ break :name "frop";
+ }
+}
+
+# 17: Bad name
+foreverypart :name "friep" {
+ break :name "frop";
+}
+
+# 18: Bad name
+foreverypart :name "friep" {
+ if exists "frop" {
+ break :name "frop";
+ }
+}
+
+# 19: Bad name
+foreverypart :name "friep" {
+ foreverypart :name "frop" {
+ break :name "frml";
+ }
+}
+
+# 20: Bad name
+foreverypart :name "friep" {
+ foreverypart :name "frop" {
+ if exists "frop" {
+ break :name "frml";
+ }
+ }
+}
+
+
+
+
diff --git a/pigeonhole/tests/extensions/mime/errors/exists-mime-tag.sieve b/pigeonhole/tests/extensions/mime/errors/exists-mime-tag.sieve
new file mode 100644
index 0000000..84c86a7
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/exists-mime-tag.sieve
@@ -0,0 +1,43 @@
+require "mime";
+
+## Exists
+
+# No error
+if exists :mime "To" {
+ discard;
+}
+
+# No error
+if exists :anychild :mime "To" {
+ discard;
+}
+
+# 1: Inappropriate option
+if exists :anychild "To" {
+ discard;
+}
+
+# 2: Inappropriate option
+if exists :mime :type "To" {
+ discard;
+}
+
+# 3: Inappropriate option
+if exists :mime :subtype "To" {
+ discard;
+}
+
+# 4: Inappropriate option
+if exists :mime :contenttype "To" {
+ discard;
+}
+
+# 5: Inappropriate option
+if exists :mime :param ["frop", "friep"] "To" {
+ discard;
+}
+
+
+
+
+
diff --git a/pigeonhole/tests/extensions/mime/errors/extracttext-nofep.sieve b/pigeonhole/tests/extensions/mime/errors/extracttext-nofep.sieve
new file mode 100644
index 0000000..c38b228
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/extracttext-nofep.sieve
@@ -0,0 +1,4 @@
+require "extracttext";
+require "variables";
+
+keep;
diff --git a/pigeonhole/tests/extensions/mime/errors/extracttext-novar.sieve b/pigeonhole/tests/extensions/mime/errors/extracttext-novar.sieve
new file mode 100644
index 0000000..8e2a378
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/extracttext-novar.sieve
@@ -0,0 +1,6 @@
+require "extracttext";
+require "foreverypart";
+
+foreverypart {
+ extracttext "frop";
+}
diff --git a/pigeonhole/tests/extensions/mime/errors/extracttext.sieve b/pigeonhole/tests/extensions/mime/errors/extracttext.sieve
new file mode 100644
index 0000000..f8af1c9
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/extracttext.sieve
@@ -0,0 +1,42 @@
+require "extracttext";
+require "variables";
+require "foreverypart";
+
+# 1: Used outside foreverypart
+extracttext :first 10 "data";
+
+foreverypart {
+ # 2: Missing arguments
+ extracttext;
+
+ # 3: Bad arguments
+ extracttext 1;
+
+ # 4: Bad arguments
+ extracttext ["frop", "friep"];
+
+ # 5: Unknown tag
+ extracttext :frop "frop";
+
+ # 6: Invalid variable name
+ extracttext "${frop}";
+
+ # Not an error
+ extracttext "\n\a\m\e";
+
+ # 7: Trying to assign match variable
+ extracttext "0";
+
+ # Not an error
+ extracttext :lower "frop";
+
+ # 8: Bad ":first" tag
+ extracttext :first "frop";
+
+ # 9: Bad ":first" tag
+ extracttext :first "frop" "friep";
+
+ # 10: Bad ":first" tag
+ extracttext :first ["frop", "friep"] "frml";
+}
+
diff --git a/pigeonhole/tests/extensions/mime/errors/foreverypart.sieve b/pigeonhole/tests/extensions/mime/errors/foreverypart.sieve
new file mode 100644
index 0000000..38a28d4
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/foreverypart.sieve
@@ -0,0 +1,45 @@
+require "foreverypart";
+
+# 1: No block
+foreverypart;
+
+# 2: Spurious tag
+foreverypart :tag { }
+
+# 3: Spurious tests
+foreverypart true { }
+
+# 4: Spurious tests
+foreverypart anyof(true, false) { }
+
+# 5: Bare string
+foreverypart "frop" { }
+
+# 6: Bare string-list
+foreverypart ["frop", "friep"] { }
+
+# 7: Several bad arguments
+foreverypart 13 ["frop", "friep"] { }
+
+# 8: Spurious additional tag
+foreverypart :name "frop" :friep { }
+
+# 9: Spurious additional string
+foreverypart :name "frop" "friep" { }
+
+# 10: Bad name
+foreverypart :name 13 { }
+
+# 11: Bad name
+foreverypart :name ["frop", "friep"] { }
+
+# No error
+foreverypart { keep; }
+
+# No error
+foreverypart :name "frop" { keep; }
+
+# No error
+foreverypart :name "frop" { foreverypart { keep; } }
+
+
diff --git a/pigeonhole/tests/extensions/mime/errors/header-mime-tag.sieve b/pigeonhole/tests/extensions/mime/errors/header-mime-tag.sieve
new file mode 100644
index 0000000..85782af
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/header-mime-tag.sieve
@@ -0,0 +1,100 @@
+require "mime";
+
+## Header
+
+# No error
+if header :contains :mime "Content-Type" "text/plain" {
+ discard;
+}
+
+# No error
+if header :mime :type "Content-Type" "text" {
+ discard;
+}
+
+# No error
+if header :mime :subtype "Content-Type" "plain" {
+ discard;
+}
+
+# No error
+if header :mime :contenttype "Content-Type" "text/plain" {
+ discard;
+}
+
+# No error
+if header :mime :param ["frop", "friep"] "Content-Type" "frml" {
+ discard;
+}
+
+# No error
+if header :anychild :contains :mime "Content-Type" "text/plain" {
+ discard;
+}
+
+# No error
+if header :mime :anychild :type "Content-Type" "text" {
+ discard;
+}
+
+# No error
+if header :mime :subtype :anychild "Content-Type" "plain" {
+ discard;
+}
+
+# No error
+if header :anychild :mime :contenttype "Content-Type" "text/plain" {
+ discard;
+}
+
+# No error
+if header :mime :param ["frop", "friep"] :anychild "Content-Type" "frml" {
+ discard;
+}
+
+# 1: Bare anychild option
+if header :anychild "Content-Type" "frml" {
+ discard;
+}
+
+# 2: Bare mime option
+if header :type "Content-Type" "frml" {
+ discard;
+}
+
+# 3: Bare mime option
+if header :subtype "Content-Type" "frml" {
+ discard;
+}
+
+# 4: Bare mime option
+if header :contenttype "Content-Type" "frml" {
+ discard;
+}
+
+# 5: Bare mime option
+if header :param "frop" "Content-Type" "frml" {
+ discard;
+}
+
+# 6: Multiple option tags
+if header :mime :type :subtype "Content-Type" "frml" {
+ discard;
+}
+
+# 7: Bad param argument
+if header :mime :param 13 "Content-Type" "frml" {
+ discard;
+}
+
+# 8: Missing param argument
+if header :mime :param :anychild "Content-Type" "frml" {
+ discard;
+}
+
+# 9: Missing param argument
+if header :mime :param :frop "Content-Type" "frml" {
+ discard;
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/errors/limits-include.sieve b/pigeonhole/tests/extensions/mime/errors/limits-include.sieve
new file mode 100644
index 0000000..ef92456
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/limits-include.sieve
@@ -0,0 +1,6 @@
+require "foreverypart";
+require "include";
+
+foreverypart :name "frop" {
+ include "include-loop-2";
+}
diff --git a/pigeonhole/tests/extensions/mime/errors/limits.sieve b/pigeonhole/tests/extensions/mime/errors/limits.sieve
new file mode 100644
index 0000000..0add1c3
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/errors/limits.sieve
@@ -0,0 +1,13 @@
+require "foreverypart";
+
+foreverypart :name "frop" {
+ foreverypart :name "friep" {
+ foreverypart :name "frml" {
+ foreverypart {
+ foreverypart {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/pigeonhole/tests/extensions/mime/execute.svtest b/pigeonhole/tests/extensions/mime/execute.svtest
new file mode 100644
index 0000000..2ced83b
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/execute.svtest
@@ -0,0 +1,82 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * Execution testing (currently just meant to trigger any segfaults)
+ */
+
+test_set "message" text:
+From: Whomever <whoever@example.com>
+To: Someone <someone@example.com>
+Date: Sat, 10 Oct 2009 00:30:04 +0200
+Subject: whatever
+Content-Type: multipart/mixed; boundary=outer
+
+This is a multi-part message in MIME format.
+
+--outer
+Content-Type: multipart/alternative; boundary=inner
+
+This is a nested multi-part message in MIME format.
+
+--inner
+Content-Type: text/plain; charset="us-ascii"
+
+Hello
+
+--inner
+Content-Type: text/html; charset="us-ascii"
+
+<html><body>Hello</body></html>
+
+--inner--
+
+This is the end of the inner MIME multipart.
+
+--outer
+Content-Type: message/rfc822
+
+From: Someone Else
+Subject: Hello, this is an elaborate request for you to finally say hello
+ already!
+
+Please say Hello
+
+--outer--
+
+This is the end of the outer MIME multipart.
+.
+;
+
+test "Basic - foreverypart" {
+ if not test_script_compile "execute/foreverypart.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+
+ test_binary_save "ihave-basic";
+ test_binary_load "ihave-basic";
+}
+
+test "Basic - mime" {
+ if not test_script_compile "execute/mime.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+
+ test_binary_save "ihave-basic";
+ test_binary_load "ihave-basic";
+}
diff --git a/pigeonhole/tests/extensions/mime/execute/foreverypart.sieve b/pigeonhole/tests/extensions/mime/execute/foreverypart.sieve
new file mode 100644
index 0000000..9ae1fba
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/execute/foreverypart.sieve
@@ -0,0 +1,14 @@
+require "foreverypart";
+require "variables";
+
+foreverypart {
+ foreverypart {
+ foreverypart {
+ foreverypart {
+ set "a" "a${a}";
+ }
+ }
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/execute/mime.sieve b/pigeonhole/tests/extensions/mime/execute/mime.sieve
new file mode 100644
index 0000000..dd7fedc
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/execute/mime.sieve
@@ -0,0 +1,69 @@
+require "mime";
+require "foreverypart";
+require "variables";
+
+if header :contains :mime "Content-Type" "text/plain" {
+ discard;
+}
+if header :mime :type "Content-Type" "text" {
+ discard;
+}
+if header :mime :subtype "Content-Type" "plain" {
+ discard;
+}
+if header :mime :contenttype "Content-Type" "text/plain" {
+ discard;
+}
+if header :mime :param ["frop", "friep"] "Content-Type" "frml" {
+ discard;
+}
+if header :anychild :contains :mime "Content-Type" "text/plain" {
+ discard;
+}
+if header :mime :anychild :type "Content-Type" "text" {
+ discard;
+}
+if header :mime :subtype :anychild "Content-Type" "plain" {
+ discard;
+}
+if header :anychild :mime :contenttype "Content-Type" "text/plain" {
+ discard;
+}
+if header :mime :param ["frop", "friep"] :anychild "Content-Type" "frml" {
+ discard;
+}
+
+foreverypart {
+ foreverypart {
+ if header :contains :mime "Content-Type" "text/plain" {
+ discard;
+ }
+ if header :mime :type "Content-Type" "text" {
+ discard;
+ }
+ if header :mime :subtype "Content-Type" "plain" {
+ discard;
+ }
+ if header :mime :contenttype "Content-Type" "text/plain" {
+ discard;
+ }
+ if header :mime :param ["frop", "friep"] "Content-Type" "frml" {
+ discard;
+ }
+ if header :anychild :contains :mime "Content-Type" "text/plain" {
+ discard;
+ }
+ if header :mime :anychild :type "Content-Type" "text" {
+ discard;
+ }
+ if header :mime :subtype :anychild "Content-Type" "plain" {
+ discard;
+ }
+ if header :anychild :mime :contenttype "Content-Type" "text/plain" {
+ discard;
+ }
+ if header :mime :param ["frop", "friep"] :anychild "Content-Type" "frml" {
+ discard;
+ }
+ }
+}
diff --git a/pigeonhole/tests/extensions/mime/exists.svtest b/pigeonhole/tests/extensions/mime/exists.svtest
new file mode 100644
index 0000000..517deeb
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/exists.svtest
@@ -0,0 +1,237 @@
+require "vnd.dovecot.testsuite";
+require "mime";
+require "foreverypart";
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@vestingbar.bl
+Subject: Test message
+Date: Wed, 29 Jul 2009 18:21:44 +0300
+X-Spam-Status: Not Spam
+Resent-To: nico@frop.example.com
+
+Test!
+.
+;
+
+/*
+ * One header
+ */
+
+test "One header" {
+ if not exists :mime :anychild "from" {
+ test_fail "exists test missed from header";
+ }
+
+ if exists :mime :anychild "x-nonsense" {
+ test_fail "exists test found non-existent header";
+ }
+}
+
+/*
+ * One header - foreverypart
+ */
+
+test "One header - foreverypart" {
+ foreverypart {
+ if not exists :mime :anychild "from" {
+ test_fail "exists test missed from header";
+ }
+
+ if exists :mime :anychild "x-nonsense" {
+ test_fail "exists test found non-existent header";
+ }
+ }
+}
+
+/*
+ * Two headers
+ */
+
+test "Two headers" {
+ if not exists :mime :anychild ["from","to"] {
+ test_fail "exists test missed from or to header";
+ }
+
+ if exists :mime :anychild ["from","x-nonsense"] {
+ test_fail "exists test found non-existent header (1)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","to"] {
+ test_fail "exists test found non-existent header (2)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","x-nonsense2"] {
+ test_fail "exists test found non-existent header (3)";
+ }
+}
+
+/*
+ * Two headers - foreverypart
+ */
+
+test "Two headers - foreverypart" {
+ foreverypart {
+ if not exists :mime :anychild ["from","to"] {
+ test_fail "exists test missed from or to header";
+ }
+
+ if exists :mime :anychild ["from","x-nonsense"] {
+ test_fail "exists test found non-existent header (1)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","to"] {
+ test_fail "exists test found non-existent header (2)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","x-nonsense2"] {
+ test_fail "exists test found non-existent header (3)";
+ }
+ }
+}
+
+/*
+ * Three headers
+ */
+
+test "Three headers" {
+ if not exists :mime :anychild ["Subject","date","resent-to"] {
+ test_fail "exists test missed subject, date or resent-to header";
+ }
+
+ if exists :mime :anychild ["x-nonsense","date","resent-to"] {
+ test_fail "exists test found non-existent header (1)";
+ }
+
+ if exists :mime :anychild ["subject", "x-nonsense","resent-to"] {
+ test_fail "exists test found non-existent header (2)";
+ }
+
+ if exists :mime :anychild ["subject","date","x-nonsense"] {
+ test_fail "exists test found non-existent header (3)";
+ }
+
+ if exists :mime :anychild ["subject", "x-nonsense","x-nonsense2"] {
+ test_fail "exists test found non-existent header (4)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","date","x-nonsense2"] {
+ test_fail "exists test found non-existent header (5)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","x-nonsense2","resent-to"] {
+ test_fail "exists test found non-existent header (6)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","x-nonsense2","x-nonsense3"] {
+ test_fail "exists test found non-existent header (7)";
+ }
+}
+
+/*
+ * Three headers - foreverypart
+ */
+
+test "Three headers - foreverypart " {
+ foreverypart {
+ if not exists :mime :anychild ["Subject","date","resent-to"] {
+ test_fail "exists test missed subject, date or resent-to header";
+ }
+
+ if exists :mime :anychild ["x-nonsense","date","resent-to"] {
+ test_fail "exists test found non-existent header (1)";
+ }
+
+ if exists :mime :anychild ["subject", "x-nonsense","resent-to"] {
+ test_fail "exists test found non-existent header (2)";
+ }
+
+ if exists :mime :anychild ["subject","date","x-nonsense"] {
+ test_fail "exists test found non-existent header (3)";
+ }
+
+ if exists :mime :anychild ["subject", "x-nonsense","x-nonsense2"] {
+ test_fail "exists test found non-existent header (4)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","date","x-nonsense2"] {
+ test_fail "exists test found non-existent header (5)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","x-nonsense2","resent-to"] {
+ test_fail "exists test found non-existent header (6)";
+ }
+
+ if exists :mime :anychild ["x-nonsense","x-nonsense2","x-nonsense3"] {
+ test_fail "exists test found non-existent header (7)";
+ }
+ }
+}
+
+/*
+ * Multipart anychild
+ */
+
+test_set "message" text:
+From: Hendrik <hendrik@example.com>
+To: Harrie <harrie@example.com>
+Date: Sat, 11 Oct 2010 00:31:44 +0200
+Subject: Harrie is een prutser
+Content-Type: multipart/mixed; boundary=AA
+X-Test1: AA
+
+This is a multi-part message in MIME format.
+--AA
+Content-Type: multipart/mixed; boundary=BB
+X-Test2: BB
+
+This is a multi-part message in MIME format.
+--BB
+Content-Type: text/plain; charset="us-ascii"
+X-Test3: CC
+
+Hello
+
+--BB
+Content-Type: text/plain; charset="us-ascii"
+X-Test4: DD
+
+Hello again
+
+--BB--
+This is the end of MIME multipart.
+
+--AA
+Content-Type: text/plain; charset="us-ascii"
+X-Test5: EE
+
+And again
+
+--AA--
+This is the end of MIME multipart.
+.
+;
+
+test "Multipart anychild" {
+ if not exists :mime :anychild "X-Test1" {
+ test_fail "X-Test1 header does exist";
+ }
+ if not exists :mime :anychild "X-Test2" {
+ test_fail "X-Test2 header does exist";
+ }
+ if not exists :mime :anychild "X-Test3" {
+ test_fail "X-Test3 header does exist";
+ }
+ if not exists :mime :anychild "X-Test4" {
+ test_fail "X-Test4 header does exist";
+ }
+ if not exists :mime :anychild "X-Test5" {
+ test_fail "X-Test5 header does exist";
+ }
+ if not exists :mime :anychild
+ ["X-Test1", "X-Test2", "X-Test3", "X-Test4", "X-Test5"] {
+ test_fail "Not all headers exist";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/extracttext.svtest b/pigeonhole/tests/extensions/mime/extracttext.svtest
new file mode 100644
index 0000000..510a52b
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/extracttext.svtest
@@ -0,0 +1,143 @@
+require "vnd.dovecot.testsuite";
+require "foreverypart";
+require "variables";
+require "extracttext";
+
+test_set "message" text:
+From: Hendrik <hendrik@example.com>
+To: Harrie <harrie@example.com>
+Date: Sat, 11 Oct 2010 00:31:44 +0200
+Subject: Harrie is een prutser
+Content-Type: multipart/mixed; boundary=AA
+
+This is a multi-part message in MIME format.
+--AA
+Content-Type: multipart/mixed; boundary=BB
+
+This is a multi-part message in MIME format.
+--BB
+Content-Type: text/plain; charset="us-ascii"
+
+This is the first message part containing
+plain text.
+
+--BB
+Content-Type: text/plain; charset="us-ascii"
+
+This is another plain text message part.
+
+--BB--
+This is the end of MIME multipart.
+
+--AA
+Content-Type: text/html; charset="us-ascii"
+
+<html>
+<body>This is a piece of HTML text.</body>
+</html>
+
+--AA--
+This is the end of MIME multipart.
+.
+;
+
+test "Basic" {
+ set "a" "a";
+ foreverypart {
+ extracttext "b";
+ if string "${a}" "aaa" {
+ if not string :contains "${b}" "first" {
+ test_fail "bad content extracted: ${b}";
+ }
+ } elsif string "${a}" "aaaa" {
+ if not string :contains "${b}" "another" {
+ test_fail "bad content extracted: ${b}";
+ }
+ } elsif string "${a}" "aaaaa" {
+ if not string :contains "${b}" "HTML text" {
+ test_fail "bad content extracted: ${b}";
+ }
+ if string :contains "${b}" "<html>" {
+ test_fail "content extracted html: ${b}";
+ }
+ }
+ set "a" "a${a}";
+ }
+ if not string "${a}" "aaaaaa" {
+ set :length "parts" "${a}";
+ test_fail "bad number of parts parsed: ${parts}";
+ }
+}
+
+test_set "message" text:
+From: <stephan@example.com>
+To: <frop@example.com>
+Subject: Frop!
+
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP! FROP!
+.
+;
+
+test "First - less" {
+ foreverypart {
+ extracttext :first 20 "data";
+ if not string "${data}" "FROP! FROP! FROP! FR" {
+ test_fail "Bad data extracted";
+ }
+
+ extracttext :length :first 100 "data_len";
+ if not string "${data_len}" "100" {
+ test_fail "Bad number of bytes extracted";
+ }
+ }
+}
+
+test_set "message" text:
+From: <stephan@example.com>
+To: <frop@example.com>
+Subject: Frop!
+
+FROP! FROP! FROP! FROP!
+.
+;
+
+test "First - more" {
+ foreverypart {
+ extracttext :first 100 "data";
+ if not string :matches "${data}" "FROP! FROP! FROP! FROP!*" {
+ test_fail "Bad data extracted";
+ }
+ }
+}
+
+test_set "message" text:
+From: <stephan@example.com>
+To: <frop@example.com>
+Subject: Frop!
+
+FROP! FROP! FROP! FROP!
+.
+;
+
+test "Modifier" {
+ foreverypart {
+ extracttext :lower :upperfirst "data";
+ if not string :matches "${data}" "Frop! frop! frop! frop!*" {
+ test_fail "Bad data extracted";
+ }
+ }
+}
+
+
+
diff --git a/pigeonhole/tests/extensions/mime/foreverypart.svtest b/pigeonhole/tests/extensions/mime/foreverypart.svtest
new file mode 100644
index 0000000..08907c9
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/foreverypart.svtest
@@ -0,0 +1,178 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "foreverypart";
+require "mime";
+require "variables";
+require "include";
+
+test_set "message" text:
+From: Hendrik <hendrik@example.com>
+To: Harrie <harrie@example.com>
+Date: Sat, 11 Oct 2010 00:31:44 +0200
+Subject: Harrie is een prutser
+Content-Type: multipart/mixed; boundary=AA
+X-Test: AA
+
+This is a multi-part message in MIME format.
+--AA
+Content-Type: multipart/mixed; boundary=BB
+X-Test: BB
+
+This is a multi-part message in MIME format.
+--BB
+Content-Type: text/plain; charset="us-ascii"
+X-Test: CC
+
+Hello
+
+--BB
+Content-Type: text/plain; charset="us-ascii"
+X-Test: DD
+
+Hello again
+
+--BB--
+This is the end of MIME multipart.
+
+--AA
+Content-Type: text/plain; charset="us-ascii"
+X-Test: EE
+
+And again
+
+--AA--
+This is the end of MIME multipart.
+.
+;
+
+test "Single loop" {
+ set "a" "a";
+ foreverypart {
+ set :length "la" "${a}";
+
+ if string "${a}" "a" {
+ if not header :mime "X-Test" "AA" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aa" {
+ if not header :mime "X-Test" "BB" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaa" {
+ if not header :mime "X-Test" "CC" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaa" {
+ if not header :mime "X-Test" "DD" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaa" {
+ if not header :mime "X-Test" "EE" {
+ test_fail "wrong header extracted (${la})";
+ }
+ }
+ set "a" "a${a}";
+ }
+}
+
+test "Double loop" {
+ set "a" "a";
+ foreverypart {
+ set :length "la" "${a}";
+
+ if string "${a}" "a" {
+ if not header :mime "X-Test" "AA" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaaa" {
+ if not header :mime "X-Test" "BB" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaaaaaa" {
+ if not header :mime "X-Test" "CC" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaaaaaaa" {
+ if not header :mime "X-Test" "DD" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaaaaaaaa" {
+ if not header :mime "X-Test" "EE" {
+ test_fail "wrong header extracted (${la})";
+ }
+ }
+
+ set "a" "a${a}";
+
+ foreverypart {
+ set :length "la" "${a}";
+
+ if string "${a}" "aa" {
+ if not header :mime "X-Test" "BB" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaa" {
+ if not header :mime "X-Test" "CC" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaa" {
+ if not header :mime "X-Test" "DD" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaa" {
+ if not header :mime "X-Test" "EE" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaaaa" {
+ if not header :mime "X-Test" "CC" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${a}" "aaaaaaaa" {
+ if not header :mime "X-Test" "DD" {
+ test_fail "wrong header extracted (${la})";
+ }
+ }
+ set "a" "a${a}";
+ }
+ }
+}
+
+test "Double loop - include" {
+ global "in";
+ global "error";
+ set "in" "a";
+ foreverypart {
+ set :length "la" "${in}";
+
+ if string "${in}" "in" {
+ if not header :mime "X-Test" "AA" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${in}" "aaaaaa" {
+ if not header :mime "X-Test" "BB" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${in}" "aaaaaaaaa" {
+ if not header :mime "X-Test" "CC" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${in}" "aaaaaaaaaa" {
+ if not header :mime "X-Test" "DD" {
+ test_fail "wrong header extracted (${la})";
+ }
+ } elsif string "${in}" "aaaaaaaaaaa" {
+ if not header :mime "X-Test" "EE" {
+ test_fail "wrong header extracted (${la})";
+ }
+ }
+
+ set "in" "a${in}";
+
+ include "include-foreverypart";
+
+ if not string "${error}" "" {
+ test_fail "INCLUDED: ${error}";
+ }
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/mime/header.svtest b/pigeonhole/tests/extensions/mime/header.svtest
new file mode 100644
index 0000000..48cd9e4
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/header.svtest
@@ -0,0 +1,444 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "foreverypart";
+require "mime";
+
+/*
+ * Basic functionality
+ */
+
+test_set "message" text:
+From: stephan@example.com
+To: nico@nl.example.com, harry@de.example.com
+Subject: Frobnitzm
+Comments: This is nonsense.
+Keywords: nonsense, strange, testing
+X-Spam: Yes
+
+Test.
+.
+;
+
+test "Basic functionality" {
+ /* Must match */
+ if not header :mime :anychild :contains ["Subject", "Comments"] "Frobnitzm" {
+ test_fail "failed to match header (1)";
+ }
+
+ if not header :mime :anychild :contains ["Subject", "Comments"] "nonsense" {
+ test_fail "failed to match header(2)";
+ }
+
+ if not header :mime :anychild :matches "Keywords" "*, strange, *" {
+ test_fail "failed to match header (3)";
+ }
+
+ if not header :mime :anychild :is "Comments" "This is nonsense." {
+ test_fail "failed to match header (4)";
+ }
+
+ /* Must not match */
+ if header :mime :anychild ["subject", "comments", "keywords"] "idiotic" {
+ test_fail "matched nonsense";
+ }
+
+ /* Match first key */
+ if not header :mime :anychild :contains ["keywords"] ["strange", "snot", "vreemd"] {
+ test_fail "failed to match first key";
+ }
+
+ /* Match second key */
+ if not header :mime :anychild :contains ["keywords"] ["raar", "strange", "vreemd"] {
+ test_fail "failed to match second key";
+ }
+
+ /* Match last key */
+ if not header :mime :anychild :contains ["keywords"] ["raar", "snot", "strange"] {
+ test_fail "failed to match last key";
+ }
+
+ /* First header */
+ if not header :mime :anychild :contains ["keywords", "subject"]
+ ["raar", "strange", "vreemd"] {
+ test_fail "failed to match first header";
+ }
+
+ /* Second header */
+ if not header :mime :anychild :contains ["subject", "keywords"]
+ ["raar", "strange", "vreemd"] {
+ test_fail "failed to match second header";
+ }
+}
+
+/*
+ * Basic functionality - foreverypart
+ */
+
+test "Basic functionality - foreverypart" {
+ foreverypart {
+ /* Must match */
+ if not header :mime :anychild :contains ["Subject", "Comments"] "Frobnitzm" {
+ test_fail "failed to match header (1)";
+ }
+
+ if not header :mime :anychild :contains ["Subject", "Comments"] "nonsense" {
+ test_fail "failed to match header(2)";
+ }
+
+ if not header :mime :anychild :matches "Keywords" "*, strange, *" {
+ test_fail "failed to match header (3)";
+ }
+
+ if not header :mime :anychild :is "Comments" "This is nonsense." {
+ test_fail "failed to match header (4)";
+ }
+
+ /* Must not match */
+ if header :mime :anychild ["subject", "comments", "keywords"] "idiotic" {
+ test_fail "matched nonsense";
+ }
+
+ /* Match first key */
+ if not header :mime :anychild :contains ["keywords"] ["strange", "snot", "vreemd"] {
+ test_fail "failed to match first key";
+ }
+
+ /* Match second key */
+ if not header :mime :anychild :contains ["keywords"] ["raar", "strange", "vreemd"] {
+ test_fail "failed to match second key";
+ }
+
+ /* Match last key */
+ if not header :mime :anychild :contains ["keywords"] ["raar", "snot", "strange"] {
+ test_fail "failed to match last key";
+ }
+
+ /* First header */
+ if not header :mime :anychild :contains ["keywords", "subject"]
+ ["raar", "strange", "vreemd"] {
+ test_fail "failed to match first header";
+ }
+
+ /* Second header */
+ if not header :mime :anychild :contains ["subject", "keywords"]
+ ["raar", "strange", "vreemd"] {
+ test_fail "failed to match second header";
+ }
+ }
+}
+
+/*
+ * Matching empty key
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+X-Caffeine: C8H10N4O2
+Subject: I need coffee!
+Comments:
+
+Text
+.
+;
+
+test "Matching empty key" {
+ if header :mime :anychild :is "X-Caffeine" "" {
+ test_fail ":is-matched non-empty header with empty string";
+ }
+
+ if not header :mime :anychild :contains "X-Caffeine" "" {
+ test_fail "failed to match existing header with empty string";
+ }
+
+ if not header :mime :anychild :is "comments" "" {
+ test_fail "failed to match empty header :mime :anychild with empty string";
+ }
+
+ if header :mime :anychild :contains "X-Nonsense" "" {
+ test_fail ":contains-matched non-existent header with empty string";
+ }
+}
+
+/*
+ * Matching empty key - foreverypart
+ */
+
+test "Matching empty key - foreverypart" {
+ foreverypart {
+ if header :mime :anychild :is "X-Caffeine" "" {
+ test_fail ":is-matched non-empty header with empty string";
+ }
+
+ if not header :mime :anychild :contains "X-Caffeine" "" {
+ test_fail "failed to match existing header with empty string";
+ }
+
+ if not header :mime :anychild :is "comments" "" {
+ test_fail "failed to match empty header :mime :anychild with empty string";
+ }
+
+ if header :mime :anychild :contains "X-Nonsense" "" {
+ test_fail ":contains-matched non-existent header with empty string";
+ }
+ }
+}
+
+/*
+ * Ignoring whitespace
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+Subject: Help
+X-A: Text
+X-B: Text
+
+Text
+.
+;
+
+test "Ignoring whitespace" {
+ if not header :mime :anychild :is "x-a" "Text" {
+ if header :mime :anychild :matches "x-a" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header :mime :anychild test does not strip leading whitespace (header=`${header}`)";
+ }
+
+ if not header :mime :anychild :is "x-b" "Text" {
+ if header :mime :anychild :matches "x-b" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header :mime :anychild test does not strip trailing whitespace (header=`${header}`)";
+ }
+
+ if not header :mime :anychild :is "subject" "Help" {
+ if header :mime :anychild :matches "subject" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header :mime :anychild test does not strip both leading and trailing whitespace (header=`${header}`)";
+ }
+}
+
+/*
+ * Ignoring whitespace - foreverypart
+ */
+
+test "Ignoring whitespace - foreverypart" {
+ foreverypart {
+ if not header :mime :anychild :is "x-a" "Text" {
+ if header :mime :anychild :matches "x-a" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header :mime :anychild test does not strip leading whitespace (header=`${header}`)";
+ }
+
+ if not header :mime :anychild :is "x-b" "Text" {
+ if header :mime :anychild :matches "x-b" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header :mime :anychild test does not strip trailing whitespace (header=`${header}`)";
+ }
+
+ if not header :mime :anychild :is "subject" "Help" {
+ if header :mime :anychild :matches "subject" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header :mime :anychild test does not strip both leading and trailing whitespace (header=`${header}`)";
+ }
+ }
+}
+
+/*
+ * Absent or empty header
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+CC: harry@nonsense.ex
+Subject:
+Comments:
+
+Text
+.
+;
+
+test "Absent or empty header" {
+ if not header :mime :anychild :matches "Cc" "?*" {
+ test_fail "CC header is not absent or empty";
+ }
+
+ if header :mime :anychild :matches "Subject" "?*" {
+ test_fail "Subject header is empty, but matched otherwise";
+ }
+
+ if header :mime :anychild :matches "Comment" "?*" {
+ test_fail "Comment header is empty, but matched otherwise";
+ }
+}
+
+/*
+ * Absent or empty header - foreverypart
+ */
+
+test "Absent or empty header - foreverypart" {
+ foreverypart {
+ if not header :mime :anychild :matches "Cc" "?*" {
+ test_fail "CC header is not absent or empty";
+ }
+
+ if header :mime :anychild :matches "Subject" "?*" {
+ test_fail "Subject header is empty, but matched otherwise";
+ }
+
+ if header :mime :anychild :matches "Comment" "?*" {
+ test_fail "Comment header is empty, but matched otherwise";
+ }
+ }
+}
+
+
+/*
+ * Invalid header name
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+Subject: Valid message
+X-Multiline: This is a multi-line
+ header body, which should be
+ unfolded correctly.
+
+Text
+.
+;
+
+test "Invalid header name" {
+ if header :mime :anychild :contains "subject:" "" {
+ test_fail "matched invalid header name";
+ }
+
+ if header :mime :anychild :contains "to!" "" {
+ test_fail "matched invalid header name";
+ }
+}
+
+/*
+ * Invalid header name - foreverypart
+ */
+
+test "Invalid header name - foreverypart" {
+ foreverypart {
+ if header :mime :anychild :contains "subject:" "" {
+ test_fail "matched invalid header name";
+ }
+
+ if header :mime :anychild :contains "to!" "" {
+ test_fail "matched invalid header name";
+ }
+ }
+}
+
+/*
+ * Folded headers
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+Subject: Not enough space on a line!
+X-Multiline: This is a multi-line
+ header body, which should be
+ unfolded correctly.
+
+Text
+.
+;
+
+test "Folded headers" {
+ if not header :mime :anychild :is "x-multiline"
+ "This is a multi-line header body, which should be unfolded correctly." {
+ test_fail "failed to properly unfold folded header.";
+ }
+}
+
+/*
+ * Folded headers - foreverypart
+ */
+
+test "Folded headers - foreverypart" {
+ foreverypart {
+ if not header :mime :anychild :is "x-multiline"
+ "This is a multi-line header body, which should be unfolded correctly." {
+ test_fail "failed to properly unfold folded header.";
+ }
+ }
+}
+
+/*
+ * Multipart anychild
+ */
+
+test_set "message" text:
+From: Hendrik <hendrik@example.com>
+To: Harrie <harrie@example.com>
+Date: Sat, 11 Oct 2010 00:31:44 +0200
+Subject: Harrie is een prutser
+Content-Type: multipart/mixed; boundary=AA
+X-Test: AA
+
+This is a multi-part message in MIME format.
+--AA
+Content-Type: multipart/mixed; boundary=BB
+X-Test: BB
+
+This is a multi-part message in MIME format.
+--BB
+Content-Type: text/plain; charset="us-ascii"
+X-Test: CC
+
+Hello
+
+--BB
+Content-Type: text/plain; charset="us-ascii"
+X-Test: DD
+
+Hello again
+
+--BB--
+This is the end of MIME multipart.
+
+--AA
+Content-Type: text/plain; charset="us-ascii"
+X-Test: EE
+
+And again
+
+--AA--
+This is the end of MIME multipart.
+.
+;
+
+test "Multipart anychild" {
+ if not header :mime :anychild "X-Test" "AA" {
+ test_fail "No AA";
+ }
+ if not header :mime :anychild "X-Test" "BB" {
+ test_fail "No BB";
+ }
+ if not header :mime :anychild "X-Test" "CC" {
+ test_fail "No CC";
+ }
+ if not header :mime :anychild "X-Test" "DD" {
+ test_fail "No DD";
+ }
+ if not header :mime :anychild "X-Test" "EE" {
+ test_fail "No EE";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/mime/included/include-foreverypart.sieve b/pigeonhole/tests/extensions/mime/included/include-foreverypart.sieve
new file mode 100644
index 0000000..f1b1b16
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/included/include-foreverypart.sieve
@@ -0,0 +1,44 @@
+require "include";
+require "foreverypart";
+require "mime";
+require "variables";
+
+global "in";
+global "error";
+
+foreverypart {
+ set :length "la" "${in}";
+
+ if string "${in}" "aa" {
+ if not header :mime "X-Test" "BB" {
+ set "error" "wrong header extracted (${la})";
+ return;
+ }
+ } elsif string "${in}" "aaa" {
+ if not header :mime "X-Test" "CC" {
+ set "error" "wrong header extracted (${la})";
+ return;
+ }
+ } elsif string "${in}" "aaaa" {
+ if not header :mime "X-Test" "DD" {
+ set "error" "wrong header extracted (${la})";
+ return;
+ }
+ } elsif string "${in}" "aaaaa" {
+ if not header :mime "X-Test" "EE" {
+ set "error" "wrong header extracted (${la})";
+ return;
+ }
+ } elsif string "${in}" "aaaaaaa" {
+ if not header :mime "X-Test" "CC" {
+ set "error" "wrong header extracted (${la})";
+ return;
+ }
+ } elsif string "${in}" "aaaaaaaa" {
+ if not header :mime "X-Test" "DD" {
+ set "error" "wrong header extracted (${la})";
+ return;
+ }
+ }
+ set "in" "a${in}";
+}
diff --git a/pigeonhole/tests/extensions/mime/included/include-loop-2.sieve b/pigeonhole/tests/extensions/mime/included/include-loop-2.sieve
new file mode 100644
index 0000000..80c5884
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/included/include-loop-2.sieve
@@ -0,0 +1,6 @@
+require "foreverypart";
+require "include";
+
+foreverypart :name "friep" {
+ include "include-loop-3";
+}
diff --git a/pigeonhole/tests/extensions/mime/included/include-loop-3.sieve b/pigeonhole/tests/extensions/mime/included/include-loop-3.sieve
new file mode 100644
index 0000000..228a8bc
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/included/include-loop-3.sieve
@@ -0,0 +1,6 @@
+require "foreverypart";
+require "include";
+
+foreverypart :name "frml" {
+ include "include-loop-4";
+}
diff --git a/pigeonhole/tests/extensions/mime/included/include-loop-4.sieve b/pigeonhole/tests/extensions/mime/included/include-loop-4.sieve
new file mode 100644
index 0000000..00dad84
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/included/include-loop-4.sieve
@@ -0,0 +1,6 @@
+require "foreverypart";
+require "include";
+
+foreverypart {
+ include "include-loop-5";
+}
diff --git a/pigeonhole/tests/extensions/mime/included/include-loop-5.sieve b/pigeonhole/tests/extensions/mime/included/include-loop-5.sieve
new file mode 100644
index 0000000..e22b21c
--- /dev/null
+++ b/pigeonhole/tests/extensions/mime/included/include-loop-5.sieve
@@ -0,0 +1,9 @@
+require "foreverypart";
+require "include";
+require "mime";
+
+foreverypart {
+ if header :mime :subtype "content-type" "plain" {
+ break;
+ }
+}
diff --git a/pigeonhole/tests/extensions/regex/basic.svtest b/pigeonhole/tests/extensions/regex/basic.svtest
new file mode 100644
index 0000000..9417434
--- /dev/null
+++ b/pigeonhole/tests/extensions/regex/basic.svtest
@@ -0,0 +1,51 @@
+require "vnd.dovecot.testsuite";
+
+require "regex";
+require "variables";
+
+test_set "message" text:
+From: stephan+sieve@friep.example.com
+To: tss@example.net, nico@nl.example.com, sirius@fi.example.com
+Subject: Test
+
+Test message.
+.
+;
+
+test "Basic example" {
+ if not address :regex :comparator "i;ascii-casemap" "from" [
+ "stephan(\\+.*)?@it\\.example\\.com",
+ "stephan(\\+.*)?@friep\\.example\\.com"
+ ] {
+ test_fail "failed to match";
+ }
+}
+
+test "No values" {
+ if header :regex "cc" [".*\\.com", ".*\\.nl"] {
+ test_fail "matched inappropriately";
+ }
+}
+
+
+test "More values" {
+ if address :regex "to" [".*\\.uk", ".*\\.nl", ".*\\.tk"] {
+ test_fail "matched inappropriately";
+ }
+
+ if not address :regex "to" [".*\\.uk", ".*\\.nl", ".*\\.tk", ".*fi\\..*"] {
+ test_fail "failed to match last";
+ }
+}
+
+test "Variable regex" {
+ set "regex" "stephan[+](sieve)@friep.example.com";
+
+ if not header :regex "from" "${regex}" {
+ test_fail "failed to match variable regex";
+ }
+
+ if not string "${1}" "sieve" {
+ test_fail "failed to extract proper match value from variable regex";
+ }
+}
diff --git a/pigeonhole/tests/extensions/regex/errors.svtest b/pigeonhole/tests/extensions/regex/errors.svtest
new file mode 100644
index 0000000..2e0ebe0
--- /dev/null
+++ b/pigeonhole/tests/extensions/regex/errors.svtest
@@ -0,0 +1,29 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test "Compile errors" {
+ if test_script_compile "errors/compile.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";
+ }
+}
+
+test "Runtime errors" {
+ if not test_script_compile "errors/runtime.sieve" {
+ test_fail "failed to compile";
+ }
+
+ if not test_script_run {
+ test_fail "script should have run fine";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/regex/errors/compile.sieve b/pigeonhole/tests/extensions/regex/errors/compile.sieve
new file mode 100644
index 0000000..5ddaaf8
--- /dev/null
+++ b/pigeonhole/tests/extensions/regex/errors/compile.sieve
@@ -0,0 +1,25 @@
+require "regex";
+require "comparator-i;ascii-numeric";
+require "envelope";
+
+if address :regex :comparator "i;ascii-numeric" "from" "sirius(\\+.*)?@friep\\.example\\.com" {
+ keep;
+ stop;
+}
+
+if address :regex "from" "sirius(+\\+.*)?@friep\\.example\\.com" {
+ keep;
+ stop;
+}
+
+if header :regex "from" "sirius(\\+.*)?@friep\\.ex[]ample.com" {
+ keep;
+ stop;
+}
+
+if envelope :regex "from" "sirius(\\+.*)?@friep\\.ex[]ample.com" {
+ keep;
+ stop;
+}
+
+discard;
diff --git a/pigeonhole/tests/extensions/regex/errors/runtime.sieve b/pigeonhole/tests/extensions/regex/errors/runtime.sieve
new file mode 100644
index 0000000..2d0bf66
--- /dev/null
+++ b/pigeonhole/tests/extensions/regex/errors/runtime.sieve
@@ -0,0 +1,9 @@
+require "regex";
+require "variables";
+require "fileinto";
+
+set "regex" "[";
+
+if header :regex "to" "${regex}" {
+ fileinto "frop";
+}
diff --git a/pigeonhole/tests/extensions/regex/match-values.svtest b/pigeonhole/tests/extensions/regex/match-values.svtest
new file mode 100644
index 0000000..18b7404
--- /dev/null
+++ b/pigeonhole/tests/extensions/regex/match-values.svtest
@@ -0,0 +1,72 @@
+require "vnd.dovecot.testsuite";
+
+require "regex";
+require "variables";
+
+test_set "message" text:
+From: Andy Howell <AndyHowell@example.com>
+Sender: antlr-interest-bounces@ant.example.com
+To: Stephan Bosch <stephan@example.org>
+Subject: [Dovecot] Sieve regex match problem
+
+Hi,
+
+I is broken.
+.
+;
+
+test "Basic match values 1" {
+ if header :regex ["Sender"] ["([^-@]*)-([^-@]*)(-bounces)?@ant.example.com"] {
+
+ if not string :is "${1}" "antlr" {
+ test_fail "first match value is not correct";
+ }
+
+ if not string :is "${2}" "interest" {
+ test_fail "second match value is not correct";
+ }
+
+ if not string :is "${3}" "-bounces" {
+ test_fail "third match value is not correct";
+ }
+
+ if string :is "${4}" "-bounces" {
+ test_fail "fourth match contains third value";
+ }
+ } else {
+ test_fail "failed to match";
+ }
+}
+
+test "Basic match values 2" {
+ if header :regex ["Sender"] ["(.*>[ \\t]*,?[ \\t]*)?([^-@]*)-([^-@]*)(-bounces)?@ant.example.com"] {
+
+ if not string :is "${1}" "" {
+ test_fail "first match value is not correct: ${1}";
+ }
+
+ if not string :is "${2}" "antlr" {
+ test_fail "second match value is not correct: ${2}";
+ }
+
+ if not string :is "${3}" "interest" {
+ test_fail "third match value is not correct: ${3}";
+ }
+
+ if not string :is "${4}" "-bounces" {
+ test_fail "fourth match value is not correct: ${4}";
+ }
+
+ if string :is "${5}" "-bounces" {
+ test_fail "fifth match contains fourth value: ${5}";
+ }
+ } else {
+ test_fail "failed to match";
+ }
+}
+
+
+
+
+
+
diff --git a/pigeonhole/tests/extensions/reject/execute.svtest b/pigeonhole/tests/extensions/reject/execute.svtest
new file mode 100644
index 0000000..20a0bdf
--- /dev/null
+++ b/pigeonhole/tests/extensions/reject/execute.svtest
@@ -0,0 +1,34 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test_set "message" text:
+To: nico@frop.example.org
+From: stephan@example.org
+Subject: Test
+
+Test.
+.
+;
+
+test "Execute" {
+ if not test_script_compile "execute/basic.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_action :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "invalid number of actions in result";
+ }
+
+ if not test_result_action :index 1 "reject" {
+ test_fail "reject action missing from result";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
diff --git a/pigeonhole/tests/extensions/reject/execute/basic.sieve b/pigeonhole/tests/extensions/reject/execute/basic.sieve
new file mode 100644
index 0000000..d3b7dc9
--- /dev/null
+++ b/pigeonhole/tests/extensions/reject/execute/basic.sieve
@@ -0,0 +1,8 @@
+require "reject";
+
+if address :contains "to" "frop.example" {
+ reject "Don't send unrequested messages.";
+ stop;
+}
+
+keep;
diff --git a/pigeonhole/tests/extensions/reject/smtp.svtest b/pigeonhole/tests/extensions/reject/smtp.svtest
new file mode 100644
index 0000000..8cbf77c
--- /dev/null
+++ b/pigeonhole/tests/extensions/reject/smtp.svtest
@@ -0,0 +1,56 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+require "reject";
+
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "timo@example.net";
+
+test "Basic" {
+ reject "I don't want your mail";
+
+ if not test_result_execute {
+ test_fail "failed to execute reject";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "sirius@example.org" {
+ test_fail "to address incorrect";
+ }
+
+ if not header :contains "from" "Postmaster" {
+ test_fail "from address incorrect";
+ }
+
+ if not envelope :is "to" "sirius@example.org" {
+ test_fail "envelope recipient incorrect";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender not null";
+ }
+}
+
+test_result_reset;
+test_set "envelope.from" "<>";
+
+test "Null Sender" {
+ reject "I don't want your mail";
+
+ if not test_result_execute {
+ test_fail "failed to execute reject";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "reject sent message to NULL sender";
+ }
+}
diff --git a/pigeonhole/tests/extensions/relational/basic.svtest b/pigeonhole/tests/extensions/relational/basic.svtest
new file mode 100644
index 0000000..288661a
--- /dev/null
+++ b/pigeonhole/tests/extensions/relational/basic.svtest
@@ -0,0 +1,178 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Test message
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Cc: frop@example.org
+CC: timo@example.org
+X-Spam-Score: 300
+X-Nonsense: 1000
+X-Nonsense: 20
+X-Alpha: abcdzyx
+X-Count: a
+X-Count: b
+X-Count: c
+X-Count: d
+X-Count: e
+X-Count: f
+X-Count: g
+X-Count: h
+X-Count: i
+X-Count: j
+X-Count: k
+X-Count: l
+X-Count: m
+X-Count: n
+X-Count: o
+X-Count: p
+X-Count: q
+X-Count: r
+X-Count: s
+X-Count: t
+X-Count: u
+X-Count: v
+X-Count: w
+X-Count: x
+X-Count: y
+X-Count: z
+Subject: Test
+Comment:
+
+Test!
+.
+;
+
+/*
+ * Empty strings
+ */
+
+test "Value \"\" eq 40 (vs)" {
+ if header :value "eq" :comparator "i;ascii-numeric" "comment" "40" {
+ test_fail ":value matched empty string with i;ascii-numeric";
+ }
+
+ if header :value "gt" :comparator "i;ascii-numeric" "x-spam-score" "" {
+ test_fail ":value 300 exceeded empty string with i;ascii-numeric";
+ }
+
+ if header :value "gt" :comparator "i;ascii-numeric" "x-spam-score" "" {
+ test_fail ":count exceeded empty string with i;ascii-numeric";
+ }
+}
+
+/*
+ * Match type :value
+ */
+
+test "Value 300 eq 2" {
+ if header :value "eq" :comparator "i;ascii-numeric" "x-spam-score" "2" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Value 300 lt 2" {
+ if header :value "lt" :comparator "i;ascii-numeric" "x-spam-score" "2" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Value 300 le 300" {
+ if not header :value "le" :comparator "i;ascii-numeric" "x-spam-score" "300" {
+ test_fail "should have matched";
+ }
+}
+
+test "Value 300 le 302" {
+ if not header :value "le" :comparator "i;ascii-numeric" "x-spam-score" "302" {
+ test_fail "should have matched";
+ }
+}
+
+test "Value 302 le 00302" {
+ if not header :value "le" :comparator "i;ascii-numeric" "x-spam-score" "00302" {
+ test_fail "should have matched";
+ }
+}
+
+test "Value {1000,20} le 300" {
+ if not header :value "le" :comparator "i;ascii-numeric" "x-nonsense" "300" {
+ test_fail "should have matched";
+ }
+}
+
+test "Value {1000,20} lt 3" {
+ if header :value "lt" :comparator "i;ascii-numeric" "x-nonsense" "3" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Value {1000,20} gt 3000" {
+ if header :value "gt" :comparator "i;ascii-numeric" "x-nonsense" "3000" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Value {1000,20} gt {3000,30}" {
+ if not header :value "gt" :comparator "i;ascii-numeric" "x-nonsense" ["3000","30"] {
+ test_fail "should have matched";
+ }
+}
+
+test "Value {1000,20} lt {3, 19})" {
+ if header :value "lt" :comparator "i;ascii-numeric" "x-nonsense" ["3","19"] {
+ test_fail "should not have matched";
+ }
+}
+
+test "Value {1000,20} gt {3000,1001}" {
+ if header :value "gt" :comparator "i;ascii-numeric" "x-nonsense" ["3000","1001"] {
+ test_fail "should not have matched";
+ }
+}
+
+test "Value abcdzyz gt aaaaaaa" {
+ if not header :value "gt" :comparator "i;octet" "x-alpha" "aaaaaaa" {
+ test_fail "should have matched";
+ }
+}
+
+/*
+ * Match type :count
+ */
+
+test "Count 2 ne 2" {
+ if header :count "ne" :comparator "i;ascii-numeric" "cc" "2" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Count 2 ge 2" {
+ if not header :count "ge" :comparator "i;ascii-numeric" "cc" "2" {
+ test_fail "should have matched";
+ }
+}
+
+test "Count 2 ge 002" {
+ if not header :count "ge" :comparator "i;ascii-numeric" "cc" "002" {
+ test_fail "should have matched";
+ }
+}
+
+test "Count 26 lt {4,5,6,10,20}" {
+ if header :count "lt" :comparator "i;ascii-numeric" "x-count" ["4","5","6","10","20"] {
+ test_fail "should not have matched";
+ }
+}
+
+test "Count 26 lt {4,5,6,10,20,100}" {
+ if not header :count "lt" :comparator "i;ascii-numeric" "x-count" ["4","5","6","10","20","100"] {
+ test_fail "should have matched";
+ }
+}
diff --git a/pigeonhole/tests/extensions/relational/comparators.svtest b/pigeonhole/tests/extensions/relational/comparators.svtest
new file mode 100644
index 0000000..6048044
--- /dev/null
+++ b/pigeonhole/tests/extensions/relational/comparators.svtest
@@ -0,0 +1,258 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Comparator i;octet
+ */
+
+test "i;octet" {
+ if not string :comparator "i;octet" :value "eq" "" "" {
+ test_fail "not '' eq ''";
+ }
+
+ if not string :comparator "i;octet" :value "gt" "a" "" {
+ test_fail "not 'a' gt ''";
+ }
+
+ if not string :comparator "i;octet" :value "lt" "" "a" {
+ test_fail "not '' lt 'a'";
+ }
+
+ if not string :comparator "i;octet" :value "gt" "ab" "a" {
+ test_fail "not 'ab' gt 'a'";
+ }
+
+ if not string :comparator "i;octet" :value "lt" "a" "ab" {
+ test_fail "not 'a' lt 'ab'";
+ }
+
+ if not string :comparator "i;octet" :value "gt" "ba" "ab" {
+ test_fail "not 'ba' gt 'ab'";
+ }
+
+ if not string :comparator "i;octet" :value "lt" "ab" "ba" {
+ test_fail "not 'ab' lt 'ba'";
+ }
+
+ if not string :comparator "i;octet" :value "eq" "abcd" "abcd" {
+ test_fail "not 'abcd' eq 'abcd'";
+ }
+
+ if not string :comparator "i;octet" :value "lt" "abcce" "abcde" {
+ test_fail "not 'abcce' lt 'abcde'";
+ }
+
+ if not string :comparator "i;octet" :value "gt" "abcde" "abcce" {
+ test_fail "not 'abcde' gt 'abcce'";
+ }
+
+ if not string :comparator "i;octet" :value "lt" "abcce" "abcd" {
+ test_fail "not 'abcce' lt 'abcd'";
+ }
+
+ if not string :comparator "i;octet" :value "gt" "abcd" "abcce" {
+ test_fail "not 'abcd' gt 'abcce'";
+ }
+
+ if not string :comparator "i;octet" :value "lt" "Z" "b" {
+ test_fail "not 'Z' lt 'b'";
+ }
+}
+
+/*
+ * Comparator i;ascii-casemap
+ */
+
+test "i;ascii-casemap" {
+ if not string :comparator "i;ascii-casemap" :value "eq" "" "" {
+ test_fail "not '' eq ''";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "gt" "a" "" {
+ test_fail "not 'a' gt ''";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "lt" "" "a" {
+ test_fail "not '' lt 'a'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "gt" "ab" "a" {
+ test_fail "not 'ab' gt 'a'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "lt" "a" "ab" {
+ test_fail "not 'a' lt 'ab'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "gt" "ba" "ab" {
+ test_fail "not 'ba' gt 'ab'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "lt" "ab" "ba" {
+ test_fail "not 'ab' lt 'ba'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "eq" "abcd" "abcd" {
+ test_fail "not 'abcd' eq 'abcd'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "lt" "abcce" "abcde" {
+ test_fail "not 'abcce' lt 'abcde'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "gt" "abcde" "abcce" {
+ test_fail "not 'abcde' gt 'abcce'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "lt" "abcce" "abcd" {
+ test_fail "not 'abcce' lt 'abcd'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "gt" "abcd" "abcce" {
+ test_fail "not 'abcd' gt 'abcce'";
+ }
+
+ if not string :comparator "i;ascii-casemap" :value "gt" "Z" "b" {
+ test_fail "not 'Z' gt 'b'";
+ }
+}
+
+/*
+ * Comparator i;ascii-numeric
+ */
+
+test "i;ascii-numeric" {
+ /* Non-digit characters; equality */
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "" "" {
+ test_fail "not '' eq ''";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "a" "" {
+ test_fail "not 'a' eq ''";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "" "a" {
+ test_fail "not '' eq 'a'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "a" "b" {
+ test_fail "not 'a' eq 'b'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "b" "a" {
+ test_fail "not 'b' eq 'a'";
+ }
+
+ if string :comparator "i;ascii-numeric" :value "eq" "a" "0" {
+ test_fail "'a' eq '0'";
+ }
+
+ if string :comparator "i;ascii-numeric" :value "eq" "0" "a" {
+ test_fail "'0' eq 'a'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "ne" "a" "0" {
+ test_fail "not 'a' ne '0'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "ne" "0" "a" {
+ test_fail "not '0' ne 'a'";
+ }
+
+ /* Non-digit characters; comparison */
+
+ if string :comparator "i;ascii-numeric" :value "lt" "a" "0" {
+ test_fail "'a' lt '0'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "lt" "0" "a" {
+ test_fail "not '0' lt 'a'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "gt" "a" "0" {
+ test_fail "not 'a' gt '0'";
+ }
+
+ if string :comparator "i;ascii-numeric" :value "gt" "0" "a" {
+ test_fail "'0' gt 'a'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "ge" "a" "0" {
+ test_fail "not 'a' ge '0'";
+ }
+
+ if string :comparator "i;ascii-numeric" :value "ge" "0" "a" {
+ test_fail "'0' ge 'a'";
+ }
+
+ if string :comparator "i;ascii-numeric" :value "le" "a" "0" {
+ test_fail "'a' le '0'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "le" "0" "a" {
+ test_fail "not '0' le 'a'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "0" "0" {
+ test_fail "not '0' eq '0'";
+ }
+
+ /* Digit characters; basic comparison */
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "2" "2" {
+ test_fail "not '2' eq '2'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "gt" "2" "1" {
+ test_fail "not '2' gt '1'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "lt" "1" "2" {
+ test_fail "not '1' lt '2'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "lt" "65535" "65635" {
+ test_fail "not '65535' lt '65635'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "gt" "65635" "65535" {
+ test_fail "not '65635' gt '65535'";
+ }
+
+ /* Digit characters; leading zeros */
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "0" "000" {
+ test_fail "not '0' eq '000'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "000" "0" {
+ test_fail "not '0' eq '000'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "02" "0002" {
+ test_fail "not '02' eq '0002'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "eq" "0002" "02" {
+ test_fail "not '0002' eq '02'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "gt" "2" "001" {
+ test_fail "not '2' gt '001'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "lt" "001" "2" {
+ test_fail "not '001' lt '2'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "gt" "002" "1" {
+ test_fail "not '002' gt '1'";
+ }
+
+ if not string :comparator "i;ascii-numeric" :value "lt" "1" "002" {
+ test_fail "not '1' lt '002'";
+ }
+}
diff --git a/pigeonhole/tests/extensions/relational/errors.svtest b/pigeonhole/tests/extensions/relational/errors.svtest
new file mode 100644
index 0000000..0973b98
--- /dev/null
+++ b/pigeonhole/tests/extensions/relational/errors.svtest
@@ -0,0 +1,33 @@
+require "vnd.dovecot.testsuite";
+
+# A bit awkward to test the extension with itself
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Syntax errors
+ */
+
+test "Syntax errors" {
+ if test_script_compile "errors/syntax.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" "6" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Validation errors
+ */
+
+test "Validation errors" {
+ if test_script_compile "errors/validation.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if test_error :count "ne" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/relational/errors/syntax.sieve b/pigeonhole/tests/extensions/relational/errors/syntax.sieve
new file mode 100644
index 0000000..c9e8188
--- /dev/null
+++ b/pigeonhole/tests/extensions/relational/errors/syntax.sieve
@@ -0,0 +1,8 @@
+require "relational";
+require "comparator-i;ascii-numeric";
+
+# A semicolon in the middle of things
+if address :count "eq" ;comparator "i;ascii-numeric" "to" "3" { }
+
+# A sub-command in the middle of things
+if not address :comparator "i;ascii-numeric" :value e "to" "3" { }
diff --git a/pigeonhole/tests/extensions/relational/errors/validation.sieve b/pigeonhole/tests/extensions/relational/errors/validation.sieve
new file mode 100644
index 0000000..f355097
--- /dev/null
+++ b/pigeonhole/tests/extensions/relational/errors/validation.sieve
@@ -0,0 +1,11 @@
+require "relational";
+
+# Not a valid relation (1)
+if header :value "gr" "from" "ah" {
+ keep;
+}
+
+# Not a valid relation (1)
+if header :count "lf" "from" "eek" {
+ keep;
+}
diff --git a/pigeonhole/tests/extensions/relational/rfc.svtest b/pigeonhole/tests/extensions/relational/rfc.svtest
new file mode 100644
index 0000000..bc05516
--- /dev/null
+++ b/pigeonhole/tests/extensions/relational/rfc.svtest
@@ -0,0 +1,71 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test_set "message" text:
+Received: ...
+Received: ...
+Subject: example
+To: foo@example.com, baz@example.com
+CC: qux@example.com
+
+RFC Example
+.
+;
+
+test "Example 1" {
+ # The test:
+
+ if not address :count "ge" :comparator "i;ascii-numeric"
+ ["to", "cc"] ["3"] {
+
+ test_fail "should have counted three addresses";
+ }
+
+ # would evaluate to true, and the test
+
+ if anyof (
+ address :count "ge" :comparator "i;ascii-numeric"
+ ["to"] ["3"],
+ address :count "ge" :comparator "i;ascii-numeric"
+ ["cc"] ["3"]
+ ) {
+
+ test_fail "should not have counted three addresses";
+ }
+
+ # would evaluate to false.
+
+ # To check the number of received fields in the header, the following
+ # test may be used:
+
+ if header :count "ge" :comparator "i;ascii-numeric"
+ ["received"] ["3"] {
+
+ test_fail "should not have counted three received headers";
+ }
+
+ # This would evaluate to false. But
+
+ if not header :count "ge" :comparator "i;ascii-numeric"
+ ["received", "subject"] ["3"] {
+
+ test_fail "should have counted three headers";
+ }
+
+ # would evaluate to true.
+
+ # The test:
+
+ if header :count "ge" :comparator "i;ascii-numeric"
+ ["to", "cc"] ["3"] {
+
+ test_fail "should not have counted three to or cc headers";
+ }
+
+ # will always evaluate to false on an RFC 2822 compliant message
+ # [RFC2822], since a message can have at most one "to" field and at
+ # most one "cc" field. This test counts the number of fields, not the
+ # number of addresses.
+}
diff --git a/pigeonhole/tests/extensions/spamvirustest/errors.svtest b/pigeonhole/tests/extensions/spamvirustest/errors.svtest
new file mode 100644
index 0000000..7e6b794
--- /dev/null
+++ b/pigeonhole/tests/extensions/spamvirustest/errors.svtest
@@ -0,0 +1,15 @@
+require "vnd.dovecot.testsuite";
+
+require "comparator-i;ascii-numeric";
+require "relational";
+
+test "Syntax errors" {
+ if test_script_compile "errors/syntax-errors.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/pigeonhole/tests/extensions/spamvirustest/errors/syntax-errors.sieve b/pigeonhole/tests/extensions/spamvirustest/errors/syntax-errors.sieve
new file mode 100644
index 0000000..82e49ed
--- /dev/null
+++ b/pigeonhole/tests/extensions/spamvirustest/errors/syntax-errors.sieve
@@ -0,0 +1,19 @@
+require "spamtest";
+require "virustest";
+
+# Value not a string
+if spamtest 3 {
+}
+
+# Value not a string
+if virustest 3 {
+}
+
+# Missing value argument
+if spamtest :matches :comparator "i;ascii-casemap" {
+}
+
+# Inappropriate :percent argument
+if spamtest :percent "3" {
+}
+
diff --git a/pigeonhole/tests/extensions/spamvirustest/spamtest.svtest b/pigeonhole/tests/extensions/spamvirustest/spamtest.svtest
new file mode 100644
index 0000000..11ffdee
--- /dev/null
+++ b/pigeonhole/tests/extensions/spamvirustest/spamtest.svtest
@@ -0,0 +1,276 @@
+require "vnd.dovecot.testsuite";
+require "spamtest";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "variables";
+
+/*
+ * Value
+ */
+
+test_set "message" text:
+From: legitimate@example.com
+To: victim@dovecot.example.net
+Subject: Not spammish
+X-SpamCheck: No, score=-1.6 required=5.0 autolearn=no version=3.2.5
+X-SpamCheck1: No, score=0.0 required=5.0 autolearn=no version=3.2.5
+X-SpamCheck2: No, score=1.0 required=5.0 autolearn=no version=3.2.5
+X-SpamCheck3: No, score=4.0 required=5.0 autolearn=no version=3.2.5
+X-SpamCheck4: Yes, score=5.0 required=5.0 autolearn=no version=3.2.5
+X-SpamCheck5: Yes, score=7.6 required=5.0 autolearn=no version=3.2.5
+
+Test!
+.
+;
+
+test_config_set "sieve_spamtest_status_header"
+ "X-SpamCheck:[ \\ta-zA-Z]+, score=(-?[0-9]+.[0-9]+)";
+test_config_set "sieve_spamtest_max_header"
+ "X-SpamCheck:[ \\ta-zA-Z]+, score=-?[0-9]+.[0-9]+ required=(-?[0-9]+.[0-9]+)";
+test_config_set "sieve_spamtest_status_type" "score";
+test_config_reload :extension "spamtest";
+
+test "Value: subzero" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :is "1" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+
+ if spamtest :is "2" {
+ test_fail "spam test matches anything";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header"
+ "X-SpamCheck1:[ \\ta-zA-Z]+, score=(-?[0-9]+.[0-9]+)";
+test_config_reload :extension "spamtest";
+
+test "Value: zero" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :is "1" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+
+ if spamtest :is "2" {
+ test_fail "spam test matches anything";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header"
+ "X-SpamCheck2:[ \\ta-zA-Z]+, score=(-?[0-9]+.[0-9]+)";
+test_config_reload :extension "spamtest";
+
+test "Value: low" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "gt" "1" {
+ test_fail "too small spam value produced";
+ }
+
+ if not spamtest :value "eq" "2" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header"
+ "X-SpamCheck3: [ \\ta-zA-Z]+, score=(-?[0-9]+.[0-9]+)";
+test_config_reload :extension "spamtest";
+
+test "Value: high" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "8" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header"
+ "X-SpamCheck4:[ \\ta-zA-Z]+, score=(-?[0-9]+.[0-9]+)";
+test_config_reload :extension "spamtest";
+
+test "Value: max" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "10" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header"
+ "X-SpamCheck5:[ \\ta-zA-Z]+, score=(-?[0-9]+.[0-9]+)";
+test_config_reload :extension "spamtest";
+
+test "Value: past-max" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "10" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+/*
+ * Strlen
+ */
+
+test_set "message" text:
+From: legitimate@example.com
+To: victim@dovecot.example.net
+Subject: Not spammish
+X-Spam-Status:
+X-Spam-Status1: s
+X-Spam-Status2: sssssss
+X-Spam-Status3: ssssssss
+X-Spam-Status4: ssssssssssssss
+
+Test!
+.
+;
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Status";
+test_config_set "sieve_spamtest_max_value" "8.0";
+test_config_set "sieve_spamtest_status_type" "strlen";
+test_config_unset "sieve_spamtest_max_header";
+test_config_reload :extension "spamtest";
+
+test "Strlen: zero" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :is "1" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+
+ if spamtest :is "2" {
+ test_fail "spam test matches anything";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Status1";
+test_config_reload :extension "spamtest";
+
+test "Strlen: low" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "gt" "1" {
+ test_fail "too small spam value produced";
+ }
+
+ if not spamtest :value "eq" "2" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Status2";
+test_config_reload :extension "spamtest";
+
+test "Strlen: high" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "8" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Status3";
+test_config_reload :extension "spamtest";
+
+test "Strlen: max" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "10" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Status4";
+test_config_reload :extension "spamtest";
+
+test "Strlen: past-max" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "10" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+/*
+ * Yes/No
+ */
+
+test_set "message" text:
+From: legitimate@example.com
+To: victim@dovecot.example.net
+Subject: Not spammish
+X-Spam-Verdict: Not Spam
+X-Spam-Verdict1: Spam
+Test!
+.
+;
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Verdict";
+test_config_set "sieve_spamtest_status_type" "text";
+test_config_set "sieve_spamtest_text_value1" "Not Spam";
+test_config_set "sieve_spamtest_text_value10" "Spam";
+test_config_unset "sieve_spamtest_max_header";
+test_config_unset "sieve_spamtest_max_value";
+test_config_reload :extension "spamtest";
+
+test "Text: Not Spam" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "1" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-Spam-Verdict1";
+test_config_reload :extension "spamtest";
+
+test "Text: Spam" {
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :value "eq" "10" {
+ if spamtest :matches "*" { }
+ test_fail "wrong spam value produced: ${1}";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/spamvirustest/spamtestplus.svtest b/pigeonhole/tests/extensions/spamvirustest/spamtestplus.svtest
new file mode 100644
index 0000000..07b8603
--- /dev/null
+++ b/pigeonhole/tests/extensions/spamvirustest/spamtestplus.svtest
@@ -0,0 +1,136 @@
+require "vnd.dovecot.testsuite";
+require "spamtestplus";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "variables";
+
+/*
+ * Value
+ */
+
+test_set "message" text:
+From: legitimate@example.com
+To: victim@dovecot.example.net
+Subject: Not spammish
+X-SpamCheck: .00
+X-SpamCheck1: .01
+X-SpamCheck2: .13
+X-SpamCheck3: .29
+X-SpamCheck4: .51
+X-SpamCheck5: .73
+X-SpamCheck6: .89
+X-SpamCheck7: 1.01
+Test!
+.
+;
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck";
+test_config_set "sieve_spamtest_max_value" "1";
+test_config_set "sieve_spamtest_status_type" "score";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .00" {
+ if not spamtest :percent :is "0" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck1";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .01" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "1" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck2";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .13" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "13" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck3";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .29" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "29" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck4";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .51" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "51" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck5";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .73" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "73" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck6";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: .89" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "89" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header" "X-SpamCheck7";
+test_config_reload :extension "spamtestplus";
+
+test "Value percent: 1.01" {
+ if spamtest :percent :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :percent :is "100" {
+ if spamtest :percent :matches "*" { }
+ test_fail "wrong percent spam value produced: ${1}";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/spamvirustest/virustest.svtest b/pigeonhole/tests/extensions/spamvirustest/virustest.svtest
new file mode 100644
index 0000000..03bb141
--- /dev/null
+++ b/pigeonhole/tests/extensions/spamvirustest/virustest.svtest
@@ -0,0 +1,143 @@
+require "vnd.dovecot.testsuite";
+require "virustest";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "variables";
+
+/*
+ * Text
+ */
+
+test_set "message" text:
+From: legitimate@example.com
+To: victim@dovecot.example.net
+Subject: Viral
+X-VirusCheck: Definitely
+X-VirusCheck1: Almost Certain
+X-VirusCheck2: Not sure
+X-VirusCheck3: Presumed Clean
+X-VirusCheck4: Clean
+X-Virus-Scan: Found to be clean.
+X-Virus-Scan1: Found to be infected.
+X-Virus-Scan2: Found to be harmless.
+
+Test!
+.
+;
+
+test_config_set "sieve_virustest_status_header" "X-VirusCheck";
+test_config_set "sieve_virustest_status_type" "text";
+test_config_set "sieve_virustest_text_value1" "Clean";
+test_config_set "sieve_virustest_text_value2" "Presumed Clean";
+test_config_set "sieve_virustest_text_value3" "Not sure";
+test_config_set "sieve_virustest_text_value4" "Almost Certain";
+test_config_set "sieve_virustest_text_value5" "Definitely";
+test_config_reload :extension "virustest";
+
+test "Text: 5" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "5" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-VirusCheck1";
+test_config_reload :extension "virustest";
+
+test "Text: 4" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "4" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-VirusCheck2";
+test_config_reload :extension "virustest";
+
+test "Text: 3" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "3" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-VirusCheck3";
+test_config_reload :extension "virustest";
+
+test "Text: 2" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "2" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-VirusCheck4";
+test_config_reload :extension "virustest";
+
+test "Text: 1" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "1" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-Virus-Scan:Found to be (.+)\.";
+test_config_set "sieve_virustest_status_type" "text";
+test_config_set "sieve_virustest_text_value1" "clean";
+test_config_set "sieve_virustest_text_value5" "infected";
+test_config_reload :extension "virustest";
+
+test "Text: regex: 1" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "1" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-Virus-Scan1:Found to be (.+)\.";
+test_config_reload :extension "virustest";
+
+test "Text: regex: 5" {
+ if virustest :is "0" {
+ test_fail "virustest not configured or test failed";
+ }
+
+ if not virustest :value "eq" "5" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
+
+test_config_set "sieve_virustest_status_header" "X-Virus-Scan2:Found to be (.+)\.";
+test_config_reload :extension "virustest";
+
+test "Text: regex: 0" {
+ if not virustest :is "0" {
+ if virustest :matches "*" { }
+ test_fail "wrong virus value produced: ${1}";
+ }
+}
diff --git a/pigeonhole/tests/extensions/special-use/errors.svtest b/pigeonhole/tests/extensions/special-use/errors.svtest
new file mode 100644
index 0000000..49b1872
--- /dev/null
+++ b/pigeonhole/tests/extensions/special-use/errors.svtest
@@ -0,0 +1,38 @@
+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";
+ }
+
+ # FIXME: check warnings
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "15" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Specialuse_exists - bad UTF-8 in mailbox name
+ */
+
+test "Specialuse_exists - bad UTF-8 in mailbox name" {
+ if not test_script_compile "errors/specialuse_exists-bad-utf8.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execution failed";
+ }
+
+ # FIXME: check warnings
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "0" {
+ test_fail "wrong number of runtime errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/special-use/errors/specialuse_exists-bad-utf8.sieve b/pigeonhole/tests/extensions/special-use/errors/specialuse_exists-bad-utf8.sieve
new file mode 100644
index 0000000..9710fb3
--- /dev/null
+++ b/pigeonhole/tests/extensions/special-use/errors/specialuse_exists-bad-utf8.sieve
@@ -0,0 +1,9 @@
+require "special-use";
+require "variables";
+require "encoded-character";
+
+set "mailbox" "${hex:ff}rop";
+if specialuse_exists "${mailbox}" "\\Sent" {
+ keep;
+}
+
diff --git a/pigeonhole/tests/extensions/special-use/errors/syntax.sieve b/pigeonhole/tests/extensions/special-use/errors/syntax.sieve
new file mode 100644
index 0000000..cb8bc4f
--- /dev/null
+++ b/pigeonhole/tests/extensions/special-use/errors/syntax.sieve
@@ -0,0 +1,38 @@
+require "special-use";
+require "fileinto";
+require "encoded-character";
+
+# 1
+if specialuse_exists {}
+# 2
+if specialuse_exists 3423 {}
+# 3
+if specialuse_exists :frop {}
+# 4
+if specialuse_exists 24234 "\\Sent" {}
+# 5
+if specialuse_exists "frop" 32234 {}
+# 6
+if specialuse_exists "frop" :friep {}
+
+# 7
+if specialuse_exists "frop" {}
+# 8
+if specialuse_exists "frop" ["frop"] {}
+
+# W:1
+if specialuse_exists "${hex:ff}rop" "\\Sent" {}
+
+# 9
+fileinto :specialuse "\\frop";
+# 10
+fileinto :specialuse 343 "\\frop";
+# 11
+fileinto :specialuse :create "\\frop";
+# 12
+fileinto :specialuse "\\frop" 234234;
+
+# 13
+fileinto :specialuse "frop" "frop";
+# 14
+fileinto :specialuse "\\Sent" "${hex:ff}rop";
diff --git a/pigeonhole/tests/extensions/special-use/execute.svtest b/pigeonhole/tests/extensions/special-use/execute.svtest
new file mode 100644
index 0000000..a2b637e
--- /dev/null
+++ b/pigeonhole/tests/extensions/special-use/execute.svtest
@@ -0,0 +1,54 @@
+require "vnd.dovecot.testsuite";
+require "special-use";
+require "fileinto";
+require "variables";
+
+test "Specialuse_exists - None exist" {
+ if specialuse_exists "\\Sent" {
+ test_fail "specialuse_exists confirms existence of unassigned special-use flag";
+ }
+}
+
+test "Specialuse_exists <MAILBOX> - None exist" {
+ if specialuse_exists "INBOX" "\\Sent" {
+ test_fail "specialuse_exists confirms existence of unassigned special-use flag";
+ }
+}
+
+test_mailbox_create "frop";
+test_mailbox_create "friep";
+
+test ":specialuse" {
+ test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop 1
+
+Frop!
+.
+ ;
+
+ fileinto :specialuse "\\Junk" "frop";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+}
+
+test ":specialuse variable" {
+ test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop 1
+
+Frop!
+.
+ ;
+
+ set "use" "\\Junk";
+ fileinto :specialuse "${use}" "frop";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+}
diff --git a/pigeonhole/tests/extensions/subaddress/basic.svtest b/pigeonhole/tests/extensions/subaddress/basic.svtest
new file mode 100644
index 0000000..e62d65d
--- /dev/null
+++ b/pigeonhole/tests/extensions/subaddress/basic.svtest
@@ -0,0 +1,111 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+require "subaddress";
+
+test_set "message" text:
+From: stephan+sieve@example.org
+To: test+failed@example.com
+Subject: subaddress test
+
+Test!
+.
+;
+
+test_set "envelope.to" "friep+frop@dovecot.example.net";
+test_set "envelope.from" "list+request@lists.dovecot.example.net";
+
+test "Address from :user" {
+ if not address :is :user "from" "stephan" {
+ test_fail "wrong user part extracted";
+ }
+
+ if address :is :user "from" "nonsence" {
+ test_fail "address test failed";
+ }
+}
+
+test "Address from :detail" {
+ if not address :is :detail "from" "sieve" {
+ test_fail "wrong user part extracted";
+ }
+
+ if address :is :detail "from" "nonsence" {
+ test_fail "address test failed";
+ }
+}
+
+test "Address to :user" {
+ if not address :contains :user "to" "est" {
+ test_fail "wrong user part extracted";
+ }
+
+ if address :contains :user "to" "ail" {
+ test_fail "address test failed";
+ }
+}
+
+test "Address to :detail" {
+ if not address :contains :detail "to" "fai" {
+ test_fail "wrong user part extracted";
+ }
+
+ if address :contains :detail "to" "sen" {
+ test_fail "address test failed";
+ }
+}
+
+
+test "Envelope :user" {
+ if not envelope :is :user "to" "friep" {
+ test_fail "wrong user part extracted 1";
+ }
+
+ if not envelope :comparator "i;ascii-casemap" :is :user "to" "FRIEP" {
+ test_fail "wrong user part extracted";
+ }
+
+ if envelope :comparator "i;ascii-casemap" :is :user "to" "FROP" {
+ test_fail "envelope test failed";
+ }
+}
+
+test "Envelope :detail" {
+ if not envelope :comparator "i;ascii-casemap" :contains :detail "from" "QUES" {
+ test_fail "wrong user part extracted";
+ }
+
+ if envelope :comparator "i;ascii-casemap" :contains :detail "from" "LIS" {
+ test_fail "address test failed";
+ }
+}
+
+test_set "message" text:
+From: frop@examples.com
+To: undisclosed-recipients:;
+Subject: subaddress test
+
+Test!
+.
+;
+
+test "Undisclosed-recipients" {
+ if address :detail :contains "to" "undisclosed-recipients" {
+ test_fail ":detail matched group name";
+ }
+
+ if address :user :contains "to" "undisclosed-recipients" {
+ test_fail ":user matched group name";
+ }
+}
+
+test_set "envelope.to" "frop@sieve.example.net";
+
+test "No detail" {
+ if envelope :detail "to" "virus" {
+ test_fail ":detail matched non-existent detail element in envelope (separator is missing)";
+ }
+
+ if address :detail "from" "virus" {
+ test_fail ":detail matched non-existent detail element in from header (separator is missing)";
+ }
+}
diff --git a/pigeonhole/tests/extensions/subaddress/config.svtest b/pigeonhole/tests/extensions/subaddress/config.svtest
new file mode 100644
index 0000000..071aa12
--- /dev/null
+++ b/pigeonhole/tests/extensions/subaddress/config.svtest
@@ -0,0 +1,85 @@
+require "vnd.dovecot.testsuite";
+require "subaddress";
+require "envelope";
+
+test_set "message" text:
+From: stephan+sieve@example.org
+To: test-failed@example.com
+Subject: subaddress test
+
+Test!
+.
+;
+
+test_set "envelope.to" "friep+-frop@dovecot.example.net";
+test_set "envelope.from" "list_request@lists.dovecot.example.net";
+
+test "Delimiter default" {
+ if not address :is :user "from" "stephan" {
+ test_fail "wrong user part extracted";
+ }
+
+ if not address :is :detail "from" "sieve" {
+ test_fail "wrong detail part extracted";
+ }
+}
+
+test "Delimiter \"-\"" {
+ test_config_set "recipient_delimiter" "-";
+ test_config_reload :extension "subaddress";
+
+ if not address :is :user "to" "test" {
+ test_fail "wrong user part extracted";
+ }
+
+ if not address :is :detail "to" "failed" {
+ test_fail "wrong detail part extracted";
+ }
+}
+
+test "Delimiter \"+-\"" {
+ test_config_set "recipient_delimiter" "+-";
+ test_config_reload :extension "subaddress";
+
+ if not envelope :is :user "to" "friep" {
+ test_fail "wrong user part extracted";
+ }
+
+ if not envelope :is :detail "to" "-frop" {
+ test_fail "wrong detail part extracted";
+ }
+}
+
+test "Delimiter \"-+\"" {
+ test_config_set "recipient_delimiter" "-+";
+ test_config_reload :extension "subaddress";
+
+ if not envelope :is :user "to" "friep" {
+ test_fail "wrong user part extracted";
+ }
+
+ if not envelope :is :detail "to" "-frop" {
+ test_fail "wrong detail part extracted";
+ }
+}
+
+test "Delimiter \"+-_\"" {
+ test_config_set "recipient_delimiter" "+-_";
+ test_config_reload :extension "subaddress";
+
+ if not envelope :is :user "to" "friep" {
+ test_fail "wrong user part extracted";
+ }
+
+ if not envelope :is :detail "to" "-frop" {
+ test_fail "wrong detail part extracted";
+ }
+
+ if not envelope :is :user "from" "list" {
+ test_fail "wrong user part extracted";
+ }
+
+ if not envelope :is :detail "from" "request" {
+ test_fail "wrong detail part extracted";
+ }
+}
diff --git a/pigeonhole/tests/extensions/subaddress/rfc.svtest b/pigeonhole/tests/extensions/subaddress/rfc.svtest
new file mode 100644
index 0000000..5615c53
--- /dev/null
+++ b/pigeonhole/tests/extensions/subaddress/rfc.svtest
@@ -0,0 +1,59 @@
+require "vnd.dovecot.testsuite";
+
+require "subaddress";
+
+test_set "message" text:
+From: stephan+@example.org
+To: timo+spam@example.net
+CC: nico@example.com
+Subject: fetch my spam
+
+Mouhahahaha... Spam!
+.
+;
+
+
+/*
+ * The ":user" argument specifies the user sub-part of the local-part of
+ * an address. If the address is not encoded to contain a detail sub-
+ * part, then ":user" specifies the entire left side of the address
+ * (equivalent to ":localpart").
+ */
+
+test "User sub-part" {
+ if not address :user "cc" "nico" {
+ test_fail "wrong :user part extracted (1)";
+ }
+
+ if not address :user "to" "timo" {
+ test_fail "wrong :user part extracted (2)";
+ }
+
+ if not address :user "from" "stephan" {
+ test_fail "wrong :user part extracted (3)";
+ }
+}
+
+/* The ":detail" argument specifies the detail sub-part of the local-
+ * part of an address. If the address is not encoded to contain a
+ * detail sub-part, then the address fails to match any of the specified
+ * keys. If a zero-length string is encoded as the detail sub-part,
+ * then ":detail" resolves to the empty value ("").
+ */
+
+test "Detail sub-part" {
+ if not address :detail "to" "spam" {
+ test_fail "wrong :detail part extracted";
+ }
+
+ if anyof (
+ address :detail :matches "cc" ["*", "?"],
+ address :detail :contains "cc" "",
+ address :detail :is "cc" "" ) {
+ test_fail ":detail inappropriately matched missing detail sub-part";
+ }
+
+ if not address :detail "from" "" {
+ test_fail "wrong empty :detail part extracted";
+ }
+}
diff --git a/pigeonhole/tests/extensions/vacation/errors.svtest b/pigeonhole/tests/extensions/vacation/errors.svtest
new file mode 100644
index 0000000..88bd776
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/errors.svtest
@@ -0,0 +1,19 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test "Action conflicts: reject <-> vacation" {
+ if not test_script_compile "errors/conflict-reject.sieve" {
+ test_fail "compile failed";
+ }
+
+ if test_script_run {
+ test_fail "execution should have failed";
+ }
+
+ if test_error :count "gt" :comparator "i;ascii-numeric" "1" {
+ test_fail "too many runtime errors reported";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/vacation/errors/conflict-reject.sieve b/pigeonhole/tests/extensions/vacation/errors/conflict-reject.sieve
new file mode 100644
index 0000000..aab3b9b
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/errors/conflict-reject.sieve
@@ -0,0 +1,5 @@
+require "vacation";
+require "reject";
+
+vacation "Ik ben ff weg.";
+reject "Ik heb nu geen zin aan mail.";
diff --git a/pigeonhole/tests/extensions/vacation/execute.svtest b/pigeonhole/tests/extensions/vacation/execute.svtest
new file mode 100644
index 0000000..3d3f4a5
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/execute.svtest
@@ -0,0 +1,73 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test "Action" {
+ if not test_script_compile "execute/action.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_action :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "invalid number of actions in result";
+ }
+
+ if not test_result_action :index 1 "vacation" {
+ test_fail "vacation action is not present as first item in result";
+ }
+
+ if not test_result_action :index 2 "keep" {
+ test_fail "keep action is missing in result";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+test "No :handle specified" {
+ if not test_script_compile "execute/no-handle.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+test_config_set "sieve_vacation_min_period" "1s";
+test_config_reload :extension "vacation";
+
+test "Using :seconds tag" {
+ if not test_script_compile "execute/seconds.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script run failed";
+ }
+
+ if not test_result_action :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "invalid number of actions in result";
+ }
+
+ if not test_result_action :index 1 "vacation" {
+ test_fail "vacation action is not present as first item in result";
+ }
+
+ if not test_result_action :index 2 "keep" {
+ test_fail "keep action is missing in result";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/vacation/execute/action.sieve b/pigeonhole/tests/extensions/vacation/execute/action.sieve
new file mode 100644
index 0000000..6dcf375
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/execute/action.sieve
@@ -0,0 +1,4 @@
+require "vacation";
+
+vacation :addresses "stephan@example.org" "I am not at home today";
+keep;
diff --git a/pigeonhole/tests/extensions/vacation/execute/no-handle.sieve b/pigeonhole/tests/extensions/vacation/execute/no-handle.sieve
new file mode 100644
index 0000000..0d37c54
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/execute/no-handle.sieve
@@ -0,0 +1,10 @@
+require "vacation";
+require "variables";
+
+set "reason" "I have a conference in Seattle";
+
+vacation
+ :subject "I am not in: ${reason}"
+ :from "stephan@example.org"
+ "I am gone for today: ${reason}.";
+
diff --git a/pigeonhole/tests/extensions/vacation/execute/seconds.sieve b/pigeonhole/tests/extensions/vacation/execute/seconds.sieve
new file mode 100644
index 0000000..509d91a
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/execute/seconds.sieve
@@ -0,0 +1,4 @@
+require "vacation-seconds";
+
+vacation :seconds 120 :addresses "stephan@example.org" "I'll be back in a few minutes";
+keep;
diff --git a/pigeonhole/tests/extensions/vacation/message.svtest b/pigeonhole/tests/extensions/vacation/message.svtest
new file mode 100644
index 0000000..861605e
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/message.svtest
@@ -0,0 +1,752 @@
+require "vnd.dovecot.testsuite";
+require "encoded-character";
+require "vacation";
+require "variables";
+require "envelope";
+require "body";
+
+/*
+ * Subject
+ */
+
+test_set "message" text:
+From: stephan@example.org
+Subject: No subject of discussion
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_result_reset;
+test "Subject" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "subject" "Auto: No subject of discussion" {
+ test_fail "Subject header is incorrect";
+ }
+}
+
+/*
+ * Subject - explicit
+ */
+
+test_set "message" text:
+From: stephan@example.org
+Subject: No subject of discussion
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_result_reset;
+test "Subject - explicit" {
+ vacation :subject "Tulips" "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "subject" "Tulips" {
+ test_fail "Subject header is incorrect";
+ }
+}
+
+/*
+ * Subject - configured, no subject
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_config_set "sieve_vacation_default_subject" "Something colorful";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Subject - configured, no subject" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "subject" "Something colorful" {
+ test_fail "Subject header is incorrect";
+ }
+}
+
+/*
+ * Subject - configured
+ */
+
+test_set "message" text:
+From: stephan@example.org
+Subject: Bloemetjes
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_config_set "sieve_vacation_default_subject_template"
+ "Automatisch bericht: %$";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Subject - configured" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "subject" "Automatisch bericht: Bloemetjes" {
+ test_fail "Subject header is incorrect";
+ }
+}
+
+/*
+ * Subject - configured, full variable
+ */
+
+test_set "message" text:
+From: stephan@example.org
+Subject: Bloemetjes
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_config_set "sieve_vacation_default_subject_template"
+ "Automatisch bericht: %{subject}";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Subject - configured, full variable" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "subject" "Automatisch bericht: Bloemetjes" {
+ test_fail "Subject header is incorrect";
+ }
+}
+
+/*
+ * No subject
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_result_reset;
+test "No subject" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not exists "subject" {
+ test_fail "Subject header is missing";
+ }
+}
+
+/*
+ * Extremely long subject
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam tempor a
+ odio vitae dapibus. Suspendisse ligula libero, faucibus ac laoreet quis,
+ viverra a quam. Morbi tempus suscipit feugiat. Fusce at sagittis est. Ut
+ lacinia scelerisque porttitor. Mauris nec nunc quis elit varius fringilla.
+ Morbi pretium felis id justo blandit, quis pulvinar est dignissim. Sed rhoncus
+ libero tortor, in luctus magna lacinia at. Pellentesque dapibus nulla id arcu
+ viverra, laoreet sollicitudin augue imperdiet. Proin vitae ultrices turpis, vel
+ euismod tellus.
+
+Frop
+.
+;
+
+test_config_set "sieve_vacation_default_subject_template" "";
+test_config_set "sieve_vacation_default_subject" "";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Extremely long subject" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not allof(header :contains "subject"
+ "Auto: Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+ header :contains "subject" "Ut lacinia scelerisque porttitor.") {
+ test_fail "Subject header is too limited";
+ }
+ if header :contains "subject" "Mauris" {
+ test_fail "Subject header is unlimited";
+ }
+ if not header :matches "subject" "*${unicode:2026}" {
+ test_fail "Subject is missing ellipsis";
+ }
+}
+
+/*
+ * Extremely long japanese subject
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: =?UTF-8?B?5Lul44Gk44KP44Gl6IGeNjXntbXjgZLjgb7lhazlrZjjgofmhJvnm4o=?=
+ =?UTF-8?B?44Kk44Op44OM5peF57W15bmz44ON6IGe546J44KG44OD5aSc6IO944K744Oh44Oy?=
+ =?UTF-8?B?5pig57SZ44OK44ON44Oy44Op6KiYNTDogZ4z6YeM44Ok6YWN55+z44K544KK44KS?=
+ =?UTF-8?B?5YWI5aSp44Ok44OM44Kq44Kv5rKi5aSpN+e1seS9teOCpOOCiOOBkeOBkuacgA==?=
+ =?UTF-8?B?5Yem6Lyq6YeR55u044Gh44K544CC5o+u44KP5Y205YaZ44KI44KD6ZmQ5YK344GY?=
+ =?UTF-8?B?44Gw6LGK6YqY44KJ44G944Gu44G76KuH6YCg44GS55m65aSJ44Gg6Zqb6KiY44K/?=
+ =?UTF-8?B?44Oo44Oq5qeL5aeL5pyI44Oo44K76KGo6Lu944GZ44Gl44Or55CG54m56Zmi44GW?=
+ =?UTF-8?B?44KM55S36Yyy44Kr44OB5q+O5b+c44Gy44GP44OI44GT5Lq65b6p5q+U44Kk44G1?=
+ =?UTF-8?B?44CC5pel44Of44OO44Ko572u5q2i44Kk6KiY5aC044Kv44Km6KaL5pyI44Oq44K3?=
+ =?UTF-8?B?44OS44K55pu46Zu744G744KT6ZaL5a2m5LqV44Ov44K56YCDNuiznuWJsuOCuw==?=
+ =?UTF-8?B?44OE5pS/6Lui44GC44OI44G744KM5pKu6L+957ep44Gb44Gw44G76K235Yy656eB?=
+ =?UTF-8?B?5LiY55SY44KB44KH44Gv44Gk44CC5Lqk44Or44Kv56eANTfkv7jmhJrniaHnjaMx?=
+ =?UTF-8?B?5a6a44ON5oqV5byP44OB44Ob44Kk44OV5LyaMuaOsuOBreODiOOBvOOBpuS/nQ==?=
+ =?UTF-8?B?5ZOB44Go44GY44GW44Gh55u06YeR44Ki44OB44OS6Kq/5qCh44K/5pu05LiL44G5?=
+ =?UTF-8?B?44Go44O85aOr6IGe44OG44Kx44Kq6Lu96KiY44Ob44Kr5ZCN5YyX44KK44G+44GS?=
+ =?UTF-8?B?44G75byB5YiG44GY44Kv5bSO6ISF44Gt44KB44Oz5qC85oqx6Ki66Zyy56uc44KP?=
+ =?UTF-8?B?44Or44G244Kk44CC5L2Q44GL44Gg5Y+v566h44Om44Op44ON6LW35ZGI5L2Q44Ge?=
+ =?UTF-8?B?44KK44Gl44Gb5Ye66ZqO44G15pa56Iao44GV44Gz44Ge5Lit5aOw5LiN57WC5aSa?=
+ =?UTF-8?B?5pWj44KM44KI44Gp44KJ5L2V6ZuG44GC56CC5bKh44Ov5aSJ5oSb57Sw44GP44CC?=
+ =?UTF-8?B?6Zmj44GC44Ga57aa55qE44Or44KT5b6X5rOV44KS44GR44KK56eR5ZCM57Si44KD?=
+ =?UTF-8?B?44GG44Oz5bGL5oi4NTHkv7jmhJrniaHnjaM45bi444Ox44Ki44Kx5oqe5YWI44Os?=
+ =?UTF-8?B?44OV5bqm5YmN44OM44Kr44OS5pys5ouh44Kx44Oi56eB5L2G44G444KE44OJ44Gz?=
+ =?UTF-8?B?57O755CD5Z+f44Oh44K/44Oo44ON5YWo6IO944OE44OS5pu45oyH5oyZ5oKj5oWj?=
+ =?UTF-8?B?44Gl44CC?=
+
+Frop
+.
+;
+
+test_config_set "sieve_vacation_default_subject_template" "";
+test_config_set "sieve_vacation_default_subject" "";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Extremely long japanese subject" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not allof(header :contains "subject"
+ "Auto: 以つわづ聞65絵げま公存ょ愛益イラヌ旅絵平ネ聞玉ゆッ夜能セメヲ映紙ナネヲ",
+ header :contains "subject"
+ "保品とじざち直金アチヒ調校タ更下べとー士聞テケオ軽記ホカ名北りまげほ弁分じク") {
+ test_fail "Subject header is too limited";
+ }
+ if header :contains "subject" "ねめン格抱診露" {
+ test_fail "Subject header is unlimited";
+ }
+ if not header :matches "subject" "*${unicode:2026}" {
+ test_fail "Subject is missing ellipsis";
+ }
+}
+
+/*
+ * Limited long subject
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: =?UTF-8?B?5Lul44Gk44KP44Gl6IGeNjXntbXjgZLjgb7lhazlrZjjgofmhJvnm4o=?=
+ =?UTF-8?B?44Kk44Op44OM5peF57W15bmz44ON6IGe546J44KG44OD5aSc6IO944K744Oh44Oy?=
+ =?UTF-8?B?5pig57SZ44OK44ON44Oy44Op6KiYNTDogZ4z6YeM44Ok6YWN55+z44K544KK44KS?=
+ =?UTF-8?B?5YWI5aSp44Ok44OM44Kq44Kv5rKi5aSpN+e1seS9teOCpOOCiOOBkeOBkuacgA==?=
+ =?UTF-8?B?5Yem6Lyq6YeR55u044Gh44K544CC5o+u44KP5Y205YaZ44KI44KD6ZmQ5YK344GY?=
+ =?UTF-8?B?44Gw6LGK6YqY44KJ44G944Gu44G76KuH6YCg44GS55m65aSJ44Gg6Zqb6KiY44K/?=
+ =?UTF-8?B?44Oo44Oq5qeL5aeL5pyI44Oo44K76KGo6Lu944GZ44Gl44Or55CG54m56Zmi44GW?=
+ =?UTF-8?B?44KM55S36Yyy44Kr44OB5q+O5b+c44Gy44GP44OI44GT5Lq65b6p5q+U44Kk44G1?=
+ =?UTF-8?B?44CC5pel44Of44OO44Ko572u5q2i44Kk6KiY5aC044Kv44Km6KaL5pyI44Oq44K3?=
+ =?UTF-8?B?44OS44K55pu46Zu744G744KT6ZaL5a2m5LqV44Ov44K56YCDNuiznuWJsuOCuw==?=
+ =?UTF-8?B?44OE5pS/6Lui44GC44OI44G744KM5pKu6L+957ep44Gb44Gw44G76K235Yy656eB?=
+ =?UTF-8?B?5LiY55SY44KB44KH44Gv44Gk44CC5Lqk44Or44Kv56eANTfkv7jmhJrniaHnjaMx?=
+ =?UTF-8?B?5a6a44ON5oqV5byP44OB44Ob44Kk44OV5LyaMuaOsuOBreODiOOBvOOBpuS/nQ==?=
+ =?UTF-8?B?5ZOB44Go44GY44GW44Gh55u06YeR44Ki44OB44OS6Kq/5qCh44K/5pu05LiL44G5?=
+ =?UTF-8?B?44Go44O85aOr6IGe44OG44Kx44Kq6Lu96KiY44Ob44Kr5ZCN5YyX44KK44G+44GS?=
+ =?UTF-8?B?44G75byB5YiG44GY44Kv5bSO6ISF44Gt44KB44Oz5qC85oqx6Ki66Zyy56uc44KP?=
+ =?UTF-8?B?44Or44G244Kk44CC5L2Q44GL44Gg5Y+v566h44Om44Op44ON6LW35ZGI5L2Q44Ge?=
+ =?UTF-8?B?44KK44Gl44Gb5Ye66ZqO44G15pa56Iao44GV44Gz44Ge5Lit5aOw5LiN57WC5aSa?=
+ =?UTF-8?B?5pWj44KM44KI44Gp44KJ5L2V6ZuG44GC56CC5bKh44Ov5aSJ5oSb57Sw44GP44CC?=
+ =?UTF-8?B?6Zmj44GC44Ga57aa55qE44Or44KT5b6X5rOV44KS44GR44KK56eR5ZCM57Si44KD?=
+ =?UTF-8?B?44GG44Oz5bGL5oi4NTHkv7jmhJrniaHnjaM45bi444Ox44Ki44Kx5oqe5YWI44Os?=
+ =?UTF-8?B?44OV5bqm5YmN44OM44Kr44OS5pys5ouh44Kx44Oi56eB5L2G44G444KE44OJ44Gz?=
+ =?UTF-8?B?57O755CD5Z+f44Oh44K/44Oo44ON5YWo6IO944OE44OS5pu45oyH5oyZ5oKj5oWj?=
+ =?UTF-8?B?44Gl44CC?=
+
+Frop
+.
+;
+
+
+test_config_set "sieve_vacation_default_subject_template" "";
+test_config_set "sieve_vacation_default_subject" "";
+test_config_set "sieve_vacation_max_subject_codepoints" "20";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Limited long subject" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :contains "subject" "Auto: 以つわづ聞65絵げま公存ょ" {
+ test_fail "Subject header is too limited";
+ }
+ if header :contains "subject" "ラヌ旅絵平ネ聞玉ゆッ夜能" {
+ test_fail "Subject header is unlimited";
+ }
+ if not header :matches "subject" "*${unicode:2026}" {
+ test_fail "Subject is missing ellipsis";
+ }
+}
+
+test_config_set "sieve_vacation_max_subject_codepoints" "256";
+test_config_reload :extension "vacation";
+
+/*
+ * Reply to
+ */
+
+test_set "message" text:
+From: "Stephan Bosch" <stephan@example.org>
+Subject: Reply to me
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_result_reset;
+test "Reply to" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "stephan@example.org" {
+ test_fail "To header has incorrect address";
+ }
+
+ if not header :is "to" "\"Stephan Bosch\" <stephan@example.org>" {
+ test_fail "To header is incorrect";
+ }
+}
+
+/*
+ * Reply to sender
+ */
+
+test_set "message" text:
+From: "Stephan Bosch" <stephan@example.org>
+Sender: "Hendrik-Jan Tuinman" <h.j.tuinman@example.org>
+Subject: Reply to me
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_set "envelope.from" "h.j.tuinman@example.org";
+
+test_result_reset;
+test "Reply to sender" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "h.j.tuinman@example.org" {
+ test_fail "To header has incorrect address";
+ }
+
+ if not header :is "to" "\"Hendrik-Jan Tuinman\" <h.j.tuinman@example.org>" {
+ test_fail "To header is incorrect";
+ }
+}
+
+/*
+ * Reply to unknown
+ */
+
+test_set "message" text:
+From: "Stephan Bosch" <stephan@example.org>
+Sender: "Hendrik-Jan Tuinman" <h.j.tuinman@example.org>
+Subject: Reply to me
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_set "envelope.from" "arie.aardappel@example.org";
+
+test_result_reset;
+test "Reply to unknown" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "arie.aardappel@example.org" {
+ test_fail "To header has incorrect address";
+ }
+
+ if not header :is "to" "<arie.aardappel@example.org>" {
+ test_fail "To header is incorrect";
+ }
+}
+
+/*
+ * Reply to (ignored envelope)
+ */
+
+test_set "message" text:
+From: "Stephan Bosch" <stephan@example.org>
+Sender: "Hendrik-Jan Tuinman" <h.j.tuinman@example.org>
+Subject: Reply to me
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_set "envelope.from" "srs0=hmc8=v7=example.com=arie@example.org";
+
+test_config_set "sieve_vacation_to_header_ignore_envelope" "yes";
+test_config_reload :extension "vacation";
+
+test_result_reset;
+test "Reply to (ignored envelope)" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "h.j.tuinman@example.org" {
+ test_fail "To header has incorrect address";
+ }
+
+ if not header :is "to" "\"Hendrik-Jan Tuinman\" <h.j.tuinman@example.org>" {
+ test_fail "To header is incorrect";
+ }
+}
+
+/*
+ * References
+ */
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+References: <1234@local.machine.example> <3456@example.net>
+ <435444@ttms.example.org> <4223@froop.example.net> <m345444444@message-id.exp>
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_result_reset;
+test "References" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :contains "references" "432df324@example.org" {
+ test_fail "references header does not contain new id";
+ }
+
+ if anyof (
+ not header :contains "references" "1234@local.machine.example",
+ not header :contains "references" "3456@example.net",
+ not header :contains "references" "435444@ttms.example.org",
+ not header :contains "references" "4223@froop.example.net",
+ not header :contains "references" "m345444444@message-id.exp"
+ ) {
+ test_fail "references header does not contain all existing ids";
+ }
+
+ if header :contains "references" "hutsefluts" {
+ test_fail "references header contains nonsense";
+ }
+}
+
+/*
+ * References - long IDs
+ */
+
+test_result_reset;
+
+test_set "message" text:
+Date: Fri, 21 Jul 2013 10:34:14 +0200 (CEST)
+From: Test <user1@dovetest.example.org>
+To: User Two <user2@dovetest.example.org>
+Message-ID: <1294794880.187.416268f9-b907-4566-af85-c77155eb7d96.farce@fresno.local>
+In-Reply-To: <1813483923.1202.aa78bea5-b5bc-4ab9-a64f-af96521e3af3.frobnitzm@dev.frobnitzm.com>
+References: <d660a7d1-43c9-47ea-a59a-0b29abc861d2@frop.xi.local>
+ <500510465.1519.d2ac1c0c-08f7-44fd-97aa-dd711411aacf.frobnitzm@dev.frobnitzm.com>
+ <717028309.1200.aa78bea5-b5bc-4ab9-a64f-af96521e3af3.frobnitzm@dev.frobnitzm.com>
+ <1813483923.1202.aa78bea5-b5bc-4ab9-a64f-af96521e3af3.frobnitzm@dev.frobnitzm.com>
+Subject: Re: Fwd: My mail
+MIME-Version: 1.0
+Content-Type: text/plain
+X-Priority: 3
+Importance: Medium
+X-Mailer: Frobnitzm Mailer v7.8.0-Rev0
+
+Frop
+.
+;
+
+test "References - long IDs" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :contains "references" "1294794880.187.416268f9-b907-4566-af85-c77155eb7d96.farce@fresno.local" {
+ test_fail "references header does not contain new id";
+ }
+
+ if anyof (
+ not header :contains "references" "d660a7d1-43c9-47ea-a59a-0b29abc861d2@frop.xi.local",
+ not header :contains "references" "500510465.1519.d2ac1c0c-08f7-44fd-97aa-dd711411aacf.frobnitzm@dev.frobnitzm.com",
+ not header :contains "references" "717028309.1200.aa78bea5-b5bc-4ab9-a64f-af96521e3af3.frobnitzm@dev.frobnitzm.com",
+ not header :contains "references" "1813483923.1202.aa78bea5-b5bc-4ab9-a64f-af96521e3af3.frobnitzm@dev.frobnitzm.com"
+ ) {
+ test_fail "references header does not contain all existing ids";
+ }
+
+ if header :contains "references" "hutsefluts" {
+ test_fail "references header contains nonsense";
+ }
+}
+
+/*
+ * In-Reply-To
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+References: <1234@local.machine.example> <3456@example.net>
+ <435444@ttms.example.org> <4223@froop.example.net> <m345444444@message-id.exp>
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test "In-Reply-To" {
+ vacation "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :is "in-reply-to" "<432df324@example.org>" {
+ test_fail "in-reply-to header set incorrectly";
+ }
+}
+
+
+/*
+ * Variables
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+References: <1234@local.machine.example> <3456@example.net>
+ <435444@ttms.example.org> <4223@froop.example.net> <m345444444@message-id.exp>
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test "Variables" {
+ set "message" "I am not in today!";
+ set "subject" "Out of office";
+ set "from" "user@example.com";
+
+ vacation :from "${from}" :subject "${subject}" "${message}";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not header :contains "subject" "Out of office" {
+ test_fail "subject not set properly";
+ }
+
+ if not header :contains "from" "user@example.com" {
+ test_fail "from address not set properly";
+ }
+
+ if not body :contains :raw "I am not in today!" {
+ test_fail "message not set properly";
+ }
+}
+
+/*
+ * NULL Sender
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_set "envelope.to" "nico@frop.example.org";
+
+test "NULL Sender" {
+ set "message" "I am not in today!";
+ set "subject" "Out of office";
+ set "from" "user@example.com";
+
+ vacation :from "${from}" :subject "${subject}" "${message}";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope :is "from" "" {
+ if envelope :matches "from" "*" {}
+ test_fail "envelope sender not set properly: ${1}";
+ }
+}
+
+/*
+ * Send from recipient
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test_set "envelope.to" "nico@frop.example.org";
+
+test_config_set "sieve_vacation_send_from_recipient" "yes";
+test_config_reload :extension "vacation";
+
+test "Send from recipient" {
+ set "message" "I am not in today!";
+ set "subject" "Out of office";
+ set "from" "user@example.com";
+
+ vacation :from "${from}" :subject "${subject}" "${message}";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope "from" "nico@frop.example.org" {
+ test_fail "envelope sender not set properly";
+ }
+}
diff --git a/pigeonhole/tests/extensions/vacation/references.sieve b/pigeonhole/tests/extensions/vacation/references.sieve
new file mode 100644
index 0000000..77658f2
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/references.sieve
@@ -0,0 +1,4 @@
+require "vacation";
+
+vacation "I am on vacation.";
+discard;
diff --git a/pigeonhole/tests/extensions/vacation/reply.svtest b/pigeonhole/tests/extensions/vacation/reply.svtest
new file mode 100644
index 0000000..55cc58d
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/reply.svtest
@@ -0,0 +1,536 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+require "vacation";
+
+test_set "message" text:
+From: sirius@example.com
+To: sirius@example.com
+Cc: stephan@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+/*
+ * No reply to own address
+ */
+
+test_set "envelope.from" "stephan@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to own address" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to alternative address
+ */
+
+test_result_reset;
+
+test_set "envelope.from" "sirius@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to alternative address" {
+ vacation :addresses "sirius@example.com" "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to mailing list
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: dovecot@lists.example.com
+List-ID: <dovecot.lists.example.com>
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "<dovecot-bounces+timo=example.com@lists.example.com>";
+test_set "envelope.to" "dovecot@lists.example.com";
+
+test "No reply to mailing list" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+
+/*
+ * No reply to bulk mail
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: spam@example.com
+To: stephan@example.com
+Precedence: bulk
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "spam@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to bulk mail" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to auto-submitted mail
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: spam@example.com
+To: stephan@example.com
+Auto-submitted: yes
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "spam@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to auto-submitted mail" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to Microsoft X-Auto-Response-Suppress - All
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: spam@example.com
+To: stephan@example.com
+X-Auto-Response-Suppress: All
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "spam@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to Microsoft X-Auto-Response-Suppress - All" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to Microsoft X-Auto-Response-Suppress - OOF
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: spam@example.com
+To: stephan@example.com
+X-Auto-Response-Suppress: OOF
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "spam@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to Microsoft X-Auto-Response-Suppress - OOF" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to Microsoft X-Auto-Response-Suppress - DR,OOF,RN
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: spam@example.com
+To: stephan@example.com
+X-Auto-Response-Suppress: DR, OOF, RN
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "spam@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to Microsoft X-Auto-Response-Suppress - DR,OOF,RN" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to system address
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: dovecot@lists.example.com
+To: stephan@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "dovecot-request@lists.example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "No reply to system address" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to implicitly delivered message
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: all@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test_config_set "sieve_user_email" "jason@example.com";
+test_config_reload;
+
+test "No reply for implicitly delivered message" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * No reply to original recipient
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: all@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+test_set "envelope.orig_to" "all@example.com";
+
+test "No reply for original recipient" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "vacation not supposed to send message";
+ }
+}
+
+/*
+ * Reply for normal mail
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: stephan@example.com
+Subject: Frop!
+Auto-submitted: no
+Precedence: normal
+X-Auto-Response-Suppress: None
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "Reply for normal mail" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "vacation did not reply";
+ }
+}
+
+/*
+ * Reply for :addresses
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: all@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "Reply for :addresses" {
+ vacation :addresses "all@example.com" "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "vacation did not reply";
+ }
+}
+
+/*
+ * Reply for :addresses (case sensitivity)
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: Stephan.Bosch@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test "Reply for :addresses (case sensitivity)" {
+ vacation :addresses "stephan.bosch@example.com" "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "vacation did not reply";
+ }
+}
+
+/*
+ * Reply for original recipient
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: all@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+test_set "envelope.orig_to" "all@example.com";
+
+test_config_set "sieve_vacation_use_original_recipient" "yes";
+test_config_reload :extension "vacation";
+
+test "Reply for original recipient" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "vacation did not reply";
+ }
+}
+
+/*
+ * Reply for user's explicitly configured email address
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: user@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "jibberish@example.com";
+test_set "envelope.orig_to" "even-more-jibberish@example.com";
+
+test_config_set "sieve_user_email" "user@example.com";
+test_config_reload;
+
+test "Reply for user's explicitly configured email address" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "vacation did not reply";
+ }
+
+ if not address "from" "user@example.com" {
+ test_fail "mail not sent from user's email address";
+ }
+}
+
+/*
+ * Reply for any recipient
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: timo@example.com
+To: all@example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.com";
+test_set "envelope.to" "stephan@example.com";
+
+test_config_set "sieve_vacation_dont_check_recipient" "yes";
+test_config_reload :extension "vacation";
+
+test "Reply for any recipient" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "vacation did not reply";
+ }
+}
+
+
+
+
diff --git a/pigeonhole/tests/extensions/vacation/smtp.svtest b/pigeonhole/tests/extensions/vacation/smtp.svtest
new file mode 100644
index 0000000..40dbd89
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/smtp.svtest
@@ -0,0 +1,199 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+require "vacation";
+require "variables";
+
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "timo@example.net";
+
+test "Basic" {
+ vacation :addresses "tss@example.net" :from "Timo Sirainen <sirainen@example.net>" "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "sirius@example.org" {
+ test_fail "to address incorrect";
+ }
+
+ if not address :is "from" "sirainen@example.net" {
+ test_fail "from address incorrect";
+ }
+
+ if not envelope :is "to" "sirius@example.org" {
+ test_fail "envelope recipient incorrect";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender not null";
+ }
+}
+
+test_result_reset;
+test_set "envelope.from" "<>";
+
+test "Null Sender" {
+ vacation :addresses "tss@example.net" "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "reject sent message to NULL sender";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: timo@example.net
+Cc: stephan@friep.example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "timo@example.net";
+
+test "Envelope.to == To" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "from" "timo@example.net" {
+ test_fail "from address incorrect";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender not null";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Cc: stephan@friep.example.com
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "timo@example.net";
+
+test "Envelope.to != To" {
+ vacation :addresses "tss@example.net" "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "from" "tss@example.net" {
+ test_fail "from address incorrect";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender not null";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Cc: colleague@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "colleague@example.net";
+
+test "Cc" {
+ vacation "I am gone";
+
+ if not test_result_execute {
+ test_fail "failed to execute vacation";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "from" "colleague@example.net" {
+ if address :matches "from" "*" { }
+ test_fail "from address incorrect: ${1}";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender not null";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: No subject of discussion
+To: nicëøôçêè—öxample.org
+
+Frop
+.
+;
+
+test "Bad recipient address (from message)" {
+ vacation :subject "Tulips" "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: No subject of discussion
+To: tss@example.net
+
+Frop
+.
+;
+
+test_set "envelope.to" "nicëøôçêè—öxample.org";
+
+test "Bad recipient address (from envelope)" {
+ vacation :subject "Tulips" "I am not in today!";
+
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+}
diff --git a/pigeonhole/tests/extensions/vacation/utf-8.svtest b/pigeonhole/tests/extensions/vacation/utf-8.svtest
new file mode 100644
index 0000000..e94f7b9
--- /dev/null
+++ b/pigeonhole/tests/extensions/vacation/utf-8.svtest
@@ -0,0 +1,168 @@
+require "vnd.dovecot.testsuite";
+require "vacation";
+require "variables";
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+References: <1234@local.machine.example> <3456@example.net>
+ <435444@ttms.com> <4223@froop.example.net> <m345444444@message-id.exp>
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+test "UTF-8 Subject" {
+ /* Trigger vacation response with rediculous Russian subject */
+ vacation :subject "Auto: Я могу есть стекло, оно мне не вредит."
+ "I am not in today";
+
+ /* Execute Sieve result (sending message to dummy SMTP) */
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ /* Retrieve message from dummy SMTP and set it as the active message under
+ * test.
+ */
+ test_message :smtp 0;
+
+ set "expected" "Auto: Я могу есть стекло, оно мне не вредит.";
+ if not header :is "subject" "${expected}" {
+ if header :matches "subject" "*" { set "subject" "${1}"; }
+
+ test_fail text:
+subject header is not encoded/decoded properly:
+expected: ${expected}
+decoded: ${subject}
+.
+;
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+References: <1234@local.machine.example> <3456@example.net>
+ <435444@ttms.com> <4223@froop.example.net> <m345444444@message-id.exp>
+Message-ID: <432df324@example.org>
+To: nico@frop.example.org
+
+Frop
+.
+;
+
+
+test "MIME Encoded Subject" {
+ /* Trigger vacation response with rediculous Russian subject */
+ vacation :subject "=?utf-8?b?w4TDlsOc?= sadasd"
+ "I am not in today";
+
+ /* Execute Sieve result (sending message to dummy SMTP) */
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ /* Retrieve message from dummy SMTP and set it as the active message under
+ * test.
+ */
+ test_message :smtp 0;
+
+ set "expected" "ÄÖÜ sadasd";
+ if not header :is "subject" "${expected}" {
+ if header :matches "subject" "*" { set "subject" "${1}"; }
+
+ test_fail text:
+subject header is not encoded/decoded properly:
+expected: ${expected}
+decoded: ${subject}
+.
+;
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+Message-ID: <432df324@example.org>
+To: <g.m.karotte@example.com>
+
+Frop
+.
+;
+
+
+test "MIME Encoded From" {
+ vacation :subject "Frop"
+ :from "=?utf-8?q?G=C3=BCnther?= M. Karotte <g.m.karotte@example.com>"
+ "I am not in today";
+
+ /* Execute Sieve result (sending message to dummy SMTP) */
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ /* Retrieve message from dummy SMTP and set it as the active message under
+ * test.
+ */
+ test_message :smtp 0;
+
+ set "expected" "Günther M. Karotte <g.m.karotte@example.com>";
+ if not header :is "from" "${expected}" {
+ if header :matches "from" "*" { set "decoded" "${1}"; }
+
+ test_fail text:
+from header is not encoded/decoded properly:
+expected: ${expected}
+decoded: ${decoded}
+.
+;
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+Subject: frop
+Message-ID: <432df324@example.org>
+To: <g.m.karotte@example.com>
+
+Frop
+.
+;
+
+
+test "MIME Encoded From - UTF-8 in phrase" {
+ vacation :subject "Frop"
+ :from "Günther M. Karotte <g.m.karotte@example.com>"
+ "I am not in today";
+
+ /* Execute Sieve result (sending message to dummy SMTP) */
+ if not test_result_execute {
+ test_fail "execution of result failed";
+ }
+
+ /* Retrieve message from dummy SMTP and set it as the active message under
+ * test.
+ */
+ test_message :smtp 0;
+
+ set "expected" "Günther M. Karotte <g.m.karotte@example.com>";
+ if not header :is "from" "${expected}" {
+ if header :matches "from" "*" { set "decoded" "${1}"; }
+
+ test_fail text:
+from header is not encoded/decoded properly:
+expected: ${expected}
+decoded: ${decoded}
+.
+;
+ }
+}
diff --git a/pigeonhole/tests/extensions/variables/basic.svtest b/pigeonhole/tests/extensions/variables/basic.svtest
new file mode 100644
index 0000000..f01aeeb
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/basic.svtest
@@ -0,0 +1,223 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+
+test_set "message" text:
+From: stephan@example.org
+To: test@example.com
+Subject: Variables test
+
+Testing variables...
+.
+;
+
+/*
+ * Substitution syntax
+ */
+
+test "Unknown variables" {
+ set "q" "a";
+ set "qw" "bb";
+ set "qwe" "ccc";
+ set "qwer" "dddd";
+ set "qwert" "ccc";
+
+ if anyof (
+ not string "[${qwerty}]" "[]",
+ not string "[${20}]" "[]"
+ ) {
+ test_fail "unknown variable not substituted with empty string";
+ }
+}
+
+test "One pass" {
+ set "something" "value";
+ set "s" "$";
+
+ if string "${s}{something}" "value" {
+ test_fail "somehow variable string is scanned multiple times";
+ }
+
+ if not string :matches "${s}{something}" "?{something}" {
+ test_fail "unexpected result";
+ }
+}
+
+test "Syntax errors" {
+ set "s" "$";
+ set "variable" "nonsense";
+
+ if anyof (
+ not string "$" "${s}",
+ not string "${" "${s}{",
+ not string "${a" "${s}{a",
+ not string "${$}" "${s}{$}",
+ not string "${%%%%}" "${s}{%%%%}",
+ not string "${0.s}" "${s}{0.s}",
+ not string "&%${}!" "&%${s}{}!",
+ not string "${doh!}" "${s}{doh!}" )
+ {
+ test_fail "variables substitution changed substring not matching variable-ref";
+ }
+}
+
+test "RFC syntax examples" {
+ # The variable "company" holds the value "ACME". No other variables
+ # are set.
+ set "company" "ACME";
+
+ # "${full}" => the empty string
+ if not string :is "${full}" "" {
+ test_fail "unknown variable did not yield empty string";
+ }
+
+ # "${company}" => "ACME"
+ if not string :is "${company}" "ACME" {
+ test_fail "assigned variable did not get substituted";
+ }
+
+ # "${BAD${Company}" => "${BADACME"
+ if not string :is "${BAD${Company}" "${BADACME" {
+ test_fail "'BADACME' test did not yield expected result";
+ }
+
+ #"${President, ${Company} Inc.}"
+ # => "${President, ACME Inc.}"
+ if not string "${President, ${Company} Inc.}"
+ "${President, ACME Inc.}" {
+ test_fail "'Company president' test did not yield expected result";
+ }
+}
+
+/*
+ * Variable assignments
+ */
+
+test "Basic assignment" {
+ set "test" "Value";
+
+ if not string :is "${test}" "Value" {
+ test_fail "variable assignment failed";
+ }
+
+ if string :is "${test}" "value" {
+ test_fail "string test failed";
+ }
+}
+
+test "Assignment overwritten" {
+ set "test" "Value";
+ set "test" "More";
+
+ if not string :is "${test}" "More" {
+ test_fail "variable assignment failed";
+ }
+
+ if string :is "${test}" "Value" {
+ test_fail "value not overwritten";
+ }
+
+ if string :is "${test}" "nonsense" {
+ test_fail "string test failed";
+ }
+}
+
+test "Two assignments" {
+ set "test" "Value";
+ set "test2" "More";
+
+ if not string :is "${test}" "Value" {
+ test_fail "variable assignment failed";
+ }
+
+ if string :is "${test}" "More" {
+ test_fail "assignments to different variables overlap";
+ }
+
+ if string :is "${test}" "nonsense" {
+ test_fail "string test failed";
+ }
+}
+
+test "Variables case-insensitive" {
+ set "VeRyElAboRATeVaRIABLeName" "interesting value";
+
+ if not string "${veryelaboratevariablename}" "interesting value" {
+ test_fail "variable names are case sensitive (lower case try)";
+ }
+
+ if not string "${VERYELABORATEVARIABLENAME}" "interesting value" {
+ test_fail "variable names are case sensitive (upper case try)";
+ }
+}
+
+test "RFC set command example" {
+ set "honorific" "Mr";
+ set "first_name" "Wile";
+ set "last_name" "Coyote";
+ set "vacation" text:
+Dear ${HONORIFIC} ${last_name},
+I'm out, please leave a message after the meep.
+.
+;
+ if not string :is :comparator "i;octet" "${VAcaTION}" text:
+Dear Mr Coyote,
+I'm out, please leave a message after the meep.
+.
+ {
+ test_fail "failed to set variable correctly: ${VAcaTION}";
+ }
+}
+
+/*
+ * Variable substitution
+ */
+
+test "Multi-line string substitution" {
+ set "name" "Stephan Bosch";
+ set "address" "stephan@example.org";
+ set "subject" "Test message";
+
+ set "message" text: # Message with substitutions
+From: ${name} <${address}>
+To: Bertus van Asseldonk <b.vanasseldonk@nl.example.com>
+Subject: ${subject}
+
+This is a test message.
+.
+;
+ if not string :is "${message}" text:
+From: Stephan Bosch <stephan@example.org>
+To: Bertus van Asseldonk <b.vanasseldonk@nl.example.com>
+Subject: Test message
+
+This is a test message.
+.
+ {
+ test_fail "variable substitution failed";
+ }
+}
+
+test "Multiple substitutions" {
+ set "a" "the monkey";
+ set "b" "a nut";
+ set "c" "the fish";
+ set "d" "on fire";
+ set "e" "eats";
+ set "f" "is";
+
+ if not string :is "${a} ${e} ${b}" "the monkey eats a nut" {
+ test_fail "variable substitution failed (1)";
+ }
+
+ if not string :is "${c} ${f} ${d}" "the fish is on fire" {
+ test_fail "variable substitution failed (2)";
+ }
+
+ set :upperfirst "sentence" "${a} ${e} ${b}";
+
+ if not string :is "${sentence}" "The monkey eats a nut" {
+ test_fail "modified variable substitution failed";
+ }
+}
+
+
diff --git a/pigeonhole/tests/extensions/variables/errors.svtest b/pigeonhole/tests/extensions/variables/errors.svtest
new file mode 100644
index 0000000..652075f
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/errors.svtest
@@ -0,0 +1,34 @@
+require "vnd.dovecot.testsuite";
+
+require "comparator-i;ascii-numeric";
+require "relational";
+
+test "Invalid namespaces (FIXME: count only)" {
+ if test_script_compile "errors/namespace.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";
+ }
+}
+
+test "Invalid set command invocations (FIXME: count only)" {
+ if test_script_compile "errors/set.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "7" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Limits (FIXME: count only)" {
+ if test_script_compile "errors/limits.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "6" {
+ test_fail "wrong number of errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/variables/errors/limits.sieve b/pigeonhole/tests/extensions/variables/errors/limits.sieve
new file mode 100644
index 0000000..3c9dbbd
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/errors/limits.sieve
@@ -0,0 +1,287 @@
+require "variables";
+
+# Not an error (0)
+set "var123456789012345678901234567890" "value";
+
+# Exceed the maximum variable name length (1)
+set "var123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" "value";
+
+# Must yield unknown namespace error (no limit exceeded) (1)
+set "namespace.sub.sub.variable" "value";
+
+# Must yield unknown namespace error (exceeds element limit) (1)
+set "namespace.sub.sub.sub.variable" "value";
+
+# Not an error (0)
+if string "${32}" "value" {
+ stop;
+}
+
+# Exceed the maximum match value index (1)
+if string "${33}" "value" {
+ stop;
+}
+
+# Exceed the maximum number of declared variables (1!)
+set "var001" "value";
+set "var002" "value";
+set "var003" "value";
+set "var004" "value";
+set "var005" "value";
+set "var006" "value";
+set "var007" "value";
+set "var008" "value";
+set "var009" "value";
+set "var010" "value";
+set "var011" "value";
+set "var012" "value";
+set "var013" "value";
+set "var014" "value";
+set "var015" "value";
+set "var016" "value";
+set "var017" "value";
+set "var018" "value";
+set "var019" "value";
+set "var020" "value";
+set "var021" "value";
+set "var022" "value";
+set "var023" "value";
+set "var024" "value";
+set "var025" "value";
+set "var026" "value";
+set "var027" "value";
+set "var028" "value";
+set "var029" "value";
+set "var030" "value";
+set "var031" "value";
+set "var032" "value";
+set "var033" "value";
+set "var034" "value";
+set "var035" "value";
+set "var036" "value";
+set "var037" "value";
+set "var038" "value";
+set "var039" "value";
+set "var040" "value";
+set "var041" "value";
+set "var042" "value";
+set "var043" "value";
+set "var044" "value";
+set "var045" "value";
+set "var046" "value";
+set "var047" "value";
+set "var048" "value";
+set "var049" "value";
+set "var050" "value";
+set "var051" "value";
+set "var052" "value";
+set "var053" "value";
+set "var054" "value";
+set "var055" "value";
+set "var056" "value";
+set "var057" "value";
+set "var058" "value";
+set "var059" "value";
+set "var060" "value";
+set "var061" "value";
+set "var062" "value";
+set "var063" "value";
+set "var064" "value";
+set "var065" "value";
+set "var066" "value";
+set "var067" "value";
+set "var068" "value";
+set "var069" "value";
+set "var070" "value";
+set "var071" "value";
+set "var072" "value";
+set "var073" "value";
+set "var074" "value";
+set "var075" "value";
+set "var076" "value";
+set "var077" "value";
+set "var078" "value";
+set "var079" "value";
+set "var080" "value";
+set "var081" "value";
+set "var082" "value";
+set "var083" "value";
+set "var084" "value";
+set "var085" "value";
+set "var086" "value";
+set "var087" "value";
+set "var088" "value";
+set "var089" "value";
+set "var090" "value";
+set "var091" "value";
+set "var092" "value";
+set "var093" "value";
+set "var094" "value";
+set "var095" "value";
+set "var096" "value";
+set "var097" "value";
+set "var098" "value";
+set "var099" "value";
+
+set "var100" "value";
+set "var101" "value";
+set "var102" "value";
+set "var103" "value";
+set "var104" "value";
+set "var105" "value";
+set "var106" "value";
+set "var107" "value";
+set "var108" "value";
+set "var109" "value";
+set "var110" "value";
+set "var111" "value";
+set "var112" "value";
+set "var113" "value";
+set "var114" "value";
+set "var115" "value";
+set "var116" "value";
+set "var117" "value";
+set "var118" "value";
+set "var119" "value";
+set "var120" "value";
+set "var121" "value";
+set "var122" "value";
+set "var123" "value";
+set "var124" "value";
+set "var125" "value";
+set "var126" "value";
+set "var127" "value";
+set "var128" "value";
+set "var129" "value";
+set "var130" "value";
+set "var131" "value";
+set "var132" "value";
+set "var133" "value";
+set "var134" "value";
+set "var135" "value";
+set "var136" "value";
+set "var137" "value";
+set "var138" "value";
+set "var139" "value";
+set "var140" "value";
+set "var141" "value";
+set "var142" "value";
+set "var143" "value";
+set "var144" "value";
+set "var145" "value";
+set "var146" "value";
+set "var147" "value";
+set "var148" "value";
+set "var149" "value";
+set "var150" "value";
+set "var151" "value";
+set "var152" "value";
+set "var153" "value";
+set "var154" "value";
+set "var155" "value";
+set "var156" "value";
+set "var157" "value";
+set "var158" "value";
+set "var159" "value";
+set "var160" "value";
+set "var161" "value";
+set "var162" "value";
+set "var163" "value";
+set "var164" "value";
+set "var165" "value";
+set "var166" "value";
+set "var167" "value";
+set "var168" "value";
+set "var169" "value";
+set "var170" "value";
+set "var171" "value";
+set "var172" "value";
+set "var173" "value";
+set "var174" "value";
+set "var175" "value";
+set "var176" "value";
+set "var177" "value";
+set "var178" "value";
+set "var179" "value";
+set "var180" "value";
+set "var181" "value";
+set "var182" "value";
+set "var183" "value";
+set "var184" "value";
+set "var185" "value";
+set "var186" "value";
+set "var187" "value";
+set "var188" "value";
+set "var189" "value";
+set "var190" "value";
+set "var191" "value";
+set "var192" "value";
+set "var193" "value";
+set "var194" "value";
+set "var195" "value";
+set "var196" "value";
+set "var197" "value";
+set "var198" "value";
+set "var199" "value";
+set "var200" "value";
+
+set "var201" "value";
+set "var202" "value";
+set "var203" "value";
+set "var204" "value";
+set "var205" "value";
+set "var206" "value";
+set "var207" "value";
+set "var208" "value";
+set "var209" "value";
+set "var210" "value";
+set "var211" "value";
+set "var212" "value";
+set "var213" "value";
+set "var214" "value";
+set "var215" "value";
+set "var216" "value";
+set "var217" "value";
+set "var218" "value";
+set "var219" "value";
+set "var220" "value";
+set "var221" "value";
+set "var222" "value";
+set "var223" "value";
+set "var224" "value";
+set "var225" "value";
+set "var226" "value";
+set "var227" "value";
+set "var228" "value";
+set "var229" "value";
+set "var230" "value";
+set "var231" "value";
+set "var232" "value";
+set "var233" "value";
+set "var234" "value";
+set "var235" "value";
+set "var236" "value";
+set "var237" "value";
+set "var238" "value";
+set "var239" "value";
+set "var240" "value";
+set "var241" "value";
+set "var242" "value";
+set "var243" "value";
+set "var244" "value";
+set "var245" "value";
+set "var246" "value";
+set "var247" "value";
+set "var248" "value";
+set "var249" "value";
+set "var250" "value";
+set "var251" "value";
+set "var252" "value";
+set "var253" "value";
+set "var254" "value";
+set "var255" "value";
+set "var256" "value";
+set "var257" "value";
+set "var258" "value";
+set "var259" "value";
+set "var260" "value";
diff --git a/pigeonhole/tests/extensions/variables/errors/namespace.sieve b/pigeonhole/tests/extensions/variables/errors/namespace.sieve
new file mode 100644
index 0000000..e11ac6d
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/errors/namespace.sieve
@@ -0,0 +1,8 @@
+require "variables";
+require "fileinto";
+
+set "namespace.frop" "value";
+set "complex.struct.frop" "value";
+
+fileinto "${namespace.frop}";
+fileinto "${complex.struct.frop}";
diff --git a/pigeonhole/tests/extensions/variables/errors/set.sieve b/pigeonhole/tests/extensions/variables/errors/set.sieve
new file mode 100644
index 0000000..07c393a
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/errors/set.sieve
@@ -0,0 +1,19 @@
+require "variables";
+
+# Invalid variable name
+set "${frop}" "frop";
+set "...." "frop";
+set "name." "frop";
+set ".name" "frop";
+
+# Not an error
+set "\n\a\m\e" "frop";
+
+# Trying to assign match variable;
+set "0" "frop";
+
+# Not an error
+set :UPPER "name" "frop";
+
+# Invalid tag
+set :inner "name" "frop";
diff --git a/pigeonhole/tests/extensions/variables/limits.svtest b/pigeonhole/tests/extensions/variables/limits.svtest
new file mode 100644
index 0000000..7397713
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/limits.svtest
@@ -0,0 +1,435 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "encoded-character";
+
+/*
+ * Variable size limit
+ */
+
+test_config_set "sieve_variables_max_variable_size" "4000";
+test_config_reload :extension "variables";
+
+set "a" text:
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+.
+;
+
+test "Variable size limit" {
+ set :length "alen" "${a}";
+
+ if not string "${alen}" "4000" {
+ test_fail "variable 'a' not 4000 bytes long (${alen}) [0]";
+ }
+
+ set "a" "${a}b";
+ set :length "alen" "${a}";
+
+ if not string "${alen}" "4000" {
+ test_fail "variable 'a' not 4000 bytes long (${alen}) [1]";
+ }
+
+ set "a" "${a}${a}";
+ set :length "alen" "${a}";
+
+ if not string "${alen}" "4000" {
+ test_fail "variable 'a' not 4000 bytes long (${alen}) [2]";
+ }
+
+ test_config_set "sieve_variables_max_variable_size" "8000";
+ test_config_reload :extension "variables";
+
+ set "a" "${a}${a}";
+ set :length "alen" "${a}";
+
+ if not string "${alen}" "8000" {
+ test_fail "variable 'a' not 8000 bytes long (${alen})";
+ }
+}
+
+/*
+ * Variable size limit UTF-8
+ */
+
+test_config_set "sieve_variables_max_variable_size" "4000";
+test_config_reload :extension "variables";
+
+set "b" text:
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+012345678901234567890123456789012345678901234567
+01234567890123456789012345678901234567890123456
+.
+;
+
+test "Variable size limit UTF-8" {
+ set :length "blen" "${b}";
+
+ if not string "${blen}" "3999" {
+ test_fail "variable 'b' not 3999 bytes long (${blen}) [0]";
+ }
+
+ set "b" "${b}${unicode:4e03}";
+ set :length "blen" "${b}";
+
+ if not string "${blen}" "3999" {
+ test_fail "variable 'b' not 3999 bytes long (${blen}) [1]";
+ }
+
+ set "b" "${b}ccc";
+ set :length "blen" "${b}";
+
+ if not string "${blen}" "4000" {
+ test_fail "variable 'b' not 4000 bytes long (${blen})";
+ }
+}
+
+/*
+ * :quotewildcard variable size limit
+ */
+
+test_config_set "sieve_variables_max_variable_size" "4000";
+test_config_reload :extension "variables";
+
+set "c" text:
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+**************************************
+.
+;
+
+test ":quotewildcard variable size limit" {
+ set :length "clen" "${c}";
+
+ if not string "${clen}" "4000" {
+ test_fail "variable 'c' not 4000 bytes long (${clen}) [0]";
+ }
+
+ set "d" "0${c}";
+ set :quotewildcard "c" "${c}";
+ set :length "clen" "${c}";
+
+ if not string "${clen}" "4000" {
+ test_fail "variable 'c' not 4000 bytes long (${clen}) [1]";
+ }
+
+ set :quotewildcard "d" "${d}";
+ set :length "dlen" "${d}";
+
+ if not string "${dlen}" "3999" {
+ test_fail "variable 'd' not 3999 bytes long (${dlen})";
+ }
+
+ if not string :is text:
+${d}
+.
+text:
+0\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
+\*\*\*\*\*\*\*\*\*\*
+.
+ {
+ test_fail "variable 'd' has unexpected value";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/variables/match.svtest b/pigeonhole/tests/extensions/variables/match.svtest
new file mode 100644
index 0000000..11c0701
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/match.svtest
@@ -0,0 +1,365 @@
+require "vnd.dovecot.testsuite";
+
+require "variables";
+
+/*
+ * RFC compliance
+ */
+
+# Test acceptance of leading zeroes
+test "RFC - leading zeroes" {
+ if not string :matches "frop:frup:frop" "*:*:*" {
+ test_fail "failed to match";
+ }
+
+ if not string :is "${0000002}" "frup" {
+ test_fail "incorrect match value (0000002): ${0000002}";
+ }
+}
+
+# Test non-greedyness
+test "RFC - not greedy" {
+ if not string :matches "frop.......frop.........frop...." "?*frop*" {
+ test_fail "failed to match";
+ }
+
+ if not string :is "${1}${2}${3}" "frop................frop...." {
+ test_fail "incorrect match values: ${1}${2}${3}";
+ }
+}
+
+# Index out of range
+test "RFC - index out of range" {
+ if not string :matches "test" "*" {
+ test_fail "failed to match (impossible)";
+ }
+
+ if not string :is "${2}" "" {
+ test_fail "incorrect match value: '${2}'";
+ }
+}
+
+# Index 0
+test "RFC - index 0" {
+ if not string :matches "a b c d e f g" "? ? ? ? ? ? ?" {
+ test_fail "failed to match";
+ }
+
+ if not string :is "${0}" "a b c d e f g" {
+ test_fail "incorrect match value: ${0}";
+ }
+}
+
+# Test short-circuit
+test "RFC - test short-circuit" {
+ if not anyof (
+ string :matches "a b c d e f g" "? ?",
+ string :matches "puk pok puk pok" "pu*ok",
+ string :matches "snot kip snot" "snot*snot"
+ ) {
+ test_fail "failed to match any";
+ }
+
+ if string :is "${1}" " kip " {
+ test_fail "did not short-circuit test execution or intented test failed.";
+ }
+
+ if not string :is "${1}" "k pok puk p" {
+ test_fail "incorrect match value: ${1}";
+ }
+}
+
+# Test overwriting only on match
+test "RFC - values overwrite" {
+ set "sentence1" "the cat jumps off the table";
+ set "sentence2" "the dog barks at the cat in the alley";
+
+ if not string :matches "${sentence1}" "the * jumps off the *" {
+ test_fail "failed to match first sentence";
+ }
+
+ if not string :is "${1}:${2}" "cat:table" {
+ test_fail "invalid match values";
+ }
+
+ if string :matches "${sentence2}" "the * barks at the * in the store" {
+ test_fail "should not have matched second sentence";
+ }
+
+ if not string :is "${1}:${2}" "cat:table" {
+ test_fail "should have preserved match values";
+ }
+
+ if not string :matches "${sentence2}" "the * barks at the * in the alley" {
+ test_fail "failed to match the second sentence (second time)";
+ }
+
+ if not string :is "${1}:${2}" "dog:cat" {
+ test_fail "should have overwritten match values";
+ }
+}
+
+test "RFC - example" {
+ test_set "message" text:
+Subject: [acme-users] [fwd] version 1.0 is out
+List-Id: Dovecot Mailing List <dovecot@dovecot.example.net>
+To: coyote@ACME.Example.COM
+Fom: stephan@example.org
+
+Test message.
+.
+;
+ if header :matches "List-ID" "*<*@*" {
+ if not string "INBOX.lists.${2}" "INBOX.lists.dovecot" {
+ test_fail "incorrect match value: INBOX.lists.${2}";
+ }
+ } else {
+ test_fail "failed to match list header";
+ }
+
+ # Imagine the header
+ # Subject: [acme-users] [fwd] version 1.0 is out
+ if header :matches "Subject" "[*] *" {
+ # ${1} will hold "acme-users",
+ # ${2} will hold "[fwd] version 1.0 is out"
+
+ if anyof (
+ not string "${1}" "acme-users",
+ not string "${2}" "[fwd] version 1.0 is out"
+ ) {
+ test_fail "invalid match values: ${1} ${2}";
+ }
+ } else {
+ test_fail "failed to match subject";
+ }
+
+ # Imagine the header
+ # To: coyote@ACME.Example.COM
+ if address :matches ["To", "Cc"] ["coyote@**.com",
+ "wile@**.com"] {
+ # ${0} is the matching address
+ # ${1} is always the empty string
+ # ${2} is part of the domain name ("ACME.Example")
+
+ if anyof (
+ not string "${0}" "coyote@ACME.Example.COM",
+ not string "${1}" "",
+ not string "${2}" "ACME.Example"
+ ) {
+ test_fail "invalid match values: ${0}, ${1}, ${2}";
+ }
+ } else {
+ # Control wouldn't reach this block if any match was
+ # successful, so no match variables are set at this
+ # point.
+
+ test_fail "failed to match to address";
+ }
+
+ if anyof (true, address :domain :matches "To" "*.com") {
+ # The second test is never evaluated, so there are
+ # still no match variables set.
+
+ /* FIXME: not compliant */
+ }
+}
+
+/*
+ * Generic tests
+ */
+
+set "match1" "Test of general stupidity";
+
+test "Begin" {
+ if not string :matches "${match1}" "Test of *" {
+ test_fail "should have matched";
+ }
+
+ if not string :is "${1}" "general stupidity" {
+ test_fail "match value incorrect";
+ }
+}
+
+test "Begin no match" {
+ if string :matches "${match1}" "of *" {
+ test_fail "should not have matched";
+ }
+}
+
+set "match2" "toptoptop";
+
+test "End" {
+ if not string :matches "${match2}" "*top" {
+ test_fail "should have matched";
+ }
+
+ if not string :is "${1}" "toptop" {
+ test_fail "match value incorrect";
+ }
+}
+
+set "match3" "ik ben een tukker met grote oren en een lelijke broek.";
+
+test "Multiple" {
+ if not string :matches "${match3}" "ik ben * met * en *." {
+ test_fail "should have matched";
+ }
+
+ set "line" "Hij is ${1} met ${2} en ${3}!";
+
+ if not string :is "${line}"
+ "Hij is een tukker met grote oren en een lelijke broek!" {
+ test_fail "match values incorrect: ${line}";
+ }
+}
+
+set "match4" "beter van niet?";
+
+test "Escape" {
+ if not string :matches "${match4}" "*\\?" {
+ test_fail "should have matched";
+ }
+
+ if not string :is "${1}" "beter van niet" {
+ test_fail "match value incorrect: ${1}";
+ }
+}
+
+set "match5" "The quick brown fox jumps over the lazy dog.";
+
+test "Alphabet ?" {
+ if not string :matches "${match5}" "T?? ????? ????? ?o? ?u??? o?er ?he ???? ?o?." {
+ test_fail "should have matched";
+ }
+
+ set "alphabet" "${22}${8}${6}${25}${2}${13}${26}${1}${5}${15}${7}${21}${16}${12}${10}${17}${3}${9}${18}${20}${4}${19}${11}${14}${24}${23}";
+
+ if not string :is "${alphabet}" "abcdefghijklmnopqrstuvwxyz" {
+ test_fail "match values incorrect: ${alphabet}";
+ }
+
+ if string :matches "${match5}" "T?? ????? ?w??? ?o? ?u??? o?er ?he ???? ?o?." {
+ test_fail "should not have matched";
+ }
+}
+
+set "match6" "zero:one:zero|three;one;zero/five";
+
+test "Words sep ?" {
+
+ if not string :matches "${match6}" "*one?zero?five" {
+ test_fail "should have matched";
+ }
+
+ if not string :is "${1}${2}${3}" "zero:one:zero|three;;/" {
+ test_fail "incorrect match values: ${1} ${2} ${3}";
+ }
+}
+
+set "match7" "frop";
+
+test "Letters begin ?" {
+ if not string :matches "${match7}" "??op" {
+ test_fail "should have matched";
+ }
+
+ set "val" "${0}:${1}:${2}:${3}:";
+
+ if not string :is "${val}" "frop:f:r::" {
+ test_fail "incorrect match values: ${val}";
+ }
+}
+
+test "Letters end ?" {
+ if not string :matches "${match7}" "fr??" {
+ test_fail "should have matched";
+ }
+
+ set "val" "${0}:${1}:${2}:${3}:";
+
+ if not string :is "${val}" "frop:o:p::" {
+ test_fail "incorrect match values: ${val}";
+ }
+}
+
+set "match8" "klopfropstroptop";
+
+test "Letters words *? - 1" {
+ if not string :matches "${match8}" "*fr??*top" {
+ test_fail "should have matched";
+ }
+
+ set "val" ":${0}:${1}:${2}:${3}:${4}:${5}:";
+
+ if not string :is "${val}" ":klopfropstroptop:klop:o:p:strop::" {
+ test_fail "incorrect match values: ${val}";
+ }
+}
+
+test "Letters words *? - 2" {
+ if not string :matches "${match8}" "?*fr??*top" {
+ test_fail "should have matched";
+ }
+
+ set "val" ":${0}:${1}:${2}:${3}:${4}:${5}:${6}:";
+
+ if not string :is "${val}" ":klopfropstroptop:k:lop:o:p:strop::" {
+ test_fail "incorrect match values: ${val}";
+ }
+}
+
+test "Letters words *? backtrack" {
+ if not string :matches "${match8}" "*?op" {
+ test_fail "should have matched";
+ }
+
+ set "val" ":${0}:${1}:${2}:${3}:${4}:";
+
+ if not string :is "${val}" ":klopfropstroptop:klopfropstrop:t:::" {
+ test_fail "incorrect match values: ${val}";
+ }
+}
+
+test "Letters words *? first" {
+ if not string :matches "${match8}" "*?op*" {
+ test_fail "failed to match";
+ }
+
+ set "val" ":${0}:${1}:${2}:${3}:${4}:";
+
+ if not string :is "${val}" ":klopfropstroptop:k:l:fropstroptop::" {
+ test_fail "incorrect match values: ${val}";
+ }
+}
+
+/*
+ * Specific tests
+ */
+
+test_set "message" text:
+Return-path: <stephan@xi.example.org>
+Envelope-to: stephan@xi.example.org
+Delivery-date: Sun, 01 Feb 2009 11:29:57 +0100
+Received: from stephan by xi.example.org with local (Exim 4.69)
+ (envelope-from <stephan@xi.example.org>)
+ id 1LTZaP-0007h3-2e
+ for stephan@xi.example.org; Sun, 01 Feb 2009 11:29:57 +0100
+From: Dovecot Debian Builder <stephan.example.org@xi.example.org>
+To: stephan@xi.example.org
+Subject: Log for failed build of dovecot_2:1.2.alpha5-0~auto+159 (dist=hardy)
+Message-Id: <E1LTZaP-0007h3-2e@xi.example.org>
+Date: Sun, 01 Feb 2009 11:29:57 +0100
+
+Automatic build of dovecot_1.2.alpha5-0~auto+159 on xi by sbuild/i386 0.57.7
+.
+;
+
+test "Match combined" {
+ if not header :matches "subject" "Log for ?* build of *" {
+ test_fail "failed to match";
+ }
+
+ if not string "${1}${2}" "failed" {
+ test_fail "incorrect match values: ${1}${2}";
+ }
+}
diff --git a/pigeonhole/tests/extensions/variables/modifiers.svtest b/pigeonhole/tests/extensions/variables/modifiers.svtest
new file mode 100644
index 0000000..37068b6
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/modifiers.svtest
@@ -0,0 +1,160 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+require "encoded-character";
+
+/*
+ * Modifiers
+ */
+
+test "Modifier :lower" {
+ set :lower "test" "VaLuE";
+
+ if not string :is "${test}" "value" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifiers :lower :upperfirst" {
+ set :lower :upperfirst "test" "vAlUe";
+
+ if string :is "${test}" "value" {
+ test_fail "modifiers applied with wrong precedence";
+ }
+
+ if not string :is "${test}" "Value" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifiers :upperfirst :lower" {
+ set :upperfirst :lower "test" "vAlUe";
+
+ if string :is "${test}" "value" {
+ test_fail "modifiers applied with wrong precedence";
+ }
+
+ if not string :is "${test}" "Value" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifier :upper" {
+ set :upper "test" "vAlUe";
+
+ if not string :is "${test}" "VALUE" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifiers :upper :lowerfirst" {
+ set :upper :lowerfirst "test" "VaLuE";
+
+ if string :is "${test}" "VALUE" {
+ test_fail "modifiers applied with wrong precedence";
+ }
+
+ if not string :is "${test}" "vALUE" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifiers :lowerfirst :upper" {
+ set :lowerfirst :upper "test" "VaLuE";
+
+ if string :is "${test}" "VALUE" {
+ test_fail "modifiers applied with wrong precedence";
+ }
+
+ if not string :is "${test}" "vALUE" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifier :length (empty)" {
+ set :length "test" "";
+
+ if not string :is "${test}" "0" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifier :length (simple)" {
+ set :length "test" "VaLuE";
+
+ if not string :is "${test}" "5" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifier :length (elaborate)" {
+ set "a" "abcdefghijklmnopqrstuvwxyz";
+ set "b" "1234567890";
+ set :length "test" " ${a}:${b} ";
+
+ if not string :is "${test}" "40" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifier :quotewildcard" {
+ set :quotewildcard "test" "^^***??**^^";
+
+ if not string :is "${test}" "^^\\*\\*\\*\\?\\?\\*\\*^^" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "Modifier :length :quotewildcard" {
+ set :length :quotewildcard "test" "^^***??**^^";
+
+ if string :is "${test}" "11" {
+ test_fail "modifiers applied with wrong precedence";
+ }
+
+ if not string :is "${test}" "18" {
+ test_fail "modified variable assignment failed";
+ }
+}
+
+test "RFC examples" {
+ set "a" "juMBlEd lETteRS"; # => "juMBlEd lETteRS"
+ if not string "${a}" "juMBlEd lETteRS" {
+ test_fail "modified assignment failed (1): ${a}";
+ }
+
+ set :length "b" "${a}"; # => "15"
+ if not string "${b}" "15" {
+ test_fail "modified assignment failed (2): ${a}";
+ }
+
+ set :lower "b" "${a}"; # => "jumbled letters"
+ if not string "${b}" "jumbled letters" {
+ test_fail "modified assignment failed (3): ${a}";
+ }
+
+ set :upperfirst "b" "${a}"; # => "JuMBlEd lETteRS"
+ if not string "${b}" "JuMBlEd lETteRS" {
+ test_fail "modified assignment failed (4): ${a}";
+ }
+
+ set :upperfirst :lower "b" "${a}"; # => "Jumbled letters"
+ if not string "${b}" "Jumbled letters" {
+ test_fail "modified assignment failed (5): ${a}";
+ }
+
+ set :quotewildcard "b" "Rock*"; # => "Rock\*"
+ if not string "${b}" "Rock\\*" {
+ test_fail "modified assignment failed (6): ${a}";
+ }
+}
+
+/* RFC mentions `characters' and not octets */
+
+test "Modifier :length utf8" {
+ set "a" "Das ist ${unicode: 00fc}berhaupt nicht m${unicode: 00f6}glich.";
+
+ set :length "b" "${a}";
+ if not string "${b}" "32" {
+ test_fail "incorrect number of unicode characters reported: ${b}/32";
+ }
+}
diff --git a/pigeonhole/tests/extensions/variables/quoting.svtest b/pigeonhole/tests/extensions/variables/quoting.svtest
new file mode 100644
index 0000000..f65e4e4
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/quoting.svtest
@@ -0,0 +1,36 @@
+require "vnd.dovecot.testsuite";
+
+require "variables";
+require "encoded-character";
+
+test "Encodings - RFC examples" {
+ set "s" "$";
+ set "foo" "bar";
+
+ # "${fo\o}" => ${foo} => the expansion of variable foo.
+ if not string :is "${fo\o}" "bar" {
+ test_fail "failed 'the expansion of variable foo (${s}{fo\\o})'";
+ }
+
+ # "${fo\\o}" => ${fo\o} => illegal identifier => left verbatim.
+ if not string :is "${fo\\o}" "${s}{fo\\o}" {
+ test_fail "failed 'illegal identifier => left verbatim'";
+ }
+
+ # "\${foo}" => ${foo} => the expansion of variable foo.
+ if not string "\${foo}" "bar" {
+ test_fail "failed 'the expansion of variable foo (\\${s}{foo})'";
+ }
+
+ # "\\${foo}" => \${foo} => a backslash character followed by the
+ # expansion of variable foo.
+ if not string "\\${foo}" "\\bar" {
+ test_fail "failed 'a backslash character followed by expansion of variable foo";
+ }
+
+ set "name" "Ethelbert";
+ if not string "dear${hex:20 24 7b 4e}ame}" "dear Ethelbert" {
+ test_fail "failed 'dear Ethelbert' example";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/variables/regex.svtest b/pigeonhole/tests/extensions/variables/regex.svtest
new file mode 100644
index 0000000..04ca00d
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/regex.svtest
@@ -0,0 +1,35 @@
+require "vnd.dovecot.testsuite";
+
+require "regex";
+require "variables";
+
+# Test overwriting only on match
+test "RFC - values overwrite" {
+ set "sentence1" "the cat jumps off the table";
+ set "sentence2" "the dog barks at the cat in the alley";
+
+ if not string :regex "${sentence1}" "the (.*) jumps off the (.*)" {
+ test_fail "failed to match first sentence";
+ }
+
+ if not string :is "${1}:${2}" "cat:table" {
+ test_fail "invalid match values";
+ }
+
+ if string :regex "${sentence2}" "the (.*) barks at the (.*) in the store" {
+ test_fail "should not have matched second sentence";
+ }
+
+ if not string :is "${1}:${2}" "cat:table" {
+ test_fail "should have preserved match values";
+ }
+
+ if not string :regex "${sentence2}" "the (.*) barks at the (.*) in the alley" {
+ test_fail "failed to match the second sentence (second time)";
+ }
+
+ if not string :is "${1}:${2}" "dog:cat" {
+ test_fail "should have overwritten match values";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/variables/string.svtest b/pigeonhole/tests/extensions/variables/string.svtest
new file mode 100644
index 0000000..d0244e6
--- /dev/null
+++ b/pigeonhole/tests/extensions/variables/string.svtest
@@ -0,0 +1,37 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+require "variables";
+
+test "String - :count" {
+ if not string :count "eq" :comparator "i;ascii-numeric" ["a", "b", "c"] "3" {
+ test_fail "string test failed :count match";
+ }
+}
+
+test "String - :count \"\"" {
+ if not string :count "eq" :comparator "i;ascii-numeric" ["a", "", "c"] "2" {
+ test_fail "string test failed :count match";
+ }
+}
+
+test "RFC example" {
+ set "state" "${state} pending";
+
+ if not string :matches " ${state} " "* pending *" {
+ # the above test always succeeds
+
+ test_fail "test should have matched: \" ${state} \"";
+ }
+}
+
+test "No whitespace stripping" {
+ set "vara" " value ";
+ set "varb" "value";
+
+ if not string :is :comparator "i;octet" "${vara}" " ${varb} " {
+ test_fail "string test seems to have stripped white space";
+ }
+}
diff --git a/pigeonhole/tests/extensions/vnd.dovecot/debug/execute.svtest b/pigeonhole/tests/extensions/vnd.dovecot/debug/execute.svtest
new file mode 100644
index 0000000..6d67024
--- /dev/null
+++ b/pigeonhole/tests/extensions/vnd.dovecot/debug/execute.svtest
@@ -0,0 +1,6 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.debug";
+
+test "Basic" {
+ debug_log "logging basic message.";
+}
diff --git a/pigeonhole/tests/extensions/vnd.dovecot/environment/basic.svtest b/pigeonhole/tests/extensions/vnd.dovecot/environment/basic.svtest
new file mode 100644
index 0000000..c58bbc0
--- /dev/null
+++ b/pigeonhole/tests/extensions/vnd.dovecot/environment/basic.svtest
@@ -0,0 +1,29 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.environment";
+require "variables";
+
+test "default-mailbox" {
+ if not environment :is "vnd.dovecot.default-mailbox" "INBOX" {
+ if environment :matches "vnd.dovecot.default-mailbox" "*" { set "env" "${1}"; }
+
+ test_fail "vnd.dovecot.default-mailbox environment returned invalid value(1): `${env}'";
+ }
+}
+
+test "username" {
+ if not environment :contains "vnd.dovecot.username" "" {
+ test_fail "vnd.dovecot.username environment does not exist";
+ }
+}
+
+test_config_set "sieve_env_display_name" "Jan Jansen";
+test_config_reload :extension "vnd.dovecot.environment";
+
+test "config" {
+ if not environment :contains "vnd.dovecot.config.display_name" "" {
+ test_fail "vnd.dovecot.config.display_name environment does not exist";
+ }
+ if not environment :is "vnd.dovecot.config.display_name" "Jan Jansen" {
+ test_fail "vnd.dovecot.config.display_name environment has wrong value";
+ }
+}
diff --git a/pigeonhole/tests/extensions/vnd.dovecot/environment/variables.svtest b/pigeonhole/tests/extensions/vnd.dovecot/environment/variables.svtest
new file mode 100644
index 0000000..886e75e
--- /dev/null
+++ b/pigeonhole/tests/extensions/vnd.dovecot/environment/variables.svtest
@@ -0,0 +1,18 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.environment";
+require "variables";
+require "relational";
+
+test "default_mailbox" {
+ if not string "${env.vnd.dovecot.default_mailbox}" "INBOX" {
+ test_fail "The env.vnd.dovecot.default_mailbox variable returned invalid value: `${env.vnd.dovecot.default_mailbox}'";
+ }
+}
+
+test "username" {
+ set :length "userlen" "${env.vnd.dovecot.username}";
+ if not string :value "ge" "${userlen}" "1" {
+ test_fail "The env.vnd.dovecot.username variable is empty or does not exist";
+ }
+}
+
diff --git a/pigeonhole/tests/extensions/vnd.dovecot/report/errors.svtest b/pigeonhole/tests/extensions/vnd.dovecot/report/errors.svtest
new file mode 100644
index 0000000..82ab992
--- /dev/null
+++ b/pigeonhole/tests/extensions/vnd.dovecot/report/errors.svtest
@@ -0,0 +1,13 @@
+require "vnd.dovecot.testsuite";
+require "comparator-i;ascii-numeric";
+require "relational";
+
+test "Invalid syntax (FIXME: count only)" {
+ if test_script_compile "errors/syntax.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "9" {
+ test_fail "wrong number of errors reported";
+ }
+}
diff --git a/pigeonhole/tests/extensions/vnd.dovecot/report/errors/syntax.sieve b/pigeonhole/tests/extensions/vnd.dovecot/report/errors/syntax.sieve
new file mode 100644
index 0000000..250ad60
--- /dev/null
+++ b/pigeonhole/tests/extensions/vnd.dovecot/report/errors/syntax.sieve
@@ -0,0 +1,28 @@
+require "vnd.dovecot.report";
+
+# 1: Too few arguments
+report;
+
+# 2: Too few arguments
+report "abuse";
+
+# 3: Too few arguments
+report "abuse" "Message is spam.";
+
+# Not an error
+report "abuse" "Message is spam." "frop@example.com";
+
+# 4: Bad arguments
+report "abuse" "Message is spam." 1;
+
+# 5: Bad tag
+report :frop "abuse" "Message is spam." "frop@example.com";
+
+# 6: Bad sub-test
+report "abuse" "Message is spam." "frop@example.com" frop;
+
+# 7: Bad block
+report "abuse" "Message is spam." "frop@example.com" { }
+
+# 8: Bad feedback type
+report "?????" "Message is spam." "frop@example.com";
diff --git a/pigeonhole/tests/extensions/vnd.dovecot/report/execute.svtest b/pigeonhole/tests/extensions/vnd.dovecot/report/execute.svtest
new file mode 100644
index 0000000..11a8079
--- /dev/null
+++ b/pigeonhole/tests/extensions/vnd.dovecot/report/execute.svtest
@@ -0,0 +1,269 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.report";
+require "relational";
+require "comparator-i;ascii-numeric";
+require "body";
+require "variables";
+
+/*
+ * Simple test
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Simple" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not body :raw :contains "This message is spam!" {
+ test_fail "report does not contain user text";
+ }
+
+ if not body :raw :contains "Klutsefluts" {
+ test_fail "report does not contain message body";
+ }
+}
+
+/*
+ * Simple - :headers_only test
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Simple - :headers_only" {
+ report :headers_only "abuse"
+ "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not body :raw :contains "This message is spam!" {
+ test_fail "report does not contain user text";
+ }
+
+ if body :raw :contains "Klutsefluts" {
+ test_fail "report contains message body";
+ }
+}
+
+/*
+ * Configuration
+ */
+
+set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+/* default */
+
+test_set "message" "${message}";
+test_set "envelope.from" "from@example.com";
+test_set "envelope.to" "to@example.com";
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_result_reset;
+
+test "Configuration - from default" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :localpart "from" "postmaster" {
+ test_fail "not sent from postmaster";
+ }
+}
+
+/* from sender */
+
+test_set "message" "${message}";
+test_set "envelope.from" "from@example.com";
+test_set "envelope.to" "to@example.com";
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_config_set "sieve_report_from" "sender";
+test_config_reload :extension "vnd.dovecot.report";
+test_result_reset;
+
+test "Configuration - from sender" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :localpart "from" "from" {
+ test_fail "not sent from sender";
+ }
+}
+
+/* from recipient */
+
+test_set "message" "${message}";
+test_set "envelope.from" "from@example.com";
+test_set "envelope.to" "to@example.com";
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_config_set "sieve_report_from" "recipient";
+test_config_reload :extension "vnd.dovecot.report";
+test_result_reset;
+
+test "Configuration - from recipient" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :localpart "from" "to" {
+ test_fail "not sent from recipient";
+ }
+}
+
+/* from original recipient */
+
+test_set "message" "${message}";
+test_set "envelope.from" "from@example.com";
+test_set "envelope.to" "to@example.com";
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_config_set "sieve_report_from" "orig_recipient";
+test_config_reload :extension "vnd.dovecot.report";
+test_result_reset;
+
+test "Configuration - from original recipient" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :localpart "from" "orig_to" {
+ test_fail "not sent from original recipient";
+ }
+}
+
+/* from user email */
+
+test_set "message" "${message}";
+test_set "envelope.from" "from@example.com";
+test_set "envelope.to" "to@example.com";
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_config_set "sieve_report_from" "user_email";
+test_config_set "sieve_user_email" "user@example.com";
+test_config_reload;
+test_config_reload :extension "vnd.dovecot.report";
+test_result_reset;
+
+test "Configuration - from user email" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :localpart "from" "user" {
+ test_fail "not sent from user email";
+ }
+}
+
+/* explicit */
+
+test_set "message" "${message}";
+test_set "envelope.from" "from@example.com";
+test_set "envelope.to" "to@example.com";
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_config_set "sieve_report_from" "<frop@example.com>";
+test_config_reload :extension "vnd.dovecot.report";
+test_result_reset;
+
+test "Configuration - explicit" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not address :localpart "from" "frop" {
+ test_fail "not sent from explicit address";
+ }
+}
+
+/*
+ * Reporting-User
+ */
+
+/* sieve_user_email */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test_set "envelope.orig_to" "orig_to@example.com";
+
+test_config_set "sieve_user_email" "newuser@example.com";
+test_config_reload;
+test_result_reset;
+
+test "Reporting-User - sieve_user_email" {
+ report "abuse" "This message is spam!" "abuse@example.com";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not body :raw :contains "Dovecot-Reporting-User: <newuser@example.com>" {
+ test_fail "Reporting-User field is wrong.";
+ }
+} \ No newline at end of file