summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com>2012-11-18 17:28:43 (GMT)
committerJeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com>2012-11-18 17:28:43 (GMT)
commit092eed219ad6863e6ce911265223ce7c1b50c01d (patch)
tree5a4ae9a1e1e29069db9e335dae5d5a01b1ef136c
downloadpython-sievelib-upstream-0.5.tar.gz
Add original sievelib-0.5 releaseupstream-0.5
-rw-r--r--COPYING13
-rw-r--r--MANIFEST.in2
-rw-r--r--PKG-INFO122
-rw-r--r--README.rst104
-rw-r--r--setup.cfg5
-rw-r--r--setup.py24
-rw-r--r--sievelib.egg-info/PKG-INFO122
-rw-r--r--sievelib.egg-info/SOURCES.txt14
-rw-r--r--sievelib.egg-info/dependency_links.txt1
-rw-r--r--sievelib.egg-info/top_level.txt1
-rw-r--r--sievelib/__init__.py0
-rw-r--r--sievelib/commands.py619
-rw-r--r--sievelib/digest_md5.py69
-rw-r--r--sievelib/factory.py291
-rw-r--r--sievelib/managesieve.py639
-rwxr-xr-xsievelib/parser.py438
16 files changed, 2464 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..97bb275
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,13 @@
+Copyright (c) 2011-2012, Antoine Nguyen <tonio@ngyn.org>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..9491f80
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst
+include COPYING
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..56ca998
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,122 @@
+Metadata-Version: 1.1
+Name: sievelib
+Version: 0.5
+Summary: Client-side SIEVE library
+Home-page: http://bitbucket.org/tonioo/sievelib
+Author: Antoine Nguyen
+Author-email: tonio@ngyn.org
+License: MIT
+Description: sievelib
+ ========
+
+ Client-side Sieve and Managesieve library written in Python.
+
+ * Sieve : An Email Filtering Language
+ (`RFC 5228 <http://tools.ietf.org/html/rfc5228>`_)
+ * ManageSieve : A Protocol for Remotely Managing Sieve Scripts
+ (`Draft <http://tools.ietf.org/html/draft-martin-managesieve-12>`_)
+
+ Sieve tools
+ -----------
+
+ What is supported
+ ^^^^^^^^^^^^^^^^^
+
+ Currently, the provided parser supports most of the functionalities
+ described in the RFC. The only exception concerns section
+ *2.4.2.4. Encoding Characters Using "encoded-character"* which is not
+ supported.
+
+ The following extensions are also supported:
+
+ * Vacation (`RFC 5230 <http://tools.ietf.org/html/rfc5230>`_)
+
+ Basic usage
+ ^^^^^^^^^^^
+
+ The parser can either be used from the command-line::
+
+ $ cd sievelib
+ $ python parser.py test.sieve
+ Syntax OK
+ $
+
+ Or can be used from a python environment (or script/module)::
+
+ >>> from sievelib.parser import Parser
+ >>> p = Parser()
+ >>> p.parse('require ["fileinto"];')
+ True
+ >>> p.dump()
+ require (type: control)
+ ["fileinto"]
+ >>>
+ >>> p.parse('require ["fileinto"]')
+ False
+ >>> p.error
+ 'line 1: parsing error: end of script reached while semicolon expected'
+ >>>
+
+ Simple filters creation
+ ^^^^^^^^^^^^^^^^^^^^^^^
+
+ Some high-level classes are provided with the ``factory`` module, they
+ make the generation of Sieve rules easier::
+
+ >>> from sievelib.factory import FilterSet
+ >>> fs.addfilter("rule1",
+ ... [("Sender", ":is", "toto@toto.com"),],
+ ... [("fileinto", "Toto"),])
+ >>> fs.tosieve()
+ require ["fileinto"];
+
+ # Filter: rule1
+ if anyof (header :is "Sender" "toto@toto.com") {
+ fileinto "Toto";
+ }
+ >>>
+
+ Additionnal documentation is available with source code.
+
+ ManageSieve tools
+ -----------------
+
+ What is supported
+ ^^^^^^^^^^^^^^^^^
+
+ All mandatory commands are supported. The ``RENAME`` extension is
+ supported, with a simulated behaviour for server that do not support
+ it.
+
+ For the ``AUTHENTICATE`` command, supported mechanisms are ``DIGEST-MD5``,
+ ``PLAIN`` and ``LOGIN``.
+
+ Basic usage
+ ^^^^^^^^^^^
+
+ The ManageSieve client is intended to be used from another python
+ application (there isn't any shell provided)::
+
+ >>> from sievelib.managesieve import Client
+ >>> c = Client("server.example.com")
+ >>> c.connect("user", "password", starttls=False, authmech="DIGEST-MD5")
+ True
+ >>> c.listscripts()
+ ("active_script", ["script1", "script2"])
+ >>> c.setactive("script1")
+ True
+ >>> c.havespace("script3", 45)
+ True
+ >>>
+
+ Additionnal documentation is available with source code.
+
+Keywords: sieve,managesieve,parser,client
+Platform: UNKNOWN
+Classifier: Programming Language :: Python
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Communications :: Email :: Filters
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f65bcc4
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,104 @@
+sievelib
+========
+
+Client-side Sieve and Managesieve library written in Python.
+
+* Sieve : An Email Filtering Language
+ (`RFC 5228 <http://tools.ietf.org/html/rfc5228>`_)
+* ManageSieve : A Protocol for Remotely Managing Sieve Scripts
+ (`Draft <http://tools.ietf.org/html/draft-martin-managesieve-12>`_)
+
+Sieve tools
+-----------
+
+What is supported
+^^^^^^^^^^^^^^^^^
+
+Currently, the provided parser supports most of the functionalities
+described in the RFC. The only exception concerns section
+*2.4.2.4. Encoding Characters Using "encoded-character"* which is not
+supported.
+
+The following extensions are also supported:
+
+* Vacation (`RFC 5230 <http://tools.ietf.org/html/rfc5230>`_)
+
+Basic usage
+^^^^^^^^^^^
+
+The parser can either be used from the command-line::
+
+ $ cd sievelib
+ $ python parser.py test.sieve
+ Syntax OK
+ $
+
+Or can be used from a python environment (or script/module)::
+
+ >>> from sievelib.parser import Parser
+ >>> p = Parser()
+ >>> p.parse('require ["fileinto"];')
+ True
+ >>> p.dump()
+ require (type: control)
+ ["fileinto"]
+ >>>
+ >>> p.parse('require ["fileinto"]')
+ False
+ >>> p.error
+ 'line 1: parsing error: end of script reached while semicolon expected'
+ >>>
+
+Simple filters creation
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Some high-level classes are provided with the ``factory`` module, they
+make the generation of Sieve rules easier::
+
+ >>> from sievelib.factory import FilterSet
+ >>> fs.addfilter("rule1",
+ ... [("Sender", ":is", "toto@toto.com"),],
+ ... [("fileinto", "Toto"),])
+ >>> fs.tosieve()
+ require ["fileinto"];
+
+ # Filter: rule1
+ if anyof (header :is "Sender" "toto@toto.com") {
+ fileinto "Toto";
+ }
+ >>>
+
+Additionnal documentation is available with source code.
+
+ManageSieve tools
+-----------------
+
+What is supported
+^^^^^^^^^^^^^^^^^
+
+All mandatory commands are supported. The ``RENAME`` extension is
+supported, with a simulated behaviour for server that do not support
+it.
+
+For the ``AUTHENTICATE`` command, supported mechanisms are ``DIGEST-MD5``,
+``PLAIN`` and ``LOGIN``.
+
+Basic usage
+^^^^^^^^^^^
+
+The ManageSieve client is intended to be used from another python
+application (there isn't any shell provided)::
+
+ >>> from sievelib.managesieve import Client
+ >>> c = Client("server.example.com")
+ >>> c.connect("user", "password", starttls=False, authmech="DIGEST-MD5")
+ True
+ >>> c.listscripts()
+ ("active_script", ["script1", "script2"])
+ >>> c.setactive("script1")
+ True
+ >>> c.havespace("script3", 45)
+ True
+ >>>
+
+Additionnal documentation is available with source code.
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..861a9f5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..93159c7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,24 @@
+from setuptools import setup
+
+setup(
+ name = "sievelib",
+ packages = ["sievelib"],
+ version = "0.5",
+ description = "Client-side SIEVE library",
+ author = "Antoine Nguyen",
+ author_email = "tonio@ngyn.org",
+ url = "http://bitbucket.org/tonioo/sievelib",
+ license = "MIT",
+ keywords = ["sieve", "managesieve", "parser", "client"],
+ classifiers = [
+ "Programming Language :: Python",
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Communications :: Email :: Filters"
+ ],
+
+ long_description = open("README.rst").read()
+)
diff --git a/sievelib.egg-info/PKG-INFO b/sievelib.egg-info/PKG-INFO
new file mode 100644
index 0000000..56ca998
--- /dev/null
+++ b/sievelib.egg-info/PKG-INFO
@@ -0,0 +1,122 @@
+Metadata-Version: 1.1
+Name: sievelib
+Version: 0.5
+Summary: Client-side SIEVE library
+Home-page: http://bitbucket.org/tonioo/sievelib
+Author: Antoine Nguyen
+Author-email: tonio@ngyn.org
+License: MIT
+Description: sievelib
+ ========
+
+ Client-side Sieve and Managesieve library written in Python.
+
+ * Sieve : An Email Filtering Language
+ (`RFC 5228 <http://tools.ietf.org/html/rfc5228>`_)
+ * ManageSieve : A Protocol for Remotely Managing Sieve Scripts
+ (`Draft <http://tools.ietf.org/html/draft-martin-managesieve-12>`_)
+
+ Sieve tools
+ -----------
+
+ What is supported
+ ^^^^^^^^^^^^^^^^^
+
+ Currently, the provided parser supports most of the functionalities
+ described in the RFC. The only exception concerns section
+ *2.4.2.4. Encoding Characters Using "encoded-character"* which is not
+ supported.
+
+ The following extensions are also supported:
+
+ * Vacation (`RFC 5230 <http://tools.ietf.org/html/rfc5230>`_)
+
+ Basic usage
+ ^^^^^^^^^^^
+
+ The parser can either be used from the command-line::
+
+ $ cd sievelib
+ $ python parser.py test.sieve
+ Syntax OK
+ $
+
+ Or can be used from a python environment (or script/module)::
+
+ >>> from sievelib.parser import Parser
+ >>> p = Parser()
+ >>> p.parse('require ["fileinto"];')
+ True
+ >>> p.dump()
+ require (type: control)
+ ["fileinto"]
+ >>>
+ >>> p.parse('require ["fileinto"]')
+ False
+ >>> p.error
+ 'line 1: parsing error: end of script reached while semicolon expected'
+ >>>
+
+ Simple filters creation
+ ^^^^^^^^^^^^^^^^^^^^^^^
+
+ Some high-level classes are provided with the ``factory`` module, they
+ make the generation of Sieve rules easier::
+
+ >>> from sievelib.factory import FilterSet
+ >>> fs.addfilter("rule1",
+ ... [("Sender", ":is", "toto@toto.com"),],
+ ... [("fileinto", "Toto"),])
+ >>> fs.tosieve()
+ require ["fileinto"];
+
+ # Filter: rule1
+ if anyof (header :is "Sender" "toto@toto.com") {
+ fileinto "Toto";
+ }
+ >>>
+
+ Additionnal documentation is available with source code.
+
+ ManageSieve tools
+ -----------------
+
+ What is supported
+ ^^^^^^^^^^^^^^^^^
+
+ All mandatory commands are supported. The ``RENAME`` extension is
+ supported, with a simulated behaviour for server that do not support
+ it.
+
+ For the ``AUTHENTICATE`` command, supported mechanisms are ``DIGEST-MD5``,
+ ``PLAIN`` and ``LOGIN``.
+
+ Basic usage
+ ^^^^^^^^^^^
+
+ The ManageSieve client is intended to be used from another python
+ application (there isn't any shell provided)::
+
+ >>> from sievelib.managesieve import Client
+ >>> c = Client("server.example.com")
+ >>> c.connect("user", "password", starttls=False, authmech="DIGEST-MD5")
+ True
+ >>> c.listscripts()
+ ("active_script", ["script1", "script2"])
+ >>> c.setactive("script1")
+ True
+ >>> c.havespace("script3", 45)
+ True
+ >>>
+
+ Additionnal documentation is available with source code.
+
+Keywords: sieve,managesieve,parser,client
+Platform: UNKNOWN
+Classifier: Programming Language :: Python
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Communications :: Email :: Filters
diff --git a/sievelib.egg-info/SOURCES.txt b/sievelib.egg-info/SOURCES.txt
new file mode 100644
index 0000000..ed1b128
--- /dev/null
+++ b/sievelib.egg-info/SOURCES.txt
@@ -0,0 +1,14 @@
+COPYING
+MANIFEST.in
+README.rst
+setup.py
+sievelib/__init__.py
+sievelib/commands.py
+sievelib/digest_md5.py
+sievelib/factory.py
+sievelib/managesieve.py
+sievelib/parser.py
+sievelib.egg-info/PKG-INFO
+sievelib.egg-info/SOURCES.txt
+sievelib.egg-info/dependency_links.txt
+sievelib.egg-info/top_level.txt \ No newline at end of file
diff --git a/sievelib.egg-info/dependency_links.txt b/sievelib.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/sievelib.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/sievelib.egg-info/top_level.txt b/sievelib.egg-info/top_level.txt
new file mode 100644
index 0000000..f789786
--- /dev/null
+++ b/sievelib.egg-info/top_level.txt
@@ -0,0 +1 @@
+sievelib
diff --git a/sievelib/__init__.py b/sievelib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sievelib/__init__.py
diff --git a/sievelib/commands.py b/sievelib/commands.py
new file mode 100644
index 0000000..6a4b0de
--- /dev/null
+++ b/sievelib/commands.py
@@ -0,0 +1,619 @@
+# 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.
+
+"""
+import sys
+
+class UnknownCommand(Exception):
+ """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
+
+class BadArgument(Exception):
+ """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)
+
+class BadValue(Exception):
+ """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)
+
+
+# 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 unkwown 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()
+
+ 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 self.arguments.has_key(arg["name"]):
+ continue
+ target.write(" ")
+ value = self.arguments[arg["name"]]
+ 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("[" + (", ".join(map(lambda v: '"%s"' % v.strip('"'), value))) + "]")
+ continue
+ if isinstance(value, Command):
+ value.tosieve(indentlevel, target=target)
+ continue
+
+ if type(value) is unicode:
+ value = value.encode("utf-8")
+
+ if "tag" in arg["type"] and arg.get("write_tag", False):
+ target.write("%s " % arg["values"][0])
+
+ if "string" in arg["type"]:
+ target.write('"%s"' % value.strip('"'))
+ else:
+ target.write(value)
+
+ if not self.accept_children:
+ if self.get_type() != "test":
+ print >>target, ";"
+ return
+ if self.get_type() != "control":
+ return
+ print >>target, " {"
+ 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:
+ print >>target, text
+
+ 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 self.arguments.has_key(arg["name"]):
+ 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 self.rargs_cnt == self.required_args
+
+ def get_type(self):
+ """Return the command's type"""
+ if self._type is None:
+ raise NotImplemented
+ 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 not arg.has_key("values"):
+ return True
+ return value.lower() in arg["values"]
+
+ def check_next_arg(self, atype, avalue, add=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 the required ones.
+
+ A special handling is also 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
+ :return: True on success, False otherwise
+ """
+ if not self.has_arguments():
+ return False
+ if self.iscomplete():
+ return False
+
+ if self.curarg is not None:
+ if atype == self.curarg["extra_arg"]["type"]:
+ if not self.curarg["extra_arg"].has_key("values") 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 self.arguments.has_key(curarg["name"]):
+ 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.rargs_cnt += 1
+ self.nextargpos = pos + 1
+ if add:
+ self.arguments[curarg["name"]] = avalue
+ break
+
+ if atype in curarg["type"]:
+ if self.__is_valid_value_for_arg(curarg, avalue):
+ if curarg.has_key("extra_arg"):
+ 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 __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 not self.arguments.has_key(name):
+ 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"]) == str:
+ exts = [self.arguments["capabilities"]]
+ else:
+ exts = self.arguments["capabilities"]
+ for ext in exts:
+ ext = ext.strip('"')
+ if not ext 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" : "mailbox",
+ "type" : ["string"],
+ "required" : True}
+ ]
+
+class RedirectCommand(ActionCommand):
+ args_definition = [
+ {"name" : "address",
+ "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" : "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},
+ ]
+
+
+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
+ """
+ cname = "%sCommand" % name.lower().capitalize()
+ if not globals().has_key(cname) or \
+ (checkexists and globals()[cname].is_extension and \
+ not name in RequireCommand.loaded_extensions):
+ raise UnknownCommand(name)
+ return globals()[cname](parent)
diff --git a/sievelib/digest_md5.py b/sievelib/digest_md5.py
new file mode 100644
index 0000000..9011177
--- /dev/null
+++ b/sievelib/digest_md5.py
@@ -0,0 +1,69 @@
+# 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('(\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):
+ 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)
+
+ 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))
+
diff --git a/sievelib/factory.py b/sievelib/factory.py
new file mode 100644
index 0000000..4231844
--- /dev/null
+++ b/sievelib/factory.py
@@ -0,0 +1,291 @@
+# 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.
+"""
+import sys
+import cStringIO
+from commands import *
+
+class FiltersSet(object):
+ def __init__(self, name):
+ self.name = name
+ self.requires = []
+ self.filters = []
+
+ def __str__(self):
+ target = cStringIO.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 name: 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:
+ map(self.require, f.arguments["capabilities"])
+ else:
+ self.require(f.arguments["capabilities"])
+ continue
+ if cpt - 1 >= len(parser.hash_comments):
+ name = "Unnamed rule %d" % cpt
+ else:
+ name = parser.hash_comments[cpt - 1].replace("# Filter: ", "")
+ self.filters += [{"name" : name,
+ "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 not name in self.requires:
+ self.requires += [name]
+
+ def __gen_require_command(self):
+ """Internal method to create a RequireCommand based on requirements
+
+ Called just before this object is going to be dumped.
+ """
+ reqcmd = get_command_instance("require")
+ reqcmd.check_next_arg("stringlist", self.requires)
+ return reqcmd
+
+ 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] in ("true", "false"):
+ cmd = get_command_instance(c[0], ifcontrol)
+ elif c[0] == "size":
+ cmd = get_command_instance("size", ifcontrol)
+ cmd.check_next_arg("tag", c[1])
+ cmd.check_next_arg("number", c[2])
+ else:
+ cmd = get_command_instance("header", ifcontrol)
+ cmd.check_next_arg("tag", c[1])
+ cmd.check_next_arg("string", c[0])
+ cmd.check_next_arg("string", c[2])
+ mtypeobj.check_next_arg("test", cmd)
+ ifcontrol.check_next_arg("test", mtypeobj)
+
+ for actdef in actions:
+ self.require(actdef[0])
+ action = get_command_instance(actdef[0], ifcontrol, False)
+ for arg in actdef[1:]:
+ action.check_next_arg("string", arg)
+ ifcontrol.addchild(action)
+ return ifcontrol
+
+ 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" : 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 ordre between filters.
+
+ :param name: the filter's name
+ :param conditions: the list of conditions
+ :param actions: the list of actions
+ :param matchtype: "anyof" or "allof"
+ """
+ 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 getfilter(self, name):
+ """Search for a specific filter
+
+ :param name: the filter's name
+ :return: the Command object if found, None otherwise
+ """
+ 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
+ """
+ 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
+ """
+ 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
+ """
+ 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
+ """
+ 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"
+ """
+ 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
+
+ print "Dumping requirements"
+ self.__gen_require_command().dump()
+ print
+
+ for f in self.filters:
+ print "Filter %s" % f["name"]
+ 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
+ """
+ self.__gen_require_command().tosieve(target=target)
+ target.write("\n")
+ for f in self.filters:
+ print >>target, "# Filter: %s" % f["name"]
+ f["content"].tosieve(target=target)
+
+
+if __name__ == "__main__":
+ fs = FiltersSet("test")
+
+ fs.addfilter("rule1",
+ [("Sender", ":is", "toto@toto.com"),],
+ [("fileinto", "Toto"),])
+ fs.tosieve()
+
diff --git a/sievelib/managesieve.py b/sievelib/managesieve.py
new file mode 100644
index 0000000..8095abc
--- /dev/null
+++ b/sievelib/managesieve.py
@@ -0,0 +1,639 @@
+#!/usr/bin/env python
+# 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 <draft-martin-managesieve-12>.
+"""
+
+import sys, os
+import socket
+import re
+import base64
+import ssl
+
+from digest_md5 import DigestMD5
+
+CRLF = '\r\n'
+
+KNOWN_CAPABILITIES = ["IMPLEMENTATION", "SASL", "SIEVE",
+ "STARTTLS", "NOTIFY", "LANGUAGE",
+ "RENAME"]
+
+SUPPORTED_AUTH_MECHS = ["DIGEST-MD5", "PLAIN", "LOGIN"]
+
+class Error(Exception):
+ pass
+
+class Response(Exception):
+ def __init__(self, code, data):
+ self.code = code
+ self.data = data
+
+ def __str__(self):
+ return "%s %s" % (self.code, self.data)
+
+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=2000, debug=False):
+ self.srvaddr = srvaddr
+ self.srvport = srvport
+ self.__debug = debug
+ self.sock = None
+ self.__read_buffer = ""
+ self.authenticated = False
+ self.errcode = None
+
+ self.__capabilities = {}
+ self.__respcode_expr = re.compile("(OK|NO|BYE)\s*(.+)?")
+ self.__error_expr = re.compile('(\(\w+\))?\s*(".+")')
+ self.__size_expr = re.compile("\{(\d+)\+?\}")
+ self.__active_expr = re.compile("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 = ""
+ 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), e:
+ raise Error("Failed to read %d bytes from the server" % size)
+ 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 = ""
+ while True:
+ try:
+ pos = self.__read_buffer.index(CRLF)
+ ret = self.__read_buffer[0:pos]
+ self.__read_buffer = self.__read_buffer[pos+len(CRLF):]
+ break
+ except ValueError:
+ pass
+ try:
+ nval = self.sock.recv(self.read_size)
+ 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) == "BYE":
+ raise Error("Connection closed by server")
+ if m.group(1) == "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 = ("", None, None)
+ cpt = 0
+ while True:
+ try:
+ line = self.__read_line()
+ except Response, inst:
+ code = inst.code
+ data = inst.data
+ break
+ except Literal, inst:
+ resp += self.__read_block(inst.value)
+ 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 type(a) in [str, unicode] and self.__size_expr.match(a) is None:
+ ret += ['"%s"' % a.encode('utf-8')]
+ continue
+ ret += ["%s" % str(a)]
+ return ret
+
+ def __send_command(self, name, args=[], withcontent=False, extralines=[], 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
+ if len(args):
+ tosend += " " + " ".join(self.__prepare_args(args))
+ self.__dprint("Command: %s" % tosend)
+ self.sock.send("%s%s" % (tosend, CRLF))
+ for l in extralines:
+ self.sock.send("%s%s" % (l, CRLF))
+ code, data, content = self.__read_response(nblines)
+
+ 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()[0:-1]:
+ parts = l.split(None, 1)
+ cname = parts[0].strip('"')
+ if not cname in KNOWN_CAPABILITIES:
+ continue
+ self.__capabilities[cname] = \
+ parts[1].strip('"') 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 = ""
+ 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("()")
+ else:
+ self.errcode = ""
+ self.errmsg = m.group(2).strip('"')
+
+ def _plain_authentication(self, login, password):
+ """SASL PLAIN authentication
+
+ :param login: username
+ :param password: clear password
+ :return: True on success, False otherwise.
+ """
+ params = base64.b64encode('\0' + '\0'.join([login, password]))
+ code, data = self.__send_command("AUTHENTICATE", ["PLAIN", params])
+ if code == "OK":
+ return True
+ return False
+
+ def _login_authentication(self, login, password):
+ """SASL LOGIN authentication
+
+ :param login: username
+ :param password: clear password
+ :return: True on success, False otherwise.
+ """
+ extralines = ['"%s"' % base64.b64encode(login),
+ '"%s"' % base64.b64encode(password)]
+ code, data = self.__send_command("AUTHENTICATE", ["LOGIN"],
+ extralines=extralines)
+ if code == "OK":
+ return True
+ return False
+
+ def _digest_md5_authentication(self, login, password):
+ """SASL DIGEST-MD5 authentication
+
+ :param login: username
+ :param password: clear password
+ :return: True on success, False otherwise.
+ """
+ code, data, challenge = \
+ self.__send_command("AUTHENTICATE", ["DIGEST-MD5"],
+ withcontent=True, nblines=1)
+ dmd5 = DigestMD5(challenge, "sieve/%s" % self.srvaddr)
+
+ code, data, challenge = \
+ self.__send_command('"%s"' % dmd5.response(login, password),
+ withcontent=True, nblines=1)
+ if not len(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, 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 authmech: prefered authentication mechanism
+ :return: True on success, False otherwise
+ """
+ if not self.__capabilities.has_key("SASL"):
+ 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 not mech in srv_mechanisms:
+ continue
+ mech = mech.lower()
+ mech = re.sub("-", "_", mech)
+ if getattr(self, "_%s_authentication" % mech.lower())(login, password):
+ self.authenticated = True
+ return True
+ return False
+
+ self.errmsg = "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, 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 self.__capabilities.has_key("STARTTLS")
+
+ 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 type(self.__capabilities["SIEVE"]) == str:
+ self.__capabilities["SIEVE"] = self.__capabilities["SIEVE"].split()
+ return self.__capabilities["SIEVE"]
+
+ def connect(self, login, password, 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, 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, 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, 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('"([^"]+)"\s*(.+)', l)
+ if m is None:
+ ret += [l.strip('"')]
+ else:
+ if self.__active_expr.match(m.group(2)):
+ active_script = m.group(1)
+ else:
+ ret += [m.group(1)]
+ 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], withcontent=True)
+ if code == "OK":
+ lines = content.splitlines()
+ if self.__size_expr.match(lines[0]) is not None:
+ lines = lines[1:]
+ return "\n".join(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
+ """
+ if type(content) is unicode:
+ content = content.encode("utf-8")
+
+ content = "{%d+}%s%s" % (len(content), CRLF, content)
+ code, data = \
+ self.__send_command("PUTSCRIPT", [name, 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])
+ 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 self.__capabilities.has_key("RENAMESCRIPT"):
+ code, data = self.__send_command("RENAMESCRIPT", [oldname, newname])
+ if code == "OK":
+ return True
+ return False
+
+ (active_script, scripts) = self.listscripts()
+ if scripts == None or not oldname in scripts:
+ self.errmsg = "Old script does not exist"
+ return False
+ if newname in scripts:
+ self.errmsg = "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])
+ if code == "OK":
+ return True
+ return False
+
diff --git a/sievelib/parser.py b/sievelib/parser.py
new file mode 100755
index 0000000..a3670d9
--- /dev/null
+++ b/sievelib/parser.py
@@ -0,0 +1,438 @@
+#!/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)
+
+"""
+import re
+import sys
+
+from commands import get_command_instance, UnknownCommand, BadArgument, BadValue
+
+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:
+
+ [("left_bracket", r'\['),]
+ """
+ def __init__(self, definitions):
+ self.definitions = definitions
+ parts = []
+ for name, part in definitions:
+ parts.append("(?P<%s>%s)" % (name, part))
+ self.regexpString = "|".join(parts)
+ self.regexp = re.compile(self.regexpString, re.MULTILINE)
+ self.wsregexp = re.compile(r'\s+', re.M)
+
+ def curlineno(self):
+ """Return the current line number"""
+ return self.text[:self.pos].count('\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 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:
+ lineno = self.curlineno()
+ 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 = [
+ ("left_bracket", r'\['),
+ ("right_bracket", r'\]'),
+ ("left_parenthesis", r'\('),
+ ("right_parenthesis", r'\)'),
+ ("left_cbracket", r'{'),
+ ("right_cbracket", r'}'),
+ ("semicolon", r';'),
+ ("comma", r','),
+ ("hash_comment", r'#.*$'),
+ ("bracket_comment", r'/\*[\s\S]*?\*/'),
+ ("multiline", r'text:[^$]*[\r\n]+\.$'),
+ ("string", r'"([^"\\]|\\.)*"'),
+ ("identifier", r'[a-zA-Z_][\w]*'),
+ ("tag", r':[a-zA-Z_][\w]*'),
+ ("number", r'[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
+
+ 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:
+ 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]
+ 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 == "multiline":
+ return self.__curcommand.check_next_arg("string", tvalue)
+
+ if ttype in ["number", "tag", "string"]:
+ return self.__curcommand.check_next_arg(ttype, tvalue)
+
+ 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, 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()
+
+ 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, 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
+ """
+ self.__reset_parser()
+ try:
+ for ttype, tvalue in self.lexer.scan(text):
+ if ttype == "hash_comment":
+ self.hash_comments += [tvalue]
+ continue
+ if ttype == "bracket_comment":
+ continue
+ if self.__expected is not None:
+ if not ttype 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, UnknownCommand, BadArgument, BadValue), 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
+ """
+ fp = open(name)
+ content = fp.read()
+ fp.close()
+ return self.parse(content)
+
+ 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")
+ 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,
+ if p.parse_file(a):
+ print "OK"
+ if options.verbose:
+ p.dump()
+ continue
+ print "ERROR"
+ print p.error
+
+