(*it);
25 |
26 | if (child == NULL)
27 | continue;
28 |
29 | if (child->value() == value.value())
30 | return it;
31 | }
32 |
33 | return this->children().end();
34 | }
35 |
36 | private:
37 | std::string _name;
38 | };
39 |
40 | } // namespace sieve
41 |
--------------------------------------------------------------------------------
/test/AST/util.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | import unittest
4 | from tempfile import NamedTemporaryFile
5 |
6 |
7 | class DiffTestCase(unittest.TestCase):
8 | def assertNoDiff(self, diff):
9 | self.assertFalse(diff, msg='\n{}'.format(diff))
10 |
11 |
12 | def run_mock(filename):
13 | current_dir = os.path.dirname(os.path.realpath(__file__))
14 | test_path = os.path.join(current_dir, 'mock', filename)
15 | check_sieve_path = os.path.join(current_dir, '../..', 'check-sieve')
16 |
17 | return subprocess.check_output([check_sieve_path, '--trace-tree', test_path])
18 |
19 | def diff(output, filename):
20 | current_dir = os.path.dirname(os.path.realpath(__file__))
21 | out_path = os.path.join(current_dir, 'mock', filename)
22 | check_sieve_path = os.path.join(current_dir, '../..', 'check-sieve')
23 |
24 | diff = False
25 |
26 | temp = NamedTemporaryFile(delete=False)
27 | temp.write(output)
28 | temp.close()
29 |
30 | diff = subprocess.Popen(["/usr/bin/diff", "-ru", temp.name, out_path], stdout=subprocess.PIPE).communicate()[0]
31 |
32 | return diff
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 - 2024 Dana Burkart
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/test/3028/strings_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class TestStrings(unittest.TestCase):
5 |
6 | def test_multiline_strings(self):
7 | sieve = '''
8 | require "vacation";
9 | vacation :mime
10 | "Content-Type: multipart/alternative; boundary=foo
11 |
12 | --foo
13 |
14 | I'm at the beach relaxing. Mmmm, surf...
15 |
16 | --foo
17 | Content-Type: text/html; charset=us-ascii
18 |
19 |
21 | How to relax
22 |
23 | I'm at the beach relaxing.
24 | Mmmm, surf...
25 |
26 |
27 | --foo--
28 | ";
29 | '''
30 | self.assertFalse(checksieve.parse_string(sieve, False))
31 |
32 | if __name__ == '__main__':
33 | unittest.main()
34 |
--------------------------------------------------------------------------------
/test/5233/examples_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class ExamplesTest(unittest.TestCase):
5 |
6 | def test_example1(self):
7 | sieve = '''
8 | require ["envelope", "subaddress", "fileinto"];
9 |
10 | # In this example the same user account receives mail for both
11 | # "ken@example.com" and "postmaster@example.com"
12 |
13 | # File all messages to postmaster into a single mailbox,
14 | # ignoring the :detail part.
15 | if envelope :user "to" "postmaster" {
16 | fileinto "inbox.postmaster";
17 | stop;
18 | }
19 |
20 | # File mailing list messages (subscribed as "ken+mta-filters").
21 | if envelope :detail "to" "mta-filters" {
22 | fileinto "inbox.ietf-mta-filters";
23 | }
24 |
25 | # Redirect all mail sent to "ken+foo".
26 | if envelope :detail "to" "foo" {
27 | redirect "ken@example.net";
28 | }
29 | '''
30 | self.assertFalse(checksieve.parse_string(sieve, False))
31 |
32 |
33 | if __name__ == '__main__':
34 | unittest.main()
--------------------------------------------------------------------------------
/src/AST/ASTString.hh:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include "ASTNode.hh"
6 |
7 | namespace sieve
8 | {
9 |
10 | class ASTVisitor;
11 |
12 | class ASTString : public ASTNode {
13 | public:
14 | ASTString() : ASTNode() {}
15 | explicit ASTString(yy::location location);
16 | explicit ASTString(std::string str);
17 | ASTString(yy::location location, std::string str);
18 |
19 | void accept(ASTVisitor &visitor) override;
20 |
21 | [[nodiscard]] std::string_view value() const { return std::string_view{ this->_str }; }
22 |
23 | template
24 | std::vector::const_iterator find(const T& value) const {
25 | for (auto it = this->children().begin(); it != this->children().end(); ++it) {
26 | const T* child = dynamic_cast(*it);
27 |
28 | if (child == NULL)
29 | continue;
30 |
31 | if (child->value() == value.value())
32 | return it;
33 | }
34 |
35 | return this->children().end();
36 | }
37 |
38 | private:
39 | std::string _str;
40 | };
41 |
42 | } // namespace sieve
43 |
--------------------------------------------------------------------------------
/test/6785/examples_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class TestExamples(unittest.TestCase):
5 |
6 | def test_example_1(self):
7 | sieve = '''
8 | require ["copy", "environment", "imapsieve"];
9 |
10 | if anyof (environment :is "imap.cause" "APPEND",
11 | environment :is "imap.cause" "COPY") {
12 | if environment :is "imap.mailbox" "ActionItems" {
13 | redirect :copy "actionitems@example.com";
14 | }
15 | }
16 | '''
17 | self.assertFalse(checksieve.parse_string(sieve, False))
18 |
19 | def test_example_2(self):
20 | sieve = '''
21 | require ["enotify", "imap4flags", "variables",
22 | "environment", "imapsieve"];
23 |
24 | if environment :matches "imap.mailbox" "*" {
25 | set "mailbox" "${1}";
26 | }
27 |
28 | if allof (hasflag "\\Flagged",
29 | environment :contains "imap.changedflags" "\\Flagged") {
30 | notify :message "Important message in ${mailbox}"
31 | "xmpp:tim@example.com?message;subject=SIEVE";
32 | }
33 | '''
34 | self.assertFalse(checksieve.parse_string(sieve, False))
35 |
36 |
37 | if __name__ == '__main__':
38 | unittest.main()
39 |
--------------------------------------------------------------------------------
/src/AST/ASTVisitor.hh:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "ASTBlock.hh"
4 | #include "ASTBoolean.hh"
5 | #include "ASTBranch.hh"
6 | #include "ASTCommand.hh"
7 | #include "ASTCondition.hh"
8 | #include "ASTNoOp.hh"
9 | #include "ASTNumeric.hh"
10 | #include "ASTRequire.hh"
11 | #include "ASTSieve.hh"
12 | #include "ASTString.hh"
13 | #include "ASTStringList.hh"
14 | #include "ASTTag.hh"
15 | #include "ASTTest.hh"
16 | #include "ASTTestList.hh"
17 |
18 | namespace sieve
19 | {
20 |
21 | class ASTVisitor {
22 | public:
23 | virtual void walk( ASTSieve *root ) =0;
24 |
25 | virtual void visit( ASTBlock* ) =0;
26 | virtual void visit( ASTBoolean* ) =0;
27 | virtual void visit( ASTBranch* ) =0;
28 | virtual void visit( ASTCommand* ) =0;
29 | virtual void visit( ASTCondition* ) =0;
30 | virtual void visit( ASTNoOp* ) =0;
31 | virtual void visit( ASTNumeric* ) =0;
32 | virtual void visit( ASTRequire* ) =0;
33 | virtual void visit( ASTSieve* ) =0;
34 | virtual void visit( ASTString* ) =0;
35 | virtual void visit( ASTStringList* ) =0;
36 | virtual void visit( ASTTag* ) =0;
37 | virtual void visit( ASTTest* ) =0;
38 | virtual void visit( ASTTestList* ) =0;
39 | };
40 |
41 | } // namespace sieve
42 |
--------------------------------------------------------------------------------
/src/AST/ASTStringList.hh:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "ASTNode.hh"
4 |
5 | namespace sieve
6 | {
7 |
8 | class ASTVisitor;
9 |
10 | class ASTStringList : public ASTNode {
11 | public:
12 | ASTStringList() : ASTNode() {}
13 | ASTStringList(yy::location location);
14 |
15 | void accept(ASTVisitor &visitor);
16 |
17 | std::string value() const { return "ASTStringList"; }
18 |
19 | template
20 | std::vector::const_iterator find(const T& value) const {
21 | for (std::vector::const_iterator it = this->children().begin(); it != this->children().end(); ++it) {
22 | const T* child = dynamic_cast(*it);
23 |
24 | if (child == NULL)
25 | continue;
26 |
27 | if (child->value() == value.value())
28 | return it;
29 | }
30 |
31 | return this->children().end();
32 | }
33 |
34 | int length() const {
35 | int length = 0;
36 | for (std::vector::const_iterator it = this->children().begin(); it != this->children().end(); ++it) {
37 | length += 1;
38 | }
39 | return length;
40 | }
41 |
42 | private:
43 | };
44 |
45 | } // namespace sieve
46 |
--------------------------------------------------------------------------------
/test/5260/index_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class TestIndex(unittest.TestCase):
5 |
6 | def test_index(self):
7 | sieve = '''
8 | # Implement the Internet-Draft cutoff date check assuming the
9 | # second Received: field specifies when the message first
10 | # entered the local email infrastructure.
11 | require ["date", "relational", "index"];
12 | if date :value "gt" :index 2 :zone "-0500" "received"
13 | "iso8601" "2007-02-26T09:00:00-05:00"
14 | { redirect "aftercutoff@example.org"; }
15 | '''
16 | self.assertFalse(checksieve.parse_string(sieve, False))
17 |
18 | def test_index_no_require(self):
19 | sieve = '''
20 | # Implement the Internet-Draft cutoff date check assuming the
21 | # second Received: field specifies when the message first
22 | # entered the local email infrastructure.
23 | require ["date", "relational"];
24 | if date :value "gt" :index 2 :zone "-0500" "received"
25 | "iso8601" "2007-02-26T09:00:00-05:00"
26 | { redirect "aftercutoff@example.org"; }
27 | '''
28 | self.assertTrue(checksieve.parse_string(sieve, True))
29 |
30 |
31 | if __name__ == '__main__':
32 | unittest.main()
--------------------------------------------------------------------------------
/test/3028/comments_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class TestComments(unittest.TestCase):
5 |
6 | def test_single_line(self):
7 | sieve = '# This is a comment'
8 | self.assertFalse(checksieve.parse_string(sieve, False))
9 |
10 | def test_single_line_with_code(self):
11 | sieve = 'keep; # This is a comment'
12 | self.assertFalse(checksieve.parse_string(sieve, False))
13 |
14 | def test_multi_line(self):
15 | sieve = '''
16 | /* This is the first line
17 | This is the second */
18 | '''
19 | self.assertFalse(checksieve.parse_string(sieve, False))
20 |
21 | def test_multi_line_2(self):
22 | sieve = '''
23 | if exists "In-Reply-To" {
24 | /* Single-line-multi-line-comment */
25 | }
26 | '''
27 | self.assertFalse(checksieve.parse_string(sieve, False))
28 |
29 | def test_multi_line_3(self):
30 | sieve = '''
31 | if exists "In-Reply-To" {
32 | /* Multi-line comment
33 | with a * in it */
34 | }
35 | '''
36 | self.assertFalse(checksieve.parse_string(sieve, False))
37 |
38 |
39 | if __name__ == '__main__':
40 | unittest.main()
--------------------------------------------------------------------------------
/test/5260/date_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class TestDate(unittest.TestCase):
5 |
6 | def test_date_pass(self):
7 | sieve = '''
8 | require ["date", "relational", "fileinto"];
9 | if anyof(date :is "received" "weekday" "0",
10 | date :is "received" "weekday" "6")
11 | { fileinto "weekend"; }
12 | '''
13 | self.assertFalse(checksieve.parse_string(sieve, False))
14 |
15 | def test_date_no_require(self):
16 | sieve = '''
17 | require ["relational", "fileinto"];
18 | if anyof(date :is "received" "weekday" "0",
19 | date :is "received" "weekday" "6")
20 | { fileinto "weekend"; }
21 | '''
22 | self.assertTrue(checksieve.parse_string(sieve, True))
23 |
24 | def test_date_2(self):
25 | sieve = '''
26 | require ["date", "relational", "fileinto"];
27 | if allof(header :is "from" "boss@example.com",
28 | date :value "ge" :originalzone "date" "hour" "09",
29 | date :value "lt" :originalzone "date" "hour" "17")
30 | { fileinto "urgent"; }
31 | '''
32 | self.assertFalse(checksieve.parse_string(sieve, False))
33 |
34 |
35 | if __name__ == '__main__':
36 | unittest.main()
--------------------------------------------------------------------------------
/src/webchecksieve.cc:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "checksieve.h"
4 |
5 | using namespace emscripten;
6 |
7 | EMSCRIPTEN_BINDINGS(sieve) {
8 |
9 | value_object("position")
10 | .field("column", &yy::position::column)
11 | .field("line", &yy::position::line)
12 | ;
13 | value_object("location")
14 | .field("begin", &yy::location::begin)
15 | .field("end", &yy::location::end)
16 | ;
17 | value_object("parse_result")
18 | .field("location", &sieve::parse_result::location)
19 | .field("status", &sieve::parse_result::status)
20 | .field("error", &sieve::parse_result::error)
21 | .field("hint", &sieve::parse_result::hint)
22 | ;
23 | class_("parse_options")
24 | .constructor<>()
25 | ;
26 |
27 | function("version", optional_override(
28 | [](){
29 | return std::string(sieve::version());
30 | }));
31 | function("sieve_parse_string", optional_override(
32 | [](const std::string s, struct sieve::parse_options o) {
33 | return sieve_parse_string(s.c_str(), o);
34 | }));
35 | }
36 |
--------------------------------------------------------------------------------
/src/diagnostic.cc:
--------------------------------------------------------------------------------
1 | #include "diagnostic.hh"
2 |
3 | #include
4 | #include
5 |
6 | namespace sieve {
7 |
8 | std::string Diagnostic::describe( parse_result& result, std::ifstream &input ) const {
9 | std::string line;
10 | std::ostringstream output;
11 |
12 | for (int i = 1; getline(input, line, '\n'); i++) {
13 | if (i == result.location.begin.line)
14 | break;
15 | }
16 |
17 | // Account for tabs in our input
18 | for (int i = 0; i < result.location.begin.column; i++) {
19 | if (line[i] == '\t') {
20 | line.replace(i, 1, std::string(_tab_size, ' '));
21 | result.location.begin.column += _tab_size - 1;
22 | result.location.end.column += _tab_size - 1;
23 | }
24 | }
25 |
26 | output << result.error << std::endl
27 | << "On line " << result.location.begin.line << ":" << std::endl
28 | << line << std::endl
29 | << std::string(result.location.begin.column - 1, ' ')
30 | << std::string(result.location.end.column - result.location.begin.column, '^')
31 | << std::endl;
32 |
33 | if (!result.hint.empty()) {
34 | output << result.hint << std::endl;
35 | }
36 |
37 | return output.str();
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/test/3894/basic_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import checksieve
3 |
4 | class TestBasic(unittest.TestCase):
5 |
6 | def test_copy_fileinto (self):
7 | sieve = '''
8 | require ["copy", "fileinto"];
9 |
10 | if header :matches "Subject" "*" {
11 | fileinto :copy "All Mail";
12 | }
13 |
14 | keep;
15 | stop;
16 | '''
17 | self.assertFalse(checksieve.parse_string(sieve, False))
18 |
19 | def test_copy_redirect (self):
20 | sieve = '''
21 | require ["copy"];
22 |
23 | if header :matches "Subject" "*" {
24 | redirect :copy "foo@example.com";
25 | }
26 |
27 | keep;
28 | stop;
29 | '''
30 | self.assertFalse(checksieve.parse_string(sieve, False))
31 |
32 | def test_copy_no_require (self):
33 | sieve = '''
34 | require ["fileinto"];
35 |
36 | if header :matches "Subject" "*" {
37 | fileinto :copy "All Mail";
38 | }
39 |
40 | keep;
41 | stop;
42 | '''
43 | self.assertTrue(checksieve.parse_string(sieve, True))
44 |
45 | if __name__ == '__main__':
46 | unittest.main()
--------------------------------------------------------------------------------
/src/checksieve.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #ifndef PLATFORM
4 | #if defined __APPLE__ && defined __MACH__
5 | #define PLATFORM "Darwin"
6 | #endif
7 | #endif
8 |
9 | #ifdef DEBUG
10 | #define DEBUGLOG(x) std::cout << "DEBUG: " << x << std::endl;
11 | #else
12 | #define DEBUGLOG(x)
13 | #endif
14 |
15 | #include
16 | #include