From 2a541e80f573529d76c4fc026989a39a9dc64879 Mon Sep 17 00:00:00 2001
From: Stephan Bosch <stephan@rename-it.nl>
Date: Wed, 30 Dec 2009 00:16:30 +0100
Subject: [PATCH] Built basic implementation of the spamtest, spamtestplus and
 virustest extensions (unfinished).

---
 TODO                                          |   4 +-
 configure.in                                  |   1 +
 doc/rfc/spamvirustest.rfc5235.txt             | 731 ++++++++++++++++++
 src/lib-sieve/Makefile.am                     |   3 +-
 src/lib-sieve/plugins/Makefile.am             |   2 +-
 .../plugins/spamvirustest/Makefile.am         |  19 +
 .../spamvirustest/ext-spamvirustest-common.c  | 487 ++++++++++++
 .../spamvirustest/ext-spamvirustest-common.h  |  37 +
 .../plugins/spamvirustest/ext-spamvirustest.c | 154 ++++
 .../plugins/spamvirustest/tst-spamvirustest.c | 311 ++++++++
 src/lib-sieve/sieve-extensions.c              |   6 +
 src/lib-sieve/sieve-extensions.h              |   4 +-
 12 files changed, 1755 insertions(+), 4 deletions(-)
 create mode 100644 doc/rfc/spamvirustest.rfc5235.txt
 create mode 100644 src/lib-sieve/plugins/spamvirustest/Makefile.am
 create mode 100644 src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c
 create mode 100644 src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.h
 create mode 100644 src/lib-sieve/plugins/spamvirustest/ext-spamvirustest.c
 create mode 100644 src/lib-sieve/plugins/spamvirustest/tst-spamvirustest.c

diff --git a/TODO b/TODO
index 0a2b37242..2c6b0ac88 100644
--- a/TODO
+++ b/TODO
@@ -4,7 +4,9 @@ Current activities:
 	- Implement proper :content "multipart" behavior
 	- Implement proper :content "message/rfc822" behavior
 	- Build test cases for decoding MIME encodings to UTF-8
-* Finish ereject extension
+* Unfinished new extensions:
+	- Finish the spamtest and virustest extensions
+	- Finish the ereject extension
 * Build a sieve tool to filter an entire existing mailbox through a Sieve 
   script:
 	- Add commandline options to fully customize execution
diff --git a/configure.in b/configure.in
index dff464f74..a6cd1750d 100644
--- a/configure.in
+++ b/configure.in
@@ -140,6 +140,7 @@ src/lib-sieve/plugins/notify/Makefile
 src/lib-sieve/plugins/environment/Makefile
 src/lib-sieve/plugins/mailbox/Makefile
 src/lib-sieve/plugins/date/Makefile
+src/lib-sieve/plugins/spamvirustest/Makefile
 src/lib-sieve-tool/Makefile
 src/plugins/Makefile
 src/plugins/lda-sieve/Makefile
diff --git a/doc/rfc/spamvirustest.rfc5235.txt b/doc/rfc/spamvirustest.rfc5235.txt
new file mode 100644
index 000000000..bfa65b81f
--- /dev/null
+++ b/doc/rfc/spamvirustest.rfc5235.txt
@@ -0,0 +1,731 @@
+
+
+
+
+
+
+Network Working Group                                           C. Daboo
+Request for Comments: 5235                                  January 2008
+Obsoletes: 3685
+Category: Standards Track
+
+
+        Sieve Email Filtering: Spamtest and Virustest Extensions
+
+Status of This Memo
+
+   This document specifies an Internet standards track protocol for the
+   Internet community, and requests discussion and suggestions for
+   improvements.  Please refer to the current edition of the "Internet
+   Official Protocol Standards" (STD 1) for the standardization state
+   and status of this protocol.  Distribution of this memo is unlimited.
+
+Abstract
+
+   The Sieve email filtering language "spamtest", "spamtestplus", and
+   "virustest" extensions permit users to use simple, portable commands
+   for spam and virus tests on email messages.  Each extension provides
+   a new test using matches against numeric "scores".  It is the
+   responsibility of the underlying Sieve implementation to do the
+   actual checks that result in proper input to the tests.
+
+Table of Contents
+
+   1. Introduction and Overview .......................................2
+   2. Conventions Used in This Document ...............................2
+   3. Sieve Extensions ................................................3
+      3.1. General Considerations .....................................3
+      3.2. Test spamtest ..............................................3
+           3.2.1. spamtest without :percent Argument ..................4
+           3.2.2. spamtest with :percent Argument .....................5
+      3.3. Test virustest .............................................7
+   4. Security Considerations .........................................9
+   5. IANA Considerations .............................................9
+      5.1. spamtest Registration ......................................9
+      5.2. virustest Registration ....................................10
+      5.3. spamtestplus Registration .................................10
+   6. References .....................................................10
+      6.1. Normative References ......................................10
+      6.2. Informative References ....................................11
+   Appendix A. Acknowledgments .......................................12
+   Appendix B. Important Changes since RFC 3685 ......................12
+
+
+
+
+
+
+Daboo                       Standards Track                     [Page 1]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+1.  Introduction and Overview
+
+   Sieve scripts are frequently being used to do spam and virus
+   filtering either based on implicit script tests (e.g., tests for
+   "black-listed" senders directly encoded in the Sieve script), or via
+   testing messages modified by some external spam or virus checker that
+   handled the message prior to Sieve.  The use of third-party spam and
+   virus checker tools poses a problem since each tool has its own way
+   of indicating the result of its checks.  These usually take the form
+   of a header added to the message, the content of which indicates the
+   status using some syntax defined by the particular tool.  Each user
+   has to then create their own Sieve scripts to match the contents of
+   these headers to do filtering.  This requires the script to stay in
+   synchronization with the third-party tool as it gets updated or
+   perhaps replaced with another.  Thus, scripts become tied to specific
+   environments and lose portability.
+
+   The purpose of this document is to introduce two Sieve tests that can
+   be used to implement "generic" tests for spam and viruses in messages
+   processed via Sieve scripts.  The spam and virus checks themselves
+   are handled by the underlying Sieve implementation in whatever manner
+   is appropriate, so that the Sieve spam and virus test commands can be
+   used in a portable way.
+
+   In order to do numeric comparisons against the returned strings,
+   server implementations MUST also support the Sieve relational
+   [RFC5231] extension, in addition to the extensions described here.
+   All examples below assume the relational extension is present.
+
+2.  Conventions Used in This Document
+
+   Conventions for notations are as in [RFC5228] Section 1.1.
+
+   The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
+   "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
+   document are to be interpreted as described in [RFC2119].
+
+   The term "spam" is used in this document to refer to unsolicited or
+   unwanted email messages.  This document does not attempt to define
+   what exactly constitutes spam, or how it should be identified, or
+   what actions should be taken when detected.
+
+   The term "virus" is used in this document to refer to any type of
+   message whose content can cause malicious damage.  This document does
+   not attempt to define what exactly constitutes a virus, or how it
+   should be identified, or what actions should be taken when detected.
+
+
+
+
+
+Daboo                       Standards Track                     [Page 2]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+3.  Sieve Extensions
+
+3.1.  General Considerations
+
+   The "spamtest" and "virustest" tests described below evaluate the
+   results of implementation-specific spam and virus checks in a
+   portable way.  The implementation may, for example, check for third-
+   party spam tool headers and determine how those map into the way the
+   test commands are used.  To do this, the underlying Sieve
+   implementation provides a normalized result string as one of the
+   inputs to each test command.  The normalized result string is
+   considered to be the value on the left-hand side of the test, and the
+   comparison values given in the test command are considered to be on
+   the right-hand side.
+
+   The normalized result starts with a digit string, with its numeric
+   value within the range of values used by the specific test,
+   indicating the severity of spam or viruses in a message or whether
+   any tests were done at all.  This may optionally be followed by a
+   space (%x20) character and arbitrary text, or in one specific case a
+   single keyword is returned.  The numeric value can be compared to
+   specific values using the Sieve relational [RFC5231] extension in
+   conjunction with the "i;ascii-numeric" comparator [RFC4790], which
+   will test for the presence of a numeric value at the start of the
+   string, ignoring any additional text in the string.  The optional
+   text can be used to carry implementation-specific details about the
+   tests and descriptive comments about the result.  Tests can be done
+   using standard string comparators against this text if it helps to
+   refine behavior; however, this will break portability of the script
+   as the text will likely be specific to a particular implementation.
+
+   In addition, the Sieve relational [RFC5231] ":count" match type can
+   be used to determine if the underlying implementation actually did a
+   test.  If the underlying spam or virus test was done, the ":count" of
+   the normalized result will return the numeric value "1", whilst if
+   the test was not done, or the Sieve implementation could not
+   determine if a test was done or not done, the ":count" value will be
+   "0" (zero).
+
+3.2.  Test spamtest
+
+           Usage:    spamtest [":percent"] [COMPARATOR] [MATCH-TYPE]
+                     <value: string>
+
+   Sieve implementations that implement the "spamtest" test use an
+   identifier of either "spamtest" or "spamtestplus" for use with the
+   capability mechanism.
+
+
+
+
+Daboo                       Standards Track                     [Page 3]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+   If the ":percent" argument is not used with any spamtest test, then
+   one or both of "spamtest" or "spamtestplus" capability identifiers
+   MUST be present.
+
+   If the ":percent" argument is used with any spamtest test, then the
+   "spamtestplus" capability identifier MUST be present.  Sieve
+   implementations MUST return an error if the ":percent" argument is
+   used and "spamtestplus" is not specified.
+
+   In the interests of brevity and clarity, scripts SHOULD NOT specify
+   both "spamtestplus" and "spamtest" capability identifiers together.
+
+   The "spamtest" test evaluates to true if the normalized spamtest
+   result matches the value.  The type of match is specified by the
+   optional match argument, which defaults to ":is" if not specified.
+
+3.2.1.  spamtest without :percent Argument
+
+   When the ":percent" argument is not present in the "spamtest" test,
+   the normalized result string provided for the left-hand side of the
+   test starts with a numeric value in the range "0" (zero) through
+   "10", with meanings summarized below:
+
+   +----------+--------------------------------------------------------+
+   | spamtest | interpretation                                         |
+   | value    |                                                        |
+   +----------+--------------------------------------------------------+
+   | 0        | message was not tested for spam, or Sieve could not    |
+   |          | determine whether any test was done                    |
+   |          |                                                        |
+   | 1        | message was tested and is clear of spam                |
+   |          |                                                        |
+   | 2 - 9    | message was tested and may contain spam; a higher      |
+   |          | number indicates a greater likelihood of spam          |
+   |          |                                                        |
+   | 10       | message was tested and definitely contains spam        |
+   +----------+--------------------------------------------------------+
+
+   The underlying Sieve implementation will map whatever spam check is
+   done into this numeric range, as appropriate.
+
+   Examples:
+
+           require ["spamtest", "fileinto", "relational", "comparator-
+                    i;ascii-numeric"];
+
+
+
+
+
+
+Daboo                       Standards Track                     [Page 4]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+           if spamtest :value "eq" :comparator "i;ascii-numeric" "0"
+           {
+               fileinto "INBOX.unclassified";
+           }
+           elsif spamtest :value "ge" :comparator "i;ascii-numeric" "3"
+           {
+               fileinto "INBOX.spam-trap";
+           }
+
+   In this example, any message that has not passed through a spam check
+   tool will be filed into the mailbox "INBOX.unclassified".  Any
+   message with a normalized result value greater than or equal to "3"
+   is filed into a mailbox called "INBOX.spam-trap" in the user's
+   mailstore.
+
+3.2.2.  spamtest with :percent Argument
+
+   When the ":percent" argument is present in the "spamtest" test, the
+   normalized result string provided for the left-hand side of the test
+   starts with a numeric value in the range "0" (zero) through "100",
+   with meanings summarized below:
+
+   +----------+-------------------------------------------------------+
+   | spamtest | interpretation                                        |
+   | value    |                                                       |
+   +----------+-------------------------------------------------------+
+   | 0        | message was tested and is clear of spam, or was not   |
+   |          | tested for spam, or Sieve could not determine whether |
+   |          | any test was done                                     |
+   |          |                                                       |
+   | 1 - 99   | message was tested and may contain spam; a higher     |
+   |          | percentage indicates a greater likelihood of spam     |
+   |          |                                                       |
+   | 100      | message was tested and definitely contains spam       |
+   +----------+-------------------------------------------------------+
+
+   The underlying Sieve implementation will map whatever spam check is
+   done into the numeric range, as appropriate.
+
+   To determine whether or not the message was tested for spam, two
+   options can be used:
+
+   a.  a test with or without the ":percent" argument and ":count" match
+       type, testing for the value "0" as described in Section 3.1.
+
+   b.  a test without the ":percent" argument using the ":value" match
+       type, testing for the normalized result value "0" as described in
+       Section 3.2.1.
+
+
+
+Daboo                       Standards Track                     [Page 5]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+   Examples:
+
+           require ["spamtestplus", "fileinto", "relational",
+                    "comparator-i;ascii-numeric"];
+
+           if spamtest :value "eq"
+                       :comparator "i;ascii-numeric" "0"
+           {
+               fileinto "INBOX.unclassified";
+           }
+           elsif spamtest :percent :value "eq"
+                          :comparator "i;ascii-numeric" "0"
+           {
+               fileinto "INBOX.not-spam";
+           }
+           elsif spamtest :percent :value "lt"
+                          :comparator "i;ascii-numeric" "37"
+           {
+               fileinto "INBOX.spam-trap";
+           }
+           else
+           {
+               discard;
+           }
+
+   In this example, any message that has not passed through a spam check
+   tool will be filed into the mailbox "INBOX.unclassified".  Any
+   message that is classified as definitely not containing spam
+   (normalized result value "0") will be filed into the mailbox
+   "INBOX.not-spam".  Any message with a normalized result value less
+   than "37" is filed into a mailbox called "INBOX.spam-trap" in the
+   user's mailstore.  Any other normalized result value will result in
+   the message being discarded.
+
+   Alternatively, the Sieve relational [RFC5231] ":count" match type can
+   be used:
+
+   Examples:
+
+           if spamtest :percent :count "eq"
+                       :comparator "i;ascii-numeric" "0"
+           {
+               fileinto "INBOX.unclassified";
+           }
+
+
+
+
+
+
+
+Daboo                       Standards Track                     [Page 6]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+           elsif spamtest :percent :value "eq"
+                          :comparator "i;ascii-numeric" "0"
+           {
+               fileinto "INBOX.not-spam";
+           }
+           elsif spamtest :percent :value "lt"
+                          :comparator "i;ascii-numeric" "37"
+           {
+               fileinto "INBOX.spam-trap";
+           }
+           else
+           {
+               discard;
+           }
+
+   This example will result in exactly the same behavior as the previous
+   one.
+
+3.3.  Test virustest
+
+           Usage:    virustest [COMPARATOR] [MATCH-TYPE]
+                     <value: string>
+
+   Sieve implementations that implement the "virustest" test have an
+   identifier of "virustest" for use with the capability mechanism.
+
+   The "virustest" test evaluates to true if the normalized result
+   string matches the value.  The type of match is specified by the
+   optional match argument, which defaults to ":is" if not specified.
+
+   The normalized result string provided for the left side of the test
+   starts with a numeric value in the range "0" (zero) through "5", with
+   meanings summarized below:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                       Standards Track                     [Page 7]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+   +-----------+-------------------------------------------------------+
+   | virustest | interpretation                                        |
+   | value     |                                                       |
+   +-----------+-------------------------------------------------------+
+   | 0         | message was not tested for viruses, or Sieve could    |
+   |           | not determine whether any test was done               |
+   |           |                                                       |
+   | 1         | message was tested and contains no known viruses      |
+   |           |                                                       |
+   | 2         | message was tested and contained a known virus that   |
+   |           | was replaced with harmless content                    |
+   |           |                                                       |
+   | 3         | message was tested and contained a known virus that   |
+   |           | was "cured" such that it is now harmless              |
+   |           |                                                       |
+   | 4         | message was tested and possibly contains a known      |
+   |           | virus                                                 |
+   |           |                                                       |
+   | 5         | message was tested and definitely contains a known    |
+   |           | virus                                                 |
+   +-----------+-------------------------------------------------------+
+
+   The underlying Sieve implementation will map whatever virus checks
+   are done into this numeric range, as appropriate.  If the message has
+   not been categorized by any virus checking tools, then the virustest
+   result is "0".
+
+   Example:
+
+           require ["virustest", "fileinto", "relational", "comparator-
+                    i;ascii-numeric"];
+
+           if virustest :value "eq" :comparator "i;ascii-numeric" "0"
+           {
+               fileinto "INBOX.unclassified";
+           }
+           if virustest :value "eq" :comparator "i;ascii-numeric" "4"
+           {
+               fileinto "INBOX.quarantine";
+           }
+           elsif virustest :value "eq" :comparator "i;ascii-numeric" "5"
+           {
+               discard;
+           }
+
+   In this example, any message that has not passed through a virus
+   check tool will be filed into the mailbox "INBOX.unclassified".  Any
+   message with a normalized result value equal to "4" is filed into a
+
+
+
+Daboo                       Standards Track                     [Page 8]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+   mailbox called "INBOX.quarantine" in the user's mailstore.  Any
+   message with a normalized result value equal to "5" is discarded
+   (removed) and not delivered to the user's mailstore.
+
+4.  Security Considerations
+
+   Sieve implementations SHOULD ensure that "spamtest" and "virustest"
+   tests only report spam and virus test results for messages that
+   actually have gone through a legitimate spam or virus check process.
+   In particular, if such checks rely on the addition and subsequent
+   checking of private header fields, it is the responsibility of the
+   implementation to ensure that such headers cannot be spoofed by the
+   sender or intermediary and thereby prevent the implementation from
+   being tricked into returning the wrong result for the test.
+
+   Server administrators must ensure that the virus checking tools are
+   kept up to date, to provide reasonable protection for users using the
+   "virustest" test.  Users should be made aware of the fact that the
+   "virustest" test does not provide a 100% reliable way to remove all
+   viruses, and they should continue to exercise caution when dealing
+   with messages of unknown content and origin.
+
+   Beyond that, the "spamtest" and "virustest" extensions do not raise
+   any security considerations that are not present in the base
+   [RFC5228] protocol, and these issues are discussed in [RFC5228].
+
+5.  IANA Considerations
+
+   The following templates specify the IANA registration of the Sieve
+   extensions specified in this document.  The registrations for
+   "spamtest" and "virustest" replace those from [RFC3685]:
+
+5.1.  spamtest Registration
+
+      To: iana@iana.org
+      Subject: Registration of new Sieve extension
+
+      Capability name: spamtest
+      Description:     Provides a test to check for varying likelihood of
+                       an email message being spam.
+      RFC number:      RFC 5235
+      Contact address: The Sieve discussion list <ietf-mta-filters@imc.org>
+
+   This information has been added to the list of Sieve extensions given
+   on http://www.iana.org/assignments/sieve-extensions.
+
+
+
+
+
+
+Daboo                       Standards Track                     [Page 9]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+5.2.  virustest Registration
+
+      To: iana@iana.org
+      Subject: Registration of new Sieve extension
+
+      Capability name: virustest
+      Description:     Provides a test to check for varying likelihood of
+                       there being malicious content in an email message.
+      RFC number:      RFC 5235
+      Contact address: The Sieve discussion list <ietf-mta-filters@imc.org>
+
+   This information has been added to the list of Sieve extensions given
+   on http://www.iana.org/assignments/sieve-extensions.
+
+5.3.  spamtestplus Registration
+
+      To: iana@iana.org
+      Subject: Registration of new Sieve extension
+
+      Capability name: spamtestplus
+      Description:     Provides a test to check for varying likelihood of
+                       an email message being spam, possibly using a
+                       percentage range.
+      RFC number:      RFC 5235
+      Contact address: The Sieve discussion list <ietf-mta-filters@imc.org>
+
+   This information has been added to the list of Sieve extensions given
+   on http://www.iana.org/assignments/sieve-extensions.
+
+6.  References
+
+6.1.  Normative References
+
+   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
+              Requirement Levels", BCP 14, RFC 2119, March 1997.
+
+   [RFC4790]  Newman, C., Duerst, M., and A. Gulbrandsen, "Internet
+              Application Protocol Collation Registry", RFC 4790, March
+              2007.
+
+   [RFC5228]  Guenther, P., Ed., and T. Showalter, Ed., "Sieve: An Email
+              Filtering Language", RFC 5228, January 2008.
+
+   [RFC5231]  Segmuller, W. and B. Leiba, "Sieve Email Filtering:
+              Relational Extension", RFC 5231, January 2008.
+
+
+
+
+
+
+Daboo                       Standards Track                    [Page 10]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+6.2.  Informative References
+
+   [RFC3685]  Daboo, C., "SIEVE Email Filtering: Spamtest and VirusTest
+              Extensions", RFC 3685, February 2004.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                       Standards Track                    [Page 11]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+Appendix A.  Acknowledgments
+
+   Thanks to Mark E. Mallett, Tony Hansen, Jutta Degener, Ned Freed,
+   Ashish Gawarikar, Alexey Melnikov, Nigel Swinson, and Aaron Stone for
+   comments and corrections.
+
+Appendix B.  Important Changes since RFC 3685
+
+   Listed below are some of the major changes from the previous
+   specification [RFC3685], which this one supersedes.
+
+   1. A ":percent" argument has been added to the "spamtest" test adding
+      a new 0-100 numerical range for test results.
+
+   2. A "spamtestplus" requires item has been added to indicate the
+      presence of this extension in scripts.
+
+   3. The "count" match type from [RFC5231] can now be used to determine
+      whether or not a message was tested.
+
+   4. Clarified that "test not done" also means "Sieve system could not
+      determine if a test was done".
+
+Author's Address
+
+   Cyrus Daboo
+
+   EMail: cyrus@daboo.name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                       Standards Track                    [Page 12]
+
+RFC 5235        Sieve: Spamtest and Virustest Extensions    January 2008
+
+
+Full Copyright Statement
+
+   Copyright (C) The IETF Trust (2008).
+
+   This document is subject to the rights, licenses and restrictions
+   contained in BCP 78, and except as set forth therein, the authors
+   retain all their rights.
+
+   This document and the information contained herein are provided on an
+   "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS
+   OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY, THE IETF TRUST AND
+   THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS
+   OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF
+   THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED
+   WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
+
+Intellectual Property
+
+   The IETF takes no position regarding the validity or scope of any
+   Intellectual Property Rights or other rights that might be claimed to
+   pertain to the implementation or use of the technology described in
+   this document or the extent to which any license under such rights
+   might or might not be available; nor does it represent that it has
+   made any independent effort to identify any such rights.  Information
+   on the procedures with respect to rights in RFC documents can be
+   found in BCP 78 and BCP 79.
+
+   Copies of IPR disclosures made to the IETF Secretariat and any
+   assurances of licenses to be made available, or the result of an
+   attempt made to obtain a general license or permission for the use of
+   such proprietary rights by implementers or users of this
+   specification can be obtained from the IETF on-line IPR repository at
+   http://www.ietf.org/ipr.
+
+   The IETF invites any interested party to bring to its attention any
+   copyrights, patents or patent applications, or other proprietary
+   rights that may cover technology that may be required to implement
+   this standard.  Please address the information to the IETF at
+   ietf-ipr@ietf.org.
+
+
+
+
+
+
+
+
+
+
+
+
+Daboo                       Standards Track                    [Page 13]
+
diff --git a/src/lib-sieve/Makefile.am b/src/lib-sieve/Makefile.am
index 9bffcf1eb..f105c6a23 100644
--- a/src/lib-sieve/Makefile.am
+++ b/src/lib-sieve/Makefile.am
@@ -44,7 +44,8 @@ comparators = \
 	cmp-i-ascii-casemap.c
 
 if BUILD_UNFINISHED
-unfinished_plugins =
+unfinished_plugins = \
+	./plugins/spamvirustest/libsieve_ext_spamvirustest.la
 endif
 
 # These are not actual plugins just yet...
diff --git a/src/lib-sieve/plugins/Makefile.am b/src/lib-sieve/plugins/Makefile.am
index 1ce185c97..f936d0579 100644
--- a/src/lib-sieve/plugins/Makefile.am
+++ b/src/lib-sieve/plugins/Makefile.am
@@ -1,5 +1,5 @@
 if BUILD_UNFINISHED
-UNFINISHED =
+UNFINISHED = spamvirustest
 endif
 
 SUBDIRS = \
diff --git a/src/lib-sieve/plugins/spamvirustest/Makefile.am b/src/lib-sieve/plugins/spamvirustest/Makefile.am
new file mode 100644
index 000000000..30b6e20ba
--- /dev/null
+++ b/src/lib-sieve/plugins/spamvirustest/Makefile.am
@@ -0,0 +1,19 @@
+noinst_LTLIBRARIES = libsieve_ext_spamvirustest.la
+
+AM_CPPFLAGS = \
+	-I../../ \
+	-I$(dovecot_incdir) \
+	-I$(dovecot_incdir)/src/lib \
+	-I$(dovecot_incdir)/src/lib-mail \
+	-I$(dovecot_incdir)/src/lib-storage 
+
+tests = \
+	tst-spamvirustest.c
+
+libsieve_ext_spamvirustest_la_SOURCES = \
+	$(tests) \
+	ext-spamvirustest-common.c \
+	ext-spamvirustest.c
+
+noinst_HEADERS = \
+	ext-spamvirustest-common.h
diff --git a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c
new file mode 100644
index 000000000..d0183fed8
--- /dev/null
+++ b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c
@@ -0,0 +1,487 @@
+/* Copyright (c) 2002-2009 Dovecot Sieve authors, see the included COPYING file
+ */
+
+#include "lib.h"
+
+#include "sieve-common.h"
+#include "sieve-settings.h"
+#include "sieve-error.h"
+#include "sieve-extensions.h"
+#include "sieve-interpreter.h"
+
+#include "ext-spamvirustest-common.h"
+
+#include <sys/types.h>
+#include <regex.h>
+#include <ctype.h>
+
+/*
+ * Extension data
+ */
+
+enum ext_spamvirustest_status_type {
+	EXT_SPAMVIRUSTEST_STATUS_TYPE_VALUE,
+	EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN,
+	EXT_SPAMVIRUSTEST_STATUS_TYPE_YESNO,
+};
+
+struct ext_spamvirustest_header_spec {
+	const char *header_name;
+	regex_t regexp;
+	bool regexp_match;
+};
+
+struct ext_spamvirustest_data {
+	struct ext_spamvirustest_header_spec status_header;
+	struct ext_spamvirustest_header_spec max_header;
+
+	enum ext_spamvirustest_status_type status_type;
+
+	float max_value;
+	const char *yes_string;
+};
+
+/*
+ * Regexp utility
+ */
+
+static bool _regexp_compile
+(regex_t *regexp, const char *data, const char **error_r)
+{
+	size_t errsize;
+	int ret;
+
+	*error_r = "";
+
+	if ( (ret=regcomp(regexp, data, REG_EXTENDED)) == 0 ) {
+		return TRUE;
+	}
+
+	errsize = regerror(ret, regexp, NULL, 0); 
+
+	if ( errsize > 0 ) {
+		char *errbuf = t_malloc(errsize);
+
+		(void)regerror(ret, regexp, errbuf, errsize);
+	 
+		/* We don't want the error to start with a capital letter */
+		errbuf[0] = i_tolower(errbuf[0]);
+
+		*error_r = errbuf;
+	}
+
+	return FALSE;
+}
+
+static const char *_regexp_match_get_value
+(const char *string, int index, regmatch_t pmatch[], int nmatch)
+{
+	if ( index > -1 && index < nmatch && pmatch[index].rm_so != -1 ) {
+		return t_strndup(string + pmatch[index].rm_so, 
+						pmatch[index].rm_eo - pmatch[index].rm_so);
+	}
+	return NULL;
+}
+
+/*
+ * Configuration parser
+ */
+
+static bool ext_spamvirustest_parse_header_spec
+(pool_t pool, const char *data, struct ext_spamvirustest_header_spec *spec,
+	const char **error_r)
+{
+	const char *p;
+	const char *regexp_error;
+
+	if ( *data == '\0' ) {
+		*error_r = "empty header specification";
+		return FALSE;
+	}
+
+	/* Parse header name */
+
+	p = data;
+
+	while ( *p == ' ' || *p == '\t' ) p++;
+	while ( *p != ':' && *p != '\0' && *p != ' ' && *p != '\t' ) p++;
+
+	if ( *p == '\0' ) {
+		spec->header_name = p_strdup(pool, data);
+		return TRUE;
+	}
+
+	spec->header_name = p_strdup_until(pool, data, p);
+	while ( *p == ' ' || *p == '\t' ) p++;
+
+	if ( p == '\0' ) {
+		spec->regexp_match = FALSE;
+		return TRUE;
+	}
+
+	/* Parse and compile regular expression */
+
+	if ( *p != ':' ) {
+		*error_r = t_strdup_printf("expecting ':', but found '%c'", *p);
+		return FALSE;
+	}
+	p++;
+
+	spec->regexp_match = TRUE;
+	if ( !_regexp_compile(&spec->regexp, p, &regexp_error) ) {
+		*error_r = t_strdup_printf("failed to compile regular expression '%s': " 
+			"%s", p, regexp_error);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static bool ext_spamvirustest_parse_strlen_value
+(const char *str_value, float *value_r, const char **error_r)
+{
+	const char *p = str_value;
+	char ch = *p;
+
+	if ( *str_value == '\0' ) {
+		*error_r = "empty value";		
+		return FALSE;
+	}
+
+	while ( *p == ch ) p++;
+
+	if ( *p != '\0' ) {
+		*error_r = t_strdup_printf(
+			"different character '%c' encountered in strlen value",
+			*p);
+		return FALSE;
+	}
+
+	*value_r = ( p - str_value );
+
+	return TRUE;
+}
+
+static bool ext_spamvirustest_parse_decimal_value
+(const char *str_value, float *value_r, const char **error_r)
+{
+	const char *p = str_value;
+	float value;
+	float sign = 1;
+	int digits;
+
+	if ( *p == '\0' ) {
+		*error_r = "empty value";		
+		return FALSE;
+	}
+
+	if ( *p == '+' || *p == '-' ) {
+		if ( *p == '-' )
+			sign = -1;
+		
+		p++;
+	}
+
+	value = 0;
+	digits = 0;
+	while ( i_isdigit(*p) ) {
+		value = value*10 + (*p-'0');
+		if ( digits++ > 4 ) {
+			*error_r = t_strdup_printf
+				("decimal value has too many digits before radix point: %s", 
+					str_value);
+			return FALSE;	
+		}
+		p++;
+	}
+
+	if ( *p == '.' || *p == ',' ) {
+		float radix = .1;
+		p++;
+
+		digits = 0;
+		while ( i_isdigit(*p) ) {
+			value = value + (*p-'0')*radix;
+
+			if ( digits++ > 4 ) {
+				*error_r = t_strdup_printf
+					("decimal value has too many digits after radix point: %s", 
+						str_value);
+				return FALSE;
+			}
+			radix /= 10;
+			p++;
+		}
+	}
+
+	if ( *p != '\0' ) {
+		*error_r = t_strdup_printf
+			("invalid decimal point value: %s", str_value);
+		return FALSE;
+	}
+
+	*value_r = value * sign;
+
+	return TRUE;
+}
+
+/*
+ * Extension initialization
+ */
+
+bool ext_spamvirustest_load(const struct sieve_extension *ext, void **context)
+{
+	struct sieve_instance *svinst = ext->svinst;
+	struct ext_spamvirustest_data *ext_data;
+	const char *status_header, *max_header, *status_type, *max_value;
+	const char *ext_name;
+	const char *error;
+
+	/* FIXME: 
+	 *   Prevent loading of both spamtest and spamtestplus: let these share 
+	 *   contexts.
+	 */
+
+	if ( sieve_extension_is(ext, spamtest_extension) || 
+		sieve_extension_is(ext, spamtestplus_extension) ) {
+		ext_name = spamtest_extension.name;
+	} else {
+		ext_name = sieve_extension_name(ext);
+	}
+
+	/* Get settings */
+
+	status_header = sieve_setting_get
+		(svinst, t_strconcat("sieve_", ext_name, "_status_header", NULL));
+	max_header = sieve_setting_get
+		(svinst, t_strconcat("sieve_", ext_name, "_max_header", NULL));
+	status_type = sieve_setting_get
+		(svinst, t_strconcat("sieve_", ext_name, "_status_type", NULL));
+	max_value = sieve_setting_get
+		(svinst, t_strconcat("sieve_", ext_name, "_max_value", NULL));
+
+	/* Verify settings */
+
+	if ( status_header == NULL ) {
+		return TRUE;
+	}
+
+	if ( max_header != NULL && max_value != NULL ) {
+		sieve_sys_warning("%s: sieve_%s_max_header and sieve_%s_max_value "
+			"cannot both be configured", ext_name, ext_name, ext_name);
+		return TRUE;
+	}
+
+	if ( max_header == NULL && max_value == NULL ) {
+		sieve_sys_warning("%s: none of sieve_%s_max_header or sieve_%s_max_value "
+			"is configured", ext_name, ext_name, ext_name);
+		return TRUE;
+	}
+
+	/* Pre-process configuration */
+
+	ext_data = p_new(svinst->pool, struct ext_spamvirustest_data, 1);
+
+	if ( !ext_spamvirustest_parse_header_spec
+		(svinst->pool, status_header, &ext_data->status_header, &error) ) {
+		sieve_sys_warning("%s: invalid status header specification "
+			"'%s': %s", ext_name, status_header, error);
+		return TRUE;
+	}
+
+	if ( max_header != NULL && !ext_spamvirustest_parse_header_spec
+		(svinst->pool, max_header, &ext_data->max_header, &error) ) {
+		sieve_sys_warning("%s: invalid max header specification "
+			"'%s': %s", ext_name, max_header, error);
+		return TRUE;
+	}
+
+	if ( status_type == NULL || strcmp(status_type, "value") == 0 ) {
+		ext_data->status_type = EXT_SPAMVIRUSTEST_STATUS_TYPE_VALUE;
+	} else if ( strcmp(status_type, "strlen") == 0 ) {
+		ext_data->status_type = EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN;
+	} else if ( strcmp(status_type, "yesno") == 0 ) {
+		ext_data->status_type = EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN;
+	}
+
+	if ( max_value != NULL ) {
+		switch ( ext_data->status_type ) {
+		case EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN:
+		case EXT_SPAMVIRUSTEST_STATUS_TYPE_VALUE:	
+			if ( !ext_spamvirustest_parse_decimal_value
+				(max_value, &ext_data->max_value, &error) ) {
+				sieve_sys_warning("%s: invalid max value specification "
+					"'%s': %s", ext_name, max_value, error);
+				return TRUE;
+			}
+			break;
+		case EXT_SPAMVIRUSTEST_STATUS_TYPE_YESNO:
+			ext_data->yes_string = p_strdup(svinst->pool, max_value);
+			ext_data->max_value = 1;
+			break;
+		}	
+	} 
+
+	*context = (void *) ext_data;
+	return TRUE;
+}
+
+/*
+ * Score extraction
+ */
+
+const char *ext_spamvirustest_get_value
+(const struct sieve_runtime_env *renv, const struct sieve_extension *ext,
+	 bool percent)
+{
+	static const char *VALUE_FAILED = "0";
+	struct ext_spamvirustest_data *ext_data = 
+		(struct ext_spamvirustest_data *) ext->context;	
+	struct ext_spamvirustest_header_spec *status_header, *max_header;
+	const struct sieve_message_data *msgdata = renv->msgdata;
+	const char *ext_name = sieve_extension_name(ext);
+	regmatch_t match_values[2];
+	const char *header_value, *error;
+	const char *status = NULL, *max = NULL, *yes = NULL;
+	float status_value, max_value;
+	int value;
+
+	/* 
+	 * Check whether extension is properly configured 
+	 */
+	if ( ext_data == NULL ) {
+		sieve_runtime_trace(renv, "%s: extension not configured", ext_name);
+		return VALUE_FAILED;
+	}
+
+	status_header = &ext_data->status_header;
+	max_header = &ext_data->max_header;
+
+	/*
+	 * Get max status value
+	 */	
+
+	if ( max_header->header_name != NULL ) {
+		/* Get header from message */
+		if ( mail_get_first_header_utf8
+			(msgdata->mail, max_header->header_name, &header_value) < 0 ||
+			header_value == NULL ) {
+			sieve_runtime_trace(renv, "%s: header '%s' not found in message", 
+				ext_name, max_header->header_name);
+			return VALUE_FAILED;
+		}
+
+		if ( max_header->regexp_match ) {
+			/* Execute regex */
+			if ( regexec(&max_header->regexp, header_value, 2, match_values, 0) 
+				!= 0 ) {
+				sieve_runtime_trace(renv, "%s: regexp for header '%s' did not match "
+					"on value '%s'", ext_name, max_header->header_name, header_value);
+				return VALUE_FAILED;
+			}
+
+			max = _regexp_match_get_value(header_value, 1, match_values, 2);
+			if ( max == NULL ) {
+				sieve_runtime_trace(renv, "%s: regexp did not return match value "
+					"for string '%s'", ext_name, header_value);
+				return VALUE_FAILED;
+			}
+		} else {
+			max = header_value;
+		}
+
+		switch ( ext_data->status_type ) {
+		case EXT_SPAMVIRUSTEST_STATUS_TYPE_VALUE:	
+		case EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN:
+			if ( !ext_spamvirustest_parse_decimal_value(max, &max_value, &error) ) {
+				sieve_runtime_trace(renv, "%s: failed to parse maximum value: %s", 
+					ext_name, error);
+				return VALUE_FAILED;
+			}
+			break;
+		case EXT_SPAMVIRUSTEST_STATUS_TYPE_YESNO:
+			yes = max;
+			max_value = 1;
+			break;
+		}	
+	} else {
+		yes = ext_data->yes_string;
+		max_value = ext_data->max_value;
+	}
+
+	if ( max_value == 0 ) {
+		sieve_runtime_trace(renv, "%s: max value is 0", ext_name);
+		return VALUE_FAILED;
+	}
+
+	/*
+	 * Get status value
+	 */
+
+	/* Get header from message */
+	if ( mail_get_first_header_utf8
+		(msgdata->mail, status_header->header_name, &header_value) < 0 ||
+		header_value == NULL ) {
+		sieve_runtime_trace(renv, "%s: header '%s' not found in message", 
+			ext_name, status_header->header_name);
+		return VALUE_FAILED;
+	}
+
+	/* Execute regex */
+	if ( status_header->regexp_match ) {
+		if ( regexec(&status_header->regexp, header_value, 2, match_values, 0) 
+			!= 0 ) {
+			sieve_runtime_trace(renv, "%s: regexp for header '%s' did not match "
+				"on value '%s'", ext_name, status_header->header_name, header_value);
+			return VALUE_FAILED;
+		}
+
+		status = _regexp_match_get_value(header_value, 1, match_values, 2);
+		if ( status == NULL ) {
+			sieve_runtime_trace(renv, "%s: regexp did not return match value "
+				"for string '%s'", ext_name, header_value);
+			return VALUE_FAILED;
+		}
+	} else {
+		status = header_value;
+	}
+
+	switch ( ext_data->status_type ) {
+	case EXT_SPAMVIRUSTEST_STATUS_TYPE_VALUE:
+		if ( !ext_spamvirustest_parse_decimal_value(status, &status_value, &error) 
+			) {
+			sieve_runtime_trace(renv, "%s: failed to parse status value '%s': %s", 
+				ext_name, status, error);
+			return VALUE_FAILED;
+		}
+		break;
+	case EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN:
+		if ( !ext_spamvirustest_parse_strlen_value(status, &status_value, &error) 
+			) {
+			sieve_runtime_trace(renv, "%s: failed to parse status value '%s': %s", 
+				ext_name, status, error);
+			return VALUE_FAILED;
+		}
+		break;
+	case EXT_SPAMVIRUSTEST_STATUS_TYPE_YESNO:
+		if ( strcmp(status, yes) == 0 )
+			status_value = 1;
+		else
+			status_value = 0;
+		break;
+	}
+	
+	/* Calculate value */
+	if ( status_value < 0 ) {
+		value = 1;
+	} else {
+		if ( percent )
+			value = (status_value / max_value) * 99 + 1;
+		else
+			value = (status_value / max_value) * 9 + 1;
+	}
+
+	return t_strdup_printf("%d", value);;
+}
+
+
diff --git a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.h b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.h
new file mode 100644
index 000000000..a46ef066d
--- /dev/null
+++ b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.h
@@ -0,0 +1,37 @@
+/* Copyright (c) 2002-2009 Dovecot Sieve authors, see the included COPYING file
+ */
+ 
+#ifndef __EXT_SPAMVIRUSTEST_COMMON_H
+#define __EXT_SPAMVIRUSTEST_COMMON_H
+
+#include "sieve-common.h"
+
+/*
+ * Extensions
+ */
+ 
+extern const struct sieve_extension_def spamtest_extension;
+extern const struct sieve_extension_def spamtestplus_extension;
+extern const struct sieve_extension_def virustest_extension;
+
+bool ext_spamvirustest_load(const struct sieve_extension *ext, void **context);
+
+/* 
+ * Tests
+ */
+
+extern const struct sieve_command_def spamtest_test;
+extern const struct sieve_command_def virustest_test;
+ 
+const char *ext_spamvirustest_get_value
+(const struct sieve_runtime_env *renv, const struct sieve_extension *ext,
+	 bool percent);
+
+/*
+ * Operations
+ */
+
+extern const struct sieve_operation_def spamtest_operation;
+extern const struct sieve_operation_def virustest_operation;
+
+#endif /* __EXT_SPAMVIRUSTEST_COMMON_H */
diff --git a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest.c b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest.c
new file mode 100644
index 000000000..019a4894a
--- /dev/null
+++ b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest.c
@@ -0,0 +1,154 @@
+/* Copyright (c) 2002-2009 Dovecot Sieve authors, see the included COPYING file
+ */
+
+/* Extensions spamtest, spamtestplus and virustest
+ * -----------------------------------------------
+ *
+ * Authors: Stephan Bosch
+ * Specification: RFC 5235
+ * Implementation: unfinished
+ * Status: experimental
+ *
+ */
+
+/* Configuration examples:
+ *
+ * # 1: X-Spam-Score: No, score=-3.2
+ *
+ * sieve_spamtest_status_header = \
+ *   X-Spam-Score: [[:alnum:]]+, score=(-?[[:digit:]]+\.[[:digit:]])
+ * sieve_spamtest_max_value = 5.0
+ * 
+ * # 2: X-Spam-Status: Yes
+ *
+ * sieve_spamtest_status_header = X-Spam-Status
+ * sieve_spamtest_status_type = yesno
+ * sieve_spamtest_max_value = Yes
+ *
+ * # 3: X-Spam-Score: sssssss
+ * sieve_spamtest_status_header = X-Spam-Score
+ * sieve_spamtest_status_type = strlen
+ * sieve_spamtest_max_value = 5
+ *
+ * # 4: X-Spam-Score: status=3.2 required=5.0
+ *
+ * sieve_spamtest_status_header = \
+ *   X-Spam-Score: score=(-?[[:digit:]]+\.[[:digit:]]).* 
+ * sieve_spamtest_max_header = \
+ *   X-Spam-Score: score=-?[[:digit:]]+\.[[:digit:]] required=([[:digit:]]+\.[[:digit:]]) 
+ *
+ * # 5: X-Virus-Scan: Clean
+ *
+ * sieve_virustest_header = X-Virus-Scan
+ * sieve_virustest_values = Clean:Cleaned:Cured:Possible:Detected
+ */
+
+/* TODO: 
+ *   - Spamtest/Spamtestplus configuration needs testing
+ *   - Virustest configuration is currently not present
+ *   - Testsuite tests
+ */
+ 
+#include "lib.h"
+#include "array.h"
+
+#include "sieve-common.h"
+
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+
+#include "sieve-validator.h"
+
+#include "ext-spamvirustest-common.h"
+
+/* 
+ * Extensions 
+ */
+
+/* Spamtest */
+
+static bool ext_spamvirustest_validator_load
+(const struct sieve_extension *ext, struct sieve_validator *validator);
+
+const struct sieve_extension_def spamtest_extension = { 
+	"spamtest", 
+	ext_spamvirustest_load, 
+	NULL,
+	ext_spamvirustest_validator_load, 
+	NULL, NULL, NULL, NULL, NULL,
+	SIEVE_EXT_DEFINE_OPERATION(spamtest_operation), 
+	SIEVE_EXT_DEFINE_NO_OPERANDS
+};
+
+const struct sieve_extension_def spamtestplus_extension = { 
+	"spamtestplus", 
+	ext_spamvirustest_load,  
+	NULL,
+	ext_spamvirustest_validator_load, 
+	NULL, NULL, NULL, NULL, NULL,
+	SIEVE_EXT_DEFINE_OPERATION(spamtest_operation), 
+	SIEVE_EXT_DEFINE_NO_OPERANDS
+};
+
+const struct sieve_extension_def virustest_extension = { 
+	"virustest", 
+	ext_spamvirustest_load, 
+	NULL,
+	ext_spamvirustest_validator_load, 
+	NULL, NULL, NULL, NULL, NULL,
+	SIEVE_EXT_DEFINE_OPERATION(virustest_operation), 
+	SIEVE_EXT_DEFINE_NO_OPERANDS
+};
+
+/*
+ * Implementation
+ */
+
+static bool ext_spamtest_validator_extension_validate
+	(const struct sieve_extension *ext, struct sieve_validator *valdtr, 
+		void *context, struct sieve_ast_argument *require_arg);
+
+const struct sieve_validator_extension spamtest_validator_extension = {
+	&spamtest_extension,
+	ext_spamtest_validator_extension_validate,
+	NULL
+};
+
+static bool ext_spamvirustest_validator_load
+(const struct sieve_extension *ext, struct sieve_validator *valdtr)
+{
+	/* Register new test */
+
+	if ( sieve_extension_is(ext, virustest_extension) ) {
+		sieve_validator_register_command(valdtr, ext, &virustest_test);
+	} else {
+		if ( sieve_extension_is(ext, spamtest_extension) ) {
+			/* Register validator extension to warn for duplicate */
+			sieve_validator_extension_register
+				(valdtr, ext, &spamtest_validator_extension, NULL);
+		}
+
+		sieve_validator_register_command(valdtr, ext, &spamtest_test);
+	}
+
+	return TRUE;
+}
+
+static bool ext_spamtest_validator_extension_validate
+(const struct sieve_extension *ext, struct sieve_validator *valdtr, 
+	void *context ATTR_UNUSED, struct sieve_ast_argument *require_arg)
+{
+	const struct sieve_extension *ext_spamtestplus =
+		sieve_extension_get_by_name(ext->svinst, "spamtestplus");
+
+	if ( ext_spamtestplus != NULL &&
+		sieve_validator_extension_loaded(valdtr, ext_spamtestplus) ) {
+		sieve_argument_validate_warning(valdtr, require_arg,
+			"the spamtest and spamtestplus extensions should not be specified "
+			"at the same time");
+	}
+
+	return TRUE;
+}
+
+
diff --git a/src/lib-sieve/plugins/spamvirustest/tst-spamvirustest.c b/src/lib-sieve/plugins/spamvirustest/tst-spamvirustest.c
new file mode 100644
index 000000000..1ba2778b9
--- /dev/null
+++ b/src/lib-sieve/plugins/spamvirustest/tst-spamvirustest.c
@@ -0,0 +1,311 @@
+/* Copyright (c) 2002-2009 Dovecot Sieve authors, see the included COPYING file
+ */
+
+#include "lib.h"
+
+#include "sieve-common.h"
+#include "sieve-extensions.h"
+#include "sieve-commands.h"
+#include "sieve-code.h"
+#include "sieve-comparators.h"
+#include "sieve-match-types.h"
+#include "sieve-address-parts.h"
+#include "sieve-validator.h"
+#include "sieve-generator.h"
+#include "sieve-interpreter.h"
+#include "sieve-dump.h"
+#include "sieve-match.h"
+
+#include "ext-spamvirustest-common.h"
+
+/*
+ * Tests
+ */
+
+static bool tst_spamvirustest_validate
+	(struct sieve_validator *valdtr, struct sieve_command *tst);
+static bool tst_spamvirustest_generate
+	(const struct sieve_codegen_env *cgenv, struct sieve_command *ctx);
+static bool tst_spamvirustest_registered
+	(struct sieve_validator *valdtr, const struct sieve_extension *ext,
+		struct sieve_command_registration *cmd_reg);
+ 
+/* Spamtest test
+ *
+ * Syntax:
+ *   spamtest [":percent"] [COMPARATOR] [MATCH-TYPE] <value: string> 
+ */
+
+const struct sieve_command_def spamtest_test = { 
+	"spamtest", 
+	SCT_TEST, 
+	1, 0, FALSE, FALSE,
+	tst_spamvirustest_registered,
+	NULL, 
+	tst_spamvirustest_validate, 
+	tst_spamvirustest_generate, 
+	NULL 
+};
+
+/* Virustest test
+ *
+ * Syntax:
+ *   virustest [COMPARATOR] [MATCH-TYPE] <value: string> 
+ */
+
+const struct sieve_command_def virustest_test = { 
+	"virustest", 
+	SCT_TEST, 
+	1, 0, FALSE, FALSE,
+	tst_spamvirustest_registered,
+	NULL, 
+	tst_spamvirustest_validate, 
+	tst_spamvirustest_generate, 
+	NULL 
+};
+
+/* 
+ * Tagged arguments 
+ */
+
+static bool tst_spamtest_validate_percent_tag
+	(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, 
+		struct sieve_command *tst);
+
+static const struct sieve_argument_def spamtest_percent_tag = {
+ 	"percent",
+	NULL, 
+	tst_spamtest_validate_percent_tag, 
+	NULL, NULL, NULL
+};
+
+/* 
+ * Spamtest and virustest operations 
+ */
+
+static bool tst_spamvirustest_operation_dump
+	(const struct sieve_dumptime_env *denv, sieve_size_t *address);
+static int tst_spamvirustest_operation_execute
+	(const struct sieve_runtime_env *renv, sieve_size_t *address);
+
+const struct sieve_operation_def spamtest_operation = { 
+	"SPAMTEST",
+	&spamtest_extension,
+	0,
+	tst_spamvirustest_operation_dump, 
+	tst_spamvirustest_operation_execute 
+};
+
+const struct sieve_operation_def virustest_operation = { 
+	"VIRUSTEST",
+	&virustest_extension,
+	0,
+	tst_spamvirustest_operation_dump, 
+	tst_spamvirustest_operation_execute 
+};
+
+
+/*
+ * Optional operands
+ */
+
+enum tst_spamvirustest_optional {
+	OPT_SPAMTEST_PERCENT = SIEVE_MATCH_OPT_LAST,
+	OPT_SPAMTEST_LAST
+};
+
+/* 
+ * Test registration 
+ */
+
+static bool tst_spamvirustest_registered
+(struct sieve_validator *valdtr, const struct sieve_extension *ext,
+	struct sieve_command_registration *cmd_reg) 
+{
+	sieve_comparators_link_tag(valdtr, cmd_reg, SIEVE_MATCH_OPT_COMPARATOR);
+	sieve_match_types_link_tags(valdtr, cmd_reg, SIEVE_MATCH_OPT_MATCH_TYPE);
+
+	if ( sieve_extension_is(ext, spamtestplus_extension) || 
+		sieve_extension_is(ext, spamtest_extension) ) {
+		sieve_validator_register_tag
+			(valdtr, cmd_reg, ext, &spamtest_percent_tag, OPT_SPAMTEST_PERCENT);
+	}
+
+	return TRUE;
+}
+
+/* 
+ * Validation 
+ */
+
+static bool tst_spamtest_validate_percent_tag
+(struct sieve_validator *valdtr, struct sieve_ast_argument **arg, 
+	struct sieve_command *tst)
+{
+	if ( !sieve_extension_is(tst->ext, spamtestplus_extension) ) {	
+		sieve_argument_validate_error(valdtr, *arg,
+			"the spamtest test only accepts the :percent argument when "
+			"the spamtestplus extension is active"); 
+		return FALSE; 
+	}
+
+	/* Skip tag */
+	*arg = sieve_ast_argument_next(*arg);
+
+	return TRUE;
+}
+
+static bool tst_spamvirustest_validate
+(struct sieve_validator *valdtr, struct sieve_command *tst) 
+{
+	struct sieve_ast_argument *arg = tst->first_positional;
+	const struct sieve_match_type mcht_default = 
+		SIEVE_MATCH_TYPE_DEFAULT(is_match_type);
+	const struct sieve_comparator cmp_default = 
+		SIEVE_COMPARATOR_DEFAULT(i_ascii_casemap_comparator);
+		
+	/* Check value */
+		
+	if ( !sieve_validate_positional_argument
+		(valdtr, tst, arg, "value", 1, SAAT_STRING) ) {
+		return FALSE;
+	}
+
+	if ( !sieve_validator_argument_activate(valdtr, tst, arg, FALSE) )
+		return FALSE;
+	
+	/* Validate the key argument to a specified match type */
+	return sieve_match_type_validate
+		(valdtr, tst, arg, &mcht_default, &cmp_default); 
+}
+
+/* 
+ * Code generation 
+ */
+
+static bool tst_spamvirustest_generate
+(const struct sieve_codegen_env *cgenv, struct sieve_command *tst) 
+{ 
+	if ( sieve_command_is(tst, spamtest_test) )
+		sieve_operation_emit(cgenv->sbin, tst->ext, &spamtest_operation);
+	else if ( sieve_command_is(tst, virustest_test) )
+		sieve_operation_emit(cgenv->sbin, tst->ext, &virustest_operation);
+	else
+		i_unreached();
+
+	/* Generate arguments */  	
+	return sieve_generate_arguments(cgenv, tst, NULL);
+}
+
+/* 
+ * Code dump 
+ */
+
+static bool tst_spamvirustest_operation_dump
+(const struct sieve_dumptime_env *denv, sieve_size_t *address)
+{
+	int opt_code = 0;
+	const struct sieve_operation *op = &denv->oprtn;
+
+	sieve_code_dumpf(denv, "%s", sieve_operation_mnemonic(op));
+	sieve_code_descend(denv);
+	
+	/* Handle any optional arguments */
+  do {
+		if ( !sieve_match_dump_optional_operands(denv, address, &opt_code) )
+			return FALSE;
+
+		switch ( opt_code ) {
+		case SIEVE_MATCH_OPT_END:
+			break;
+		case OPT_SPAMTEST_PERCENT:
+			sieve_code_dumpf(denv, "percent");
+			break;
+    default:
+			return FALSE;
+		}
+	} while ( opt_code != SIEVE_MATCH_OPT_END );
+
+	return
+		sieve_opr_string_dump(denv, address, "value");
+}
+
+/* 
+ * Code execution 
+ */
+
+static int tst_spamvirustest_operation_execute
+(const struct sieve_runtime_env *renv, sieve_size_t *address)
+{	
+	const struct sieve_operation *op = &renv->oprtn;
+	const struct sieve_extension *this_ext = op->ext;
+	bool result = TRUE, matched = FALSE;
+	int opt_code = 0;
+	struct sieve_match_type mcht = 
+		SIEVE_MATCH_TYPE_DEFAULT(is_match_type);
+	struct sieve_comparator cmp = 
+		SIEVE_COMPARATOR_DEFAULT(i_ascii_casemap_comparator);
+	bool percent = FALSE;
+	struct sieve_coded_stringlist *key_value;
+	struct sieve_match_context *mctx;
+	const char *value;
+	int ret;
+	
+	/* Read optional operands */
+	do {
+		if ( (ret=sieve_match_read_optional_operands
+			(renv, address, &opt_code, &cmp, &mcht)) <= 0 )
+			return ret;
+
+		switch ( opt_code ) {
+		case SIEVE_MATCH_OPT_END:
+			break;
+		case OPT_SPAMTEST_PERCENT:
+			percent = TRUE;
+			break;
+		default:
+			sieve_runtime_trace_error(renv, "unknown optional operand");
+			return SIEVE_EXEC_BIN_CORRUPT;
+		}
+	} while ( opt_code != SIEVE_MATCH_OPT_END );
+
+	/* Read value part */
+	if ( (key_value=sieve_opr_stringlist_read(renv, address)) == NULL ) {
+		sieve_runtime_trace_error(renv, "invalid value operand");
+		return SIEVE_EXEC_BIN_CORRUPT;
+	}
+			
+	/* Perform test */
+
+	sieve_runtime_trace(renv, "%s test", sieve_operation_mnemonic(op));
+
+	/* Initialize match */
+	mctx = sieve_match_begin(renv->interp, &mcht, &cmp, NULL, key_value); 	
+
+	/* Perform match */
+
+	matched = FALSE;
+
+	value = ext_spamvirustest_get_value(renv, this_ext, percent);
+
+	if ( (ret=sieve_match_value(mctx, value, strlen(value))) < 0 ) {
+		result = FALSE;
+	} else {
+		matched = ( ret > 0 );				
+	}
+
+	/* Finish match */
+	if ( (ret=sieve_match_end(&mctx)) < 0 ) 
+		result = FALSE;
+	else
+		matched = ( ret > 0 || matched );
+
+	/* Set test result for subsequent conditional jump */
+	if ( result ) {
+		sieve_interpreter_set_test_result(renv->interp, matched);
+		return SIEVE_EXEC_OK;
+	}	
+
+	sieve_runtime_trace_error(renv, "invalid string-list item");
+	return SIEVE_EXEC_BIN_CORRUPT;
+}
diff --git a/src/lib-sieve/sieve-extensions.c b/src/lib-sieve/sieve-extensions.c
index b1fe94768..808a4ecf8 100644
--- a/src/lib-sieve/sieve-extensions.c
+++ b/src/lib-sieve/sieve-extensions.c
@@ -141,9 +141,15 @@ const unsigned int sieve_deprecated_extensions_count =
 #ifdef HAVE_SIEVE_UNFINISHED
 
 extern const struct sieve_extension_def ereject_extension;
+extern const struct sieve_extension_def spamtest_extension;
+extern const struct sieve_extension_def spamtestplus_extension;
+extern const struct sieve_extension_def virustest_extension;
 
 const struct sieve_extension_def *sieve_unfinished_extensions[] = {
 	&ereject_extension,
+	&spamtest_extension,
+	&spamtestplus_extension,
+	&virustest_extension
 };
 
 const unsigned int sieve_unfinished_extensions_count =
diff --git a/src/lib-sieve/sieve-extensions.h b/src/lib-sieve/sieve-extensions.h
index 58c6b3037..1854842ca 100644
--- a/src/lib-sieve/sieve-extensions.h
+++ b/src/lib-sieve/sieve-extensions.h
@@ -24,7 +24,7 @@ struct sieve_extension_def {
 	const char *name;
 
 	/* Registration */		
-	bool (*load)(const struct sieve_extension *ext, void **);
+	bool (*load)(const struct sieve_extension *ext, void **context);
 	void (*unload)(const struct sieve_extension *ext);
 
 	/* Compilation */
@@ -78,6 +78,8 @@ struct sieve_extension {
 
 #define sieve_extension_name(ext) \
 	(ext)->def->name
+#define sieve_extension_is(ext, definition) \
+	( (ext)->def == &(definition) )
 
 /* 
  * Defining opcodes and operands 
-- 
GitLab