From 86e79b89f1068c1a7b1e6615950514d5780e64ab Mon Sep 17 00:00:00 2001 From: fioannidis-noris Date: Thu, 4 Dec 2025 11:47:26 +0200 Subject: [PATCH 1/6] Refactored to allow for tests --- pysieved/main.py | 86 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/pysieved/main.py b/pysieved/main.py index 1599ddd..2d50c21 100755 --- a/pysieved/main.py +++ b/pysieved/main.py @@ -44,7 +44,28 @@ class Server(SocketServer.ForkingTCPServer): address_family = socket.AF_INET6 -def main(): +# Define defaults before they get overwritten +VERBOSITY = 10 +DEBUG = False + + +def log(l, s): + if l <= VERBOSITY: + if DEBUG: + sys.stderr.write("%s %s\n" % ("=" * l, s)) + else: + if l > 0: + lvl = syslog.LOG_NOTICE + elif l == 0: + lvl = syslog.LOG_WARNING + else: + lvl = syslog.LOG_ERR + syslog.syslog(lvl, s) + + +def cli(): + global VERBOSITY, DEBUG + parser = optparse.OptionParser() parser.add_option( "-i", @@ -136,14 +157,16 @@ def main(): dest="tls_cert", default="", ) - (options, args) = parser.parse_args() - # Read config file - config = Config(options.config) + options, args = parser.parse_args() - port = options.port or config.getint("main", "port", 2000) - addr = options.bindaddr or config.get("main", "bindaddr", "") - pidfile = options.pidfile or config.get("main", "pidfile", "/var/run/pysieved.pid") + VERBOSITY = options.verbosity + DEBUG = options.debug + + return options, args + + +def get_handler(options, config): base = options.base or config.get("main", "base", "") tls_required = options.tls_required or config.getboolean("TLS", "required", False) tls_key = options.tls_key or config.get("TLS", "key", "") @@ -153,19 +176,6 @@ def main(): # Define the log function syslog.openlog("pysieved[%d]" % (os.getpid()), 0, syslog.LOG_MAIL) - def log(l, s): - if l <= options.verbosity: - if options.debug: - sys.stderr.write("%s %s\n" % ("=" * l, s)) - else: - if l > 0: - lvl = syslog.LOG_NOTICE - elif l == 0: - lvl = syslog.LOG_WARNING - else: - lvl = syslog.LOG_ERR - syslog.syslog(lvl, s) - # Load TLS key and cert tls_privateKey = None tls_certChain = None @@ -210,10 +220,16 @@ def passphrase(): ## Import plugins ## auth = __import__( - "pysieved.plugins.%s" % config.get("main", "auth", "SASL").lower(), None, None, True + "pysieved.plugins.%s" % config.get("main", "auth", "SASL").lower(), + None, + None, + True, ) userdb = __import__( - "pysieved.plugins.%s" % config.get("main", "userdb", "passwd").lower(), None, None, True + "pysieved.plugins.%s" % config.get("main", "userdb", "passwd").lower(), + None, + None, + True, ) storage = __import__( "pysieved.plugins.%s" % config.get("main", "storage", "Dovecot").lower(), @@ -242,7 +258,8 @@ class handler(RequestHandler): def __init__(self, *args): self.params = {} - RequestHandler.__init__(self, *args) + + super().__init__(*args) def log(self, l, s): log(l, s) @@ -299,6 +316,19 @@ def get_tls_params(self): "cert": tls_certChain, } + return handler + + +def main(options: optparse.Values, _: list): + # Read config file + config = Config(options.config) + + addr = options.bindaddr or config.get("main", "bindaddr", "") + port = options.port or config.getint("main", "port", 4190) + pidfile = options.pidfile or config.get("main", "pidfile", "/var/run/pysieved.pid") + + handler = get_handler(options, config) + if options.stdin: sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM) h = handler(sock, sock.getpeername(), None) @@ -309,9 +339,15 @@ def get_tls_params(self): if not options.debug: daemon.daemon(pidfile=pidfile) - log(1, "Listening on %s port %d" % (addr or "INADDR_ANY", port)) + + log(1, f"Listening on {addr or 'INADDR_ANY'} port {port}") s.serve_forever() +def entry(): + options, args = cli() + main(options, args) + + if __name__ == "__main__": - main() + entry() From 957a6b990f77146652d030e60c6232e8e58fefe2 Mon Sep 17 00:00:00 2001 From: fioannidis-noris Date: Thu, 4 Dec 2025 11:48:41 +0200 Subject: [PATCH 2/6] Added tests --- tests/base.py | 211 +++++++++++++++++++++ tests/config.py | 48 +++++ tests/mock.passwd | 1 + tests/mock_srv/mail/t/test/.gitkeep | 1 + tests/test_managesieve.py | 278 ++++++++++++++++++++++++++++ 5 files changed, 539 insertions(+) create mode 100644 tests/base.py create mode 100644 tests/config.py create mode 100644 tests/mock.passwd create mode 100644 tests/mock_srv/mail/t/test/.gitkeep create mode 100644 tests/test_managesieve.py diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..048eb15 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,211 @@ +import base64 +import socket +from os import PathLike +from pathlib import Path +from typing import Any + +from pysieved.main import Server + + +class MockConfig: + def __init__(self, config: dict[str, dict[str, str | Any]]) -> None: + self.config = config + + def get(self, section: str, key: str, default: Any | None = None) -> str | Any: + try: + return self.config[section][key] + except KeyError: + return default + + def getint(self, section: str, key: str, default: bool | None = None): + return self.get(section, key, default) + + def getboolean(self, section: str, key: str, default: bool | None = None): + return self.get(section, key, default) + + +class MockFilesystem: + def __init__(self, base: str | PathLike, username: str) -> None: + self.base = Path(base) + + if not self.base.exists(): + raise FileNotFoundError(f"Base folder '{base}' does not exist") + + self.filters = self.base.joinpath("mail", username[0], username, ".pysieved") + self.filters.mkdir(exist_ok=True) + + def has_filter(self, name: str) -> bool: + """Check if the filter exists in the mock filesystem.""" + + return self.filters.joinpath(name).exists() + + def get_filter(self, name: str) -> bytes | None: + """Get the content of a filter.""" + + filter_path = self.filters.joinpath(name) + + if not filter_path.exists(): + return None + + with open(filter_path, "rb") as file: + content = file.read() + + return content + + def remove_filter(self, name: str) -> bool: + """Remove a filter from the filesystem.""" + + filter_path = self.filters.joinpath(name) + + if not filter_path.exists(): + return False + + filter_path.unlink() + + return True + + def create_filter(self, name: str, content: bytes) -> bool: + """Create a filter in the user's filesystem.""" + + filter_path = self.filters.joinpath(name) + + if filter_path.exists(): + return False + + with open(filter_path, "wb") as file: + file.write(content) + + return True + + +class MockClient: + def __init__(self, server: Server) -> None: + self.server = server + self._is_authenticated = False + + self.BUF_SIZE = 4096 + + address, port, *_ = self.server.socket.getsockname() + + self._default_timeout = 0.1 + + try: + # Try connecting with IPv6 first + self.conn = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + self.conn.settimeout(self._default_timeout) + self.conn.connect((address, port)) + except Exception: + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.conn.settimeout(self._default_timeout) + self.conn.connect((address, port)) + + def get_full_response(self) -> bytes: + """Read all the received lines until the server stops responding until the timeout.""" + + full_response = b"" + + while True: + try: + part = self.conn.recv(self.BUF_SIZE) + except Exception: + break + + full_response += part + + return full_response + + def _send(self, command: bytes) -> bytes: + self.conn.send(command) + return self.get_full_response() + + def authenticate(self, username: str, password: str) -> bytes | None: + """Send an AUTHENTICATE command.""" + + if self._is_authenticated: + return + + credentials = f"\x00{username}\x00{password}" + auth = base64.b64encode(credentials.encode()).decode() + + command = f'AUTHENTICATE "PLAIN" "{auth}"\r\n' + self.conn.send(command.encode()) + + response = self.get_full_response() + self._is_authenticated = True + + return response + + def listscripts(self) -> bytes: + """Send a LISTSCRIPTS command.""" + + command = b"LISTSCRIPTS\r\n" + return self._send(command) + + def capability(self) -> bytes: + """Send a CAPABILITY command.""" + + command = b"CAPABILITY\r\n" + return self._send(command) + + def havespace(self, name: str, space: int | str) -> bytes: + """Send a HAVESPACE command.""" + + command = f'HAVESPACE "{name}" "{space}"\r\n' + return self._send(command.encode()) + + def putscript( + self, + name: str, + content: bytes, + size: int | None = None, + ) -> bytes: + """Send a PUTSCRIPT command.""" + + self.conn.settimeout(2) + + if size is None: + size = len(content) + + if not content.endswith(b"\r\n"): + content += b"\r\n" + + byte_size = "{%d+}" % size + command = f'PUTSCRIPT "{name}" {byte_size}\r\n' + self.conn.sendall(command.encode()) + self.conn.sendall(content) + + response = self.get_full_response() + + self.conn.settimeout(self._default_timeout) + + return response + + def setactive(self, name: str | None = None) -> bytes: + """Send a SETACTIVE command.""" + + command = f'SETACTIVE "{name}"\r\n' + if name is None: + command = 'SETACTIVE ""\r\n' + + return self._send(command.encode()) + + def getscript(self, name: str) -> bytes: + """Send a GETSCRIPT command.""" + + command = f'GETSCRIPT "{name}"\r\n' + return self._send(command.encode()) + + def deletescript(self, name: str) -> bytes: + """Send a DELETESCRIPT command.""" + + command = f'DELETESCRIPT "{name}"\r\n' + return self._send(command.encode()) + + def logout(self) -> bytes | None: + """Send a LOGOUT command.""" + + if not self._is_authenticated: + return + + command = b"LOGOUT\r\n" + return self._send(command) diff --git a/tests/config.py b/tests/config.py new file mode 100644 index 0000000..46487d0 --- /dev/null +++ b/tests/config.py @@ -0,0 +1,48 @@ +from pathlib import Path + +cwd = Path(__file__).parent.absolute() + +# main +main_configuration = { + "auth": "htpasswd", + "userdb": "virtual", + "storage": "Exim", + "consumer": "Exim", + "port": 0, + "pidfile": "/tmp/pysieved.pid", +} + +# TLS +tls_configuration = {} + +# Virtual +storage_path = cwd.joinpath("mock_srv", "mail", "t", "test") +virtual_configuration = { + "path": str(storage_path), + "defaultdomain": "none.invalid", + "uid": -1, + "gid": -1, +} + +# htpasswd +dovecot_passwords_path = cwd.joinpath("mock.passwd") +htpasswd_configuration = { + "passwdfile": str(dovecot_passwords_path), +} + +# Exim +exim_configuration = { + "sendmail": "/usr/sbin/sendmail", + "scripts": ".pysieved", + "active": ".forward", + "uid": -1, + "gid": -1, +} + +DEFAULT_CONFIG = { + "main": main_configuration, + "TLS": tls_configuration, + "Virtual": virtual_configuration, + "htpasswd": htpasswd_configuration, + "Exim": exim_configuration, +} diff --git a/tests/mock.passwd b/tests/mock.passwd new file mode 100644 index 0000000..8edd474 --- /dev/null +++ b/tests/mock.passwd @@ -0,0 +1 @@ +test:12eMC4Wi9/C9o diff --git a/tests/mock_srv/mail/t/test/.gitkeep b/tests/mock_srv/mail/t/test/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/mock_srv/mail/t/test/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/test_managesieve.py b/tests/test_managesieve.py new file mode 100644 index 0000000..3b2d686 --- /dev/null +++ b/tests/test_managesieve.py @@ -0,0 +1,278 @@ +import warnings +from pathlib import Path +from threading import Thread +from unittest import TestCase + +from base import MockClient, MockConfig, MockFilesystem +from config import DEFAULT_CONFIG + +from pysieved.main import Server, get_handler + + +class MockOptions: + bindaddr = "" + port = 0 + pidfile = "/tmp/pysieved.pid" + base = str(Path(__file__).parent.joinpath("mock_srv")) + tls_required = False + tls_key = "" + tls_cert = "" + tls_passphrase = "" + debug = True + + +class AuthenticateTest(TestCase): + def setUp(self) -> None: + super().setUp() + + options = MockOptions() + config = MockConfig(DEFAULT_CONFIG) + + handler = get_handler(options, config) + + self.server = Server((options.bindaddr, options.port), handler) + self._t = Thread(target=self.server.serve_forever) + self._t.start() + + self.client = MockClient(self.server) + + # Ignore start up echoes + self.client.get_full_response() + + self.username = "test" + self.password = "12345" + + def tearDown(self) -> None: + self.server.shutdown() + self._t.join() + + super().tearDown() + + def test_authenticate(self) -> None: + """Test authenticating to the server.""" + + response = self.client.authenticate(self.username, self.password) + self.assertEqual(response, b"OK\r\n") + + def test_authenticate_invalid_user(self) -> None: + """Test authenticating to the server with a non-existing user.""" + + response = self.client.authenticate("does-not-exist", self.password) + + expected_response = b'NO "Bad username or password"\r\n' + self.assertEqual(response, expected_response) + + def test_authenticate_wrong_password(self) -> None: + """Test authenticating to the server with a wrong password.""" + + response = self.client.authenticate(self.username, "wrong-password") + + expected_response = b'NO "Bad username or password"\r\n' + self.assertEqual(response, expected_response) + + +class ManagesieveTest(TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + options = MockOptions() + config = MockConfig(DEFAULT_CONFIG) + + handler = get_handler(options, config) + + cls.server = Server((options.bindaddr, options.port), handler) + cls._t = Thread(target=cls.server.serve_forever) + cls._t.start() + + cls.username = "test" + cls.password = "12345" + cls.filter_name = "test_filter" + cls.filter_content = b"# Sieve filter\n# Test filter\n" + cls.OK = b"OK\r\n" + + cls.fs = MockFilesystem(options.base, cls.username) + cls.client = MockClient(cls.server) + + # Ignore start up echoes + cls.client.get_full_response() + + # Add a test script to the filesystem. + # It is removed in the teardown + created = cls.fs.create_filter(cls.filter_name, cls.filter_content) + if not created: + warnings.warn(f"Test filter '{cls.filter_name}' already exists.") + + @classmethod + def tearDownClass(cls) -> None: + # Remove initial test filter + # removed = cls.fs.remove_filter(cls.filter_name) + # if not removed: + # warnings.warn(f"Could not remove filter '{cls.filter_name}'.") + + cls.server.shutdown() + cls._t.join() + + super().tearDownClass() + + def test_listscripts(self) -> None: + """Test that valid scripts are listed.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.listscripts() + + self.assertIn(self.filter_name.encode(), response) + self.assertTrue(response.endswith(self.OK)) + + def test_capability(self) -> None: + """Test that the CAPABILITY command returns a valid response.""" + + response = self.client.capability() + + lines = response.split(b"\r\n") + + expected_sieve = b'"SIEVE" "envelope fileinto encoded-character enotify subaddress vacation copy comparator-i;ascii-casemap comparator-en;ascii-casemap comparator-i;octet comparator-i;ascii-numeric"' + self.assertEqual(lines[0], b'"IMPLEMENTATION" "pysieved 1.0"') + self.assertEqual(lines[1], b'"SASL" "PLAIN"') + self.assertEqual(lines[2], expected_sieve) + self.assertEqual(lines[3], b"OK") + + def test_havespace(self) -> None: + """Test a valid HAVESPACE command.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.havespace("mock_script", 1) + self.assertEqual(response, self.OK) + + def test_havespace_not_a_number(self) -> None: + """Test an invalid HAVESPACE command.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.havespace("mock_script", "not-a-number") + self.assertEqual(response, b'NO "Not a number"\r\n') + + def test_havespace_exceeds_quota(self) -> None: + """Test an valid HAVESPACE command with a very large byte size.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.havespace("mock_script", 999_999_999_999) + self.assertEqual(response, b'NO (QUOTA) "Quota exceeded"\r\n') + + def test_putscript(self) -> None: + """Test a valid PUTSCRIPT command with the correct size.""" + + self.client.authenticate(self.username, self.password) + + filter_name = "putscript_test" + + response = self.client.putscript(filter_name, b"# This is a test") + self.assertEqual(response, self.OK) + self.assertTrue(self.fs.has_filter(filter_name)) + + content = self.fs.get_filter(filter_name) + if content is None: + raise Exception(f"Could not read content for '{filter_name}'.") + + self.assertEqual(len(content), 32) + self.assertEqual(content, b"# Sieve filter\n# This is a test\n") + + removed = self.fs.remove_filter(filter_name) + self.assertTrue(removed) + + def test_setactive(self) -> None: + """Test the SETACTIVE command on the test script.""" + + self.client.authenticate(self.username, self.password) + + # Activate the script + response = self.client.setactive(self.filter_name) + self.assertEqual(response, self.OK) + + filters = self.client.listscripts() + has_active = any(line.endswith(b"ACTIVE") for line in filters.split(b"\r\n")) + self.assertTrue(has_active, "Script was not activated") + + # Deactivate the script + response = self.client.setactive() + self.assertEqual(response, self.OK) + + filters = self.client.listscripts() + has_active = any(line.endswith(b"ACTIVE") for line in filters.split(b"\r\n")) + self.assertFalse(has_active, "Script was not deactivated") + + # Re-activate for other tests + self.client.setactive(self.filter_name) + + def test_setactive_invalid_script(self) -> None: + """Test the SETACTIVE command without giving a filter name.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.setactive("does-not-exist") + self.assertEqual(response, b'NO "No script by that name"\r\n') + + def test_getscript(self) -> None: + """Test the GETSCRIPT command with a valid filter name.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.getscript(self.filter_name) + self.assertTrue(response.endswith(self.OK)) + + lines = response.split(b"\r\n") + byte_size, content, ok = lines[:-1] + + self.assertEqual(byte_size, b"{29}") + + self.assertEqual(content, b"# Sieve filter\n# Test filter\n") + self.assertEqual(ok, b"OK") + + self.assertEqual(response, b"{29}\r\n# Sieve filter\n# Test filter\n\r\nOK\r\n") + + def test_getscript_invalid_script(self) -> None: + """Test the GETSCRIPT command with a non-existing filter name.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.getscript("does-not-exist") + self.assertEqual(response, b'NO "No script by that name"\r\n') + + def test_deletescript(self) -> None: + """Test the DELETESCRIPT command on a valid filter.""" + + self.client.authenticate(self.username, self.password) + + # Deactivate script + self.client.setactive() + + # Send the delete command + response = self.client.deletescript(self.filter_name) + self.assertEqual(response, self.OK) + + # Recreate script and reactivate script + self.fs.create_filter(self.filter_name, self.filter_content) + self.client.setactive(self.filter_name) + + def test_deletescript_invalid_script(self) -> None: + """Test the DELETESCRIPT command with a non-existing filter name.""" + + self.client.authenticate(self.username, self.password) + + response = self.client.deletescript("does-not-exist") + self.assertEqual(response, b'NO "No script by that name"\r\n') + + def test_deletescript_active_script(self) -> None: + """Test the DELETSCRIPT command on an active script.""" + + self.client.authenticate(self.username, self.password) + + # Activate script + self.client.setactive(self.filter_name) + + # Try to delete script + response = self.client.deletescript(self.filter_name) + self.assertEqual(response, b'NO "Script is active"\r\n') From 1c8bbb4b7853680d7cc2c458cd2604a921fa5e26 Mon Sep 17 00:00:00 2001 From: fioannidis-noris Date: Thu, 4 Dec 2025 11:49:30 +0200 Subject: [PATCH 3/6] Updated gitignore to ignore test generated files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index afaa810..8b07141 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ pyrightconfig.json # Debian packages *.deb + +# Tests +.coverage From 174ea2bd58c2d8974cdf8e7e7397649cb5503d31 Mon Sep 17 00:00:00 2001 From: fioannidis-noris Date: Thu, 4 Dec 2025 11:52:11 +0200 Subject: [PATCH 4/6] Added test actions for Python 3.11 and 3.12 --- .github/workflows/tests.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a5e32e1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Unit tests + +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v5 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install application requirements + run: | + apt update --yes + apt install exim4 --yes + - name: Install test tools + run: pip install pytest==9.0.1 pytest-cov==7.0.0 + - name: Install project + run: pip install . + - name: Run tests + run: pytest --cov-report=html --cov-report=term-missing --cov=pysieved tests/ From a50b10d8337a4ca68b05f6926b3e1091e1fc1332 Mon Sep 17 00:00:00 2001 From: fioannidis-noris Date: Thu, 4 Dec 2025 12:02:12 +0200 Subject: [PATCH 5/6] Updated CHANGES and bumped version --- CHANGES.md | 13 ++++++++++++- build_deb.sh | 2 +- debian/changelog | 7 +++++++ pysieved/main.py | 2 +- setup.py | 4 ++-- tests/mock_srv/mail/t/test/.gitignore | 3 +++ 6 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 tests/mock_srv/mail/t/test/.gitignore diff --git a/CHANGES.md b/CHANGES.md index b52c80b..662ed34 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,13 +6,24 @@ All notable changes to this project will be documented in this file. ### Changed +#### 2025-12-04 + +* `./.github/workflows/tests.yml`: Added Github action for running tests. +* `./pysieved/main.py`: Refactored `main` to separate the handler logic and the `ini` parser logic. +* Added unit tests: + * `./tests/mock_srv/mail/t/test/.gitignore`: Added to ignore any filters generated by the tests. + * `./tests/mock_srv/mail/t/test/.gitkeep`: Added to keep the folder in the source control. Works as a mock storage for filters. + * `./tests/base.py`: Added classes for easier testing. Added a socket client, a filesystem handler and a config parser class. + * `./tests/config.py`: Added default configuration values to overwrite the `ini` file with. + * `./tests/mock.passwd`: Added a mock authentication file for a theoretical `test` user. + * `./tests/test_managesieve.py`: Added tests for the MANAGESIEVE protocol of the server. + #### 2025-11-26 * `./pysieved/managesieve.py`: Fixed the `readline` and `bread` to read bytes from the socket, and fixed GETSCRIPT to return bytes. Also made some small refactoring changes. * `./pysieved/plugins/FileStorage.py`: Updated the file reading function to open files in `rb` mode and return the bytes. * `./pysieved/plugins/exim.py`: Updated the `__setitem__` method to remove trailing and starting `\n` characters from the filter. - #### 2025-11-21 * Prevented duplicate headers from being added to Sieve filters in `./pysieved/plugins/exim.py`. diff --git a/build_deb.sh b/build_deb.sh index 23d9143..876ceec 100755 --- a/build_deb.sh +++ b/build_deb.sh @@ -1,6 +1,6 @@ #!/bin/bash -export BUILD_VERSION=${BUILD_VERSION:-0.2.4} +export BUILD_VERSION=${BUILD_VERSION:-0.2.5} docker build -t pysieved-deb-builder . docker run --name pysieved-builder pysieved-deb-builder bash -c " diff --git a/debian/changelog b/debian/changelog index 5f95863..fc4e33b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +pysieved (0.2.5) jammy; urgency=medium + + * Added tests + * Refactored entrypoint to separate server/handler logic and parser logic + + -- Team noris SKG-PRJ Mon, 04 DEC 2025 12:00:12 +0200 + pysieved (0.2.4) jammy; urgency=medium * Fixed PUTSCRIPT and GETSCRIPT to read and write bytes, as to support UTF-8 characters diff --git a/pysieved/main.py b/pysieved/main.py index 2d50c21..3fb4eb9 100755 --- a/pysieved/main.py +++ b/pysieved/main.py @@ -18,7 +18,7 @@ ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 ## USA # -# 30 May 2025 - Modified by F. Ioannidis. +# 04 December 2025 - Modified by F. Ioannidis. import optparse diff --git a/setup.py b/setup.py index 1637d32..907735f 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,11 @@ setup( name="pysieved", description="Core daemon for the pysieved project", - version=os.getenv("BUILD_VERSION", "0.2.4"), + version=os.getenv("BUILD_VERSION", "0.2.5"), packages=find_packages(where="."), entry_points={ "console_scripts": [ - "pysieved=pysieved.main:main", + "pysieved=pysieved.main:entry", ] }, ) diff --git a/tests/mock_srv/mail/t/test/.gitignore b/tests/mock_srv/mail/t/test/.gitignore new file mode 100644 index 0000000..c3d07e5 --- /dev/null +++ b/tests/mock_srv/mail/t/test/.gitignore @@ -0,0 +1,3 @@ +* +!.gitkeep +!.gitignore From ef954551bf4663a307dfb86486b6993e037d1233 Mon Sep 17 00:00:00 2001 From: fioannidis-noris Date: Thu, 4 Dec 2025 12:08:45 +0200 Subject: [PATCH 6/6] Fixed permission issue when using apt --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a5e32e1..fd50ca7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,8 +17,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install application requirements run: | - apt update --yes - apt install exim4 --yes + sudo apt update --yes + sudo apt install exim4 --yes - name: Install test tools run: pip install pytest==9.0.1 pytest-cov==7.0.0 - name: Install project