Build and publish container / build (push) Successful in 6m4s
## What - `pyproject.toml` — setuptools metadata with a `legacy-email-proxy` console script. Runtime dependencies are read dynamically from `requirements.txt`, so the Docker build and the package cannot drift apart. - `proxy_server.run()` — a synchronous entry point for that console script; `main` is a coroutine and cannot be referenced by one. The `__main__` path behaves as before. - `flake.nix` / `package.nix` — the package, `overlays.default`, a dev shell, and `checks`. The pytest suite runs as part of the build. - `module.nix` — a NixOS module: `services.legacy-email-proxy` with freeform `settings` (environment variables) and a separate `environmentFile` for credentials, so secrets stay out of the Nix store. Runs under `DynamicUser`, takes `CAP_NET_BIND_SERVICE` only while a listener is on a privileged port, and opens no firewall ports. - README: pip install, Nix usage, and a NixOS service example. ## Why The proxy could only be consumed as a container. Anyone deploying it on NixOS had to vendor a package definition into their own configuration — which is exactly what happened downstream, and is now deleted there in favour of this. ## Not in scope - The `Dockerfile` and its CI workflow are untouched. - No Nix job in CI; the runner has no Nix. The build is reproducible locally with `nix flake check`. - `version` is static and tracks the latest tag (`0.3.0`); bump it with the tag. ## Verification - `nix flake check` — package builds, 14 tests pass inside the build. - Consumed from a downstream NixOS host with `--override-input`: the unit's `ExecStart` resolves to the module's own build, and that flake's checks pass too. --------- Co-authored-by: Emma Thorpe <emma.thorpe@citrix.com> Reviewed-on: #16
140 lines
4.8 KiB
Markdown
140 lines
4.8 KiB
Markdown
# Legacy Email Proxy
|
|
|
|
Proxy an unauthenticated, unencrypted POP3 / SMTP server to authenticated IMAPS and SMTPS backends.
|
|
|
|
## Features
|
|
|
|
- Exposes legacy `POP3` on `0.0.0.0:110` and legacy `SMTP` on `0.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` (default `0.0.0.0`)
|
|
- `POP3_BIND_PORT` (default `110`)
|
|
- `SMTP_BIND_ADDR` (default `0.0.0.0`)
|
|
- `SMTP_BIND_PORT` (default `25`)
|
|
|
|
- `BACKEND_IMAP_HOST`
|
|
- `BACKEND_IMAP_PORT` (default `993`)
|
|
- `BACKEND_IMAP_USER`
|
|
- `BACKEND_IMAP_PASS`
|
|
- `BACKEND_IMAP_USE_SSL` (default `true`)
|
|
- `BACKEND_IMAP_USE_STARTTLS` (default `false`)
|
|
|
|
- `BACKEND_SMTP_HOST`
|
|
- `BACKEND_SMTP_PORT` (default `465`)
|
|
- `BACKEND_SMTP_USER`
|
|
- `BACKEND_SMTP_PASS`
|
|
- `BACKEND_SMTP_USE_SSL` (default `true`)
|
|
- `BACKEND_SMTP_USE_TLS` (default `false`)
|
|
|
|
- `BACKEND_MUTATE` (default `false`) - when `true`, 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.
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```nix
|
|
{
|
|
inputs.legacy-email-proxy.url = "git+https://code.emmathe.dev/lyrathorpe/legacy-email-proxy";
|
|
# optionally: inputs.legacy-email-proxy.inputs.nixpkgs.follows = "nixpkgs";
|
|
}
|
|
```
|
|
|
|
```nix
|
|
{
|
|
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:
|
|
|
|
```bash
|
|
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`.
|