sievelib-1.1.0/ 0000755 0001750 0001750 00000000000 13210277642 013621 5 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib/ 0000755 0001750 0001750 00000000000 13210277642 015423 5 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib/factory.py 0000644 0001750 0001750 00000035736 13210256471 017457 0 ustar tonio tonio 0000000 0000000 # coding: utf-8 """ Tools for simpler sieve filters generation. This module is intented to facilitate the creation of sieve filters without having to write or to know the syntax. Only commands (control/test/action) defined in the ``commands`` module are supported. """ from __future__ import print_function, unicode_literals import sys from future.utils import python_2_unicode_compatible import six from sievelib.commands import ( get_command_instance, IfCommand, RequireCommand, FalseCommand ) @python_2_unicode_compatible class FiltersSet(object): """A set of filters.""" def __init__(self, name, filter_name_pretext=u"# Filter: ", filter_desc_pretext=u"# Description: "): """Represents a set of one or more filters :param name: the filterset's name :param filter_name_pretext: the text that is used to mark a filter name (as comment preceding the filter) :param filter_desc_pretext: the text that is used to mark a filter description """ self.name = name self.filter_name_pretext = filter_name_pretext self.filter_desc_pretext = filter_desc_pretext self.requires = [] self.filters = [] def __str__(self): target = six.StringIO() self.tosieve(target) ret = target.getvalue() target.close() return ret def __isdisabled(self, fcontent): """Tells if a filter is disabled or not Simply checks if the filter is surrounded by a "if false" test. :param fcontent: the filter's name """ if not isinstance(fcontent, IfCommand): return False if not isinstance(fcontent["test"], FalseCommand): return False return True def from_parser_result(self, parser): cpt = 1 for f in parser.result: if isinstance(f, RequireCommand): if type(f.arguments["capabilities"]) == list: [self.require(c) for c in f.arguments["capabilities"]] else: self.require(f.arguments["capabilities"]) continue name = "Unnamed rule %d" % cpt description = "" for comment in f.hash_comments: if isinstance(comment, six.binary_type): comment = comment.decode("utf-8") if comment.startswith(self.filter_name_pretext): name = comment.replace(self.filter_name_pretext, "") if comment.startswith(self.filter_desc_pretext): description = comment.replace(self.filter_desc_pretext, "") self.filters += [{"name": name, "description": description, "content": f, "enabled": not self.__isdisabled(f)}] cpt += 1 def require(self, name): """Add a new extension to the requirements list :param name: the extension's name """ name = name.strip('"') if name not in self.requires: self.requires += [name] def check_if_arg_is_extension(self, arg): """Include extension if arg requires one.""" args_using_extensions = { ":copy": "copy" } if arg in args_using_extensions: self.require(args_using_extensions[arg]) def __gen_require_command(self): """Internal method to create a RequireCommand based on requirements Called just before this object is going to be dumped. """ if not len(self.requires): return None reqcmd = get_command_instance("require") reqcmd.check_next_arg("stringlist", self.requires) return reqcmd def __quote_if_necessary(self, value): """Add double quotes to the given string if necessary :param value: the string to check :return: the string between quotes """ if not value.startswith(('"', "'")): return '"%s"' % value return value def __build_condition(self, condition, parent, tag=None): """Translate a condition to a valid sievelib Command. :param list condition: condition's definition :param ``Command`` parent: the parent :param str tag: tag to use instead of the one included into :keyword:`condition` :rtype: Command :return: the generated command """ if tag is None: tag = condition[1] cmd = get_command_instance("header", parent) cmd.check_next_arg("tag", tag) cmd.check_next_arg("string", self.__quote_if_necessary(condition[0])) cmd.check_next_arg("string", self.__quote_if_necessary(condition[2])) return cmd def __create_filter(self, conditions, actions, matchtype="anyof"): """Create a new filter A filter is composed of: * a name * one or more conditions (tests) combined together using ``matchtype`` * one or more actions A condition must be given as a 3-uple of the form:: (test's name, operator, value) An action must be given as a 2-uple of the form:: (action's name, value) It uses the "header" test to generate the sieve syntax corresponding to the given conditions. :param conditions: the list of conditions :param actions: the list of actions :param matchtype: "anyof" or "allof" """ ifcontrol = get_command_instance("if") mtypeobj = get_command_instance(matchtype, ifcontrol) for c in conditions: if c[0].startswith("not"): negate = True cname = c[0].replace("not", "", 1) else: negate = False cname = c[0] if cname in ("true", "false"): cmd = get_command_instance(c[0], ifcontrol) elif cname == "size": cmd = get_command_instance("size", ifcontrol) cmd.check_next_arg("tag", c[1]) cmd.check_next_arg("number", c[2]) elif cname == "exists": cmd = get_command_instance("exists", ifcontrol) cmd.check_next_arg( "stringlist", "[%s]" % (",".join('"%s"' % val for val in c[1:])) ) else: if c[1].startswith(':not'): cmd = self.__build_condition( c, ifcontrol, c[1].replace("not", "", 1)) not_cmd = get_command_instance("not", ifcontrol) not_cmd.check_next_arg("test", cmd) cmd = not_cmd else: cmd = self.__build_condition(c, ifcontrol) if negate: not_cmd = get_command_instance("not", ifcontrol) not_cmd.check_next_arg("test", cmd) cmd = not_cmd mtypeobj.check_next_arg("test", cmd) ifcontrol.check_next_arg("test", mtypeobj) for actdef in actions: action = get_command_instance(actdef[0], ifcontrol, False) if action.is_extension: self.require(actdef[0]) for arg in actdef[1:]: self.check_if_arg_is_extension(arg) if arg.startswith(":"): atype = "tag" else: atype = "string" arg = self.__quote_if_necessary(arg) action.check_next_arg(atype, arg, check_extension=False) ifcontrol.addchild(action) return ifcontrol def _unicode_filter_name(self, name): """Convert name to unicode if necessary.""" return ( name.decode("utf-8") if isinstance(name, six.binary_type) else name ) def addfilter(self, name, conditions, actions, matchtype="anyof"): """Add a new filter to this filters set :param name: the filter's name :param conditions: the list of conditions :param actions: the list of actions :param matchtype: "anyof" or "allof" """ ifcontrol = self.__create_filter(conditions, actions, matchtype) self.filters += [{ "name": self._unicode_filter_name(name), "content": ifcontrol, "enabled": True} ] def updatefilter( self, oldname, newname, conditions, actions, matchtype="anyof"): """Update a specific filter Instead of removing and re-creating the filter, we update the content in order to keep the original order between filters. :param oldname: the filter's current name :param newname: the filter's new name :param conditions: the list of conditions :param actions: the list of actions :param matchtype: "anyof" or "allof" """ oldname = self._unicode_filter_name(oldname) newname = self._unicode_filter_name(newname) for f in self.filters: if f["name"] == oldname: f["name"] = newname f["content"] = \ self.__create_filter(conditions, actions, matchtype) if not f["enabled"]: return self.disablefilter(newname) return True return False def replacefilter( self, oldname, sieve_filter, newname=None, description=None): """replace a specific sieve_filter Instead of removing and re-creating the sieve_filter, we update the content in order to keep the original order between filters. :param oldname: the sieve_filter's current name :param newname: the sieve_filter's new name :param sieve_filter: the sieve_filter object as get from FiltersSet.getfilter() """ oldname = self._unicode_filter_name(oldname) newname = self._unicode_filter_name(newname) if newname is None: newname = oldname for f in self.filters: if f["name"] == oldname: f["name"] = newname f["content"] = sieve_filter if description is not None: f['description'] = description if not f["enabled"]: return self.disablefilter(newname) return True return False def getfilter(self, name): """Search for a specific filter :param name: the filter's name :return: the Command object if found, None otherwise """ name = self._unicode_filter_name(name) for f in self.filters: if f["name"] == name: if not f["enabled"]: return f["content"].children[0] return f["content"] return None def removefilter(self, name): """Remove a specific filter :param name: the filter's name """ name = self._unicode_filter_name(name) for f in self.filters: if f["name"] == name: self.filters.remove(f) return True return False def enablefilter(self, name): """Enable a filter Just removes the "if false" test surrouding this filter. :param name: the filter's name """ name = self._unicode_filter_name(name) for f in self.filters: if f["name"] != name: continue if not self.__isdisabled(f["content"]): return False f["content"] = f["content"].children[0] f["enabled"] = True return True return False # raise NotFound def is_filter_disabled(self, name): """Tells if the filter is currently disabled or not :param name: the filter's name """ name = self._unicode_filter_name(name) for f in self.filters: if f["name"] == name: return self.__isdisabled(f["content"]) return True def disablefilter(self, name): """Disable a filter Instead of commenting the filter, we just surround it with a "if false { }" test. :param name: the filter's name :return: True if filter was disabled, False otherwise """ name = self._unicode_filter_name(name) ifcontrol = get_command_instance("if") falsecmd = get_command_instance("false", ifcontrol) ifcontrol.check_next_arg("test", falsecmd) for f in self.filters: if f["name"] != name: continue ifcontrol.addchild(f["content"]) f["content"] = ifcontrol f["enabled"] = False return True return False def movefilter(self, name, direction): """Moves the filter up or down :param name: the filter's name :param direction: string "up" or "down" """ name = self._unicode_filter_name(name) cpt = 0 for f in self.filters: if f["name"] == name: if direction == "up": if cpt == 0: return False self.filters.remove(f) self.filters.insert(cpt - 1, f) return True if cpt == len(self.filters) - 1: return False self.filters.remove(f) self.filters.insert(cpt + 1, f) return True cpt += 1 return False # raise not found def dump(self): """Dump this object Available for debugging purposes """ print("Dumping filters set %s\n" % self.name) cmd = self.__gen_require_command() if cmd: print("Dumping requirements") cmd.dump() print for f in self.filters: print("Filter Name: %s" % f["name"]) print("Filter Description: %s" % f["description"]) f["content"].dump() def tosieve(self, target=sys.stdout): """Generate the sieve syntax corresponding to this filters set This method will usually be called when this filters set is done. The default is to print the sieve syntax on the standard output. You can pass an opened file pointer object if you want to write the content elsewhere. :param target: file pointer where the sieve syntax will be printed """ cmd = self.__gen_require_command() if cmd: cmd.tosieve(target=target) target.write(u"\n") for f in self.filters: target.write("{}{}\n".format(self.filter_name_pretext, f["name"])) if "description" in f and f["description"]: target.write(u"{}{}\n".format( self.filter_desc_pretext, f["description"])) f["content"].tosieve(target=target) if __name__ == "__main__": fs = FiltersSet("test") fs.addfilter("rule1", [("Sender", ":is", "toto@toto.com"), ], [("fileinto", "Toto"), ]) fs.tosieve() sievelib-1.1.0/sievelib/commands.py 0000644 0001750 0001750 00000055106 13210264573 017604 0 ustar tonio tonio 0000000 0000000 # coding: utf-8 """ SIEVE commands representation This module contains classes that represent known commands. They all inherit from the Command class which provides generic method for command manipulation or parsing. There are three command types (each one represented by a class): * control (ControlCommand) : Control structures are needed to allow for multiple and conditional actions * action (ActionCommand) : Actions that can be applied on emails * test (TestCommand) : Tests are used in conditionals to decide which part(s) of the conditional to execute Finally, each known command is represented by its own class which provides extra information such as: * expected arguments, * completion callback, * etc. """ from __future__ import unicode_literals from collections import Iterable import sys from future.utils import python_2_unicode_compatible class CommandError(Exception): """Base command exception class.""" pass @python_2_unicode_compatible class UnknownCommand(CommandError): """Specific exception raised when an unknown command is encountered""" def __init__(self, name): self.name = name def __str__(self): return "unknown command %s" % self.name @python_2_unicode_compatible class BadArgument(CommandError): """Specific exception raised when a bad argument is encountered""" def __init__(self, command, seen, expected): self.command = command self.seen = seen self.expected = expected def __str__(self): return "bad argument %s for command %s (%s expected)" \ % (self.seen, self.command, self.expected) @python_2_unicode_compatible class BadValue(CommandError): """Specific exception raised when a bad argument value is encountered""" def __init__(self, argument, value): self.argument = argument self.value = value def __str__(self): return "bad value %s for argument %s" \ % (self.value, self.argument) @python_2_unicode_compatible class ExtensionNotLoaded(CommandError): """Raised when an extension is not loaded.""" def __init__(self, name): self.name = name def __str__(self): return "extension '{}' not loaded".format(self.name) # Statement elements (see RFC, section 8.3) # They are used in different commands. comparator = {"name": "comparator", "type": ["tag"], "values": [":comparator"], "extra_arg": {"type": "string", "values": ['"i;octet"', '"i;ascii-casemap"']}, "required": False} address_part = {"name": "address-part", "values": [":localpart", ":domain", ":all"], "type": ["tag"], "required": False} match_type = {"name": "match-type", "values": [":is", ":contains", ":matches"], "type": ["tag"], "required": False} class Command(object): """Generic command representation. A command is described as follow: * A name * A type * A description of supported arguments * Does it accept an unknown quantity of arguments? (ex: anyof, allof) * Does it accept children? (ie. subcommands) * Is it an extension? * Must follow only certain commands """ _type = None variable_args_nb = False accept_children = False must_follow = None is_extension = False def __init__(self, parent=None): self.parent = parent self.arguments = {} self.children = [] self.nextargpos = 0 self.required_args = -1 self.rargs_cnt = 0 self.curarg = None # for arguments that expect an argument :p (ex: :comparator) self.name = self.__class__.__name__.replace("Command", "") self.name = self.name.lower() self.hash_comments = [] def __repr__(self): return "%s (type: %s)" % (self.name, self._type) def tosieve(self, indentlevel=0, target=sys.stdout): """Generate the sieve syntax corresponding to this command Recursive method. :param indentlevel: current indentation level :param target: opened file pointer where the content will be printed """ self.__print(self.name, indentlevel, nocr=True, target=target) if self.has_arguments(): for arg in self.args_definition: if not arg["name"] in self.arguments: continue target.write(" ") value = self.arguments[arg["name"]] if "tag" in arg["type"] and arg.get("write_tag", False): target.write("%s " % arg["values"][0]) if type(value) == list: if self.__get_arg_type(arg["name"]) == ["testlist"]: target.write("(") for t in value: t.tosieve(target=target) if value.index(t) != len(value) - 1: target.write(", ") target.write(")") else: target.write( "[{}]".format(", ".join( ['"%s"' % v.strip('"') for v in value]) ) ) continue if isinstance(value, Command): value.tosieve(indentlevel, target=target) continue if "string" in arg["type"]: target.write(value) if not value.startswith('"'): target.write("\n") else: target.write(value) if not self.accept_children: if self.get_type() != "test": target.write(";\n") return if self.get_type() != "control": return target.write(" {\n") for ch in self.children: ch.tosieve(indentlevel + 4, target=target) self.__print("}", indentlevel, target=target) def __print(self, data, indentlevel, nocr=False, target=sys.stdout): text = "%s%s" % (" " * indentlevel, data) if nocr: target.write(text) else: target.write(text + "\n") def __get_arg_type(self, arg): """Return the type corresponding to the given name. :param arg: a defined argument name """ for a in self.args_definition: if a["name"] == arg: return a["type"] return None def complete_cb(self): """Completion callback Called when a command is considered as complete by the parser. """ pass def get_expected_first(self): """Return the first expected token for this command""" return None def has_arguments(self): return len(self.args_definition) != 0 def dump(self, indentlevel=0, target=sys.stdout): """Display the command Pretty printing of this command and its eventual arguments and children. (recursively) :param indentlevel: integer that indicates indentation level to apply """ self.__print(self, indentlevel, target=target) indentlevel += 4 if self.has_arguments(): for arg in self.args_definition: if not arg["name"] in self.arguments: continue value = self.arguments[arg["name"]] if type(value) == list: if self.__get_arg_type(arg["name"]) == ["testlist"]: for t in value: t.dump(indentlevel, target) else: self.__print("[" + (",".join(value)) + "]", indentlevel, target=target) continue if isinstance(value, Command): value.dump(indentlevel, target) continue self.__print(str(value), indentlevel, target=target) for ch in self.children: ch.dump(indentlevel, target) def addchild(self, child): """Add a new child to the command A child corresponds to a command located into a block (this command's block). It can be either an action or a control. :param child: the new child :return: True on succes, False otherwise """ if not self.accept_children: return False self.children += [child] return True def iscomplete(self): """Check if the command is complete Check if all required arguments have been encountered. For commands that allow an undefined number of arguments, this method always returns False. :return: True if command is complete, False otherwise """ if self.variable_args_nb: return False if self.required_args == -1: self.required_args = 0 for arg in self.args_definition: if arg["required"]: self.required_args += 1 return (not self.curarg or "extra_arg" not in self.curarg) \ and (self.rargs_cnt == self.required_args) def get_type(self): """Return the command's type""" if self._type is None: raise NotImplementedError return self._type def __is_valid_value_for_arg(self, arg, value): """Check if value is allowed for arg Some commands only allow a limited set of values. The method always returns True for methods that do not provide such a set. :param arg: the argument's name :param value: the value to check :return: True on succes, False otherwise """ if "values" not in arg: return True return value.lower() in arg["values"] def check_next_arg(self, atype, avalue, add=True, check_extension=True): """Argument validity checking This method is usually used by the parser to check if detected argument is allowed for this command. We make a distinction between required and optional arguments. Optional (or tagged) arguments can be provided unordered but not the required ones. A special handling is also done for arguments that require an argument (example: the :comparator argument expects a string argument). The "testlist" type is checked separately as we can't know in advance how many arguments will be provided. If the argument is incorrect, the method raises the appropriate exception, or return False to let the parser handle the exception. :param atype: the argument's type :param avalue: the argument's value :param add: indicates if this argument should be recorded on success :param check_extension: raise ExtensionNotLoaded if extension not loaded :return: True on success, False otherwise """ if not self.has_arguments(): return False if self.iscomplete(): return False if self.curarg is not None and "extra_arg" in self.curarg: if atype in self.curarg["extra_arg"]["type"]: if "values" not in self.curarg["extra_arg"] \ or avalue in self.curarg["extra_arg"]["values"]: if add: self.arguments[self.curarg["name"]] = avalue self.curarg = None return True raise BadValue(self.curarg["name"], avalue) failed = False pos = self.nextargpos while pos < len(self.args_definition): curarg = self.args_definition[pos] if curarg["required"]: if curarg["type"] == ["testlist"]: if atype != "test": failed = True elif add: if not curarg["name"] in self.arguments: self.arguments[curarg["name"]] = [] self.arguments[curarg["name"]] += [avalue] elif atype not in curarg["type"] or \ not self.__is_valid_value_for_arg(curarg, avalue): failed = True else: self.curarg = curarg self.rargs_cnt += 1 self.nextargpos = pos + 1 if add: self.arguments[curarg["name"]] = avalue break if atype in curarg["type"]: ext = curarg.get("extension") condition = ( check_extension and ext and ext not in RequireCommand.loaded_extensions) if condition: raise ExtensionNotLoaded(ext) if self.__is_valid_value_for_arg(curarg, avalue): if "extra_arg" in curarg: self.curarg = curarg break if add: self.arguments[curarg["name"]] = avalue break pos += 1 if failed: raise BadArgument(self.name, avalue, self.args_definition[pos]["type"]) return True def __contains__(self, name): """Check if argument is provided with command.""" return name in self.arguments def __getitem__(self, name): """Shorcut to access a command argument :param name: the argument's name """ found = False for ad in self.args_definition: if ad["name"] == name: found = True break if not found: raise KeyError(name) if name not in self.arguments: raise KeyError(name) return self.arguments[name] class ControlCommand(Command): """Indermediate class to represent "control" commands""" _type = "control" class RequireCommand(ControlCommand): """The 'require' command This class has one big difference with others as it is used to store loaded extension names. (The result is we can check for unloaded extensions during the parsing) """ args_definition = [ {"name": "capabilities", "type": ["string", "stringlist"], "required": True} ] loaded_extensions = [] def complete_cb(self): if type(self.arguments["capabilities"]) != list: exts = [self.arguments["capabilities"]] else: exts = self.arguments["capabilities"] for ext in exts: ext = ext.strip('"') if ext not in RequireCommand.loaded_extensions: RequireCommand.loaded_extensions += [ext] class StopCommand(ControlCommand): args_definition = [] class IfCommand(ControlCommand): accept_children = True args_definition = [ {"name": "test", "type": ["test"], "required": True} ] def get_expected_first(self): return ["identifier"] class ElsifCommand(ControlCommand): accept_children = True must_follow = ["if", "elsif"] args_definition = [ {"name": "test", "type": ["test"], "required": True} ] def get_expected_first(self): return ["identifier"] class ElseCommand(ControlCommand): accept_children = True must_follow = ["if", "elsif"] args_definition = [] class ActionCommand(Command): """Indermediate class to represent "action" commands""" _type = "action" class FileintoCommand(ActionCommand): is_extension = True args_definition = [ {"name": "copy", "type": ["tag"], "values": [":copy"], "required": False, "extension": "copy"}, {"name": "mailbox", "type": ["string"], "required": True} ] class RedirectCommand(ActionCommand): args_definition = [ {"name": "copy", "type": ["tag"], "values": [":copy"], "required": False, "extension": "copy"}, {"name": "address", "type": ["string"], "required": True} ] class RejectCommand(ActionCommand): is_extension = True args_definition = [ {"name": "text", "type": ["string"], "required": True} ] class KeepCommand(ActionCommand): args_definition = [] class DiscardCommand(ActionCommand): args_definition = [] class TestCommand(Command): """Indermediate class to represent "test" commands""" _type = "test" class AddressCommand(TestCommand): args_definition = [ comparator, address_part, match_type, {"name": "header-list", "type": ["string", "stringlist"], "required": True}, {"name": "key-list", "type": ["string", "stringlist"], "required": True} ] class AllofCommand(TestCommand): accept_children = True variable_args_nb = True args_definition = [ {"name": "tests", "type": ["testlist"], "required": True} ] def get_expected_first(self): return ["left_parenthesis"] class AnyofCommand(TestCommand): accept_children = True variable_args_nb = True args_definition = [ {"name": "tests", "type": ["testlist"], "required": True} ] def get_expected_first(self): return ["left_parenthesis"] class EnvelopeCommand(TestCommand): args_definition = [ comparator, address_part, match_type, {"name": "header-list", "type": ["string", "stringlist"], "required": True}, {"name": "key-list", "type": ["string", "stringlist"], "required": True} ] class ExistsCommand(TestCommand): args_definition = [ {"name": "header-names", "type": ["stringlist"], "required": True} ] class TrueCommand(TestCommand): args_definition = [] class FalseCommand(TestCommand): args_definition = [] class HeaderCommand(TestCommand): args_definition = [ comparator, match_type, {"name": "header-names", "type": ["string", "stringlist"], "required": True}, {"name": "key-list", "type": ["string", "stringlist"], "required": True} ] class NotCommand(TestCommand): accept_children = True args_definition = [ {"name": "test", "type": ["test"], "required": True} ] def get_expected_first(self): return ["identifier"] class SizeCommand(TestCommand): args_definition = [ {"name": "comparator", "type": ["tag"], "values": [":over", ":under"], "required": True}, {"name": "limit", "type": ["number"], "required": True}, ] class VacationCommand(ActionCommand): args_definition = [ {"name": "subject", "type": ["tag"], "write_tag": True, "values": [":subject"], "extra_arg": {"type": "string"}, "required": False}, {"name": "days", "type": ["tag"], "write_tag": True, "values": [":days"], "extra_arg": {"type": "number"}, "required": False}, {"name": "from", "type": ["tag"], "write_tag": True, "values": [":from"], "extra_arg": {"type": "string"}, "required": False}, {"name": "addresses", "type": ["tag"], "write_tag": True, "values": [":addresses"], "extra_arg": {"type": ["string", "stringlist"]}, "required": False}, {"name": "handle", "type": ["tag"], "write_tag": True, "values": [":handle"], "extra_arg": {"type": "string"}, "required": False}, {"name": "mime", "type": ["tag"], "write_tag": True, "values": [":mime"], "required": False}, {"name": "reason", "type": ["string"], "required": True}, ] class SetCommand(ControlCommand): """currentdate command, part of the variables extension http://tools.ietf.org/html/rfc5229 """ is_extension = True args_definition = [ {"name": "startend", "type": ["string"], "required": True}, {"name": "date", "type": ["string"], "required": True} ] class CurrentdateCommand(ControlCommand): """currentdate command, part of the date extension http://tools.ietf.org/html/rfc5260#section-5 """ is_extension = True accept_children = True args_definition = [ {"name": "zone", "type": ["tag"], "write_tag": True, "values": [":zone"], "extra_arg": {"type": "string"}, "required": False}, {"name": "match-value", "type": ["tag"], "required": True}, {"name": "comparison", "type": ["string"], "required": True}, {"name": "match-against", "type": ["string"], "required": True}, {"name": "match-against-field", "type": ["string"], "required": True} ] def add_commands(cmds): """ Adds one or more commands to the module namespace. Commands must end in "Command" to be added. Example (see tests/parser.py): sievelib.commands.add_commands(MytestCommand) :param cmds: a single Command Object or list of Command Objects """ if not isinstance(cmds, Iterable): cmds = [cmds] for command in cmds: if command.__name__.endswith("Command"): globals()[command.__name__] = command def get_command_instance(name, parent=None, checkexists=True): """Try to guess and create the appropriate command instance Given a command name (encountered by the parser), construct the associated class name and, if known, return a new instance. If the command is not known or has not been loaded using require, an UnknownCommand exception is raised. :param name: the command's name :param parent: the eventual parent command :return: a new class instance """ # Mapping between extension names and command names extension_map = { 'date': set(['currentdate']), 'variables': set(['set']) } extname = name for extension in extension_map: if name in extension_map[extension]: extname = extension break cname = "%sCommand" % name.lower().capitalize() if cname not in globals() or \ (checkexists and globals()[cname].is_extension and extname not in RequireCommand.loaded_extensions): raise UnknownCommand(name) return globals()[cname](parent) sievelib-1.1.0/sievelib/managesieve.py 0000644 0001750 0001750 00000054352 13174356535 020302 0 ustar tonio tonio 0000000 0000000 # coding: utf-8 """ A MANAGESIEVE client. A protocol for securely managing Sieve scripts on a remote server. This protocol allows a user to have multiple scripts, and also alerts a user to syntactically flawed scripts. Implementation based on RFC 5804. """ from __future__ import print_function import base64 import re import socket import ssl from future.utils import python_2_unicode_compatible import six from .digest_md5 import DigestMD5 from . import tools CRLF = b"\r\n" KNOWN_CAPABILITIES = [u"IMPLEMENTATION", u"SASL", u"SIEVE", u"STARTTLS", u"NOTIFY", u"LANGUAGE", u"VERSION"] SUPPORTED_AUTH_MECHS = [u"DIGEST-MD5", u"PLAIN", u"LOGIN"] class Error(Exception): pass @python_2_unicode_compatible class Response(Exception): def __init__(self, code, data): self.code = code self.data = data def __str__(self): return "%s %s" % (self.code, self.data) @python_2_unicode_compatible class Literal(Exception): def __init__(self, value): self.value = value def __str__(self): return "{%d}" % self.value def authentication_required(meth): """Simple class method decorator. Checks if the client is currently connected. :param meth: the original called method """ def check(cls, *args, **kwargs): if cls.authenticated: return meth(cls, *args, **kwargs) raise Error("Authentication required") return check class Client(object): read_size = 4096 read_timeout = 5 def __init__(self, srvaddr, srvport=4190, debug=False): self.srvaddr = srvaddr self.srvport = srvport self.__debug = debug self.sock = None self.__read_buffer = b"" self.authenticated = False self.errcode = None self.__capabilities = {} self.__respcode_expr = re.compile(br"(OK|NO|BYE)\s*(.+)?") self.__error_expr = re.compile(br'(\([\w/-]+\))?\s*(".+")') self.__size_expr = re.compile(br"\{(\d+)\+?\}") self.__active_expr = re.compile(br"ACTIVE", re.IGNORECASE) def __del__(self): if self.sock is not None: self.sock.close() self.sock = None def __dprint(self, message): if not self.__debug: return print("DEBUG: %s" % message) def __read_block(self, size): """Read a block of 'size' bytes from the server. An internal buffer is used to read data from the server. If enough data is available from it, we return that data. Eventually, we try to grab the missing part from the server for Client.read_timeout seconds. If no data can be retrieved, it is considered as a fatal error and an 'Error' exception is raised. :param size: number of bytes to read :rtype: string :returns: the read block (can be empty) """ buf = b"" if len(self.__read_buffer): limit = ( size if size <= len(self.__read_buffer) else len(self.__read_buffer) ) buf = self.__read_buffer[:limit] self.__read_buffer = self.__read_buffer[limit:] size -= limit if not size: return buf try: buf += self.sock.recv(size) except (socket.timeout, ssl.SSLError): raise Error("Failed to read %d bytes from the server" % size) self.__dprint(buf) return buf def __read_line(self): """Read one line from the server. An internal buffer is used to read data from the server (blocks of Client.read_size bytes). If the buffer is not empty, we try to find an entire line to return. If we failed, we try to read new content from the server for Client.read_timeout seconds. If no data can be retrieved, it is considered as a fatal error and an 'Error' exception is raised. :rtype: string :return: the read line """ ret = b"" while True: try: pos = self.__read_buffer.index(CRLF) ret = self.__read_buffer[:pos] self.__read_buffer = self.__read_buffer[pos + len(CRLF):] break except ValueError: pass try: nval = self.sock.recv(self.read_size) self.__dprint(nval) if not len(nval): break self.__read_buffer += nval except (socket.timeout, ssl.SSLError): raise Error("Failed to read data from the server") if len(ret): m = self.__size_expr.match(ret) if m: raise Literal(int(m.group(1))) m = self.__respcode_expr.match(ret) if m: if m.group(1) == b"BYE": raise Error("Connection closed by server") if m.group(1) == b"NO": self.__parse_error(m.group(2)) raise Response(m.group(1), m.group(2)) return ret def __read_response(self, nblines=-1): """Read a response from the server. In the usual case, we read lines until we find one that looks like a response (OK|NO|BYE\s*(.+)?). If *nblines* > 0, we read excactly nblines before returning. :param nblines: number of lines to read (default : -1) :rtype: tuple :return: a tuple of the form (code, data, response). If nblines is provided, code and data can be equal to None. """ resp, code, data = (b"", None, None) cpt = 0 while True: try: line = self.__read_line() except Response as inst: code = inst.code data = inst.data break except Literal as inst: resp += self.__read_block(inst.value) if not resp.endswith(CRLF): resp += self.__read_line() + CRLF continue if not len(line): continue resp += line + CRLF cpt += 1 if nblines != -1 and cpt == nblines: break return (code, data, resp) def __prepare_args(self, args): """Format command arguments before sending them. Command arguments of type string must be quoted, the only exception concerns size indication (of the form {\d\+?}). :param args: list of arguments :return: a list for transformed arguments """ ret = [] for a in args: if isinstance(a, six.binary_type): if self.__size_expr.match(a): ret += [a] else: ret += [b'"' + a + b'"'] continue ret += [bytes(str(a).encode("utf-8"))] return ret def __send_command( self, name, args=None, withcontent=False, extralines=None, nblines=-1): """Send a command to the server. If args is not empty, we concatenate the given command with the content of this list. If extralines is not empty, they are sent one by one to the server. (CLRF are automatically appended to them) We wait for a response just after the command has been sent. :param name: the command to sent :param args: a list of arguments for this command :param withcontent: tells the function to return the server's response or not :param extralines: a list of extra lines to sent after the command :param nblines: the number of response lines to read (all by default) :returns: a tuple of the form (code, data[, response]) """ tosend = name.encode("utf-8") if args: tosend += b" " + b" ".join(self.__prepare_args(args)) self.__dprint(b"Command: " + tosend) self.sock.sendall(tosend + CRLF) if extralines: for l in extralines: self.sock.sendall(l + CRLF) code, data, content = self.__read_response(nblines) if isinstance(code, six.binary_type): code = code.decode("utf-8") data = data.decode("utf-8") if withcontent: return (code, data, content) return (code, data) def __get_capabilities(self): code, data, capabilities = self.__read_response() if code == "NO": return False for l in capabilities.splitlines(): parts = l.split(None, 1) cname = parts[0].strip(b'"').decode("utf-8") if cname not in KNOWN_CAPABILITIES: continue self.__capabilities[cname] = ( parts[1].strip(b'"').decode("utf-8") if len(parts) > 1 else None ) return True def __parse_error(self, text): """Parse an error received from the server. if text corresponds to a size indication, we grab the remaining content from the server. Otherwise, we try to match an error of the form \(\w+\)?\s*".+" On succes, the two public members errcode and errmsg are filled with the parsing results. :param text: the response to parse """ m = self.__size_expr.match(text) if m is not None: self.errcode = b"" self.errmsg = self.__read_block(int(m.group(1)) + 2) return m = self.__error_expr.match(text) if m is None: raise Error("Bad error message") if m.group(1) is not None: self.errcode = m.group(1).strip(b"()") else: self.errcode = b"" self.errmsg = m.group(2).strip(b'"') def _plain_authentication(self, login, password, authz_id=b""): """SASL PLAIN authentication :param login: username :param password: clear password :return: True on success, False otherwise. """ if isinstance(login, six.text_type): login = login.encode("utf-8") if isinstance(login, six.text_type): password = password.encode("utf-8") params = base64.b64encode(b'\0'.join([authz_id, login, password])) code, data = self.__send_command("AUTHENTICATE", [b"PLAIN", params]) if code == "OK": return True return False def _login_authentication(self, login, password, authz_id=""): """SASL LOGIN authentication :param login: username :param password: clear password :return: True on success, False otherwise. """ extralines = [b'"%s"' % base64.b64encode(login.encode("utf-8")), b'"%s"' % base64.b64encode(password.encode("utf-8"))] code, data = self.__send_command("AUTHENTICATE", [b"LOGIN"], extralines=extralines) if code == "OK": return True return False def _digest_md5_authentication(self, login, password, authz_id=""): """SASL DIGEST-MD5 authentication :param login: username :param password: clear password :return: True on success, False otherwise. """ code, data, challenge = \ self.__send_command("AUTHENTICATE", [b"DIGEST-MD5"], withcontent=True, nblines=1) dmd5 = DigestMD5(challenge, "sieve/%s" % self.srvaddr) code, data, challenge = self.__send_command( '"%s"' % dmd5.response(login, password, authz_id), withcontent=True, nblines=1 ) if not challenge: return False if not dmd5.check_last_challenge(login, password, challenge): self.errmsg = "Bad challenge received from server" return False code, data = self.__send_command('""') if code == "OK": return True return False def __authenticate(self, login, password, authz_id=b"", authmech=None): """AUTHENTICATE command Actually, it is just a wrapper to the real commands (one by mechanism). We try all supported mechanisms (from the strongest to the weakest) until we find one supported by the server. Then we try to authenticate (only once). :param login: username :param password: clear password :param authz_id: authorization ID :param authmech: prefered authentication mechanism :return: True on success, False otherwise """ if "SASL" not in self.__capabilities: raise Error("SASL not supported by the server") srv_mechanisms = self.get_sasl_mechanisms() if authmech is None or authmech not in SUPPORTED_AUTH_MECHS: mech_list = SUPPORTED_AUTH_MECHS else: mech_list = [authmech] for mech in mech_list: if mech not in srv_mechanisms: continue mech = mech.lower().replace("-", "_") auth_method = getattr(self, "_%s_authentication" % mech) if auth_method(login, password, authz_id): self.authenticated = True return True return False self.errmsg = b"No suitable mechanism found" return False def __starttls(self, keyfile=None, certfile=None): """STARTTLS command See MANAGESIEVE specifications, section 2.2. :param keyfile: an eventual private key to use :param certfile: an eventual certificate to use :rtype: boolean """ if not self.has_tls_support(): raise Error("STARTTLS not supported by the server") code, data = self.__send_command("STARTTLS") if code != "OK": return False try: nsock = ssl.wrap_socket(self.sock, keyfile, certfile) except ssl.SSLError as e: raise Error("SSL error: %s" % str(e)) self.sock = nsock self.__capabilities = {} self.__get_capabilities() return True def get_implementation(self): """Returns the IMPLEMENTATION value. It is read from server capabilities. (see the CAPABILITY command) :rtype: string """ return self.__capabilities["IMPLEMENTATION"] def get_sasl_mechanisms(self): """Returns the supported authentication mechanisms. They're read from server capabilities. (see the CAPABILITY command) :rtype: list of string """ return self.__capabilities["SASL"].split() def has_tls_support(self): """Tells if the server has STARTTLS support or not. It is read from server capabilities. (see the CAPABILITY command) :rtype: boolean """ return "STARTTLS" in self.__capabilities def get_sieve_capabilities(self): """Returns the SIEVE extensions supported by the server. They're read from server capabilities. (see the CAPABILITY command) :rtype: string """ if isinstance(self.__capabilities["SIEVE"], six.string_types): self.__capabilities["SIEVE"] = self.__capabilities["SIEVE"].split() return self.__capabilities["SIEVE"] def connect( self, login, password, authz_id=b"", starttls=False, authmech=None): """Establish a connection with the server. This function must be used. It read the server capabilities and wraps calls to STARTTLS and AUTHENTICATE commands. :param login: username :param password: clear password :param starttls: use a TLS connection or not :param authmech: prefered authenticate mechanism :rtype: boolean """ try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.srvaddr, self.srvport)) self.sock.settimeout(Client.read_timeout) except socket.error as msg: raise Error("Connection to server failed: %s" % str(msg)) if not self.__get_capabilities(): raise Error("Failed to read capabilities from server") if starttls and not self.__starttls(): return False if self.__authenticate(login, password, authz_id, authmech): return True return False def logout(self): """Disconnect from the server See MANAGESIEVE specifications, section 2.3 """ self.__send_command("LOGOUT") def capability(self): """Ask server capabilities. See MANAGESIEVE specifications, section 2.4 This command does not affect capabilities recorded by this client. :rtype: string """ code, data, capabilities = ( self.__send_command("CAPABILITY", withcontent=True)) if code == "OK": return capabilities return None @authentication_required def havespace(self, scriptname, scriptsize): """Ask for available space. See MANAGESIEVE specifications, section 2.5 :param scriptname: script's name :param scriptsize: script's size :rtype: boolean """ code, data = self.__send_command( "HAVESPACE", [scriptname.encode("utf-8"), scriptsize]) if code == "OK": return True return False @authentication_required def listscripts(self): """List available scripts. See MANAGESIEVE specifications, section 2.7 :returns: a 2-uple (active script, [script1, ...]) """ code, data, listing = self.__send_command( "LISTSCRIPTS", withcontent=True) if code == "NO": return None ret = [] active_script = None for l in listing.splitlines(): if self.__size_expr.match(l): continue m = re.match(br'"([^"]+)"\s*(.+)', l) if m is None: ret += [l.strip(b'"').decode("utf-8")] continue script = m.group(1).decode("utf-8") if self.__active_expr.match(m.group(2)): active_script = script continue ret += [script] self.__dprint(ret) return (active_script, ret) @authentication_required def getscript(self, name): """Download a script from the server See MANAGESIEVE specifications, section 2.9 :param name: script's name :rtype: string :returns: the script's content on succes, None otherwise """ code, data, content = self.__send_command( "GETSCRIPT", [name.encode("utf-8")], withcontent=True) if code == "OK": lines = content.splitlines() if self.__size_expr.match(lines[0]) is not None: lines = lines[1:] return u"\n".join([line.decode("utf-8") for line in lines]) return None @authentication_required def putscript(self, name, content): """Upload a script to the server See MANAGESIEVE specifications, section 2.6 :param name: script's name :param content: script's content :rtype: boolean """ content = tools.to_bytes(content) content = tools.to_bytes("{%d+}" % len(content)) + CRLF + content code, data = ( self.__send_command("PUTSCRIPT", [name.encode("utf-8"), content])) if code == "OK": return True return False @authentication_required def deletescript(self, name): """Delete a script from the server See MANAGESIEVE specifications, section 2.10 :param name: script's name :rtype: boolean """ code, data = self.__send_command( "DELETESCRIPT", [name.encode("utf-8")]) if code == "OK": return True return False @authentication_required def renamescript(self, oldname, newname): """Rename a script on the server See MANAGESIEVE specifications, section 2.11.1 As this command is optional, we emulate it if the server does not support it. :param oldname: current script's name :param newname: new script's name :rtype: boolean """ if "VERSION" in self.__capabilities: code, data = self.__send_command( "RENAMESCRIPT", [oldname.encode("utf-8"), newname.encode("utf-8")]) if code == "OK": return True return False (active_script, scripts) = self.listscripts() condition = ( oldname != active_script and (scripts is None or oldname not in scripts) ) if condition: self.errmsg = b"Old script does not exist" return False if newname in scripts: self.errmsg = b"New script already exists" return False oldscript = self.getscript(oldname) if oldscript is None: return False if not self.putscript(newname, oldscript): return False if active_script == oldname: if not self.setactive(newname): return False if not self.deletescript(oldname): return False return True @authentication_required def setactive(self, scriptname): """Define the active script See MANAGESIEVE specifications, section 2.8 If scriptname is empty, the current active script is disabled, ie. there will be no active script anymore. :param scriptname: script's name :rtype: boolean """ code, data = self.__send_command( "SETACTIVE", [scriptname.encode("utf-8")]) if code == "OK": return True return False @authentication_required def checkscript(self, content): """Check whether a script is valid See MANAGESIEVE specifications, section 2.12 :param name: script's content :rtype: boolean """ if "VERSION" not in self.__capabilities: raise NotImplementedError( "server does not support CHECKSCRIPT command") content = tools.to_bytes( u"{%d+}%s%s" % (len(content), str(CRLF), content)) code, data = self.__send_command("CHECKSCRIPT", [content]) if code == "OK": return True return False sievelib-1.1.0/sievelib/__init__.py 0000644 0001750 0001750 00000000000 12307043331 017512 0 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib/tests/ 0000755 0001750 0001750 00000000000 13210277642 016565 5 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib/tests/__init__.py 0000644 0001750 0001750 00000000000 12313016362 020655 0 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib/tests/files/ 0000755 0001750 0001750 00000000000 13210277642 017667 5 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib/tests/files/utf8_sieve.txt 0000644 0001750 0001750 00000000266 12307043331 022505 0 ustar tonio tonio 0000000 0000000 require ["fileinto", "reject"]; # Filter: UTF8 Test Filter äöüß 汉语/漢語 Hànyǔ if allof (header :contains ["Subject"] ["€ 300"]) { fileinto "Spam"; stop; } sievelib-1.1.0/sievelib/tests/test_parser.py 0000644 0001750 0001750 00000040327 13210252676 021500 0 ustar tonio tonio 0000000 0000000 # coding: utf-8 """ Unit tests for the SIEVE language parser. """ import unittest import os.path import codecs import six 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): self.__checkCompilation(script, True) def compilation_ko(self, script): self.__checkCompilation(script, False) def representation_is(self, content): target = six.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 = six.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: control) else (type: control) reject (type: action) text: ================================ Your email has been canceled too ================================ . """) 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) "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: control) """) 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_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
I'm at the beach relaxing.
Mmmm, surf...
--foo--
.
;
""")
def test_reject_extension(self):
self.compilation_ok(b"""
require "reject";
if header :contains "subject" "viagra" {
reject;
}
""")
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_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_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) {
}
""")
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;")
class DateCommands(SieveTest):
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;
}""")
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";
}
""")
if __name__ == "__main__":
unittest.main()
sievelib-1.1.0/sievelib/tests/test_managesieve.py 0000644 0001750 0001750 00000012626 13174356445 022501 0 ustar tonio tonio 0000000 0000000 # coding: utf-8
"""Managesieve test cases."""
import unittest
try:
from unittest import mock
except ImportError:
import mock
from sievelib import managesieve
CAPABILITIES = (
b'"IMPLEMENTATION" "Example1 ManageSieved v001"\r\n'
b'"VERSION" "1.0"\r\n'
b'"SASL" "PLAIN SCRAM-SHA-1 GSSAPI"\r\n'
b'"SIEVE" "fileinto vacation"\r\n'
b'"STARTTLS"\r\n'
)
CAPABILITIES_WITHOUT_VERSION = (
b'"IMPLEMENTATION" "Example1 ManageSieved v001"\r\n'
b'"SASL" "PLAIN SCRAM-SHA-1 GSSAPI"\r\n'
b'"SIEVE" "fileinto vacation"\r\n'
b'"STARTTLS"\r\n'
)
AUTHENTICATION = (
CAPABILITIES +
b'OK "Dovecot ready."\r\n'
b'OK "Logged in."\r\n'
)
LISTSCRIPTS = (
b'"summer_script"\r\n'
b'"vac\xc3\xa0tion_script"\r\n'
b'{13}\r\n'
b'clever"script\r\n'
b'"main_script" ACTIVE\r\n'
b'OK "Listscripts completed."\r\n'
)
GETSCRIPT = (
b'{54}\r\n'
b'#this is my wonderful script\r\n'
b'reject "I reject all";\r\n'
b'OK "Getscript completed."\r\n'
)
@mock.patch("socket.socket")
class ManageSieveTestCase(unittest.TestCase):
"""Managesieve test cases."""
def setUp(self):
"""Create client."""
self.client = managesieve.Client("127.0.0.1")
def authenticate(self, mock_socket):
"""Authenticate client."""
mock_socket.return_value.recv.side_effect = (AUTHENTICATION, )
self.client.connect(b"user", b"password")
def test_connection(self, mock_socket):
"""Test connection."""
self.authenticate(mock_socket)
self.assertEqual(
self.client.get_sieve_capabilities(), ["fileinto", "vacation"])
mock_socket.return_value.recv.side_effect = (b"OK test\r\n", )
self.client.logout()
def test_capabilities(self, mock_socket):
"""Test capabilities command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (
CAPABILITIES + b'OK "Capability completed."\r\n', )
capabilities = self.client.capability()
self.assertEqual(capabilities, CAPABILITIES)
def test_listscripts(self, mock_socket):
"""Test listscripts command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (LISTSCRIPTS, )
active_script, others = self.client.listscripts()
self.assertEqual(active_script, "main_script")
self.assertEqual(
others, [u'summer_script', u'vacàtion_script', u'clever"script'])
def test_getscript(self, mock_socket):
"""Test getscript command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (GETSCRIPT, )
content = self.client.getscript("main_script")
self.assertEqual(
content, u'#this is my wonderful script\nreject "I reject all";')
def test_putscript(self, mock_socket):
"""Test putscript command."""
self.authenticate(mock_socket)
script = """require ["fileinto"];
if envelope :contains "to" "tmartin+sent" {
fileinto "INBOX.sent";
}
"""
mock_socket.return_value.recv.side_effect = (
b'OK "putscript completed."\r\n', )
self.assertTrue(self.client.putscript(u"test_script", script))
def test_deletescript(self, mock_socket):
"""Test deletescript command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (
b'OK "deletescript completed."\r\n', )
self.assertTrue(self.client.deletescript(u"test_script"))
def test_checkscript(self, mock_socket):
"""Test checkscript command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (
b'OK "checkscript completed."\r\n', )
script = "#comment\r\nInvalidSieveCommand\r\n"
self.assertTrue(self.client.checkscript(script))
def test_setactive(self, mock_socket):
"""Test setactive command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (
b'OK "setactive completed."\r\n', )
self.assertTrue(self.client.setactive(u"test_script"))
def test_havespace(self, mock_socket):
"""Test havespace command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (
b'OK "havespace completed."\r\n', )
self.assertTrue(self.client.havespace(u"test_script", 1000))
def test_renamescript(self, mock_socket):
"""Test renamescript command."""
self.authenticate(mock_socket)
mock_socket.return_value.recv.side_effect = (
b'OK "renamescript completed."\r\n', )
self.assertTrue(self.client.renamescript(u"old_script", u"new_script"))
def test_renamescript_simulated(self, mock_socket):
"""Test renamescript command simulation."""
mock_socket.return_value.recv.side_effect = (
CAPABILITIES_WITHOUT_VERSION +
b'OK "Dovecot ready."\r\n'
b'OK "Logged in."\r\n',
)
self.client.connect(b"user", b"password")
mock_socket.return_value.recv.side_effect = (
LISTSCRIPTS,
GETSCRIPT,
b'OK "putscript completed."\r\n',
b'OK "setactive completed."\r\n',
b'OK "deletescript completed."\r\n'
)
self.assertTrue(
self.client.renamescript(u"main_script", u"new_script"))
if __name__ == "__main__":
unittest.main()
sievelib-1.1.0/sievelib/tests/test_factory.py 0000644 0001750 0001750 00000011547 13210252676 021655 0 ustar tonio tonio 0000000 0000000 # coding: utf-8
from __future__ import unicode_literals
import unittest
import six
from sievelib.factory import FiltersSet
class FactoryTestCase(unittest.TestCase):
def setUp(self):
self.fs = FiltersSet("test")
def test_add_header_filter(self):
output = six.StringIO()
self.fs.addfilter(
"rule1",
[('Sender', ":is", 'toto@toto.com'), ],
[("fileinto", ":copy", "Toto"), ])
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["fileinto", "copy"];
# Filter: rule1
if anyof (header :is "Sender" "toto@toto.com") {
fileinto :copy "Toto";
}
""")
output.close()
def test_use_action_with_tag(self):
output = six.StringIO()
self.fs.addfilter(
"rule1",
[('Sender', ":is", 'toto@toto.com'), ],
[("redirect", ":copy", "toto@titi.com"), ])
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["copy"];
# Filter: rule1
if anyof (header :is "Sender" "toto@toto.com") {
redirect :copy "toto@titi.com";
}
""")
output.close()
def test_add_header_filter_with_not(self):
output = six.StringIO()
self.fs.addfilter(
"rule1",
[('Sender', ":notcontains", 'toto@toto.com')],
[("fileinto", 'Toto')])
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["fileinto"];
# Filter: rule1
if anyof (not header :contains "Sender" "toto@toto.com") {
fileinto "Toto";
}
""")
def test_add_exists_filter(self):
output = six.StringIO()
self.fs.addfilter(
"rule1",
[('exists', "list-help", "list-unsubscribe",
"list-subscribe", "list-owner")],
[("fileinto", 'Toto')]
)
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["fileinto"];
# Filter: rule1
if anyof (exists ["list-help","list-unsubscribe","list-subscribe","list-owner"]) {
fileinto "Toto";
}
""")
def test_add_exists_filter_with_not(self):
output = six.StringIO()
self.fs.addfilter(
"rule1",
[('notexists', "list-help", "list-unsubscribe",
"list-subscribe", "list-owner")],
[("fileinto", 'Toto')]
)
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["fileinto"];
# Filter: rule1
if anyof (not exists ["list-help","list-unsubscribe","list-subscribe","list-owner"]) {
fileinto "Toto";
}
""")
def test_add_size_filter(self):
output = six.StringIO()
self.fs.addfilter(
"rule1",
[('size', ":over", "100k")],
[("fileinto", 'Totoéé')]
)
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["fileinto"];
# Filter: rule1
if anyof (size :over 100k) {
fileinto "Totoéé";
}
""")
def test_remove_filter(self):
self.fs.addfilter("rule1",
[('Sender', ":is", 'toto@toto.com')],
[("fileinto", 'Toto')])
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.assertEqual(self.fs.removefilter("rule1"), True)
self.assertIs(self.fs.getfilter("rule1"), None)
def test_disablefilter(self):
"""
FIXME: Extra spaces are written between if and anyof, why?!
"""
self.fs.addfilter("rule1",
[('Sender', ":is", 'toto@toto.com')],
[("fileinto", 'Toto')])
self.assertIsNot(self.fs.getfilter("rule1"), None)
self.assertEqual(self.fs.disablefilter("rule1"), True)
output = six.StringIO()
self.fs.tosieve(output)
self.assertEqual(output.getvalue(), """require ["fileinto"];
# Filter: rule1
if false {
if anyof (header :is "Sender" "toto@toto.com") {
fileinto "Toto";
}
}
""")
output.close()
self.assertEqual(self.fs.is_filter_disabled("rule1"), True)
def test_add_filter_unicode(self):
"""Add a filter containing unicode data."""
name = u"Test\xe9".encode("utf-8")
self.fs.addfilter(
name,
[('Sender', ":is", 'toto@toto.com'), ],
[("fileinto", 'Toto'), ])
self.assertIsNot(self.fs.getfilter("Testé"), None)
self.assertEqual("{}".format(self.fs), """require ["fileinto"];
# Filter: Testé
if anyof (header :is "Sender" "toto@toto.com") {
fileinto "Toto";
}
""")
if __name__ == "__main__":
unittest.main()
sievelib-1.1.0/sievelib/tools.py 0000644 0001750 0001750 00000000360 13173113726 017134 0 ustar tonio tonio 0000000 0000000 """Some tools."""
import six
def to_bytes(s, encoding="utf-8"):
"""Convert a string to bytes."""
if isinstance(s, six.binary_type):
return s
if six.PY3:
return bytes(s, encoding)
return s.encode(encoding)
sievelib-1.1.0/sievelib/parser.py 0000755 0001750 0001750 00000037223 13210252676 017303 0 ustar tonio tonio 0000000 0000000 #!/usr/bin/env python
# coding: utf-8
"""
This module provides a simple but functional parser for the SIEVE
language used to filter emails.
This implementation is based on RFC 5228 (http://tools.ietf.org/html/rfc5228)
"""
from __future__ import print_function
import re
import sys
from future.utils import python_2_unicode_compatible, text_type
import six
from sievelib.commands import (
get_command_instance, CommandError, RequireCommand)
@python_2_unicode_compatible
class ParseError(Exception):
"""Generic parsing error"""
def __init__(self, msg):
self.msg = msg
def __str__(self):
return "parsing error: %s" % self.msg
class Lexer(object):
"""
The lexical analysis part.
This class provides a simple way to define tokens (with patterns)
to be detected.
Patterns are provided into a list of 2-uple. Each 2-uple consists
of a token name and an associated pattern, example:
[(b"left_bracket", br'\['),]
"""
def __init__(self, definitions):
self.definitions = definitions
parts = []
for name, part in definitions:
param = "(?P<%s>%s)" % (name.decode(), part.decode())
if six.PY3:
param = bytes(param, "utf-8")
parts.append(param)
self.regexpString = b"|".join(parts)
self.regexp = re.compile(self.regexpString, re.MULTILINE)
self.wsregexp = re.compile(br'\s+', re.M)
def curlineno(self):
"""Return the current line number"""
return self.text[:self.pos].count(b'\n') + 1
def scan(self, text):
"""Analyse some data
Analyse the passed content. Each time a token is recognized, a
2-uple containing its name and parsed value is raised (via
yield).
On error, a ParseError exception is raised.
:param text: a binary string containing the data to parse
"""
self.pos = 0
self.text = text
while self.pos < len(text):
m = self.wsregexp.match(text, self.pos)
if m is not None:
self.pos = m.end()
continue
m = self.regexp.match(text, self.pos)
if m is None:
raise ParseError("unknown token %s" % text[self.pos:])
self.pos = m.end()
yield (m.lastgroup, m.group(m.lastgroup))
class Parser(object):
"""The grammatical analysis part.
Here we define the SIEVE language tokens and grammar. This class
works with a Lexer object in order to check for grammar validity.
"""
lrules = [
(b"left_bracket", br'\['),
(b"right_bracket", br'\]'),
(b"left_parenthesis", br'\('),
(b"right_parenthesis", br'\)'),
(b"left_cbracket", br'{'),
(b"right_cbracket", br'}'),
(b"semicolon", br';'),
(b"comma", br','),
(b"hash_comment", br'#.*$'),
(b"bracket_comment", br'/\*[\s\S]*?\*/'),
(b"multiline", br'text:[^$]*?[\r\n]+\.$'),
(b"string", br'"([^"\\]|\\.)*"'),
(b"identifier", br'[a-zA-Z_][\w]*'),
(b"tag", br':[a-zA-Z_][\w]*'),
(b"number", br'[0-9]+[KMGkmg]?'),
]
def __init__(self, debug=False):
self.debug = debug
self.lexer = Lexer(Parser.lrules)
def __dprint(self, *msgs):
if not self.debug:
return
for m in msgs:
print(m)
def __reset_parser(self):
"""Reset parser's internal variables
Restore the parser to an initial state. Useful when creating a
new parser or reusing an existing one.
"""
self.result = []
self.hash_comments = []
self.__cstate = None
self.__curcommand = None
self.__curstringlist = None
self.__expected = None
self.__opened_blocks = 0
RequireCommand.loaded_extensions = []
def __set_expected(self, *args, **kwargs):
"""Set the next expected token.
One or more tokens can be provided. (they will represent the
valid possibilities for the next token).
"""
self.__expected = args
def __up(self, onlyrecord=False):
"""Return to the current command's parent
This method should be called each time a command is
complete. In case of a top level command (no parent), it is
recorded into a specific list for further usage.
:param onlyrecord: tell to only record the new command into its parent.
"""
if self.__curcommand.must_follow is not None:
if not self.__curcommand.parent:
prevcmd = self.result[-1] if len(self.result) else None
else:
prevcmd = self.__curcommand.parent.children[-2] \
if len(self.__curcommand.parent.children) >= 2 else None
if prevcmd is None or prevcmd.name not in self.__curcommand.must_follow:
raise ParseError("the %s command must follow an %s command" %
(self.__curcommand.name,
" or ".join(self.__curcommand.must_follow)))
if not self.__curcommand.parent:
# collect current amount of hash comments for later
# parsing into names and desciptions
self.__curcommand.hash_comments = self.hash_comments
self.hash_comments = []
self.result += [self.__curcommand]
if not onlyrecord:
self.__curcommand = self.__curcommand.parent
def __check_command_completion(self, testsemicolon=True):
"""Check for command(s) completion
This function should be called each time a new argument is
seen by the parser in order to check a command is complete. As
not only one command can be ended when receiving a new
argument (nested commands case), we apply the same work to
parent commands.
:param testsemicolon: if True, indicates that the next
expected token must be a semicolon (for commands that need one)
:return: True if command is
considered as complete, False otherwise.
"""
if not self.__curcommand.iscomplete():
return True
ctype = self.__curcommand.get_type()
if ctype == "action" or \
(ctype == "control" and
not self.__curcommand.accept_children):
if testsemicolon:
self.__set_expected("semicolon")
return True
while self.__curcommand.parent:
cmd = self.__curcommand
self.__curcommand = self.__curcommand.parent
if self.__curcommand.get_type() in ["control", "test"]:
if self.__curcommand.iscomplete():
if self.__curcommand.get_type() == "control":
break
continue
if not self.__curcommand.check_next_arg("test", cmd, add=False):
return False
if not self.__curcommand.iscomplete():
if self.__curcommand.variable_args_nb:
self.__set_expected("comma", "right_parenthesis")
break
return True
def __stringlist(self, ttype, tvalue):
"""Specific method to parse the 'string-list' type
Syntax:
string-list = "[" string *("," string) "]" / string
; if there is only a single string, the brackets
; are optional
"""
if ttype == "string":
self.__curstringlist += [tvalue.decode("utf-8")]
self.__set_expected("comma", "right_bracket")
return True
if ttype == "comma":
self.__set_expected("string")
return True
if ttype == "right_bracket":
self.__curcommand.check_next_arg("stringlist", self.__curstringlist)
self.__cstate = self.__arguments
return self.__check_command_completion()
return False
def __argument(self, ttype, tvalue):
"""Argument parsing method
This method acts as an entry point for 'argument' parsing.
Syntax:
string-list / number / tag
:param ttype: current token type
:param tvalue: current token value
:return: False if an error is encountered, True otherwise
"""
if ttype in ["multiline", "string"]:
return self.__curcommand.check_next_arg("string", tvalue.decode("utf-8"))
if ttype in ["number", "tag"]:
return self.__curcommand.check_next_arg(ttype, tvalue.decode("ascii"))
if ttype == "left_bracket":
self.__cstate = self.__stringlist
self.__curstringlist = []
self.__set_expected("string")
return True
return False
def __arguments(self, ttype, tvalue):
"""Arguments parsing method
Entry point for command arguments parsing. The parser must
call this method for each parsed command (either a control,
action or test).
Syntax:
*argument [ test / test-list ]
:param ttype: current token type
:param tvalue: current token value
:return: False if an error is encountered, True otherwise
"""
if ttype == "identifier":
test = get_command_instance(tvalue.decode("ascii"), self.__curcommand)
self.__curcommand.check_next_arg("test", test)
self.__expected = test.get_expected_first()
self.__curcommand = test
return self.__check_command_completion(testsemicolon=False)
if ttype == "left_parenthesis":
self.__set_expected("identifier")
return True
if ttype == "comma":
self.__set_expected("identifier")
return True
if ttype == "right_parenthesis":
self.__up()
return True
if self.__argument(ttype, tvalue):
return self.__check_command_completion(testsemicolon=False)
return False
def __command(self, ttype, tvalue):
"""Command parsing method
Entry point for command parsing. Here is expected behaviour:
* Handle command beginning if detected,
* Call the appropriate sub-method (specified by __cstate) to
handle the body,
* Handle command ending or block opening if detected.
Syntax:
identifier arguments (";" / block)
:param ttype: current token type
:param tvalue: current token value
:return: False if an error is encountered, True otherwise
"""
if self.__cstate is None:
if ttype == "right_cbracket":
self.__up()
self.__opened_blocks -= 1
self.__cstate = None
return True
if ttype != "identifier":
return False
command = get_command_instance(
tvalue.decode("ascii"), self.__curcommand)
if command.get_type() == "test":
raise ParseError(
"%s may not appear as a first command" % command.name)
if command.get_type() == "control" and command.accept_children \
and command.has_arguments():
self.__set_expected("identifier")
if self.__curcommand is not None:
if not self.__curcommand.addchild(command):
raise ParseError("%s unexpected after a %s" %
(tvalue, self.__curcommand.name))
self.__curcommand = command
self.__cstate = self.__arguments
return True
if self.__cstate(ttype, tvalue):
return True
if ttype == "left_cbracket":
self.__opened_blocks += 1
self.__cstate = None
return True
if ttype == "semicolon":
self.__cstate = None
if not self.__check_command_completion(testsemicolon=False):
return False
self.__curcommand.complete_cb()
self.__up()
return True
return False
def parse(self, text):
"""The parser entry point.
Parse the provided text to check for its validity.
On success, the parsing tree is available into the result
attribute. It is a list of sievecommands.Command objects (see
the module documentation for specific information).
On error, an string containing the explicit reason is
available into the error attribute.
:param text: a string containing the data to parse
:return: True on success (no error detected), False otherwise
"""
if isinstance(text, text_type):
text = text.encode("utf-8")
self.__reset_parser()
try:
for ttype, tvalue in self.lexer.scan(text):
if ttype == "hash_comment":
self.hash_comments += [tvalue.strip()]
continue
if ttype == "bracket_comment":
continue
if self.__expected is not None:
if ttype not in self.__expected:
if self.lexer.pos < len(text):
msg = "%s found while %s expected near '%s'" \
% (ttype, "|".join(self.__expected), text[self.lexer.pos])
else:
msg = "%s found while %s expected at end of file" \
% (ttype, "|".join(self.__expected))
raise ParseError(msg)
self.__expected = None
if not self.__command(ttype, tvalue):
msg = "unexpected token '%s' found near '%s'" \
% (tvalue, text[self.lexer.pos])
raise ParseError(msg)
if self.__opened_blocks:
self.__set_expected("right_cbracket")
if self.__expected is not None:
raise ParseError("end of script reached while %s expected" %
"|".join(self.__expected))
except (ParseError, CommandError) as e:
self.error = "line %d: %s" % (self.lexer.curlineno(), str(e))
return False
return True
def parse_file(self, name):
"""Parse the content of a file.
See 'parse' method for information.
:param name: the pathname of the file to parse
:return: True on success (no error detected), False otherwise
"""
with open(name, "rb") as fp:
return self.parse(fp.read())
def dump(self, target=sys.stdout):
"""Dump the parsing tree.
This method displays the parsing tree on the standard output.
"""
for r in self.result:
r.dump(target=target)
if __name__ == "__main__":
from optparse import OptionParser
op = OptionParser()
op.usage = "%prog: [options] files"
op.add_option("-v", "--verbose", action="store_true", default=False,
help="Activate verbose mode")
op.add_option("-d", "--debug", action="store_true", default=False,
help="Activate debug traces")
op.add_option("--tosieve", action="store_true",
help="Print parser results using sieve")
options, args = op.parse_args()
if not len(args):
print("Nothing to parse, exiting.")
sys.exit(0)
for a in args:
p = Parser(debug=options.debug)
print("Parsing file %s... " % a, end=' ')
if p.parse_file(a):
print("OK")
if options.verbose:
p.dump()
if options.tosieve:
for r in p.result:
r.tosieve()
continue
print("ERROR")
print(p.error)
sievelib-1.1.0/sievelib/digest_md5.py 0000644 0001750 0001750 00000004722 12542516124 020024 0 ustar tonio tonio 0000000 0000000 # coding: utf-8
"""
Simple Digest-MD5 implementation (client side)
Implementation based on RFC 2831 (http://www.ietf.org/rfc/rfc2831.txt)
"""
import base64
import hashlib
import binascii
import re
import random
class DigestMD5(object):
def __init__(self, challenge, digesturi):
self.__digesturi = digesturi
self.__challenge = challenge
self.__params = {}
pexpr = re.compile(r'(\w+)="(.+)"')
for elt in base64.b64decode(challenge).split(","):
m = pexpr.match(elt)
if m is None:
continue
self.__params[m.group(1)] = m.group(2)
def __make_cnonce(self):
ret = ""
for i in xrange(12):
ret += chr(random.randint(0, 0xff))
return base64.b64encode(ret)
def __digest(self, value):
return hashlib.md5(value).digest()
def __hexdigest(self, value):
return binascii.hexlify(hashlib.md5(value).digest())
def __make_response(self, username, password, check=False):
a1 = "%s:%s:%s" % (
self.__digest("%s:%s:%s" % (username, self.realm, password)),
self.__params["nonce"], self.cnonce
)
if check:
a2 = ":%s" % self.__digesturi
else:
a2 = "AUTHENTICATE:%s" % self.__digesturi
resp = "%s:%s:00000001:%s:auth:%s" \
% (self.__hexdigest(a1), self.__params["nonce"],
self.cnonce, self.__hexdigest(a2))
return self.__hexdigest(resp)
def response(self, username, password, authz_id=''):
self.realm = self.__params["realm"] \
if self.__params.has_key("realm") else ""
self.cnonce = self.__make_cnonce()
respvalue = self.__make_response(username, password)
dgres = 'username="%s",%snonce="%s",cnonce="%s",nc=00000001,qop=auth,' \
'digest-uri="%s",response=%s' \
% (username,
('realm="%s",' % self.realm) if len(self.realm) else "",
self.__params["nonce"], self.cnonce, self.__digesturi, respvalue)
if authz_id:
if type(authz_id) is unicode:
authz_id = authz_id.encode("utf-8")
dgres += ',authzid="%s"' % authz_id
return base64.b64encode(dgres)
def check_last_challenge(self, username, password, value):
challenge = base64.b64decode(value.strip('"'))
return challenge == \
("rspauth=%s" % self.__make_response(username, password, True))
sievelib-1.1.0/setup.cfg 0000644 0001750 0001750 00000000046 13210277642 015442 0 ustar tonio tonio 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
sievelib-1.1.0/requirements.txt 0000644 0001750 0001750 00000000013 12611674351 017101 0 ustar tonio tonio 0000000 0000000 future
six
sievelib-1.1.0/sievelib.egg-info/ 0000755 0001750 0001750 00000000000 13210277642 017115 5 ustar tonio tonio 0000000 0000000 sievelib-1.1.0/sievelib.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13210277642 023163 0 ustar tonio tonio 0000000 0000000
sievelib-1.1.0/sievelib.egg-info/top_level.txt 0000644 0001750 0001750 00000000011 13210277642 021637 0 ustar tonio tonio 0000000 0000000 sievelib
sievelib-1.1.0/sievelib.egg-info/PKG-INFO 0000644 0001750 0001750 00000013413 13210277642 020214 0 ustar tonio tonio 0000000 0000000 Metadata-Version: 1.1
Name: sievelib
Version: 1.1.0
Summary: Client-side SIEVE library
Home-page: https://github.com/tonioo/sievelib
Author: Antoine Nguyen
Author-email: tonio@ngyn.org
License: MIT
Description: sievelib
========
|travis| |codecov| |latest-version|
Client-side Sieve and Managesieve library written in Python.
* Sieve : An Email Filtering Language
(`RFC 5228