Commit Graph
20 Commits
Author SHA1 Message Date
Linus Rath b082ce582e feat: put offline, privacy and about on settings pages of their own and widen the settings search
Offline sat in the middle of Backup & reset, privacy settings were spread
over Reading, Composing and Message list, and the version card took room
from the page list in the sidebar's footer. The search matched letters
inside words ("lock" found Time format and Send Checks) and knew no other
words for a setting ("offline", "autoreply", "logout" found nothing).

- Offline is a page in the top group: Keep mail on this device, what is
  kept, Remove everything from this device, and the Outbox as its own
  section. Old `/settings/backup/offline` links and the offline notice
  land there.
- Privacy holds remote images and trusted senders, read receipts, sender
  pictures from the web, the desktop app's usage info and crash reports,
  and a link to the push relay on Notifications. Reading, Composing and
  Message list keep a link row; old links to those sections land on
  Privacy.
- About is the last page and holds the version card; the footer keeps the
  Advanced switch.
- The search matches whole words and word starts (substrings only in
  Chinese, Japanese and Thai), and each row can carry synonyms in the
  user's language. A page matches by its name and keywords or when one of
  its rows has every word. Account gets a Sign out row so "logout" leads
  somewhere.
- The index test now also fails when a page draws a section the index
  doesn't know, and a new test fails on two indexed rows with the same
  name. Tests that rendered remote content and sender pictures on Reading
  and Message list render Privacy instead, since that's where the rows are.
2026-11-04 07:37:25 +01:00
Linus Rath b92b488e39 fix: stop a local import losing messages on a held file and name what a resume leaves out
Fixes from the review of the desktop import.

- A message another program holds is no longer recorded as deleted. Only a
  real absence moves the import past it; anything else is tried again and
  then reported as a failure, and the next run reads it.
- A resume that can't read some folders names them in a toast with a choice
  to stop trying, keeps the job open and retries only those at the next start.
  Files chosen by hand in the same dialog are named, since they can't come
  back by themselves.
- A wipe or sign-out releases the job in the shell before the record goes.
  Leaving the dialog without starting releases the confirmed files. Job
  records are per account, and a start is refused while another window holds
  that account's job.
- Dedupe goes by the bytes without state headers: a reused Message-ID of the
  same length dropped a real message. Older records still count, so nothing
  goes in twice.
- A lost connection on a folder or Email/import pauses instead of counting
  towards the failures that stop an import.
- The taskbar goes through `lib/desktop/taskbar-progress.ts`.

Test changes: the taskbar test now renders the host, because its helper in
`local-import-job.ts` is gone, and the dedupe tests use `check`, which replaced
`keyFor` and `has`.
2026-11-03 19:17:38 +01:00
Linus Rath ef3e428ff3 Merge branch 'feat/desktop-offline' into feat/do-import
# Conflicts:
#	components/mail-transfer/mail-transfer-host.tsx
#	lib/desktop/types.ts
2026-11-03 18:34:24 +01:00
Linus Rath 084f93a786 feat: import Thunderbird and Outlook data from this computer in the desktop app
The import dialog in the desktop app can now find Thunderbird profiles and
Outlook PST files, show what it found and import what the user confirms.
Browsers behave as before.

- The bridge's `localImport` gives opaque handles and range URLs. The shell
  asks natively before it looks, before it reads and before it goes on after
  a restart.
- `lib/mail-import` gets a Maildir source and a per-file `folder`, so
  Thunderbird's `.sbd` tree maps onto the account's folders. The PST worker
  reads through a synchronous range XHR when there is no Blob.
- What was imported is remembered by Message-ID and size per folder, because
  compacting a folder changes its checkpoint key and Email/import doesn't
  dedupe. It runs from the dialog and on a resume.
- The job's record lives in IndexedDB, since Chromium writes localStorage too
  late to survive a kill. Windows wait for the job's lock, so one takes over
  when another goes.
- The desktop dialog says how long an import takes at the upload limit, warns
  when Thunderbird is running, and turns down .ost files with the reason.

The engine used to be able to lose an import while offline: uploadBlob kept
the file on this device under a stand-in id and reported success. It now
passes `noHold`, Email/import is network only, an offline spell pauses without
using up a message's tries, and a device-only client stops at once. The same
fix applies to imports in the browser.
2026-11-03 17:57:54 +01:00
Linus Rath 530364b11c fix: stop a cancelled or wiped export from downloading, and say why an export stopped
Review of the local store export. A cancel, sign-out or wipe during the
final step still downloaded the file; now the sink takes the job's signal
and drops the Blob, and a job that outlasts the wait is forgotten when it
ends. Finished exports are in a list a later wipe or sign-out can empty,
also after the dialog closed.

- `localExportAllowed` uses `scopeDecided`, so a domain or group scope
  can't be missed before the subject arrives. It's checked before the
  save picker, in the same tick as the click.
- WebKit holds the final Blob in memory: exports stop at 1 GB, and a
  larger choice or one over `storage.estimate()` is refused up front.
- An empty folder that refuses during a full listing counts as empty,
  only when the store's own count agrees. Any other refusal stops the run.
- A picked file goes on an abort or an all-failed run; if it can't, the
  dialog names it. Failure texts come from our strings, not the browser's.
- A selection keeps to the chosen folders on a retry.

The test that the organisation shows as the reason now flips the policy
through the policy store, because the old one replaced the guard and so
never reached the watch that a real change goes through. The spec moves
to an account of its own, dso-export: other specs running at the same
time work in ent-dave, and its folder went missing there mid-run.
2026-11-03 17:29:42 +01:00
Linus Rath a52cd8c829 fix: save no empty file when an export could write no message at all 2026-11-03 15:57:19 +01:00
Linus Rath 3cda05a1fa feat: export mail kept on this device to mbox or EML zip, also offline 2026-11-03 15:40:57 +01:00
Linus Rath 436703b0f7 fix: keep mail encrypted in the browser's private files during an export
Firefox and Safari have no save picker, so an export wrote a plain file
into the origin private file system for as long as it ran, and a crash
left a whole mailbox readable there until the next start. The check that
nothing readable sits in OPFS would find it.

The file is now a spool: each piece is sealed with AES-GCM under a key
that exists only in the tab's memory. When the export is done it opens
into a Blob the browser holds, and the spool goes at once.

The test that the private file stays while its download reads it changes
to say the same of the Blob: the file no longer outlives the export, so
what a download depends on is what gets released after the grace period.
2026-11-03 15:40:54 +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 cbd23db2fe fix: give mail import and export their own settings section and name the upload limit as the server does
- Settings > Backup & reset gets a Mail files section for Import mail
  and Export all mail, instead of sitting under settings backup.
- The pause message names Stalwart's 50000000-byte quota as 50 MB, not
  48 MB, and the dialog and settings texts mention .ost files.
- The old import's leftovers go: expandImportableEmails and
  EML_IMPORT_ACCEPT in lib/eml-import.ts, and the strings
  email_viewer.import_email, mailbox_context_menu.import_email and
  notifications.import_email_success/_error in all 30 locales.

lib/__tests__/eml-import.test.ts loses the tests of the removed
functions; its #891 checks that an .eml and an .eml in a ZIP keep their
Date header now run against the import's own sources.
2026-11-01 07:05:02 +01:00
Linus Rath 75aa66e0d9 feat: say when an import or export running in the background ends, and keep the focus in its dialog 2026-11-01 07:01:28 +01:00
Linus Rath 9d4d3261da fix: clear exported mail from the browser's storage and say plainly where an export goes
- The private file an export writes in Firefox and Safari is deleted
  when the app starts and when someone signs in, not only at the next
  export, and at once on sign-out. Close keeps it until its download
  had time to finish. Each tab holds its own files under a Web Lock, so
  one tab's sweep no longer deletes another tab's running export.
- A browser whose private file system can't write files (Safari before
  26) is told the export stays in memory instead of being promised a
  download.
- The export's live region announces only the phase and pauses; the
  count next to it no longer floods screen readers, and the format
  group is named once.
2026-11-01 06:50:13 +01:00
Linus Rath 7102a6f0cb fix: read Outlook files more safely and put their special folders in the right place
- A message whose attachments are bigger than the server takes is
  skipped before anything is read (a 1 GB attachment took several GB in
  the worker and could crash it), and a worker that stops answering
  ends the import with a message instead of a wait that never ends.
- The items after a broken one in a PST folder are counted as
  unreadable in the summary instead of disappearing.
- Sent Items and Deleted Items are known from the PST's own entry IDs,
  and the Inbox and the others by their names in more languages, so a
  PST imported at the top level fills the user's own folders whatever
  its language. The hint about that shows only for the top level.
- Message-ID, In-Reply-To and Content-ID from the PST can't carry line
  breaks into the rebuilt headers.
2026-11-01 06:45:33 +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 75fbd18bb9 fix: space the icon on the import dialog's Choose files button 2026-11-01 05:53:08 +01:00
Linus Rath d8f6be7b1e fix: delete a finished export's file from the browser when its account signs out 2026-11-01 05:42:00 +01:00
Linus Rath 58f66c4fdc fix: list the folders in the import dialog when it opens from Settings 2026-11-01 05:42:00 +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 a06281f23f feat: import Outlook PST files into their folders 2026-11-01 05:13:02 +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