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, ®exp_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