Commit Graph
122 Commits
Author SHA1 Message Date
Linus Rath 409cdb139e feat: preview images, text and PDFs in the Files tab
Tapping a file in the Files tab downloaded it and went straight to the
Android "Open with" chooser or the iOS share sheet. It now opens the
reader's attachment preview for the types that preview can show, and
hands only the rest to another app, as before.

- `filePreviewKind` (`src/lib/file-preview.ts`) builds on the reader's
  `previewKindFor` and adds source code and config files as text; HTML,
  SVG, images React Native cannot decode and PDFs on Android (no in-app
  renderer) still go to another app
- files over a cap per type (text 512 KB, .eml 10 MB, images 25 MB,
  PDFs 50 MB) open in another app instead; when a node has no size,
  the downloaded file is checked
- the preview downloads the file once, with the same authenticated
  download as Download, into a temp folder of its own; Open with, Share
  and Download reuse that copy, so they work without another fetch
- the folder is deleted when the preview closes, or left to the
  stale-export sweep, which now also clears preview folders, once
  another app got the file
- the per-file menu offers Preview and "Open with…" instead of the
  combined "Preview / Share"
- the shared preview gains a header Open button, an optional Download
  button, labels on its icon buttons and a fallback when an image does
  not decode; the .eml parsing moves into `emlPreviewFromBytes`, which
  the reader uses too
2026-09-24 19:28:37 +02:00
Linus Rath 1d755fc066 fix: list event participants once with contact names and organizer status
Ported from webmail 8cbc5bc5 and 6f99c017. Stalwart can emit the
organizer both as the ORGANIZER-derived participant and as an ATTENDEE,
so the detail sheet listed them twice and counted a phantom
participant. It also drops the organizer's display name, leaving a bare
address, and the organizer without a status got the pending dot.

- fold participants with the same address into one entry, keeping a
  real RSVP over needs-action, and count addresses in
  `getParticipantCount`
- show the organizer as accepted when they have no status, with the
  status label like everyone else
- fill missing names from contact cards (exact address, then the same
  local part on a related domain), then the account and identity
  names, via `useContactNameResolver`; the editor uses it for the
  organizer name of a new invitation
2026-09-24 19:12:03 +02:00
Linus Rath c0582ab950 fix: send the RSVP for invitations outside the loaded calendar window
Accepting an invitation looked the event up in the calendar store,
which only holds the window the calendar last showed. For an event
further out the lookup came back empty, no response was sent, and the
banner still said "Added to calendar".

- find the event on the server with a `uid` filter, like webmail's
  `queryCalendarEvents({ uid })`, both when the banner opens (so it
  shows as already in the calendar) and after importing it
- let `rsvpEvent` take the event when it isn't in the store, so the
  organizer repair still applies
- show "Could not send your response" whenever no response went out
2026-09-24 19:12:02 +02:00
Linus Rath 15717f137a fix: keep what was typed in the event editor when calendars refresh
The editor reset its form whenever `calendars` or `currentUserEmails`
got a new identity, which happens when a calendar push refetches the
calendars or the alias lookup finishes, so the title and times the user
had typed were replaced mid-edit. Reset only when the editor opens or
moves to another event, and fill in the calendar separately when the
calendars arrive after a new-event form opened.
2026-09-24 19:12:02 +02:00
Linus Rath 6a975b822c fix: show a task's due time in local time like events
A task's `due` is wall time in the task's `timeZone`, but the tasks
sheet, the calendar chips and task reminders read it as device time,
so a task due at 17:00 Europe/Berlin showed and fired at 17:00
wherever the phone was. Events already go through `utcStart`.

- resolve the due in its zone with a new `getTaskDueDate` and use it
  for the list, sorting, overdue, the chips and reminder times
- save an edited timed due with the effective time zone, as the event
  editor does, so the converted local time isn't read back in the
  task's old zone
2026-09-24 19:12:02 +02:00
Linus Rath 548b647f15 feat: add background colour and the webmail's 16 text colours to the composer
The composer offered 8 text colours and no background colour. It now
has the webmail's 2 x 8 palette (7e0a86a6, 3957b358) for text colour,
and a background colour button with the same palette and a "Remove
background color" action, applied with the editor's `hiliteColor`.

The webmail's font-size picker is not ported: the editor has no mark
model to scope a size to, so it needs a selection-rewriting command in
the page script that can only be checked on a device.
2026-09-24 19:10:46 +02:00
Linus Rath 3faac98627 fix: sign quick replies with the sending identity's signature
Quick replies went out with no signature at all. They are now signed
the way the composer signs a plain-text reply: between the reply and
the quote or at the end, following the signature position setting, with
the "-- " separator when it is on, and with the primary identity's
signature when the sending alias has none of its own.

The quick reply also resolves its sending identity like the composer
(webmail 4fc7b8b5): recipients in To, Cc, Bcc order, and a reply to our
own message from the identity that sent it. It still never takes the
catch-all From rewrite, having no From row to show it.
2026-09-24 19:10:45 +02:00
Linus Rath 2d907ffab8 feat: choose exact-address or same-domain matching for reply from received address #1000
Ported from the webmail (565d5c10). With "Reply from received address"
on, a new "Received address matching" choice limits the catch-all From
rewrite: "Exact address only" keeps replies to configured identities,
"Any address on my domains" (the default, and the old behaviour) also
sends from other addresses on an identity's domain. Deployments whose
extra addresses are distribution lists rather than aliases want exact.

The setting is `replyIdentityMatch`, the webmail's key, so it round-trips
through settings export and import. The toggle's description now says
that identities always match, since that no longer depends on it.
2026-09-24 19:10:45 +02:00
Linus Rath 235f4ea77f fix: stop replies and forwards loading the remote images the viewer blocked
The composer's editor page had no Content-Security-Policy, so quoting a
message loaded every remote image and tracking pixel in it, even when
the viewer had shown that message with remote content blocked.

The editor page now carries a CSP that mirrors the viewer's: nothing
loads but our inline script, styles and images, and remote images and
fonts are blocked whenever the quoted original is one the viewer would
block (untrusted sender and a policy other than "allow"). A reopened
draft that carries a quote counts as untrusted. Inline and pasted
images (`data:`, `blob:`) keep working, and the sent HTML is unchanged.
2026-09-24 19:10:44 +02:00
Linus Rath aa250759e5 fix: keep swipe bands unmounted until a mail row is swiped
Every row in the mail list rendered both swipe action bands behind its
content, with an icon, a label and four fresh `Animated` interpolations
on each render, although a band is only visible while the row is
dragged or held open. `SwipeableRow` was also not memoized.

- the bands now mount when a drag is granted and unmount once the row
  springs back to rest (or has flown off), where they are covered and
  fully transparent, so the swipe looks the same
- the band interpolations are built once per row
- `SwipeableRow` is memoized; `context` compares by its fields since
  callers build it per row
2026-09-24 19:10:43 +02:00
Linus Rath a3d3af797c fix: keep push off after it is turned off or this device is revoked
The launch-time effect in `App.tsx` re-ran the full setup for the
active account whenever a relay URL was stored, and that URL is shared
by every account on the device. Turning push off for one account, or
revoking this device, lasted only until the next launch or token
rotation. A subscription revoked from another device was re-created
without asking.

- Turning push off in settings and revoking this device now go through
  `disablePushForAccount`, which tears the registration down and marks
  the account as opted out. Enabling push again clears the mark.
- The effect calls `resyncPushNotifications`, which skips opted-out
  accounts. If the server no longer has the recorded subscription
  although it still had a day or more to live, it treats the
  subscription as revoked and turns push off for the account instead
  of registering again. A subscription that lapsed is re-created as
  before.
- Setup records the expiry the server reports: from the create
  response (`createPushSubscription` now returns it) and from every
  listing.
2026-09-24 19:09:35 +02:00
Linus Rath 9e95273c14 feat: tell the user when a viewer action fails and when mail is sent
The viewer fired its store actions with `void` and went back to the
list, so a refused delete, archive, move, spam report, star, tag or
read change failed silently, and nothing confirmed a sent message.

- each viewer action now reports a failure as an error toast, and a
  refused star, tag or read change is taken back on screen
- the viewer mounts its own toast host, since the list's sits under it
- an immediate send from the composer or the quick reply box shows
  "Email sent"; a send held for the undo delay keeps its undo bar
  (webmail `ec67ab99`, `b03a0c1d`)
2026-09-24 19:06:55 +02:00
Linus Rath 5c7301b3f1 fix: stop viewer actions from hitting another account's message with the same id
Stalwart ids are only unique per account. When its opener named no
account, the viewer used the account of the folder open in the list,
and it resolved Trash, Junk and move targets from that folder as well.
Deleting a team message from the Unified Inbox therefore sent
`Email/set` to the user's own account and trashed an unrelated message
with the same id, and a message opened from a notification, deep link
or contact while a shared folder was open went to the shared account.

- the list names the open folder's account when it opens the viewer,
  and the viewer takes the account from its opener only, fixed at open
- Trash, Junk, Inbox and move targets come from the message's account,
  and delete and move take the message out of its own folder
- the store's delete and move take the viewed message: they only touch
  the list's row when the list shows that account, and refuse folders
  of any other account
- another account's message is marked read on the server directly
  instead of through the open folder's list
- quick reply, unsubscribe and read receipts file into the message's
  account's folders
- undo no longer slots another account's message into the open list
- Archive is only offered for mail of the open folder's account, since
  it still resolves the archive folder from there
2026-09-24 19:06:54 +02:00
Linus Rath 182774884e feat: add a setting to sort contacts by last name #963
Port of the webmail's `sortContactsByLastName` (fb24ef5d). With the new
"Sort by last name" switch in Settings > Contacts, the contact list is
ordered and letter-grouped by "Surname, Given Middle", so family members
sit together. Cards without a structured surname fall back to the last
word of `name.full`; organizations, nicknames and bare addresses keep
their display name. The setting defaults to off, is persisted, and is
exported and imported under the webmail's key.
2026-09-24 19:03:34 +02:00
Linus Rath eca3871545 feat: show the sender's contact photo as their avatar #54
Sender avatars ignored address-book photos, so a contact whose photo
shows in Contacts still got initials or a favicon in the mail list,
the viewer and the thread cards. Like the webmail's `Avatar`, the
contact photo now wins over the favicon and the initials; `disableImages`
(Junk) still shows initials only.

The lookup goes through a lowercased email -> photo index that is built
once per contacts array, and each avatar selects just its own photo
string, so a list of avatars neither rescans the address book per row
nor re-renders when an unrelated contact changes. Stalwart returns
photos as inline data URIs, which the index normalizes the same way
Contacts does, so no authenticated download is involved.
2026-09-24 19:03:34 +02:00
Linus Rath d89e33110a fix: load remote content only for senders in the trusted senders book
With "Sync trusted senders to address book" on, the viewer trusted every
address in every address book, so remote images and tracking pixels
loaded for any contact, including mail with a spoofed From that uses a
contact's address. The toggle also switched itself on the first time
Content & Senders was opened on a contacts-capable account, so merely
visiting settings widened trust to the whole address book.

Now only the local allow-list and, while sync is on, the dedicated
"Trusted Senders" book count, as in the webmail viewer. The setting
stays off until the user turns it on, and the viewer only fetches the
book while sync is on.
2026-09-24 19:03:33 +02:00
Linus Rath 2511df1059 fix: translate the plain text font, message spacing and read receipt settings 2026-09-24 19:01:24 +02:00
Linus Rath a86cd1d0eb feat: manage filters and vacation of shared and group accounts
The phone only ever read and wrote Sieve scripts and the vacation
responder of the user's own account, and the "Shared with me" list in
Account settings said to use the webmail for the rest. Port the
webmail's scoped settings (4c6c1aab) for these two panes:

- `api/sieve.ts` and `api/vacation.ts` take an optional account id and
  gate on the account's own `accountCapabilities` for
  `urn:ietf:params:jmap:sieve` and `urn:ietf:params:jmap:vacationresponse`,
  treating non-personal accounts as capable because Stalwart doesn't
  always advertise capabilities on them. Script blobs are uploaded and
  downloaded against the same account.
- Tapping a shared account in Account settings scopes Settings to it:
  only the Filters and Vacation Responder panes it supports are listed,
  behind a "Managing: <name>" banner that leads back to the own account.
  The scope is dropped when the signed-in account changes.
- The filter and vacation stores remember which account they hold,
  reset when it changes and drop replies for an account the user has
  already left, so one account's rules are never saved into another.
- The rule editor offers the managed account's folders as targets, and
  the "vacation active" banner only shows for the own account.
2026-09-24 19:01:24 +02:00
Linus Rath 4e7ae2fa30 fix: only offer scheduled-send times the server accepts
Stalwart advertises `maxDelayedSend` as a fixed 30 days, but its MTA
rejects any hold beyond its `futureRelease` limit, 7 days by default.
Now that scheduled send works on Stalwart, a time 8 to 30 days ahead
passed the checks and then failed with the raw "501 5.5.4 Requested
hold time exceeds maximum of 604800 seconds" reply.

Ported from the webmail:
- on Stalwart the advertised 30 days count as 7
- a submission or reschedule refused for its hold time throws
  `ScheduleTooLateError`, and the limit it names replaces the advertised
  one for that server
- the composer and the Scheduled screen show "Too far ahead" for it
  instead of the SMTP reply

The composer's and the Scheduled screen's presets now only list times
within the limit, and the undo-send hold, from the composer and from
quick reply, is capped at it.
2026-09-24 19:00:08 +02:00
Linus Rath d45c7996d2 fix: keep list previews and subjects on one line
Previews keep the body's paragraph breaks, so a line-clamped row showed
a dangling "…" line and rows came out at uneven heights, and a subject
carrying CR/LF rendered over several lines in the list and the viewer.
Fold every whitespace run to a single space before display with a new
`singleLine` helper, in the mailbox and unified inbox rows, the viewer
subject and the collapsed thread card preview.
2026-09-24 18:50:55 +02:00
Linus Rath 476780553c fix: show large message bodies in full instead of a cut-off stub #884
Full-message fetches cap body values at 512 KB and never read
`isTruncated`, so a bigger body rendered whatever the server cut off
with no notice; for a 586 KB HTML report Stalwart returned a 59
character stub and the viewer showed a heading, one line and "…".

Port the webmail's refetch: when the first HTML or text body part
comes back truncated, `getFullEmail`, `getFullEmails` and
`getThreadEmails` fetch the HTML and text body values of those messages
again in one batched `Email/get` with an 8 MB cap, keeping the values
of other parts. Truncated text attachments don't trigger it. If the
body is still truncated, or the refetch fails, the viewer now says the
message is too large to show in full.
2026-09-24 18:50:55 +02:00
Linus Rath 910c503f4a fix: render right-to-left mail right-to-left
The HTML and plain-text body documents had no direction, so an Arabic
or Hebrew message was laid out left-to-right: left-aligned, with its
sentence-final punctuation on the wrong side. Port the webmail's
`dir="auto"` on the body of both documents so the first strong
character picks the direction. An email that declares its own `dir`
on its `<html>` or `<body>` tag keeps it; the parser merges those
attributes into ours only while ours has none.

The shrink-to-fit reporter widens its wrapper to the content's natural
width and scales it from the top left. In a right-to-left body that
over-wide block overflowed to the left instead, so a 600px newsletter
scaled into the space left of the screen. A negative right margin now
keeps the widened wrapper flush left in either direction, so the
existing origin, pan clamp and pinch anchoring still hold.
2026-09-24 18:50:55 +02:00
Linus Rath e28e8b4d9c fix: turn scheduled send and the undo-send delay back on for Stalwart #57
RFC 8621 puts the submission limits (`maxDelayedSend`,
`submissionExtensions`) and the mail limits
(`maxSizeAttachmentsPerEmail`) under each account's
`accountCapabilities`. Stalwart advertises an empty object for them at
session level, which is the only place the client looked, so scheduled
send was refused, the undo-send delay silently fell back to an instant
send, the Scheduled view listed nothing and the per-message attachment
cap was never enforced.

The client now reads these from the account that sends (the account's
own submission capability, else the session's primary submission
account, like the webmail), falling back to the session-level object
for servers that only advertise it there. Quick reply, which can send
from a shared account, now checks that account before holding the
message for the undo-send delay.
2026-09-24 18:43:56 +02:00
Linus Rath 5f3f584ca8 fix: auto-send read receipts only for the message on screen and clean up failed ones
With "Always send" the banner's auto-send ran in every page the viewer
pager keeps mounted, so opening one message also answered the receipt
requests of its unseen neighbours. The receipt is imported into Sent
before it is submitted, so a refused submission left a "Read: ..." copy
there, and the failure cleared the per-instance guard, so the next
render or remount imported another one.

- the pager marks the page on screen `active`, and the banner only
  auto-sends there
- an automatic receipt is tried once per message per session, keyed by
  account and id, so a failure or a remounted page does not send again
- `sendReadReceipt` destroys the imported copy when the submission
  fails, which also covers the manual "Send receipt" button
2026-09-24 18:36:15 +02:00
Linus Rath ee04e95d4f feat: add UnifiedPush as an alternative to FCM for push notifications 2026-09-24 18:17:43 +02:00
Linus Rath f228257ec9 fix: stop the viewer crashing on any message with attachments by keeping store selectors referentially stable 2026-08-29 22:41:09 +02:00
Linus Rath 3f2dfe83ee chore: merge upstream fixes for reply threading, iOS session auth drop, calendar paging and the camera prompt (#35, #38, #39, #40) 2026-08-29 20:16:33 +02:00
Linus Rath 96272b6e17 fix: confirm the host before opening meeting links from calendar events and invitations 2026-08-29 18:56:46 +02:00
Linus Rath 2abea9742e feat: add a send-invitations switch and custom reminder offsets, keeping alerts the picker cannot show 2026-08-29 18:53:58 +02:00
Linus Rath 5fe8e407c7 feat: duplicate, share as .ics and copy the meeting link of an event; link imported UIDs instead of skipping them (#113) 2026-08-29 18:49:08 +02:00
Linus Rath 09c24fa805 feat: edit tasks with description, priority and due time, filter the list and overlay due tasks on the calendar 2026-08-29 18:44:36 +02:00
Linus Rath a222e89611 refactor: merge the quick reply's email-body imports 2026-08-29 18:40:42 +02:00
Linus Rath 6ae21d5690 feat: folder actions, tags and unified views in the drawer, folder parent/role editing, tag discovery and account-routed unified actions (#404, #498, #495, #175, #658, #855) 2026-08-29 18:39:45 +02:00
Linus Rath d2ed27fd3b feat: conversation view with collapsible cards, header details, auth chips, unsubscribe and read-receipt banners, attachment preview and zip, embedded-message and TNEF unwrap in the reader (#480, #830, #891) 2026-08-29 18:38:53 +02:00
Linus Rath 0f80e98fbc feat: localize the calendar, add Saturday week start, ISO week numbers, month chips and 12h labels (#666) 2026-08-29 18:35:46 +02:00
Linus Rath c659b5489c feat: signature, read-receipt, empty-subject, autosave and sub-address settings, identity Reply-To/Bcc/HTML signature with a default identity, template category/favourite, and reschedule/send-now/edit on the scheduled screen (#507) 2026-08-29 18:33:22 +02:00
Linus Rath 3a8264c7f7 feat: translate the account security pane 2026-08-29 18:33:16 +02:00
Linus Rath dc3ac4c7ed feat: translate the downloads pane 2026-08-29 18:30:32 +02:00
Linus Rath 22697fd111 feat: translate the appearance, reading, composing, identity, template, keyword and sidebar-app panes 2026-08-29 18:29:34 +02:00
Linus Rath fe45a444d3 feat: port the webmail's built-in colour themes to the Themes pane 2026-08-29 18:29:18 +02:00
Linus Rath bea3fc4c42 feat: message-list order presets with the Stalwart keyword-sort polarity probe, avatars-in-junk and folder-count toggles (#718, #498) 2026-08-29 18:28:43 +02:00
Linus Rath 5b72c4d314 feat: drafts with autosave and a save/discard close guard, Bcc, signatures, templates, sub-addressing, From override, read receipts, upload progress, inline-image hydration and an undo-send snackbar in the composer (#188, #849, #848, #540, #570, #684, #163) 2026-08-29 18:28:38 +02:00
Linus Rath 3194b5ece1 feat: tags, pins, recipients, not-spam, delete confirm, folder-scoped search and draft editing in the mail list 2026-08-29 18:23:08 +02:00
Linus Rath 8206893860 feat: assess invitation trust, read the iTIP method from the ICS and pick the target calendar in the invitation banner 2026-08-29 18:22:00 +02:00
Linus Rath 3ae9bbe702 feat: add a toast host, prompt dialog, haptics helper and reduce-motion aware animations 2026-08-29 18:20:01 +02:00
Linus Rath 06742ef7d5 fix: render HTML-only mail as HTML, keep stylesheets and block every external-content vector in the reader (#46, #49) 2026-08-29 18:15:22 +02:00
Linus Rath dae5700688 feat: manage calendars from the drawer and scope, edit and auto-refresh iCal subscriptions 2026-08-29 18:14:48 +02:00
Linus Rath ddae1e63f2 feat: manage public keys and encryption at rest from account security 2026-08-29 18:11:13 +02:00
Linus Rath 490355c65e feat: list logged-in accounts, storage quota and auth method in account settings 2026-08-29 18:11:13 +02:00
Linus Rath 240471764a feat: localize the sign-in flow and offer previously used addresses 2026-08-29 18:10:25 +02:00