Sharing a mail folder does nothing. `Mailbox/set` takes the `shareWith` map, `Mailbox/get` reads it back, and the folder never appears for the account it was shared with -- confirmed on the live 0.16.19 with a folder shared read-only to another account on the same server, which never saw it. Stalwart's sharing documentation lists calendars, address books and file storage; mail folders are not among them. Nothing anywhere reports a failure, so a client that trusts what it reads back shows the share as live for ever, which is what happened. The entry point is withdrawn. Address book sharing goes with it on a report that it behaved the same way -- not reproduced, and contradicted by Stalwart's own docs, so that one is expected back; it is out because offering a share nobody can verify was worse than the gap. Files and calendars are untouched. Removing a share was impossible, for a reason worth writing down. The dialog rendered the list of who a thing was shared with *inside* the branch that runs when the directory has principals to offer. A server with `allowDirectoryQueries` off returns none -- that is the default, and it is how these shares came to be made in the first place -- so the dialog showed one line of hint and nothing else. The share was there, and there was no way to see it, let alone remove it. The list is now rendered whatever the directory says; only the control for adding somebody new depends on having somebody to add. So the withdrawn entry points do not strand what they created: a folder or book already shared still offers "Stop sharing", which is the one thing you want when the share is invisible everywhere else. The API was never the problem, which is worth recording since it was the first guess: `shareWith: null` is accepted and clears the map, tested against the live server on the stuck folder, which is now unshared.
16 KiB
Known issues and pending QA
What was checked, against which server, and when. For a failure you are hitting right now, start with Troubleshooting; for what is not built yet, see ROADMAP.md.
The live instance runs 0.16.19, and as of 2026-08-26 there is nothing left pending: every entry below has been exercised against it. What remains here is not a list of unknowns but of things worth knowing — where Stalwart departs from a spec, where a setting has to be turned on for a feature to work, and what ihasmail deliberately does not do.
Entries keep saying what was checked and when, because this section has been wrong before: the 0.16 registry path was once recorded as verified live when a capability looked for in the wrong place meant it had never run at all.
Some entries record what a live 0.15.5 proved before that server was
upgraded on 2026-08-25. They are kept where the finding is about ihasmail
rather than about 0.15 — a byte cap that still applies, a flow that still
works the same way — and dropped where 0.15 was the whole subject. Support for
0.15 was removed on 2026-08-26; the last release that runs on it is tagged
stalwart-0.15-support.
- Delete all spam destroys, and does not pass through Deleted Items — this is the point of the feature and the thing worth checking on a real server, since a folder that empties into another folder has solved nothing.
Email/set destroy, walked a page at a time so it survivesmaxObjectsInSetthe way emptying Deleted Items already had to. Confirmed live on 0.16.19 (2026-08-26): Junk Mail emptied and Deleted Items stayed empty afterwards. There is no undo, which is why all three entry points share one dialog that says so. Only Deleted Items and Junk Mail can be emptied this way, enforced in the store rather than only hidden in the menus. - Sharing a mail folder is accepted and does nothing.
Mailbox/setwith ashareWithmap is applied,Mailbox/getreads it back, and the folder never appears for the account it was shared with — confirmed live on 0.16.19 (2026-08-27) with a folder shared read-only to another account on the same server, which never saw it. Stalwart's own sharing documentation lists calendars, address books and file storage; mail folders are not among them. Nothing reports a failure at any point, which is the whole problem: the share is stored, so a client that trusts what it reads back shows it as live for ever. The entry point is withdrawn. A folder that is already shared still offers Stop sharing, because a share nobody can see is exactly the one you want to be able to clear, and there is no other way to. File sharing is unaffected and works end to end. - Address book sharing is withdrawn without being disproved. It was taken out alongside mail folders on 2026-08-27, on a report that it behaved the same way, and that report has not been reproduced: there was no shared address book left on the account by the time anyone looked. Stalwart documents address books as shareable, so the expectation is that this one does work and the entry point should come back — it is out because offering a share nobody can verify was worse than the gap. Testing it needs two accounts and someone to confirm the book arrives. Stop sharing remains for a book already shared.
- Read receipts are built here, not by the server — JMAP has an extension for them, RFC 9007's
MDN/send, and Stalwart does not implement it:urn:ietf:params:jmap:mdnis not among its capabilities. So ihasmail assembles themultipart/reportitself and sends it the long way round — raw MIME uploaded as a blob,Email/import, thenEmailSubmission— which is also why the receipt lands in Sent, where it honestly belongs. Non-ASCII parts are base64 rather than8bit, so nothing depends on 8BITMIME surviving every hop. There is deliberately no "always send" setting: a receipt confirms to whoever asked that the address is live and when it was read, to an address of the sender's choosing, so each one is a decision. Verified against the mock end to end (upload, import, submit,$mdnsent), and confirmed live on 0.16.19 (2026-08-26): a receipt asked for by a real sender was assembled, uploaded, imported and submitted, landed in Sent, and set$mdnsentso a second look does not offer to send another. - Where 0.16 advertises
urn:stalwart:jmap— not where a JMAP client would look, and this now decides whether a sign-in is allowed at all. Stalwart builds the session-levelcapabilitiesfrom a fixed list (Session::new, plus WebSocket) that has never contained this capability, in any 0.16.x from 0.16.0 to 0.16.19. It hands it out per-account instead, so it appears inprimaryAccountsand in each account'saccountCapabilities. ihasmail tested for it incapabilitiesalone, which made every real 0.16 server read as older than 0.16 — and that one check drove three things: self-service credentials fell back toPOST /api/account/auth, which 0.16 removed, so password changes, 2FA and app passwords all failed with "this mail server does not offer self-service credential management"; About reported the wrong generation; and Files took the older code path. It now looks in all three places, and is covered by tests on each. Worth restating plainly, because the stakes went up when 0.15 support was dropped: there is no longer a fallback path for this check to be wrong into. Getting it wrong now refuses every sign-in against a perfectly good server — a loud failure rather than a quiet misrouting, which is the trade the removal was making. - HTML signatures — Stalwart caps a signature at 2047 bytes (
value.len() < 2048on a Rust string, so UTF-8 bytes, not characters). ihasmail compacts pasted HTML, moves images to Files and, if still too large, keeps the full signature in Files behind a short marker; other clients see a text fallback. Confirmed live on 0.15.5 (2026-08-24): oversized, non-ASCII and inline-image signatures all save, and a test message arrived intact at Gmail with the logo inline. - Settings live in the account's Files, not the browser — every preference used to sit in
localStorage, so none of them followed anyone between devices. The sharpest edge was the default identity: with none set the address that sorts first wins, so someone who set it at work found it unset at home and mail went out from an address the recipient might not recognise (#54). They are now asettings.jsonin theihasmailfolder in JMAP Files, beside the signature images already kept there — which keeps ihasmail itself stateless: no volume, no database, nothing to back up separately, and the settings are covered by whatever backs up the mail store.x:AccountSettingswas the other candidate and does not fit; its schema islocale/timeZone/descriptionwith no free-form field, and writing it needssysAccountSettingsSet, where the built-in user role carries only the…Gethalf.localStoragestays on as a cache rather than the source of truth, so the first frame paints from it and the file corrects it a moment later; a browser with no cache shows defaults for that one frame, which is the trade for not gating the whole app on a round trip. Settings that describe this screen or browser deliberately stay local — list-pane sizes, density, font size, sidebar state, and the notification toggles, which track a permission the browser grants per-device and would be a claim about somewhere else it cannot make. That split is written as a list of exceptions, so a setting added later syncs by default. Writes are coalesced behind a three-second debounce, sinceupdate()fires on every frame of a splitter drag, and a tab going away or a sign-out flushes first. Theihasmailfolder is now hidden from the Files view, contents and all: hiding the folder alone would be worse than showing it, because the tree attaches a node whose parent is missing to the root, so the signature images — visible there since signatures shipped — would have spilled into the top level. Confirmed live on 0.16.19 (2026-08-26): settings set in Chrome came back on a fresh login in Firefox and in an incognito session, both of which start with an empty cache, so each read the account's file rather than anything local. Confirmed again on the deployed instance rather than only a pre-deployment build. Requires 0.16, which ihasmail now requires everywhere —FileNode/querycannot see directories before that, and sign-in refuses an older server outright. Two limits worth knowing: conflicts are last-write-wins, and a change made on one device does not reach another that already has ihasmail open until it signs in again. - Files on 0.16 — the pre-0.16 quirks this entry used to describe are gone with the support for them:
FileNode/querymasking directories out of its own results,nodeTypenot existing, and rights being a singlemayWrite. What is left is what has actually been exercised on 0.16.19. Finding and creating a folder, creating a node withnodeType, uploading and downloading its blob, and pointing an existing node at a new one all ran live on 2026-08-26, as a side effect of the settings file. Rename, move and delete are confirmed live on 0.16.19 (2026-08-26) as well, which closes this out: what had been confirmed on 0.15.5 (2026-08-24) was the older code path, and that path no longer exists. Two fallbacks went with the removal and are worth knowing about:ensureFolderandfindInFoldernow filter onparentId/isTopLevelalone and match names client-side, sincenameis not a filter Stalwart is known to implement and one it does not know fails the whole query; and a refused filter or sort no longer drops the view into fetching every node in the account, which would have hidden a real fault behind a performance cliff nobody would notice. - Self-service credentials — the registry path is confirmed live against Stalwart 0.16.19 (2026-08-25): app passwords created and revoked, password changed, 2FA enabled and disabled, with the browser session surviving the switch to an app password. The 0.15 REST path was confirmed live too, on 0.15.5 (2026-08-24), and has since been removed along with the rest of 0.15 support. The mock enforces the same rules the real server does (current password required, password policy, a TOTP code on every request once 2FA is on, app passwords exempt from it). Password changes are refused by Stalwart for accounts backed by an external directory (LDAP/SQL/OIDC); the server's own message is shown when that happens.
- Scheduled send needs one setting turned on, and says nothing when it is off. Stalwart advertises the delay in the account's
urn:ietf:params:jmap:submissioncapability —maxDelayedSend: 2592000(30 days) andFUTURERELEASEamong itssubmissionExtensions, and note it is the account capability, not the session-level one, which is empty. But the MTA only honours a hold whenfutureReleaseis set under the session's MTA extensions, and that setting defaults tofalse. With it off, Stalwart takes theHOLDUNTILparameter, skips the hold and sends the message immediately without an error — the capability still says thirty days. So setfutureRelease(to the longest hold you want to allow) before relying on this; a value shorter than 30 days is fine, and a request past it is refused honestly, with aforbiddenMailFromnaming the limit.npm run dev:mock:no-future-releasereproduces the silent-drop case. ihasmail asks for the delay the way JMAP requires — aHOLDUNTILparameter on the envelope'smailFrom, since RFC 8621 makessendAtread-only and server-derived — and files the held message in a Scheduled folder, becauseonSuccessUpdateEmailwould otherwise drop it in Sent the moment the submission is created. Nothing moves it out when the hold expires, so ihasmail reconciles the folder on the way in: released messages to Sent, cancelled ones back to Drafts. Three fixes this depends on landed in 0.16.17, below the live instance's 0.16.19:HOLDUNTILtaking RFC 3339 date-times again (0.16.16 had it wanting Unix timestamps),EmailSubmission/queryonundoStatusagreeing with/getabout held submissions, andEmailSubmission/getwithoutidsiterating the right index. The hold itself is now confirmed against the live 0.16.19 (2026-08-25), oncefutureReleasewas set to30dthere: a submission carrying aHOLDUNTILten minutes out came backpending, withsendAtequal to the time asked for and a250 2.1.5 Queuedfrom the MTA, rather than going out at once. Worth repeating that the capability is no evidence either way — it advertisedmaxDelayedSend: 2592000andFUTURERELEASEwhile the setting was still off. Only a submission tells you. The rest of the journey is confirmed live too (2026-08-26): a hold expired and was delivered, and the Scheduled folder reconciled on the way in — a released message moved to Sent, a cancelled one back to Drafts. Nothing in Stalwart does that moving, so if ihasmail is never opened again the message still goes out; it is only the folder that waits to be tidied. - Stalwart 0.16 and RFC 8984 disagree about the calendar vocabulary, and the server only says so half the time. A participant's address lives in
calendarAddress, not RFC 8984'ssendTo/email; the organizer isorganizerCalendarAddress, notreplyTo; and a recurrence is a singlerecurrenceRule, not arecurrenceRulesarray. Addressed the RFC's way,CalendarEvent/setkeeps the event and discards the whole participant map without an error — guests disappeared on save and no invitation was ever sent, which is what #26 reported. The array form of the rule is refused honestly, withinvalidProperties, so recurring events could not be created at all and existing ones showed no repeat (#30). ihasmail now writes Stalwart's names and reads either, and the mock refuses what the real server refuses, since advertising the RFC spelling is precisely how this got as far as a live server. Verified against 0.16.19 on 2026-08-25, end to end: participants, organizer and rule all survive a create, an update and a re-read; an invitation to an external Gmail address arrived as an invite card, and the decline came back and was applied to the event (needs-action→declined, sequence 1). Cancelling the event notified the guest too. Adding guests to an event that had none, and clearing them again withnull, both work on the update path, as does RSVP — which patchesparticipants/{key}/participationStatus(andparticipationComment) rather than sending the whole map. That patch has to be aimed at the base event:CalendarEvent/setrefuses a synthetic id with "Updating synthetic ids is not yet supported", which is why RSVP resolvesbaseEventIdfirst. Adding a new participant by patch is refused as well (Patch operation failed), so a changed guest list is written as the wholeparticipantsproperty. One more thing to know when reading this code: an expanded occurrence carries arecurrenceIdbut no rule of its own, andbaseEventIdis set on everything an expanded query returns — a one-off included, whose own id differs from its base — so neither is a test for recurrence. - Recurring events: colour/category/edit/delete apply to the whole series (per-occurrence overrides aren't supported by the server yet).
- Editable date boxes are always Gregorian and in Latin digits, even for locales whose display uses another calendar or numbering system (
fa-IR,th-TH,ar-EG) — they keep the locale's field order and separator, but a Buddhist-era year in a text box does not round-trip against the Gregorian calendar grid. Non-Gregorian calendar support is not implemented. - The account locale is read from
x:AccountSettings/get, whose permission the built-in user role has, falling back tox:Account/get(which needs the admin-onlysysAccountGet). Both are Stalwart 0.16 methods: on older servers neither is reachable — they do not implement the registry and reject a request that so much as names theurn:stalwart:jmapcapability — so there the locale still falls back to the browser's and can be chosen by hand. Confirmed live on 0.16.19 (2026-08-25), once the capability was looked for where Stalwart advertises it; a locale request that is merely refused no longer downgrades the detected generation.