LibrePortal
Sponsor
Install

LibrePost for developers

How LibrePost is built, how a script drives it, what its JSON looks like, and how the tests and releases work.

On this page
  1. How it fits together
  2. Scripting
  3. Building
  4. The end-to-end test
  5. Releases

LibrePost is a small Go program, librepost, that configures and watches a bundled copy of the Stalwart mail engine. It lives in its own repository. The user guide is LibrePost on any server.

How it fits together#

One declarative file, /etc/librepost/librepost.toml, is the truth. librepost apply turns it into Stalwart's settings and puts them in the engine through Stalwart's own command line tool, over a port that listens on loopback only. The program does not write a mail server of its own.

The running service, librepost run, does three things next to the engine. It starts and watches the engine. It answers the engine's recipient check: the engine asks, before it accepts a recipient, and LibrePost asks the upstream mail server with a short SMTP conversation that stops before any message, and remembers the answer. And it counts the engine's events for status.

Stalwart has no built-in way to ask another mail server whether a mailbox exists, so this is done by LibrePost through the engine's MTA hook, which calls a small local web server. If that server is down the engine defers instead of accepting, so the check fails safe.

Scripting#

Every command takes --json. Every document starts with:

{"schema_version": 1, "ok": true, "command": "status", ...}

Fields are only ever added, never renamed or removed. A change that cannot be additive raises schema_version. Errors come back as {"ok": false, "error": {"code": ..., "message": ..., "exit_code": ...}}.

Exit code Meaning
0 Success
1 Failure
2 Wrong usage
3 Invalid configuration
4 Not configured yet
5 The service is not running
6 check found a failing test, or dns-records --strict found a problem
7 Not found

A script can install and configure the relay with no prompts:

sudo apt install ./librepost_*.deb
sudo librepost init --hostname post.example.org --domain example.org --upstream-address 10.99.0.2
sudo librepost apply --json
sudo systemctl enable --now librepost
sudo librepost status --json
sudo librepost dns-records --json

status --json reports the link to the upstream (link.state, last_contact), the queue (queue.size, queue.deferred), the last 24 hours (last_24h.accepted, delivered, rejected, deferred, failed), the certificate (tls.not_after, days_left), the last errors and the version. dns-records --json lists each record with its status (ok, missing or mismatch) against live DNS, and the reverse-DNS entries.

Building#

git clone https://git.libreportal.org/LibrePortal/LibrePost
cd LibrePost
go test ./...
scripts/build-deb.sh --arch amd64

The build downloads the Stalwart engine and its command line tool from the Stalwart project and refuses to continue unless each file matches the digest pinned in packaging/engine.pins. Taking a newer engine means trying it first, because Stalwart changes its settings between 0.x releases, then changing the version and digests together.

The end-to-end test#

test/e2e/run.sh

needs only Docker. It builds the package, starts four throwaway plain Debian containers on private networks (a resolver, "the internet", the relay with the package installed, and a Stalwart server playing the private mail server) with a WireGuard link between the last two that the test makes itself, and proves, among others:

  • mail for a configured domain reaches the upstream, and an unknown recipient is refused at RCPT TO;
  • outgoing mail is delivered with a valid DKIM signature in both modes, and carries no trace of the upstream's address;
  • an open-relay attempt is refused in every way the test knows, and a password is not accepted without TLS;
  • when the upstream is away mail is queued, deferred and then delivered once it is back;
  • status, dns-records and check give the documented JSON;
  • the machine is unchanged outside the package's own files: firewall rules, network settings, listening ports and the checksum of every file.

It cleans up every container and network it made. --keep leaves them for debugging.

Releases#

A release is the .deb for amd64 and arm64, a SHA256SUMS file and a minisign signature of it, made with LibrePortal's release key. latest.json is signed too. They are published under get.libreportal.org/librepost/.

Next steps