Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-sievelib for openSUSE:Factory
checked in at 2026-03-24 18:50:12
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-sievelib (Old)
and /work/SRC/openSUSE:Factory/.python-sievelib.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-sievelib"
Tue Mar 24 18:50:12 2026 rev:10 rq:1342218 version:1.5.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-sievelib/python-sievelib.changes
2025-01-28 15:00:51.894240833 +0100
+++
/work/SRC/openSUSE:Factory/.python-sievelib.new.8177/python-sievelib.changes
2026-03-24 18:51:06.724009484 +0100
@@ -1,0 +2,20 @@
+Tue Mar 24 11:59:32 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.5.0:
+ * Add optional srvhostname argument for managesieve client
+ * Added support for environment extension.
+ * Improved get_filter_actions() method to support extra args.
+ * Python 3.14 support
+ * Python 3.9 support removed
+- update to 1.4.3:
+ * Added support for address test in factory.
+ * Dropped python 3.8 support and added python 3.12+
+ * Added missing require statement when using create arg with
+ fileinto
+ * Add Notify Extension
+ * Improve code discoverability and fix warnings
+ * Add xoauth2 auth mechanism
+ * Support implicit TLS connections
+ * Refactored SSL connection related code
+
+-------------------------------------------------------------------
Old:
----
sievelib-1.4.2.tar.gz
New:
----
sievelib-1.5.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-sievelib.spec ++++++
--- /var/tmp/diff_new_pack.0UYUEQ/_old 2026-03-24 18:51:07.356035626 +0100
+++ /var/tmp/diff_new_pack.0UYUEQ/_new 2026-03-24 18:51:07.356035626 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-sievelib
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
# Copyright (c) 2016 Aeneas Jaissle <[email protected]>
#
# All modifications and additions to the file contributed by third parties
@@ -19,7 +19,7 @@
%define modname sievelib
Name: python-%{modname}
-Version: 1.4.2
+Version: 1.5.0
Release: 0
Summary: Client-side Sieve and Managesieve library written in Python
License: MIT
@@ -27,7 +27,8 @@
Source:
https://files.pythonhosted.org/packages/source/s/sievelib/%{modname}-%{version}.tar.gz
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest}
-BuildRequires: %{python_module setuptools_scm}
+BuildRequires: %{python_module setuptools >= 61.0}
+BuildRequires: %{python_module setuptools_scm >= 6.4}
BuildRequires: %{python_module typing-extensions}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
++++++ sievelib-1.4.2.tar.gz -> sievelib-1.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/.github/workflows/sievelib.yml
new/sievelib-1.5.0/.github/workflows/sievelib.yml
--- old/sievelib-1.4.2/.github/workflows/sievelib.yml 2025-01-08
10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/.github/workflows/sievelib.yml 2026-02-25
09:16:26.000000000 +0100
@@ -14,7 +14,7 @@
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: [3.8, 3.9, '3.10', '3.11']
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
fail-fast: false
steps:
- uses: actions/checkout@v4
@@ -27,15 +27,15 @@
pip install codecov pytest pytest-cov
pip install -e .
- name: Run tests
- if: ${{ matrix.python-version != '3.11' }}
+ if: ${{ matrix.python-version != '3.14' }}
run: |
pytest
- name: Run tests and coverage
- if: ${{ matrix.python-version == '3.11' }}
+ if: ${{ matrix.python-version == '3.14' }}
run: |
pytest --cov=sievelib --cov-report xml
- name: Upload coverage result
- if: ${{ matrix.python-version == '3.11' }}
+ if: ${{ matrix.python-version == '3.14' }}
uses: actions/upload-artifact@v4
with:
name: coverage-results
@@ -69,13 +69,14 @@
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Set up Python 3.11
+ - name: Set up Python 3.14
uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: '3.14'
- name: Create package
run: |
- python setup.py sdist
+ python -m pip install build
+ python -m build
- name: Publish to Test PyPI
if: endsWith(github.event.ref, '/master')
uses: pypa/gh-action-pypi-publish@release/v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/PKG-INFO new/sievelib-1.5.0/PKG-INFO
--- old/sievelib-1.4.2/PKG-INFO 2025-01-08 10:26:07.744957700 +0100
+++ new/sievelib-1.5.0/PKG-INFO 2026-02-25 09:16:29.279206000 +0100
@@ -1,11 +1,30 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: sievelib
-Version: 1.4.2
+Version: 1.5.0
Summary: Client-side SIEVE library
-Home-page: https://github.com/tonioo/sievelib
-Author: Antoine Nguyen
-Author-email: [email protected]
-License: MIT
+Author-email: Antoine Nguyen <[email protected]>
+License: Copyright (c) 2011-2024 Antoine Nguyen <[email protected]>
+
+ Permission is hereby granted, free of charge, to any person obtaining
a copy
+ of this software and associated documentation files (the "Software"),
to deal
+ in the Software without restriction, including without limitation the
rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN
+ THE SOFTWARE.
+
+Project-URL: Repository, https://github.com/tonioo/sievelib
+Project-URL: Issues, https://github.com/tonioo/sievelib/issues
Keywords: sieve,managesieve,parser,client
Classifier: Programming Language :: Python
Classifier: Development Status :: 5 - Production/Stable
@@ -14,8 +33,16 @@
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Communications :: Email :: Filters
-Requires-Python: >=3.7
+Requires-Python: >=3.10
+Description-Content-Type: text/x-rst
License-File: COPYING
+Requires-Dist: typing-extensions
+Provides-Extra: dev
+Requires-Dist: pre-commit; extra == "dev"
+Requires-Dist: black; extra == "dev"
+Requires-Dist: pylint; extra == "dev"
+Requires-Dist: pytest; extra == "dev"
+Dynamic: license-file
sievelib
========
@@ -62,6 +89,8 @@
* Relational (`RFC 5231 <https://tools.ietf.org/html/rfc5231>`_)
* Imap4flags (`RFC 5232 <https://tools.ietf.org/html/rfc5232>`_)
* Regular expression (`Draft
<https://datatracker.ietf.org/doc/html/draft-murchison-sieve-regex-08/>`_)
+* Notifications (`RFC 5435 <https://datatracker.ietf.org/doc/html/rfc5435>`_)
+* Environment (`RFC 5183 <https://datatracker.ietf.org/doc/html/rfc5183>`_)
The following extensions are partially supported:
@@ -80,7 +109,6 @@
args_definition = [
{"name": "testtag",
"type": ["tag"],
- "write_tag": True,
"values": [":testtag"],
"extra_arg": {"type": "number",
"required": False},
@@ -151,7 +179,9 @@
it.
For the ``AUTHENTICATE`` command, supported mechanisms are ``DIGEST-MD5``,
-``PLAIN``, ``LOGIN`` and ``OAUTHBEARER``.
+``PLAIN``, ``LOGIN``, ``OAUTHBEARER`` and ``XOAUTH2``.
+
+Both explicit TLS via STARTTLS and implicit TLS are supported.
Basic usage
^^^^^^^^^^^
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/README.rst
new/sievelib-1.5.0/README.rst
--- old/sievelib-1.4.2/README.rst 2025-01-08 10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/README.rst 2026-02-25 09:16:26.000000000 +0100
@@ -43,6 +43,8 @@
* Relational (`RFC 5231 <https://tools.ietf.org/html/rfc5231>`_)
* Imap4flags (`RFC 5232 <https://tools.ietf.org/html/rfc5232>`_)
* Regular expression (`Draft
<https://datatracker.ietf.org/doc/html/draft-murchison-sieve-regex-08/>`_)
+* Notifications (`RFC 5435 <https://datatracker.ietf.org/doc/html/rfc5435>`_)
+* Environment (`RFC 5183 <https://datatracker.ietf.org/doc/html/rfc5183>`_)
The following extensions are partially supported:
@@ -61,7 +63,6 @@
args_definition = [
{"name": "testtag",
"type": ["tag"],
- "write_tag": True,
"values": [":testtag"],
"extra_arg": {"type": "number",
"required": False},
@@ -132,7 +133,9 @@
it.
For the ``AUTHENTICATE`` command, supported mechanisms are ``DIGEST-MD5``,
-``PLAIN``, ``LOGIN`` and ``OAUTHBEARER``.
+``PLAIN``, ``LOGIN``, ``OAUTHBEARER`` and ``XOAUTH2``.
+
+Both explicit TLS via STARTTLS and implicit TLS are supported.
Basic usage
^^^^^^^^^^^
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/pyproject.toml
new/sievelib-1.5.0/pyproject.toml
--- old/sievelib-1.4.2/pyproject.toml 1970-01-01 01:00:00.000000000 +0100
+++ new/sievelib-1.5.0/pyproject.toml 2026-02-25 09:16:26.000000000 +0100
@@ -0,0 +1,37 @@
+[build-system]
+requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.4"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "sievelib"
+dynamic = [
+ "version",
+ "dependencies",
+ "optional-dependencies"
+]
+authors = [
+ { name="Antoine Nguyen", email="[email protected]" },
+]
+description = "Client-side SIEVE library"
+readme = "README.rst"
+requires-python = ">=3.10"
+classifiers = [
+ "Programming Language :: Python",
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Communications :: Email :: Filters",
+]
+keywords = ["sieve", "managesieve", "parser", "client"]
+license = { file = "COPYING" }
+
+[project.urls]
+Repository = "https://github.com/tonioo/sievelib"
+Issues = "https://github.com/tonioo/sievelib/issues"
+
+[tool.setuptools.dynamic]
+version = { attr = "sievelib.get_version" }
+dependencies = { file = ["requirements.txt"] }
+optional-dependencies.dev = { file = ["dev-requirements.txt"] }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/requirements.txt
new/sievelib-1.5.0/requirements.txt
--- old/sievelib-1.4.2/requirements.txt 2025-01-08 10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/requirements.txt 2026-02-25 09:16:26.000000000 +0100
@@ -1,3 +1 @@
-# Requirements are listed in setup.py. The dot on the next line refers to
-# the current directory, instructing installers to use this package's setup.py
-.
+typing-extensions
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/setup.cfg new/sievelib-1.5.0/setup.cfg
--- old/sievelib-1.4.2/setup.cfg 2025-01-08 10:26:07.744957700 +0100
+++ new/sievelib-1.5.0/setup.cfg 2026-02-25 09:16:29.279206000 +0100
@@ -1,6 +1,3 @@
-[bdist_wheel]
-universal = 1
-
[egg_info]
tag_build =
tag_date = 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/setup.py new/sievelib-1.5.0/setup.py
--- old/sievelib-1.4.2/setup.py 2025-01-08 10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/setup.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,51 +0,0 @@
-#!/usr/bin/env python
-
-"""
-A setuptools based setup module.
-
-See:
-https://packaging.python.org/en/latest/distributing.html
-"""
-
-import io
-from os import path
-from setuptools import setup, find_packages
-
-
-def local_scheme(version):
- """
- Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2)
- to be able to upload to Test PyPI
- """
- return ""
-
-
-if __name__ == "__main__":
- HERE = path.abspath(path.dirname(__file__))
- with io.open(path.join(HERE, "README.rst"), encoding="utf-8") as readme:
- LONG_DESCRIPTION = readme.read()
- setup(
- name="sievelib",
- packages=find_packages(),
- include_package_data=True,
- description="Client-side SIEVE library",
- author="Antoine Nguyen",
- author_email="[email protected]",
- url="https://github.com/tonioo/sievelib",
- license="MIT",
- keywords=["sieve", "managesieve", "parser", "client"],
- install_requires=["typing-extensions"],
- setup_requires=["setuptools_scm"],
- use_scm_version={"local_scheme": local_scheme},
- classifiers=[
- "Programming Language :: Python",
- "Development Status :: 5 - Production/Stable",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Operating System :: OS Independent",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: Communications :: Email :: Filters",
- ],
- python_requires=">=3.7",
- long_description=LONG_DESCRIPTION,
- )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/__init__.py
new/sievelib-1.5.0/sievelib/__init__.py
--- old/sievelib-1.4.2/sievelib/__init__.py 2025-01-08 10:26:03.000000000
+0100
+++ new/sievelib-1.5.0/sievelib/__init__.py 2026-02-25 09:16:26.000000000
+0100
@@ -0,0 +1,16 @@
+import os
+
+
+def local_scheme(version):
+ return ""
+
+
+def get_version():
+ from setuptools_scm import get_version as default_version
+
+ github_version = os.environ.get("GITHUB_REF_NAME", None)
+ github_type = os.environ.get("GITHUB_REF_TYPE", None)
+ if github_version is not None and github_type == "tag":
+ print(f"GITHUB_REF_NAME found, using version: {github_version}")
+ return github_version
+ return default_version(local_scheme=local_scheme)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/commands.py
new/sievelib-1.5.0/sievelib/commands.py
--- old/sievelib-1.4.2/sievelib/commands.py 2025-01-08 10:26:03.000000000
+0100
+++ new/sievelib-1.5.0/sievelib/commands.py 2026-02-25 09:16:26.000000000
+0100
@@ -85,6 +85,7 @@
type: Union[str, List[str]]
values: NotRequired[List[str]]
valid_for: NotRequired[List[str]]
+ required: NotRequired[bool]
class CommandArg(TypedDict):
@@ -301,7 +302,7 @@
atype = arg["extra_arg"]["type"]
else:
continue
- if type(value) == list:
+ if isinstance(value, list):
if self.__get_arg_type(arg["name"]) == ["testlist"]:
for t in value:
t.dump(indentlevel, target)
@@ -325,7 +326,7 @@
if not arg["name"] in self.arguments:
continue
value = self.arguments[arg["name"]]
- if type(value) == list:
+ if isinstance(value, list):
if self.__get_arg_type(arg["name"]) == ["testlist"]:
for t in value:
for node in t.walk():
@@ -465,7 +466,7 @@
return False
if self.curarg is not None and "extra_arg" in self.curarg:
- condition = atype in self.curarg["extra_arg"]["type"] and (
+ condition: bool = atype in self.curarg["extra_arg"]["type"] and (
"values" not in self.curarg["extra_arg"]
or avalue in self.curarg["extra_arg"]["values"]
)
@@ -500,7 +501,7 @@
self.arguments[curarg["name"]] = avalue
break
- condition: bool = atype in curarg["type"] and
self.__is_valid_value_for_arg(
+ condition = atype in curarg["type"] and
self.__is_valid_value_for_arg(
curarg, avalue, check_extension
)
if condition:
@@ -570,7 +571,7 @@
loaded_extensions: List[str] = []
def complete_cb(self):
- if type(self.arguments["capabilities"]) != list:
+ if not isinstance(self.arguments["capabilities"], list):
exts = [self.arguments["capabilities"]]
else:
exts = self.arguments["capabilities"]
@@ -611,16 +612,22 @@
def args_as_tuple(self):
args = []
- for name, value in list(self.arguments.items()):
+ for argdef in self.args_definition:
unquote = False
- for argdef in self.args_definition:
- if name == argdef["name"]:
- condition = (
- "string" in argdef["type"] or "stringlist" in
argdef["type"]
- )
- if condition:
- unquote = True
- break
+ if argdef["name"] not in self.arguments:
+ continue
+ value = self.arguments[argdef["name"]]
+ atype = argdef["type"]
+ if "tag" in atype:
+ args.append(value)
+ if argdef["name"] in self.extra_arguments:
+ value = self.extra_arguments[argdef["name"]]
+ atype = argdef["extra_arg"]["type"]
+ else:
+ continue
+ condition = "string" in atype or "stringlist" in atype
+ if condition:
+ unquote = True
if unquote:
if "," in value:
args += tools.to_list(value)
@@ -727,6 +734,47 @@
extension = "imap4flags"
+class NotifyCommand(ActionCommand):
+ """
+ Notify extension
+
+ https://datatracker.ietf.org/doc/html/rfc5435
+ """
+
+ extension = "enotify"
+ args_definition = [
+ {
+ "name": "from",
+ "type": ["tag"],
+ "values": [":from"],
+ "required": False,
+ "extra_arg": {"type": "string", "required": True},
+ },
+ {
+ "name": "importance",
+ "type": ["tag"],
+ "values": [":importance"],
+ "required": False,
+ "extra_arg": {"type": "string", "required": True},
+ },
+ {
+ "name": "options",
+ "type": ["tag"],
+ "values": [":options"],
+ "required": False,
+ "extra_arg": {"type": "stringlist", "required": True},
+ },
+ {
+ "name": "message",
+ "type": ["tag"],
+ "values": [":message"],
+ "required": False,
+ "extra_arg": {"type": "string", "required": True},
+ },
+ {"name": "method", "type": ["string"], "required": True},
+ ]
+
+
class TestCommand(Command):
"""Indermediate class to represent "test" commands"""
@@ -997,6 +1045,22 @@
return result
+class EnvironmentCommand(TestCommand):
+ """
+ environment test.
+
+ https://datatracker.ietf.org/doc/html/rfc5183
+ """
+
+ extension = "environment"
+ args_definition = [
+ comparator,
+ match_type,
+ {"name": "name", "type": ["string"], "required": True},
+ {"name": "key-list", "type": ["string", "stringlist"], "required":
True},
+ ]
+
+
class VacationCommand(ActionCommand):
extension = "vacation"
args_definition = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/factory.py
new/sievelib-1.5.0/sievelib/factory.py
--- old/sievelib-1.4.2/sievelib/factory.py 2025-01-08 10:26:03.000000000
+0100
+++ new/sievelib-1.5.0/sievelib/factory.py 2026-02-25 09:16:26.000000000
+0100
@@ -14,6 +14,7 @@
from typing_extensions import NotRequired
from sievelib import commands
+from sievelib.parser import Parser
class FilterAlreadyExists(Exception):
@@ -73,7 +74,7 @@
return False
return True
- def from_parser_result(self, parser: "sievelib.parser.Parser"):
+ def from_parser_result(self, parser: Parser) -> None:
cpt = 1
for f in parser.result:
if isinstance(f, commands.RequireCommand):
@@ -113,7 +114,7 @@
def check_if_arg_is_extension(self, arg: str):
"""Include extension if arg requires one."""
- args_using_extensions = {":copy": "copy"}
+ args_using_extensions = {":copy": "copy", ":create": "mailbox"}
if arg in args_using_extensions:
self.require(args_using_extensions[arg])
@@ -154,11 +155,15 @@
cmd = commands.get_command_instance("header", parent)
cmd.check_next_arg("tag", tag)
if isinstance(condition[0], list):
- cmd.check_next_arg("stringlist", [self.__quote_if_necessary(c) for
c in condition[0]])
+ cmd.check_next_arg(
+ "stringlist", [self.__quote_if_necessary(c) for c in
condition[0]]
+ )
else:
cmd.check_next_arg("string",
self.__quote_if_necessary(condition[0]))
if isinstance(condition[2], list):
- cmd.check_next_arg("stringlist", [self.__quote_if_necessary(c) for
c in condition[2]])
+ cmd.check_next_arg(
+ "stringlist", [self.__quote_if_necessary(c) for c in
condition[2]]
+ )
else:
cmd.check_next_arg("string",
self.__quote_if_necessary(condition[2]))
return cmd
@@ -169,7 +174,8 @@
actions: List[tuple],
matchtype: str = "anyof",
) -> commands.Command:
- """Create a new filter
+ """
+ Create a new filter.
A filter is composed of:
* a name
@@ -228,6 +234,23 @@
"stringlist",
"[{}]".format(",".join('"{}"'.format(val) for val in
c[3])),
)
+ elif cname == "address":
+ cmd = commands.get_command_instance("address", ifcontrol,
False)
+ if c[1].startswith(":not"):
+ comp_tag = c[1].replace("not", "")
+ negate = True
+ else:
+ comp_tag = c[1]
+ cmd.check_next_arg("tag", comp_tag)
+ for arg in c[2:]:
+ if isinstance(arg, str):
+ finalarg = self.__quote_if_necessary(arg)
+ else:
+ finalarg = "[{}]".format(
+ ",".join('"{}"'.format(val) for val in arg)
+ )
+ cmd.check_next_arg("stringlist", finalarg)
+
elif cname == "body":
cmd = commands.get_command_instance("body", ifcontrol, False)
self.require(cmd.extension)
@@ -317,7 +340,7 @@
conditions: List[tuple],
actions: List[tuple],
matchtype: str = "anyof",
- ):
+ ) -> None:
"""Add a new filter to this filters set
:param name: the filter's name
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/managesieve.py
new/sievelib-1.5.0/sievelib/managesieve.py
--- old/sievelib-1.4.2/sievelib/managesieve.py 2025-01-08 10:26:03.000000000
+0100
+++ new/sievelib-1.5.0/sievelib/managesieve.py 2026-02-25 09:16:26.000000000
+0100
@@ -1,5 +1,5 @@
"""
-A MANAGESIEVE client.
+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
@@ -9,9 +9,10 @@
"""
import base64
+from functools import wraps
import re
import socket
-import ssl
+import ssl as ssllib
from typing import Any, List, Optional, Tuple
from .digest_md5 import DigestMD5
@@ -30,7 +31,7 @@
"VERSION",
]
-SUPPORTED_AUTH_MECHS = ["DIGEST-MD5", "PLAIN", "LOGIN", "OAUTHBEARER"]
+SUPPORTED_AUTH_MECHS = ["DIGEST-MD5", "PLAIN", "LOGIN", "OAUTHBEARER",
"XOAUTH2"]
class Error(Exception):
@@ -62,6 +63,7 @@
:param meth: the original called method
"""
+ @wraps(meth)
def check(cls, *args, **kwargs):
if cls.authenticated:
return meth(cls, *args, **kwargs)
@@ -74,7 +76,13 @@
read_size = 4096
read_timeout = 5
- def __init__(self, srvaddr: str, srvport: int = 4190, debug: bool = False):
+ def __init__(
+ self,
+ srvaddr: str,
+ srvport: int = 4190,
+ srvhostname: Optional[str] = None,
+ debug: bool = False,
+ ):
self.srvaddr = srvaddr
self.srvport = srvport
self.__debug = debug
@@ -83,6 +91,7 @@
self.authenticated: bool = False
self.errcode: bytes = None
self.errmsg: bytes = b""
+ self.srvhostname = srvhostname if srvhostname else self.srvaddr
self.__capabilities: dict[str, str] = {}
self.__respcode_expr = re.compile(rb"(OK|NO|BYE)\s*(.+)?")
@@ -125,7 +134,7 @@
return buf
try:
buf += self.sock.recv(size)
- except (socket.timeout, ssl.SSLError):
+ except (socket.timeout, ssllib.SSLError):
raise Error("Failed to read %d bytes from the server" % size)
self.__dprint(buf)
return buf
@@ -160,7 +169,7 @@
if not len(nval):
break
self.__read_buffer += nval
- except (socket.timeout, ssl.SSLError):
+ except (socket.timeout, ssllib.SSLError):
raise Error("Failed to read data from the server")
if len(ret):
@@ -181,7 +190,7 @@
"""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*(.+)?).
+ like a response (OK|NO|BYE\\s*(.+)?).
If *nblines* > 0, we read excactly nblines before returning.
@@ -217,7 +226,7 @@
"""Format command arguments before sending them.
Command arguments of type string must be quoted, the only
- exception concerns size indication (of the form {\d\+?}).
+ exception concerns size indication (of the form {\\d\\+?}).
:param args: list of arguments
:return: a list for transformed arguments
@@ -312,7 +321,7 @@
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*".+"
+ 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.
@@ -419,6 +428,27 @@
return True
return False
+ def _xoauth2_authentication(
+ self, login: bytes, password: bytes, authz_id: bytes = b""
+ ) -> bool:
+ """
+ OAUTHBEARER authentication.
+
+ :param login: username
+ :param password: access token
+ :return: True on success, False otherwise.
+ """
+ if isinstance(login, str):
+ login = login.encode("utf-8")
+ if isinstance(password, str):
+ password = password.encode("utf-8")
+ token = b"user=" + login + b",\001auth=Bearer " + password +
b"\001\001"
+ token = base64.b64encode(token)
+ code, data = self.__send_command("AUTHENTICATE", [b"XOAUTH2", token])
+ if code == "OK":
+ return True
+ return False
+
def __authenticate(
self,
login: str,
@@ -467,7 +497,20 @@
self.errmsg = b"No suitable mechanism found"
return False
- def __starttls(self, keyfile=None, certfile=None) -> bool:
+ def __enable_ssl(
+ self, keyfile: Optional[str] = None, certfile: Optional[str] = None
+ ):
+ """Enable encryption for current socket."""
+ context = ssllib.create_default_context()
+ if certfile is not None:
+ context.load_cert_chain(certfile, keyfile=keyfile)
+ try:
+ nsock = context.wrap_socket(self.sock,
server_hostname=self.srvhostname)
+ except ssllib.SSLError as e:
+ raise Error("SSL error: %s" % str(e))
+ self.sock = nsock
+
+ def __starttls(self, **kwargs) -> bool:
"""STARTTLS command
See MANAGESIEVE specifications, section 2.2.
@@ -481,15 +524,7 @@
code, data = self.__send_command("STARTTLS")
if code != "OK":
return False
- context = ssl.create_default_context()
- if certfile is not None:
- context.load_cert_chain(certfile, keyfile=keyfile)
- try:
- # nsock = ssl.wrap_socket(self.sock, keyfile, certfile)
- nsock = context.wrap_socket(self.sock,
server_hostname=self.srvaddr)
- except ssl.SSLError as e:
- raise Error("SSL error: %s" % str(e))
- self.sock = nsock
+ self.__enable_ssl(**kwargs)
self.__capabilities = {}
self.__get_capabilities()
return True
@@ -542,6 +577,7 @@
password: str,
authz_id: str = "",
starttls: bool = False,
+ ssl: bool = False,
authmech: Optional[str] = None,
):
"""Establish a connection with the server.
@@ -552,6 +588,7 @@
:param login: username
:param password: clear password
:param starttls: use a TLS connection or not
+ :param ssl: use implict TLS/SSL when connecting
:param authmech: prefered authenticate mechanism
:rtype: boolean
"""
@@ -561,9 +598,12 @@
except socket.error as msg:
raise Error("Connection to server failed: %s" % str(msg))
+ if ssl:
+ self.__enable_ssl()
+
if not self.__get_capabilities():
raise Error("Failed to read capabilities from server")
- if starttls and not self.__starttls():
+ if not ssl and starttls and not self.__starttls():
return False
if self.__authenticate(login, password, authz_id, authmech):
return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/parser.py
new/sievelib-1.5.0/sievelib/parser.py
--- old/sievelib-1.4.2/sievelib/parser.py 2025-01-08 10:26:03.000000000
+0100
+++ new/sievelib-1.5.0/sievelib/parser.py 2026-02-25 09:16:26.000000000
+0100
@@ -25,7 +25,7 @@
class Lexer:
- """
+ r"""
The lexical analysis part.
This class provides a simple way to define tokens (with patterns)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/tests/test_factory.py
new/sievelib-1.5.0/sievelib/tests/test_factory.py
--- old/sievelib-1.4.2/sievelib/tests/test_factory.py 2025-01-08
10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/sievelib/tests/test_factory.py 2026-02-25
09:16:26.000000000 +0100
@@ -1,8 +1,8 @@
import unittest
import io
+from sievelib import parser
from sievelib.factory import FilterAlreadyExists, FiltersSet
-from .. import parser
class FactoryTestCase(unittest.TestCase):
@@ -577,7 +577,17 @@
self.fs.addfilter(
"test",
[("Subject", ":matches", "*")],
- [("vacation", ":subject", "Example Autoresponder Subject",
":days", 7, ":mime", "Example Autoresponder Body")],
+ [
+ (
+ "vacation",
+ ":subject",
+ "Example Autoresponder Subject",
+ ":days",
+ 7,
+ ":mime",
+ "Example Autoresponder Body",
+ )
+ ],
)
output = io.StringIO()
self.fs.tosieve(output)
@@ -595,7 +605,18 @@
def test_dump(self):
self.fs.addfilter(
"test",
- [("Subject", ":matches", "*")], [("vacation", ":subject", "Example
Autoresponder Subject", ":days", 7, ":mime", "Example Autoresponder Body")]
+ [("Subject", ":matches", "*")],
+ [
+ (
+ "vacation",
+ ":subject",
+ "Example Autoresponder Subject",
+ ":days",
+ 7,
+ ":mime",
+ "Example Autoresponder Body",
+ )
+ ],
)
output = io.StringIO()
self.fs.dump(output)
@@ -634,8 +655,134 @@
"""# Filter: test
if anyof (header :contains ["X-Foo", "X-Bar"] ["bar", "baz"]) {
}
-"""
+""",
+ )
+
+ def test_address_string_args(self):
+ self.fs.addfilter(
+ "test",
+ [("address", ":is", "from", "[email protected]")],
+ [("fileinto", "folder")],
+ )
+ output = io.StringIO()
+ self.fs.tosieve(output)
+ self.assertEqual(
+ output.getvalue(),
+ """require ["fileinto"];
+
+# Filter: test
+if anyof (address :is "from" "[email protected]") {
+ fileinto "folder";
+}
+""",
+ )
+
+ def test_address_list_args(self):
+ self.fs.addfilter(
+ "test",
+ [
+ (
+ "address",
+ ":is",
+ ["from", "reply-to"],
+ ["[email protected]", "[email protected]"],
+ )
+ ],
+ [("fileinto", ":create", "folder")],
+ )
+ output = io.StringIO()
+ self.fs.tosieve(output)
+ self.assertEqual(
+ output.getvalue(),
+ """require ["fileinto", "mailbox"];
+
+# Filter: test
+if anyof (address :is ["from","reply-to"] ["[email protected]","[email protected]"])
{
+ fileinto :create "folder";
+}
+""",
+ )
+
+ def test_notify_action(self):
+ self.fs.addfilter(
+ "test",
+ [
+ (
+ "from",
+ ":contains",
+ "[email protected]",
+ )
+ ],
+ [
+ (
+ "notify",
+ ":importance",
+ "1",
+ ":message",
+ "This is probably very important",
+ "mailto:[email protected]",
+ )
+ ],
+ )
+
+ output = io.StringIO()
+ self.fs.tosieve(output)
+ self.assertEqual(
+ output.getvalue(),
+ """require ["enotify"];
+
+# Filter: test
+if anyof (header :contains "from" "[email protected]") {
+ notify :importance "1" :message "This is probably very important"
"mailto:[email protected]";
+}
+""",
+ )
+
+ def test_get_filter_actions_with_extra_args(self):
+ self.fs.require("date")
+ self.fs.require("relational")
+ self.fs.require("vacation")
+ conditions = [
+ ("currentdate", ":zone", "+0500", ":value", "ge", "date",
"2026-01-01"),
+ ("currentdate", ":zone", "+0500", ":value", "le", "date",
"2026-01-31"),
+ ]
+
+ actions = [
+ (
+ "vacation",
+ ":days",
+ 1,
+ ":addresses",
+ "[email protected]",
+ ":from",
+ "[email protected]",
+ ":subject",
+ "Subject",
+ "I'll be off until december 31th",
)
+ ]
+
+ rule_name = "Vacation rule"
+ self.fs.addfilter(rule_name, conditions, actions, "allof")
+ actionsr = self.fs.get_filter_actions(rule_name)
+ self.assertEqual(
+ actionsr,
+ [
+ (
+ "vacation",
+ ":subject",
+ "Subject",
+ ":days",
+ 1,
+ ":from",
+ "[email protected]",
+ ":addresses",
+ "[email protected]",
+ "I'll be off until december 31th",
+ )
+ ],
+ )
+
if __name__ == "__main__":
unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/tests/test_managesieve.py
new/sievelib-1.5.0/sievelib/tests/test_managesieve.py
--- old/sievelib-1.4.2/sievelib/tests/test_managesieve.py 2025-01-08
10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/sievelib/tests/test_managesieve.py 2026-02-25
09:16:26.000000000 +0100
@@ -8,14 +8,14 @@
CAPABILITIES = (
b'"IMPLEMENTATION" "Example1 ManageSieved v001"\r\n'
b'"VERSION" "1.0"\r\n'
- b'"SASL" "PLAIN SCRAM-SHA-1 GSSAPI OAUTHBEARER"\r\n'
+ b'"SASL" "PLAIN SCRAM-SHA-1 GSSAPI OAUTHBEARER XOAUTH2"\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 OAUTHBEARER"\r\n'
+ b'"SASL" "PLAIN SCRAM-SHA-1 GSSAPI OAUTHBEARER XOAUTH2"\r\n'
b'"SIEVE" "fileinto vacation"\r\n'
b'"STARTTLS"\r\n'
)
@@ -64,6 +64,11 @@
mock_socket.return_value.recv.side_effect = (AUTHENTICATION,)
self.assertTrue(self.client.connect("user", "token",
authmech="OAUTHBEARER"))
+ def test_auth_xoauth2(self, mock_socket):
+ """Test XOAUTH2 mechanism."""
+ mock_socket.return_value.recv.side_effect = (AUTHENTICATION,)
+ self.assertTrue(self.client.connect("user", "token",
authmech="XOAUTH2"))
+
def test_capabilities(self, mock_socket):
"""Test capabilities command."""
self.authenticate(mock_socket)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib/tests/test_parser.py
new/sievelib-1.5.0/sievelib/tests/test_parser.py
--- old/sievelib-1.4.2/sievelib/tests/test_parser.py 2025-01-08
10:26:03.000000000 +0100
+++ new/sievelib-1.5.0/sievelib/tests/test_parser.py 2026-02-25
09:16:26.000000000 +0100
@@ -4,7 +4,6 @@
import unittest
import os.path
-import codecs
import io
from sievelib.parser import Parser
@@ -17,7 +16,6 @@
{
"name": "testtag",
"type": ["tag"],
- "write_tag": True,
"values": [":testtag"],
"extra_arg": {"type": "number", "required": False},
"required": False,
@@ -31,7 +29,6 @@
{
"name": "subject",
"type": ["tag"],
- "write_tag": True,
"values": [":subject"],
"extra_arg": {"type": "string"},
"required": False,
@@ -39,7 +36,6 @@
{
"name": "recipient",
"type": ["tag"],
- "write_tag": True,
"values": [":recipient"],
"extra_arg": {"type": "stringlist"},
"required": True,
@@ -102,7 +98,7 @@
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:
+ with open(utf8_sieve, encoding="utf8") as fobj:
source_sieve = fobj.read()
self.parser.parse_file(utf8_sieve)
self.sieve_is(source_sieve)
@@ -663,6 +659,20 @@
"""
)
+ def test_notify_extension(self):
+ self.compilation_ok(
+ b"""require ["enotify", "fileinto", "variables"];
+
+if header :contains "from" "[email protected]" {
+ notify :importance "1"
+ :message "This is probably very important"
+ "mailto:[email protected]";
+ # Don't send any further notifications
+ stop;
+}
+"""
+ )
+
class InvalidSyntaxes(SieveTest):
def test_nested_comments(self):
@@ -850,6 +860,20 @@
"""
)
+ def test_notify_extension_importance_no_args(self):
+ self.compilation_ko(
+ b"""require ["enotify", "fileinto", "variables"];
+
+if header :contains "from" "[email protected]" {
+ notify :importance
+ :message "This is probably very important";
+ "mailto:[email protected]"
+ # Don't send any further notifications
+ stop;
+}
+"""
+ )
+
class LanguageRestrictions(SieveTest):
def test_unknown_control(self):
@@ -1042,6 +1066,32 @@
)
+class EnvironmentCommand(SieveTest):
+
+ def test_environment_test(self):
+ self.compilation_ok(
+ b"""require ["environment", "fileinto"];
+if environment :matches "remote_ip" "192.168.*" {
+ fileinto "INBOX.Unsafe_Emails";
+ stop;
+}
+"""
+ )
+
+ def test_missing_import(self):
+ self.compilation_ko(
+ b"""require ["fileinto"];
+if environment :matches "remote_ip" "192.168.*" {
+ fileinto "INBOX.Unsafe_Emails";
+ stop;
+}
+"""
+ )
+ self.assertEqual(
+ self.parser.error, "line 2: extension 'environment' not loaded"
+ )
+
+
class VariablesCommands(SieveTest):
def test_set_command(self):
self.compilation_ok(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib.egg-info/PKG-INFO
new/sievelib-1.5.0/sievelib.egg-info/PKG-INFO
--- old/sievelib-1.4.2/sievelib.egg-info/PKG-INFO 2025-01-08
10:26:07.000000000 +0100
+++ new/sievelib-1.5.0/sievelib.egg-info/PKG-INFO 2026-02-25
09:16:29.000000000 +0100
@@ -1,11 +1,30 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: sievelib
-Version: 1.4.2
+Version: 1.5.0
Summary: Client-side SIEVE library
-Home-page: https://github.com/tonioo/sievelib
-Author: Antoine Nguyen
-Author-email: [email protected]
-License: MIT
+Author-email: Antoine Nguyen <[email protected]>
+License: Copyright (c) 2011-2024 Antoine Nguyen <[email protected]>
+
+ Permission is hereby granted, free of charge, to any person obtaining
a copy
+ of this software and associated documentation files (the "Software"),
to deal
+ in the Software without restriction, including without limitation the
rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN
+ THE SOFTWARE.
+
+Project-URL: Repository, https://github.com/tonioo/sievelib
+Project-URL: Issues, https://github.com/tonioo/sievelib/issues
Keywords: sieve,managesieve,parser,client
Classifier: Programming Language :: Python
Classifier: Development Status :: 5 - Production/Stable
@@ -14,8 +33,16 @@
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Communications :: Email :: Filters
-Requires-Python: >=3.7
+Requires-Python: >=3.10
+Description-Content-Type: text/x-rst
License-File: COPYING
+Requires-Dist: typing-extensions
+Provides-Extra: dev
+Requires-Dist: pre-commit; extra == "dev"
+Requires-Dist: black; extra == "dev"
+Requires-Dist: pylint; extra == "dev"
+Requires-Dist: pytest; extra == "dev"
+Dynamic: license-file
sievelib
========
@@ -62,6 +89,8 @@
* Relational (`RFC 5231 <https://tools.ietf.org/html/rfc5231>`_)
* Imap4flags (`RFC 5232 <https://tools.ietf.org/html/rfc5232>`_)
* Regular expression (`Draft
<https://datatracker.ietf.org/doc/html/draft-murchison-sieve-regex-08/>`_)
+* Notifications (`RFC 5435 <https://datatracker.ietf.org/doc/html/rfc5435>`_)
+* Environment (`RFC 5183 <https://datatracker.ietf.org/doc/html/rfc5183>`_)
The following extensions are partially supported:
@@ -80,7 +109,6 @@
args_definition = [
{"name": "testtag",
"type": ["tag"],
- "write_tag": True,
"values": [":testtag"],
"extra_arg": {"type": "number",
"required": False},
@@ -151,7 +179,9 @@
it.
For the ``AUTHENTICATE`` command, supported mechanisms are ``DIGEST-MD5``,
-``PLAIN``, ``LOGIN`` and ``OAUTHBEARER``.
+``PLAIN``, ``LOGIN``, ``OAUTHBEARER`` and ``XOAUTH2``.
+
+Both explicit TLS via STARTTLS and implicit TLS are supported.
Basic usage
^^^^^^^^^^^
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib.egg-info/SOURCES.txt
new/sievelib-1.5.0/sievelib.egg-info/SOURCES.txt
--- old/sievelib-1.4.2/sievelib.egg-info/SOURCES.txt 2025-01-08
10:26:07.000000000 +0100
+++ new/sievelib-1.5.0/sievelib.egg-info/SOURCES.txt 2026-02-25
09:16:29.000000000 +0100
@@ -4,9 +4,8 @@
MANIFEST.in
README.rst
dev-requirements.txt
+pyproject.toml
requirements.txt
-setup.cfg
-setup.py
.github/workflows/sievelib.yml
sievelib/__init__.py
sievelib/commands.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sievelib-1.4.2/sievelib.egg-info/requires.txt
new/sievelib-1.5.0/sievelib.egg-info/requires.txt
--- old/sievelib-1.4.2/sievelib.egg-info/requires.txt 2025-01-08
10:26:07.000000000 +0100
+++ new/sievelib-1.5.0/sievelib.egg-info/requires.txt 2026-02-25
09:16:29.000000000 +0100
@@ -1 +1,7 @@
typing-extensions
+
+[dev]
+pre-commit
+black
+pylint
+pytest