## The defect The release step added in #17 commits the computed version unconditionally: ```sh git add pyproject.toml git commit -m "chore(release): v${VERSION}" ``` If `pyproject.toml` already carries that version — a re-run of a release, or a version corrected by hand — nothing is staged, `git commit` exits non-zero, and `set -euo pipefail` fails the job. By that point the image has already been pushed, so the release is half-done: published container, no tag. The next run then computes the same version again from the same commits. ## The fix Skip the commit and its branch push when there is nothing staged; tag either way. ## Verification The step was extracted from the workflow and run against a scratch repository with a real `origin`: - `VERSION` equal to the file's version → logs `pyproject.toml is already at 0.1.0`, pushes the tag, no commit. - `VERSION` different → commits `chore(release): v0.2.0`, pushes the branch, then the tag. Both paths leave `main` and the tags consistent. `bash -n` clean. Found while porting this same release scheme to `music-mirror`, which carries the guarded version from the start. --------- Co-authored-by: Emma Thorpe <emma.thorpe@citrix.com> Reviewed-on: #18
Legacy Email Proxy
Proxy an unauthenticated, unencrypted POP3 / SMTP server to authenticated IMAPS and SMTPS backends.
Features
- Exposes legacy
POP3on0.0.0.0:110and legacySMTPon0.0.0.0:25 - Forwards POP3 mailbox access to an IMAP backend
- Forwards SMTP submissions to an SMTPS backend
- Backend host, ports, and credentials are configured via environment variables
Environment Variables
-
POP3_BIND_ADDR(default0.0.0.0) -
POP3_BIND_PORT(default110) -
SMTP_BIND_ADDR(default0.0.0.0) -
SMTP_BIND_PORT(default25) -
BACKEND_IMAP_HOST -
BACKEND_IMAP_PORT(default993) -
BACKEND_IMAP_USER -
BACKEND_IMAP_PASS -
BACKEND_IMAP_USE_SSL(defaulttrue) -
BACKEND_IMAP_USE_STARTTLS(defaultfalse) -
BACKEND_SMTP_HOST -
BACKEND_SMTP_PORT(default465) -
BACKEND_SMTP_USER -
BACKEND_SMTP_PASS -
BACKEND_SMTP_USE_SSL(defaulttrue) -
BACKEND_SMTP_USE_TLS(defaultfalse) -
BACKEND_MUTATE(defaultfalse) - whentrue, POP3 deletions are propagated to the backend IMAP server (STORE +FLAGS / EXPUNGE). Default behaviour is to never mutate the backend mailbox on POP client deletions; the proxy only hides messages for the duration of the client session.
Build and run
This project targets the latest Python LTS release. The included Dockerfile uses python:3.12-slim, which is compatible with Python 3.12 and later LTS releases.
docker build -t legacy-email-proxy .
docker run --rm -p 110:110 -p 25:25 \
-e BACKEND_IMAP_HOST=imap.example.com \
-e BACKEND_IMAP_PORT=993 \
-e BACKEND_IMAP_USER=imap-user \
-e BACKEND_IMAP_PASS=imap-pass \
-e BACKEND_SMTP_HOST=smtp.example.com \
-e BACKEND_SMTP_PORT=465 \
-e BACKEND_SMTP_USER=smtp-user \
-e BACKEND_SMTP_PASS=smtp-pass \
legacy-email-proxy
The project is also a standard Python package (pyproject.toml), so it
installs without Docker. This puts a legacy-email-proxy command on PATH:
pip install .
legacy-email-proxy
requirements.txt remains the single source of runtime dependencies;
pyproject.toml reads it, so the Docker build and the package cannot drift.
Nix
The repository is a flake. It exposes the package, an overlay, and a NixOS module.
nix run .#legacy-email-proxy # run it
nix build .#legacy-email-proxy # build it; the test suite runs as part of the build
nix develop # dev shell with pytest
As a NixOS service, with the proxy's own flake as an input:
{
inputs.legacy-email-proxy.url = "git+https://code.emmathe.dev/lyrathorpe/legacy-email-proxy";
# optionally: inputs.legacy-email-proxy.inputs.nixpkgs.follows = "nixpkgs";
}
{
imports = [ inputs.legacy-email-proxy.nixosModules.default ];
services.legacy-email-proxy = {
enable = true;
settings = {
POP3_BIND_ADDR = "10.0.0.1";
SMTP_BIND_ADDR = "10.0.0.1";
BACKEND_IMAP_HOST = "imap.example.com";
BACKEND_IMAP_USER = "someone@example.com";
BACKEND_SMTP_HOST = "smtp.example.com";
BACKEND_SMTP_USER = "someone@example.com";
};
# Credentials belong here, not in `settings` -- anything in `settings`
# lands in the world-readable Nix store.
environmentFile = "/var/lib/legacy-email-proxy/backend.env";
};
}
settings accepts any of the environment variables listed above; booleans and
integers are converted for you. The service runs under DynamicUser, with
CAP_NET_BIND_SERVICE granted only while a listener is on a privileged port.
It opens no firewall ports — see "Security".
Prefer to manage the package yourself? overlays.default provides
pkgs.legacy-email-proxy, and nixosModules.legacy-email-proxy is the same
module without the package default wired to this flake.
Tests
Install development dependencies and run the test suite:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pytest -q
Notes
This implementation begins the proxy with a minimal POP3 command set and SMTP delivery path. It is designed to start development on the required application architecture.
Security
By design, the front-end POP3 (port 110) and SMTP (port 25) listeners are unencrypted and unauthenticated. Anyone who can reach port 110 obtains full mailbox access, and anyone who can reach port 25 can relay mail through the configured backend SMTP credentials, which is an open relay from the network's perspective.
Because of this, the listeners must be bound to a trusted internal network only, such as a private Docker bridge, a VPN interface, or localhost, and must not be exposed to untrusted networks or the public internet.
Operators who need to restrict the bind address can set POP3_BIND_ADDR / SMTP_BIND_ADDR to a specific internal interface instead of 0.0.0.0.