summaryrefslogtreecommitdiffstats
path: root/pigeonhole/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
commit0441d265f2bb9da249c7abf333f0f771fadb4ab5 (patch)
tree3f3789daa2f6db22da6e55e92bee0062a7d613fe /pigeonhole/tests
parentInitial commit. (diff)
downloaddovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.tar.xz
dovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.zip
Adding upstream version 1:2.3.21+dfsg1.upstream/1%2.3.21+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'pigeonhole/tests')
-rw-r--r--pigeonhole/tests/comparators/i-ascii-casemap.svtest39
-rw-r--r--pigeonhole/tests/comparators/i-octet.svtest37
-rw-r--r--pigeonhole/tests/compile/compile.svtest16
-rw-r--r--pigeonhole/tests/compile/errors.svtest395
-rw-r--r--pigeonhole/tests/compile/errors/address-part.sieve17
-rw-r--r--pigeonhole/tests/compile/errors/address.sieve71
-rw-r--r--pigeonhole/tests/compile/errors/comparator.sieve21
-rw-r--r--pigeonhole/tests/compile/errors/encoded-character.sieve23
-rw-r--r--pigeonhole/tests/compile/errors/envelope.sieve23
-rw-r--r--pigeonhole/tests/compile/errors/fileinto.sieve38
-rw-r--r--pigeonhole/tests/compile/errors/header.sieve57
-rw-r--r--pigeonhole/tests/compile/errors/if.sieve78
-rw-r--r--pigeonhole/tests/compile/errors/keep.sieve14
-rw-r--r--pigeonhole/tests/compile/errors/lexer.sieve68
-rw-r--r--pigeonhole/tests/compile/errors/match-type.sieve7
-rw-r--r--pigeonhole/tests/compile/errors/out-address.sieve33
-rw-r--r--pigeonhole/tests/compile/errors/parser.sieve78
-rw-r--r--pigeonhole/tests/compile/errors/require.sieve42
-rw-r--r--pigeonhole/tests/compile/errors/size.sieve47
-rw-r--r--pigeonhole/tests/compile/errors/stop.sieve33
-rw-r--r--pigeonhole/tests/compile/errors/tag.sieve16
-rw-r--r--pigeonhole/tests/compile/errors/typos.sieve29
-rw-r--r--pigeonhole/tests/compile/errors/unsupported.sieve30
-rw-r--r--pigeonhole/tests/compile/recover.svtest50
-rw-r--r--pigeonhole/tests/compile/recover/commands-endblock.sieve27
-rw-r--r--pigeonhole/tests/compile/recover/commands-semicolon.sieve16
-rw-r--r--pigeonhole/tests/compile/recover/tests-endcomma.sieve17
-rw-r--r--pigeonhole/tests/compile/redirect.sieve23
-rw-r--r--pigeonhole/tests/compile/trivial.sieve17
-rw-r--r--pigeonhole/tests/compile/warnings.svtest8
-rw-r--r--pigeonhole/tests/compile/warnings/eof.sieve2
-rw-r--r--pigeonhole/tests/compile/warnings/invalid-headers.sieve14
-rw-r--r--pigeonhole/tests/control-if.svtest292
-rw-r--r--pigeonhole/tests/control-stop.svtest29
-rw-r--r--pigeonhole/tests/deprecated/imapflags/errors.svtest24
-rw-r--r--pigeonhole/tests/deprecated/imapflags/errors/conflict-ihave.sieve6
-rw-r--r--pigeonhole/tests/deprecated/imapflags/errors/conflict.sieve4
-rw-r--r--pigeonhole/tests/deprecated/imapflags/execute.svtest92
-rw-r--r--pigeonhole/tests/deprecated/imapflags/execute/flags.sieve12
-rw-r--r--pigeonhole/tests/deprecated/imapflags/execute/mark.sieve11
-rw-r--r--pigeonhole/tests/deprecated/notify/basic.svtest59
-rw-r--r--pigeonhole/tests/deprecated/notify/denotify.svtest279
-rw-r--r--pigeonhole/tests/deprecated/notify/errors.svtest33
-rw-r--r--pigeonhole/tests/deprecated/notify/errors/conflict-ihave.sieve8
-rw-r--r--pigeonhole/tests/deprecated/notify/errors/conflict.sieve4
-rw-r--r--pigeonhole/tests/deprecated/notify/errors/options.sieve11
-rw-r--r--pigeonhole/tests/deprecated/notify/execute.svtest25
-rw-r--r--pigeonhole/tests/deprecated/notify/execute/duplicates.sieve4
-rw-r--r--pigeonhole/tests/deprecated/notify/mailto.svtest317
-rw-r--r--pigeonhole/tests/execute/actions.svtest80
-rw-r--r--pigeonhole/tests/execute/actions/fileinto.sieve17
-rw-r--r--pigeonhole/tests/execute/actions/redirect.sieve17
-rw-r--r--pigeonhole/tests/execute/address-normalize.svtest46
-rw-r--r--pigeonhole/tests/execute/errors-cpu-limit.svtest363
-rw-r--r--pigeonhole/tests/execute/errors.svtest152
-rw-r--r--pigeonhole/tests/execute/errors/action-duplicates.sieve4
-rw-r--r--pigeonhole/tests/execute/errors/actions-limit.sieve35
-rw-r--r--pigeonhole/tests/execute/errors/conflict-reject-fileinto.sieve5
-rw-r--r--pigeonhole/tests/execute/errors/conflict-reject-keep.sieve4
-rw-r--r--pigeonhole/tests/execute/errors/conflict-reject-redirect.sieve4
-rw-r--r--pigeonhole/tests/execute/errors/cpu-limit.sieve145
-rw-r--r--pigeonhole/tests/execute/errors/fileinto-bad-utf8.sieve7
-rw-r--r--pigeonhole/tests/execute/errors/fileinto-invalid-name.sieve5
-rw-r--r--pigeonhole/tests/execute/errors/fileinto.sieve3
-rw-r--r--pigeonhole/tests/execute/errors/redirect-limit.sieve5
-rw-r--r--pigeonhole/tests/execute/examples.svtest115
-rw-r--r--pigeonhole/tests/execute/mailstore.svtest84
-rw-r--r--pigeonhole/tests/execute/smtp.svtest449
-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
-rw-r--r--pigeonhole/tests/failures/fuzz1.svtest33
-rw-r--r--pigeonhole/tests/failures/fuzz2.svtest37
-rw-r--r--pigeonhole/tests/failures/fuzz3.svtest12
-rw-r--r--pigeonhole/tests/failures/mailbox-bad-utf8.svtest6
-rw-r--r--pigeonhole/tests/lexer.svtest39
-rw-r--r--pigeonhole/tests/match-types/contains.svtest81
-rw-r--r--pigeonhole/tests/match-types/is.svtest22
-rw-r--r--pigeonhole/tests/match-types/matches.svtest241
-rw-r--r--pigeonhole/tests/multiscript/basic.svtest91
-rw-r--r--pigeonhole/tests/multiscript/conflicts.svtest100
-rw-r--r--pigeonhole/tests/multiscript/fileinto-frop.sieve3
-rw-r--r--pigeonhole/tests/multiscript/fileinto-inbox.sieve4
-rw-r--r--pigeonhole/tests/multiscript/keep.sieve1
-rw-r--r--pigeonhole/tests/multiscript/notify.sieve3
-rw-r--r--pigeonhole/tests/multiscript/reject-1.sieve3
-rw-r--r--pigeonhole/tests/multiscript/reject-2.sieve3
-rw-r--r--pigeonhole/tests/multiscript/vacation.sieve3
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/addheader6
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/big8
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/cat3
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/cat-stdin3
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/crlf3
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/env3
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/frame7
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/modify8
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/program5
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/replace12
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/sleep103
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/sleep23
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/spamc6
-rwxr-xr-xpigeonhole/tests/plugins/extprograms/bin/stderr20
-rw-r--r--pigeonhole/tests/plugins/extprograms/errors.svtest32
-rw-r--r--pigeonhole/tests/plugins/extprograms/errors/arguments.sieve5
-rw-r--r--pigeonhole/tests/plugins/extprograms/errors/programname.sieve25
-rw-r--r--pigeonhole/tests/plugins/extprograms/execute/command.svtest27
-rw-r--r--pigeonhole/tests/plugins/extprograms/execute/errors.svtest53
-rw-r--r--pigeonhole/tests/plugins/extprograms/execute/errors/syntax.sieve38
-rw-r--r--pigeonhole/tests/plugins/extprograms/execute/errors/unknown-program.sieve3
-rw-r--r--pigeonhole/tests/plugins/extprograms/execute/errors/variables.sieve7
-rw-r--r--pigeonhole/tests/plugins/extprograms/execute/execute.svtest177
-rw-r--r--pigeonhole/tests/plugins/extprograms/filter/command.svtest10
-rw-r--r--pigeonhole/tests/plugins/extprograms/filter/errors.svtest39
-rw-r--r--pigeonhole/tests/plugins/extprograms/filter/errors/syntax.sieve22
-rw-r--r--pigeonhole/tests/plugins/extprograms/filter/errors/unknown-program.sieve3
-rw-r--r--pigeonhole/tests/plugins/extprograms/filter/execute.svtest213
-rw-r--r--pigeonhole/tests/plugins/extprograms/pipe/command.svtest10
-rw-r--r--pigeonhole/tests/plugins/extprograms/pipe/errors.svtest94
-rw-r--r--pigeonhole/tests/plugins/extprograms/pipe/errors/syntax.sieve22
-rw-r--r--pigeonhole/tests/plugins/extprograms/pipe/errors/timeout.sieve3
-rw-r--r--pigeonhole/tests/plugins/extprograms/pipe/errors/unknown-program.sieve3
-rw-r--r--pigeonhole/tests/plugins/extprograms/pipe/execute.svtest56
-rw-r--r--pigeonhole/tests/test-address.svtest434
-rw-r--r--pigeonhole/tests/test-allof.svtest446
-rw-r--r--pigeonhole/tests/test-anyof.svtest445
-rw-r--r--pigeonhole/tests/test-exists.svtest93
-rw-r--r--pigeonhole/tests/test-header.svtest280
-rw-r--r--pigeonhole/tests/test-size.svtest74
-rw-r--r--pigeonhole/tests/testsuite.svtest75
347 files changed, 23660 insertions, 0 deletions
diff --git a/pigeonhole/tests/comparators/i-ascii-casemap.svtest b/pigeonhole/tests/comparators/i-ascii-casemap.svtest
new file mode 100644
index 0000000..0891f3e
--- /dev/null
+++ b/pigeonhole/tests/comparators/i-ascii-casemap.svtest
@@ -0,0 +1,39 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: stephan@example.org
+Cc: frop@example.com
+To: test@dovecot.example.net
+X-A: This is a TEST header
+Subject: Test Message
+
+Test!
+.
+;
+
+test "i;ascii-casemap :contains (1)" {
+ if not header :contains :comparator "i;ascii-casemap" "X-A" "TEST" {
+ test_fail "should have matched";
+ }
+}
+
+test "i;ascii-casemap :contains (2)" {
+ if not header :contains :comparator "i;ascii-casemap" "X-A" "test" {
+ test_fail "should have matched";
+ }
+}
+
+test "i;ascii-casemap :matches (1)" {
+ if not header :matches :comparator "i;ascii-casemap" "X-A" "This*TEST*r" {
+ test_fail "should have matched";
+ }
+}
+
+test "i;ascii-casemap :matches (2)" {
+ if not header :matches :comparator "i;ascii-casemap" "X-A" "ThIs*tEsT*R" {
+ test_fail "should have matched";
+ }
+}
+
+
+
diff --git a/pigeonhole/tests/comparators/i-octet.svtest b/pigeonhole/tests/comparators/i-octet.svtest
new file mode 100644
index 0000000..b6041bc
--- /dev/null
+++ b/pigeonhole/tests/comparators/i-octet.svtest
@@ -0,0 +1,37 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: stephan@example.org
+Cc: frop@example.com
+To: test@dovecot.example.net
+X-A: This is a TEST header
+Subject: Test Message
+
+Test!
+.
+;
+
+test "i;octet :contains" {
+ if not header :contains :comparator "i;octet" "X-A" "TEST" {
+ test_fail "should have matched";
+ }
+}
+
+test "i;octet not :contains" {
+ if header :contains :comparator "i;octet" "X-A" "test" {
+ test_fail "should not have matched";
+ }
+}
+
+test "i;octet :matches" {
+ if not header :matches :comparator "i;octet" "X-A" "This*TEST*r" {
+ test_fail "should have matched";
+ }
+}
+
+test "i;octet not :matches" {
+ if header :matches :comparator "i;octet" "X-A" "ThIs*tEsT*R" {
+ test_fail "should not have matched";
+ }
+}
+
diff --git a/pigeonhole/tests/compile/compile.svtest b/pigeonhole/tests/compile/compile.svtest
new file mode 100644
index 0000000..7abda7f
--- /dev/null
+++ b/pigeonhole/tests/compile/compile.svtest
@@ -0,0 +1,16 @@
+require "vnd.dovecot.testsuite";
+
+# Just test whether valid scripts will compile without problems
+
+test "Trivial" {
+ if not test_script_compile "trivial.sieve" {
+ test_fail "could not compile";
+ }
+}
+
+test "Redirect" {
+ if not test_script_compile "redirect.sieve" {
+ test_fail "could not compile";
+ }
+}
+
diff --git a/pigeonhole/tests/compile/errors.svtest b/pigeonhole/tests/compile/errors.svtest
new file mode 100644
index 0000000..f17ea3f
--- /dev/null
+++ b/pigeonhole/tests/compile/errors.svtest
@@ -0,0 +1,395 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Errors triggered in the compiled scripts are pretty reduntant over the
+ * tested commands, but we want to be thorough.
+ */
+
+/*
+ * Lexer errors
+ */
+
+test "Lexer errors (FIXME: count only)" {
+ if test_script_compile "errors/lexer.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";
+ }
+}
+
+/*
+ * Parser errors
+ */
+
+test "Parser errors (FIXME: count only)" {
+ if test_script_compile "errors/parser.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";
+ }
+}
+
+/*
+ * Header test
+ */
+
+test "Header errors" {
+ if test_script_compile "errors/header.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";
+ }
+
+ if not test_error :index 1 :matches
+ "unknown * ':all' for * header test *" {
+ test_fail "error 1 is invalid";
+ }
+
+ if not test_error :index 2 :matches
+ "*header test * string list * 1 (header names), but * number *" {
+ test_fail "error 2 is invalid";
+ }
+
+ if not test_error :index 3 :matches
+ "*header test * string list * 2 (key list), * number *" {
+ test_fail "error 3 is invalid";
+ }
+
+ if not test_error :index 4 :matches
+ "unknown tagged argument ':tag' for the header test *" {
+ test_fail "error 4 is invalid";
+ }
+
+ if not test_error :index 5 :matches
+ "* header test requires 2 *, but 1 *" {
+ test_fail "error 5 is invalid";
+ }
+
+ if not test_error :index 6 :matches
+ "* header test requires 2 *, but 0 *" {
+ test_fail "error 6 is invalid";
+ }
+
+ if not test_error :index 7 :matches
+ "*header test accepts no sub-tests* specified*" {
+ test_fail "error 7 is invalid";
+ }
+
+ if not test_error :index 8 :matches
+ "* use test 'header' * command*" {
+ test_fail "error 8 is invalid";
+ }
+
+ if not test_error :index 9 :matches
+ "* use test 'header' * command*" {
+ test_fail "error 9 is invalid";
+ }
+
+ if test_error :index 4 :contains "radish" {
+ test_fail "error test matched nonsense";
+ }
+}
+
+/*
+ * Address test
+ */
+
+
+test "Address errors" {
+ if test_script_compile "errors/address.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";
+ }
+
+ if not test_error :index 1 :matches
+ "*unknown * ':nonsense' * address test*" {
+ test_fail "error 1 is invalid";
+ }
+
+ if not test_error :index 2 :matches
+ "*address test expects *string list * 1 (header list),* number * found*" {
+ test_fail "error 2 is invalid";
+ }
+
+ if not test_error :index 3 :matches
+ "*address test expects *string list * 2 (key list),* number * found*" {
+ test_fail "error 3 is invalid";
+ }
+
+ if not test_error :index 4 :matches
+ "*unexpected *':is' * address test*" {
+ test_fail "error 4 is invalid";
+ }
+
+ if not test_error :index 5 :matches
+ "*address test * 2 positional arg*, but 1*" {
+ test_fail "error 5 is invalid";
+ }
+
+ if not test_error :index 6 :matches
+ "*address test * 2 positional arg*, but 0*" {
+ test_fail "error 6 is invalid";
+ }
+
+ if not test_error :index 7 :matches
+ "*'frop' *not allowed *address test*" {
+ test_fail "error 7 is invalid";
+ }
+
+ if not test_error :index 8 :matches
+ "*'frop' *not allowed *address test*" {
+ test_fail "error 8 is invalid";
+ }
+
+ if test_error :index 23 :contains "radish" {
+ test_fail "error test matched nonsense";
+ }
+}
+
+/*
+ * If command
+ */
+
+test "If errors (FIXME: count only)" {
+ if test_script_compile "errors/if.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";
+ }
+}
+
+/*
+ * Require command
+ */
+
+test "Require errors (FIXME: count only)" {
+ if test_script_compile "errors/require.sieve" {
+ test_fail "compile should have failed.";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "15" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Size test
+ */
+
+test "Size errors (FIXME: count only)" {
+ if test_script_compile "errors/size.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";
+ }
+}
+
+/*
+ * Envelope test
+ */
+
+test "Envelope errors (FIXME: count only)" {
+ if test_script_compile "errors/envelope.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";
+ }
+}
+
+/*
+ * Stop command
+ */
+
+test "Stop errors (FIXME: count only)" {
+ if test_script_compile "errors/stop.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";
+ }
+}
+
+/*
+ * Keep command
+ */
+
+test "Keep errors (FIXME: count only)" {
+ if test_script_compile "errors/keep.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";
+ }
+}
+
+/*
+ * Fileinto command
+ */
+
+test "Fileinto errors (FIXME: count only)" {
+ if test_script_compile "errors/fileinto.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";
+ }
+}
+
+/*
+ * COMPARATOR errors
+ */
+
+test "COMPARATOR errors (FIXME: count only)" {
+ if test_script_compile "errors/comparator.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";
+ }
+}
+
+/*
+ * ADDRESS-PART errors
+ */
+
+test "ADDRESS-PART errors (FIXME: count only)" {
+ if test_script_compile "errors/address-part.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";
+ }
+}
+
+/*
+ * MATCH-TYPE errors
+ */
+
+test "MATCH-TYPE errors (FIXME: count only)" {
+ if test_script_compile "errors/match-type.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";
+ }
+}
+
+/*
+ * Encoded-character errors
+ */
+
+test "Encoded-character errors (FIXME: count only)" {
+ if test_script_compile "errors/encoded-character.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";
+ }
+}
+
+/*
+ * Outgoing address errors
+ */
+
+test "Outgoing address errors (FIXME: count only)" {
+ if test_script_compile "errors/out-address.sieve" {
+ test_fail "compile should have failed.";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "16" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Tagged argument errors
+ */
+
+test "Tagged argument errors (FIXME: count only)" {
+ if test_script_compile "errors/tag.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";
+ }
+}
+
+/*
+ * Typos
+ */
+
+test "Typos" {
+ if test_script_compile "errors/typos.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";
+ }
+
+ if not test_error :index 1 :matches
+ "missing semicolon * fileinto *" {
+ test_fail "error 1 is invalid";
+ }
+
+ if not test_error :index 2 :matches
+ "*fileinto command * no *tests* specified*" {
+ test_fail "error 2 is invalid";
+ }
+
+ if not test_error :index 3 :matches
+ "missing semicolon * fileinto *" {
+ test_fail "error 3 is invalid";
+ }
+
+ if not test_error :index 4 :matches
+ "*address test requires 2 * 0 * specified" {
+ test_fail "error 4 is invalid";
+ }
+
+ if not test_error :index 5 :matches
+ "missing colon *matches* tag * address test" {
+ test_fail "error 5 is invalid";
+ }
+}
+
+
+/*
+ * Unsupported language features
+ */
+
+test "Unsupported language features (FIXME: count only)" {
+ if test_script_compile "errors/unsupported.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";
+ }
+}
diff --git a/pigeonhole/tests/compile/errors/address-part.sieve b/pigeonhole/tests/compile/errors/address-part.sieve
new file mode 100644
index 0000000..1d10cbf
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/address-part.sieve
@@ -0,0 +1,17 @@
+/*
+ * Address part errors
+ *
+ * Total errors: 2 (+1 = 3)
+ */
+
+# Duplicate address part (1)
+if address :all :comparator "i;octet" :domain "from" "STEPHAN" {
+
+ # Duplicate address part (2)
+ if address :domain :localpart :comparator "i;octet" "from" "friep.example.com" {
+ keep;
+ }
+
+ stop;
+}
+
diff --git a/pigeonhole/tests/compile/errors/address.sieve b/pigeonhole/tests/compile/errors/address.sieve
new file mode 100644
index 0000000..f7d3b26
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/address.sieve
@@ -0,0 +1,71 @@
+require "comparator-i;ascii-numeric";
+
+/*
+ * Address test errors
+ *
+ * Total count: 8 (+1 = 9)
+ */
+
+/*
+ * Command structure
+ */
+
+# Invalid tag
+if address :nonsense :comparator "i;ascii-casemap" :localpart "From" "nico" {
+ discard;
+}
+
+# Invalid first argument
+if address :is :comparator "i;ascii-numeric" :localpart 45 "nico" {
+ discard;
+}
+
+# Invalid second argument
+if address :is :comparator "i;ascii-numeric" :localpart "From" 45 {
+ discard;
+}
+
+# Invalid second argument
+if address :comparator "i;ascii-numeric" :localpart "From" :is {
+ discard;
+}
+
+# Missing second argument
+if address :is :comparator "i;ascii-numeric" :localpart "From" {
+ discard;
+}
+
+# Missing arguments
+if address :is :comparator "i;ascii-numeric" :localpart {
+ discard;
+}
+
+# Not an error
+if address :localpart :is :comparator "i;ascii-casemap" "from" ["frop", "frop"] {
+ discard;
+}
+
+/*
+ * Specified headers must contain addresses
+ */
+
+# Invalid header
+if address :is "frop" "frml" {
+ keep;
+}
+
+# Not an error
+if address :is "reply-to" "frml" {
+ keep;
+}
+
+# Invalid header (#2)
+if address :is ["to", "frop"] "frml" {
+ keep;
+}
+
+# Not an error
+if address :is ["to", "reply-to"] "frml" {
+ keep;
+}
+
diff --git a/pigeonhole/tests/compile/errors/comparator.sieve b/pigeonhole/tests/compile/errors/comparator.sieve
new file mode 100644
index 0000000..368b56b
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/comparator.sieve
@@ -0,0 +1,21 @@
+/*
+ * Address part errors
+ *
+ * Total errors: 5 (+1 = 6)
+ */
+
+# 1: No argument
+if address :comparator { }
+
+# 2: Number argument
+if address :comparator 1 "from" "frop" { }
+
+# 3: String list argument
+if address :comparator ["a", "b"] "from" "frop" { }
+
+# 4: Unknown tag
+if address :comparator :frop "from" "frop" { }
+
+# 5: Known tag
+if address :comparator :all "from" "frop" { }
+
diff --git a/pigeonhole/tests/compile/errors/encoded-character.sieve b/pigeonhole/tests/compile/errors/encoded-character.sieve
new file mode 100644
index 0000000..04d9de4
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/encoded-character.sieve
@@ -0,0 +1,23 @@
+/*
+ * Encoded-character errors
+ *
+ * Total errors: 2 (+1 = 3)
+ */
+
+require "encoded-character";
+require "fileinto";
+
+# Invalid unicode character (1)
+fileinto "INBOX.${unicode:200000}";
+
+# Not an error
+fileinto "INBOX.${unicode:200000";
+
+# Invalid unicode character (2)
+fileinto "INBOX.${Unicode:DF01}";
+
+# Not an error
+fileinto "INBOX.${Unicode:DF01";
+
+
+
diff --git a/pigeonhole/tests/compile/errors/envelope.sieve b/pigeonhole/tests/compile/errors/envelope.sieve
new file mode 100644
index 0000000..9639846
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/envelope.sieve
@@ -0,0 +1,23 @@
+/*
+ * Envelope test errors
+ *
+ * Total errors: 2 (+1 = 3)
+ */
+
+require "envelope";
+
+# Not an error
+if envelope :is "to" "frop@example.org" {
+}
+
+# Unknown envelope part (1)
+if envelope :is "frop" "frop@example.org" {
+}
+
+# Not an error
+if envelope :is ["to","from"] "frop@example.org" {
+}
+
+# Unknown envelope part (2)
+if envelope :is ["to","frop"] "frop@example.org" {
+}
diff --git a/pigeonhole/tests/compile/errors/fileinto.sieve b/pigeonhole/tests/compile/errors/fileinto.sieve
new file mode 100644
index 0000000..0598557
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/fileinto.sieve
@@ -0,0 +1,38 @@
+require "fileinto";
+require "encoded-character";
+
+/*
+ * Fileinto errors
+ *
+ * Total erors: 9 (+1 = 10)
+ */
+
+# Missing string argument
+fileinto;
+
+# Spurious test
+fileinto true;
+
+# Spurious test
+fileinto "Frop" true;
+
+# Spurious number argument
+fileinto 33;
+
+# Spurious tag argument
+fileinto :frop;
+
+# Spurious additional string argument
+fileinto "Frop" "Friep";
+
+# Spurious additional number argument
+fileinto "Frop" 123;
+
+# Spurious additional tag argument
+fileinto "Frop" :frop;
+
+# Bad mailbox name
+fileinto "${hex:ff}rop";
+
+# Not an error
+fileinto "Frop";
diff --git a/pigeonhole/tests/compile/errors/header.sieve b/pigeonhole/tests/compile/errors/header.sieve
new file mode 100644
index 0000000..1c87f94
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/header.sieve
@@ -0,0 +1,57 @@
+require "comparator-i;ascii-numeric";
+
+/*
+ * Compile errors for the header test
+ *
+ * Total errors: 9 (+1 validation failed msg = 10)
+ */
+
+# Unknown tagged argument
+if header :all :comparator "i;ascii-casemap" "From" "nico" {
+ keep;
+}
+
+# Wrong first argument
+if header :is :comparator "i;ascii-numeric" 45 "nico" {
+ keep;
+}
+
+# Wrong second argument
+if header :is :comparator "i;ascii-numeric" "From" 45 {
+ discard;
+}
+
+# Wrong second argument
+if header :is :comparator "i;ascii-numeric" "From" :tag {
+ stop;
+}
+
+# Missing second argument
+if header :is :comparator "i;ascii-numeric" "From" {
+ stop;
+}
+
+# Missing arguments
+if header :is :comparator "i;ascii-numeric" {
+ keep;
+}
+
+# Not an error
+if header :is :comparator "i;ascii-casemap" "frop" ["frop", "frop"] {
+ discard;
+}
+
+# Spurious sub-test
+if header "frop" "frop" true {
+ discard;
+}
+
+# Test used as command with block
+header "frop" "frop" {
+ discard;
+}
+
+# Test used as command
+header "frop" "frop";
+
+
diff --git a/pigeonhole/tests/compile/errors/if.sieve b/pigeonhole/tests/compile/errors/if.sieve
new file mode 100644
index 0000000..6a8537b
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/if.sieve
@@ -0,0 +1,78 @@
+/*
+ * If command errors
+ *
+ * Total errors: 11 (+1 = 12)
+ */
+
+# Spurious argument
+if "frop" true {}
+
+# Spurious argument
+elsif "frop" true {}
+
+# Spurious string list
+if [ "false", "false", "false" ] false {
+ stop;
+}
+
+# No block
+if true;
+
+# No test
+if {
+ keep;
+}
+
+# Spurious test list
+if ( false, false, true ) {
+ keep;
+}
+
+stop;
+
+# If-less else
+else {
+ keep;
+}
+
+# Not an error
+if true {
+ keep;
+}
+
+stop;
+
+# If-less if structure (should produce only one error)
+elsif true {
+ keep;
+}
+elsif true {
+ keep;
+}
+else {
+}
+
+# Elsif after else
+if true {
+ keep;
+} else {
+ stop;
+} elsif true {
+ stop;
+}
+
+# If used as test
+if if true {
+}
+
+# Else if instead of elsif
+
+if true {
+ stop;
+} else if false {
+ keep;
+}
+
+
+
+
diff --git a/pigeonhole/tests/compile/errors/keep.sieve b/pigeonhole/tests/compile/errors/keep.sieve
new file mode 100644
index 0000000..7b3397c
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/keep.sieve
@@ -0,0 +1,14 @@
+/*
+ * Keep errors
+ *
+ * Total erors: 2 (+1 = 3)
+ */
+
+# Spurious string argument
+keep "frop";
+
+# Spurious test
+keep true;
+
+# Not an error
+keep;
diff --git a/pigeonhole/tests/compile/errors/lexer.sieve b/pigeonhole/tests/compile/errors/lexer.sieve
new file mode 100644
index 0000000..9675db1
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/lexer.sieve
@@ -0,0 +1,68 @@
+/*
+ * Lexer tests
+ *
+ * Total errors: 8 (+1 = 9)
+ */
+
+/*
+ * Number limits
+ */
+
+# 1: Number too large
+if size :under 18446744073709551617 {
+ stop;
+}
+
+# 2: Number too large
+if size :under 18446744073709551616 {
+ stop;
+}
+
+# 3: Number too large
+if size :over 180143985094819840k {
+ stop;
+}
+
+# 4: Number too large
+if size :over 1006622342342296M {
+ stop;
+}
+
+# 5: Number too large
+if size :over 34359738368G {
+ stop;
+}
+
+# 6: Number far too large
+if size :over 49834598293485814273947921734981723971293741923 {
+ stop;
+}
+
+# Not an error
+if size :under 18446744073709551615 {
+ stop;
+}
+
+# Not an error
+if size :under 18446744073709551614 {
+ stop;
+}
+
+# Not an error
+if size :under 800G {
+ stop;
+}
+
+/*
+ * Identifier limits
+ */
+
+# 7: Identifier too long
+if this_is_a_rediculously_long_test_name {
+ stop;
+}
+
+# 8: Identifier way too long
+if test :this_is_an_even_more_rediculously_long_tagged_argument_name {
+ stop;
+}
diff --git a/pigeonhole/tests/compile/errors/match-type.sieve b/pigeonhole/tests/compile/errors/match-type.sieve
new file mode 100644
index 0000000..d8e1681
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/match-type.sieve
@@ -0,0 +1,7 @@
+require "comparator-i;ascii-numeric";
+
+if header :contains :comparator "i;ascii-numeric" "from" "friep.example.com" {
+ keep;
+}
+
+keep;
diff --git a/pigeonhole/tests/compile/errors/out-address.sieve b/pigeonhole/tests/compile/errors/out-address.sieve
new file mode 100644
index 0000000..3e39599
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/out-address.sieve
@@ -0,0 +1,33 @@
+require "vacation";
+
+# Error
+
+redirect "@wrong.example.com";
+redirect "error";
+redirect "error@";
+redirect "Stephan Bosch error@example.org";
+redirect "Stephan Bosch <error@example.org";
+redirect " more error @ example.com ";
+redirect "@";
+redirect "<>";
+redirect "Error <";
+redirect "Error <stephan";
+redirect "Error <stephan@";
+redirect "stephan@example.org,tss@example.net";
+redirect "stephan@example.org,%&^&!!~";
+redirect "rüdiger@example.com";
+
+vacation :from "Error" "Ik ben er niet.";
+
+# Ok
+
+redirect "Ok Good <stephan@example.org>";
+redirect "ok@example.com";
+redirect " more @ example.com ";
+
+redirect ".japanese@example.com";
+redirect "japanese.@example.com";
+redirect "japanese...localpart@example.com";
+redirect "..japanese...localpart..@example.com";
+
+vacation :from "good@voorbeeld.nl.example.com" "Ik ben weg!";
diff --git a/pigeonhole/tests/compile/errors/parser.sieve b/pigeonhole/tests/compile/errors/parser.sieve
new file mode 100644
index 0000000..26a1e53
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/parser.sieve
@@ -0,0 +1,78 @@
+/*
+ * Parser errors
+ *
+ * Total errors: 8 (+1 = 9)
+ */
+
+# Too many arguments (1)
+frop :this "is" "a" 2 :long "argument" "list" :and :it :should "fail" :during "parsing" :but "it" "should" "be"
+ "recoverable" "." :this "is" "a" 2 :long "argument" "list" :and :it :should "fail" :during "parsing" :but
+ "it" "should" "be" "recoverable" {
+ stop;
+}
+
+# Garbage argument (2)
+friep $$$;
+
+# Deep block nesting (1)
+if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ stop;
+ } } } } } } } }
+ } } } } } } } }
+ } } } } } } } }
+ } } } } } } } }
+} } } } } } } }
+
+# Deepest block and too deep test (list) nesting (1)
+if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true {
+ if
+ anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof (
+ anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof (
+ anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof (
+ anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof (
+ anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof ( anyof (
+ true
+ ))))))))
+ ))))))))
+ ))))))))
+ ))))))))
+ ))))))))
+ {
+ stop;
+ }
+ } } } } } }
+ } } } } } } } }
+ } } } } } } } }
+} } } } } } } }
+
+# Deepest block and too deep test nesting (1)
+if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true { if true { if true {
+ if true { if true { if true { if true { if true { if true {
+ if
+ not not not not not not not not
+ not not not not not not not not
+ not not not not not not not not
+ not not not not not not not not
+ not not not not not not not not false
+ {
+ stop;
+ }
+ } } } } } }
+ } } } } } } } }
+ } } } } } } } }
+} } } } } } } }
+
+
+# Garbage command; test wether previous errors were resolved (2)
+frop $$$$;
+
+
diff --git a/pigeonhole/tests/compile/errors/require.sieve b/pigeonhole/tests/compile/errors/require.sieve
new file mode 100644
index 0000000..ecbd7a2
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/require.sieve
@@ -0,0 +1,42 @@
+/*
+ * Require errors
+ *
+ * Total errors: 11 (+1 = 12)
+ */
+
+# Not an error
+require "fileinto";
+
+# Missing argument
+require;
+
+# Too many arguments
+require "fileinto" "vacation";
+
+# Invalid argument
+require 45;
+
+# Invalid extensions (3 errors)
+require ["_frop", "_friep", "_frml"];
+
+# Core commands required
+require ["redirect", "keep", "discard"];
+
+# Invalid arguments
+require "dovecot.test" true;
+
+# Invalid extension
+require "_frop";
+
+# Spurious command block
+require "fileinto" {
+ keep;
+}
+
+# Nested require
+if true {
+ require "relional";
+}
+
+# Require after other command than require
+require "copy";
diff --git a/pigeonhole/tests/compile/errors/size.sieve b/pigeonhole/tests/compile/errors/size.sieve
new file mode 100644
index 0000000..14dbee5
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/size.sieve
@@ -0,0 +1,47 @@
+/*
+ * Size test errors
+ *
+ * Total errors: 6 (+1 = 7)
+ */
+
+# Used as command (1)
+size :under 23;
+
+# Missing argument (2)
+if size {
+}
+
+# Missing :over/:under (3)
+if size 45 {
+ discard;
+}
+
+# No error
+if size :over 34K {
+ stop;
+}
+
+# No error
+if size :under 34M {
+ stop;
+}
+
+# Conflicting tags (4)
+if size :under :over 34 {
+ keep;
+}
+
+# Duplicate tags (5)
+if size :over :over 45M {
+ stop;
+}
+
+# Wrong argument order (6)
+if size 34M :over {
+ stop;
+}
+
+# No error; but worthy of a warning
+if size :under 0 {
+ stop;
+}
diff --git a/pigeonhole/tests/compile/errors/stop.sieve b/pigeonhole/tests/compile/errors/stop.sieve
new file mode 100644
index 0000000..75a3d76
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/stop.sieve
@@ -0,0 +1,33 @@
+/*
+ * Stop command errors
+ *
+ * Total errors: 7 (+1 = 8)
+ */
+
+# Spurious string argument
+stop "frop";
+
+# Spurious number argument
+stop 13;
+
+# Spurious string list argument
+stop [ "frop", "frop" ];
+
+# Spurious test
+stop true;
+
+# Spurious test list
+stop ( true, false );
+
+# Spurious command block
+stop {
+ keep;
+}
+
+# Spurious argument and test
+stop "frop" true {
+ stop;
+}
+
+# Not an error
+stop;
diff --git a/pigeonhole/tests/compile/errors/tag.sieve b/pigeonhole/tests/compile/errors/tag.sieve
new file mode 100644
index 0000000..7fa65e9
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/tag.sieve
@@ -0,0 +1,16 @@
+/*
+ * Tag errors
+ *
+ * Total errors: 2 (+1 = 3)
+ */
+
+# Unknown tag (1)
+if envelope :isnot :comparator "i;ascii-casemap" :localpart "From" "nico" {
+ discard;
+}
+
+# Spurious tag (1)
+if true :comparator "i;ascii-numeric" {
+ keep;
+}
+
diff --git a/pigeonhole/tests/compile/errors/typos.sieve b/pigeonhole/tests/compile/errors/typos.sieve
new file mode 100644
index 0000000..3d65b26
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/typos.sieve
@@ -0,0 +1,29 @@
+/*
+ * This test is primarily meant to check the compiler's handling of typos
+ * at various locations.
+ */
+
+require "fileinto";
+
+/*
+ * Missing semicolon
+ */
+
+fileinto "frop"
+keep;
+
+/* Other situations */
+
+fileinto "frup"
+true;
+
+fileinto "friep"
+snot;
+
+/*
+ * Forgot tag colon
+ */
+
+if address matches "from" "*frop*" {
+ stop;
+}
diff --git a/pigeonhole/tests/compile/errors/unsupported.sieve b/pigeonhole/tests/compile/errors/unsupported.sieve
new file mode 100644
index 0000000..9943f7b
--- /dev/null
+++ b/pigeonhole/tests/compile/errors/unsupported.sieve
@@ -0,0 +1,30 @@
+/*
+ * Handling of unsupported language features.
+ *
+ * Total errors: 3 (+1 = 4)
+ */
+
+require "variables";
+require "include";
+require "regex";
+
+/*
+ * Unsupported use of variables
+ */
+
+/* Comparator argument */
+
+set "comp" "i;ascii-numeric";
+
+if address :comparator "${comp}" "from" "stephan@example.org" {
+ stop;
+}
+
+/* Included script */
+
+set "script" "blacklist";
+
+include "${blacklist}";
+
+
+
diff --git a/pigeonhole/tests/compile/recover.svtest b/pigeonhole/tests/compile/recover.svtest
new file mode 100644
index 0000000..9c24c11
--- /dev/null
+++ b/pigeonhole/tests/compile/recover.svtest
@@ -0,0 +1,50 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Test parser's recover capability
+ */
+
+/*
+ * Commands
+ */
+
+/* Missing semicolon */
+
+test "Missing semicolons" {
+ if test_script_compile "recover/commands-semicolon.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";
+ }
+}
+
+/* End of block recovery*/
+
+test "Missing semicolon at end of block" {
+ if test_script_compile "recover/commands-endblock.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";
+ }
+}
+
+/*
+ * Tests
+ */
+
+test "Spurious comma at end of test list" {
+ if test_script_compile "recover/tests-endcomma.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";
+ }
+}
diff --git a/pigeonhole/tests/compile/recover/commands-endblock.sieve b/pigeonhole/tests/compile/recover/commands-endblock.sieve
new file mode 100644
index 0000000..c06e218
--- /dev/null
+++ b/pigeonhole/tests/compile/recover/commands-endblock.sieve
@@ -0,0 +1,27 @@
+if true {
+ if true {
+ # Missing semicolon
+ keep
+ }
+}
+
+if true {
+ # Erroneous syntax
+ keep,
+ keep
+}
+
+if true {
+ if anyof(true,true,false) {
+ keep;
+ }
+}
+
+if true {
+ if anyof(true,true,false) {
+ keep;
+ # Missing semicolon
+ discard
+ }
+}
+
diff --git a/pigeonhole/tests/compile/recover/commands-semicolon.sieve b/pigeonhole/tests/compile/recover/commands-semicolon.sieve
new file mode 100644
index 0000000..effb389
--- /dev/null
+++ b/pigeonhole/tests/compile/recover/commands-semicolon.sieve
@@ -0,0 +1,16 @@
+
+keep;
+
+discard;
+
+# Missing semicolon
+keep
+
+redirect "frop@nl.example.com";
+
+discard;
+
+# Missing semicolon
+keep
+
+redirect "frml@nl.example.com";
diff --git a/pigeonhole/tests/compile/recover/tests-endcomma.sieve b/pigeonhole/tests/compile/recover/tests-endcomma.sieve
new file mode 100644
index 0000000..54c93ec
--- /dev/null
+++ b/pigeonhole/tests/compile/recover/tests-endcomma.sieve
@@ -0,0 +1,17 @@
+if true {
+ if true {
+ # Spurious comma
+ if anyof(true,true,true,) {
+ }
+ }
+}
+
+if true {
+ if anyof(true,true) {
+ # Spurious comma
+ if anyof(true,true,true,) {
+ if anyof(true,true,true) {
+ }
+ }
+ }
+}
diff --git a/pigeonhole/tests/compile/redirect.sieve b/pigeonhole/tests/compile/redirect.sieve
new file mode 100644
index 0000000..fb9f23d
--- /dev/null
+++ b/pigeonhole/tests/compile/redirect.sieve
@@ -0,0 +1,23 @@
+# Test various white space occurrences
+redirect "stephan@example.org";
+redirect " stephan@example.org";
+redirect "stephan @example.org";
+redirect "stephan@ example.org";
+redirect "stephan@example.org ";
+redirect " stephan @ example.org ";
+redirect "Stephan Bosch<stephan@example.org>";
+redirect " Stephan Bosch<stephan@example.org>";
+redirect "Stephan Bosch <stephan@example.org>";
+redirect "Stephan Bosch< stephan@example.org>";
+redirect "Stephan Bosch<stephan @example.org>";
+redirect "Stephan Bosch<stephan@ example.org>";
+redirect "Stephan Bosch<stephan@example.org >";
+redirect "Stephan Bosch<stephan@example.org> ";
+redirect " Stephan Bosch < stephan @ example.org > ";
+
+# Test address syntax
+redirect "\"Stephan Bosch\"@example.org";
+redirect "Stephan.Bosch@eXamPle.oRg";
+redirect "Stephan.Bosch@example.org";
+redirect "Stephan Bosch <stephan@example.org>";
+
diff --git a/pigeonhole/tests/compile/trivial.sieve b/pigeonhole/tests/compile/trivial.sieve
new file mode 100644
index 0000000..a3dcbc1
--- /dev/null
+++ b/pigeonhole/tests/compile/trivial.sieve
@@ -0,0 +1,17 @@
+# Commands must be case-insensitive
+keep;
+Keep;
+KEEP;
+discard;
+DisCaRD;
+
+# Tags must be case-insensitive
+if size :UNDER 34 {
+}
+
+if header :Is "from" "tukker@example.com" {
+}
+
+# Numbers must be case-insensitive
+if anyof( size :UNDER 34m, size :oVeR 50M ) {
+}
diff --git a/pigeonhole/tests/compile/warnings.svtest b/pigeonhole/tests/compile/warnings.svtest
new file mode 100644
index 0000000..8261551
--- /dev/null
+++ b/pigeonhole/tests/compile/warnings.svtest
@@ -0,0 +1,8 @@
+require "vnd.dovecot.testsuite";
+
+test "EOF Warnings" {
+ if not test_script_compile "warnings/eof.sieve" {
+ test_fail "compile should have succeeded.";
+ }
+}
+
diff --git a/pigeonhole/tests/compile/warnings/eof.sieve b/pigeonhole/tests/compile/warnings/eof.sieve
new file mode 100644
index 0000000..cf906dc
--- /dev/null
+++ b/pigeonhole/tests/compile/warnings/eof.sieve
@@ -0,0 +1,2 @@
+keep;
+# Final comment without newline
diff --git a/pigeonhole/tests/compile/warnings/invalid-headers.sieve b/pigeonhole/tests/compile/warnings/invalid-headers.sieve
new file mode 100644
index 0000000..a6b12a8
--- /dev/null
+++ b/pigeonhole/tests/compile/warnings/invalid-headers.sieve
@@ -0,0 +1,14 @@
+# Header test
+if header "from:" "frop@example.org" {
+ stop;
+}
+
+# Address test
+if address "from:" "frop@example.org" {
+ stop;
+}
+
+# Exists test
+if exists "from:" {
+ stop;
+}
diff --git a/pigeonhole/tests/control-if.svtest b/pigeonhole/tests/control-if.svtest
new file mode 100644
index 0000000..e11de64
--- /dev/null
+++ b/pigeonhole/tests/control-if.svtest
@@ -0,0 +1,292 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * ## RFC 5228, Section 3.1. Control if (page 21) ##
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: test@dovecot.example.net
+Cc: friep@example.com
+Subject: Test
+
+Test!
+.
+;
+
+/*
+ * Basic functionality
+ */
+
+/* "The semantics are similar to those of any of the many other
+ * programming languages these control structures appear in. When the
+ * interpreter sees an "if", it evaluates the test associated with it.
+ * If the test is true, it executes the block associated with it.
+ *
+ * If the test of the "if" is false, it evaluates the test of the first
+ * "elsif" (if any). If the test of "elsif" is true, it runs the
+ * elsif's block. An elsif may be followed by an elsif, in which case,
+ * the interpreter repeats this process until it runs out of elsifs.
+ *
+ * When the interpreter runs out of elsifs, there may be an "else" case.
+ * If there is, and none of the if or elsif tests were true, the
+ * interpreter runs the else's block.
+ *
+ * This provides a way of performing exactly one of the blocks in the
+ * chain.
+ * "
+ */
+
+/*
+ * TEST: Basic functionality: if true/false
+ */
+
+test "Basic functionality: if true/false" {
+ /* Static */
+ if true {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for static true";
+ }
+
+ if false {
+ test_fail "executed wrong alternative for static false";
+ } else {
+ /* Correct */
+ }
+
+ /* Dynamic */
+ if exists "to" {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for dynamic true";
+ }
+
+ if exists "flierp" {
+ test_fail "executed wrong alternative for dynamic false";
+ } else {
+ /* Correct */
+ }
+}
+
+/*
+ * TEST: Basic functionality: if not true/false
+ */
+
+test "Basic functionality: if not true/false" {
+ /* Static */
+ if not true {
+ test_fail "executed wrong alternative for static not true";
+ } else {
+ /* Correct */
+ }
+
+ if not false {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for static not false";
+ }
+
+ /* Dynamic */
+ if not exists "to" {
+ test_fail "executed wrong alternative for dynamic not true";
+ } else {
+ /* Correct */
+ }
+
+ if not exists "flierp" {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for dynamic not false";
+ }
+}
+
+/*
+ * TEST: Basic functionality: elseif true/false
+ */
+
+test "Basic functionality: elseif true/false" {
+ /* Static */
+ if true {
+ /* Correct */
+ } elsif true {
+ test_fail "executed wrong alternative for static true-true (elsif)";
+ } else {
+ test_fail "executed wrong alternative for static true-true (else)";
+ }
+
+ if true {
+ /* Correct */
+ } elsif false {
+ test_fail "executed wrong alternative for static true-false (elsif)";
+ } else {
+ test_fail "executed wrong alternative for static true-false (else)";
+ }
+
+ if false {
+ test_fail "executed wrong alternative for static false-true (if)";
+ } elsif true {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for static false-false (else)";
+ }
+
+ if false {
+ test_fail "executed wrong alternative for static false-false (if)";
+ } elsif false {
+ test_fail "executed wrong alternative for static false-false (elsif)";
+ } else {
+ /* Correct */
+ }
+
+ /* Dynamic */
+ if address :is "from" "stephan@example.org" {
+ /* Correct */
+ } elsif address :contains "from" "stephan" {
+ test_fail "executed wrong alternative for dynamic true-true (elsif)";
+ } else {
+ test_fail "executed wrong alternative for dynamic true-true (else)";
+ }
+
+ if address :is "from" "stephan@example.org" {
+ /* Correct */
+ } elsif address :is "from" "frop@example.com" {
+ test_fail "executed wrong alternative for dynamic true-false (elsif)";
+ } else {
+ test_fail "executed wrong alternative for dynamic true-false (else)";
+ }
+
+ if address :is "from" "tss@example.net" {
+ test_fail "executed wrong alternative for dynamic false-true (if)";
+ } elsif address :is "from" "stephan@example.org" {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for dynamic false-true(else)";
+ }
+
+ if address :is "from" "tss@example.net" {
+ test_fail "executed wrong alternative for dynamic false-false (if)";
+ } elsif address :is "to" "stephan@example.org" {
+ test_fail "executed wrong alternative for dynamic false-false (elsif)";
+ } else {
+ /* Correct */
+ }
+
+ /* Static/Dynamic */
+
+ if true {
+ /* Correct */
+ } elsif address :contains "from" "stephan" {
+ test_fail "executed wrong alternative for first-static true-true (elsif)";
+ } else {
+ test_fail "executed wrong alternative for first-static true-true (else)";
+ }
+
+ if address :is "from" "stephan@example.org" {
+ /* Correct */
+ } elsif true {
+ test_fail "executed wrong alternative for second-static true-true (elsif)";
+ } else {
+ test_fail "executed wrong alternative for second-static true-true (else)";
+ }
+
+ if true {
+ /* Correct */
+ } elsif address :is "from" "frop@example.com" {
+ test_fail "executed wrong alternative for first-static true-false (elsif)";
+ } else {
+ test_fail "executed wrong alternative for first-static true-false (else)";
+ }
+
+ if address :is "from" "stephan@example.org" {
+ /* Correct */
+ } elsif false {
+ test_fail "executed wrong alternative for second-static true-false (elsif)";
+ } else {
+ test_fail "executed wrong alternative for second-static true-false (else)";
+ }
+
+ if false {
+ test_fail "executed wrong alternative for first-static false-true (if)";
+ } elsif address :is "from" "stephan@example.org" {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for first-static false-true(else)";
+ }
+
+ if address :is "from" "tss@example.net" {
+ test_fail "executed wrong alternative for second-static false-true (if)";
+ } elsif true {
+ /* Correct */
+ } else {
+ test_fail "executed wrong alternative for second-static false-true(else)";
+ }
+
+ if false {
+ test_fail "executed wrong alternative for first-static false-false (if)";
+ } elsif address :is "to" "stephan@example.org" {
+ test_fail "executed wrong alternative for first-static false-false (elsif)";
+ } else {
+ /* Correct */
+ }
+
+ if address :is "from" "tss@example.net" {
+ test_fail "executed wrong alternative for second-static false-false (if)";
+ } elsif false {
+ test_fail "executed wrong alternative for second-static false-false (elsif)";
+ } else {
+ /* Correct */
+ }
+}
+
+/*
+ * TEST: Basic functionality: nesting
+ */
+
+test "Basic functionality: nesting" {
+ /* Static */
+ if true {
+ if true {
+ if false {
+ test_fail "chose wrong static outcome: true->true->false";
+ } else {
+ /* Correct */
+ }
+ } else {
+ test_fail "chose wrong static outcome: true->false";
+ }
+ } elsif true {
+ if false {
+ test_fail "chose wrong static outcome: false->true->false";
+ } elsif true {
+ test_fail "chose wrong static outcome: false->true->true";
+ }
+ } else {
+ test_fail "chose wrong static outcome: false->false";
+ }
+
+ /* Dynamic */
+
+ if exists "to" {
+ if exists "from" {
+ if exists "friep" {
+ test_fail "chose wrong dynamic outcome: true->true->false";
+ } else {
+ /* Correct */
+ }
+ } else {
+ test_fail "chose wrong dynamic outcome: true->false";
+ }
+ } elsif exists "cc" {
+ if exists "frop" {
+ test_fail "chose wrong dynamic outcome: false->true->false";
+ } elsif exists "from" {
+ test_fail "chose wrong dynamic outcome: false->true->true";
+ }
+ } else {
+ test_fail "chose wrong dynamic outcome: false->false";
+ }
+}
+
+
+
diff --git a/pigeonhole/tests/control-stop.svtest b/pigeonhole/tests/control-stop.svtest
new file mode 100644
index 0000000..b49199d
--- /dev/null
+++ b/pigeonhole/tests/control-stop.svtest
@@ -0,0 +1,29 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * ## RFC 5228, Section 3.3. Control stop (page 22) ##
+ */
+
+/*
+ * TEST: End processing
+ */
+
+/* "The "stop" action ends all processing.
+ * "
+ */
+
+test "End processing" {
+ stop;
+
+ test_fail "continued after stop";
+}
+
+/*
+ * TEST: Implicit keep
+ */
+
+/* "If the implicit keep has not been cancelled, then it is taken.
+ * "
+ */
+
+/* FIXME */
diff --git a/pigeonhole/tests/deprecated/imapflags/errors.svtest b/pigeonhole/tests/deprecated/imapflags/errors.svtest
new file mode 100644
index 0000000..a9d9cde
--- /dev/null
+++ b/pigeonhole/tests/deprecated/imapflags/errors.svtest
@@ -0,0 +1,24 @@
+require "vnd.dovecot.testsuite";
+
+require "comparator-i;ascii-numeric";
+require "relational";
+
+test "Deprecated imapflags extension used with imap4flags" {
+ 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 "Deprecated imapflags extension used with imap4flags (ihave)" {
+ if test_script_compile "errors/conflict-ihave.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";
+ }
+}
diff --git a/pigeonhole/tests/deprecated/imapflags/errors/conflict-ihave.sieve b/pigeonhole/tests/deprecated/imapflags/errors/conflict-ihave.sieve
new file mode 100644
index 0000000..e924923
--- /dev/null
+++ b/pigeonhole/tests/deprecated/imapflags/errors/conflict-ihave.sieve
@@ -0,0 +1,6 @@
+require "imap4flags";
+require "ihave";
+
+if ihave "imapflags" {
+ addflags "Frop";
+}
diff --git a/pigeonhole/tests/deprecated/imapflags/errors/conflict.sieve b/pigeonhole/tests/deprecated/imapflags/errors/conflict.sieve
new file mode 100644
index 0000000..1b18a42
--- /dev/null
+++ b/pigeonhole/tests/deprecated/imapflags/errors/conflict.sieve
@@ -0,0 +1,4 @@
+require "imapflags";
+require "imap4flags";
+
+addflag "\\flagged";
diff --git a/pigeonhole/tests/deprecated/imapflags/execute.svtest b/pigeonhole/tests/deprecated/imapflags/execute.svtest
new file mode 100644
index 0000000..ea6657b
--- /dev/null
+++ b/pigeonhole/tests/deprecated/imapflags/execute.svtest
@@ -0,0 +1,92 @@
+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 "Mark / Unmark" {
+ if not test_script_compile "execute/mark.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "failed to execute first result";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Marked" 0;
+
+ if not hasflag "\\flagged" {
+ test_fail "message not marked";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Unmarked" 0;
+
+ if hasflag "\\flagged" {
+ test_fail "message not unmarked";
+ }
+}
+
+test_result_reset;
+test "Setflag / Addflag / Removeflag" {
+ if not test_script_compile "execute/flags.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "script execute failed";
+ }
+
+ if not test_result_execute {
+ test_fail "failed to execute first result";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Set" 0;
+
+ if not hasflag "\\draft" {
+ test_fail "flag not set";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Add" 0;
+
+ if not hasflag "\\draft" {
+ test_fail "flag not retained";
+ }
+
+ if not hasflag "\\flagged" {
+ test_fail "flag not added";
+ }
+
+ test_result_reset;
+
+ test_message :folder "Remove" 0;
+
+ if not hasflag "\\flagged" {
+ test_fail "flag not retained";
+ }
+
+ if hasflag "\\draft" {
+ test_fail "flag not removed";
+ }
+}
diff --git a/pigeonhole/tests/deprecated/imapflags/execute/flags.sieve b/pigeonhole/tests/deprecated/imapflags/execute/flags.sieve
new file mode 100644
index 0000000..ba68b44
--- /dev/null
+++ b/pigeonhole/tests/deprecated/imapflags/execute/flags.sieve
@@ -0,0 +1,12 @@
+require "imapflags";
+require "fileinto";
+require "mailbox";
+
+setflag "\\draft";
+fileinto :create "Set";
+
+addflag "\\flagged";
+fileinto :create "Add";
+
+removeflag "\\draft";
+fileinto :create "Remove";
diff --git a/pigeonhole/tests/deprecated/imapflags/execute/mark.sieve b/pigeonhole/tests/deprecated/imapflags/execute/mark.sieve
new file mode 100644
index 0000000..3216ca4
--- /dev/null
+++ b/pigeonhole/tests/deprecated/imapflags/execute/mark.sieve
@@ -0,0 +1,11 @@
+require "imapflags";
+require "fileinto";
+require "mailbox";
+
+mark;
+
+fileinto :create "Marked";
+
+unmark;
+
+fileinto :create "Unmarked";
diff --git a/pigeonhole/tests/deprecated/notify/basic.svtest b/pigeonhole/tests/deprecated/notify/basic.svtest
new file mode 100644
index 0000000..974f8ca
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/basic.svtest
@@ -0,0 +1,59 @@
+require "vnd.dovecot.testsuite";
+require "notify";
+require "body";
+
+test "Execute" {
+ /* Test to catch runtime segfaults */
+ notify
+ :message "This is probably very important"
+ :low
+ :method "mailto"
+ :options ["stephan@example.com", "stephan@example.org"];
+
+ if not test_result_execute {
+ test_fail "Execute failed";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+To: user@example.com
+From: stephan@example.org
+Subject: Mail
+
+Test!
+.
+;
+
+test "Substitutions" {
+ notify
+ :message "$from$: $subject$"
+ :options "stephan@example.com";
+ if not test_result_execute {
+ test_fail "Execute failed";
+ }
+ test_message :smtp 0;
+ if not body :contains "stephan@example.org: Mail" {
+ test_fail "Substitution failed";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+To: user@example.com
+
+Test!
+.
+;
+
+test "Empty substitutions" {
+ notify
+ :message "$from$: $subject$"
+ :options "stephan@example.com";
+ if not test_result_execute {
+ test_fail "Execute failed";
+ }
+}
+
diff --git a/pigeonhole/tests/deprecated/notify/denotify.svtest b/pigeonhole/tests/deprecated/notify/denotify.svtest
new file mode 100644
index 0000000..9f752e1
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/denotify.svtest
@@ -0,0 +1,279 @@
+require "vnd.dovecot.testsuite";
+require "notify";
+require "envelope";
+
+/*
+ * Denotify all
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Denotify All" {
+ notify :options "timo@example.com";
+ notify :options "stephan@dovecot.example.net";
+ notify :options "postmaster@frop.example.org";
+ denotify;
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "no notifications should have been sent";
+ }
+}
+
+/*
+ * Denotify First
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Denotify ID First" {
+ /* #1 */
+ notify :options "timo@example.com" :id "aap";
+
+ /* #2 */
+ notify :options "stephan@dovecot.example.net" :id "noot";
+
+ /* #3 */
+ notify :options "postmaster@frop.example.org" :id "mies";
+
+ denotify :is "aap";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "two notifications should have been sent (#2 missing)";
+ }
+
+ if not envelope "to" "stephan@dovecot.example.net" {
+ test_fail "message #2 unexpectedly missing from output";
+ }
+
+ if not test_message :smtp 1 {
+ test_fail "two notifications should have been sent (#3 missing)";
+ }
+
+ if not envelope "to" "postmaster@frop.example.org" {
+ test_fail "message #3 unexpectedly missing from output";
+ }
+
+ if test_message :smtp 2 {
+ test_fail "too many notifications sent";
+ }
+}
+
+/*
+ * Denotify Middle
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Denotify ID Middle" {
+ /* #1 */
+ notify :options "timo@example.com" :id "aap";
+
+ /* #2 */
+ notify :options "stephan@dovecot.example.net" :id "noot";
+
+ /* #3 */
+ notify :options "postmaster@frop.example.org" :id "mies";
+
+ denotify :is "noot";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "two notifications should have been sent (#1 missing)";
+ }
+
+ if not envelope "to" "timo@example.com" {
+ test_fail "message #1 unexpectedly missing from output";
+ }
+
+ if not test_message :smtp 1 {
+ test_fail "two notifications should have been sent (#3 missing)";
+ }
+
+ if not envelope "to" "postmaster@frop.example.org" {
+ test_fail "message #3 unexpectedly missing from output";
+ }
+
+ if test_message :smtp 2 {
+ test_fail "too many notifications sent";
+ }
+}
+
+/*
+ * Denotify Last
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Denotify ID Last" {
+ /* #1 */
+ notify :options "timo@example.com" :id "aap";
+
+ /* #2 */
+ notify :options "stephan@dovecot.example.net" :id "noot";
+
+ /* #3 */
+ notify :options "postmaster@frop.example.org" :id "mies";
+
+ denotify :is "mies";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "two notifications should have been sent (#1 missing)";
+ }
+
+ if not envelope "to" "timo@example.com" {
+ test_fail "message #1 unexpectedly missing from output";
+ }
+
+ if not test_message :smtp 1 {
+ test_fail "two notifications should have been sent (#2 missing)";
+ }
+
+ if not envelope "to" "stephan@dovecot.example.net" {
+ test_fail "message #2 unexpectedly missing from output";
+ }
+
+ if test_message :smtp 2 {
+ test_fail "too many notifications sent";
+ }
+}
+
+
+/*
+ * Denotify Matching
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Denotify Matching" {
+ /* #1 */
+ notify :options "timo@example.com" :id "frop";
+
+ /* #2 */
+ notify :options "stephan@dovecot.example.net" :id "noot";
+
+ /* #3 */
+ notify :options "postmaster@frop.example.org" :id "friep";
+
+ denotify :matches "fr*";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "one notification should have been sent";
+ }
+
+ if not envelope "to" "stephan@dovecot.example.net" {
+ test_fail "message #2 unexpectedly missing from output";
+ }
+
+ if test_message :smtp 1 {
+ test_fail "too many notifications sent";
+ }
+}
+
+
+/*
+ * Denotify Matching
+ */
+
+test_result_reset;
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Frop!
+
+Klutsefluts.
+.
+;
+
+test "Denotify Matching Importance" {
+ /* #1 */
+ notify :options "timo@example.com" :id "frop" :low;
+
+ /* #2 */
+ notify :options "stephan@dovecot.example.net" :id "frml" :high;
+
+ /* #3 */
+ notify :options "postmaster@frop.example.org" :id "friep" :low;
+
+ denotify :matches "fr*" :low;
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if not test_message :smtp 0 {
+ test_fail "one notification should have been sent";
+ }
+
+ if not envelope "to" "stephan@dovecot.example.net" {
+ test_fail "message #2 unexpectedly missing from output";
+ }
+
+ if test_message :smtp 1 {
+ test_fail "too many notifications sent";
+ }
+}
+
+
diff --git a/pigeonhole/tests/deprecated/notify/errors.svtest b/pigeonhole/tests/deprecated/notify/errors.svtest
new file mode 100644
index 0000000..549cb6b
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/errors.svtest
@@ -0,0 +1,33 @@
+require "vnd.dovecot.testsuite";
+require "comparator-i;ascii-numeric";
+require "relational";
+
+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" "3" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+test "Deprecated notify extension used with enotify" {
+ if test_script_compile "errors/conflict.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 "Deprecated notify extension used with enotify (ihave)" {
+ if test_script_compile "errors/conflict-ihave.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";
+ }
+}
diff --git a/pigeonhole/tests/deprecated/notify/errors/conflict-ihave.sieve b/pigeonhole/tests/deprecated/notify/errors/conflict-ihave.sieve
new file mode 100644
index 0000000..9686f03
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/errors/conflict-ihave.sieve
@@ -0,0 +1,8 @@
+require "enotify";
+require "ihave";
+
+# 1: Conflict
+if ihave "notify" {
+ # 2: Syntax wrong for enotify (and not skipped in compile)
+ notify :options "frop@frop.example.org";
+}
diff --git a/pigeonhole/tests/deprecated/notify/errors/conflict.sieve b/pigeonhole/tests/deprecated/notify/errors/conflict.sieve
new file mode 100644
index 0000000..46a6283
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/errors/conflict.sieve
@@ -0,0 +1,4 @@
+require "enotify";
+require "notify";
+
+notify :options "frop@frop.example.org";
diff --git a/pigeonhole/tests/deprecated/notify/errors/options.sieve b/pigeonhole/tests/deprecated/notify/errors/options.sieve
new file mode 100644
index 0000000..c86fea0
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/errors/options.sieve
@@ -0,0 +1,11 @@
+require "notify";
+
+# 1: empty option
+notify :options "";
+
+# 2: invalid address syntax
+notify :options "frop#frop.example.org";
+
+# Valid
+notify :options "frop@frop.example.org";
+
diff --git a/pigeonhole/tests/deprecated/notify/execute.svtest b/pigeonhole/tests/deprecated/notify/execute.svtest
new file mode 100644
index 0000000..90fde47
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/execute.svtest
@@ -0,0 +1,25 @@
+require "vnd.dovecot.testsuite";
+require "relational";
+
+
+/*
+ * Execution testing (currently just meant to trigger any segfaults)
+ */
+
+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/deprecated/notify/execute/duplicates.sieve b/pigeonhole/tests/deprecated/notify/execute/duplicates.sieve
new file mode 100644
index 0000000..ef3fa5f
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/execute/duplicates.sieve
@@ -0,0 +1,4 @@
+require "notify";
+
+notify :message "Incoming stupidity." :options ["stephan@example.org", "stephan@friep.example.com", "idiot@example.org"];
+notify :message "There it is." :options ["tss@example.net", "stephan@example.org", "idiot@example.org", "nico@frop.example.org", "stephan@friep.example.com"];
diff --git a/pigeonhole/tests/deprecated/notify/mailto.svtest b/pigeonhole/tests/deprecated/notify/mailto.svtest
new file mode 100644
index 0000000..1724339
--- /dev/null
+++ b/pigeonhole/tests/deprecated/notify/mailto.svtest
@@ -0,0 +1,317 @@
+require "vnd.dovecot.testsuite";
+
+require "notify";
+require "body";
+require "relational";
+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 :method "mailto" :options "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-generated*" {
+ test_fail "auto-submitted header set inappropriately";
+ }
+
+ if not exists "X-Sieve" {
+ test_fail "x-sieve header missing from outgoing message";
+ }
+}
+
+/*
+ * 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 :options ["timo@example.com","stephan@dovecot.example.net","postmaster@frop.example.org"];
+
+ 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";
+ }
+
+ test_message :smtp 1;
+
+ if not address :is "to" "stephan@dovecot.example.net" {
+ test_fail "second To address missing";
+ }
+
+ if not header :matches "Auto-Submitted" "auto-generated*" {
+ test_fail "auto-submitted header not found for second message";
+ }
+
+ test_message :smtp 2;
+
+ if not address :is "to" "postmaster@frop.example.org" {
+ test_fail "third To address missing";
+ }
+
+ if not header :matches "Auto-Submitted" "auto-generated*" {
+ test_fail "auto-submitted header not found for third message";
+ }
+
+ if not address :count "eq" :comparator "i;ascii-numeric" "to" "3" {
+ test_fail "wrong number of recipients in To header";
+ }
+
+ if not address :count "eq" :comparator "i;ascii-numeric" "cc" "0" {
+ test_fail "too many recipients in Cc header";
+ }
+}
+
+/*
+ * 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 :options ["timo@example.com", "stephan@dovecot.example.net", "stephan@dovecot.example.net"];
+ notify :options ["timo@example.com", "stephan@example.org"];
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 2;
+
+ if address "To" "stephan@dovecot.example.net" {
+ test_fail "duplicate recipient not removed from first message";
+ }
+
+ if address "To" "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 :options "stephan@example.org";
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "notified of auto-submitted message";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+To: nico@frop.example.org
+From: stephan@example.org
+Subject: Test
+
+Test. Test
+Frop!
+.
+;
+
+test "Body; Singular Message" {
+ notify :low :id "frop" :options "stephan@example.org"
+ :message text:
+Received interesting message:
+
+$text$
+
+You have been notified.
+.
+;
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not body :raw :contains "Received interesting message" {
+ test_fail "notification has no heading";
+ }
+
+ if not body :raw :contains "You have been notified" {
+ test_fail "notification has no footer";
+ }
+
+ if not allof(
+ body :raw :contains "Test. Test",
+ body :raw :contains "Frop" ) {
+ test_fail "notification has no original message";
+ }
+}
+
+test_result_reset;
+
+test_set "message" text:
+To: nico@frop.example.org
+From: stephan@example.org
+Subject: Test
+
+Test. Test
+Frop!
+.
+;
+
+test "Body; $text[maxsize]$" {
+ notify :low :id "frop" :options "sirius@example.org"
+ :message text:
+Received interesting message:
+
+$text[5]$
+
+You have been notified.
+.
+;
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not body :raw :contains "Received interesting message" {
+ test_fail "notification has no heading";
+ }
+
+ if not body :raw :contains "You have been notified" {
+ test_fail "notification has no footer";
+ }
+
+ if anyof(
+ body :raw :contains "Test. Test",
+ body :raw :contains "Frop" ) {
+ test_fail "original message in notification is not truncated";
+ }
+
+ if not body :raw :contains "Test." {
+ test_fail "notification does not contain the required message";
+ }
+}
+
+test_result_reset;
+
+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: application/sieve; charset="us-ascii"
+
+keep;
+
+--inner
+Content-Type: text/plain; charset="us-ascii"
+
+Friep!
+
+--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.
+.
+;
+
+test "Body; Multipart Message" {
+ notify :low :id "frop" :options "stephan@example.org"
+ :message text:
+Received interesting message:
+
+$text$
+
+You have been notified.
+.
+;
+
+ if not test_result_execute {
+ test_fail "failed to execute notify";
+ }
+
+ test_message :smtp 0;
+
+ if not body :raw :contains "Friep!" {
+ test_fail "notification has incorrect content";
+ }
+}
+
+
+
diff --git a/pigeonhole/tests/execute/actions.svtest b/pigeonhole/tests/execute/actions.svtest
new file mode 100644
index 0000000..3f517fa
--- /dev/null
+++ b/pigeonhole/tests/execute/actions.svtest
@@ -0,0 +1,80 @@
+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_mailbox_create "INBOX.VB";
+test_mailbox_create "INBOX.backup";
+
+test "Fileinto" {
+ if not test_script_compile "actions/fileinto.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" "3" {
+ test_fail "wrong number of actions in result";
+ }
+
+ if not test_result_action :index 1 "store" {
+ test_fail "first action is not 'store'";
+ }
+
+ if not test_result_action :index 2 "store" {
+ test_fail "second action is not 'store'";
+ }
+
+ if not test_result_action :index 3 "keep" {
+ test_fail "third action is not 'keep'";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+test "Redirect" {
+ if not test_script_compile "actions/redirect.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execute failed";
+ }
+
+ if not test_result_action :count "eq" :comparator "i;ascii-numeric" "4" {
+ test_fail "wrong number of actions in result";
+ }
+
+ if not test_result_action :index 1 "redirect" {
+ test_fail "first action is not 'redirect'";
+ }
+
+ if not test_result_action :index 2 "keep" {
+ test_fail "second action is not 'keep'";
+ }
+
+ if not test_result_action :index 3 "redirect" {
+ test_fail "third action is not 'redirect'";
+ }
+
+ if not test_result_action :index 4 "redirect" {
+ test_fail "fourth action is not 'redirect'";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
diff --git a/pigeonhole/tests/execute/actions/fileinto.sieve b/pigeonhole/tests/execute/actions/fileinto.sieve
new file mode 100644
index 0000000..e9c133b
--- /dev/null
+++ b/pigeonhole/tests/execute/actions/fileinto.sieve
@@ -0,0 +1,17 @@
+require "fileinto";
+
+/* Three store actions */
+
+if address :contains "to" "frop.example" {
+ /* #1 */
+ fileinto "INBOX.VB";
+}
+
+/* #2 */
+fileinto "INBOX.backup";
+
+/* #3 */
+keep;
+
+/* Duplicate of keep */
+fileinto "INBOX";
diff --git a/pigeonhole/tests/execute/actions/redirect.sieve b/pigeonhole/tests/execute/actions/redirect.sieve
new file mode 100644
index 0000000..7adc23e
--- /dev/null
+++ b/pigeonhole/tests/execute/actions/redirect.sieve
@@ -0,0 +1,17 @@
+if address :contains "to" "frop.example" {
+ /* #1 */
+ redirect "stephan@example.com";
+
+ /* #2 */
+ keep;
+}
+
+/* #3 */
+redirect "stephan@example.org";
+
+/* #4 */
+redirect "nico@example.nl";
+
+/* Duplicates */
+redirect "Stephan Bosch <stephan@example.com>";
+keep;
diff --git a/pigeonhole/tests/execute/address-normalize.svtest b/pigeonhole/tests/execute/address-normalize.svtest
new file mode 100644
index 0000000..e826bde
--- /dev/null
+++ b/pigeonhole/tests/execute/address-normalize.svtest
@@ -0,0 +1,46 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+
+test_set "message" text:
+From: tss@example.net
+To: stephan@example.org
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_set "envelope.from" "timo@example.net";
+test_set "envelope.to" "\"sirius\"@example.org";
+
+/*
+ * Mail address normalization - redirect
+ */
+
+test "Mail address normalization - redirect" {
+ redirect "\"S[r]us\"@example.net";
+ redirect "\"Sirius\"@example.net";
+ redirect "\"Stephan Bosch\" <\"S.Bosch\"@example.net>";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope :is "to" "\"S[r]us\"@example.net" {
+ test_fail "envelope recipient incorrect";
+ }
+
+ test_message :smtp 1;
+
+ if not envelope :is "to" "Sirius@example.net" {
+ test_fail "envelope recipient incorrect";
+ }
+
+ test_message :smtp 2;
+
+ if not envelope :is "to" "S.Bosch@example.net" {
+ test_fail "envelope recipient incorrect";
+ }
+}
diff --git a/pigeonhole/tests/execute/errors-cpu-limit.svtest b/pigeonhole/tests/execute/errors-cpu-limit.svtest
new file mode 100644
index 0000000..4a045bc
--- /dev/null
+++ b/pigeonhole/tests/execute/errors-cpu-limit.svtest
@@ -0,0 +1,363 @@
+require "vnd.dovecot.testsuite";
+
+test_config_set "sieve_max_cpu_time" "2";
+test_config_reload;
+
+test_set "message" text:
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary=
+
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+--
+.
+;
+
+test "CPU limit" {
+ if not test_script_compile "errors/cpu-limit.sieve" {
+ test_fail "script compile failed";
+ }
+
+ if test_script_run {
+ test_fail "script execute should have failed";
+ }
+}
+
diff --git a/pigeonhole/tests/execute/errors.svtest b/pigeonhole/tests/execute/errors.svtest
new file mode 100644
index 0000000..45bc39c
--- /dev/null
+++ b/pigeonhole/tests/execute/errors.svtest
@@ -0,0 +1,152 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+require "fileinto";
+
+test "Action conflicts: reject <-> fileinto" {
+ if not test_script_compile "errors/conflict-reject-fileinto.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";
+ }
+}
+
+test "Action conflicts: reject <-> keep" {
+ if not test_script_compile "errors/conflict-reject-keep.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";
+ }
+}
+
+test "Action conflicts: reject <-> redirect" {
+ if not test_script_compile "errors/conflict-reject-redirect.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";
+ }
+}
+
+test "Action limit" {
+ if not test_script_compile "errors/actions-limit.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";
+ }
+
+ if not test_error :index 1 :contains "total number of actions exceeds policy limit"{
+ test_fail "unexpected error reported";
+ }
+}
+
+test "Redirect limit" {
+ if not test_script_compile "errors/redirect-limit.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";
+ }
+
+ if not test_error :index 1 :contains "number of redirect actions exceeds policy limit"{
+ test_fail "unexpected error reported";
+ }
+}
+
+test "Fileinto missing folder" {
+ if not test_script_compile "errors/fileinto.sieve" {
+ test_fail "compile failed";
+ }
+
+ test_mailbox_create "INBOX";
+
+ if not test_script_run {
+ test_fail "execution failed";
+ }
+
+ if test_result_execute {
+ test_fail "execution of result should have failed";
+ }
+
+ if test_error :count "gt" :comparator "i;ascii-numeric" "1" {
+ test_fail "too many runtime errors reported";
+ }
+
+ if not allof (
+ test_error :index 1 :contains "failed to store into mailbox",
+ test_error :index 1 :contains "exist",
+ test_error :index 1 :contains "FROP") {
+ test_fail "unexpected error reported";
+ }
+}
+
+test "Fileinto invalid folder name" {
+ if not test_script_compile "errors/fileinto-invalid-name.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execution failed";
+ }
+
+ if test_result_execute {
+ test_fail "execution of result should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of runtime errors reported";
+ }
+
+ if not allof (
+ test_error :index 1 :contains "failed to store into mailbox",
+ test_error :index 1 :contains "name") {
+ test_fail "unexpected error reported";
+ }
+}
+
+test "Fileinto bad UTF-8 in folder name" {
+ if not test_script_compile "errors/fileinto-bad-utf8.sieve" {
+ test_fail "compile failed";
+ }
+
+ if test_script_run {
+ test_fail "execution should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of runtime errors reported";
+ }
+
+ if not test_error :index 1 :contains "invalid folder name" {
+ test_fail "unexpected error reported";
+ }
+}
diff --git a/pigeonhole/tests/execute/errors/action-duplicates.sieve b/pigeonhole/tests/execute/errors/action-duplicates.sieve
new file mode 100644
index 0000000..6d5370d
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/action-duplicates.sieve
@@ -0,0 +1,4 @@
+require "reject";
+
+reject "Message is not appreciated.";
+reject "No, really, it is not appreciated.";
diff --git a/pigeonhole/tests/execute/errors/actions-limit.sieve b/pigeonhole/tests/execute/errors/actions-limit.sieve
new file mode 100644
index 0000000..3ae33a3
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/actions-limit.sieve
@@ -0,0 +1,35 @@
+require "fileinto";
+
+fileinto "box1";
+fileinto "box2";
+fileinto "box3";
+fileinto "box4";
+fileinto "box5";
+fileinto "box6";
+fileinto "box7";
+fileinto "box8";
+fileinto "box9";
+fileinto "box10";
+fileinto "box11";
+fileinto "box12";
+fileinto "box13";
+fileinto "box14";
+fileinto "box15";
+fileinto "box16";
+fileinto "box17";
+fileinto "box18";
+fileinto "box19";
+fileinto "box20";
+fileinto "box21";
+fileinto "box22";
+fileinto "box23";
+fileinto "box24";
+fileinto "box25";
+fileinto "box26";
+fileinto "box27";
+fileinto "box28";
+redirect "address1@example.com";
+redirect "address2@example.com";
+redirect "address3@example.com";
+redirect "address4@example.com";
+keep;
diff --git a/pigeonhole/tests/execute/errors/conflict-reject-fileinto.sieve b/pigeonhole/tests/execute/errors/conflict-reject-fileinto.sieve
new file mode 100644
index 0000000..85ef139
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/conflict-reject-fileinto.sieve
@@ -0,0 +1,5 @@
+require "reject";
+require "fileinto";
+
+reject "No nonsense in my mailbox.";
+fileinto "Spam";
diff --git a/pigeonhole/tests/execute/errors/conflict-reject-keep.sieve b/pigeonhole/tests/execute/errors/conflict-reject-keep.sieve
new file mode 100644
index 0000000..569a4ac
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/conflict-reject-keep.sieve
@@ -0,0 +1,4 @@
+require "reject";
+
+reject "I am not interested in your nonsense.";
+keep;
diff --git a/pigeonhole/tests/execute/errors/conflict-reject-redirect.sieve b/pigeonhole/tests/execute/errors/conflict-reject-redirect.sieve
new file mode 100644
index 0000000..d012269
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/conflict-reject-redirect.sieve
@@ -0,0 +1,4 @@
+require "reject";
+
+reject "I am not interested in your nonsense.";
+redirect "frop@example.com";
diff --git a/pigeonhole/tests/execute/errors/cpu-limit.sieve b/pigeonhole/tests/execute/errors/cpu-limit.sieve
new file mode 100644
index 0000000..8532a4b
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/cpu-limit.sieve
@@ -0,0 +1,145 @@
+require ["mime","foreverypart","fileinto", "variables", "regex"];
+
+# Here we create an inefficient regex with long compilation time
+set "my_exp" "^(((A)|(AB)|(ABC)|(ABCD)|(ABCDE)|(ABCDEF)|(ABCDEFG)|(ABCDEFGH)|(ABCDEFGHI)|(ABCDEFGHIJ)|(ABCDEFGHIJK)|(ABCDEFGHIJKL)|(ABCDEFGHIJKLM)|(ABCDEFGHIJKLMN)|(ABCDEFGHIJKLMNO)|(ABCDEFGHIJKLMNOP)|(ABCDEFGHIJKLMNOPQ)|(ABCDEFGHIJKLMNOPQR))?((B)|(BC)|(BCD)|(BCDE)|(BCDEF)|(BCDEFG)|(BCDEFGH)|(BCDEFGHI)|(BCDEFGHIJ)|(BCDEFGHIJK)|(BCDEFGHIJKL)|(BCDEFGHIJKLM)|(BCDEFGHIJKLMN)|(BCDEFGHIJKLMNO)|(BCDEFGHIJKLMNOP)|(BCDEFGHIJKLMNOPQ)|(BCDEFGHIJKLMNOPQR))?((C)|(CD)|(CDE)|(CDEF)|(CDEFG)|(CDEFGH)|(CDEFGHI)|(CDEFGHIJ)|(CDEFGHIJK)|(CDEFGHIJKL)|(CDEFGHIJKLM)|(CDEFGHIJKLMN)|(CDEFGHIJKLMNO)|(CDEFGHIJKLMNOP)|(CDEFGHIJKLMNOPQ)|(CDEFGHIJKLMNOPQR))?((D)|(DE)|(DEF)|(DEFG)|(DEFGH)|(DEFGHI)|(DEFGHIJ)|(DEFGHIJK)|(DEFGHIJKL)|(DEFGHIJKLM)|(DEFGHIJKLMN)|(DEFGHIJKLMNO)|(DEFGHIJKLMNOP)|(DEFGHIJKLMNOPQ)|(DEFGHIJKLMNOPQR))?((E)|(EF)|(EFG)|(EFGH)|(EFGHI)|(EFGHIJ)|(EFGHIJK)|(EFGHIJKL)|(EFGHIJKLM)|(EFGHIJKLMN)|(EFGHIJKLMNO)|(EFGHIJKLMNOP)|(EFGHIJKLMNOPQ)|(EFGHIJKLMNOPQR))?((F)|(FG)|(FGH)|(FGHI)|(FGHIJ)|(FGHIJK)|(FGHIJKL)|(FGHIJKLM)|(FGHIJKLMN)|(FGHIJKLMNO)|(FGHIJKLMNOP)|(FGHIJKLMNOPQ)|(FGHIJKLMNOPQR))?((G)|(GH)|(GHI)|(GHIJ)|(GHIJK)|(GHIJKL)|(GHIJKLM)|(GHIJKLMN)|(GHIJKLMNO)|(GHIJKLMNOP)|(GHIJKLMNOPQ)|(GHIJKLMNOPQR))?((H)|(HI)|(HIJ)|(HIJK)|(HIJKL)|(HIJKLM)|(HIJKLMN)|(HIJKLMNO)|(HIJKLMNOP)|(HIJKLMNOPQ)|(HIJKLMNOPQR))?((I)|(IJ)|(IJK)|(IJKL)|(IJKLM)|(IJKLMN)|(IJKLMNO)|(IJKLMNOP)|(IJKLMNOPQ)|(IJKLMNOPQR))?((J)|(JK)|(JKL)|(JKLM)|(JKLMN)|(JKLMNO)|(JKLMNOP)|(JKLMNOPQ)|(JKLMNOPQR))?((K)|(KL)|(KLM)|(KLMN)|(KLMNO)|(KLMNOP)|(KLMNOPQ)|(KLMNOPQR))?((L)|(LM)|(LMN)|(LMNO)|(LMNOP)|(LMNOPQ)|(LMNOPQR))?((M)|(MN)|(MNO)|(MNOP)|(MNOPQ)|(MNOPQR))?((N)|(NO)|(NOP)|(NOPQ)|(NOPQR))?((O)|(OP)|(OPQ)|(OPQR))?((P)|(PQ)|(PQR))?((Q)|(QR))?((R))?((R)|(RQ)|(RQP)|(RQPO)|(RQPON)|(RQPONM)|(RQPONML)|(RQPONMLK)|(RQPONMLKJ)|(RQPONMLKJI)|(RQPONMLKJIH)|(RQPONMLKJIHG)|(RQPONMLKJIHGF)|(RQPONMLKJIHGFE)|(RQPONMLKJIHGFED)|(RQPONMLKJIHGFEDC)|(RQPONMLKJIHGFEDCB)|(RQPONMLKJIHGFEDCBA))?((Q)|(QP)|(QPO)|(QPON)|(QPONM)|(QPONML)|(QPONMLK)|(QPONMLKJ)|(QPONMLKJI)|(QPONMLKJIH)|(QPONMLKJIHG)|(QPONMLKJIHGF)|(QPONMLKJIHGFE)|(QPONMLKJIHGFED)|(QPONMLKJIHGFEDC)|(QPONMLKJIHGFEDCB)|(QPONMLKJIHGFEDCBA))?((P)|(PO)|(PON)|(PONM)|(PONML)|(PONMLK)|(PONMLKJ)|(PONMLKJI)|(PONMLKJIH)|(PONMLKJIHG)|(PONMLKJIHGF)|(PONMLKJIHGFE)|(PONMLKJIHGFED)|(PONMLKJIHGFEDC)|(PONMLKJIHGFEDCB)|(PONMLKJIHGFEDCBA))?((O)|(ON)|(ONM)|(ONML)|(ONMLK)|(ONMLKJ)|(ONMLKJI)|(ONMLKJIH)|(ONMLKJIHG)|(ONMLKJIHGF)|(ONMLKJIHGFE)|(ONMLKJIHGFED)|(ONMLKJIHGFEDC)|(ONMLKJIHGFEDCB)|(ONMLKJIHGFEDCBA))?((N)|(NM)|(NML)|(NMLK)|(NMLKJ)|(NMLKJI)|(NMLKJIH)|(NMLKJIHG)|(NMLKJIHGF)|(NMLKJIHGFE)|(NMLKJIHGFED)|(NMLKJIHGFEDC)|(NMLKJIHGFEDCB)|(NMLKJIHGFEDCBA))?((M)|(ML)|(MLK)|(MLKJ)|(MLKJI)|(MLKJIH)|(MLKJIHG)|(MLKJIHGF)|(MLKJIHGFE)|(MLKJIHGFED)|(MLKJIHGFEDC)|(MLKJIHGFEDCB)|(MLKJIHGFEDCBA))?((L)|(LK)|(LKJ)|(LKJI)|(LKJIH)|(LKJIHG)|(LKJIHGF)|(LKJIHGFE)|(LKJIHGFED)|(LKJIHGFEDC)|(LKJIHGFEDCB)|(LKJIHGFEDCBA))?((K)|(KJ)|(KJI)|(KJIH)|(KJIHG)|(KJIHGF)|(KJIHGFE)|(KJIHGFED)|(KJIHGFEDC)|(KJIHGFEDCB)|(KJIHGFEDCBA))?((J)|(JI)|(JIH)|(JIHG)|(JIHGF)|(JIHGFE)|(JIHGFED)|(JIHGFEDC)|(JIHGFEDCB)|(JIHGFEDCBA))?((I)|(IH)|(IHG)|(IHGF)|(IHGFE)|(IHGFED)|(IHGFEDC)|(IHGFEDCB)|(IHGFEDCBA))?((H)|(HG)|(HGF)|(HGFE)|(HGFED)|(HGFEDC)|(HGFEDCB)|(HGFEDCBA))?((G)|(GF)|(GFE)|(GFED)|(GFEDC)|(GFEDCB)|(GFEDCBA))?((F)|(FE)|(FED)|(FEDC)|(FEDCB)|(FEDCBA))?((E)|(ED)|(EDC)|(EDCB)|(EDCBA))?((D)|(DC)|(DCB)|(DCBA))?((C)|(CB)|(CBA))?((B)|(BA))?((A))?)+$";
+set "a" "ABCDEFGHIJKLMNOPQR";
+set "b" "RQPONMLKJIHGFEDCBA";
+set "c" "${a}${b}${a}${b}${a}${b}${a}${b}";
+set "e" "${c}${c}${c}${c}${c}${c}${c}${c}";
+set "f" "${e}${e}${e}${e}${e}${e}${e}${e}";
+
+# We create a string on which this regex will spend enough time (around 200 ms)
+set "final" "${f}${f}${f}${f}${f}${f}${f}${f}${f}${f}${f}${f}${f}${f}@";
+
+# We repeat the throttling process for every mime part
+foreverypart {
+ # We use several if statements to multiply the cpu time consumed by one match
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+ if string :regex "${final}" "${my_exp}" { discard; }
+ if string :regex "${final}" "${my_exp}" { keep; }
+}
diff --git a/pigeonhole/tests/execute/errors/fileinto-bad-utf8.sieve b/pigeonhole/tests/execute/errors/fileinto-bad-utf8.sieve
new file mode 100644
index 0000000..3e57c92
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/fileinto-bad-utf8.sieve
@@ -0,0 +1,7 @@
+require "fileinto";
+require "variables";
+require "encoded-character";
+
+set "mailbox" "${hex:ff}rop";
+fileinto "${mailbox}";
+
diff --git a/pigeonhole/tests/execute/errors/fileinto-invalid-name.sieve b/pigeonhole/tests/execute/errors/fileinto-invalid-name.sieve
new file mode 100644
index 0000000..871323e
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/fileinto-invalid-name.sieve
@@ -0,0 +1,5 @@
+require "fileinto";
+require "mailbox";
+
+fileinto :create "foo//somedomain/org";
+
diff --git a/pigeonhole/tests/execute/errors/fileinto.sieve b/pigeonhole/tests/execute/errors/fileinto.sieve
new file mode 100644
index 0000000..185674c
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/fileinto.sieve
@@ -0,0 +1,3 @@
+require "fileinto";
+
+fileinto "FROP";
diff --git a/pigeonhole/tests/execute/errors/redirect-limit.sieve b/pigeonhole/tests/execute/errors/redirect-limit.sieve
new file mode 100644
index 0000000..86cfda0
--- /dev/null
+++ b/pigeonhole/tests/execute/errors/redirect-limit.sieve
@@ -0,0 +1,5 @@
+redirect "address1@example.com";
+redirect "address2@example.com";
+redirect "address3@example.com";
+redirect "address4@example.com";
+redirect "address5@example.com";
diff --git a/pigeonhole/tests/execute/examples.svtest b/pigeonhole/tests/execute/examples.svtest
new file mode 100644
index 0000000..6143018
--- /dev/null
+++ b/pigeonhole/tests/execute/examples.svtest
@@ -0,0 +1,115 @@
+require "vnd.dovecot.testsuite";
+
+/* Compile and execute all example scripts to trigger
+ * any Segfaults. No message is set and no results are checked.
+ */
+
+test "Elvey example" {
+ if not test_script_compile "../../examples/elvey.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "elvey";
+ test_binary_load "elvey";
+
+ if not test_script_run { }
+}
+
+test "M. Johnson example" {
+ if not test_script_compile "../../examples/mjohnson.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "mjohnson";
+ test_binary_load "mjohnson";
+
+ if not test_script_run { }
+}
+
+test "RFC 3028 example" {
+ if not test_script_compile "../../examples/rfc3028.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "rfc3028";
+ test_binary_load "rfc3028";
+
+ if not test_script_run { }
+}
+
+test "Sieve examples" {
+ if not test_script_compile "../../examples/sieve_examples.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "sieve_examples";
+ test_binary_load "sieve_examples";
+
+ if not test_script_run { }
+}
+
+test "Vivil example" {
+ if not test_script_compile "../../examples/vivil.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "vivil";
+ test_binary_load "vivil";
+
+ if not test_script_run { }
+}
+
+test "Jerry example" {
+ if not test_script_compile "../../examples/jerry.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "jerry";
+ test_binary_load "jerry";
+
+ if not test_script_run { }
+}
+
+test "M. Klose example" {
+ if not test_script_compile "../../examples/mklose.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "mklose";
+ test_binary_load "mklose";
+
+ if not test_script_run { }
+}
+
+test "Sanjay example" {
+ if not test_script_compile "../../examples/sanjay.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "sanjay";
+ test_binary_load "sanjay";
+
+ if not test_script_run { }
+}
+
+test "Relational (RFC5231) example" {
+ if not test_script_compile "../../examples/relational.rfc5231.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "relational";
+ test_binary_load "relational";
+
+ if not test_script_run { }
+}
+
+test "Subaddress (RFC5233) example" {
+ if not test_script_compile "../../examples/subaddress.rfc5233.sieve" {
+ test_fail "could not compile";
+ }
+
+ test_binary_save "subaddress";
+ test_binary_load "subaddress";
+
+ if not test_script_run { }
+}
diff --git a/pigeonhole/tests/execute/mailstore.svtest b/pigeonhole/tests/execute/mailstore.svtest
new file mode 100644
index 0000000..d6cc220
--- /dev/null
+++ b/pigeonhole/tests/execute/mailstore.svtest
@@ -0,0 +1,84 @@
+require "vnd.dovecot.testsuite";
+require "fileinto";
+require "variables";
+require "mailbox";
+
+set "message1" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: First message
+
+Frop
+.
+;
+
+set "message2" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Second message
+
+Frop
+.
+;
+
+set "message3" text:
+From: stephan@example.org
+To: nico@frop.example.org
+Subject: Third message
+
+Frop
+.
+;
+
+test "Duplicates" {
+ test_set "message" "${message1}";
+
+ fileinto :create "Folder";
+ fileinto :create "Folder";
+
+ if not test_result_execute {
+ test_fail "failed to execute first result";
+ }
+
+ test_result_reset;
+
+ test_set "message" "${message2}";
+
+ fileinto :create "Folder";
+ fileinto :create "Folder";
+
+ if not test_result_execute {
+ test_fail "failed to execute second result";
+ }
+
+ test_result_reset;
+
+ test_set "message" "${message3}";
+
+ fileinto :create "Folder";
+ fileinto :create "Folder";
+
+ if not test_result_execute {
+ test_fail "failed to execute third result";
+ }
+
+ test_message :folder "Folder" 0;
+
+ if not header :is "subject" "First message" {
+ test_fail "first message incorrect";
+ }
+
+ test_message :folder "Folder" 1;
+
+ if not header :is "subject" "Second message" {
+ test_fail "first message incorrect";
+ }
+
+ test_message :folder "Folder" 2;
+
+ if not header :is "subject" "Third message" {
+ test_fail "first message incorrect";
+ }
+}
+
+
diff --git a/pigeonhole/tests/execute/smtp.svtest b/pigeonhole/tests/execute/smtp.svtest
new file mode 100644
index 0000000..2c7d2e6
--- /dev/null
+++ b/pigeonhole/tests/execute/smtp.svtest
@@ -0,0 +1,449 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+
+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 "Redirect" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if not address :is "to" "tss@example.net" {
+ test_fail "to address incorrect (strange forward)";
+ }
+
+ if not address :is "from" "stephan@example.org" {
+ test_fail "from address incorrect (strange forward)";
+ }
+
+ if not envelope :is "to" "cras@example.net" {
+ test_fail "envelope recipient incorrect";
+ }
+
+ if not envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+test_set "envelope.from" "<>";
+test_set "envelope.to" "timo@example.net";
+
+test "Redirect from <>" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect (not changed)";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+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_config_set "sieve_redirect_envelope_from" " recipient ";
+test_config_reload;
+
+test "Redirect from [recipient]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect (not changed)";
+ }
+
+ if not envelope :is "from" "timo@example.net" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+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_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "orig_recipient ";
+test_config_reload;
+
+test "Redirect from [original recipient]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect (not changed)";
+ }
+
+ if not envelope :is "from" "tss@example.net" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+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_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "<backscatter@example.net> ";
+test_config_reload;
+
+test "Redirect from [<explicit>]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect (not changed)";
+ }
+
+ if not envelope :is "from" "backscatter@example.net" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+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_config_set "sieve_redirect_envelope_from" "<>";
+test_config_reload;
+
+test "Redirect from [<>]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect (not changed)";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+test_set "message" text:
+From: stephan@example.org
+To: tss@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+test_set "envelope.from" "<>";
+test_set "envelope.to" "timo@example.net";
+test_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "<backscatter@example.net>";
+test_config_reload;
+
+test "Redirect from <> with [<explicit>]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "backscatter@example.net" {
+ test_fail "envelope sender incorrect (erroneously changed)";
+ }
+
+ if not envelope :is "from" "" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+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_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "user_email";
+test_config_reload;
+
+test "Redirect from [user email - fallback default]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if not envelope :is "from" "timo@example.net" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "timo@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+test_result_reset;
+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_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "user_email";
+test_config_set "sieve_user_email" "t.sirainen@example.net";
+test_config_reload;
+
+test "Redirect from [user email]" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ test_message :smtp 0;
+
+ if envelope :is "from" "sirius@example.org" {
+ test_fail "envelope sender incorrect (not changed)";
+ }
+
+ if not envelope :is "from" "t.sirainen@example.net" {
+ test_fail "envelope sender incorrect";
+ }
+
+ if not header :contains "x-sieve-redirected-from"
+ "t.sirainen@example.net" {
+ test_fail "x-sieve-redirected-from header is incorrect";
+ }
+}
+
+/*
+ * Redirect mail loop (sieve_user_email)
+ */
+
+test_result_reset;
+test_set "message" text:
+X-Sieve-Redirected-From: t.sirainen@example.net
+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_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "user_email";
+test_config_set "sieve_user_email" "t.sirainen@example.net";
+test_config_reload;
+
+test "Redirect mail loop (sieve_user_email)" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "failed to recognize mail loop";
+ }
+}
+
+/*
+ * Redirect mail loop (final recipient)
+ */
+
+test_result_reset;
+test_set "message" text:
+X-Sieve-Redirected-From: timo@example.net
+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_set "envelope.orig_to" "tss@example.net";
+
+test_config_reload;
+
+test "Redirect mail loop (final recipient)" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "failed to recognize mail loop";
+ }
+}
+
+/*
+ * Redirect mail loop (multiple headers)
+ */
+
+test_result_reset;
+test_set "message" text:
+X-Sieve-Redirected-From: stephan@example.net
+From: stephan@example.org
+To: tss@example.net
+Subject: Frop!
+X-Sieve-Redirected-From: t.sirainen@example.net
+X-Sieve-Redirected-From: t.sirainen@example.com
+
+Frop!
+.
+;
+test_set "envelope.from" "sirius@example.org";
+test_set "envelope.to" "timo@example.net";
+test_set "envelope.orig_to" "tss@example.net";
+
+test_config_set "sieve_redirect_envelope_from" "user_email";
+test_config_set "sieve_user_email" "t.sirainen@example.net";
+test_config_reload;
+
+test "Redirect mail loop (sieve_user_email)" {
+ redirect "cras@example.net";
+
+ if not test_result_execute {
+ test_fail "failed to execute redirect";
+ }
+
+ if test_message :smtp 0 {
+ test_fail "failed to recognize mail loop";
+ }
+}
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
diff --git a/pigeonhole/tests/failures/fuzz1.svtest b/pigeonhole/tests/failures/fuzz1.svtest
new file mode 100644
index 0000000..a6fe086
--- /dev/null
+++ b/pigeonhole/tests/failures/fuzz1.svtest
@@ -0,0 +1,33 @@
+# Used to cause the test suite to segfault
+
+require "vnd.dovecot.testsuite";
+require "fileinto";
+require "imap4flags";
+require "mailbox";
+
+
+test_set "message" text:
+Subject: Test message.
+
+Test message.
+.
+;
+
+test "Flag changes between stores" {
+ fileinto :create "FolderA";
+
+ if not test_result_execute {
+ test_fail "failed to execute first result";
+ }
+
+ test_message :folder "FolderA" 0;
+
+ test_result_reset;
+
+ test_message :folder "Uninteiesting" 0;
+
+ if not hasflag "$label1" {
+ test_fail "flags not stored for fired for third message";
+ }
+
+}
diff --git a/pigeonhole/tests/failures/fuzz2.svtest b/pigeonhole/tests/failures/fuzz2.svtest
new file mode 100644
index 0000000..9fa63ea
--- /dev/null
+++ b/pigeonhole/tests/failures/fuzz2.svtest
@@ -0,0 +1,37 @@
+require "vnd.dovecot.testsuite";
+require "fileinto";
+require "variables";
+require "mailbox";
+
+set "message" text:
+From:.org
+To:rg
+Subject: First message
+
+Frop
+.
+;
+
+
+test "sometest" {
+ test_set "message" "${message}";
+
+ fileinto :create "Folder";
+
+ if not test_result_execute {
+ test_fail "";
+ }
+
+ test_message :folder "Folder" 0;
+
+ if not header "subject" "First message" {
+ test_fail "";
+ }
+
+ test_message :folder " .Folder" 1;
+
+ if not header "subject" "Second message" {
+ test_fail "";
+ }
+
+}
diff --git a/pigeonhole/tests/failures/fuzz3.svtest b/pigeonhole/tests/failures/fuzz3.svtest
new file mode 100644
index 0000000..c1c22dc
--- /dev/null
+++ b/pigeonhole/tests/failures/fuzz3.svtest
@@ -0,0 +1,12 @@
+require "vnd.dovecot.testsuite";
+require "fileinto";
+require "mailbox";
+
+test"" {
+ fileinto :create "Folder";
+
+ if test_result_execute {
+ }
+
+ test_message :folder "Folder" 2;
+}
diff --git a/pigeonhole/tests/failures/mailbox-bad-utf8.svtest b/pigeonhole/tests/failures/mailbox-bad-utf8.svtest
new file mode 100644
index 0000000..ad104e5
--- /dev/null
+++ b/pigeonhole/tests/failures/mailbox-bad-utf8.svtest
@@ -0,0 +1,6 @@
+require "vnd.dovecot.testsuite";
+require "encoded-character";
+
+test "Mailbox parameter with bad UTF-8" {
+ test_message :folder "I${hex:9b}BOX" 0;
+}
diff --git a/pigeonhole/tests/lexer.svtest b/pigeonhole/tests/lexer.svtest
new file mode 100644
index 0000000..491309d
--- /dev/null
+++ b/pigeonhole/tests/lexer.svtest
@@ -0,0 +1,39 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+
+/* Test conformance to RFC 5228 - 2.4.2. Strings */
+
+set "text" text: # Comment
+Line 1
+.Line 2
+..Line 3
+.Line 4
+Line 5
+.
+;
+
+set "quoted"
+"Line 1
+.Line 2
+.Line 3
+.Line 4
+Line 5
+";
+
+test "String Literal" {
+ if not string :is "${text}" "${quoted}" {
+ test_fail "lexer messed-up dot stuffing";
+ }
+
+ if string :is "${text}" "" {
+ test_fail "variable substitution failed";
+ }
+}
+
+test "Unknown Escapes" {
+ if not string :is "\a\a\a\a\a" "aaaaa" {
+ test_fail "unknown quoted string escape sequences are handled inappropriately";
+ }
+}
+
+
diff --git a/pigeonhole/tests/match-types/contains.svtest b/pigeonhole/tests/match-types/contains.svtest
new file mode 100644
index 0000000..710afca
--- /dev/null
+++ b/pigeonhole/tests/match-types/contains.svtest
@@ -0,0 +1,81 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: stephan@example.org
+Cc: frop@example.com
+To: test@dovecot.example.net
+X-Bullshit: f fr fro frop frob frobn frobnitzn
+Subject: Test Message
+Comment:
+
+Test!
+.
+;
+
+# Match tests
+
+test "Match empty" {
+ if not header :contains "x-bullshit" "" {
+ test_fail "contains tests fails to match \"\" against non-empty string";
+ }
+
+ if not header :contains "comment" "" {
+ test_fail "contains tests fails to match \"\" against empty string";
+ }
+}
+
+test "Match full" {
+ if not address :contains "from" "stephan@example.org" {
+ test_fail "should have matched";
+ }
+}
+
+test "Match begin" {
+ if not address :contains "from" "stephan" {
+ test_fail "should have matched";
+ }
+}
+
+test "Match end" {
+ if not address :contains "from" "example.org" {
+ test_fail "should have matched";
+ }
+}
+
+test "Match middle" {
+ if not address :contains "from" "@" {
+ test_fail "should have matched";
+ }
+}
+
+test "Match similar beginnings" {
+ if not header :contains "x-bullshit" "frobnitzn" {
+ test_fail "should have matched";
+ }
+}
+
+test "Match case-insensitive" {
+ if not address :contains :comparator "i;ascii-casemap" "from" "EXAMPLE" {
+ test_fail "match fails to apply correct comparator";
+ }
+
+ if not address :contains "from" "EXAMPLE" {
+ test_fail "default comparator is wrong";
+ }
+}
+
+# Non-match tests
+
+test "No match full (typo)" {
+ if address :contains "to" "frob@example.com" {
+ test_fail "should not have matched";
+ }
+}
+
+test "No match end (typo)" {
+ if header :contains "x-bullshit" "frobnitzm" {
+ test_fail "should not have matched";
+ }
+}
+
+
diff --git a/pigeonhole/tests/match-types/is.svtest b/pigeonhole/tests/match-types/is.svtest
new file mode 100644
index 0000000..c715db8
--- /dev/null
+++ b/pigeonhole/tests/match-types/is.svtest
@@ -0,0 +1,22 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: Stephan Bosch <stephan@example.org>
+To: nico@frop.example.org
+Subject: Test message
+Comment:
+
+Test!
+
+.
+;
+
+test "Empty key" {
+ if header :is "from" "" {
+ test_fail "erroneously matched empty key against non-empty string";
+ }
+
+ if not header :is "comment" "" {
+ test_fail "failed to match empty string";
+ }
+}
diff --git a/pigeonhole/tests/match-types/matches.svtest b/pigeonhole/tests/match-types/matches.svtest
new file mode 100644
index 0000000..bcc188d
--- /dev/null
+++ b/pigeonhole/tests/match-types/matches.svtest
@@ -0,0 +1,241 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: stephan+sieve@friep.example.com
+To: sirius@example.org
+To: nico@frop.example.org
+Cc: me@example.com
+Cc: timo@dovecot.example.com
+X-Hufter: TRUE
+Subject: make your money very fast!!!
+X-Spam-Score: **********
+X-Bullshit: 33333???a
+Message-ID: <90a02fe01fc25e131d0e9c4c45975894@example.com>
+Comment:
+X-Subject: Log for successful build of Dovecot.
+
+Het werkt!
+.
+;
+
+/*
+ * General conformance testing
+ */
+
+test "Empty string" {
+ if not header :matches "comment" "" {
+ test_fail "failed to match \"\" against \"\"";
+ }
+
+ if not header :matches "comment" "*" {
+ test_fail "failed to match \"\" against \"*\"";
+ }
+
+ if header :matches "comment" "?" {
+ test_fail "inappropriately matched \"\" against \"?\"";
+ }
+}
+
+test "Multiple '*'" {
+ if not address :matches "from" "*@fri*p*examp*.com" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "*@f*pex*mple.com" {
+ test_fail "should not have matched";
+ }
+}
+
+test "End '*'" {
+ if not address :matches "from" "stephan+sieve@friep.*" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "stepan+sieve@friep.*" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Begin '*'" {
+ if not address :matches "from" "*+sieve@friep.example.com" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "*+sieve@friep.example.om" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Middle '?'" {
+ if not address :matches "from" "stephan+sieve?friep.example.com" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "stephan+sieve?fiep.example.com" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Begin '?'" {
+ if not address :matches "from" "?tephan+sieve@friep.example.com" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "?tephan+sievefriep.example.com" {
+ test_fail "should not have matched";
+ }
+}
+
+test "End '?'" {
+ if not address :matches "from" "stephan+sieve@friep.example.co?" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "sephan+sieve@friep.example.co?" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Multiple '?'" {
+ if not address :matches "from" "?t?phan?sieve?fri?p.exampl?.co?" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "from" "?t?phan?sieve?fiep.exam?le.co?" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Escaped '?'" {
+ if not header :matches "x-bullshit" "33333\\?\\?\\??" {
+ test_fail "should have matched";
+ }
+
+ if header :matches "x-bullshit" "33333\\?\\?\\?" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Escaped '?' following '*'" {
+ if not header :matches "x-bullshit" "33333*\\?\\??" {
+ test_fail "should have matched";
+ }
+
+}
+
+test "Escaped '?' directly following initial '*'" {
+ if not header :matches "X-Bullshit" "*\\?\\?\\?a" {
+ test_fail "should have matched";
+ }
+}
+
+test "Escaped '?' following initial '*'" {
+ if not header :matches "x-bullshit" "*3333\\?\\?\\?a" {
+ test_fail "should have matched";
+ }
+}
+
+test "Escaped '*' with active '*' at the end" {
+ if not header :matches "x-spam-score" "\\*\\*\\*\\*\\**" {
+ test_fail "should have matched";
+ }
+}
+
+test "All escaped '*'" {
+ if not header :matches "x-spam-score" "\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*" {
+ test_fail "should have matched";
+ }
+
+ if header :matches "x-spam-score" "\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Middle not escaped '*'" {
+ if not header :matches "x-spam-score" "\\*\\*\\***\\*\\*" {
+ test_fail "should have matched";
+ }
+}
+
+test "Escaped '*' alternating with '?'" {
+ if not header :matches "x-spam-score" "\\*?\\*?\\*?\\*?\\*?" {
+ test_fail "should have matched";
+ }
+
+ if header :matches "x-spam-score" "\\*?\\*?\\*?\\*?\\*??" {
+ test_fail "should not have matched";
+ }
+}
+
+test "All escaped" {
+ if header :matches "x-bullshit" "\\*3333\\?\\?\\?a" {
+ test_fail "should not have matched";
+ }
+
+
+ if header :matches "x-bullshit" "33333\\?\\?\\?aa" {
+ test_fail "should not have matched";
+ }
+
+ if header :matches "x-bullshit" "\\f3333\\?\\?\\?a" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Put '*' directly before '?'" {
+ if header :matches "x-subject" "Log for *??????????? build of *" {
+ test_fail "should not have matched";
+ }
+
+ if not header :matches "x-subject" "Log for *?????????? build of *" {
+ test_fail "should have matched";
+ }
+
+ if not header :matches "x-subject" "Log for *? build of *" {
+ test_fail "should have matched";
+ }
+}
+
+test "Put '?' directly before '*'" {
+ if header :matches "x-subject" "Log for ???????????* build of *" {
+ test_fail "should not have matched";
+ }
+
+ if not header :matches "x-subject" "Log for ??????????* build of *" {
+ test_fail "should have matched";
+ }
+
+ if not header :matches "x-subject" "Log for ?* build of *" {
+ test_fail "should have matched";
+ }
+}
+
+test "Fixed beginning" {
+ if not header :matches "subject" "make your *" {
+ test_fail "should have matched";
+ }
+}
+
+test "Fixed end" {
+ if not header :matches "subject" "* very fast!!!" {
+ test_fail "should have matched";
+ }
+
+ if header :matches "subject" "* very fast!!" {
+ test_fail "should not have matched";
+ }
+}
+
+test "Fixed string" {
+ if not address :matches "to" "sirius@example.org" {
+ test_fail "should have matched";
+ }
+
+ if address :matches "to" "example.org" {
+ test_fail "should not have matched";
+ }
+
+ if address :matches "to" "sirius" {
+ test_fail "should not have matched";
+ }
+}
diff --git a/pigeonhole/tests/multiscript/basic.svtest b/pigeonhole/tests/multiscript/basic.svtest
new file mode 100644
index 0000000..ce9bb66
--- /dev/null
+++ b/pigeonhole/tests/multiscript/basic.svtest
@@ -0,0 +1,91 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: stephan@example.org
+Message-ID: <frop33333333333333333@frutsens.example.nl>
+To: nico@frop.example.org
+Subject: Frop.
+
+Friep.
+.
+;
+
+test "Append" {
+ if not allof (
+ test_script_compile "fileinto-inbox.sieve",
+ test_script_run ){
+ test_fail "failed to compile and run first script";
+ }
+
+ if not allof (
+ test_script_compile "vacation.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run second script";
+ }
+
+ if not allof (
+ test_script_compile "notify.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run third script";
+ }
+
+ if not test_result_action :index 1 "store" {
+ test_fail "first action is not 'store'";
+ }
+
+ if not test_result_action :index 2 "vacation" {
+ test_fail "second action is not 'vacation'";
+ }
+
+ if not test_result_action :index 3 "notify" {
+ test_fail "third action is not 'notify'";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed";
+ }
+}
+
+test "Sequential Execute" {
+ if not allof (
+ test_script_compile "fileinto-inbox.sieve",
+ test_script_run ) {
+ test_fail "failed to compile and run first script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after first script";
+ }
+
+ if not allof (
+ test_script_compile "vacation.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run second script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after second script";
+ }
+
+ if not allof (
+ test_script_compile "notify.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run third script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after third script";
+ }
+
+ if not test_result_action :index 1 "store" {
+ test_fail "first action is not 'store'";
+ }
+
+ if not test_result_action :index 2 "vacation" {
+ test_fail "second action is not 'vacation'";
+ }
+
+ if not test_result_action :index 3 "notify" {
+ test_fail "third action is not 'notify'";
+ }
+}
diff --git a/pigeonhole/tests/multiscript/conflicts.svtest b/pigeonhole/tests/multiscript/conflicts.svtest
new file mode 100644
index 0000000..a2b8fab
--- /dev/null
+++ b/pigeonhole/tests/multiscript/conflicts.svtest
@@ -0,0 +1,100 @@
+require "vnd.dovecot.testsuite";
+
+test_set "message" text:
+From: stephan@example.org
+Message-ID: <frop33333333333333333@nl.example.com>
+To: nico@frop.example.org
+Subject: Frop.
+
+Friep.
+.
+;
+
+test "Graceful Conflicts" {
+ if not allof (
+ test_script_compile "fileinto-inbox.sieve",
+ test_script_run ){
+ test_fail "failed to compile and run first script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after first script";
+ }
+
+ if not allof (
+ test_script_compile "reject-1.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run second script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after second script";
+ }
+
+ if not allof (
+ test_script_compile "reject-2.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run third script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after third script";
+ }
+
+ if not test_result_action :index 1 "store" {
+ test_result_print;
+ test_fail "first action is not 'store'";
+ }
+
+ if not test_result_action :index 2 "reject" {
+ test_result_print;
+ test_fail "first reject action not retained";
+ }
+
+ if test_result_action :index 3 "reject" {
+ test_result_print;
+ test_fail "second reject action not discarded";
+ }
+
+}
+
+test "Duplicates" {
+ if not allof (
+ test_script_compile "fileinto-inbox.sieve",
+ test_script_run ){
+ test_fail "failed to compile and run first script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after first script";
+ }
+
+ if not allof (
+ test_script_compile "fileinto-inbox.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run second script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after second script";
+ }
+
+ if not allof (
+ test_script_compile "keep.sieve",
+ test_script_run :append_result ) {
+ test_fail "failed to compile and run third script";
+ }
+
+ if not test_result_execute {
+ test_fail "result execute failed after third script";
+ }
+
+ if not test_result_action :index 1 "keep" {
+ test_fail "first action is not 'keep'";
+ }
+
+ if test_result_action :index 2 "store" {
+ test_fail "fileinto action not discarded";
+ }
+}
+
diff --git a/pigeonhole/tests/multiscript/fileinto-frop.sieve b/pigeonhole/tests/multiscript/fileinto-frop.sieve
new file mode 100644
index 0000000..9aafb95
--- /dev/null
+++ b/pigeonhole/tests/multiscript/fileinto-frop.sieve
@@ -0,0 +1,3 @@
+require "fileinto";
+
+fileinto "frop";
diff --git a/pigeonhole/tests/multiscript/fileinto-inbox.sieve b/pigeonhole/tests/multiscript/fileinto-inbox.sieve
new file mode 100644
index 0000000..b5da850
--- /dev/null
+++ b/pigeonhole/tests/multiscript/fileinto-inbox.sieve
@@ -0,0 +1,4 @@
+require "fileinto";
+
+fileinto "INBOX";
+
diff --git a/pigeonhole/tests/multiscript/keep.sieve b/pigeonhole/tests/multiscript/keep.sieve
new file mode 100644
index 0000000..6203a21
--- /dev/null
+++ b/pigeonhole/tests/multiscript/keep.sieve
@@ -0,0 +1 @@
+keep;
diff --git a/pigeonhole/tests/multiscript/notify.sieve b/pigeonhole/tests/multiscript/notify.sieve
new file mode 100644
index 0000000..af47ad9
--- /dev/null
+++ b/pigeonhole/tests/multiscript/notify.sieve
@@ -0,0 +1,3 @@
+require "enotify";
+
+notify "mailto:stephan@example.org";
diff --git a/pigeonhole/tests/multiscript/reject-1.sieve b/pigeonhole/tests/multiscript/reject-1.sieve
new file mode 100644
index 0000000..06744f6
--- /dev/null
+++ b/pigeonhole/tests/multiscript/reject-1.sieve
@@ -0,0 +1,3 @@
+require "reject";
+
+reject "Message is not wanted.";
diff --git a/pigeonhole/tests/multiscript/reject-2.sieve b/pigeonhole/tests/multiscript/reject-2.sieve
new file mode 100644
index 0000000..96b7564
--- /dev/null
+++ b/pigeonhole/tests/multiscript/reject-2.sieve
@@ -0,0 +1,3 @@
+require "reject";
+
+reject "Will not accept this nonsense.";
diff --git a/pigeonhole/tests/multiscript/vacation.sieve b/pigeonhole/tests/multiscript/vacation.sieve
new file mode 100644
index 0000000..d735da5
--- /dev/null
+++ b/pigeonhole/tests/multiscript/vacation.sieve
@@ -0,0 +1,3 @@
+require "vacation";
+
+vacation "I am not home";
diff --git a/pigeonhole/tests/plugins/extprograms/bin/addheader b/pigeonhole/tests/plugins/extprograms/bin/addheader
new file mode 100755
index 0000000..8f9805a
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/addheader
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+echo "$1: $2"
+cat
+
+exit 0
diff --git a/pigeonhole/tests/plugins/extprograms/bin/big b/pigeonhole/tests/plugins/extprograms/bin/big
new file mode 100755
index 0000000..ce1df51
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/big
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+N="0123456701234567012345670123456701234567012345670123456701234567"
+N="$N$N$N$N$N$N$N$N$N$N$N$N$N$N$N$N"
+echo -n "$N$N"
+
+exit 0
+
diff --git a/pigeonhole/tests/plugins/extprograms/bin/cat b/pigeonhole/tests/plugins/extprograms/bin/cat
new file mode 100755
index 0000000..02b9858
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/cat
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+cat
diff --git a/pigeonhole/tests/plugins/extprograms/bin/cat-stdin b/pigeonhole/tests/plugins/extprograms/bin/cat-stdin
new file mode 100755
index 0000000..781d70b
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/cat-stdin
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+cat /dev/stdin
diff --git a/pigeonhole/tests/plugins/extprograms/bin/crlf b/pigeonhole/tests/plugins/extprograms/bin/crlf
new file mode 100755
index 0000000..a0028cf
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/crlf
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+tr -s '\r' '#'
diff --git a/pigeonhole/tests/plugins/extprograms/bin/env b/pigeonhole/tests/plugins/extprograms/bin/env
new file mode 100755
index 0000000..a7b81ac
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/env
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+eval echo -n "\${$1}"
diff --git a/pigeonhole/tests/plugins/extprograms/bin/frame b/pigeonhole/tests/plugins/extprograms/bin/frame
new file mode 100755
index 0000000..225005e
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/frame
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+echo -n "FRAMED $1{ "
+cat
+echo -n " }"
+
+exit 0
diff --git a/pigeonhole/tests/plugins/extprograms/bin/modify b/pigeonhole/tests/plugins/extprograms/bin/modify
new file mode 100755
index 0000000..ce87014
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/modify
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+echo "X-Frop: Extra header"
+cat
+echo
+echo "Extra body content!"
+
+exit 0
diff --git a/pigeonhole/tests/plugins/extprograms/bin/program b/pigeonhole/tests/plugins/extprograms/bin/program
new file mode 100755
index 0000000..4b5edbf
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/program
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+cat > /dev/null
+
+exit 0
diff --git a/pigeonhole/tests/plugins/extprograms/bin/replace b/pigeonhole/tests/plugins/extprograms/bin/replace
new file mode 100755
index 0000000..b010f06
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/replace
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+cat > /dev/null
+
+echo "From: hatseflat@example.com"
+echo "To: frutsel@example.org"
+echo "Subject: replacement message"
+echo
+echo "Replaced!"
+
+
+exit 0
diff --git a/pigeonhole/tests/plugins/extprograms/bin/sleep10 b/pigeonhole/tests/plugins/extprograms/bin/sleep10
new file mode 100755
index 0000000..8c1b96d
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/sleep10
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+sleep 10
diff --git a/pigeonhole/tests/plugins/extprograms/bin/sleep2 b/pigeonhole/tests/plugins/extprograms/bin/sleep2
new file mode 100755
index 0000000..a814acd
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/sleep2
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+sleep 2
diff --git a/pigeonhole/tests/plugins/extprograms/bin/spamc b/pigeonhole/tests/plugins/extprograms/bin/spamc
new file mode 100755
index 0000000..a3232f4
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/spamc
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+echo 'X-Spam-Status: Yes, score=66.5/5.0 tests=CONTAINS_LARGE_ROOSTER'
+cat
+
+exit 0
diff --git a/pigeonhole/tests/plugins/extprograms/bin/stderr b/pigeonhole/tests/plugins/extprograms/bin/stderr
new file mode 100755
index 0000000..75b94b0
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/bin/stderr
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+echo "========================================" 1>&2
+echo "Test shell script successfully executed!" 1>&2
+echo 1>&2
+echo "Arguments: $1 $2" 1>&2
+echo 1>&2
+echo "Environment:" 1>&2
+env 1>&2
+echo 1>&2
+echo "Message:" 1>&2
+cat 1>&2
+echo "========================================" 1>&2
+echo 1>&2
+
+echo "Subject: frop!"
+echo "From: stephan@example.org"
+echo "To: tss@example.com"
+echo
+echo "Frop!"
diff --git a/pigeonhole/tests/plugins/extprograms/errors.svtest b/pigeonhole/tests/plugins/extprograms/errors.svtest
new file mode 100644
index 0000000..148f4da
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/errors.svtest
@@ -0,0 +1,32 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Invalid program names
+ */
+
+test "Invalid Program Names" {
+ if test_script_compile "errors/programname.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";
+ }
+}
+
+/*
+ * Invalid arguments
+ */
+
+test "Invalid Arguments" {
+ if test_script_compile "errors/arguments.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/plugins/extprograms/errors/arguments.sieve b/pigeonhole/tests/plugins/extprograms/errors/arguments.sieve
new file mode 100644
index 0000000..04f0aa0
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/errors/arguments.sieve
@@ -0,0 +1,5 @@
+require "vnd.dovecot.pipe";
+
+pipe :args "aaaa
+ aaaa" "frop";
+
diff --git a/pigeonhole/tests/plugins/extprograms/errors/programname.sieve b/pigeonhole/tests/plugins/extprograms/errors/programname.sieve
new file mode 100644
index 0000000..1d2d19c
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/errors/programname.sieve
@@ -0,0 +1,25 @@
+require "variables";
+require "encoded-character";
+require "vnd.dovecot.pipe";
+
+# Slash
+pipe "../frop";
+
+# More slashes
+pipe "../../james/sieve/vacation";
+
+# 0000-001F; [CONTROL CHARACTERS]
+pipe "idiotic${unicode: 001a}";
+
+# 007F; DELETE
+pipe "idiotic${unicode: 007f}";
+
+# 0080-009F; [CONTROL CHARACTERS]
+pipe "idiotic${unicode: 0085}";
+
+# 2028; LINE SEPARATOR
+pipe "idiotic${unicode: 2028}";
+
+# 2029; PARAGRAPH SEPARATOR
+pipe "idiotic${unicode: 2029}";
+
diff --git a/pigeonhole/tests/plugins/extprograms/execute/command.svtest b/pigeonhole/tests/plugins/extprograms/execute/command.svtest
new file mode 100644
index 0000000..92c1fd1
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/execute/command.svtest
@@ -0,0 +1,27 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.execute";
+require "variables";
+
+test_config_set "sieve_execute_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.execute";
+
+test "Basic" {
+ execute "program";
+}
+
+test "Input message" {
+ execute :pipe "program";
+}
+
+test "Input string" {
+ execute :input "DATA" "program";
+}
+
+test "Input variable" {
+ set "DATA" "DATA";
+ execute :input "${DATA}" "program";
+}
+
+test "Output variable" {
+ execute :output "DATA" "program";
+}
diff --git a/pigeonhole/tests/plugins/extprograms/execute/errors.svtest b/pigeonhole/tests/plugins/extprograms/execute/errors.svtest
new file mode 100644
index 0000000..3dd2d5f
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/execute/errors.svtest
@@ -0,0 +1,53 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+test_config_set "sieve_execute_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.execute";
+
+/*
+ * Command syntax
+ */
+
+test "Command syntax" {
+ if test_script_compile "errors/syntax.sieve" {
+ test_fail "compile should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "13" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Variables
+ */
+
+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" "2" {
+ test_fail "wrong number of errors reported";
+ }
+}
+
+/*
+ * Unknown program
+ */
+
+test "Unknown program" {
+ if not test_script_compile "errors/unknown-program.sieve" {
+ test_fail "compile should have succeeded";
+ }
+
+ if test_script_run {
+ test_fail "execution should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of errors reported";
+ }
+}
diff --git a/pigeonhole/tests/plugins/extprograms/execute/errors/syntax.sieve b/pigeonhole/tests/plugins/extprograms/execute/errors/syntax.sieve
new file mode 100644
index 0000000..1f4646a
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/execute/errors/syntax.sieve
@@ -0,0 +1,38 @@
+require "vnd.dovecot.execute";
+
+# 1: error: no arguments
+execute;
+
+# 2: error: numeric argument
+execute 1;
+
+# 3: error: tag argument
+execute :frop;
+
+# 4: error: numeric second argument
+execute "sdfd" 1;
+
+# 5: error: stringlist first argument
+execute ["sdfd","werwe"] "sdfs";
+
+# 6: error: too many arguments
+execute "sdfs" "sdfd" "werwe";
+
+# 7: error: inappropriate :copy argument
+execute :copy "234234" ["324234", "23423"];
+
+# 8: error: invalid :input argument; missing parameter
+execute :input "frop";
+
+# 9: error: invalid :input argument; invalid parameter
+execute :input 1 "frop";
+
+# 10: error: invalid :input argument; invalid parameter
+execute :input ["23423","21342"] "frop";
+
+# 11: error: invalid :input argument; invalid parameter
+execute :input :friep "frop";
+
+# 12: error: :output not allowed without variables extension
+execute :output "${frop}" "frop";
+
diff --git a/pigeonhole/tests/plugins/extprograms/execute/errors/unknown-program.sieve b/pigeonhole/tests/plugins/extprograms/execute/errors/unknown-program.sieve
new file mode 100644
index 0000000..3a79bb6
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/execute/errors/unknown-program.sieve
@@ -0,0 +1,3 @@
+require "vnd.dovecot.execute";
+
+execute "unknown";
diff --git a/pigeonhole/tests/plugins/extprograms/execute/errors/variables.sieve b/pigeonhole/tests/plugins/extprograms/execute/errors/variables.sieve
new file mode 100644
index 0000000..3d0b3e7
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/execute/errors/variables.sieve
@@ -0,0 +1,7 @@
+require "vnd.dovecot.execute";
+require "variables";
+
+# 1: invalid variable name
+execute :output "wqwe-aeqwe" "frop";
+
+
diff --git a/pigeonhole/tests/plugins/extprograms/execute/execute.svtest b/pigeonhole/tests/plugins/extprograms/execute/execute.svtest
new file mode 100644
index 0000000..f16af11
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/execute/execute.svtest
@@ -0,0 +1,177 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.execute";
+require "vnd.dovecot.debug";
+require "variables";
+require "relational";
+require "environment";
+require "encoded-character";
+
+test_set "message" text:
+From: stephan@example.com
+To: pipe@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_config_set "sieve_execute_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.execute";
+test_result_reset;
+
+test "Execute - bare" {
+ execute "program";
+}
+
+test_result_reset;
+test "Execute - i/-" {
+ execute :input "FROP" "frame";
+}
+
+test_result_reset;
+test "Execute - -/o" {
+ execute :output "out" "frame";
+
+ if not string "${out}" "FRAMED { }" {
+ test_fail "wrong string returned: ${out}";
+ }
+}
+
+test_result_reset;
+test "Execute - i/o" {
+ execute :input "FROP" :output "out" "frame";
+
+ if not string "${out}" "FRAMED { FROP }" {
+ test_fail "wrong string returned: ${out}";
+ }
+}
+
+test_result_reset;
+test "Execute - i/o and arguments" {
+ execute :input "FROP" :output "out" "frame" ["FRIEP "];
+
+ if not string "${out}" "FRAMED FRIEP { FROP }" {
+ test_fail "wrong string returned: ${out}";
+ }
+}
+
+test_result_reset;
+test "Execute - pipe" {
+ execute :pipe :output "msg" "cat";
+
+ if not string :contains "${msg}" "Subject: Frop!" {
+ test_fail "wrong string returned: ${out}";
+ }
+}
+
+test_result_reset;
+test "Execute - pipe /dev/stdin" {
+ execute :pipe :output "msg" "cat-stdin";
+
+ if not string :contains "${msg}" "Subject: Frop!" {
+ test_fail "wrong string returned: ${out}";
+ }
+}
+
+test_result_reset;
+test "Execute - env" {
+ test_set "envelope.from" "stephan@sub.example.com";
+ test_set "envelope.to" "stephan@sub.example.net";
+ test_set "envelope.orig_to" "all@sub.example.net";
+
+ execute :output "out" "env" "SENDER";
+ if not string :is "${out}" "stephan@sub.example.com" {
+ test_fail "wrong SENDER env returned: '${out}'";
+ }
+
+ execute :output "out" "env" "RECIPIENT";
+ if not string :is "${out}" "stephan@sub.example.net" {
+ test_fail "wrong RECIPIENT env returned: '${out}'";
+ }
+
+ execute :output "out" "env" "ORIG_RECIPIENT";
+ if not string :is "${out}" "all@sub.example.net" {
+ test_fail "wrong ORIG_RECIPIENT env returned: '${out}'";
+ }
+
+ execute :output "out" "env" "HOST";
+ if not environment :is "host" "${out}" {
+ test_fail "wrong HOST env returned: '${out}'";
+ }
+
+ execute :output "out" "env" "HOME";
+ if string :count "eq" "${out}" "0" {
+ test_fail "empty HOME env returned";
+ }
+
+ execute :output "out" "env" "USER";
+ if string :count "eq" "${out}" "0" {
+ test_fail "empty USER env returned";
+ }
+}
+
+test_result_reset;
+test "Execute - used as test" {
+ if execute :pipe :output "msg" "dog" {
+ test_fail "execute action indicated success with invalid program";
+ }
+
+ if not execute :pipe :output "msg" "cat" {
+ test_fail "execute action indicated failure with valid program";
+ }
+
+ if not string :contains "${msg}" "Subject: Frop!" {
+ test_fail "wrong string returned: ${out}";
+ }
+}
+
+test_config_set "sieve_execute_input_eol" "crlf";
+test_config_reload :extension "vnd.dovecot.execute";
+test_result_reset;
+set "out" "";
+
+test "Execute - CRLF" {
+ execute
+ :input "FROP${hex:0A}FRIEP${hex:0a}"
+ :output "out"
+ "crlf";
+
+ if not string "${out}" "FROP#${hex:0A}FRIEP#${hex:0a}" {
+ test_fail "wrong string returned: '${out}'";
+ }
+}
+
+test_config_set "sieve_execute_input_eol" "lf";
+test_config_reload :extension "vnd.dovecot.execute";
+test_result_reset;
+set "out" "";
+
+test "Execute - LF" {
+ execute
+ :input "FROP${hex:0D 0A}FRIEP${hex:0d 0a}"
+ :output "out"
+ "crlf";
+
+ if not string "${out}" "FROP${hex:0A}FRIEP${hex:0a}" {
+ test_fail "wrong string returned: '${out}'";
+ }
+}
+
+set "D" "0123456701234567012345670123456701234567012345670123456701234567";
+set "D" "${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}${D}";
+set "data" "${D}${D}";
+
+test_config_set "sieve_execute_input_eol" "crlf";
+test_config_reload :extension "vnd.dovecot.execute";
+test_result_reset;
+set "out" "";
+
+test "Execute - big" {
+ execute
+ :output "out"
+ "big";
+
+ if not string "${out}" "${data}" {
+ test_fail "wrong string returned: '${out}'";
+ }
+}
diff --git a/pigeonhole/tests/plugins/extprograms/filter/command.svtest b/pigeonhole/tests/plugins/extprograms/filter/command.svtest
new file mode 100644
index 0000000..50f949a
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/filter/command.svtest
@@ -0,0 +1,10 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.filter";
+require "variables";
+
+test_config_set "sieve_filter_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.filter";
+
+test "Basic" {
+ filter "program";
+}
diff --git a/pigeonhole/tests/plugins/extprograms/filter/errors.svtest b/pigeonhole/tests/plugins/extprograms/filter/errors.svtest
new file mode 100644
index 0000000..1d04ba1
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/filter/errors.svtest
@@ -0,0 +1,39 @@
+require "vnd.dovecot.testsuite";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Command syntax
+ */
+
+test_config_set "sieve_filter_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.filter";
+
+test "Command syntax" {
+ if test_script_compile "errors/syntax.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";
+ }
+}
+
+/*
+ * Unknown program
+ */
+
+test "Unknown program" {
+ if not test_script_compile "errors/unknown-program.sieve" {
+ test_fail "compile should have succeeded";
+ }
+
+ if test_script_run {
+ test_fail "execution should have failed";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "1" {
+ test_fail "wrong number of errors reported";
+ }
+}
diff --git a/pigeonhole/tests/plugins/extprograms/filter/errors/syntax.sieve b/pigeonhole/tests/plugins/extprograms/filter/errors/syntax.sieve
new file mode 100644
index 0000000..00a3a23
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/filter/errors/syntax.sieve
@@ -0,0 +1,22 @@
+require "vnd.dovecot.filter";
+
+# 1: error: no arguments
+filter;
+
+# 2: error: numeric argument
+filter 1;
+
+# 3: error: tag argument
+filter :frop;
+
+# 4: error: numeric second argument
+filter "sdfd" 1;
+
+# 5: error: stringlist first argument
+filter ["sdfd","werwe"] "sdfs";
+
+# 6: error: too many arguments
+filter "sdfd" "werwe" "sdfs";
+
+# 7: error: inappropriate :copy argument
+filter :try :copy "234234" ["324234", "23423"];
diff --git a/pigeonhole/tests/plugins/extprograms/filter/errors/unknown-program.sieve b/pigeonhole/tests/plugins/extprograms/filter/errors/unknown-program.sieve
new file mode 100644
index 0000000..7e530ee
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/filter/errors/unknown-program.sieve
@@ -0,0 +1,3 @@
+require "vnd.dovecot.filter";
+
+filter "unknown";
diff --git a/pigeonhole/tests/plugins/extprograms/filter/execute.svtest b/pigeonhole/tests/plugins/extprograms/filter/execute.svtest
new file mode 100644
index 0000000..15fab69
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/filter/execute.svtest
@@ -0,0 +1,213 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.filter";
+require "vnd.dovecot.debug";
+require "variables";
+require "editheader";
+require "spamtest";
+require "body";
+require "fileinto";
+require "mailbox";
+
+test_set "message" text:
+From: stephan@example.com
+To: pipe@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_config_set "sieve_filter_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.filter";
+test_result_reset;
+
+test_result_reset;
+test "Replace" {
+ if header :contains "subject" "replacement" {
+ test_fail "message already replaced";
+ }
+
+ filter "replace";
+
+ if not header :contains "subject" "replacement" {
+ test_fail "message not replaced";
+ }
+}
+
+test_result_reset;
+test "Used as test" {
+ if filter "nonsense" {
+ test_fail "filter action indicated success with invalid program";
+ }
+
+ if not filter "replace" {
+ test_fail "filter action indicated failure with valid program";
+ }
+
+ if not header :contains "subject" "replacement" {
+ test_fail "message not replaced; filter not actually executed";
+ }
+}
+
+test_result_reset;
+test "Modify" {
+ if anyof (
+ body :contains "extra",
+ exists "x-frop") {
+ test_fail "message already modified";
+ }
+
+ if not header "subject" "Frop!" {
+ test_fail "message is wrong";
+ }
+
+ filter "modify";
+
+ if not header "subject" "Frop!" {
+ test_fail "message replaced erroneously";
+ }
+
+ if not header :contains "x-frop" "extra" {
+ test_fail "message header not modified";
+ }
+
+ if not body :contains "Extra" {
+ test_fail "message body not modified";
+ }
+}
+
+test_result_reset;
+test "Editheader" {
+ if anyof ( exists "X-A", exists "X-B", exists "X-C", exists "X-D",
+ exists "X-E") {
+ test_fail "message already modified";
+ }
+
+ addheader "X-A" "1";
+ if not header "X-A" "1" {
+ test_fail "X-A header missing";
+ }
+
+ fileinto :create "A";
+
+ filter "addheader" ["X-B", "2"];
+ if not header "X-B" "2" {
+ test_fail "X-B header missing";
+ }
+
+ fileinto :create "B";
+
+ addheader "X-C" "3";
+ if not header "X-C" "3" {
+ test_fail "X-C header missing";
+ }
+
+ fileinto :create "C";
+
+ filter "addheader" ["X-D", "4"];
+ if not header "X-D" "4" {
+ test_fail "X-D header missing";
+ }
+
+ fileinto :create "D";
+
+ addheader "X-E" "5";
+ if not header "X-E" "5" {
+ test_fail "X-E header missing";
+ }
+
+ fileinto :create "E";
+
+ if not test_result_execute {
+ test_fail "failed to execute result";
+ }
+
+ test_message :folder "A" 0;
+
+ if not header "X-A" "1" {
+ test_fail "X-A header missing";
+ }
+ if anyof (
+ header "X-B" "2", header "X-C" "3",
+ header "X-D" "4", header "X-E" "5") {
+ test_fail "X-B, X-C, X-D or X-E header found";
+ }
+
+ test_message :folder "B" 0;
+
+ if not header "X-B" "2" {
+ test_fail "X-B header missing";
+ }
+ if anyof (
+ header "X-C" "3", header "X-D" "4", header "X-E" "5") {
+ test_fail "X-C, X-D or X-E header found";
+ }
+
+ test_message :folder "C" 0;
+
+ if not header "X-C" "3" {
+ test_fail "X-C header missing";
+ }
+ if anyof (header "X-D" "4", header "X-E" "5") {
+ test_fail "X-D or X-E header found";
+ }
+
+ test_message :folder "D" 0;
+
+ if not header "X-D" "4" {
+ test_fail "X-D header missing";
+ }
+ if anyof (header "X-E" "5") {
+ test_fail "X-E header found";
+ }
+
+ test_message :folder "E" 0;
+
+ if not header "X-A" "1" {
+ test_fail "X-A header missing in final message";
+ }
+ if not header "X-B" "2" {
+ test_fail "X-B header missing in final message";
+ }
+ if not header "X-C" "3" {
+ test_fail "X-C header missing in final message";
+ }
+ if not header "X-D" "4" {
+ test_fail "X-D header missing in final message";
+ }
+ if not header "X-E" "5" {
+ test_fail "X-E header missing in final message";
+ }
+}
+
+test_config_set "sieve_spamtest_status_header"
+ "X-Spam-Status: [^,]*, score=(-?[[:digit:]]+\\.[[:digit:]]).*";
+test_config_set "sieve_spamtest_max_value" "10";
+test_config_set "sieve_spamtest_status_type" "score";
+test_config_reload :extension "spamtest";
+
+test_result_reset;
+test "Spamtest" {
+ if exists "x-spam-status" {
+ test_fail "message already modified";
+ }
+
+ if not header "subject" "Frop!" {
+ test_fail "message is wrong";
+ }
+
+ filter "spamc";
+
+ if not exists "x-spam-status" {
+ test_fail "x-spam-score header not added";
+ }
+
+ if spamtest :is "0" {
+ test_fail "spamtest not configured or test failed";
+ }
+
+ if not spamtest :is "10" {
+ test_fail "spamtest yields incorrect value";
+ }
+}
+
diff --git a/pigeonhole/tests/plugins/extprograms/pipe/command.svtest b/pigeonhole/tests/plugins/extprograms/pipe/command.svtest
new file mode 100644
index 0000000..dabd970
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/pipe/command.svtest
@@ -0,0 +1,10 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.pipe";
+
+test_config_set "sieve_pipe_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.pipe";
+
+test "Basic" {
+ pipe "program";
+}
+
diff --git a/pigeonhole/tests/plugins/extprograms/pipe/errors.svtest b/pigeonhole/tests/plugins/extprograms/pipe/errors.svtest
new file mode 100644
index 0000000..af36b91
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/pipe/errors.svtest
@@ -0,0 +1,94 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+
+require "relational";
+require "comparator-i;ascii-numeric";
+
+/*
+ * Command syntax
+ */
+
+test "Command syntax" {
+ if test_script_compile "errors/syntax.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";
+ }
+}
+
+/* Unknown program */
+
+test_set "message" text:
+From: stephan@example.com
+To: pipe@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_config_set "sieve_pipe_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.pipe";
+test_result_reset;
+
+test "Unknown program" {
+ if not test_script_compile "errors/unknown-program.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execute failed";
+ }
+
+ if test_result_execute {
+ test_fail "pipe 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 :index 1 :contains "failed to pipe" {
+ test_fail "wrong error reported";
+ }
+}
+
+/* Timeout */
+
+test_set "message" text:
+From: stephan@example.com
+To: pipe@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+test_config_set "sieve_pipe_bin_dir" "${tst.path}/../bin";
+test_config_set "sieve_pipe_exec_timeout" "1s";
+test_config_reload :extension "vnd.dovecot.pipe";
+test_result_reset;
+
+test "Timeout" {
+ if not test_script_compile "errors/timeout.sieve" {
+ test_fail "compile failed";
+ }
+
+ if not test_script_run {
+ test_fail "execute failed";
+ }
+
+ if test_result_execute {
+ test_fail "pipe should have timed out";
+ }
+
+ if not test_error :count "eq" :comparator "i;ascii-numeric" "2" {
+ test_fail "wrong number of errors reported";
+ }
+
+ if not test_error :index 2 :contains "failed to pipe" {
+ test_fail "wrong error reported";
+ }
+}
diff --git a/pigeonhole/tests/plugins/extprograms/pipe/errors/syntax.sieve b/pigeonhole/tests/plugins/extprograms/pipe/errors/syntax.sieve
new file mode 100644
index 0000000..64d5310
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/pipe/errors/syntax.sieve
@@ -0,0 +1,22 @@
+require "vnd.dovecot.pipe";
+
+# 1: error: no arguments
+pipe;
+
+# 2: error: numeric argument
+pipe 1;
+
+# 3: error: tag argument
+pipe :frop;
+
+# 4: error: numeric second argument
+pipe "sdfd" 1;
+
+# 5: error: stringlist first argument
+pipe ["sdfd","werwe"] "sdfs";
+
+# 6: error: too many arguments
+pipe "sdfd" "werwe" "sdfs";
+
+# 7: error: inappropriate :copy argument
+pipe :try :copy "234234" ["324234", "23423"];
diff --git a/pigeonhole/tests/plugins/extprograms/pipe/errors/timeout.sieve b/pigeonhole/tests/plugins/extprograms/pipe/errors/timeout.sieve
new file mode 100644
index 0000000..7a940c8
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/pipe/errors/timeout.sieve
@@ -0,0 +1,3 @@
+require "vnd.dovecot.pipe";
+
+pipe "sleep10";
diff --git a/pigeonhole/tests/plugins/extprograms/pipe/errors/unknown-program.sieve b/pigeonhole/tests/plugins/extprograms/pipe/errors/unknown-program.sieve
new file mode 100644
index 0000000..fd6338b
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/pipe/errors/unknown-program.sieve
@@ -0,0 +1,3 @@
+require "vnd.dovecot.pipe";
+
+pipe "unknown";
diff --git a/pigeonhole/tests/plugins/extprograms/pipe/execute.svtest b/pigeonhole/tests/plugins/extprograms/pipe/execute.svtest
new file mode 100644
index 0000000..34b6798
--- /dev/null
+++ b/pigeonhole/tests/plugins/extprograms/pipe/execute.svtest
@@ -0,0 +1,56 @@
+require "vnd.dovecot.testsuite";
+require "vnd.dovecot.pipe";
+require "vnd.dovecot.debug";
+require "variables";
+
+test_set "message" text:
+From: stephan@example.com
+To: pipe@example.net
+Subject: Frop!
+
+Frop!
+.
+;
+
+/* Basic pipe */
+
+test_config_set "sieve_pipe_bin_dir" "${tst.path}/../bin";
+test_config_reload :extension "vnd.dovecot.pipe";
+test_result_reset;
+
+test "Pipe" {
+ pipe "stderr" ["ONE", "TWO"];
+
+ if not test_result_execute {
+ test_fail "failed to pipe message to script";
+ }
+}
+
+/* Timeout */
+
+test_config_set "sieve_pipe_bin_dir" "${tst.path}/../bin";
+test_config_set "sieve_pipe_exec_timeout" "3s";
+test_config_reload :extension "vnd.dovecot.pipe";
+test_result_reset;
+
+test "Timeout 3s" {
+ pipe "sleep2";
+
+ if not test_result_execute {
+ test_fail "failed to pipe message to script";
+ }
+}
+
+test_result_reset;
+test_config_set "sieve_pipe_bin_dir" "${tst.path}/../bin";
+test_config_set "sieve_pipe_exec_timeout" "0";
+test_config_reload :extension "vnd.dovecot.pipe";
+test_result_reset;
+
+test "Timeout infinite" {
+ pipe "sleep2";
+
+ if not test_result_execute {
+ test_fail "failed to pipe message to script";
+ }
+}
diff --git a/pigeonhole/tests/test-address.svtest b/pigeonhole/tests/test-address.svtest
new file mode 100644
index 0000000..135d549
--- /dev/null
+++ b/pigeonhole/tests/test-address.svtest
@@ -0,0 +1,434 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * ## RFC 5228, Section 5.1. Test address (page 26) ##
+ */
+
+/*
+ * TEST: Basic functionionality
+ */
+
+/* "The "address" test matches Internet addresses in structured headers
+ * that contain addresses. It returns true if any header contains any
+ * key in the specified part of the address, as modified by the
+ * comparator and the match keyword. Whether there are other addresses
+ * present in the header doesn't affect this test; this test does not
+ * provide any way to determine whether an address is the only address
+ * in a header.
+ *
+ * Like envelope and header, this test returns true if any combination
+ * of the header-list and key-list arguments match and returns false
+ * otherwise.
+ * "
+ */
+
+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 :contains ["to", "from"] "harry" {
+ test_fail "failed to match address (1)";
+ }
+
+ if not address :contains ["to", "from"] "de.example" {
+ test_fail "failed to match address (2)";
+ }
+
+ if not address :matches "to" "*@*.example.com" {
+ test_fail "failed to match address (3)";
+ }
+
+ if not address :is "to" "harry@de.example.com" {
+ test_fail "failed to match address (4)";
+ }
+
+ /* Must not match */
+ if address :is ["to", "from"] "nonsense@example.com" {
+ test_fail "matches erroneous address";
+ }
+
+ /* Match first key */
+ if not address :contains ["to"] ["nico", "fred", "henk"] {
+ test_fail "failed to match first key";
+ }
+
+ /* Match second key */
+ if not address :contains ["to"] ["fred", "nico", "henk"] {
+ test_fail "failed to match second key";
+ }
+
+ /* Match last key */
+ if not address :contains ["to"] ["fred", "henk", "nico"] {
+ test_fail "failed to match last key";
+ }
+
+ /* First header */
+ if not address :contains ["to", "from"] ["fred", "nico", "henk"] {
+ test_fail "failed to match first header";
+ }
+
+ /* Second header */
+ if not address :contains ["from", "to"] ["fred", "nico", "henk"] {
+ test_fail "failed to match second header";
+ }
+
+ /* Comment */
+ if not address :is "cc" "tss@fi.iki" {
+ test_fail "failed to ignore comment in address";
+ }
+}
+
+/*
+ * TEST: Case-sensitivity
+ */
+
+/* "Internet email addresses [RFC 2822] have the somewhat awkward characteristic
+ * that the local-part to the left of the at-sign is considered case sensitive,
+ * and the domain-part to the right of the at-sign is case insensitive. The
+ * "address" command does not deal with this itself, but provides the
+ * ADDRESS-PART argument for allowing users to deal with it.
+ * "
+ */
+
+test_set "message" text:
+From: stephan@example.com
+To: Nico@nl.example.com, harry@DE.EXAMPLE.COM
+Subject: Case-sensitivity
+
+Test.
+.
+;
+
+
+test "Case-sensitivity" {
+ /* Default: i;ascii-casemap */
+
+ if not address :is ["to", "from"] "nico@nl.example.com" {
+ test_fail "address comparator is i;octet by default (1)";
+ }
+
+ if not address :is ["to", "from"] "harry@de.example.com" {
+ test_fail "address comparator is i;octet by default (2)";
+ }
+
+ if not address :is ["to", "from"] "STEPHAN@example.com" {
+ test_fail "address comparator is i;octet by default (3)";
+ }
+
+ if not address :is :localpart ["to"] "nico" {
+ test_fail "address comparator is i;octet by default (4)";
+ }
+
+ /* Match case-sensitively */
+
+ if not address :is :comparator "i;octet" ["to"] "Nico@nl.example.com" {
+ test_fail "failed to match case-sensitive address (1)";
+ }
+
+ if not address :is :comparator "i;octet" ["to"] "harry@DE.EXAMPLE.COM" {
+ test_fail "failed to match case-sensitive address (2)";
+ }
+
+ if address :is :comparator "i;octet" ["to"] "harry@de.example.com" {
+ test_fail "failed to notice case difference in address with i;octet (1)";
+ }
+
+ if address :is :comparator "i;octet" ["from"] "STEPHAN@example.com" {
+ test_fail "failed to notice case difference in address with i;octet (2)";
+ }
+
+ if not address :is :localpart :comparator "i;octet" ["to"] "Nico" {
+ test_fail "failed to match case-sensitive localpart";
+ }
+
+ if address :is :localpart :comparator "i;octet" ["to"] "nico" {
+ test_fail "failed to notice case difference in local_part with i;octet";
+ }
+
+ if not address :is :domain :comparator "i;octet" ["to"] "DE.EXAMPLE.COM" {
+ test_fail "failed to match case-sensitive localpart";
+ }
+
+ if address :is :domain :comparator "i;octet" ["to"] "de.example.com" {
+ test_fail "failed to notice case difference in domain with i;octet";
+ }
+}
+
+/*
+ * TEST: Phrase part, comments and group names
+ */
+
+/* "The address primitive never acts on the phrase part of an email
+ * address or on comments within that address. It also never acts on
+ * group names, ...
+ * "
+ */
+
+test_set "message" text:
+From: Stephan Bosch <stephan(the author)@example.com>
+To: Nico Thalens <nico@nl.example.com>, Harry Becker <harry@de.example.com>
+cc: tukkers: henk@tukkerland.ex, theo@tukkerland.ex, frits@tukkerland.ex;
+Subject: Frobnitzm
+
+Test.
+.
+;
+
+test "Phrase part, comments and group names" {
+ if address :contains :all :comparator "i;ascii-casemap"
+ ["to","from"] ["Bosch", "Thalens", "Becker"] {
+ test_fail "matched phrase part";
+ }
+
+ if address :contains :all :comparator "i;ascii-casemap" "from" "author" {
+ test_fail "matched comment";
+ }
+
+
+ if address :contains :all :comparator "i;ascii-casemap" ["cc"] ["tukkers"] {
+ test_fail "matched group name";
+ }
+}
+
+
+/*
+ * TEST: Group addresses
+ */
+
+/* "... although it does act on the addresses within the group
+ * construct.
+ * "
+ */
+
+test_set "message" text:
+From: stephan@friep.frop
+To: undisclosed-recipients:;
+cc: tukkers: henk@tukkerland.ex, theo@tukkerland.ex, frits@tukkerland.ex;
+Subject: Invalid addresses
+
+Test.
+.
+;
+
+test "Group addresses" {
+ if not address :is :domain ["cc"] ["tukkerland.ex"] {
+ test_fail "failed to match group address (1)";
+ }
+
+ if not address :is :localpart ["cc"] ["henk"] {
+ test_fail "failed to match group address (2)";
+ }
+
+ if not address :is :localpart ["cc"] ["theo"] {
+ test_fail "failed to match group address (3)";
+ }
+
+ if not address :is :localpart ["cc"] ["frits"] {
+ test_fail "failed to match group address (4)";
+ }
+}
+
+/*
+ * TEST: Address headers
+ */
+
+/* "Implementations MUST restrict the address test to headers that
+ * contain addresses, but MUST include at least From, To, Cc, Bcc,
+ * Sender, Resent-From, and Resent-To, and it SHOULD include any other
+ * header that utilizes an "address-list" structured header body.
+ * "
+ */
+
+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 "from" "stephan@friep.frop" {
+ test_fail "from header not recognized";
+ }
+
+ if not address "to" "henk@tukkerland.ex" {
+ test_fail "to header not recognized";
+ }
+
+ if not address "cc" "ivo@boer.ex" {
+ test_fail "cc header not recognized";
+ }
+
+ if not address "bcc" "joop@hooibaal.ex" {
+ test_fail "bcc header not recognized";
+ }
+
+ if not address "sender" "s.bosch@friep.frop" {
+ test_fail "sender header not recognized";
+ }
+
+ if not address "resent-from" "ivo@boer.ex" {
+ test_fail "resent-from header not recognized";
+ }
+
+ if not address "resent-to" "idioot@dombo.ex" {
+ test_fail "resent-to header not recognized";
+ }
+}
+
+/* ## RFC 5228, Section 2.7.4. Comparisons against Addresses (page 16) ## */
+
+/*
+ * TEST: Invalid addresses
+ */
+
+/*
+ * "If an address is not syntactically valid, then it will not be matched
+ * by tests specifying ":localpart" or ":domain".
+ * "
+ */
+
+test_set "message" text:
+From: stephan@
+To: @example.org
+Cc: nonsense
+Resent-To:
+Bcc: nico@frop.example.com, @example.org
+Resent-Cc:<jürgen@example.com>
+Subject: Invalid addresses
+
+Test.
+.
+;
+
+test "Invalid addresses" {
+ if address :localpart "from" "stephan" {
+ test_fail ":localpart matched invalid address";
+ }
+
+ if address :localpart "resent-cc" "jürgen" {
+ test_fail ":localpart matched invalid UTF-8 address";
+ }
+
+ if address :domain "to" "example.org" {
+ test_fail ":domain matched invalid address";
+ }
+
+ if address :domain "resent-cc" "example.com" {
+ test_fail ":domain matched invalid UTF-8 address";
+ }
+
+ if not address :is :all "resent-to" "" {
+ test_fail ":all failed to match empty address";
+ }
+
+ if not address :is :all "cc" "nonsense" {
+ test_fail ":all failed to match invalid address";
+ }
+
+ if not address :is :all "resent-cc" "<jürgen@example.com>" {
+ test_fail ":all failed to match invalid UTF-8 address";
+ }
+
+ if address :is :localpart "bcc" "" {
+ test_fail ":localpart matched invalid address";
+ }
+
+ if address :is :domain "cc" "example.org" {
+ test_fail ":domain matched invalid address";
+ }
+}
+
+/*
+ * TEST: Default address part
+ */
+
+/* "If an optional address-part is omitted, the default is ":all".
+ * "
+ */
+
+test_set "message" text:
+From: stephan@example.com
+To: nico@nl.example.com, harry@de.example.com
+Subject: Frobnitzm
+
+Test.
+.
+;
+
+test "Default address part" {
+ if not address :is :comparator "i;ascii-casemap" "from" "stephan@example.com"
+ {
+ test_fail "invalid default address part (1)";
+ }
+
+ if not address :is :comparator "i;ascii-casemap" "to"
+ ["harry@de.example.com"] {
+ test_fail "invalid default address part (2)";
+ }
+}
+
+/*
+ * TEST: Mime encoding of '@' in display name
+ */
+
+test_set "message" text:
+From: "Frop <frop@example.org>"
+To: =?UTF-8?B?RnJpZXBAZnJvcA0K?=
+ <friep@example.com>
+Subject: Test
+
+Frop!
+.
+;
+
+
+test "Mime encoding of '@' in display name" {
+ # Relevant sieve rule:
+
+ if not address :is "To"
+ ["friep@example.com"] {
+ test_fail "Invalid address extracted";
+ }
+}
+
+/*
+ * TEST: Erroneous mime encoding
+ */
+
+test_set "message" text:
+From: "William Wallace <william@scotsmen.ex>"
+To: "=?UTF-8?B?IkR1bWIgTWFpbGVyIg==?="
+ <horde@lists.scotsmen.ex>
+Subject: Test
+
+Frop!
+.
+;
+
+
+test "Erroneous mime encoding" {
+ # Relevant sieve rule:
+
+ if not address :is ["To","CC"] ["horde@lists.scotsmen.ex","archers@lists.scotsmen.ex"] {
+ test_fail "Failed to match improperly encoded address headers";
+ }
+}
+
+
diff --git a/pigeonhole/tests/test-allof.svtest b/pigeonhole/tests/test-allof.svtest
new file mode 100644
index 0000000..1ebef67
--- /dev/null
+++ b/pigeonhole/tests/test-allof.svtest
@@ -0,0 +1,446 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * ## RFC 5228, Section 5.2. Test allof (page 27) ##
+ */
+
+/* "The "allof" test performs a logical AND on the tests supplied to it.
+ *
+ * Example: allof (false, false) => false
+ * allof (false, true) => false
+ * allof (true, true) => true
+ *
+ * The allof test takes as its argument a test-list.
+ * "
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: test@dovecot.example.net
+cc: stephan@idiot.ex
+Subject: Test
+
+Test!
+.
+;
+
+/*
+ * TEST: Basic functionality: static
+ */
+
+test "Basic functionality: static" {
+ if allof ( true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong single outcome: false";
+ }
+
+ if allof ( false ) {
+ test_fail "chose wrong single outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( true, true, true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true outcome: false";
+ }
+
+ if allof ( false, false, false ) {
+ test_fail "chose wrong all-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( true, false, false ) {
+ test_fail "chose wrong first-true outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( false, true, false ) {
+ test_fail "chose wrong second-true outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( false, false, true ) {
+ test_fail "chose wrong last-true outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( false, true, true ) {
+ test_fail "chose wrong first-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( true, false, true ) {
+ test_fail "chose wrong second-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( true, true, false ) {
+ test_fail "chose wrong last-false outcome: true";
+ } else {
+ /* Correct */
+ }
+}
+
+/*
+ * TEST: Basic functionality: dynamic
+ */
+
+test "Basic functionality: dynamic" {
+ if allof ( exists "from" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong single outcome: false";
+ }
+
+ if allof ( exists "friep" ) {
+ test_fail "chose wrong single outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "from", exists "to", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true outcome: false";
+ }
+
+ if allof ( exists "friep", exists "frop", exists "frml" ) {
+ test_fail "chose wrong all-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", exists "frop", exists "frml" ) {
+ test_fail "chose wrong first-true outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "from", exists "frml" ) {
+ test_fail "chose wrong second-true outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "frop", exists "cc" ) {
+ test_fail "chose wrong last-true outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "from", exists "cc" ) {
+ test_fail "chose wrong first-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", exists "frop", exists "cc" ) {
+ test_fail "chose wrong second-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", exists "from", exists "frml" ) {
+ test_fail "chose wrong last-false outcome: true";
+ } else {
+ /* Correct */
+ }
+}
+
+/*
+ * TEST: Basic functionality: static/dynamic
+ */
+
+test "Basic functionality: static/dynamic" {
+ /* All true */
+
+ if allof ( true, exists "to", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true first-static outcome: false";
+ }
+
+ if allof ( exists "from", true, exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true second-static outcome: false";
+ }
+
+ if allof ( exists "from", exists "to", true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true third-static outcome: false";
+ }
+
+ /* All false */
+
+ if allof ( false, exists "frop", exists "frml" ) {
+ test_fail "chose wrong all-false first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", false, exists "frml" ) {
+ test_fail "chose wrong all-false second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "frop", false ) {
+ test_fail "chose wrong all-false third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* First true */
+
+ if allof ( true, exists "frop", exists "frml" ) {
+ test_fail "chose wrong first-true first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", false, exists "frml" ) {
+ test_fail "chose wrong first-true second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", exists "frop", false ) {
+ test_fail "chose wrong first-true third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* Second true */
+
+ if allof ( false, exists "from", exists "frml" ) {
+ test_fail "chose wrong second-true first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", true, exists "frml" ) {
+ test_fail "chose wrong second-true second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "from", false ) {
+ test_fail "chose wrong second-true third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* Last true */
+
+ if allof ( false, exists "frop", exists "cc" ) {
+ test_fail "chose wrong last-true first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", false, exists "cc" ) {
+ test_fail "chose wrong last-true second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "frop", true ) {
+ test_fail "chose wrong last-true third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* First false */
+
+ if allof ( false, exists "from", exists "cc" ) {
+ test_fail "chose wrong first-false first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", true, exists "cc" ) {
+ test_fail "chose wrong first-false second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "friep", exists "from", true ) {
+ test_fail "chose wrong first-false third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* Second false */
+
+ if allof ( true, exists "frop", exists "cc" ) {
+ test_fail "chose wrong second-false first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", false, exists "cc" ) {
+ test_fail "chose wrong second-false second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", exists "frop", true ) {
+ test_fail "chose wrong second-false third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* Last false */
+
+ if allof ( true, exists "from", exists "frml" ) {
+ test_fail "chose wrong last-false first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", true, exists "frml" ) {
+ test_fail "chose wrong last-false second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( exists "to", exists "from", false ) {
+ test_fail "chose wrong last-false last-static outcome: true";
+ } else {
+ /* Correct */
+ }
+}
+
+/*
+ * TEST: Basic functionality: nesting
+ */
+
+test "Basic functionality: nesting" {
+ /* Static */
+
+ if allof ( allof(true, true), allof(true, true) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static nesting ((true, true),(true,true)) outcome: false";
+ }
+
+ if allof ( allof(false, true), allof(true, true) ) {
+ test_fail "chose wrong static nesting ((false, true),(true,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(true, false), allof(true, true) ) {
+ test_fail "chose wrong static nesting ((true,false),(true,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(true, true), allof(false, true) ) {
+ test_fail "chose wrong static nesting ((true, true),(false,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(true, true), allof(true, false) ) {
+ test_fail "chose wrong static nesting ((true, true),(true,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(true, false), allof(true, false) ) {
+ test_fail "chose wrong static nesting ((true, false),(true,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* Dynamic */
+
+ if allof ( allof(exists "to", exists "from"), allof(exists "cc", exists "subject") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((true, true),(true,true)) outcome: false";
+ }
+
+ if allof ( allof(exists "frop", exists "from"), allof(exists "cc", exists "subject") ) {
+ test_fail "chose wrong dynamic nesting ((false, true),(true,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", exists "friep"), allof(exists "cc", exists "subject") ) {
+ test_fail "chose wrong dynamic nesting ((true,false),(true,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", exists "from"), allof(exists "frml", exists "subject") ) {
+ test_fail "chose wrong dynamic nesting ((true, true),(false,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", exists "from"), allof(exists "cc", exists "fruts") ) {
+ test_fail "chose wrong dynamic nesting ((true, true),(true,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", exists "friep"), allof(exists "cc", exists "fruts") ) {
+ test_fail "chose wrong dynamic nesting ((true, false),(true,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* Static/Dynamic */
+
+ if allof ( allof(exists "to", true), allof(true, exists "subject") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static/dynamic nesting ((true, true),(true,true)) outcome: false";
+ }
+
+ if allof ( allof(false, exists "from"), allof(exists "cc", exists "subject") ) {
+ test_fail "chose wrong static/dynamic nesting ((false, true),(true,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", false), allof(exists "cc", exists "subject") ) {
+ test_fail "chose wrong static/dynamic nesting ((true,false),(true,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", exists "from"), allof(false, exists "subject") ) {
+ test_fail "chose wrong static/dynamic nesting ((true, true),(false,true)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", exists "from"), allof(exists "cc", false) ) {
+ test_fail "chose wrong static/dynamic nesting ((true, true),(true,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if allof ( allof(exists "to", false), allof(true, exists "fruts") ) {
+ test_fail "chose wrong static/dynamic nesting ((true, false),(true,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+}
+
+
diff --git a/pigeonhole/tests/test-anyof.svtest b/pigeonhole/tests/test-anyof.svtest
new file mode 100644
index 0000000..77a9c79
--- /dev/null
+++ b/pigeonhole/tests/test-anyof.svtest
@@ -0,0 +1,445 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * ## RFC 5228, Section 5.3. Test anyof (page 27) ##
+ */
+
+/* "The "anyof" test performs a logical OR on the tests supplied to it.
+ *
+ * Example: anyof (false, false) => false
+ * anyof (false, true) => true
+ * anyof (true, true) => true
+ * "
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: test@dovecot.example.net
+cc: stephan@idiot.ex
+Subject: Test
+
+Test!
+.
+;
+
+/*
+ * TEST: Basic functionality: static
+ */
+
+test "Basic functionality: static" {
+ if anyof ( true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong single outcome: false";
+ }
+
+ if anyof ( false ) {
+ test_fail "chose wrong single outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( true, true, true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true outcome: false";
+ }
+
+ if anyof ( false, false, false ) {
+ test_fail "chose wrong all-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( true, false, false ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-true outcome: false";
+ }
+
+ if anyof ( false, true, false ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-true outcome: false";
+ }
+
+ if anyof ( false, false, true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-true outcome: false";
+ }
+
+ if anyof ( false, true, true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-false outcome: false";
+ }
+
+ if anyof ( true, false, true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-false outcome: false";
+ }
+
+ if anyof ( true, true, false ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-false outcome: false";
+ }
+}
+
+/*
+ * TEST: Basic functionality: dynamic
+ */
+
+test "Basic functionality: dynamic" {
+ if anyof ( exists "from" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong single outcome: false";
+ }
+
+ if anyof ( exists "friep" ) {
+ test_fail "chose wrong single outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( exists "from", exists "to", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "frop", exists "frml" ) {
+ test_fail "chose wrong all-false outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( exists "to", exists "frop", exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-true outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "from", exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-true outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "frop", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-true outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "from", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-false outcome: false";
+ }
+
+ if anyof ( exists "to", exists "frop", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-false outcome: false";
+ }
+
+ if anyof ( exists "to", exists "from", exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-false outcome: false";
+ }
+}
+
+/*
+ * TEST: Basic functionality: static/dynamic
+ */
+
+test "Basic functionality: static/dynamic" {
+ /* All true */
+
+ if anyof ( true, exists "to", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true first-static outcome: false";
+ }
+
+ if anyof ( exists "from", true, exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true second-static outcome: false";
+ }
+
+ if anyof ( exists "from", exists "to", true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong all-true third-static outcome: false";
+ }
+
+ /* All false */
+
+ if anyof ( false, exists "frop", exists "frml" ) {
+ test_fail "chose wrong all-false first-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( exists "friep", false, exists "frml" ) {
+ test_fail "chose wrong all-false second-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( exists "friep", exists "frop", false ) {
+ test_fail "chose wrong all-false third-static outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ /* First true */
+
+ if anyof ( true, exists "frop", exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-true first-static outcome: false";
+ }
+
+ if anyof ( exists "to", false, exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-true second-static outcome: false";
+ }
+
+ if anyof ( exists "to", exists "frop", false ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-true third-static outcome: false";
+ }
+
+ /* Second true */
+
+ if anyof ( false, exists "from", exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-true first-static outcome: false";
+ }
+
+ if anyof ( exists "friep", true, exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-true second-static outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "from", false ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-true third-static outcome: false";
+ }
+
+ /* Last true */
+
+ if anyof ( false, exists "frop", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-true first-static outcome: false";
+ }
+
+ if anyof ( exists "friep", false, exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-true second-static outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "frop", true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-true third-static outcome: false";
+ }
+
+ /* First false */
+
+ if anyof ( false, exists "from", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-false first-static outcome: false";
+ }
+
+ if anyof ( exists "friep", true, exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-false second-static outcome: false";
+ }
+
+ if anyof ( exists "friep", exists "from", true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong first-false third-static outcome: false";
+ }
+
+ /* Second false */
+
+ if anyof ( true, exists "frop", exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-false first-static outcome: false";
+ }
+
+ if anyof ( exists "to", false, exists "cc" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-false second-static outcome: false";
+ }
+
+ if anyof ( exists "to", exists "frop", true ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong second-false third-static outcome: false";
+ }
+
+ /* Third false */
+
+ if anyof ( true, exists "from", exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-false first-static outcome: false";
+ }
+
+ if anyof ( exists "to", true, exists "frml" ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-false second-static outcome: false";
+ }
+
+ if anyof ( exists "to", exists "from", false ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong last-false third-static outcome: false";
+ }
+}
+
+/*
+ * TEST: Basic functionality: nesting
+ */
+
+test "Basic functionality: nesting" {
+ /* Static */
+
+ if anyof ( anyof(false, false), anyof(false, false) ) {
+ test_fail "chose wrong static nesting ((false, false),(false,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( anyof(true, false), anyof(false, false) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static nesting ((true, false),(false,false)) outcome: false";
+ }
+
+ if anyof ( anyof(false, true), anyof(false, false) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static nesting ((false, true),(false,false)) outcome: false";
+ }
+
+ if anyof ( anyof(false, false), anyof(true, false) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static nesting ((false, false),(true,false)) outcome: false";
+ }
+
+ if anyof ( anyof(false, false), anyof(false, true) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static nesting ((false, false),(false,true)) outcome: false";
+ }
+
+ if anyof ( anyof(true, false), anyof(false, true) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static nesting ((true, false),(false,true)) outcome: false";
+ }
+
+ /* Dynamic */
+
+ if anyof ( anyof(exists "frop", exists "friep"), anyof(exists "frml", exists "fruts") ) {
+ test_fail "chose wrong dynamic nesting ((false, false),(false,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( anyof(exists "to", exists "friep"), anyof(exists "frml", exists "fruts") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((true, false),(false,false)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "frop", exists "from"), anyof(exists "frml", exists "fruts") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((false, true),(false,false)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "frop", exists "friep"), anyof(exists "cc", exists "fruts") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((false, false),(true,false)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "frop", exists "friep"), anyof(exists "frml", exists "subject") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((false, false),(false,true)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "to", exists "friep"), anyof(exists "frml", exists "subject") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((true, false),(false,true)) outcome: false";
+ }
+
+ /* Static/Dynamic */
+
+ if anyof ( anyof(false, exists "friep"), anyof(exists "frml", exists "fruts") ) {
+ test_fail "chose wrong static/dynamic nesting ((false, false),(false,false)) outcome: true";
+ } else {
+ /* Correct */
+ }
+
+ if anyof ( anyof(exists "to", false), anyof(exists "frml", exists "fruts") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static/dynamic nesting ((true, false),(false,false)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "frop", exists "from"), anyof(false, exists "fruts") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static/dynamic nesting ((false, true),(false,false)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "frop", exists "friep"), anyof(exists "cc", false) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static/dynamic nesting ((false, false),(true,false)) outcome: false";
+ }
+
+ if anyof ( anyof(exists "frop", exists "friep"), anyof(exists "frml", true) ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong static/dynamic nesting ((false, false),(false,true)) outcome: false";
+ }
+
+ if anyof ( anyof(true, exists "friep"), anyof(false, exists "subject") ) {
+ /* Correct */
+ } else {
+ test_fail "chose wrong dynamic nesting ((true, false),(false,true)) outcome: false";
+ }
+
+}
+
+
+
diff --git a/pigeonhole/tests/test-exists.svtest b/pigeonhole/tests/test-exists.svtest
new file mode 100644
index 0000000..8c4c2fc
--- /dev/null
+++ b/pigeonhole/tests/test-exists.svtest
@@ -0,0 +1,93 @@
+require "vnd.dovecot.testsuite";
+
+/* "The "exists" test is true if the headers listed in the header-names
+ * argument exist within the message. All of the headers must exist or
+ * the test is false.
+ * "
+ */
+
+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!
+.
+;
+
+/*
+ * TEST: One header
+ */
+
+test "One header" {
+ if not exists "from" {
+ test_fail "exists test missed from header";
+ }
+
+ if exists "x-nonsense" {
+ test_fail "exists test found non-existent header";
+ }
+}
+
+/*
+ * TEST: Two headers
+ */
+
+test "Two headers" {
+ if not exists ["from","to"] {
+ test_fail "exists test missed from or to header";
+ }
+
+ if exists ["from","x-nonsense"] {
+ test_fail "exists test found non-existent header (1)";
+ }
+
+ if exists ["x-nonsense","to"] {
+ test_fail "exists test found non-existent header (2)";
+ }
+
+ if exists ["x-nonsense","x-nonsense2"] {
+ test_fail "exists test found non-existent header (3)";
+ }
+}
+
+/*
+ * TEST: Three headers
+ */
+
+test "Three headers" {
+ if not exists ["Subject","date","resent-to"] {
+ test_fail "exists test missed subject, date or resent-to header";
+ }
+
+ if exists ["x-nonsense","date","resent-to"] {
+ test_fail "exists test found non-existent header (1)";
+ }
+
+ if exists ["subject", "x-nonsense","resent-to"] {
+ test_fail "exists test found non-existent header (2)";
+ }
+
+ if exists ["subject","date","x-nonsense"] {
+ test_fail "exists test found non-existent header (3)";
+ }
+
+ if exists ["subject", "x-nonsense","x-nonsense2"] {
+ test_fail "exists test found non-existent header (4)";
+ }
+
+ if exists ["x-nonsense","date","x-nonsense2"] {
+ test_fail "exists test found non-existent header (5)";
+ }
+
+ if exists ["x-nonsense","x-nonsense2","resent-to"] {
+ test_fail "exists test found non-existent header (6)";
+ }
+
+ if exists ["x-nonsense","x-nonsense2","x-nonsense3"] {
+ test_fail "exists test found non-existent header (7)";
+ }
+}
diff --git a/pigeonhole/tests/test-header.svtest b/pigeonhole/tests/test-header.svtest
new file mode 100644
index 0000000..138fb82
--- /dev/null
+++ b/pigeonhole/tests/test-header.svtest
@@ -0,0 +1,280 @@
+require "vnd.dovecot.testsuite";
+require "variables";
+
+/*
+ * ## RFC 5228, Section 5.7. Test header (page 29) ##
+ */
+
+/*
+ * TEST: Basic functionality
+ */
+
+/* "The "header" test evaluates to true if the value of any of the named
+ * headers, ignoring leading and trailing whitespace, matches any key.
+ * The type of match is specified by the optional match argument, which
+ * defaults to ":is" if not specified, as specified in section 2.6.
+ *
+ * Like address and envelope, this test returns true if any combination
+ * of the header-names list and key-list arguments match and returns
+ * false otherwise.
+ * "
+ */
+
+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 :contains ["Subject", "Comments"] "Frobnitzm" {
+ test_fail "failed to match header (1)";
+ }
+
+ if not header :contains ["Subject", "Comments"] "nonsense" {
+ test_fail "failed to match header(2)";
+ }
+
+ if not header :matches "Keywords" "*, strange, *" {
+ test_fail "failed to match header (3)";
+ }
+
+ if not header :is "Comments" "This is nonsense." {
+ test_fail "failed to match header (4)";
+ }
+
+ /* Must not match */
+ if header ["subject", "comments", "keywords"] "idiotic" {
+ test_fail "matched nonsense";
+ }
+
+ /* Match first key */
+ if not header :contains ["keywords"] ["strange", "snot", "vreemd"] {
+ test_fail "failed to match first key";
+ }
+
+ /* Match second key */
+ if not header :contains ["keywords"] ["raar", "strange", "vreemd"] {
+ test_fail "failed to match second key";
+ }
+
+ /* Match last key */
+ if not header :contains ["keywords"] ["raar", "snot", "strange"] {
+ test_fail "failed to match last key";
+ }
+
+ /* First header */
+ if not header :contains ["keywords", "subject"]
+ ["raar", "strange", "vreemd"] {
+ test_fail "failed to match first header";
+ }
+
+ /* Second header */
+ if not header :contains ["subject", "keywords"]
+ ["raar", "strange", "vreemd"] {
+ test_fail "failed to match second header";
+ }
+}
+
+/*
+ * TEST: Matching empty key
+ */
+
+/* "If a header listed in the header-names argument exists, it contains
+ * the empty key (""). However, if the named header is not present, it
+ * does not match any key, including the empty key. So if a message
+ * contained the header
+ *
+ * X-Caffeine: C8H10N4O2
+ *
+ * these tests on that header evaluate as follows:
+ *
+ * header :is ["X-Caffeine"] [""] => false
+ * header :contains ["X-Caffeine"] [""] => true
+ * "
+ */
+
+
+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 :is "X-Caffeine" "" {
+ test_fail ":is-matched non-empty header with empty string";
+ }
+
+ if not header :contains "X-Caffeine" "" {
+ test_fail "failed to match existing header with empty string";
+ }
+
+ if not header :is "comments" "" {
+ test_fail "failed to match empty header with empty string";
+ }
+
+ if header :contains "X-Nonsense" "" {
+ test_fail ":contains-matched non-existent header with empty string";
+ }
+}
+
+/*
+ * TEST: Ignoring whitespace
+ */
+
+/* "The "header" test evaluates to true if the value of any of the named
+ * headers, ignoring leading and trailing whitespace, matches any key.
+ * ...
+ * "
+ */
+
+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 :is "x-a" "Text" {
+ if header :matches "x-a" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header test does not strip leading whitespace (header=`${header}`)";
+ }
+
+ if not header :is "x-b" "Text" {
+ if header :matches "x-b" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header test does not strip trailing whitespace (header=`${header}`)";
+ }
+
+ if not header :is "subject" "Help" {
+ if header :matches "subject" "*" {
+ set "header" "${1}";
+ }
+ test_fail "header test does not strip both leading and trailing whitespace (header=`${header}`)";
+ }
+}
+
+/*
+ * TEST: Absent or empty header
+ */
+
+/* "Testing whether a given header is either absent or doesn't contain
+ * any non-whitespace characters can be done using a negated "header"
+ * test:
+ *
+ * not header :matches "Cc" "?*"
+ * "
+ */
+
+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 :matches "Cc" "?*" {
+ test_fail "CC header is not absent or empty";
+ }
+
+ if header :matches "Subject" "?*" {
+ test_fail "Subject header is empty, but matched otherwise";
+ }
+
+ if header :matches "Comment" "?*" {
+ test_fail "Comment header is empty, but matched otherwise";
+ }
+}
+
+/*
+ * ## RFC 5228, Section 2.4.2.2. Headers (page 9)
+ */
+
+/*
+ * TEST: Invalid header name
+ */
+
+/* "A header name never contains a colon. The "From" header refers to a
+ * line beginning "From:" (or "From :", etc.). No header will match
+ * the string "From:" due to the trailing colon.
+ *
+ * Similarly, no header will match a syntactically invalid header name.
+ * An implementation MUST NOT cause an error for syntactically invalid
+ * header names in tests.
+ */
+
+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 :contains "subject:" "" {
+ test_fail "matched invalid header name";
+ }
+
+ if header :contains "to!" "" {
+ test_fail "matched invalid header name";
+ }
+}
+
+/*
+ * TEST: Folded headers
+ */
+
+/* "Header lines are unfolded as described in [RFC 2822] section 2.2.3.
+ * ...
+ * "
+ */
+
+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 header" {
+ if not header :is "x-multiline"
+ "This is a multi-line header body, which should be unfolded correctly." {
+ test_fail "failed to properly unfold folded header.";
+ }
+}
diff --git a/pigeonhole/tests/test-size.svtest b/pigeonhole/tests/test-size.svtest
new file mode 100644
index 0000000..dd5cdc4
--- /dev/null
+++ b/pigeonhole/tests/test-size.svtest
@@ -0,0 +1,74 @@
+require "vnd.dovecot.testsuite";
+
+/*
+ * ## RFC 5228, Section 5.9. Test size (page 29) ##
+ */
+
+/*
+ * TEST: Basic functionality
+ */
+
+/* "The "size" test deals with the size of a message. It takes either a
+ * tagged argument of ":over" or ":under", followed by a number
+ * representing the size of the message.
+ *
+ * If the argument is ":over", and the size of the message is greater
+ * than the number provided, the test is true; otherwise, it is false.
+
+ * If the argument is ":under", and the size of the message is less than
+ * the number provided, the test is true; otherwise, it is false.
+ * "
+ */
+
+test_set "message" text:
+From: stephan@example.org
+To: nico@frop.example.com
+Subject: Help
+X-A: Text
+X-B: Text
+X-Multiline: This is a multi-line
+ header body, which should be
+ unfolded correctly.
+
+Text
+
+.
+;
+
+test "Basic functionality" {
+ if not size :under 1000 {
+ test_fail "size test produced unexpected result (1)";
+ }
+
+ if size :under 10 {
+ test_fail "size test produced unexpected result (2)";
+ }
+
+ if not size :over 10 {
+ test_fail "size test produced unexpected result (3)";
+ }
+
+ if size :over 1000 {
+ test_fail "size test produced unexpected result (4)";
+ }
+}
+
+/*
+ * TEST: Exact size
+ */
+
+/* "Note that for a message that is exactly 4,000 octets, the message is
+ * neither ":over" nor ":under" 4000 octets.
+ * "
+ */
+
+test "Exact size" {
+ if size :under 221 {
+ test_fail "size :under matched exact limit";
+ }
+
+ if size :over 221 {
+ test_fail "size :over matched exact limit";
+ }
+}
+
diff --git a/pigeonhole/tests/testsuite.svtest b/pigeonhole/tests/testsuite.svtest
new file mode 100644
index 0000000..349ba89
--- /dev/null
+++ b/pigeonhole/tests/testsuite.svtest
@@ -0,0 +1,75 @@
+require "vnd.dovecot.testsuite";
+require "envelope";
+
+/* Test message environment */
+
+test "Message Environment" {
+ test_set "message" text:
+From: sirius@example.org
+To: nico@frop.example.com
+Subject: Frop!
+
+Frop!
+.
+ ;
+
+ if not header :contains "from" "example.org" {
+ test_fail "message data not set properly.";
+ }
+
+ test_set "message" text:
+From: nico@frop.example.com
+To: stephan@nl.example.com
+Subject: Friep!
+
+Friep!
+.
+ ;
+
+ if not header :is "from" "nico@frop.example.com" {
+ test_fail "message data not set properly.";
+ }
+
+ keep;
+}
+
+/* Test envelope environment */
+
+test "Envelope Environment" {
+ test_set "envelope.from" "stephan@hutsefluts.example.net";
+
+ if not envelope :is "from" "stephan@hutsefluts.example.net" {
+ test_fail "envelope.from data not set properly (1).";
+ }
+
+ test_set "envelope.to" "news@example.org";
+
+ if not envelope :is "to" "news@example.org" {
+ test_fail "envelope.to data not set properly (1).";
+ }
+
+ test_set "envelope.auth" "sirius";
+
+ if not envelope :is "auth" "sirius" {
+ test_fail "envelope.auth data not set properly (1).";
+ }
+
+ test_set "envelope.from" "stephan@example.org";
+
+ if not envelope :is "from" "stephan@example.org" {
+ test_fail "envelope.from data not reset properly (2).";
+ }
+
+ test_set "envelope.to" "past-news@example.org";
+
+ if not envelope :is "to" "past-news@example.org" {
+ test_fail "envelope.to data not reset properly (2).";
+ }
+
+ test_set "envelope.auth" "zilla";
+
+ if not envelope :is "auth" "zilla" {
+ test_fail "envelope.auth data not reset properly (2).";
+ }
+}
+