Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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: |
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
run: pip install .
- name: Run tests
run: pytest --cov-report=html --cov-report=term-missing --cov=pysieved tests/
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ pyrightconfig.json

# Debian packages
*.deb

# Tests
.coverage
13 changes: 12 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion build_deb.sh
Original file line number Diff line number Diff line change
@@ -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 "
Expand Down
7 changes: 7 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -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 <prj-d@noris.de> 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
Expand Down
88 changes: 62 additions & 26 deletions pysieved/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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", "")
Expand All @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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()
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
},
)
Loading