0059: The calendar and the mail write through hz, after a confirmation¶
Context and Problem Statement¶
ADR 0028 gave calendar and mail read modes only, and
ADR 0042 signs gws in with read-only scopes. The maintainer asked
(2026-10-24) for a calendar module and a mail module people use every day: create, move, cancel and answer events;
draft, send and file mail; free/busy times and whole conversations. Which methods join the protocols, in which mode,
how do these writes relate to ADR 0038's approval in place and
ADR 0045's confirmation, and which scopes does the
Google adapter ask for?
Decision Drivers¶
- Tenet 3 (done, not nagged) and tenet 4 (humans own, with lineage): hz drafts, the person confirms.
- Tenet 9: the protocols stay vendor-neutral; no Gmail or Google Calendar vocabulary in the core.
- ADR 0028's growth rule and ADR 0026: a method added to a mode breaks that mode's adapters.
- ADR 0035: the narrowest scopes, per mode.
- Nothing is sent or changed that the person did not see: no double sends, no silent edits between seeing and sending.
Considered Options¶
- A
writemode oncalendarandmail, three reads, drafts checked in the core (chosen) - A vendor search method on
calendar: one more frozen method every adapter owes, for whatevents(window)and a filter answer. - Writes as operations of
heinzel-googleonly: ties the calendar and mail modules to one vendor.
Decision Outcome¶
Chosen option: both capabilities gain write; the reads the modules need join read; writes reach only publish
operations that declared them, and run after the person confirmed what they will do.
calendar:free_busy(people, window)(read, contract-shaped;peopleare comma-separated plain addresses, checked in the core byfree_busy_people, aCapability.checksentry like the mail query's);create_event(EventDraft),update_event(event, EventChange),cancel_event(event, notify),respond(event, response)(write).Eventgainslocation,response(the user's own answer, normalized:accepted,declined,tentative, orpendingwhile unanswered) andorganized_by_user. No search method.mail:thread(thread)anddraft(draft)(read, id reads for operations);create_draft(MailDraft),send_draft(draft, seen)andrelabel(thread, add, remove)(write).send_draftsends a draft that exists only if its message is stillseen, the one the person was shown, so an edit in between sends nothing.relabelknows the portable namesinbox(removing it archives) andunreadin every adapter; other names are the vendor's labels or folders. Nothing deletes mail or files it into spam or trash.- The drafts are checked in the core (
EventDraft,EventChange,MailDraft,plain_addresses): plain addresses only, at most 100, one-line titles and subjects without control characters, bounded texts. Adapters pass addresses they derive (a reply's recipients) throughplain_addressestoo. - Approval: for mail, the draft in the mailbox is the draft in place (ADR 0038):
draftreads its state andsend_draftis the approval. The calendar has no place to draft in: the confirmation is the approval,--confirmon an operation that otherwise only shows what it would do (as ADR 0047'sadd), and in a chat theActionGate, which holds every write. This amends ADR 0038 §2:place_draftanddraft_statebelong to capabilities that place drafts in documents, trackers and sheets, not tocalendarandmail. - Growth: the new reads break
calendar:readandmail:readadapters written before them. Before 0.1.0 none is released, so they are added at once (ADR 0026's deprecation cycle applies from the first release); the repository's test adapters gained them. - Google (amending ADR 0042): writes through
calendar events insert/get/patch/deleteonprimaryandgmail users drafts create/sendandusers threads modify; reads throughcalendar freebusy query,gmail users threads getandusers drafts get. A request body is--json <JSON>. A write is retried only after a rate limit (Google refused it); one that timed out or failed without a refusal (5xx, no answer) says it may have happened and to look first. A new event carries an id of hz's choosing, so a repeated insert is refused, never doubled. A recurring series' own id is refused for moves and cancellations: only one occurrence changes. Ids are checked against Google's alphabets before they reachgws. The scopescalendar.eventsandgmail.modify(which reads too and replacesgmail.readonly, and can't delete mail for good) are asked for only when the person lets hz write (hz setup google, "no" by default); a missing one names thegws auth loginthat adds it and how to keep the other scopes.
Consequences¶
- Good: the first everyday modules without a vendor in core code; another provider binds in.
- Bad: every future
calendarormailadapter owes more methods, and maps the portable label names. - Bad:
gmail.modifycovers all of a person's mail; it is opt-in, and the setup question says what it allows.
Confirmation¶
tests/unit/kernel/capabilities_test.py (modes, contract shape, no delete, the free_busy check),
tests/unit/kernel/calendar_test.py and tests/unit/kernel/mail_test.py (draft checks),
plugins/heinzel-google/tests/unit/{calendar,mail,gws,setup,contract}_test.py (requests, refusals, retries, scopes)
and plugins/heinzel-google/tests/integration/capability_test.py (only a publish operation that declared
calendar:write creates an event).