Commit Graph
360 Commits
Author SHA1 Message Date
Linus Rath 2f31b435a2 Merge branch 'feat/desktop-offline' into feat/do-menu-fixes
# Conflicts:
#	components/desktop/desktop-host.tsx
2026-11-04 07:57:50 +01:00
Linus Rath 5fbc9b1f47 feat: open a mail link from another program in a compose window of its own in the desktop app 2026-11-04 07:12:06 +01:00
Linus Rath 0746679475 feat: reply with Ctrl+R, mark read with Ctrl+Q and search with Ctrl+E in the desktop app, as in Outlook 2026-11-04 07:00:38 +01:00
Linus Rath 141748e1d6 fix: show first-start notices one at a time above the message list, each with a close button
The dictionary test now reads the notice queue instead of the toasts: the
question is no longer a toast.
2026-11-04 06:29:23 +01:00
Linus Rath 5fc87eafbd Merge branch 'feat/do-menu-audit' into feat/do-menu-fixes
# Conflicts:
#	components/email/email-context-menu.tsx
#	components/mail/mail-app.tsx
2026-11-04 06:19:07 +01:00
Linus Rath 6efa589448 Merge branch 'feat/do-menu-pos' into feat/do-menu-fixes 2026-11-04 06:12:08 +01:00
Linus Rath f92b7d942d fix: grey out folder and message actions that can't work offline or with nothing unread 2026-11-04 05:32:23 +01:00
Linus Rath 4d404929e2 fix: show the message's own folders in Move to, in mail-client order, with recents and a filter
The Move lists (message menu, reader Move button and its phone panel,
selection toolbar, Move dialog) now share one model in lib/move-menu.ts.
In the desktop app the native copy of the menu lost the shared accounts'
headers and every folder's nesting, so Inbox, Sent, Spam and Trash
showed once per account with nothing to tell them apart.

- Only the message's own account at the top; other accounts a move can
  reach are submenus under their names.
- System folders first (Inbox, Sent, Archive, Spam, Trash), then the
  user's folders by name; subfolders nest, as submenus natively.
- The open folder shows greyed instead of missing.
- A Recent group, a filter field on long lists, and "Choose folder…"
  for the dialog, which has the filter too.

Tests changed: the open folder is now shown disabled rather than left
out, the own account of a shared-mailbox message is a submenu, and the
selection toolbar's filter field has its own label.
2026-11-04 05:20:14 +01:00
Linus Rath 2525c27a7d fix: group the message menus and keep app-level items out of the reader's more menu
The reader's more menu no longer offers Star while the star sits by the
subject, so the tests that expected Star in the menu and the phone panel
now expect it gone. Import mail moved to the command palette (it stays in
the folder menu and Settings); Keyboard shortcuts stay behind ?, the rail
and Help.
2026-11-04 05:19:08 +01:00
Linus Rath 551b5ce4fa fix: name the account sign out leaves in the command palette, add star, hide dead key hints 2026-11-04 05:09:09 +01:00
Linus Rath cb9a5ac810 Merge branch 'feat/do-edit-attachments' into feat/desktop-offline 2026-11-04 03:16:35 +01:00
Linus Rath 81d6948240 Merge branch 'main' of https://github.com/bulwarkmail/webmail 2026-11-04 03:08:16 +01:00
Linus Rath 97c0bd633a Merge branch 'feat/desktop-offline' into feat/do-edit-attachments
# Conflicts:
#	lib/desktop/languages.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/et/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/th/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/vi/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
2026-11-04 02:40:47 +01:00
Linus Rath d31d0af560 fix: say what an open program still holds, keep forwarded parts under their rules, and let main decide what a sent draft leaves 2026-11-04 02:36:48 +01:00
Linus Rath 71aaadef50 fix: run plugins' compose check in quick compose, keep a composer with one owner, and bring it back after a crash
The first version of quick compose skipped `onBeforeCompose`, a composer
moved back could end up open in both windows, a reply moved back under
Pro flagged the selected message, and a crashed pop-out came back blank.

- A new message in a pop-out runs `onBeforeCompose` with the same result
  as the main window: a plugin that stops it closes the window.
- The way back has two steps, so a pop-out that gave up can't leave a
  second owner. A handoff behind a plugin's gate makes it say busy.
- A Pro tab made from a composer moved back answers its own message.
- Each save writes `draft`, `account` and `owner` into the pop-out's
  address, which a reload after a crash starts from. What was typed since
  the last save, up to the 2 s autosave delay, is still lost.
- The recorder refuses keys that type a character on this keyboard, and
  what the shell refuses from a page, and says why.
2026-11-04 01:31:43 +01:00
Linus Rath 3da18cc2c0 feat: set the quick compose keys in the desktop settings and move a composer back into the main window 2026-11-03 23:25:10 +01:00
Linus Rath 7040a6a049 fix: reply and forward from the account holding the message in Pro tabs #1511 2026-11-03 22:06:21 +01:00
Linus Rath 529d334496 Merge branch 'fix/ui-bug-hunt' 2026-11-03 19:44:46 +01:00
Linus Rath deb73cc6e2 Merge branch 'feat/do-native-ui' into feat/desktop-offline 2026-11-03 18:28:49 +01:00
Linus Rath d09e400d04 fix: keep the tab bar on the mail list on a phone held sideways, hiding it only over what fills the window 2026-11-03 16:11:16 +01:00
Linus Rath f51d6ac9ac fix: let c compose from any Pro tab again except Calendar, whose c is New event 2026-11-03 15:55:06 +01:00
Linus Rath c1e0e5e340 fix: stop a Mountain View list cursor marking the row under it as read 2026-11-03 15:36:55 +01:00
Linus Rath 404d1acdc6 fix: stop an old message reopening by itself on a phone and keep a Mountain View cursor closed when the window narrows 2026-11-03 15:33:35 +01:00
Linus Rath cc9aa64eb7 fix: flag the mail a reply answers when it crosses between a Pro tab and the standard composer 2026-11-03 14:57:35 +01:00
Linus Rath fcfae58a70 fix: keep the open message when / focuses Mountain View's search field in the top bar 2026-11-03 14:47:08 +01:00
Linus Rath 0f799454e4 fix: keep text typed after a clean Mountain View composer move and ask before dropping handed-over edits 2026-11-03 14:42:41 +01:00
Linus Rath 4d244aad0a fix: stop mail keys acting on the hidden mail tab from other Pro tabs 2026-11-03 13:49:01 +01:00
Linus Rath 4f4660723e fix: close the phone and tablet drawer when something in it opens a composer 2026-11-03 13:47:44 +01:00
Linus Rath 426078e6ef Merge branch 'feat/desktop-offline' into feat/do-native-ui 2026-11-03 12:26:59 +01:00
Linus Rath 9719bc5fd1 Merge branch 'feat/do-os-integration' into feat/do-reminders
# Conflicts:
#	lib/desktop/types.ts
#	locales/ar/common.json
#	locales/ca/common.json
#	locales/cs/common.json
#	locales/da/common.json
#	locales/de/common.json
#	locales/en/common.json
#	locales/es/common.json
#	locales/et/common.json
#	locales/fa/common.json
#	locales/fr/common.json
#	locales/he/common.json
#	locales/hu/common.json
#	locales/it/common.json
#	locales/ja/common.json
#	locales/ko/common.json
#	locales/lv/common.json
#	locales/mn/common.json
#	locales/nb/common.json
#	locales/nl/common.json
#	locales/pl/common.json
#	locales/pt/common.json
#	locales/ro/common.json
#	locales/ru/common.json
#	locales/sk/common.json
#	locales/th/common.json
#	locales/tr/common.json
#	locales/uk/common.json
#	locales/vi/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
2026-11-03 11:57:45 +01:00
Linus Rath d9c9fc03f7 feat: give the desktop app a native menu bar and native right-click menus built from the webmail's own 2026-11-03 10:44:03 +01:00
Linus Rath 60eef9c1ef chore: merge the reviewed mail branch, pop-outs and desktop core into the offline drafts branch
The flush keeps one `outbox.dropOrphans` caller, the mail branch's daily
one with its 7 days, and drafts' own query says what's needed, so
`addHeldFileSource` and its test are gone. `unsentCount` counts local
drafts, and a draft a waiting send names counts once. The scope check in
`recordBlob` and the permanent-status rule moved to
`lib/store/mail/attachments.ts`, with tests.
2026-11-03 10:40:52 +01:00
Linus Rath 947743cc9e feat: connect the desktop app's tray, badge, notifications, files and settings to the webmail 2026-11-03 10:13:06 +01:00
Linus Rath 5c5d982f9d feat: save drafts and keep attachments while offline
A draft saved offline now stays on the device under a stand-in id and
goes to the server by itself when it's back. Until now every save was
refused and the composer showed "not saved".

- `lib/store/drafts`: one pending create per draft, however often it's
  saved. A draft from the server edited offline is replaced by create,
  then destroy, so a failure leaves a copy. Each create has a fixed
  Message-ID, looked up in Drafts after a lost response. A refused
  draft stays on the device with its error and can be opened. Changed
  on another device: both are kept and the user is told.
- `JMAPClient`: stand-in ids in Email/get and Email/set resolve like
  calendar's, also when answering from the device. A file attached
  offline stays readable, and is uploaded before a save that names it.
- Held files and kept mail files are stored in 1 MB chunks. Orphans are
  dropped at the end of a flush, counting what drafts need.
- `lib/store/mail/attachments.ts`: attachments, inline images and raw
  messages per scope, newest first, after everything else in a pass,
  with eviction that spares pinned mail and never sees held files.
- `mail.rawBlob` and `mail.rawStatus` for an offline export.

Changed test assertions: `outbox.heldFile` and `mail.getBlob` return
`blob` instead of `data`, so a file isn't held whole in memory. The
attachment tests ignore raw downloads, and mail-sync runs with
attachments off, because raw messages add a commit per message.
2026-11-03 08:49:19 +01:00
Linus Rath b79781cc94 fix: never lose a draft moving between windows and keep credentials off the window channel
Review of the pop-out work found ways to lose text, a wrong sign-out and
changes reaching ordinary browser tabs.

- Drafts: a closing window hands text over only to a main window holding
  its login, and asks instead when the last save failed, the browser is
  offline, a save is running or plugins could still have a say. A failed
  rescue goes to "Continue draft". The opening window lets go only once
  the composer is on screen, also for Edit draft in a message window.
  Send options travel with the draft. Closing during a send asks first.
- Sessions: answers are encrypted for the window that asked, and only a
  window holding the login answers. A renewal queued on the lock can be
  called off by a sign-out, signs nobody out once its account is gone, and
  takes the cached token after waiting (Rauthy `nbf`, #552).
- Ordinary tabs run none of the host. No lock is held for life.
- A message window hides after a refused session and waits for its
  folders; a composer stays on screen with its text. Another tab dropping a
  login it couldn't restore no longer closes pop-outs.
- Plain-text mail stays selectable in the desktop app. Folder save renames
  reserved and shell names and skips one refused name. All display-mode
  checks know the overlay. The attachment bar keeps to one row.

The earlier test that accepted text being gone after a failed rescue now
asserts that it reaches the "Continue draft" slot.
2026-11-03 08:37:10 +01:00
Linus Rath 4ccec4c850 test: drop an unused import from the composer move test 2026-11-03 08:30:13 +01:00
Linus Rath 7061c55985 fix: keep a typed compose tab when the window crosses Pro's width, without a reload 2026-11-03 08:24:30 +01:00
Linus Rath 3ccb2582c2 fix: keep typed text when the Mountain View composer moves between window, card and pane 2026-11-03 08:22:32 +01:00
Linus Rath ea8b27689a fix: give a composer in the bottom pane room for the message body 2026-11-03 07:55:39 +01:00
Linus Rath dccf4e8b9f chore: merge feat/desktop-offline into the windows branch 2026-11-03 07:31:32 +01:00
Linus Rath d6ae3d172b feat: merge opening files and sharing into the installed app
# Conflicts:
#	components/mail/mail-app.tsx
#	lib/__tests__/sw-harness.ts
2026-11-03 07:11:39 +01:00
Linus Rath f716ea66ee fix: ask before a share starts a message and save nothing until the first edit
Any site could POST a form to the share target on load: with no clicks
the composer opened with its subject and file, and autosave stored a
real draft on the server 2 seconds later. The worker can't see where a
POST came from, and Chromium's link capturing accepts form submissions,
so an installed-app window proves nothing either.

- every share landing now asks first, in every window and browser; a
  text landing address gets the same question. Nothing goes up before
  the answer.
- handed-in text and files are the composer's starting state: no
  autosave, page-close save or sign-out flush before the first real
  edit.
- the worker ignores frames, keeps at most 5 entries and 100 MB for 15
  minutes, sweeps on activate, on each share and from a page timer,
  and seals what it keeps with a key passed in the landing fragment.
  Offline wipes clear it too.
- the server fallback uses the fragment and caps by encoded length:
  3000 Japanese characters made a 27 KB `Location` and a 431.
- a second compose handoff waits until the composer closes, also one
  set aside by reading a message, where it used to replace the first.
- a launch leaving the mail page saves what was just typed first.
- an expired handoff or a missing stored file now says so; shares get
  the control-character stripping and limits of `parseMailto`.

Existing tests changed because the landing moved from the query to the
fragment, stored entries gained a sealed part, and a share now asks
first.
2026-11-03 07:08:38 +01:00
Linus Rath 590edb943f fix: keep an open composer or message in view when the window narrows to phone size 2026-11-03 06:58:03 +01:00
Linus Rath bb2393cf04 feat: open messages and drafts in their own windows and save all attachments to a folder
An installed app had no title bar of its own, no way to save attachments
except a zip, and "pop out" stayed inside the page. A new window was a
second copy of the app that had to restore the session itself, and on
start-up that evicts any login made without "Remember me" from the list
every window shares.

- Title bar: `display_override` with `window-controls-overlay`, one
  `--titlebar-inset` variable at the page roots and skeletons, and a drag
  strip with the window title. 0 without an overlay, so nothing moves.
- Save all to a folder: `showDirectoryPicker`, asking the device policy
  before the picker and before each file, with the zip as the fallback.
- Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in
  an installed app or when the desktop bridge has windows. Hidden in Lite.
- A pop-out borrows the main window's session and never evicts or starts a
  token timer. Every renewal now takes a Web Lock, so two windows can't
  send the same refresh token, which Stalwart 1.0 may treat as a replay.
- A draft moves to its window under the same id. A closing window hands
  unsaved text to the main window to save, since its own request wouldn't
  survive the close.
- Read state, drafts, theme and language follow across windows, and
  `html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
2026-11-03 06:50:21 +01:00
Linus Rath 51c09a1a69 fix: never let a notification's buttons go round plugins, session limits or Trash
Review of the notification actions. With a plugin that may stop a move
or read change, or a silent tab holding the login, the message now
opens instead of the route acting. The route answers 401 under a
session limit, and neither path touches a message since moved to Trash
or Junk. A tab that thawed declines an old click, Archive follows the
gate of the notification's login and files into a shared-out Archive
as the app's button does, Reply opens a composer in the Pro shell, the
route retries once when another client made the folder first and
leaves no empty folder behind, and logins match however the server
address is written.

Two assertions of this feature's own tests change: the route answers
401, not 403, when a login's scopes can't be worked out, since that now
also covers session limits; a tab's message to the page now carries
`at`.

The Pro shell's mail surface now reads a parked link's query from the
handoff: the address bar may have none by its first render, which lost
`?slot=` and `?owner=` there too.
2026-11-03 06:28:04 +01:00
Linus Rath 9e12266e1b fix: stop the welcome banner and message list running under the bottom bar on short screens 2026-11-03 06:00:20 +01:00
Linus Rath b0f540f482 fix: show the results after a filter search on a phone and dim the compose button behind the drawer 2026-11-03 05:42:36 +01:00
Linus Rath 8b50344e6c fix: close the folder drawer on tablets once a folder is picked 2026-11-03 05:36:27 +01:00
Linus Rath 2bb0797996 feat: archive, mark read or reply right from a new mail notification
A notification for one message gets buttons where the browser has room
for them, and a click on its body works as before. The service worker
has no login, so an open tab takes the action first, through the store
actions. With no tab open a new same-origin route, /api/push/action,
does Mark read and Archive with the context cookie. When the cookie is
gone, the token has expired or the login can't be told apart, the
message opens in the app instead. Reply opens the composer through a
`?reply=1` link, with any text typed on the notification.

- `public/sw.js`: buttons, asking open tabs, the route, `sync` handling
- `app/api/push/action`: same-origin, checks the login and the account,
  honours the admin's Archive gate, files by the mirrored archive mode
- `lib/pwa-notify`: the page's side, the mirror for the worker, and
  `requestOutboxSync`, which the offline queue calls
- Background Sync, thin version: a `sync` event tells open tabs to flush

Two push-preview-route assertions change: they match the whole answer
for a named message, which now also carries `login`, what the action
route checks so a stale notification can't act on whoever holds the
cookie slot now.
2026-11-03 05:26:43 +01:00
Linus Rath 64184d23f8 fix: keep Tab and the / shortcut out of a message list hidden by the open message 2026-11-03 05:26:39 +01:00