Commit Graph
8 Commits
Author SHA1 Message Date
Linus Rath 5a553a315b fix: report refused read changes and re-register push at its limit
Follow-ups to the review of the push, keyword and identity work:

- A send that names no sender, like the List-Unsubscribe mailto: of the
  unified view, fell back to the first identity in server order, which
  on Stalwart 1.0 is a group's. It now takes the login's own identity,
  with addresses compared regardless of case.
- Re-register creates the replacement subscription before it destroys
  the old one, so at Stalwart's limit (15 per account by default) it
  failed with `overQuota`. It now frees the old record's place and
  tries once more.
- On 1.0 the push settings note goes by the emailpush capability alone
  and ignores refusals remembered under 0.16. On 0.16 a group the user
  belongs to is recognised by its identity, so a member without
  calendars, contacts or files keeps the group's junk filter and is no
  longer told the group sends no notifications.
- Plugin imports can no longer set Stalwart 1.0's attachment keywords,
  and `keywords.discover` leaves them out.
- Batch mark-as-read showed messages the server refused as read. It now
  updates only what was written and reports the rest, and thread
  expansion and category-tab moves keep what a partly refused batch
  did write.
- Refused read and star changes from shortcuts, the list, the viewer
  and Pro tabs show "Failed to update email" instead of an unhandled
  rejection. The automatic mark-as-read on opening stays quiet.
- A tag whose id predates the 128-byte limit can have its colour
  changed again.
- The state-regression guard notes that a multi-node 1.0 cluster can
  trip it harmlessly, at the cost of one extra full fetch.
2026-09-25 21:57:11 +02:00
Linus Rath d7004b3851 fix: report refused tags and keep Stalwart 1.0's attachment keywords intact
Stalwart 1.0 puts `$hasattachment` or `$hasnoattachment` on every
message and derives `hasAttachment` from them alone. It also refuses
keywords over 128 bytes (0.16 cuts them to 128 characters) and more than
100 keywords on one message. The keyword writers ignored Email/set
`notUpdated`, so a refused tag showed until the next reload and then
vanished.

- `markAsRead`, `batchMarkAsRead`, `toggleStar`, `updateEmailKeywords`,
  `setKeyword`, `removeKeyword` and `batchUpdateKeywords` check the
  Email/set outcome, and the errors carry the JMAP type. Tagging from
  the list or the viewer shows "Tagging failed"; a refused category-tab
  move rejects the plugin's promise and skips the categorize hooks.
- Tag ids are capped at 121 characters (128 bytes with `$label:`).
- The attachment keywords are hidden in view source, the plugin
  projection and the `getKeywords` fallback labels, reserved for
  category tabs, and refused by the plugin keyword API. Email/copy and
  the cross-server import still carry them over.
- A tag comes off by leaving its key out of the full keyword map instead
  of being sent as false, which RFC 8621 does not allow;
  `updateEmailKeywords` also drops the false entries local updates leave
  behind (`$seen: false` after marking unread).
- The automatic mark-as-read no longer leaves an unhandled rejection
  when the server refuses it.
2026-09-25 19:24:30 +02:00
Ctaoist b09d5c857d fix: make label reordering atomic 2026-08-13 21:17:05 +08:00
Ctaoist 6e792f8834 feat: expose label reordering to extensions 2026-08-13 10:59:45 +08:00
Ctaoist cf89eaad27 feat: expose gateway keywords to extensions 2026-08-12 21:40:02 +08:00
Ctaoist ddfd9e43e6 feat: expose JMAP keyword helpers to extensions 2026-08-12 20:43:55 +08:00
Ctaoist f2c379b929 feat: expose label settings to extensions 2026-08-12 15:38:05 +08:00
Linus Rath f056db5a05 fix(plugins): make attachment offload workable and keep the write gate closed
Review follow-up to #751.

The offload path could not actually run: onBeforeBlobUpload goes through
HookBus.transform, which capped every handler at the 5s default. A handler
that pushes the file to an external store blows past that, and on timeout
the error is swallowed, the original file id is kept, and the composer
attaches the big file anyway - silently - while three such timeouts in a
minute auto-disable the plugin. Buses can now carry their own per-handler
budget and this one gets 120s. The same ceiling existed inside the sandbox,
where http.post/http.fetch were bounded by the 30s default API timeout;
those two are transfers rather than round-trips and now get 120s as well.

PRIVILEGED_ONLY_METHODS listed 'upfiles.set', which matches no dispatched
method - the dispatcher calls it 'upfiles.save'. So the write was never
tier-gated despite the comment saying it was, and an untrusted plugin with
email:blob-write could replace the bytes of a file the user was about to
send. Fixed the name. Relaxing the read while that stayed open was the
wrong order.

Moving upfiles.get to email:blob-read is kept, but it is a wider grant than
the PR described: onBeforeBlobUpload hands the file id to every registered
handler, so the id is not a secret and any untrusted plugin holding
email:blob-read can read every file the user attaches. The consent copy now
says that; email:blob-write's copy was describing the read and now describes
the replace it actually gates.

Also in the composer offload path:
- sanitize the handler's html before it reaches the document and the sent
  message, and insert it through the editor at the caret rather than
  concatenating it after the signature and quoted block
- honour an abort that lands while the handler is offloading
- delete the right staged file when the handler re-saved it under a new id
  via upfiles.save, which otherwise stranded a copy in IndexedDB forever
2026-08-06 14:08:07 +02:00