"""
Unit tests for the SIEVE language parser.
"""

import unittest
import os.path
import codecs
import io

from sievelib.parser import Parser
from sievelib.factory import FiltersSet
import sievelib.commands


class MytestCommand(sievelib.commands.ActionCommand):
    args_definition = [
        {
            "name": "testtag",
            "type": ["tag"],
            "write_tag": True,
            "values": [":testtag"],
            "extra_arg": {"type": "number", "required": False},
            "required": False,
        },
        {"name": "recipients", "type": ["string", "stringlist"], "required": True},
    ]


class Quota_notificationCommand(sievelib.commands.ActionCommand):
    args_definition = [
        {
            "name": "subject",
            "type": ["tag"],
            "write_tag": True,
            "values": [":subject"],
            "extra_arg": {"type": "string"},
            "required": False,
        },
        {
            "name": "recipient",
            "type": ["tag"],
            "write_tag": True,
            "values": [":recipient"],
            "extra_arg": {"type": "stringlist"},
            "required": True,
        },
    ]


class SieveTest(unittest.TestCase):
    def setUp(self):
        self.parser = Parser()

    def __checkCompilation(self, script, result):
        self.assertEqual(self.parser.parse(script), result)

    def compilation_ok(self, script, **kwargs):
        self.__checkCompilation(script, True, **kwargs)

    def compilation_ko(self, script):
        self.__checkCompilation(script, False)

    def representation_is(self, content):
        target = io.StringIO()
        self.parser.dump(target)
        repr_ = target.getvalue()
        target.close()
        self.assertEqual(repr_, content.lstrip())

    def sieve_is(self, content):
        filtersset = FiltersSet("Testfilterset")
        filtersset.from_parser_result(self.parser)
        target = io.StringIO()
        filtersset.tosieve(target)
        repr_ = target.getvalue()
        target.close()
        self.assertEqual(repr_, content)


class AdditionalCommands(SieveTest):
    def test_add_command(self):
        self.assertRaises(
            sievelib.commands.UnknownCommand,
            sievelib.commands.get_command_instance,
            "mytest",
        )
        sievelib.commands.add_commands(MytestCommand)
        sievelib.commands.get_command_instance("mytest")
        self.compilation_ok(
            b"""
        mytest :testtag 10 ["testrecp1@example.com"];
        """
        )

    def test_quota_notification(self):
        sievelib.commands.add_commands(Quota_notificationCommand)
        quota_notification_sieve = """# Filter: Testrule\nquota_notification :subject "subject here" :recipient ["somerecipient@example.com"];\n"""
        self.compilation_ok(quota_notification_sieve)
        self.sieve_is(quota_notification_sieve)


class ValidEncodings(SieveTest):
    def test_utf8_file(self):
        utf8_sieve = os.path.join(os.path.dirname(__file__), "files", "utf8_sieve.txt")
        with codecs.open(utf8_sieve, encoding="utf8") as fobj:
            source_sieve = fobj.read()
        self.parser.parse_file(utf8_sieve)
        self.sieve_is(source_sieve)


class ValidSyntaxes(SieveTest):
    def test_hash_comment(self):
        self.compilation_ok(
            b"""
if size :over 100k { # this is a comment
    discard;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    size (type: test)
        :over
        100k
    discard (type: action)
"""
        )

    def test_bracket_comment(self):
        self.compilation_ok(
            b"""
if size :over 100K { /* this is a comment
    this is still a comment */ discard /* this is a comment
    */ ;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    size (type: test)
        :over
        100K
    discard (type: action)
"""
        )

    def test_string_with_bracket_comment(self):
        self.compilation_ok(
            b"""
if header :contains "Cc" "/* comment */" {
    discard;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    header (type: test)
        :contains
        "Cc"
        "/* comment */"
    discard (type: action)
"""
        )

    def test_multiline_string(self):
        self.compilation_ok(
            b"""
require "reject";

if allof (false, address :is ["From", "Sender"] ["blka@bla.com"]) {
    reject text:
noreply
============================
Your email has been canceled
============================
.
;
    stop;
} else {
    reject text:
================================
Your email has been canceled too
================================
.
;
}
"""
        )
        self.representation_is(
            """
require (type: control)
    "reject"
if (type: control)
    allof (type: test)
        false (type: test)
        address (type: test)
            :is
            ["From","Sender"]
            ["blka@bla.com"]
    reject (type: action)
        text:
noreply
============================
Your email has been canceled
============================
.
    stop (type: action)
else (type: control)
    reject (type: action)
        text:
================================
Your email has been canceled too
================================
.
"""
        )

    def test_complex_allof_with_not(self):
        """Test for allof/anyof commands including a not test.

        See https://github.com/tonioo/sievelib/issues/69.
        """
        self.compilation_ok(
            b"""
require ["fileinto", "reject"];

if allof (not allof (address :is ["From","sender"] ["test1@test2.priv","test2@test2.priv"], header :matches "Subject" "INACTIVE*"), address :is "From" "user3@test3.priv")
{
    reject;
}
"""
        )
        self.representation_is(
            """
require (type: control)
    ["fileinto","reject"]
if (type: control)
    allof (type: test)
        not (type: test)
            allof (type: test)
                address (type: test)
                    :is
                    ["From","sender"]
                    ["test1@test2.priv","test2@test2.priv"]
                header (type: test)
                    :matches
                    "Subject"
                    "INACTIVE*"
        address (type: test)
            :is
            "From"
            "user3@test3.priv"
    reject (type: action)
"""
        )

    def test_nested_blocks(self):
        self.compilation_ok(
            b"""
if header :contains "Sender" "example.com" {
  if header :contains "Sender" "me@" {
    discard;
  } elsif header :contains "Sender" "you@" {
    keep;
  }
}
"""
        )
        self.representation_is(
            """
if (type: control)
    header (type: test)
        :contains
        "Sender"
        "example.com"
    if (type: control)
        header (type: test)
            :contains
            "Sender"
            "me@"
        discard (type: action)
    elsif (type: control)
        header (type: test)
            :contains
            "Sender"
            "you@"
        keep (type: action)
"""
        )

    def test_true_test(self):
        self.compilation_ok(
            b"""
if true {

}
"""
        )
        self.representation_is(
            """
if (type: control)
    true (type: test)
"""
        )

    def test_rfc5228_extended(self):
        self.compilation_ok(
            b"""
#
# Example Sieve Filter
# Declare any optional features or extension used by the script
#
require ["fileinto"];

#
# Handle messages from known mailing lists
# Move messages from IETF filter discussion list to filter mailbox
#
if header :is "Sender" "owner-ietf-mta-filters@imc.org"
        {
        fileinto "filter";  # move to "filter" mailbox
        }
#
# Keep all messages to or from people in my company
#
elsif address :domain :is ["From", "To"] "example.com"
        {
        keep;               # keep in "In" mailbox
        }

#
# Try and catch unsolicited email.  If a message is not to me,
# or it contains a subject known to be spam, file it away.
#
elsif anyof (NOT address :all :contains
               ["To", "Cc", "Bcc"] "me@example.com",
             header :matches "subject"
               ["*make*money*fast*", "*university*dipl*mas*"])
        {
        fileinto "spam";   # move to "spam" mailbox
        }
else
        {
        # Move all other (non-company) mail to "personal"
        # mailbox.
        fileinto "personal";
        }
"""
        )
        self.representation_is(
            """
require (type: control)
    ["fileinto"]
if (type: control)
    header (type: test)
        :is
        "Sender"
        "owner-ietf-mta-filters@imc.org"
    fileinto (type: action)
        "filter"
elsif (type: control)
    address (type: test)
        :domain
        :is
        ["From","To"]
        "example.com"
    keep (type: action)
elsif (type: control)
    anyof (type: test)
        not (type: test)
            address (type: test)
                :all
                :contains
                ["To","Cc","Bcc"]
                "me@example.com"
        header (type: test)
            :matches
            "subject"
            ["*make*money*fast*","*university*dipl*mas*"]
    fileinto (type: action)
        "spam"
else (type: control)
    fileinto (type: action)
        "personal"
"""
        )

    def test_explicit_comparator(self):
        self.compilation_ok(
            b"""
if header :contains :comparator "i;octet" "Subject" "MAKE MONEY FAST" {
  discard;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    header (type: test)
        :comparator
        "i;octet"
        :contains
        "Subject"
        "MAKE MONEY FAST"
    discard (type: action)
"""
        )

    def test_non_ordered_args(self):
        self.compilation_ok(
            b"""
if address :all :is "from" "tim@example.com" {
    discard;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    address (type: test)
        :all
        :is
        "from"
        "tim@example.com"
    discard (type: action)
"""
        )

    def test_multiple_not(self):
        self.compilation_ok(
            b"""
if not not not not true {
    stop;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    not (type: test)
        not (type: test)
            not (type: test)
                not (type: test)
                    true (type: test)
    stop (type: action)
"""
        )

    def test_just_one_command(self):
        self.compilation_ok(b"keep;")
        self.representation_is(
            """
keep (type: action)
"""
        )

    def test_singletest_testlist(self):
        self.compilation_ok(
            b"""
if anyof (true) {
    discard;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    anyof (type: test)
        true (type: test)
    discard (type: action)
"""
        )

    def test_multitest_testlist(self):
        self.compilation_ok(
            b"""
if anyof(allof(address :contains "From" ""), allof(header :contains "Subject" "")) {}
"""
        )

    def test_truefalse_testlist(self):
        self.compilation_ok(
            b"""
if anyof(true, false) {
    discard;
}
"""
        )
        self.representation_is(
            """
if (type: control)
    anyof (type: test)
        true (type: test)
        false (type: test)
    discard (type: action)
"""
        )

    def test_vacationext_basic(self):
        self.compilation_ok(
            b"""
require "vacation";
if header :contains "subject" "cyrus" {
    vacation "I'm out -- send mail to cyrus-bugs";
} else {
    vacation "I'm out -- call me at +1 304 555 0123";
}
"""
        )

    def test_vacationext_medium(self):
        self.compilation_ok(
            b"""
require "vacation";
if header :contains "subject" "lunch" {
    vacation :handle "ran-away" "I'm out and can't meet for lunch";
} else {
    vacation :handle "ran-away" "I'm out";
}
"""
        )

    def test_vacationext_with_limit(self):
        self.compilation_ok(
            b"""
require "vacation";
vacation :days 23 :addresses ["tjs@example.edu",
                              "ts4z@landru.example.edu"]
   "I'm away until October 19.
   If it's an emergency, call 911, I guess." ;
"""
        )

    def test_vacationext_with_single_mail_address(self):
        self.compilation_ok(
            """
require "vacation";
vacation :days 23 :addresses "tjs@example.edu"
   "I'm away until October 19.
   If it's an emergency, call 911, I guess." ;
"""
        )

    def test_vacationext_with_multiline(self):
        self.compilation_ok(
            b"""
require "vacation";
vacation :mime text:
Content-Type: multipart/alternative; boundary=foo

--foo

I'm at the beach relaxing.  Mmmm, surf...

--foo
Content-Type: text/html; charset=us-ascii

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"
 "http://www.w3.org/TR/REC-html40/strict.dtd">
<HTML><HEAD><TITLE>How to relax</TITLE>
<BASE HREF="http://home.example.com/pictures/"></HEAD>
<BODY><P>I'm at the <A HREF="beach.gif">beach</A> relaxing.
Mmmm, <A HREF="ocean.gif">surf</A>...
</BODY></HTML>

--foo--
.
;
"""
        )

    def test_vacation_seconds(self):
        self.compilation_ok(
            """
require ["vacation", "vacation-seconds"];
vacation :seconds 10 :addresses ["test@example.org"] "Gone";
"""
        )

    def test_reject_extension(self):
        self.compilation_ok(
            b"""
require "reject";

if header :contains "subject" "viagra" {
    reject;
}
"""
        )

    def test_fileinto_create(self):
        self.compilation_ok(
            b"""require ["fileinto", "mailbox"];
if header :is "Sender" "owner-ietf-mta-filters@imc.org"
        {
        fileinto :create "filter";  # move to "filter" mailbox
        }
"""
        )

    def test_imap4flags_extension(self):
        self.compilation_ok(
            rb"""
require ["fileinto", "imap4flags", "variables"];
if size :over 1M {
    addflag "MyFlags" "Big";
    if header :is "From" "boss@company.example.com" {
       # The message will be marked as "\Flagged Big" when filed into
       # mailbox "Big messages"
       addflag "MyFlags" "\\Flagged";
    }
    fileinto :flags "${MyFlags}" "Big messages";
}
"""
        )

    def test_imap4flags_hasflag(self):
        self.compilation_ok(
            b"""
require ["imap4flags", "fileinto"];

if hasflag ["test", "toto"] {
    fileinto "Test";
}
addflag "Var1" "Truc";
if hasflag "Var1" "Truc" {
    fileinto "Truc";
}
"""
        )

    def test_body_extension(self):
        self.compilation_ok(
            b"""
require ["body", "fileinto"];

if body :content "text" :contains ["missile", "coordinates"] {
    fileinto "secrets";
}
"""
        )
        self.compilation_ok(
            b"""
require "body";

if body :raw :contains "MAKE MONEY FAST" {
    discard;
}
"""
        )
        self.compilation_ok(
            b"""
require ["body", "fileinto"];

# Save messages mentioning the project schedule in the
# project/schedule folder.
if body :text :contains "project schedule" {
    fileinto "project/schedule";
}
"""
        )


class InvalidSyntaxes(SieveTest):
    def test_nested_comments(self):
        self.compilation_ko(
            b"""
/* this is a comment /* with a nested comment inside */
it is allowed by the RFC :p */
"""
        )

    def test_nonopened_block(self):
        self.compilation_ko(
            b"""
if header :is "Sender" "me@example.com"
    discard;
}
"""
        )

    def test_nonclosed_block(self):
        self.compilation_ko(
            b"""
if header :is "Sender" "me@example.com" {
    discard;

"""
        )

    def test_nonopened_parenthesis(self):
        self.compilation_ko(
            b"""
if header :is "Sender" "me@example.com") {
    discard;
}
"""
        )

    def test_nonopened_block2(self):
        self.compilation_ko(b"""}""")

    def test_unknown_token(self):
        self.compilation_ko(
            b"""
if header :is "Sender" "Toto" & header :contains "Cc" "Tata" {

}
"""
        )

    def test_empty_string_list(self):
        self.compilation_ko(b"require [];")

    def test_unopened_string_list(self):
        self.compilation_ko(b'require "fileinto"];')

    def test_unclosed_string_list(self):
        self.compilation_ko(b'require ["toto", "tata";')

    def test_misplaced_comma_in_string_list(self):
        self.compilation_ko(b'require ["toto",];')

    def test_nonopened_tests_list(self):
        self.compilation_ko(
            b"""
if anyof header :is "Sender" "me@example.com",
          header :is "Sender" "myself@example.com") {
    fileinto "trash";
}
"""
        )

    def test_nonclosed_tests_list(self):
        self.compilation_ko(
            b"""
if anyof (header :is "Sender" "me@example.com",
          header :is "Sender" "myself@example.com" {
    fileinto "trash";
}
"""
        )

    def test_nonclosed_tests_list2(self):
        self.compilation_ko(
            b"""
if anyof (header :is "Sender" {
    fileinto "trash";
}
"""
        )

    def test_misplaced_comma_in_tests_list(self):
        self.compilation_ko(
            b"""
if anyof (header :is "Sender" "me@example.com",) {

}
"""
        )

    def test_comma_inside_arguments(self):
        self.compilation_ko(
            b"""
require "fileinto", "enveloppe";
"""
        )

    def test_non_ordered_args(self):
        self.compilation_ko(
            b"""
if address "From" :is "tim@example.com" {
    discard;
}
"""
        )

    def test_extra_arg(self):
        self.compilation_ko(
            b"""
if address :is "From" "tim@example.com" "tutu" {
    discard;
}
"""
        )

    def test_empty_not(self):
        self.compilation_ko(
            b"""
if not {
    discard;
}
"""
        )

    def test_missing_semicolon(self):
        self.compilation_ko(
            b"""
require ["fileinto"]
"""
        )

    def test_missing_semicolon_in_block(self):
        self.compilation_ko(
            b"""
if true {
    stop
}
"""
        )

    def test_misplaced_parenthesis(self):
        self.compilation_ko(
            b"""
if (true) {

}
"""
        )

    def test_control_command_in_test(self):
        self.compilation_ko(
            b"""
if stop;
"""
        )

    def test_extra_test_in_simple_control(self):
        self.compilation_ko(
            b"""
if address "From" "example.com" header "Subject" "Example" { stop; }
"""
        )

    def test_missing_comma_in_test_list(self):
        self.compilation_ko(
            b"""
if allof(anyof(address "From" "example.com") header "Subject" "Example") { stop; }
"""
        )

    def test_vacation_seconds_no_arg(self):
        self.compilation_ko(
            """
require ["vacation", "vacation-seconds"];
vacation :seconds :addresses ["test@example.org"] "Gone";
"""
        )


class LanguageRestrictions(SieveTest):
    def test_unknown_control(self):
        self.compilation_ko(
            b"""
macommande "Toto";
"""
        )

    def test_misplaced_elsif(self):
        self.compilation_ko(
            b"""
elsif true {

}
"""
        )

    def test_misplaced_elsif2(self):
        self.compilation_ko(
            b"""
elsif header :is "From" "toto" {

}
"""
        )

    def test_misplaced_nested_elsif(self):
        self.compilation_ko(
            b"""
if true {
  elsif false {

  }
}
"""
        )

    def test_unexpected_argument(self):
        self.compilation_ko(b'stop "toto";')

    def test_bad_arg_value(self):
        self.compilation_ko(
            b"""
if header :isnot "Sent" "me@example.com" {
  stop;
}
"""
        )

    def test_bad_arg_value2(self):
        self.compilation_ko(
            b"""
if header :isnot "Sent" 10000 {
  stop;
}
"""
        )

    def test_bad_comparator_value(self):
        self.compilation_ko(
            b"""
if header :contains :comparator "i;prout" "Subject" "MAKE MONEY FAST" {
  discard;
}
"""
        )

    def test_not_included_extension(self):
        self.compilation_ko(
            b"""
if header :contains "Subject" "MAKE MONEY FAST" {
  fileinto "spam";
}
"""
        )

    def test_test_outside_control(self):
        self.compilation_ko(b"true;")

    def test_fileinto_create_without_mailbox(self):
        self.compilation_ko(
            b"""require ["fileinto"];
if header :is "Sender" "owner-ietf-mta-filters@imc.org"
        {
        fileinto :create "filter";  # move to "filter" mailbox
        }
"""
        )
        self.assertEqual(self.parser.error, "line 4: extension 'mailbox' not loaded")

    def test_fileinto_create_without_fileinto(self):
        self.compilation_ko(
            b"""require ["mailbox"];
if header :is "Sender" "owner-ietf-mta-filters@imc.org"
        {
        fileinto :create "filter";  # move to "filter" mailbox
        }
"""
        )
        self.assertEqual(self.parser.error, "line 4: extension 'fileinto' not loaded")

    def test_unknown_command(self):
        self.compilation_ko(
            b"""require ["mailbox"];
if header :is "Sender" "owner-ietf-mta-filters@imc.org"
        {
        foobar :create "filter";  # move to "filter" mailbox
        }
"""
        )
        self.assertEqual(self.parser.error, "line 4: unknown command 'foobar'")

    def test_exists_get_string_or_list(self):
        self.compilation_ok(
            b"""
if exists "subject"
{
       discard;
}
"""
        )
        self.compilation_ok(
            b"""
if exists ["subject"]
{
       discard;
}
"""
        )


class DateCommands(SieveTest):

    def test_date_command(self):
        self.compilation_ok(
            b"""require ["date", "relational", "fileinto"];
if allof(header :is "from" "boss@example.com",
         date :value "ge" :originalzone "date" "hour" "09",
         date :value "lt" :originalzone "date" "hour" "17")
{ fileinto "urgent"; }
"""
        )

    def test_currentdate_command(self):
        self.compilation_ok(
            b"""require ["date", "relational"];

if allof(currentdate :value "ge" "date" "2013-10-23",
         currentdate :value "le" "date" "2014-10-12")
{
    discard;
}
"""
        )

    def test_currentdate_command_timezone(self):
        self.compilation_ok(
            b"""require ["date", "relational"];

if allof(currentdate :zone "+0100" :value "ge" "date" "2013-10-23",
         currentdate :value "le" "date" "2014-10-12")
{
    discard;
}
"""
        )

    def test_currentdate_norel(self):
        self.compilation_ok(
            b"""require ["date"];

if allof (
  currentdate :zone "+0100" :is "date" "2013-10-23"
)
{
    discard;
}"""
        )

    def test_currentdate_extension_not_loaded(self):
        self.compilation_ko(
            b"""require ["date"];

if allof ( currentdate :value "ge" "date" "2013-10-23" , currentdate :value "le" "date" "2014-10-12" )
{
    discard;
}
"""
        )


class VariablesCommands(SieveTest):
    def test_set_command(self):
        self.compilation_ok(
            b"""require ["variables"];

set "matchsub" "testsubject";

if allof (
  header :contains ["Subject"] "${header}"
)
{
  discard;
}
"""
        )


class CopyWithoutSideEffectsTestCase(SieveTest):
    """RFC3894 test cases."""

    def test_redirect_with_copy(self):
        self.compilation_ko(
            b"""
if header :contains "subject" "test" {
    redirect :copy "dev@null.com";
}
"""
        )

        self.compilation_ok(
            b"""require "copy";
if header :contains "subject" "test" {
    redirect :copy "dev@null.com";
}
"""
        )

    def test_fileinto_with_copy(self):
        self.compilation_ko(
            b"""require "fileinto";
if header :contains "subject" "test" {
    fileinto :copy "Spam";
}
"""
        )
        self.assertEqual(self.parser.error, "line 3: extension 'copy' not loaded")

        self.compilation_ok(
            b"""require ["fileinto", "copy"];
if header :contains "subject" "test" {
    fileinto :copy "Spam";
}
"""
        )


class RegexMatchTestCase(SieveTest):
    def test_header_regex(self):
        self.compilation_ok(
            b"""require "regex";
if header :regex "Subject" "^Test" {
    discard;
}
"""
        )

    def test_header_regex_no_middle(self):
        self.compilation_ko(
            b"""require "regex";
if header "Subject" :regex "^Test" {
    discard;
}
"""
        )

    def test_envelope_regex(self):
        self.compilation_ok(
            b"""require ["regex","envelope"];
if envelope :regex "from" "^test@example\\.org$" {
    discard;
}
"""
        )

    def test_envelope_regex_no_middle(self):
        self.compilation_ko(
            b"""require "regex";
if envelope "from" :regex "^test@example\\.org$" {
    discard;
}
"""
        )

    def test_address_regex(self):
        self.compilation_ok(
            b"""require "regex";
if address :regex "from" "^test@example\\.org$" {
    discard;
}
"""
        )

    def test_address_regex_no_middle(self):
        self.compilation_ko(
            b"""require "regex";
if address "from" :regex "^test@example\\.org$" {
    discard;
}
"""
        )

    def test_body_raw_regex(self):
        self.compilation_ok(
            b"""require ["body", "regex"];
if body :raw :regex "Sample" {
    discard;
}
"""
        )

    def test_body_content_regex(self):
        self.compilation_ok(
            b"""require ["body", "regex"];
if body :content "text" :regex "Sample" {
    discard;
}
"""
        )


if __name__ == "__main__":
    unittest.main()
