Add outbound relay (smarthost) admin UI#4
Open
cyberb wants to merge 9 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.
- Element Plus forwards data-testid onto the inner <input>, so target it
directly instead of .locator('input') (which searched for an input
inside the input and timed out).
- The relay save path (ApplyRelay -> RegenerateConfigs + postmap + platform
RestartService) needs privilege; run the webui daemon as root like the
hooks that already call the platform. The socket is chmod 0666 so nginx
(mail) can still connect.
Roundcube (the default web UI at /) had no way to reach the new relay admin page. Add a small mailadmin plugin that registers an "Admin" button in the Elastic taskbar (with a gear icon via CSS mask so it matches the skin), linking to /admin/. Packaged into roundcubemail/plugins and enabled in config.inc.php. The e2e now logs into Roundcube and clicks the button to reach the relay form.
Roundcube rewrote the plugin button href '/admin/' into a skin-relative 'skins/elastic/admin/' (404). Set the correct absolute href and force navigation on click in the footer script, so the taskbar Admin button reliably opens /admin/ regardless of Roundcube's taskbar click handling.
On upgrade the stable DB's postgresql.conf (with unix_socket_directories baked to /var/snap/mail/common/database) is carried to $SNAP_DATA by the migration but still targets the now-gone common path, so postgres fails to create its lock file and the refresh aborts. Rewrite the data dir's postgresql.conf from the template on every InitConfig (extracted as Database.UpdateConfig, reused by Init), so it always matches the current data-dir location.
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, Nov-2025 hard 5.7.26 rejections) and many ISPs block port 25 (platform issue #567). SPF/DKIM/DMARC/PTR are necessary but not sufficient. The industry-standard fix — shipped by mailcow/mailu/docker-mailserver — is an authenticated relay (smarthost). This productizes the manual workaround from platform issue #553 and makes it survive upgrades.
What
1. Outbound relay (smarthost) + admin UI
enabled/host/port/user/password) persist in the[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) servesGET/POST /api/relayover a unix socket, reusing the installer to apply changes and restart postfix via the platform. Runs as root (like the hooks); socket is0666so nginx can proxy it. Blank password on save = keep current./admin/; nginx proxies/api/to the webui socket, alongside Roundcube. Mailcow/mailu-style split of webmail vs server admin.mailadminplugin) linking to/admin/, with a gear icon via CSS mask to match the skin.1.6.1 → 1.6.15(LTS security).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 never rolls back on a failed refresh. Moved it all to$SNAP_DATA, keeping onlyweb.socketin$SNAP_COMMON(platform contract).common → current; newAppCommonDirvar so nginx still bindsweb.socketin$SNAP_COMMON, which staysmail-owned so nginx can create the socket.MigrateCommonToData()renames every$SNAP_COMMONentry (except sockets) into$SNAP_DATAonce, guarded by a marker; runs at the top ofInitConfig(install + post-refresh).$SNAP_DATA(except theweb.socketline);php-fpm.confpaths templated.postgresql.conf(which bakesunix_socket_directories) is refreshed from the template on everyInitConfig, so upgrades repoint it fromcommontocurrent.$SNAP_COMMON).Validation (CI build #364, amd64 — all green)
test bookworm/buster(device),e2e(Roundcube → Admin button →/admin/→ fill relay → Save → "Saved"),e2e-mobile, andtest-upgrade(published-stable → this build, live migration) all pass.Closes syncloud/platform#553 · relates to syncloud/platform#567