Skip to content

Commit 6ba19ff

Browse files
committed
docs: add jmap cairn entry for managing more domains
1 parent 08ea378 commit 6ba19ff

3 files changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
cairn: delta
3+
change: jmap-domains
4+
---
5+
6+
## ADDED Requirements
7+
8+
### Requirement: A JMAP account watches every domain it configures
9+
The JMAP backend SHALL watch mail, contacts and calendar events, each under the collection key its domain uses (`jmap.mailbox`, `jmap.addressbook`, `jmap.calendar`), of which at least one SHALL be given. It SHALL fire `on-message-*` and `on-flag-*` for mail, `on-card-*` for contacts and `on-event-*` for calendar events, and SHALL NOT offer `on-task-*`, the JMAP calendars draft having no task type. A hook naming a domain the account configured no collection for SHALL be refused when the configuration is read, naming the hook and the key it would need. All the domains SHALL share one session, one connection and one event stream, that being what JMAP offers over a protocol needing an account per domain.
10+
11+
#### Scenario: One account, three domains
12+
- **GIVEN** a JMAP account configuring `jmap.mailbox`, `jmap.addressbook` and `jmap.calendar`
13+
- **WHEN** a contact is edited
14+
- **THEN** `jmap.hook.on-card-changed` fires, over the same connection the mail watch holds
15+
16+
#### Scenario: A hook for a domain the account does not watch
17+
- **GIVEN** a JMAP account configuring `jmap.mailbox` and `jmap.hook.on-card-added`
18+
- **WHEN** the configuration is read
19+
- **THEN** it is refused, naming the hook and the `jmap.addressbook` it would need
20+
21+
## MODIFIED Requirements
22+
23+
### Requirement: An account watches one collection, one way
24+
An account SHALL watch the collection its backend names, and MAY name the one method it watches with. Neither SHALL be overridable from the command line: what an account watches is its configuration, and watching a second collection of the same domain is a second account, which is also how it gets its own hooks. A backend serving several domains MAY name one collection per domain, since the domains do not share an event name and each therefore already has hooks of its own; what they share is the connection and the credential, which is what a second account would waste.
25+
26+
#### Scenario: A second collection
27+
- **GIVEN** an account watching one mailbox
28+
- **WHEN** a second mailbox is to be watched
29+
- **THEN** it is a second account, with its own hooks, and no flag exists to ask for it
30+
31+
### Requirement: The collection belongs to the backend, under its own name
32+
Each backend SHALL take the collection it watches, required, under the name its domain uses: `imap.mailbox`, `maildir.mailbox`, `caldav.calendar` and `carddav.addressbook`, and `jmap.mailbox`, `jmap.addressbook` and `jmap.calendar` of which at least one. No account-level key SHALL name it, so an account block carries nothing that needs a backend to be understood. A hook SHALL template against the name the collection its event is about was configured under, `$id` being the one variable every backend means the same way.
33+
34+
#### Scenario: A mail hook naming its mailbox
35+
- **GIVEN** an account whose `imap.hook.on-message-added` summary reads `New mail in $mailbox`
36+
- **WHEN** a message arrives
37+
- **THEN** the notification names the mailbox from `imap.mailbox`
38+
39+
#### Scenario: A hook naming another domain's word
40+
- **GIVEN** an account whose `jmap.hook.on-card-added` summary reads `$mailbox`
41+
- **WHEN** the configuration is read
42+
- **THEN** it is refused, since a card is configured and templated as `$addressbook`
43+
44+
## REMOVED Requirements
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
cairn: change
3+
id: jmap-domains
4+
status: active
5+
created: 2026-08-22
6+
---
7+
8+
# Let one JMAP account watch its contacts and its calendar, not only its mail
9+
10+
## Why
11+
12+
The JMAP backend watches `Email` and nothing else. Its EventSource subscribes to one type, its rounds call `Email/changes` and `Email/get`, and `JmapHookConfig` declares four mail hooks, so `jmap.hook.on-card-added` is refused when the file is read. A Fastmail account watching mail over JMAP has to watch its addressbook and its calendar over CardDAV and CalDAV, against the same server, with a second and a third credential block.
13+
14+
That is backwards for the one protocol that does not need it. JMAP is the clean case for the domain split [hook-under-backend](../hook-under-backend/proposal.md) left open: `Email/changes`, `ContactCard/changes` and `CalendarEvent/changes` are separate method calls, so a change knows its domain for free. CalDAV had to be asked what its collection holds and, for a mixed calendar, read a member's content type to tell a VEVENT from a VTODO; JMAP is told, by which method answered.
15+
16+
The protocol crate is ready. io-jmap covers contacts (RFC 9610: `address_book_get`, `address_book_changes`, `contact_card_get`, `contact_card_query`, `contact_card_changes`) and calendars (draft-ietf-jmap-calendars-27: `calendar_get`, `calendar_changes`, `calendar_event_get`, `calendar_event_query`, `calendar_event_changes`). Nothing new has to be written below carillon.
17+
18+
And there is a payoff no other backend can offer: one session, one connection, one event stream covering all three domains. An account watching mail, contacts and a calendar over DAV holds three connections and three polls; over JMAP it can hold one stream that names which of the three moved.
19+
20+
## What
21+
22+
The `jmap` block takes a collection per domain it watches, each optional and at least one required: `jmap.mailbox` as today, plus `jmap.addressbook` and `jmap.calendar`. The hook table gains the domains' events: `on-card-added`, `on-card-removed` and `on-card-changed` beside the mail four, and `on-event-added`, `on-event-removed` and `on-event-changed`.
23+
24+
There is no `on-task-*`. JMAP calendars carry `CalendarEvent` and the draft has no task type, which is a protocol fact rather than a gap, and it is why CalDAV's `on-task-*` has no JMAP twin.
25+
26+
A hook naming a domain the account does not configure is refused when the file is read, the way a hook a backend cannot fire already is. It cannot be a serde refusal, since what is allowed depends on a sibling key rather than on the table's own shape, so it joins the template-variable check in `AccountConfig::validate`, naming the hook and the collection key it would need.
27+
28+
The EventSource subscribes to the types the account configured, and a round asks each configured domain what moved. The collection a hook templates against follows the event's domain rather than the backend: `$mailbox` for a message, `$addressbook` for a card, `$calendar` for an event, which makes `HookCollection` a per-event answer where it is currently a per-backend one.
29+
30+
## The invariant this bends, deliberately
31+
32+
[collection-under-backend](../collection-under-backend/proposal.md) settled that an account watches one collection, and that watching a second is a second account, "which is also how it gets its own hooks". A JMAP account watching three domains breaks the letter of that.
33+
34+
It keeps the reason. The rule exists so each watched thing has hooks of its own, and it still does: the domains do not share an event name, so `on-card-added` and `on-message-added` are as separate as two accounts would have made them. What is shared is the connection, the credential and the session, which is exactly what is worth sharing and what a second account would waste.
35+
36+
The alternatives were weighed and are worse. Splitting JMAP into `jmap`, `jmap-contacts` and `jmap-calendar` blocks would keep the invariant literally and match how DAV split, at the cost of three copies of one server and credential, three sessions and three event streams against a server that offers one; it throws away the only thing JMAP does better here. Requiring exactly one of the three keys per account would keep both the invariant and the strict serde refusal, and costs the same three connections for no gain over the split. Neither is worth the letter of a rule whose purpose survives.
37+
38+
## Not in this change
39+
40+
Discovery of which domains an account actually holds. The JMAP session lists its capabilities per account (`urn:ietf:params:jmap:mail`, `:contacts`, `:calendars`), so configuring a domain the server does not serve could be refused when the session is read rather than when the first round fails. That is a startup check like CalDAV's `supported-calendar-component-set`, and it wants its own change once there is something to check against.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
cairn: tasks
3+
change: jmap-domains
4+
---
5+
6+
- [ ] Take a collection per domain under `jmap`, at least one required
7+
- [ ] Add the card and event hooks to the JMAP table, with no `on-task-*`
8+
- [ ] Refuse a hook whose domain the account configured no collection for, at load
9+
- [ ] Subscribe the event stream to the types the account configured
10+
- [ ] Ask each configured domain what moved, and reconcile each against its own picture
11+
- [ ] Template the collection from the event's domain rather than the backend
12+
- [ ] Build, clippy and fmt green on every feature combination
13+
- [ ] Verify against a live JMAP server that a contact and an event fire their hooks
14+
- [ ] Fold the delta into the spec and log the change

0 commit comments

Comments
 (0)