14 Commits
Author SHA1 Message Date
lukasandClaude Opus 5.5 f82c7eb444 Loading splash: animated logo over a dark overlay on desktop
index.html carries the overlay markup and styles inline so it paints before
any script; public/splash.js (external, for the CSP) plays the logo
animation from icons/ultimo-splash.html, holds, fades out after ~3.25 s and
removes itself. Skipped at phone sizes (same query as useIsMobile) and in
pop-out compose windows; a click skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:23:56 +01:00
lukasandClaude Opus 5.5 05db376b89 Updated logo, carried to every derived icon
New ultimo-logo and signet SVGs, re-rendered into the PWA icons (any and
maskable), apple-touch icon, favicon, the public and calendar-plugin
signet copies, the BIMI Tiny-PS logo and the system-email logo PNG, each
at its previous size, placement and background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:13:15 +01:00
lukasandClaude Opus 5.5 1aa0673720 Phone reading pane: icon-only top bar, roomier header; dark app icon
- Top bar: icons only (names kept for screen readers and as tooltips),
  20% larger, twice the room between them; Archive 2 px smaller
- Subject wraps to two lines; subject-bar icons twice as far apart
- Reply / Translate / Actions on their own row with more room around
  it; the ▼ for the full header can no longer be squeezed out, and the
  full header spans the card's width
- Maskable app icons on the app's dark grey, so Android's launch screen
  shows the logo without a white circle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 07:59:56 +02:00
lukasandClaude Opus 5.5 972e10876b Translation on Firefox Translations models; new logo
- Email translation moves from LibreTranslate to our own translator
  service (translator/): Mozilla's Firefox Translations models on the
  Bergamot engine, fastText language detection (176 languages). Polish,
  English, German, French, Spanish and Italian on first start (~350 MB
  instead of ~10 GB); Admin panel > Email translation installs and
  removes languages. Built and pushed as the `translator` image.
- Every message is translated paragraph by paragraph as plain text;
  names travel with their sentence and are kept only where the model
  changed them; a sentence start the model copied ("Enclosed") is tried
  again in lower case; list markers are not sent
- The Translate button shows only for installed languages
- New logo: favicon, app/maskable/iPhone icons, notification badge,
  email logo and calendar copy regenerated; BIMI logo rebuilt in SVG
  Tiny PS with its gradients; masks render in browsers
- Launch screen background is the app's dark grey

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:04:04 +02:00
lukasandClaude Opus 5.5 fa552392c8 Calendar reminders as push notifications
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>
2026-10-09 21:49:04 +02:00
lukasandClaude Opus 5.5 9c08cbb140 Offline: newest 100 conversations; notifications no longer wait silently
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>
2026-10-08 20:20:21 +02:00
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
lukasandClaude Opus 5.5 fba3c947bf Attachment previews, forwarded attachments, row dividers
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>
2026-10-06 20:15:17 +02:00
lukasandClaude Opus 5.5 c9c24e7e2f Offline access: self-updating worker, quiet console, setting in Accounts
- 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>
2026-10-05 20:45:37 +02:00
lukasandClaude Opus 5.5 01bfbb37d4 Offline access: save through the service worker, not the page's fetch
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>
2026-10-05 19:27:36 +02:00
lukasandClaude Opus 5.5 a2c52f33a8 Offline access: read recent mail without a connection
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>
2026-10-05 18:54:17 +02:00
lukasandClaude Opus 5.5 d1f6d183e0 Serve the BIMI logo, favicon.ico and an apple-touch icon as real files
ultimo.email is the webmail itself, so any path that was not a real file fell
back to the app's index.html: /bimi/ultimo-logo.svg (the BIMI record's logo)
and /favicon.ico both came back as the webmail page. They now ship in
frontend/public, which nginx serves before the SPA fallback. The BIMI SVG
moves there from frontend/icons, so there is one copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:23:33 +02:00
lukasandClaude Opus 4.8 4c200ab94a Gmail label sync + no-dup backfill, live label/UI updates, account-delete cleanup, Sync-SPAM toggle, sidebar logo
Gmail sync (SPEC: Gmail sync):
- Provider fetches X-GM-LABELS when X-GM-EXT-1 is advertised; parses custom labels
  (system labels dropped, mUTF-7 decoded) onto RemoteMessage.
- Engine syncs only system folders + All Mail for Gmail, skips every label folder,
  and dedupes the All Mail pass by Message-ID — kills the INBOX/All-Mail duplication.
- Custom labels ride the message.new event; the labels plugin subscribes and maps
  them to chips as "<name> [Gmail]" (per-message + thread aggregate + known set),
  so labels fill in gradually with the sync window. Non-ASCII labels decode right.

Labels self-healing (message-driven reconcile):
- Per-message keys are the source of truth; t: aggregates and _labels are derived.
  Reconcile drops labels of deleted messages, regroups survivors by their current
  thread (fixing JWZ thread merges), rewrites changed t: keys, and rebuilds _labels
  from live messages — keeping user labels, dropping orphaned Gmail ones.
- New SDK reads: sdk.threads.existing / sdk.messages.threads_of. Sidebar refreshes
  live via a plugin.labels.changed WS event on new labels.

Account deletion: prune threads left empty after the cascade (they were user-scoped,
so only messages cascaded) + de-list the account from surviving cross-account threads.

Sync SPAM: opt-in per-account sync_spam (migration 0015, default off) — the engine
skips the Junk folder in backfill and poll unless enabled; wizard checkbox added.

Robustness / UX:
- WebSocket auto-reconnects with backoff and resyncs on (re)connect, and account
  add/delete invalidates threads/labels — no more hard refresh for live updates.
- Backfill banner + sidebar ring also require done < total, so a completed account
  can't leave a stuck "X of X".
- Scrub lone UTF-16 surrogates from parsed headers/bodies so bad messages no longer
  crash the backfill on Postgres text/JSONB.
- Ultimo signet logo in the Mail/Calendar/Contacts sidebar header (and at the top of
  the collapsed rail), via the shared plugin-ui.css + MailLayout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
2026-08-05 18:04:49 +02:00
lukasandClaude Opus 4.8 0a478a6b26 Labels in Mail UI (chips + Actions + sidebar), plain-text Lato, calendar-panel polish
Labels:
- Moved labelling out of the "+label" message slot into Mail: label chips on the
  thread subject bar (removable ✕) and before list subjects, and a Labels submenu
  in the per-message Actions menu (multi-select checkboxes + "Create new…").
  Per-message storage kept; the reading pane writes a thread-union aggregate so the
  list (no message ids) can show chips.
- Sidebar LABELS section rewritten: aligned monocolor tag rows, collapsible
  (collapsed by default), inline "+" create popover, hover "−" delete with a confirm
  popover, and click-to-filter the mail list by label.
- Backend: batch message/thread label reads, thread aggregate, create + delete
  (delete unhooks from all mail), and GET /labels/{name}/threads. Core GET /threads
  gained an `ids` param to power the label view. Removed the plugin-pages list from
  the Mail sidebar.

Plain-text mail now renders in Lato (self-hosted at /fonts, @font-face'd into the
sandboxed message iframe) instead of the monospace <pre> default.

Calendar right-panel (embed mode): restored the "…" view-options menu, pinned the
toolbar + mini-calendar (only the agenda scrolls), plain "All calendars" button
(inline resets), and the event popover now closes on any outside click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
2026-08-03 20:53:35 +02:00