car-integrations 0.52.0

OS-native account-bound integrations (Calendar, Contacts, Mail) for CAR
Documentation
# car-integrations

Account-bound integrations for [Common Agent Runtime](https://github.com/Parslee-ai/car) — Calendar, Contacts, Mail, Messages, Notes, Reminders, Photos, Bookmarks, Files, Keychain.

## What it does

The logical capabilities are the same on every OS; the backends are not. v1 defines the **release contract** — stable return shapes that carry explicit `available` + `backend` fields so downstream apps branch cleanly while backends light up incrementally.

| OS | Calendar | Contacts | Mail | Messages | Other Apple account surfaces |
|----|----------|----------|------|----------|------------------------------|
| macOS | EventKit (`EKEventStore`) | Contacts.framework | Mail.app automation | Messages.app automation | Notes/Reminders/Photos app automation, Safari bookmarks, iCloud Drive paths, Keychain |
| Windows | MS Graph + Outlook MAPI | Windows.Contacts + Graph | MS Graph + MAPI | Not modeled | Not modeled |
| Linux | Evolution DS + CalDAV | Evolution DS + CardDAV | Evolution DS + IMAP | Not modeled | Not modeled |

## Dependencies (and honest gaps)

Full operation requires:
- **`car-secrets`** — where credentials and tokens live
- **`car-accounts`** — to know which account a call is bound to
- **`car-permissions`** — to preflight OS consent before the side effect

The macOS Calendar, Contacts, Mail account discovery, and Messages account discovery paths are implemented. Backends still return `available:false` with an explicit reason when macOS denies TCC access or an OS service is not configured.

### Mail read surface

| Capability | macOS (Mail.app / JXA) | Microsoft Graph |
|---|---|---|
| `list_accounts` | `Mail.accounts()` | the signed-in M365 account |
| `list_inbox` | per-account unread/total | `/me/mailFolders/inbox` |
| `list_mailboxes` | recursive `mailboxes()` walk | `/me/mailFolders` |
| `list_messages` | bulk array property gets on one mailbox | `/me/mailFolders/{id}/messages` |
| `message_body` | `content()` of one message | `/me/messages/{id}?$select=body` |
| `send` | `OutgoingMessage` | `/me/sendMail` |

`list_mailboxes` / `list_messages` / `message_body` closed Parslee-ai/car-releases#84. Before them the read path could only reach INBOX and carried no message id, so a caller whose server-side rules file mail out of INBOX got an empty result that reads as "nothing there" rather than "cannot see there" — and had to shell out to `osascript`, which splits the macOS Automation TCC identity between CAR-spawned sends and caller-spawned reads.

Two properties worth knowing before extending them:

- **Message ids are composite on macOS.** A Mail.app numeric message id is only addressable inside one mailbox of one account, so the wire id is `mailapp:<b64url account>:<b64url mailbox path>:<numeric>` (Graph ids are just `msgraph:<id>`). `encode_message_id` / `decode_message_id` are the only supported readers.
- **The message walk uses bulk array property gets, not a per-message loop.** Reading `subject`/`sender`/`dateReceived`/`readStatus`/`id` off the mailbox's `messages` specifier is five Apple Events for a mailbox of any size; reading them per message is one Apple Event *per message per field*, which is what puts a full mailbox scan near the 15s JXA timeout. Per-message reads (recipients, bodies) happen only for the rows that survive the `limit`.

Messages chat listing falls back to `~/Library/Messages/chat.db` when Messages.app automation cannot enumerate chats. macOS protects that database behind Full Disk Access, which must be granted to the host app by the user; there is no entitlement that can silently grant it.

## Where it fits

Surfaced via the WebSocket `calendar.*`, `contacts.*`, `mail.*`, `messages.*`, `notes.*`, `reminders.*`, `photos.*`, `bookmarks.*`, `files.*`, and `keychain.*` methods. See [`docs/websocket-protocol.md`](../../../docs/websocket-protocol.md).