340 lines
14 KiB
Markdown
340 lines
14 KiB
Markdown
# Local mail protocol fixtures
|
|
|
|
These fixtures are intentionally bound to loopback and use non-standard ports.
|
|
They are development tools, not production configuration.
|
|
|
|
## Release and sanitizer harnesses
|
|
|
|
The scripts used for the 0.7 release evidence are kept here rather than as
|
|
one-off files in `/tmp`:
|
|
|
|
- `test-config-tls-material.sh` generates disposable GnuTLS material and
|
|
covers CFG-09/CFG-10 generation, permissions, reuse, and unsafe-input cases.
|
|
- `web-direct-tls-smoke.sh` covers the CFG-08 direct Web TLS 1.2/1.3 listener,
|
|
security headers, plain-HTTP rejection, and a mismatched private key.
|
|
- `generate-protocol-test-config.py` creates the disposable local protocol
|
|
configuration used by sanitizer sessions.
|
|
- `protocol-smoke.py` delivers through authenticated SMTP submission and reads
|
|
the message through IMAP/POP over explicit and implicit TLS.
|
|
- `sanitizer-protocol-session.sh` runs that protocol path under ASan/UBSan/LSan
|
|
or TSan and validates clean process shutdown.
|
|
- `asan-protocol-session.sh` and `tsan-protocol-session.sh` preserve the two
|
|
explicit entry points used by the original release runs.
|
|
- `sieve-validate-no-lsan.sh` is the narrow helper used by the ASan session;
|
|
the Sieve validator remains leak-tested directly by CTest.
|
|
- `cpack-stage-smoke.sh` checks imports and required executables from an
|
|
extracted CPack staging tree in a private mount namespace.
|
|
- `debian-source-probe.sh` and `debian-libspf2-package-build.sh` preserve the
|
|
disposable Debian container probes used before the complete bundle builder
|
|
was available.
|
|
- `setup-wizard-defaults.py`, `config-tui-edit.py`,
|
|
`legacy-config-install.py`, `protocol-diagnostics.py`, and
|
|
`store-diagnostics.py` preserve the remaining interactive and diagnostic
|
|
bring-up tests. Destructive live helpers require an explicit opt-in
|
|
environment variable.
|
|
- `mail-dns-live-check.py` logs in through the real Web and IMAPS interfaces
|
|
and verifies that setup produced both an actionable SPF/DKIM/DMARC task and
|
|
a forwardable administrator message without private-key material.
|
|
- `dkim-material-check.py` compares a real OpenDKIM-generated RSA-2048 key
|
|
with Bongo's GnuTLS-derived public value, imports it, checks permissions and
|
|
DNS output, then verifies byte-identical reuse.
|
|
- `manager-lifecycle-check.sh` exercises systemd reload/restart/stop/start,
|
|
one agent crash, and an unclean manager death which leaves a stale PID file.
|
|
It requires explicit live-test opt-in and restores an active service on exit.
|
|
- `agent-privilege-check.sh` verifies the live manager/agent process tree,
|
|
real/effective/saved UID and GID tuples, supplementary groups, and inherited,
|
|
permitted, effective, and ambient capabilities.
|
|
- `store-user-layout-check.sh` idempotently provisions disposable Store,
|
|
Queue, quota, and protocol users, then proves that two freshly accessed
|
|
accounts have separate Store databases and the complete standard folder
|
|
layout, including the IMAP `\\Junk` spam collection.
|
|
- `store-backup-compare.py` compares backups made before and after a restore.
|
|
It requires byte-identical payloads, paths, GUIDs, flags, IMAP UIDs, custom
|
|
properties, and calendar-link metadata while ignoring only Store-generated
|
|
creation/modification timestamps.
|
|
- `store-protocol-safety-check.py` sends unknown, malformed, oversized, and
|
|
unauthorised commands to a live Store listener. It verifies that
|
|
maintenance/debug commands remain manager-only, a user cannot inspect a
|
|
foreign Store, and the listener remains available after every rejection.
|
|
- `store-quota-concurrency-check.py` writes oversized contact and calendar
|
|
fixtures without charging mail quota, then races two Store deliveries
|
|
against one remaining quota slot. Exactly one delivery must succeed and the
|
|
other must receive Store status 5220.
|
|
|
|
The complete mapping from the original one-off `/tmp` names to maintained
|
|
scripts is recorded in `tmp-script-map.md`.
|
|
|
|
Every script which creates credentials requires disposable values through the
|
|
environment; no test password or private key is stored in Git. For example:
|
|
|
|
```sh
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
export BONGO_TEST_ROOT=/tmp/bongo-0.7-r1-asan-root/usr
|
|
export BONGO_SANITIZER=asan
|
|
unshare --user --map-root-user --net \
|
|
./contrib/testing/sanitizer-protocol-session.sh
|
|
```
|
|
|
|
The live user-layout check deliberately retains its clearly named accounts so
|
|
later Queue, quota, and protocol matrix rows can reuse them:
|
|
|
|
```sh
|
|
export BONGO_ALLOW_LIVE_USER_TEST=1
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
./contrib/testing/store-user-layout-check.sh
|
|
```
|
|
|
|
The Queue-to-Store byte-preservation check uses the retained `stqqueue`
|
|
account. It creates a target-999 diagnostic entry, exports the exact
|
|
Queue-owned bytes, delivers that entry through Queue's local Store path, and
|
|
compares the resulting Store object byte-for-byte:
|
|
|
|
```sh
|
|
./contrib/testing/queue-store-byte-check.sh
|
|
```
|
|
|
|
The script deletes the held Queue entry even on failure. It deliberately
|
|
retains the delivered Store object as release evidence. Set
|
|
`BONGO_QUEUE_TOOL`, `BONGO_STORE_TOOL`, or `BONGO_TEST_RUN_AS` only when
|
|
testing a non-standard installation.
|
|
|
|
The Queue disk-space check brackets `QDSPC` with filesystem samples and
|
|
verifies that the suffixless `queue` configuration's `minimumfreespace`
|
|
reserve was subtracted without 32-bit truncation:
|
|
|
|
```sh
|
|
./contrib/testing/queue-disk-space-check.sh
|
|
```
|
|
|
|
The Queue failure-safety check retains one isolated message in an 8 MiB tmpfs
|
|
fixture while it first fills that filesystem below the operative reserve and
|
|
then overlays the spool with an unreadable directory. It verifies
|
|
rejection/defer responses, restores all mounts through traps, and compares the
|
|
retained message byte-for-byte after both recoveries:
|
|
|
|
```sh
|
|
./contrib/testing/queue-failure-safety-check.sh
|
|
```
|
|
|
|
It requires passwordless permission for Bongo service start/stop/restart,
|
|
`mount`, `umount`, and `stat` on the dedicated test spool. Run it only on a
|
|
disposable test installation.
|
|
|
|
The Store protocol safety check is non-destructive and requires the two
|
|
disposable users retained by `store-user-layout-check.sh`:
|
|
|
|
```sh
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
./contrib/testing/store-protocol-safety-check.py
|
|
```
|
|
|
|
The password is read only from the environment and is redacted by the Store's
|
|
command logger. The test deliberately attempts `REINDEX`, `REPAIR`, `RESET`,
|
|
and `SHUTDOWN` as a normal user; run it only against a build where those
|
|
commands require manager privileges.
|
|
|
|
The live Store quota check uses the retained `stqquota` account. It reads and
|
|
restores the account's original limit, removes every fixture document, and
|
|
requires explicit destructive-test opt-in:
|
|
|
|
```sh
|
|
export BONGO_ALLOW_LIVE_USER_TEST=1
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
./contrib/testing/store-quota-concurrency-check.py
|
|
```
|
|
|
|
The script invokes `sudo -n bongo-admin user quota` only to set and restore the
|
|
temporary administrative limit. The password is never passed on a command
|
|
line.
|
|
|
|
The partial Queue quota check creates two held messages and temporarily leaves
|
|
space for exactly one of them. It verifies that the first reaches Store, the
|
|
second remains byte-identical in Queue after Store returns quota exceeded, and
|
|
the retained message is delivered exactly once after the first is deleted:
|
|
|
|
```sh
|
|
export BONGO_ALLOW_LIVE_USER_TEST=1
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
./contrib/testing/queue-quota-partial-check.py
|
|
```
|
|
|
|
Every fixture message and Queue entry is removed and the original quota is
|
|
restored, including after a failed assertion.
|
|
|
|
The normal SMTP companion check submits an authenticated message to the full
|
|
test mailbox. It verifies that SMTP accepts the durable Queue transaction,
|
|
Store receives no partial object, the sender receives exactly the standard
|
|
`5.2.2` DSN, and that ordinary SMTP mail is not retained indefinitely:
|
|
|
|
```sh
|
|
export BONGO_ALLOW_LIVE_USER_TEST=1
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
./contrib/testing/smtp-quota-dsn-check.py
|
|
```
|
|
|
|
The Collector companion creates a real Queue transaction carrying
|
|
`MSG_FLAG_COLLECTED_MESSAGE`. It proves that a full Store keeps the sole
|
|
durable copy in local-delivery queue 6, never hands it to the outgoing SMTP
|
|
agent, and delivers it exactly once after the quota is released:
|
|
|
|
```sh
|
|
export BONGO_ALLOW_LIVE_USER_TEST=1
|
|
export BONGO_TEST_PASSWORD='a-disposable-password-of-at-least-12-characters'
|
|
./contrib/testing/collector-quota-retry-check.py
|
|
```
|
|
|
|
Set `BONGO_TEST_TRACE=1` to print bounded per-stage timings. On failure,
|
|
fixtures are normally removed and the original quota is restored. For local
|
|
diagnosis only, `BONGO_TEST_KEEP_FAILURE=1` preserves the matching Queue entry
|
|
while still restoring the quota; remove that entry with `bongo-queuetool
|
|
delete` after inspection.
|
|
|
|
The Debian source probes and package-build commands used by BLD-14/BLD-15 are
|
|
consolidated in `contrib/debian/build-trixie-bundle.sh`; they are not maintained
|
|
as duplicate container-only snippets here.
|
|
|
|
## Cyrus IMAP, POP3, and LMTP
|
|
|
|
Install `net-mail/cyrus-imapd`, then start the disposable fixture as root:
|
|
|
|
```sh
|
|
sudo ./contrib/testing/cyrus-fixture.py start
|
|
```
|
|
|
|
It creates no files outside `/tmp/bongo-cyrus-fixture` and listens on:
|
|
|
|
- IMAP: `127.0.0.1:18143`
|
|
- POP3: `127.0.0.1:18110`
|
|
- LMTP: `127.0.0.1:18024`
|
|
|
|
The source mailboxes form a multi-account-per-user matrix:
|
|
|
|
| Bongo owner | Cyrus source account | Protocol | Bongo destination |
|
|
| --- | --- | --- | --- |
|
|
| `test1` | `test1-pop@cyrus.bongo.test` | POP3 | `INBOX` |
|
|
| `test1` | `test1-imap@cyrus.bongo.test` | IMAP | `Cyrus IMAP` |
|
|
| `test2` | `test2-pop@cyrus.bongo.test` | POP3 | `INBOX` |
|
|
| `test2` | `test2-imap@cyrus.bongo.test` | IMAP | `Cyrus IMAP` |
|
|
|
|
All source accounts use the test-only password
|
|
`BongoCollector-Test-2026`. Insert a complete RFC 5322 message through Cyrus'
|
|
real LMTP delivery path:
|
|
|
|
```sh
|
|
sudo ./contrib/testing/cyrus-fixture.py inject message.eml
|
|
```
|
|
|
|
Seed three different messages in each source mailbox and exercise Bongo's real
|
|
Collector POP3 and IMAP transport implementation:
|
|
|
|
```sh
|
|
./contrib/testing/cyrus-fixture.py verify-collector \
|
|
--probe ./build/src/agents/collector/collector-remote-transport-probe \
|
|
--count 3
|
|
```
|
|
|
|
The probe lists and fetches every message, checks its fixture marker, deletes
|
|
it through the selected protocol, and verifies that every Cyrus source mailbox
|
|
is empty. A live end-to-end run creates all four Bongo external-account
|
|
records. This proves that accounts, remote UID/UIDL values, retry state and
|
|
deletion state remain scoped by account ID even when one Bongo user owns
|
|
multiple sources. All four use `after_import`, so Cyrus deletion happens only
|
|
after the Bongo Queue has durably accepted and committed the complete message.
|
|
From that point the normal scanner, rules, quota, and Store delivery pipeline
|
|
owns it.
|
|
|
|
The fixture can also seed deterministic scanner and quota cases. All ports
|
|
remain loopback-only and the flood is deliberately bounded:
|
|
|
|
```sh
|
|
sudo ./contrib/testing/cyrus-fixture.py seed --profile gtube --count 1
|
|
sudo ./contrib/testing/cyrus-fixture.py seed --profile eicar --count 1
|
|
sudo ./contrib/testing/cyrus-fixture.py seed --profile flood --count 32 \
|
|
--message-size 131072
|
|
sudo ./contrib/testing/cyrus-fixture.py seed-suite
|
|
```
|
|
|
|
`gtube` contains SpamAssassin's standard GTUBE string. `eicar` creates the
|
|
harmless standard EICAR attachment at runtime; the complete signature is kept
|
|
out of the Git checkout to avoid host antivirus false alarms. `seed-suite`
|
|
places three normal messages, one GTUBE message, one EICAR message, and 32
|
|
128-KiB flood messages in every source mailbox (148 messages total). Set a
|
|
small quota on disposable Bongo users before collection, then restore it after
|
|
the test. The test passes only when clean messages are imported, scanner
|
|
policy is applied, quota cannot be exceeded, and a message rejected before a
|
|
durable import remains retrievable from Cyrus.
|
|
|
|
The same LMTP listener is the forwarding target for the exact transport-domain
|
|
mapping below. This accepts `*@cyrus.bongo.test` without making the test domain
|
|
a local Bongo domain:
|
|
|
|
```json
|
|
"lmtp_transports": ["cyrus.bongo.test=127.0.0.1:18024"]
|
|
```
|
|
|
|
Cyrus `lmtpd` is pre-authorized only on this loopback-only fixture listener.
|
|
That matches a trusted local MTA-to-store LMTP hop and must not be copied to a
|
|
network-exposed Cyrus listener.
|
|
|
|
The generated short-lived test CA at
|
|
`/tmp/bongo-cyrus-fixture/tls/ca.crt` is the fixture trust anchor. Set
|
|
`BONGO_COLLECTOR_CAINFO` to that path for the Collector process during this
|
|
test; normal installations continue to use the operating-system trust store.
|
|
The packaged `bongo.service` uses `PrivateTmp=true`, so a live systemd test
|
|
must make the fixture visible inside that service namespace. A runtime-only
|
|
drop-in can bind precisely this fixture read-only:
|
|
|
|
```ini
|
|
[Service]
|
|
BindReadOnlyPaths=/tmp/bongo-cyrus-fixture
|
|
```
|
|
|
|
Install it with `systemctl edit --runtime bongo.service`, restart Bongo, and
|
|
set the Collector `ca_file` to the path above. Do not weaken certificate or
|
|
host-name verification for the test.
|
|
|
|
After a live collection cycle, show the source-mailbox counts with:
|
|
|
|
```sh
|
|
./contrib/testing/cyrus-fixture.py mailbox-status
|
|
```
|
|
|
|
Stop or remove all fixture state with:
|
|
|
|
```sh
|
|
sudo ./contrib/testing/cyrus-fixture.py stop
|
|
sudo ./contrib/testing/cyrus-fixture.py reset
|
|
```
|
|
|
|
## Outbound SMTP capture
|
|
|
|
Use `net-mail/xeams-devnullsmtp-bin` from the Bongo Gentoo overlay:
|
|
|
|
```sh
|
|
xeams-devnullsmtp -p 2526 -d bongo.test -s /tmp/bongo-devnull-mail
|
|
```
|
|
|
|
Configure `127.0.0.1:2526` as Bongo's relay host before testing any non-local
|
|
recipient. Xeams DevNullSMTP has no listen-address option, so isolate it with a
|
|
firewall or network namespace; do not expose it on a production network.
|
|
|
|
## HAProxy and PROXY v2
|
|
|
|
`haproxy-bongo-test.cfg` exposes loopback-only high ports and forwards every
|
|
connection to the corresponding normal Bongo listener with a PROXY v2 header.
|
|
Before starting it, temporarily enable `proxy_protocol_enabled` and set
|
|
`proxy_protocol_networks` to `["127.0.0.1/32"]` in the `smtp`, `imap`, `pop3`,
|
|
and `sieve` documents. Never trust an unbounded network for PROXY headers.
|
|
|
|
Validate and run the fixture in the foreground:
|
|
|
|
```sh
|
|
haproxy -c -f contrib/testing/haproxy-bongo-test.cfg
|
|
haproxy -db -f contrib/testing/haproxy-bongo-test.cfg
|
|
```
|
|
|
|
The test listeners are SMTP `11025`, submission `11587`, submissions `11465`,
|
|
IMAP `11143`, IMAPS `11993`, POP3 `11110`, POP3S `11995`, and ManageSieve
|
|
`14190`. Exercise real protocol greetings, capabilities, authentication, and
|
|
TLS handshakes through these ports, then restore the original Bongo documents.
|