Repository: tonioo/sievelib

Issue:
"if address" creation?
I have filters like this:

```
if address :is ["from", "reply-to", "sender"] ["email@one", "email@other"]
{
    fileinto :create "some/folder/name";
}
```

They work for me but I wanted to use your code for something more than just `client.putscript()` function.

`filterset.addfilter()` does not allow 4 arguments for `address`  (`entry[filter_name]` has a list of emails):

```
    fs.addfilter(filter_name,
                 [("address", ":is", "from", entry[filter_name])],
                 [("fileinto", ":create", folder_name)])
```

ends with

```
if anyof (header :is "address" "from") {
    fileinto :create "some/folder/name";
}
```

The only way I got was using `envelope` but then it does not work on server:

```
if anyof (envelope :is ["from"] ["email@one", "email@other"]) {
    fileinto :create "some/folder/name";
}
```

Server info:

localhost.
Escape character is '^]'.
"IMPLEMENTATION" "Dovecot Pigeonhole"
"SIEVE" "fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext vacation-seconds editheader imapflags notify imapsieve vnd.dovecot.imapsieve"
"NOTIFY" "mailto"
"SASL" ""
"STARTTLS"
"VERSION" "1.0"
OK "Dovecot ready."
^]

Discussion:
@tonioo: @hrw Could you check with the PR I created?@hrw: Generated new rules, pushed to server, will check.

Example one:
```
if anyof (address :is ["from","reply-to","sender"] ["powiadomienia@allegro.pl","*@allegromail.pl"]) {
    fileinto :create "Powiadomienia/allegro";
}
```@hrw: It works ;D

Solution patch:
diff --git a/sievelib/factory.py b/sievelib/factory.py
index 294fb29..bd7aeed 100644
--- a/sievelib/factory.py
+++ b/sievelib/factory.py
@@ -14,6 +14,7 @@
 from typing_extensions import NotRequired
 
 from sievelib import commands
+from sievelib.parser import Parser
 
 
 class FilterAlreadyExists(Exception):
@@ -73,7 +74,7 @@ def __isdisabled(self, fcontent: commands.Command) -> bool:
             return False
         return True
 
-    def from_parser_result(self, parser: "sievelib.parser.Parser"):
+    def from_parser_result(self, parser: Parser) -> None:
         cpt = 1
         for f in parser.result:
             if isinstance(f, commands.RequireCommand):
@@ -154,11 +155,15 @@ def __build_condition(
         cmd = commands.get_command_instance("header", parent)
         cmd.check_next_arg("tag", tag)
         if isinstance(condition[0], list):
-            cmd.check_next_arg("stringlist", [self.__quote_if_necessary(c) for c in condition[0]])
+            cmd.check_next_arg(
+                "stringlist", [self.__quote_if_necessary(c) for c in condition[0]]
+            )
         else:
             cmd.check_next_arg("string", self.__quote_if_necessary(condition[0]))
         if isinstance(condition[2], list):
-            cmd.check_next_arg("stringlist", [self.__quote_if_necessary(c) for c in condition[2]])
+            cmd.check_next_arg(
+                "stringlist", [self.__quote_if_necessary(c) for c in condition[2]]
+            )
         else:
             cmd.check_next_arg("string", self.__quote_if_necessary(condition[2]))
         return cmd
@@ -169,7 +174,8 @@ def __create_filter(
         actions: List[tuple],
         matchtype: str = "anyof",
     ) -> commands.Command:
-        """Create a new filter
+        """
+        Create a new filter.
 
         A filter is composed of:
          * a name
@@ -228,6 +234,23 @@ def __create_filter(
                     "stringlist",
                     "[{}]".format(",".join('"{}"'.format(val) for val in c[3])),
                 )
+            elif cname == "address":
+                cmd = commands.get_command_instance("address", ifcontrol, False)
+                if c[1].startswith(":not"):
+                    comp_tag = c[1].replace("not", "")
+                    negate = True
+                else:
+                    comp_tag = c[1]
+                cmd.check_next_arg("tag", comp_tag)
+                for arg in c[2:]:
+                    if isinstance(arg, str):
+                        finalarg = self.__quote_if_necessary(arg)
+                    else:
+                        finalarg = "[{}]".format(
+                            ",".join('"{}"'.format(val) for val in arg)
+                        )
+                    cmd.check_next_arg("stringlist", finalarg)
+
             elif cname == "body":
                 cmd = commands.get_command_instance("body", ifcontrol, False)
                 self.require(cmd.extension)
@@ -317,7 +340,7 @@ def addfilter(
         conditions: List[tuple],
         actions: List[tuple],
         matchtype: str = "anyof",
-    ):
+    ) -> None:
         """Add a new filter to this filters set
 
         :param name: the filter's name


Test patch:
diff --git a/sievelib/tests/test_factory.py b/sievelib/tests/test_factory.py
index 006faa5..cf5a709 100644
--- a/sievelib/tests/test_factory.py
+++ b/sievelib/tests/test_factory.py
@@ -577,7 +577,17 @@ def test_vacation(self):
         self.fs.addfilter(
             "test",
             [("Subject", ":matches", "*")],
-            [("vacation", ":subject", "Example Autoresponder Subject", ":days", 7, ":mime", "Example Autoresponder Body")],
+            [
+                (
+                    "vacation",
+                    ":subject",
+                    "Example Autoresponder Subject",
+                    ":days",
+                    7,
+                    ":mime",
+                    "Example Autoresponder Body",
+                )
+            ],
         )
         output = io.StringIO()
         self.fs.tosieve(output)
@@ -595,7 +605,18 @@ def test_vacation(self):
     def test_dump(self):
         self.fs.addfilter(
             "test",
-            [("Subject", ":matches", "*")], [("vacation", ":subject", "Example Autoresponder Subject", ":days", 7, ":mime", "Example Autoresponder Body")]
+            [("Subject", ":matches", "*")],
+            [
+                (
+                    "vacation",
+                    ":subject",
+                    "Example Autoresponder Subject",
+                    ":days",
+                    7,
+                    ":mime",
+                    "Example Autoresponder Body",
+                )
+            ],
         )
         output = io.StringIO()
         self.fs.dump(output)
@@ -634,8 +655,54 @@ def test_stringlist_condition(self):
             """# Filter: test
 if anyof (header :contains ["X-Foo", "X-Bar"] ["bar", "baz"]) {
 }
-"""
-            )
+""",
+        )
+
+    def test_address_string_args(self):
+        self.fs.addfilter(
+            "test",
+            [("address", ":is", "from", "user1@test.com")],
+            [("fileinto", ":create", "folder")],
+        )
+        output = io.StringIO()
+        self.fs.tosieve(output)
+        self.assertEqual(
+            output.getvalue(),
+            """require ["fileinto"];
+
+# Filter: test
+if anyof (address :is "from" "user1@test.com") {
+    fileinto :create "folder";
+}
+""",
+        )
+
+    def test_address_list_args(self):
+        self.fs.addfilter(
+            "test",
+            [
+                (
+                    "address",
+                    ":is",
+                    ["from", "reply-to"],
+                    ["user1@test.com", "user2@test.com"],
+                )
+            ],
+            [("fileinto", ":create", "folder")],
+        )
+        output = io.StringIO()
+        self.fs.tosieve(output)
+        self.assertEqual(
+            output.getvalue(),
+            """require ["fileinto"];
+
+# Filter: test
+if anyof (address :is ["from","reply-to"] ["user1@test.com","user2@test.com"]) {
+    fileinto :create "folder";
+}
+""",
+        )
+
 
 if __name__ == "__main__":
     unittest.main()