Skip to content

Add outbound relay (smarthost) admin UI#4

Open
cyberb wants to merge 9 commits into
masterfrom
relay-outbound-admin
Open

Add outbound relay (smarthost) admin UI#4
cyberb wants to merge 9 commits into
masterfrom
relay-outbound-admin

Conversation

@cyberb

@cyberb cyberb commented Jul 15, 2026

Copy link
Copy Markdown
Member

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

  • Relay settings (enabled/host/port/user/password) persist in the [relay] section of user_mail.cfg.
  • main.cf renders a relayhost + SASL block when enabled; RegenerateConfigs writes and postmaps sasl_passwd, re-applied on every install/refresh.
  • 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. Runs as root (like the hooks); socket is 0666 so nginx can proxy it. Blank password on save = keep current.
  • Vue 3 + Element Plus admin page at /admin/; nginx proxies /api/ to the webui socket, alongside Roundcube. Mailcow/mailu-style split of webmail vs server admin.
  • Admin button in the Roundcube Elastic taskbar (mailadmin plugin) linking to /admin/, with a gear icon via CSS mask to match the skin.
  • Roundcube bumped 1.6.1 → 1.6.15 (LTS security).

2. Migrate app state $SNAP_COMMON$SNAP_DATA

The 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 only web.socket in $SNAP_COMMON (platform contract).

  • Data root common → current; new AppCommonDir var so nginx still binds web.socket in $SNAP_COMMON, which stays mail-owned so nginx can create the socket.
  • MigrateCommonToData() renames every $SNAP_COMMON entry (except sockets) into $SNAP_DATA once, guarded by a marker; runs at the top of InitConfig (install + post-refresh).
  • Service scripts use $SNAP_DATA (except the web.socket line); php-fpm.conf paths templated.
  • The DB's postgresql.conf (which bakes unix_socket_directories) is refreshed from the template on every InitConfig, so upgrades repoint it from common to current.

⚠️ One-way migration: reverting to a pre-migration revision is lossy (old binary would look in $SNAP_COMMON).

Validation (CI build #364, amd64 — all green)

test bookworm/buster (device), e2e (Roundcube → Admin button → /admin/ → fill relay → Save → "Saved"), e2e-mobile, and test-upgrade (published-stable → this build, live migration) all pass.

Closes syncloud/platform#553 · relates to syncloud/platform#567

cyberb added 9 commits July 15, 2026 15:31
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[mail] implement ISP relay (indirect) delivery feature

1 participant