Add outbound relay (smarthost) admin UI#4
Open
cyberb wants to merge 5 commits into
Open
Conversation
Self-hosted mail sent directly from home/residential IPs is widely rejected by Gmail/Outlook (IP reputation, Spamhaus PBL) and many ISPs block port 25, so direct delivery fails regardless of SPF/DKIM/DMARC. The industry-standard fix, as shipped by mailcow/mailu/docker-mailserver, is to route outbound mail through an authenticated relay (smarthost). Productizes the manual workaround from platform issue #553 so it also survives upgrades: - Persist relay settings (enabled/host/port/user/password) in the [relay] section of user_mail.cfg under $SNAP_COMMON. - Postfix main.cf template renders a relayhost + SASL block when relay is enabled; RegenerateConfigs writes and postmaps sasl_passwd, so the relay is reapplied on every install/refresh. - New webui daemon (cmd/webui, rest.Server) serves GET/POST /api/relay over a unix socket, reusing the installer to apply changes and restart postfix via the platform. - nginx serves a Vue 3 + Element Plus admin page at /admin/ and proxies /api/ to the webui socket, alongside Roundcube webmail (mailcow-style split of webmail vs server admin). - Playwright spec covers the relay form end to end. Also bump Roundcube 1.6.1 -> 1.6.15 (LTS security releases).
$SNAP_DATA is snapshotted and rolled back transactionally on a failed refresh; $SNAP_COMMON is shared across revisions and never rolls back. The app kept all mutable state (Postgres DB, DKIM keys, maildirs, spool, relay credentials, generated configs) in $SNAP_COMMON, so a rolled-back binary could face newer-format data. Move it all to $SNAP_DATA, keeping only web.socket in $SNAP_COMMON (the Syncloud platform routes app traffic to that hardcoded path). - installer data root common -> current; add AppCommonDir so nginx still binds web.socket in $SNAP_COMMON. - MigrateCommonToData() renames every $SNAP_COMMON entry (except sockets) into $SNAP_DATA once, guarded by a marker; runs at the top of InitConfig so both install and post-refresh migrate existing devices. - Service scripts use $SNAP_DATA (except the web.socket line); php-fpm.conf pid/listen paths templated via AppDataDir.
After moving app state to $SNAP_DATA, InitConfig only chowned the data dir, leaving $SNAP_COMMON root-owned. The nginx service runs as mail and must create web.socket under $SNAP_COMMON (platform contract), so bind failed with EACCES and nginx crash-looped (platform served 503). Ensure $SNAP_COMMON exists and is owned by mail, as it was before the migration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Self-hosted mail sent directly from home/residential IPs is widely rejected by Gmail/Outlook — IP reputation, Spamhaus PBL, and (Nov 2025) hard 5.7.26 rejections — and many ISPs block port 25 outright (platform issue #567). SPF/DKIM/DMARC/PTR are necessary but not sufficient; a clean/new/residential IP still gets throttled or blocked.
The industry-standard fix — shipped by mailcow, mailu, docker-mailserver — is to route outbound mail through an authenticated relay (smarthost). This productizes the manual
relayhost+sasl_passwdworkaround from platform issue #553 and makes it survive upgrades.What
1. Outbound relay (smarthost) + admin UI
enabled/host/port/user/password) in a[relay]section ofuser_mail.cfg.main.cfrenders arelayhost+ SASL block when enabled;RegenerateConfigswrites andpostmapssasl_passwd, re-applied on every install/refresh.webuidaemon (cmd/webui,rest.Server) servingGET/POST /api/relayover a unix socket, reusing the installer and restarting Postfix via the platform. Runs asmail; blank password = keep current./admin/; nginx proxies/api/to the webui socket, alongside Roundcube. Mirrors the mailcow/mailu split of webmail vs server admin (Roundcube is a webmail client and has no concept of MTA relay).data-testid).1.6.1 → 1.6.15(LTS security; stays on PHP 8.0).2. Migrate app state
$SNAP_COMMON→$SNAP_DATAThe app kept all mutable state (Postgres DB, DKIM keys, maildirs, spool, relay creds, generated configs) in
$SNAP_COMMON, which is shared across revisions and never rolls back — so a rolled-back binary could hit newer-format data. Moved it all to$SNAP_DATA(snapshotted + transactional rollback), keeping onlyweb.socketin$SNAP_COMMON(platform contract).common → current; newAppCommonDirvar so nginx still bindsweb.socketin$SNAP_COMMON.MigrateCommonToData()renames every$SNAP_COMMONentry (except sockets) into$SNAP_DATAonce, guarded by a marker; runs at the top ofInitConfig, so existing devices migrate on post-refresh (while services are stopped,os.Renameon the same filesystem).$SNAP_DATA(except theweb.socketline);php-fpm.confpid/listen paths templated.$SNAP_DATA. Reverting to a pre-migration revision would leave the old binary looking in$SNAP_COMMON(now empty of app data) — asnap revertacross this boundary is lossy, as expected when relocating data.Verification
go build ./... && go vet ./... && go test ./...clean;gofmtclean./admin/);.drone.jsonnetregenerates; adds awwwbuild step (node 20) →build/snap/www.commonpaths remain except theweb.socketline.Not yet exercised on a real device — CI
test/e2e/test-upgradewill validate the full snap + migration path.Closes syncloud/platform#553 · relates to syncloud/platform#567