Commit Graph
150 Commits
Author SHA1 Message Date
Linus Rath 8523289a4b feat: let an admin wipe a lost device, and clear an account the server refuses for good
Until now a lost laptop kept its offline mail for as long as anyone could
open it, and an account whose login the server had ended kept its copy too.

- `lib/devices`, `app/api/devices`: browsers with offline on and desktop
  apps register, in a durable collection with no expiry. A check-in goes
  by a per-device secret, so a revoked device still hears its wipe.
- `app/api/admin/devices` and the org devices page: wipe one device or all
  of a user's, with audit entries for each request and each confirmation.
- `lib/offline/device-wipe.ts`: a start finishes any wipe cut short, then
  waits up to a second for the instance before showing the local copy. A
  wipe takes the store, keys, plugin databases and handoffs, signs out,
  and reports back, again at the next contact if the report failed.
- `lib/jmap/client.ts`, `stores/auth-store.ts`: only `invalid_grant`, the
  session limit, or a 401 lasting past a re-auth count as refused. A 500,
  a proxy page or an outage never do.
- Settings: "Remove everything from this device", with a confirmation.

`lib/shared-store/__tests__/import-data.test.ts` changes its assertion
because it lists every collection the import copies, and `devices` is new.
2026-11-03 13:19:44 +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 20b2e7054e chore: merge search, notification actions and calendar into the mail branch 2026-11-03 07:02:39 +01:00
Linus Rath 2eb835b0b6 feat: merge offline calendar
# Conflicts:
#	lib/jmap/offline-hooks.ts
2026-11-03 06:39:38 +01:00
Linus Rath a3819f6a1c fix: keep offline calendar writes in order, and stop them getting lost or doubled
Review of the calendar store found writes that went missing, went out
wrong, or appeared twice.

- Writes with different scheduling flags stay separate and go out in
  order. Merging them collapsed the two steps of an occurrence change, so
  attendees who had answered got no mail, and "don't notify" went out to
  everyone with a later RSVP.
- An edit made after a create's lost response is sent as an update. An
  import of a uid the server has updates what differs.
- `createCalendarEvent`, `batchCreateCalendarEvents` and
  `createCalendarTask` choose a uid before the first send, so a lost
  answer online doesn't duplicate.
- The client resolves stand-in ids before the network, which covers an
  open dialog once the server is back.
- A stale whole `recurrenceOverrides` map no longer erases overrides.
- A 4xx refuses that write and the queue goes on. `serverFail`,
  `rateLimit` or an answer with no verdict retry instead of deleting.
- "This and all following" is refused offline with a clear message.
- A full list only drops what the server says it doesn't have. Sync adopts
  a stand-in by uid. Reminder keys survive a stand-in becoming real.
  Accounts the session no longer lists are forgotten.

Changed tests: the one asserting a sticky scheduling flag is gone, it
locked in the bug. Reminder keys now use the account and uid. Two flush
assertions drop `sent: 1`, because sync reads a lost write back first. The
two-account id test uses a listed account, as unlisted ones are removed.
2026-11-03 06:32:53 +01:00
Linus Rath d44738af87 fix: keep deleted mail out of merged search results and keep paging right after a refresh
The providers test now expects the palette option, because the palette keeps its paging apart from the mail list's. The search-merge test for a position with no cursor changed too: it used to expect that position sent to the server, which skips rows after a merge.
2026-11-03 06:16:45 +01:00
Linus Rath 687e7cc012 feat: move offline mode onto the encrypted local store
The IndexedDB cache, its crypto, the responder and the page-side sync
go. lib/offline keeps its structure, policy, wipes, sign-out gates and
notices; answers, the outbox and sync now live in lib/store, in a worker
behind lib/store/client/api.ts.

What users get besides that:
- every folder is kept, not just the Inbox and opened ones
- delete for good and empty trash work offline
- a change the server refuses, or that meets a message changed on
  another device, is shown instead of dropped
- a send whose response got lost is looked up by a Message-ID fixed when
  it was queued, then finished, submitted or sent again
- with the store on, a start paints from the local copy, then connects

An account the app drops by itself no longer wipes its data: only a
sign-out or an explicit removal does. The outbox of the old database
moves over once, in lib/offline/migrate-legacy.ts, and the old database
goes only after the store committed. `isNetworkFailure` now counts
timeouts and proxy 5xx. Sends and outbox requests are never answered
from the device.

Tests moved with the code: cache.test.ts and responder.test.ts are
replaced by lib/store/__tests__/, and review-fixes.test.ts, outbox.test.ts
and client-fallback.test.ts now run the real engine on node:sqlite.
Changed assertions: an uncertain send is looked up rather than left alone
forever, destroy and empty trash are queued rather than refused, and #9
(opened folders) became "a folder the server deleted goes", since all
folders are kept.
2026-11-03 05:54:39 +01:00
Linus Rath 6c3e68ce4e Merge branch 'feat/assistant-everywhere' into feat/assistant-d 2026-11-03 01:01:29 +01:00
Linus Rath f8de35dd85 feat: let plugins count a conversation's messages, and read only the newest 50
api.email.getThread fetches only the newest 50 messages of a thread
(Thread/get, then Email/get for those ids) instead of all of them with
their bodies. api.email.getThreadCount answers the number of messages
from Thread/get, with its own rate limit.

plugin-host-thread.test.ts now expects getThreadEmails to be called with
{ newest: 50 }, the new option that limits what is fetched.
2026-11-03 00:38:07 +01:00
Linus Rath 12dc2ce3c8 feat: let plugins find, read, attach, save and create files in Files 2026-11-03 00:17:46 +01:00
Linus Rath e80f5ade81 feat: let admins turn off downloads, export, printing and the install prompt, and add a watermark
Organisation > Devices now sets policy.json `organisation.devices`, each
switch off by default and scoped per domain or group:

- No downloads: attachments and files only preview (reader, conversation
  view, Pro tab, list chips, .eml previews, the Files app). Download,
  Download all, Open in new tab, Save to Files, drag-out, the browser's
  PDF viewer (pdf.js instead) and media download controls go away.
- No export: no .eml export, View source, message drag-out, or folder and
  account export; a running export stops and its file is deleted when
  the switch turns on. Import stays.
- No printing: no Print buttons in mail, calendar and contacts; Ctrl+P
  prints a note; the reader frame prints blank.
- No install prompt: no card, and the browser's own banner is held back.
- Watermark: the user's address and the date over the reader,
  previews and every printout (mail, calendar, contacts, plugins).

A message follows its own account's policy; what isn't about one message
follows the strictest signed-in account. Every user path hides its
button, and the anchor and new-tab helpers refuse as a backstop
(lib/download-guard.ts), never the blob readers. The plugin APIs
`ui.downloadFile` and `ui.print` refuse too, and the office editor frame
loses `allow-downloads`.

Two assertions in components/mail/__tests__/blob-routing.test.tsx now
expect `{ kind: 'export' }` as the fifth argument of downloadBlob for an
.eml export: the export says what it is, so the backstop checks "No
export" rather than "No downloads" for it.
2026-11-01 08:52:34 +01:00
Linus Rath 2a952f4b72 Merge branch 'feat/enterprise-pack-c'
# Conflicts:
#	lib/sign-out-cleanup.ts
2026-11-01 07:39:30 +01:00
Linus Rath ebb33619a0 Merge branch 'feat/enterprise-pack-a' 2026-11-01 07:36:59 +01:00
Linus Rath 8d48f98261 fix: keep failed messages for the next import run and stop an import that can't go on
A message that failed for a passing reason (a dropped connection, a
server error, an expired token) used to count as done, so continuing
the import skipped it for good, and a slow upload followed by a cancel
imported the messages after it twice.

- The checkpoint keeps every message that is done, also past a gap,
  and the next run of the same import tries only the rest again.
- Ten failures in a row, a full mailbox (overQuota) or a refusal
  (forbidden) stop the import with a message instead of going through
  the whole file.
- An upload without an answer pauses the import instead of failing the
  message: 30 seconds, then until the upload window ends.
- Email/import, the export's Blob/get and folder listing pass the
  client's own 429 pause by, and a 429 on them doesn't pause the rest
  of the app.
- Signing out stops the job of that account only, writes no checkpoint
  and imports nothing more afterwards; another account's sign-out
  leaves the job alone.
- One tab imports a file at a time (Web Locks), and Import waits until
  the dialog knows whether an earlier run can be continued.

The import dialog tests now wait for the Import button to be enabled
before clicking it, since it stays off during that lookup.
2026-11-01 06:37:08 +01:00
Linus Rath 1c7f81bcc6 feat: pick a classification label in the composer and stamp it on sent mail 2026-11-01 05:33:12 +01:00
Linus Rath 15f0bd198c feat: export a folder or the whole account as mbox or ZIP 2026-11-01 05:28:58 +01:00
Linus Rath 877198a4bf feat: answer invitations in a calendar shared with you as its owner #634 2026-11-01 05:05:12 +01:00
Linus Rath d714957f98 feat: import mbox, EML and ZIP mail with progress, pause and resume 2026-11-01 04:58:55 +01:00
Linus Rath 50f95f4463 feat: report a message as phishing and move it to Junk 2026-11-01 04:58:01 +01:00
Linus Rath 73a7bb7bcd fix: send the headers plugins add to an outgoing message instead of dropping them 2026-10-31 17:40:20 +01:00
Linus Rath 96797597c5 Merge branch 'main' into feat/org-features 2026-10-28 15:14:34 +01:00
Linus Rath 1322f6c3f0 fix: switch between demo accounts without the app crashing 2026-10-28 09:48:49 +01:00
Linus Rath 5d90a850bd Merge branch 'feat/org-recovery' into feat/org-features
# Conflicts:
#	components/email/email-list.tsx
2026-10-28 09:17:56 +01:00
Linus Rath 4d846cfc91 fix: read the whole deleted items archive newest first and only for the account shown #1319
Stalwart's x:ArchivedItem/query without calculateTotal scans ids
ascending, stops at 5000 and only then reverses them, so the newest
items of a large archive never showed, and admin accounts got every
account's items. The review of the branch also found gaps around
account switches, restores and focus.

- Read the archive in pages of 500 with calculateTotal, newest first,
  stop at a whole page older than the days limit, and offer
  "Show older" after 2000 items; detection asks for one item.
- Send the accountId filter and fall back without it on
  unsupportedFilter; keep only items of the account, never one without
  an account id.
- Ask nothing until config and policy are loaded and the policy is the
  current account's; the later check after a refusal is forced.
- Show the Trash link only when the archive holds a message inside the
  days limit; the Settings page shows whenever the server has the
  feature and says that archiving must be on in Stalwart.
- Restore: notFound leaves the list with a note, forbidden hides the
  page, results for another account are dropped, the button keeps focus.
- Forget what servers said at a full sign-out.
2026-10-28 09:08:44 +01:00
Linus Rath 33ca8efb0f fix: keep shared tags safe when members edit at once or the server fails
The first version wrote the whole tag list on every change, guessed a
group when the server wouldn't say, and treated a failed read like a
server without metadata. Now:

- Definitions are a map by tag id under `tags`, and each change patches
  one tag (`metadata/org.bulwarkmail.tags/tags/<id>`), so two members
  adding tags at once both keep theirs. Fields and colors this version
  doesn't know survive. The `{ list }` and bare-array shapes are still
  read and converted by the first save.
- Shared tags appear only for confirmed group principals. A failed
  principal lookup keeps the user's own tags and asks again later.
- "No metadata advertised" (0.16) and "the read failed" are separate.
  A failed read keeps the last good tags; a failed save keeps the change,
  says so, and sends it with the next good read after the client's
  pause.
- Push reloads wait for 5 s of quiet and skip the echo of our own save;
  a returning tab reloads definitions only, and the scan for unnamed tags
  runs once per sign-in and then at most every 30 minutes.
- The editor names its buttons after the tag, shows the delete warning
  and 44 px buttons on phones, returns focus to the edit button, and
  asks for Inbox rights only for confirmed groups.
2026-10-28 08:58:11 +01:00
Linus Rath 42b694fb2c feat: save attachments of shared and group mailboxes to Files by copying the blob on the server 2026-10-28 08:16:47 +01:00
Linus Rath 7f06ed7637 Merge branch 'feat/org-features' into feat/org-tags
# Conflicts:
#	lib/admin/restriction-keys.ts
2026-10-28 08:14:39 +01:00
Linus Rath ba6e6af014 feat: recover deleted items from Stalwart's archive #1319 2026-10-28 07:27:47 +01:00
Linus Rath b049b6f470 feat: share tag names and colours in group mailboxes #1310 2026-10-28 06:43:46 +01:00
Linus Rath 5ce0f96f26 fix: read delegate sharing only for the apps that are on, with the file tree alongside 2026-10-28 06:27:07 +01:00
Linus Rath bcf4f3a7a6 feat: give a delegate access to mail, calendars and contacts in one step 2026-10-28 05:11:23 +01:00
Linus Rath 6c5aead20b feat: undo a delete to Trash, archive, move, spam report or remove from folder from its toast 2026-10-28 03:11:43 +01:00
Linus Rath 7ea0a7d58d feat: read and write account metadata for shared Bulwark data 2026-10-28 01:14:30 +01:00
Linus Rath 4b69f1307f feat: let admins scope the user policy per domain or group 2026-10-28 00:29:02 +01:00
VasilisNtovolos ac99c68ef3 fix: reconcile confirmed permanent bulk deletions 2026-10-27 14:51:38 +02:00
Linus Rath cf2c27157d Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	components/contacts/contacts-app.tsx
#	components/email/__tests__/composer-discard.test.tsx
#	components/email/calendar-invitation-banner.tsx
#	components/email/email-composer.tsx
#	components/mail/mail-app.tsx
#	components/pro/pro-compose-tab-body.tsx
#	components/pro/pro-email-tab-body.tsx
#	components/ui/__tests__/avatar.test.tsx
#	components/ui/avatar.tsx
#	hooks/use-mailbox-drop.ts
#	lib/__tests__/calendar-alerts.test.ts
#	lib/__tests__/jmap-calendar-tasks.test.ts
#	lib/__tests__/jmap-send-threading.test.ts
#	lib/demo/demo-client.ts
#	lib/demo/fixtures/calendars.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/web-push.ts
#	stores/__tests__/contact-store.test.ts
#	stores/__tests__/email-store-thread-key.test.ts
#	stores/calendar-store.ts
#	stores/contact-store.ts
#	stores/email-store.ts
2026-10-26 10:14:12 +01:00
Stefan Hildebrandt 04efcfc1f3 fix: save a group identity's drafts in the group account's Drafts (#1090)
Drafts were always created in the primary account, so a message composed as
a group identity sat in the member's own Drafts while its Sent copy (since the
previous commit) went to the group.

- createDraft takes { accountId, previousDraftAccountId }: the draft goes to
  the account that owns the From identity (resolveSendAccountId, now public
  and briefly caching the identity lists for autosave), and the replaced
  version is destroyed in the account it lives in.
- The composer tracks draftAccountId next to draftId (save, re-resolve of
  attachment blobs, send, discard, stashed state), since draft ids are only
  unique within one account.
- sendEmail destroys the sent draft in options.draftAccountId; discarding
  (inline and Pro tab) deletes it there, through the identity's login.
- Re-opening a draft from a group/shared Drafts folder (or a stamped
  aggregate row) keeps its account.
- resolveSendAccountId is optional on IJMAPClient; without it drafts stay in
  the primary account.
- Tests: client unit tests for draft placement / replacement / send cleanup,
  E2E spec 18 draft cases (autosave + send, discard).
2026-10-19 16:33:27 +02:00
Linus Rath 1c8e7025e6 Merge pull request #1178 from VasilisNtovolos/fix/mixed-account-bulk-archive
fix: route bulk archive by owning account
2026-10-18 14:33:41 +02:00
Linus Rath a11a89d689 fix: skip vacation filter rules on shared accounts without sieve #1263 2026-10-18 13:46:47 +02:00
Linus Rath 107d731af7 feat: remove a message from just the open folder when it is in several #615 2026-10-17 19:18:25 +02:00
Linus Rath 1ad1e31781 feat: set message priority in the composer #970 2026-10-17 19:18:23 +02:00
Linus Rath f8ef54dbea fix: enable push notifications when another user shares a folder with you #1050 2026-10-17 19:17:59 +02:00
Linus Rath 964626794b feat: browse the company directory in contacts and the composer #1253 2026-10-16 20:46:27 +02:00
Linus Rath 41a0716c50 Merge branch 'main' into feat/stalwart-1.0 2026-10-16 01:41:48 +02:00
Linus Rath 6ef0d97900 fix: count storage quota against softLimit when the server sets one #1242 2026-10-15 14:31:09 +02:00
VasilisNtovolos 69519bf344 fix: respect account permissions when creating Archive 2026-10-15 14:24:44 +03:00
Linus Rath db2c0aff3d perf: load conversation sizes with the folder page instead of in a second request 2026-10-15 00:14:57 +02:00
Linus Rath 2d67442a2b perf: list search results without waiting for the highlighted excerpts 2026-10-14 23:51:17 +02:00
Linus Rath c633185cda Merge branch 'main' into feat/stalwart-1.0
# Conflicts:
#	CONTRIBUTING.md
#	LICENSE
#	README.md
#	app/api/calendar-agenda/route.ts
#	app/api/wopi/files/[fileId]/contents/route.ts
#	app/api/wopi/launch/route.ts
#	components/calendar/calendar-app.tsx
#	components/calendar/calendar-day-view.tsx
#	components/calendar/calendar-month-view.tsx
#	components/calendar/calendar-week-view.tsx
#	components/calendar/event-modal.tsx
#	components/contacts/contact-detail.tsx
#	components/email/email-composer.tsx
#	components/email/email-list.tsx
#	components/files/file-browser.tsx
#	components/files/files-app.tsx
#	components/files/folder-tree-sidebar.tsx
#	components/filters/filter-rule-modal.tsx
#	components/mail/mail-app.tsx
#	hooks/use-mailbox-drop.ts
#	integration/run-tests.sh
#	lib/__tests__/calendar-agenda-route.test.ts
#	lib/__tests__/jmap-draft-lifecycle.test.ts
#	lib/__tests__/web-push.test.ts
#	lib/account-state-manager.ts
#	lib/jmap/client-interface.ts
#	lib/jmap/client.ts
#	lib/jmap/types.ts
#	lib/message-list-order.ts
#	lib/sieve/generator.ts
#	lib/sieve/parser.ts
#	lib/telemetry/payload.ts
#	lib/thread-utils.ts
#	lib/unified-mailbox.ts
#	lib/web-push.ts
#	lib/wopi/token.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/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/tr/common.json
#	locales/uk/common.json
#	locales/zh-TW/common.json
#	locales/zh/common.json
#	scripts/lite/lib.mjs
#	stores/__tests__/calendar-store-synthetic-ids.test.ts
#	stores/__tests__/email-store-shared-folder-keywords.test.ts
#	stores/__tests__/email-store-unified-actions.test.ts
#	stores/__tests__/file-store.test.ts
#	stores/email-store.ts
#	stores/file-store.ts
#	stores/message-list-tabs-store.ts
2026-10-12 08:35:08 +02:00
Linus Rath 37440e02b0 fix: let tasks move into and live in calendars shared with you #1204 2026-10-12 07:36:46 +02:00