From 0871d3291b223d24bf2caadb3ddba01a95dc3eb6 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 1 Sep 2026 11:00:04 -0700 Subject: [PATCH] Write down what the last week's features actually do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FEATURES.md had nothing on three of them. The scheduling panel and what it will not show, the filter editors no longer discarding work in silence, and where compose-as-new can be reached from — including the reply strip, which is the one a thumb finds. KNOWN-ISSUES.md gains the free/busy finding, which is the sort of thing that page exists for: it was checked against the live server rather than assumed, free/busy between accounts turns out to need no sharing set up, and a principal offers no route to its calendars at all. Also what the check did *not* settle, and which way ihasmail errs in the meantime. ROADMAP.md gains the two things left deliberately unbuilt: a scheduling view you can visit with no event in hand, and per-message actions from the message list on a touchscreen, where the gesture that would open them already means "select". --- FEATURES.md | 42 ++++++++++++++++++++++++++++++++++++++---- KNOWN-ISSUES.md | 2 ++ ROADMAP.md | 2 ++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index f4a10f0..17b8ce0 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -327,7 +327,9 @@ minimisable and maximisable; full-screen on mobile. bounced or went to a misspelled address. Recipients, Reply-To, subject, body and attachments come across as they stand; the Message-ID, date and threading headers do not, so it sends as a mail that has never been sent, and the - original is neither altered nor marked. + original is neither altered nor marked. In a message's own menu, in the list's + right-click menu, and behind the overflow on the reply strip at the foot of a + thread — which is the one a thumb finds on a phone. - **Send and archive**, and **archive on reply**, as options. ### Undo send, and scheduled send @@ -422,9 +424,34 @@ status (confirmed / tentative / cancelled), show-as (busy / free), visibility Invitations go out as iTIP when guests are added, replies come back and are applied to the event, and cancelling notifies the guests. Guests are added by -name or address with the same autocomplete the composer uses. Where the server -implements `Principal/getAvailability`, each guest's busy periods are looked up -for the day being scheduled. +name or address with the same autocomplete the composer uses. + +Where the server implements `Principal/getAvailability`, the event editor grows +a **scheduling panel**: a row per participant — you first, because scheduling +around everybody except yourself is how two things end up at the same time — +over the days the event spans, marked by the hour or by the day depending on how +wide that is. + +- **It is somewhere to put the event, not only something to read.** The pointer + shows the half hour it is over, and clicking moves the event there keeping its + length. +- **It steps backwards and forwards** a screenful at a time without touching the + event, and offers its way back. Clicking while stepped away moves the event to + where you clicked and brings the view with it. +- **A week is as far as it stretches.** Something running longer is not an event + anybody is hunting a free slot in; it says how many days it left out instead. + +**Whoever cannot be read is drawn hatched, never blank.** Free/busy is answered +per principal, and only accounts on this server are principals — so for a guest +at another domain there is nothing to read. Leaving them out, which is what +ihasmail used to do, is the one presentation that lies: a row with nothing in it +reads as a diary with nothing in it. A line under the grid says how many and +why. + +That limit is the protocol's rather than a gap waiting to be closed. A +`Principal` exposes no route to its calendars at all, so free/busy is not the +weaker permission — it is the only channel between two accounts, and it needs no +sharing to be set up first. ## Recurring events: series and single occurrence @@ -546,6 +573,13 @@ Also: script Stalwart will not accept is refused in front of you rather than failing quietly at delivery. - **Preview generated Sieve** for the visual rules. +- **Nothing is discarded without asking.** Both editors keep their edits until + you save, so every way out of the page used to throw them away silently — a + settings link, the app rail, even the Rules/Scripts switch — and with a + screenful of rules the save bar had already scrolled past the bottom of the + window. Leaving now asks, offering to save rather than making "leave without + saving" the easy answer, and the bar is pinned to the foot of the pane so + "Unsaved changes" is on screen whether or not the rules fit in it. - **Refuse to save from a script we only partly read** — a save is checked for completeness against the shape the generator emits, after a compressing proxy once truncated a download and the next save wrote the short version back over diff --git a/KNOWN-ISSUES.md b/KNOWN-ISSUES.md index bf85018..838533d 100644 --- a/KNOWN-ISSUES.md +++ b/KNOWN-ISSUES.md @@ -46,6 +46,8 @@ works the same way — and dropped where 0.15 was the whole subject. Support for - **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:submission` capability — `maxDelayedSend: 2592000` (30 days) and `FUTURERELEASE` among its `submissionExtensions`, and note it is the *account* capability, not the session-level one, which is empty. But the MTA only honours a hold when `futureRelease` is set under the session's MTA extensions, and [that setting defaults to `false`](https://stalw.art/docs/ref/object/mta-extensions/). With it off, Stalwart takes the `HOLDUNTIL` parameter, skips the hold and sends the message immediately **without an error** — the capability still says thirty days. So set `futureRelease` (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 a `forbiddenMailFrom` naming the limit. `npm run dev:mock:no-future-release` reproduces the silent-drop case. ihasmail asks for the delay the way JMAP requires — a `HOLDUNTIL` parameter on the envelope's `mailFrom`, since RFC 8621 makes `sendAt` read-only and server-derived — and files the held message in a **Scheduled** folder, because `onSuccessUpdateEmail` would 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: `HOLDUNTIL` taking RFC 3339 date-times again (0.16.16 had it wanting Unix timestamps), `EmailSubmission/query` on `undoStatus` agreeing with `/get` about held submissions, and `EmailSubmission/get` without `ids` iterating the right index. The hold itself is now **confirmed against the live 0.16.19** (2026-08-25), once `futureRelease` was set to `30d` there: a submission carrying a `HOLDUNTIL` ten minutes out came back `pending`, with `sendAt` equal to the time asked for and a `250 2.1.5 Queued` from the MTA, rather than going out at once. Worth repeating that the capability is no evidence either way — it advertised `maxDelayedSend: 2592000` and `FUTURERELEASE` while 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's `sendTo`/`email`; the organizer is `organizerCalendarAddress`, not `replyTo`; and a recurrence is a single `recurrenceRule`, not a `recurrenceRules` array. Addressed the RFC's way, `CalendarEvent/set` **keeps 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](https://github.com/Coffey-Labs/ihasmail/issues/26) reported. The array form of the rule is refused honestly, with `invalidProperties`, so recurring events could not be created at all and existing ones showed no repeat ([#30](https://github.com/Coffey-Labs/ihasmail/issues/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 with `null`, both work on the update path, as does RSVP — which patches `participants/{key}/participationStatus` (and `participationComment`) rather than sending the whole map. That patch had to be aimed at the base event: through 0.16.19 `CalendarEvent/set` refused a synthetic id with *"Updating synthetic ids is not yet supported"*, which is why RSVP resolves `baseEventId` first. 0.16.20 accepts one, so that resolution is now a choice rather than the only option — an RSVP aimed at an occurrence would answer for that date alone. It still resolves the base, which is the answer people mean. Adding a *new* participant by patch is refused as well (`Patch operation failed`), so a changed guest list is written as the whole `participants` property. One more thing to know when reading this code: an expanded occurrence carries a `recurrenceId` but *no* rule of its own, and `baseEventId` is 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. +- **Free/busy between accounts needs no sharing, and calendar contents cannot be reached at all.** These are the two halves of the same finding, and the second is what makes the first safe. **Confirmed live on 0.16.20 (2026-09-01)** against the deployed instance: `Principal/getAvailability` was called for all seven principals the directory returns, none of whose calendars are shared with the calling account, and every one was answered — no `forbidden`, no error of any kind, from a server that refuses a malformed call instantly. It returns real data rather than a polite empty list: the caller's own principal reported one busy period against the one event in the next sixty days. And a `Principal` carries only `id`, `type`, `name`, `description` and `email` — **no `accountId`** — so there is no handle with which to ask for anybody's calendars. Free/busy is therefore not the weaker of two permissions, it is the only channel between two accounts, and it is open by default. That is the right posture and worth recording, because a client that assumed sharing was a precondition would hide a working feature behind a setting nobody needs to touch. **One thing this did not settle**: the other six principals reported nothing over a nine-month window, which is equally consistent with "those accounts have empty calendars" — likely, since the session reaches one account — and with "an unreadable principal answers with an empty list rather than an error". Distinguishing them needs a second account with an event in it, and until somebody has one, ihasmail assumes the pessimistic reading everywhere it matters: a participant it cannot read is drawn as unknown rather than as free. + - **An override can move an occurrence, and then `start` and `recurrenceId` mean two different times.** The slot stays where the rule put it and only the clock time moves. **Confirmed live on 0.16.20 (2026-08-31)**: one occurrence of a weekly 09:00 series moved to 14:00 came back `start: 2027-06-14T14:00:00` with `recurrenceId` still `2027-06-14T09:00:00`. This is the right behaviour and it is the reason `recurrenceId` is the handle ihasmail holds: it is the one name for an instance that survives *both* a renumbering and a move, so a mutation can always be re-resolved from it. Worth recording because the mock got it wrong in the other direction — it overwrote an override's `start` with the slot time, so a moved occurrence did not move, and per-occurrence *time* editing looked broken against the mock and correct against the server. Found by asking a real server rather than by reading the mock, which is the only way this kind of disagreement ever surfaces. - **A synthetic id is only true until the next write, and a stale one is wrong rather than invalid.** Stalwart's expanded-occurrence ids encode a position in the series, and writing a `recurrenceOverrides` entry adds a component that renumbers it. **Confirmed live on 0.16.20 (2026-08-31)**: a five-week series came back as `e i m q u` over 03-01 … 03-29; one override written to 03-08 left the *same five ids* addressing 03-01, 03-15, 03-29, 03-08 and 03-22. Nothing was rejected and nothing reported a change — `i` simply meant a week later than it had a moment earlier. So an id cached across a write silently points at another date, and a delete meant for one occurrence removes a different one. This is the second time the same shape of problem has cost a live debugging session, and it is worth saying plainly why it is dangerous: the failure is not a `notFound` a client would notice, it is a confident answer about the wrong day. ihasmail therefore never mutates an occurrence by an id it is holding. `recurrenceId` is the stable name for a slot in a series — it is the date — so `updateEvent` and `destroyEvent` look the current id up by it immediately before they act, and refuse outright if the date is no longer in the series rather than falling back to the id in hand. The mock renumbers too, by a different permutation to the real server's but with the property that matters, since a mock that kept ids stable would agree with precisely the belief that is wrong. diff --git a/ROADMAP.md b/ROADMAP.md index a9bc8a6..8ad1209 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,6 +9,8 @@ the rest is here because the answer is "no", not "not yet". See [KNOWN-ISSUES.md](KNOWN-ISSUES.md) for what is built but worth knowing about. - **Sharing a mail folder.** Stalwart stores the share and never delivers it; see [KNOWN-ISSUES.md](KNOWN-ISSUES.md). Withdrawn until the server does something with it. Sharing files, calendars and address books is unaffected and works. +- **A scheduling view of its own**, for asking "when is everyone free next week?" without an event in hand. The grid itself is built and lives in the event editor — a row per participant, steppable, and clickable to place the event — which is where the question gets asked while you are arranging something. What is not built is the same thing as a destination you can visit with nothing in progress. Came out of [#172](https://github.com/Coffey-Labs/ihasmail/issues/172), which asked for a separate view and is closed by the panel: the reasoning for putting it in the editor is that a separate surface can only ever tell you a time you then retype, whereas one beside the event can set it. It stays here rather than in the tracker because nobody has yet said they want to ask the question on its own. +- **Per-message actions from the message list on a touchscreen.** Reply, Forward and compose-as-new are on the list row's context menu, which is a right-click — and holding a row on a phone starts selection instead, so none of them are reachable there. They are all available inside a thread, which is where the actions on a single message belong; what is missing is the shortcut from the list. Fixing it means deciding what a long press should do when it already means something, which is a bigger question than the actions themselves. - Snooze (nothing in JMAP or Stalwart supports it, and ihasmail never stores a password, so nothing could act on a mailbox while you are away) - **A translation anybody has checked.** The translations themselves shipped on 2026-08-31 and are no longer on this page: nine of them, alongside English, and the extraction that had always been the hard half is done — see [FEATURES.md](FEATURES.md#interface-language). What is *not* done is the other half, and it is the half that cannot be bought or automated. All nine were produced by AI against standard dictionaries and **not one has been read by anybody who speaks the language**, which is exactly where a bad translation does harm rather than merely looking untidy. They ship marked Beta, with that said in Settings and a link for reporting anything wrong, because shipping them quietly would ask people to trust text nobody has checked. A language loses the Beta mark when a speaker reads it and says so — a deliberate act by a person, not something a coverage percentage earns. If you speak one of them and are willing to read a few hundred strings, that is the single most useful thing anyone could contribute right now. - **Right-to-left languages.** Arabic, Hebrew and Persian are held back deliberately, and not for want of translators. RTL is bidi and layout work throughout — mirrored panes, gesture directions, icon sides, the message list's own geometry — and a catalogue without it produces a page that is translated and unusable. Adding one is not another entry in the picker.