A job every minute finds the event reminders that fell due — repeating events
and moved occurrences expanded as the calendar view does — and pushes each
once to every device with notifications on (sdk.push, new: plugins can notify
through the new-mail push delivery). A reminder missed while the worker was down
is still sent within 10 minutes; cancelled events and declined invitations stay
quiet. Title, then "In 15 minutes, 14:00 · Room 2" in the user's zone and clock
format; it stays until dismissed and shows even with the app open; clicking it
opens that day (/p/calendar?date=…, new deep link).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offline access keeps the newest 100 Inbox conversations (the list's first two
pages, saved as the app asks for them) plus recently opened ones, up to 130.
Turning notifications on in a browser that holds the permission request
without showing it (Opera, Chrome's quiet prompts) said nothing and never
finished. After a few seconds it now says how to allow notifications for the
site, and carries on by itself as soon as they are allowed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Attachments in the reading pane are tiles with a preview: images and a PDF's
first page, rendered on the server (Pillow, pypdfium2) as WebP of at most
480 px, cached beside the blob store and in the browser. Tiles are all one
size — at most 230 px wide, narrower so three still fit in a row — and frame
each preview by its shape: close to the tile's it fills it, very tall / wide /
square ones show whole over a blurred copy, small ones are not enlarged, PDFs
show the top of the page. The name bar (checkbox, type, name, size, download)
slides in on hover and adapts to the tile's width; files without a preview
get a large type icon. Previews of opened conversations are kept offline.
Forward now carries the original's attachments: new compose attachments
sharing the stored blobs (POST /messages/{id}/forward-attachments).
Mailbox display / Settings → Appearance: "Show a dividing line between
rows" (off by default, per device), with a new switch. Settings drop the
"On a phone…" note, and "Language" reads "Spellchecking language".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The page asks the service worker for its version before using it, and
has the browser fetch a newer one when it is out of date; the app also
checks for a new sw.js on every start. An older worker left over from a
previous release silently ignored the page's requests, so saving only
resumed after offline access was switched off and on.
- Fewer console errors offline: the server probe goes through the worker;
fonts, the illustration, lazy chunks and the PDF worker (named inside
the app's scripts and styles) are saved with the app; sender pictures
are pre-loaded, and an unsaved one is "no picture" (204), not a 503;
plugin bundles and plugin lookups are saved (per-message ones go with
their message); POST lookups and live-update reconnects wait for the
server to be back instead of failing over and over.
- The setting moves from Settings → Security to Settings → Accounts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pre-loading relied on the page being controlled by the service worker, so
its requests would pass through it and be saved. A page loaded with a hard
reload (Ctrl+Shift+R) never is: pre-loading then waited, gave up silently,
and nothing new was saved — the app still could not start offline, and
Settings kept showing the old sync time.
The page now asks the worker by message to fetch and save each URL, which
works however the page was loaded. Sign-out reaches the worker through its
registration for the same reason. Thread URLs come from shared helpers in
api.ts, so pre-loading saves exactly what the app asks for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An opt-in per device (Settings → Security → Offline access) installs a
service worker that keeps the newest 50 Inbox conversations, and the ones
opened recently, for reading when the server cannot be reached. While
online everything still comes from the server; the saved copy is only the
fallback, marked X-WM-Offline so the app knows it is offline.
Offline, an amber cloud next to the theme toggle says so and when mail was
last saved; actions that need the server are greyed out and say "Not
available offline"; conversations and folders not saved here are faded or
say so. A probe notices the server coming back, shows "Back online" and
refreshes everything. Saved mail is wiped on sign-out, on any 401, and when
another user signs in. nginx never caches /sw.js, so releases reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>