• v0.11.0 a978d5ac19

    v0.11.0 — calendar management, and iCloud Mail M1
    All checks were successful
    CI / gate (push) Successful in 1m37s
    CI / image (push) Successful in 1m4s
    Stable

    joeyrendar released this 2026-08-11 22:09:03 -05:00 | 64 commits to main since this release

    Recorded retroactively on 2026-08-12. The iCloud Mail, tenant #2 section below
    describes work that was already in this tag and went undescribed — the entry as published
    covered only calendar management. It is written here, against the version that actually
    carries the code, rather than under a later heading that would misdate it. This is the
    drift the no-[Unreleased] rule exists to prevent, and it happened anyway because the
    mail commits landed between a release and its successor without a version of their own.

    Calendar management, behind CALENDAR_ADMIN. Creating, renaming and deleting whole
    collections rather than the events inside them — and the delete is the first thing here
    that cannot be undone by any later call. A wrong delete_event is repaired by creating
    the event again; a wrong delete_calendar takes every event in it, on every device, and
    CalDAV has no trash.

    Like attendees before it, this shipped only because a probe cleared it first. iCloud was
    asked on 2026-08-11 whether it permits MKCALENDAR, PROPPATCH and DELETE for an
    app-specific password. It does — and the same probe found the trap the implementation is
    now shaped around.

    Added

    • create_calendar, update_calendar and delete_calendar, registered only when
      CALENDAR_ADMIN is set. Deliberately not nested under WRITABLE_CALENDARS the way
      the attendee tools are. That setting is about the events inside a collection and this
      one is about the collection; a deployment can reasonably add events to a shared calendar
      without being allowed to delete it, and creating a scratch calendar needs write access
      to nothing that already exists. Parallel grants, not a ladder.

    • A two-step delete. delete_calendar takes expected_name, matching the shape of
      delete_event — and expected_event_count, which does not exist anywhere else. Called
      without a count it refuses and reports the real one. That refusal is the feature:
      the model cannot cheaply look the number up, so serving it through a refusal is what
      puts "this will delete 42 events" in front of a human before anything happens. The plan
      had this open as "deleting a calendar should be harder to reach than deleting an event,
      but that is not a design"; this is the design.

    • count_events, a Depth: 1 PROPFIND that counts resources without reading them.
      Bodies run to megabytes on a busy calendar and the count is all that is wanted.

    Fixed

    Both of these were found by writing the tests, not by running the code, and both would
    have presented as something other than what they were.

    • calendar-home-set was read as the first href in the multistatus — which is the
      href of the request URI, so it resolved to the principal. New calendars would have
      been created under the principal collection: no error, and nothing anyone could find.
      _organizer_address never hit this only because its mailto: filter excludes that href
      by accident.

    • The collection-level operations were filtered by the CALENDARS allowlist. A
      calendar created just now is by definition not in the allowlist, so create_calendar
      reported its own success as a failure — on every deployment that sets CALENDARS, which
      is all of them. Renaming a calendar out of the allowlist failed the same way. These
      operations now read the account rather than the readable subset, which also closes the
      duplicate-name hole: two collections sharing a name are indistinguishable in every later
      response, and the allowlist has no opinion on what exists.

    Notes

    • DELETE returns 204 whether or not it deleted anything. Observed live. Against a
      URL built from calendar-home-set it answered 204 and left the collection in place;
      against the href the calendar list reports, the same 204 removed it. So collections are
      addressed by the href the server gave us — the 0.8.1 lesson one level up — and every
      delete reads the collection back afterwards. The status is not evidence.

    • A calendar created here is not readable until an operator adds it to CALENDARS.
      The tool says so in its success envelope, because otherwise the user is looking at a new
      calendar that list_events insists is empty.

    Added — iCloud Mail, tenant #2 (M1, read-only)

    The second tenant, and the first thing here that is not a calendar. Read-only by
    construction: nothing this surface exposes can mark, move, delete or send anything, and the
    settings that would grant those (MAIL_WRITABLE_FOLDERS, MAIL_SEND_ENABLED,
    MAIL_FOLDER_ADMIN) deliberately do not exist yet — an unread setting reads as a granted
    permission.

    • Six read tools over IMAP: list_folders, get_unread_count, list_messages,
      search_messages, get_message, get_attachment. New package mcp_gateway/icloudmail/,
      mirroring the calendar's layout, with messageid.py inheriting eventid.py's reasoning
      wholesale.

    • TENANT picks the surface, one process per tenant, never both. Defaults to
      icloudcalendar so the calendar service needs no edit. Registering both tool sets in one
      process would let mail's ALLOWED_SUBJECTS hand out the calendar and vice versa, which is
      the whole boundary — so it is a process-level choice, not a registration-level one.

    • MAIL_FOLDERS is the read scope, starting at INBOX. A folder not named there is
      invisible, not empty: list_folders omits it and every read tool refuses the name
      rather than answering zero. "There is nothing in that folder" is a sentence the model will
      repeat to you as fact.

    • Nothing is marked read by being read. readonly=True on SELECT and BODY.PEEK on
      fetch, verified explicitly against the live mailbox rather than assumed from the flags.

    • Its own hostname, icloudmail.mcp.jrendar.org, with its own Forgejo OAuth
      application, client id, signing key, ALLOWED_SUBJECTS and NFS-backed OAuth store — a
      sibling of the calendar's, never nested inside it. Probe 0 settled why on 2026-08-11: a
      token is bound to PUBLIC_BASE_URL + MCP_PATH and a hostname has exactly one /token.

    • deploy/nginx/icloudmail.conf, a near-copy of the calendar's block, kept in step by
      the rule in its own header rather than by a shared file.

    Notes on the mail tenant

    • The IMAP username is not the Apple ID. Probe 1 found the account's Apple ID is a
      third-party address while IMAP authenticates as the @icloud.com mail address. There is
      deliberately no fallback: it would work on some accounts and fail here as
      [AUTHENTICATIONFAILED], which reads as a wrong password and sends you after the one
      value that was right.

    • Verified through the tool layer against the live mailbox, over real HTTP with bearer
      auth and driven by an MCP client rather than by Python calls, so registration, argument
      coercion and the JSON envelopes were all exercised. Six refusals were checked explicitly,
      because the failure that matters is an error arriving as an empty success — every one
      returned an error envelope carrying no results.

    • The log holds counts and error type names only. No folder name, subject, address or
      body, including in the boot summary, which reports the mail address as a bare domain.

    Downloads