From a7a5b155e3583b37df3837871c76efd73f38232e Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:59:50 -0400 Subject: [PATCH] Roundcube as its own image built by the workflow (no config file or newer Compose needed) - roundcube/: official Roundcube plus hub.php settings - workflow builds and pushes git.necloud.us/nate/hub-roundcube, and runs the player and webmail tests - compose uses the image instead of an inline config Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/build.yml | 20 +++++++++++++++ README.md | 4 +-- docker-compose.yml | 50 +++----------------------------------- roundcube/Dockerfile | 7 ++++++ roundcube/hub.php | 35 ++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 roundcube/Dockerfile create mode 100644 roundcube/hub.php diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 7647ddf..ce4236f 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -20,6 +20,8 @@ jobs: - run: node tools/smoke-test.js - run: node tools/users-test.js # accounts: private data, tab limits, invites, migration - run: node tools/plex-test.js # Plex sign-in stays signed in and repairs itself + - run: node tools/player-test.js # music player: queue, shuffle/repeat, stall recovery + - run: node tools/webmail-test.js # the /webmail/ pass-through to Roundcube - uses: docker/setup-buildx-action@v3 @@ -50,3 +52,21 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=git.necloud.us/nate/hub:buildcache cache-to: type=registry,ref=git.necloud.us/nate/hub:buildcache,mode=max + + # The webmail container (official Roundcube + the hub's settings) + - id: meta_rc + uses: docker/metadata-action@v5 + with: + images: git.necloud.us/nate/hub-roundcube + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,format=short + type=ref,event=tag + + - uses: docker/build-push-action@v6 + with: + context: ./roundcube + platforms: linux/amd64 + push: true + tags: ${{ steps.meta_rc.outputs.tags }} + labels: ${{ steps.meta_rc.outputs.labels }} diff --git a/README.md b/README.md index a529be1..6ce415a 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ e.g. hub.necloud.us. Settings: `SEAFILE_URL`, `ONLYOFFICE_URL` env vars in docke authenticated SMTP must be allowed for the mailbox. Then put `M365_CLIENT_ID`, `M365_CLIENT_SECRET`, `M365_TENANT` (the tenant ID) and `PUBLIC_URL` (your hub address, used for the redirect) in `.env` and `docker compose up -d`. The Roundcube login then shows **Login with Microsoft 365**. Microsoft's sign-in page cannot be shown inside a frame, so that one step opens in the whole window and returns you to the Mail tab. Personal Outlook.com accounts are not covered. - Roundcube's data (settings, address book, its encryption key) is in the `nehub-roundcube` volume. To change the presets, edit the `roundcube_hub` config in - `docker-compose.yml` (needs Docker Compose 2.23.1 or newer). To use a webmail you run elsewhere instead, set `WEBMAIL_URL` (it must allow being framed: + Roundcube's data (settings, address book, its encryption key) is in the `nehub-roundcube` volume. The webmail is its own image (`git.necloud.us/nate/hub-roundcube`, built by the Gitea workflow from `roundcube/`: the official Roundcube plus + `roundcube/hub.php`), so any compose tool can run it. To change the presets, edit `roundcube/hub.php`. To use a webmail you run elsewhere instead, set `WEBMAIL_URL` (it must allow being framed: no `X-Frame-Options`, and `Content-Security-Policy: frame-ancestors https://`). With neither, Mail is just the shortcut cards. roundcube: diff --git a/docker-compose.yml b/docker-compose.yml index f673934..581152d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,64 +33,20 @@ services: - hub-data:/data # passkeys, calendar links and the session secret live here # Webmail shown inside the Mail tab. Only the hub can reach it (no published port), so it sits behind the passkey sign-in. - # Sign in to it with your mail address and an app password (Gmail, iCloud and Yahoo are preset in the config below). + # Sign in to it with your mail address and an app password (Gmail, iCloud and Yahoo are preset in roundcube/hub.php). roundcube: - image: roundcube/roundcubemail:latest-apache + image: git.necloud.us/nate/hub-roundcube:latest # built by the Gitea workflow from ./roundcube; `docker compose build` builds locally instead + build: ./roundcube restart: unless-stopped environment: - ROUNDCUBEMAIL_DB_TYPE: sqlite - ROUNDCUBEMAIL_REQUEST_PATH: /webmail/ - ROUNDCUBEMAIL_PLUGINS: archive,zipdownload - ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE: 30M # Microsoft 365 sign-in (optional, see README -> Mail). Leave empty to skip. M365_CLIENT_ID: ${M365_CLIENT_ID:-} M365_CLIENT_SECRET: ${M365_CLIENT_SECRET:-} M365_TENANT: ${M365_TENANT:-} PUBLIC_URL: ${PUBLIC_URL:-} - configs: - - source: roundcube_hub - target: /var/roundcube/config/hub.php volumes: - roundcube-data:/var/roundcube/db # address book, settings and the encryption key -configs: - roundcube_hub: - content: | - 'Gmail', - 'ssl://imap.mail.me.com:993' => 'iCloud Mail', - 'ssl://imap.mail.yahoo.com:993' => 'Yahoo Mail', - ]; - // Microsoft 365 through OAuth 2.0 (needs an app registration, see README). It goes first because that is where OAuth sign-ins are sent. - if (getenv('M365_CLIENT_ID')) { - $$hosts = ['ssl://outlook.office365.com:993' => 'Microsoft 365 (use the Microsoft button)'] + $$hosts; - $$tenant = getenv('M365_TENANT') ?: 'organizations'; - $$config['oauth_provider'] = 'generic'; - $$config['oauth_provider_name'] = 'Microsoft 365'; - $$config['oauth_client_id'] = getenv('M365_CLIENT_ID'); - $$config['oauth_client_secret'] = getenv('M365_CLIENT_SECRET'); - $$config['oauth_auth_uri'] = "https://login.microsoftonline.com/$${tenant}/oauth2/v2.0/authorize"; - $$config['oauth_token_uri'] = "https://login.microsoftonline.com/$${tenant}/oauth2/v2.0/token"; - $$config['oauth_identity_uri'] = 'https://graph.microsoft.com/oidc/userinfo'; - $$config['oauth_identity_fields'] = ['email', 'preferred_username']; - $$config['oauth_scope'] = 'email openid profile offline_access https://outlook.office365.com/IMAP.AccessAsUser.All https://outlook.office365.com/SMTP.Send'; - $$config['oauth_login_redirect'] = false; - if (getenv('PUBLIC_URL')) $$config['oauth_redirect_uri'] = rtrim(getenv('PUBLIC_URL'), '/') . '/webmail/index.php/login/oauth'; - } - $$config['imap_host'] = $$hosts; - $$config['smtp_host'] = 'tls://smtp.%z:587'; // smtp.gmail.com, smtp.mail.me.com, smtp.mail.yahoo.com - $$config['session_lifetime'] = 43200; // minutes (30 days); the hub's own sign-in is what protects access - // Behind the hub: believe its forwarded address and https. - if (($$_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') $$_SERVER['HTTPS'] = 'on'; - if (!empty($$_SERVER['HTTP_X_FORWARDED_HOST'])) $$_SERVER['HTTP_HOST'] = $$_SERVER['HTTP_X_FORWARDED_HOST']; - // A fixed encryption key kept with the data, so saved settings and sessions survive restarts and updates. - $$kf = '/var/roundcube/db/des_key'; - if (!is_file($$kf)) @file_put_contents($$kf, bin2hex(random_bytes(12))); - if (is_file($$kf)) $$config['des_key'] = trim(file_get_contents($$kf)); volumes: hub-data: name: nehub-data # fixed name so the data survives redeploys and renamed stacks diff --git a/roundcube/Dockerfile b/roundcube/Dockerfile new file mode 100644 index 0000000..05ec62a --- /dev/null +++ b/roundcube/Dockerfile @@ -0,0 +1,7 @@ +# Roundcube webmail for NE Hub: the official image plus the hub's settings (so no config file has to be mounted). +FROM roundcube/roundcubemail:latest-apache +COPY hub.php /var/roundcube/config/hub.php +ENV ROUNDCUBEMAIL_DB_TYPE=sqlite \ + ROUNDCUBEMAIL_REQUEST_PATH=/webmail/ \ + ROUNDCUBEMAIL_PLUGINS=archive,zipdownload \ + ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE=30M diff --git a/roundcube/hub.php b/roundcube/hub.php new file mode 100644 index 0000000..dab7034 --- /dev/null +++ b/roundcube/hub.php @@ -0,0 +1,35 @@ + 'Gmail', + 'ssl://imap.mail.me.com:993' => 'iCloud Mail', + 'ssl://imap.mail.yahoo.com:993' => 'Yahoo Mail', +]; +// Microsoft 365 through OAuth 2.0 (needs an app registration, see README). It goes first because that is where OAuth sign-ins are sent. +if (getenv('M365_CLIENT_ID')) { + $hosts = ['ssl://outlook.office365.com:993' => 'Microsoft 365 (use the Microsoft button)'] + $hosts; + $tenant = getenv('M365_TENANT') ?: 'organizations'; + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = 'Microsoft 365'; + $config['oauth_client_id'] = getenv('M365_CLIENT_ID'); + $config['oauth_client_secret'] = getenv('M365_CLIENT_SECRET'); + $config['oauth_auth_uri'] = "https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize"; + $config['oauth_token_uri'] = "https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token"; + $config['oauth_identity_uri'] = 'https://graph.microsoft.com/oidc/userinfo'; + $config['oauth_identity_fields'] = ['email', 'preferred_username']; + $config['oauth_scope'] = 'email openid profile offline_access https://outlook.office365.com/IMAP.AccessAsUser.All https://outlook.office365.com/SMTP.Send'; + $config['oauth_login_redirect'] = false; + if (getenv('PUBLIC_URL')) $config['oauth_redirect_uri'] = rtrim(getenv('PUBLIC_URL'), '/') . '/webmail/index.php/login/oauth'; +} +$config['imap_host'] = $hosts; +$config['smtp_host'] = 'tls://smtp.%z:587'; // smtp.gmail.com, smtp.mail.me.com, smtp.mail.yahoo.com +$config['session_lifetime'] = 43200; // minutes (30 days); the hub's own sign-in is what protects access +// Behind the hub: believe its forwarded address and https. +if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') $_SERVER['HTTPS'] = 'on'; +if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST']; +// A fixed encryption key kept with the data, so saved settings and sessions survive restarts and updates. +$kf = '/var/roundcube/db/des_key'; +if (!is_file($kf)) @file_put_contents($kf, bin2hex(random_bytes(12))); +if (is_file($kf)) $config['des_key'] = trim(file_get_contents($kf));