Files
webmail-client/docker/nginx-app.conf
T
lukasandClaude Opus 5.5 fda4cc6f16 Installable app, push notifications about new mail, Install app entry
The webmail is now a PWA: web app manifest, icons (any + maskable, a status-bar
badge), theme-coloured title bar following the chosen theme, and an app-icon
badge with the unread Inbox count. The service worker is always installed;
offline access is on only when it is registered as /sw.js?offline=1, so
turning it on or off is an update that applies at once.

Notifications (Settings → Accounts → Notifications, per device) use Web Push.
Encryption (RFC 8291) and VAPID signing (RFC 8292) are implemented in
app/push/webpush.py — pywebpush needs a newer cryptography than we pin — and
checked against the RFC's worked example. The server makes its key pair on
first use (private half encrypted with MASTER_KEY). New mail found by the live
sync in an Inbox enqueues a job 5 s later that tells the user's devices about
messages still unread there, under 2 hours old and not from the user; more
than three become one "N new messages". A subscription belongs to the session
it was made under: signed out or revoked, nothing more is sent there. Endpoints
are limited to the known push services (no SSRF). Nothing shows while the app
is in front (except on Apple, which requires it). Tested with real pushes
through Google's service in Chromium and Opera.

The mobile drawer gets "Install app": Chrome's own install dialog where the
browser offers one, else short steps for iPhone/iPad, Opera and Firefox, with
"Don't show this again". docs/nas-install.md §5.3 covers installing,
notifications, Public URL and a later Android app (TWA).

Migration 0022: push_subscriptions, app_settings.vapid_*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:52:26 +02:00

95 lines
3.3 KiB
Plaintext

# The webmail front door's shared body, included by BOTH listeners in
# nginx.conf (port 80 = public, port 81 = admin). Keeping it in one file is the
# point: a location that existed on only one listener would be a silent hole or
# a silent outage. Included at `server` scope, so everything here is server-level.
root /usr/share/nginx/html;
index index.html;
# Attachment uploads. Keep this comfortably above MAX_ATTACHMENT_MB (default
# 25) to leave room for multipart/base64 overhead.
client_max_body_size 64m;
client_body_timeout 300s;
# The NAS is doing other jobs too — don't buffer big uploads to disk twice.
proxy_request_buffering off;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/javascript application/json
image/svg+xml application/manifest+json;
# Clickjacking / sniffing basics. The app's own CSP for mail HTML is applied
# server-side in app/indexer/sanitize.py; this is only the shell document.
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy no-referrer always;
# ---- API ----------------------------------------------------------------
location /api/ {
proxy_pass http://webmail_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $fwd_proto;
proxy_set_header X-Forwarded-Host $host;
# WebSocket (/api/ws) and any long-poll: no upgrade means no live mail.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Live updates hold the connection open; don't cut them at 60 s.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
# ---- static -------------------------------------------------------------
# Vite emits content-hashed filenames into /assets, so they are immutable.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location /fonts/ {
expires 30d;
add_header Cache-Control "public";
try_files $uri =404;
}
location = /healthz {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
# The SPA fallback lives in each server block, not here: the two listeners serve
# two different apps out of the same dist (index.html vs admin.html).
# An entry document must never be cached, or a deploy won't be picked up. Each
# listener adds its own entry document's rule; this one is harmless on both
# because the public listener 404s /admin.html before it can be served.
location = /index.html {
add_header Cache-Control "no-store, must-revalidate";
}
# The offline-access service worker (frontend/public/sw.js): a fixed URL the
# browser re-checks for a new version, so it must not be cached either.
location = /sw.js {
add_header Cache-Control "no-cache";
try_files $uri =404;
}
# The installable app's manifest (frontend/public/manifest.webmanifest):
# re-checked on each load so a change of name or icons reaches installed apps.
location = /manifest.webmanifest {
default_type application/manifest+json;
types { }
add_header Cache-Control "no-cache";
try_files $uri =404;
}