Porting Mail-in-a-Box v76 to Ubuntu 24.04
How we got from upstream Mail-in-a-Box v76 to a working custom fork on Ubuntu 24.04 LTS (noble) called MeetrMail.
Live and Operationalv76-base in the fork)
noble-php83-py312-rspamd
1. Why v76 does not install on 24.04
Upstream v76 supports Ubuntu 22.04 only. Running it on 24.04 fails in several independent places, any one of which stops the install:
| Blocker | Where | What happens on 24.04 |
|---|---|---|
| OS version gate | setup/preflight.sh |
Refuses anything but 22.04 |
| PHP 8.0 from ppa:ondrej/php | setup/system.sh, setup/functions.sh |
Third-party PPA for an EOL PHP; 24.04 ships 8.3 |
| PEP 668 | setup/questions.sh, setup/management.sh |
noble's pip 24.0 refuses pip3 install into the system Python: error: externally-managed-environment |
cryptography==37.0.2 |
setup/management.sh |
No wheel for Python 3.12; pip falls back to a source build needing an old Rust toolchain |
| SpamAssassin 4.0 | setup/spamassassin.sh |
Major version jump; /etc/default/spamassassin no longer exists; spampd's embedding of SpamAssassin untested against 4.0 |
| Hard-coded php8.0 paths | nginx config, backup code, tools | Every PHP page returns an error; backups abort |
systemctl link |
setup/management.sh, setup/munin.sh |
systemd 255 refuses to enable the unit — management daemon never starts at boot |
2. The approach
The goal was the smallest diff from v76 that runs correctly on 24.04 — not a rewrite. The plan was worked in phases:
| Phase | Work |
|---|---|
| 1 | Preflight gate, trivial package fixes, remove PPAs |
| 2 | Python daemon on 3.12 via uv |
| 3 | PHP 8.3, remove Z-Push |
| 4 | Fresh Nextcloud 33 install |
| 5a | Rspamd for spam + greylisting (OpenDKIM/OpenDMARC kept) |
| — | Sandbox mode, test harness, and fixes found by testing |
Two decisions shaped everything else:
OpenDKIM and OpenDMARC stayed exactly as v76 had them. Rspamd can do DKIM signing, but moving it means rewriting key generation in management/dns_update.py and validation in management/status_checks.py. DKIM decides whether Gmail accepts your mail, so that change was deferred to a later, unhurried "Phase 5b". The successful Gmail/Outlook tests confirm this held up.
Every change was tested by actually installing it. A static read of the code found the obvious problems. A real install in an Ubuntu 24.04 container found eight more that would have stopped or silently broken a live box.
3. Operating system and packages
Preflight
setup/preflight.sh and setup/bootstrap.sh now accept exactly Ubuntu 24.04 — an exact match, not "24.04 or newer". A box should refuse to install on an untested release rather than half-work.
Third-party apt repositories
| Repository | v76 | Fork | Why |
|---|---|---|---|
ppa:ondrej/php |
Yes (PHP 8.0) | Removed | noble ships PHP 8.3 with every module needed |
ppa:duplicity-team/duplicity-release-git |
Yes | Removed | noble ships duplicity 2.1.4 in main |
| Rspamd project repo | No | Added | Needed to replace SpamAssassin smoothly |
The Rspamd repository is the only third-party source left. Its signing key is pinned by fingerprint and apt-pinned so it can supply only the rspamd package. Setting RSPAMD_PACKAGE_SOURCE=ubuntu uses Ubuntu's package instead and leaves no third-party repositories at all. The choice is saved in /etc/mailinabox.conf so a later re-run of setup can't silently switch.
Individual package changes
| Package | Change | Reason |
|---|---|---|
ntp |
Removed | On noble it's a transitional package pulling in ntpsec, which fights systemd-timesyncd. MIAB never configured it. |
libmagic1 |
→ libmagic1t64 | Renamed in noble's 64-bit time_t transition. The old name still resolves, but naming the real package is safer. |
bind9-host, bind9-dnsutils |
Added to base packages | v76 only installed host from the network checks, so a box that skipped them had no DNS tools. |
python3 |
Installed at start | tools/editconf.py and setup/migrate.py use python3 and run before anything else installs it. |
Versions verified against the noble archive
Every package name was checked against the real noble package indexes before any code was changed. Highlights include Postfix jumping to 3.8.6, Dovecot to 2.3.21, and nsd to 4.8.0.
nsd 4.8: nsd jumped five minor versions. setup/dns.sh now runs nsd-checkconf on the generated config straight after install, so an unsupported directive fails immediately with a precise message rather than as a mysterious service failure later.
4. PHP: what broke and what we changed
The version change
PHP_VER=8.0 → PHP_VER=8.3 in setup/functions.sh. All required modules exist in noble's stock archive. Roundcube 1.6.15 supports PHP 8.3 without changes.
Hard-coded PHP 8.0 paths — the part that would have hurt
Changing the variable was not enough. v76 had 8.0 written directly into files that never read the variable, causing every PHP page to fail and backups to abort. Setup now records PHP_VERSION=8.3 in /etc/mailinabox.conf. A new helper reads it, and the nginx template uses a placeholder that is filled in when the web config is generated.
5. Python: what broke and what we changed
Problem 1 — PEP 668 & Problem 2 — the cryptography pin
Ubuntu 24.04's pip enforces PEP 668, preventing installation into the system Python. Furthermore, v76 pinned cryptography==37.0.2 which has no wheel for Python 3.12, causing pip to attempt a source build.
The fix: uv
uv installs its own CPython, so the management daemon never touches the system Python at all. The installer changed from pip install --upgrade to uv sync --frozen using a lockfile (management/uv.lock) to guarantee reproducibility.
Unpinning resolved to cryptography 50.0.1. All functions were tested directly (Loading certificate chains, validity checks, CSRs, TOTP two-factor codes, QR code generation) and passed.
6. Nextcloud
Nextcloud was updated from 26.0.13 to 33.0.9 to support PHP 8.3 and the updated user_external app. The upgrade ladder was removed since a fresh install doesn't need it.
Finding: Nextcloud login no longer needs PHP's IMAP extension, it uses cURL's built-in imap support, preparing the ground well for Ubuntu 26.04 which drops ext/imap from PHP core.
7. The spam system: SpamAssassin to Rspamd
Keeping SpamAssassin looked safer but wasn't. noble ships SpamAssassin 4.0.0, and the v76 config no longer exists. Rspamd with Postfix and Dovecot is a very widely used, well-documented setup.
| v76 component | Job | Replaced by |
|---|---|---|
| spampd + spamassassin | Spam scoring | Rspamd, connected to Postfix as a milter |
| postgrey | Greylisting | Rspamd's greylist module |
| razor, pyzor | Shared spam signatures | Rspamd's fuzzy hashing and reputation |
| dovecot-antispam | Learn from Spam-folder moves | Dovecot IMAPSieve + rspamc |
| libmail-dkim-perl | DKIM checks inside SpamAssassin | Rspamd's own DKIM checks |
| — | — | redis-server (new data store) |
Rspamd checks mail while the sender is still connected, enabling immediate greylisting rejections without needing a separate daemon.
8. Z-Push / ActiveSync removed
Z-Push was removed completely rather than carried forward to PHP 8.3, including its config, nginx locations, and autodiscover subdomains. The general CalDAV/CardDAV proxy in nginx stays.
9. Bugs found only by actually installing
Each of these was found by installing the fork in a real Ubuntu 24.04 container, not by reading code.
| # | Bug | What would have happened |
|---|---|---|
| 1 | php8.0-fpm.sock hard-coded in nginx | Every PHP page fails |
| 2 | systemctl link on a unit already in /lib/systemd/system | systemd 255 refuses to enable it; management daemon never starts |
| 3 | Both classifier-bayes.conf and statistic.conf in Rspamd's local.d | Rspamd refuses to start |
| 4 | postgrey still installed by mail-postfix.sh | Two greylisting systems running; postgrey doing the work |
| 5 | Redis data-folder permissions set in the wrong order | Redis fails at next reboot; spam filtering silently degrades |
| 6 | Redis blocked from /home by its systemd lockdown | Redis won't start at all |
| 7 | fail2ban won't start if /var/log/auth.log doesn't exist yet | No brute-force protection on a fresh box |
| 8 | tr < /dev/urandom | head with set -o pipefail | Setup exits with code 141 (broken pipe) |
| 9 | boto3 missing from the daemon's environment | Admin panel returns HTTP 500 |
| 10 | sievec doesn't read plugin settings on its own | Spam-learning scripts can't be compiled |
| 11 | resolved.conf missing on minimal images | Setup crashes |
10. Security hardening found along the way
The self-test checks that internal-only services aren't reachable from the internet. Dovecot quota-status (port 12340) and munin-node (port 4949) were changed to listen on localhost only, resolving issues that existed in v76.
11. Sandbox mode and the test harness
A sandbox mode was created allowing installation of the full box on a local VM/container with no public DNS, Let's Encrypt, or outbound port 25. The test harness runs 88 distinct checks covering services, ports, config validity, DNS, TLS, and mail flow (spam headers, greylisting, training).
What is still open
Backups via duplicity need to be tested end-to-end to confirm they fully backup and restore properly with the new b2sdk/boto3 UV environment. Also deferred is moving DKIM signing and DMARC into Rspamd (Phase 5b), and testing for Ubuntu 26.04.