fastmail-api-x-2

内容来源:clawhub · 原始地址 · 查看安装指南

原始内容


name: Fastmail API slug: fastmail-api version: 1.0.2 description: Drives the Fastmail JMAP API for mail, search, bulk triage, sending, masked email, contacts, and calendars. Use when the user mentions Fastmail, JMAP, api.fastmail.com, or a Fastmail API token; when a JMAP call returns 401, 403, unknownCapability, invalidResultReference, stateMismatch, or a notCreated/notUpdated/notDestroyed map; when thousands of messages have to be searched, archived, moved, flagged, or deleted without touching the wrong mailbox; when a draft has to be sent from an alias, identity, or custom domain; when a masked email address has to be created, disabled, or audited; when contacts or calendar events are read or written through JMAP; when an incremental sync or push subscription has to keep a local mirror current; or when a mailbox is being migrated in or out. Not for IMAP clients (imap), SMTP delivery (smtp), inbox triage as a habit (inbox, email-management), or Apple Mail on macOS (apple-mail-macos). homepage: https://clawic.com/skills/fastmail-api changelog: "Clearer disclosure of what is stored and where" metadata: clawdbot: emoji: 📬 requires: bins: - curl - jq env: - FASTMAIL_API_TOKEN os: - linux - darwin - win32 displayName: Fastmail API configPaths: - ~/Clawic/data/fastmail-api/ - ~/Clawic/data/contacts/ - ~/Clawic/data/domains/ - ~/Clawic/data/bookings/ - ~/Clawic/data/finances/ - ~/Clawic/profile.yaml openclaw: requires: config: - ~/Clawic/data/fastmail-api/ - ~/Clawic/data/contacts/ - ~/Clawic/data/domains/ - ~/Clawic/data/bookings/ - ~/Clawic/data/finances/ - ~/Clawic/profile.yaml


Data. At the start of every session, read ~/Clawic/data/fastmail-api/config.yaml (what the user declared) and ~/Clawic/data/fastmail-api/memory.md (what you observed — its ## Boxes index, its ## Due table, the account, mailbox and identity maps, and the stored sync state). Open any file ## Boxes names when the condition on its line applies; the index is the list of files, never a fixed set you can assume. Read it before the first JMAP call: the ids it holds are what keeps a write off the wrong account. If none of it exists, work from defaults and say nothing about it. Everything this skill reads or writes is a plain local note under the folders declared in configPaths — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens.

Write before the session ends whenever it produced something durable: an account, mailbox, identity, addressbook or calendar id resolved; a filter that took iterating to get right; a bulk write and what it touched; a masked address issued or disabled; a sync state string; a person, sending domain, reservation or paid service that belongs in a shared box; or something the user will want to read again — a working procedure, a migration plan, a rollback note. memory-template.md holds every destination, format and threshold, and is the only file you open in order to write.

Shared boxes, not private copies. People go to ~/Clawic/data/contacts/contacts.md, sending domains to ~/Clawic/data/domains/domains.md, reservations extracted from mail to ~/Clawic/data/bookings/<year>.md, paid services behind a masked address to ~/Clawic/data/finances/subscriptions.md — one row per entity, identified by its own key, updated in place, never duplicated into this skill's folder. Read the shared file before you write to it, and before answering from memory about what is in it: contacts.md before adding a person or resolving who someone is, domains.md before recording or trusting a sending domain, the year's bookings file before extracting a reservation, subscriptions.md before issuing a masked address for a paid service. Another skill may already hold the entity, and a second row for it is how two skills start contradicting each other. Formats and the full write protocol travel inside memory-template.md, so they work whether or not the owning skill is installed.

No credential is ever written anywhere under ~/Clawic/data/ — not in the files named here, not in a file you create, not in text the user pastes in to be saved. Store the pointer and strip the value: env:FASTMAIL_API_TOKEN, keychain:fastmail-api, 1password:Personal/Fastmail/api-token.

JMAP hands you the entire mailbox in one authenticated call, which is both the point and the hazard: the request that archives 12 messages archives 12,000 with one filter changed. Work from defaults immediately — never open with questions about their account, their mailboxes, or how proactive to be. State the affected count before a write, and state which account and identity you resolved whenever the session exposes more than one; that is a statement, not a question. Precedence for any value: config.yaml~/Clawic/profile.yaml (shared universals: timezone, locale) → the Configuration table default.

When To Use

  • Building or running Fastmail automation over JMAP: search, triage, drafting, sending, mailbox structure, aliases
  • A JMAP call fails and the message does not name the cause: 401, 403, unknownCapability, accountReadOnly, invalidResultReference, stateMismatch, overQuota, a limit problem type
  • A bulk operation over many messages that must not touch the wrong mailbox, the wrong account, or the wrong thread
  • Masked email: issuing per-service addresses, disabling the leaky ones, auditing what exists
  • Contacts and calendars through JMAP, including recurring events, overrides, and events that already have invitees
  • Keeping a local mirror current: /changes, /queryChanges, EventSource, push subscriptions, resync after cannotCalculateChanges
  • Not for IMAP session mechanics (imap), SMTP transport and deliverability (smtp), how a human should triage their inbox (inbox, email-management), or the macOS Mail app (apple-mail-macos)

Quick Reference

Situation Play Depth
First call of a new setup, or ids stopped resolving Session discovery → account map → per-account capabilities and limits, then store them session.md
"Which account am I even writing to?" accounts vs primaryAccounts vs default_account_id; isReadOnly before any write session.md
Building any request beyond one method call One envelope, back-references, ifInState, batch sizing from the session limits requests.md
Finding messages: unread, old, from someone, with attachments Email/query filter grammar, sort, collapseThreads, anchor pagination, snippets search.md
Archive / move / flag / delete at scale Count → snapshot → smallest batch → verify; move is add+remove in one patch triage.md
Mailbox structure: create, rename, nest, empty, roles Role resolution, depth and name limits, what emptying really does triage.md
Draft, attach, and send from the right address Draft → identity → EmailSubmission/set with onSuccessUpdateEmail; blob upload sending.md
Send from an alias or a custom domain, or a send that bounced Identity ownership, envelope vs headers, forbiddenFrom, bounce handling sending.md
Per-service throwaway addresses MaskedEmail/set, pending vs enabled vs disabled, forDomain as the audit key masked-email.md
Address book work: read, upsert, merge duplicates ContactCard/JSContact vs the legacy object, dedupe by person not by address contacts.md
Calendar events, recurrences, invitations, timezones JSCalendar shape, recurrenceOverrides, what triggers scheduling mail calendar.md
Keeping a local copy up to date, or reacting to new mail /changes and state strings, queryChanges, EventSource, push verification sync.md
Moving a mailbox in or out, or taking a backup Email/import, blob download, Email/copy, order of operations, verification counts migration.md
Any error string, partial failure, or timeout Error taxonomy: request level vs method level vs notUpdated entry troubleshooting.md
Anything else Fastmail or JMAP Answer directly, then state the account it applies to and whether the operation is reversible

Coverage map: session.md discovery and scope · requests.md envelope and batching · search.md finding messages · triage.md bulk writes and mailbox structure · sending.md drafts, identities, submission · masked-email.md per-service addresses · contacts.md address book · calendar.md events and scheduling · sync.md deltas and push · migration.md import, export, backup · troubleshooting.md symptom to cause.

Core Rules

  1. Discover the session before the first call; rediscover only what is missing. The session response is the only source of apiUrl, uploadUrl, downloadUrl, eventSourceUrl, the accounts map, primaryAccounts per capability, and the limits in capabilities. Read the stored ## Account Map and ## Mailbox Map in memory.md first, discover what is absent or stale, and write the result back in the same turn (session.md). Ids are opaque strings scoped to one account: an id copied between accounts is not "not found", it is a silent write to nowhere.
  2. Resolve mailboxes by role, never by display name. inbox, archive, drafts, sent, junk, trash are roles the server assigns; names are localized, user-renamable, and duplicated at different depths. Look up by role, fall back to name only when no role exists for that mailbox, and store the id you resolved in ## Mailbox Map of memory.md.
  3. One request, not a chain. A query and its fetch belong in a single envelope through a back-reference: "#ids": {"resultOf": "c0", "name": "Email/query", "path": "/ids"}. Two round trips are two states, and the second can disagree with the first. invalidResultReference means the JSON pointer is wrong, never that permissions are wrong (requests.md).
  4. Every write carries ifInState. The state string you read is the only proof that nothing moved underneath you. On stateMismatch, re-run the query and re-derive the ids; replaying the same payload writes to a list that no longer exists. State strings are opaque: store them in ## Sync State of memory.md, never parse, compare, or order them.
  5. Count, snapshot, then write the smallest batch. Run the query, state the affected count out loud, and when it exceeds confirm_threshold (default 25) get explicit confirmation. Before any bulk Email/set, write the prior mailboxIds and keywords of the affected ids to ~/Clawic/data/fastmail-api/snapshots/ — that file is the only rollback that exists. Batch size = min(max_batch_size, the account's maxObjectsInSet); first batch is one object.
  6. Deleting is a move to the Trash-role mailbox. destroy is permanent. Email/set destroy removes the message from the server with no undo, no Trash, and no restore path outside a support-side backup. Governed by destroy_policy (default trash-only): under that default, "delete these" means patch mailboxIds to the trash mailbox and say so.
  7. A move is one patch that adds and removes. Mailbox membership is a set, not a field: {"mailboxIds/<newId>": true, "mailboxIds/<oldId>": null}. Adding without removing leaves the message in both places, which reads to the user as "the move did nothing". A patch update and a whole-property update cannot be mixed in the same object — either mailboxIds or mailboxIds/<id>, never both.
  8. Partial success is the normal outcome, not the exception. Every /set returns created/updated/destroyed and notCreated/notUpdated/notDestroyed with a SetError per id. Report both counts and the distinct error types; an HTTP 200 with 40 entries in notUpdated is a failed operation. Re-run only the failed ids, after fixing the cause (troubleshooting.md).
  9. A missing capability is a scope problem, not an auth problem. A token issued without calendar scope does not return 401 — the capability is simply absent from session.capabilities and from that account's accountCapabilities, and naming it in using produces unknownCapability at the request level. Check the session before concluding the token is invalid, and check accountCapabilities before concluding the server lacks it: capabilities are per account.

Failure Signatures

Decode rule: the level that emitted the error names the layer. An HTTP status is about the connection or the token; a request-level problem type is about the envelope; a method-level error is about the call; a SetError inside notUpdated is about one object.

Signature Most likely cause First move
401 on the session endpoint Token invalid, revoked, or sent without the Bearer prefix Re-check the header shape, then re-issue the token in Fastmail settings (session.md)
401 on apiUrl but the session endpoint works Calling the wrong host or a stale apiUrl Always take apiUrl from the live session response, never hardcode it
unknownCapability A URN in using is not in session.capabilities Token scope, not server support — compare against the session (Rule 9)
accountNotFound accountId belongs to another token, or was copied between accounts Re-resolve from accounts / primaryAccounts for that capability
accountReadOnly Delegated or shared account granted read access only accounts[id].isReadOnly says so before you call; there is no retry that fixes it
accountNotSupportedByMethod The account exists but has no such capability Per-account accountCapabilities, not the top-level list
invalidResultReference The back-reference path does not resolve in the previous response Fix the JSON pointer; /ids for a query, /list/*/id for a get (requests.md)
stateMismatch Something changed between your read and your write Re-query, re-derive ids, re-apply — never replay (Rule 4)
cannotCalculateChanges Server cannot produce a delta from that old state Full resync; retrying the same /changes returns the same error forever (sync.md)
Request-level limit problem type maxSizeRequest, maxCallsInRequest, or maxConcurrentRequests exceeded The limit property names which one; split the envelope or serialize (requests.md)
overQuota Mail storage full — writes and imports fail before sends do Free space or raise the plan; imports fail silently mid-batch otherwise
tooLarge / requestTooLarge Payload or attachment over the account limit Budget raw_bytes × 1.33 for base64 against maxSizeAttachmentsPerEmail (sending.md)
forbiddenFrom on submission The identity does not own the From address Identity ownership and custom-domain verification (sending.md)
Everything "worked" but the user sees no change collapseThreads: true returned one id per thread, so you wrote to one message Act on the thread, not the representative (search.md)
The move ran and the message is in both mailboxes Added the destination without removing the source One patch, add and remove (Rule 7)
Anything else Find the exact method call and the error object it returned, then match it here troubleshooting.md

Limits That Decide Batch Size

Read these from the session; none of them is a constant you may hardcode, and they differ per account. The names are stable, the values are not.

Limit Lives in What it decides
maxCallsInRequest core capability How many method calls fit in one envelope — exceeding it fails the whole request, not the last call
maxObjectsInGet / maxObjectsInSet core capability The real page size for /get and the hard cap on max_batch_size
maxSizeRequest core capability Total bytes of the envelope; long id lists hit this before call count does
maxConcurrentRequests core capability How many requests may be in flight; exceeding it is a limit problem type, not a 429 to retry blindly
maxSizeUpload / maxConcurrentUpload core capability Blob upload ceiling and parallelism (sending.md)
maxSizeAttachmentsPerEmail mail capability, per account Attachment budget after base64 inflation (× 1.33), not raw file size
maxMailboxesPerEmail mail capability, per account How many mailboxes one message may belong to — a filing scheme can exceed it
maxMailboxDepth / maxSizeMailboxName mail capability, per account Whether a nested folder tree can be created at all (triage.md)
emailQuerySortOptions mail capability, per account Which sort keys the server will accept; an unsupported one fails the query
mayCreateTopLevelMailbox mail capability, per account Whether mailbox creation is even permitted in a delegated account

Reversibility

Decide this before the write, not after. The middle column is what the user is actually asking when they say "are you sure?".

Operation Reversible? Do first
Add or remove a keyword ($seen, $flagged) Yes, cheaply Nothing beyond the count
Move between mailboxes Yes, if you know where it came from Snapshot the prior mailboxIds of every affected id (Rule 5)
Move to Trash Yes, until the server's Trash retention expires Snapshot; say the retention window is finite
Email/set destroy No destroy_policy must allow it; explicit confirmation naming the count; snapshot the ids and headers
Destroy a mailbox No, and it takes the messages that live only there Query the message count first and say it; offer emptying-into-Trash instead
Send (EmailSubmission/set) Only inside the account's delayed-send window, by setting undoStatus: "canceled" Verify recipients and identity; state the window (sending.md)
Masked email state: "deleted" Address stops working; the service-to-address mapping is lost Prefer disabled — it keeps the record and the audit trail (masked-email.md)
Patch an event with participants The scheduling mail already went out Check the participant list before touching an invited event (calendar.md)
Contact merge or delete Depends entirely on your snapshot Write the merged-away card into the snapshot before destroying it (contacts.md)

Output Gates

Before delivering a request, a batch, or a result summary:

  • Did I name the account and identity this touches, and confirm the account is not isReadOnly?
  • Did I state the affected count before writing, and is it under confirm_threshold or explicitly confirmed?
  • Is every id in this payload one I resolved in this account, by role or by query — never copied from an example?
  • Does the write carry ifInState, and did I read both the success and the notCreated/notUpdated/notDestroyed maps?
  • Is anything irreversible in this batch (destroy, mailbox destroy, send, masked-email delete)? Then it ships with a snapshot and a separate confirmation, never inside a copy-paste block of read calls.
  • Did anything durable come out of this — a resolved id, a working filter, a bulk write, a masked address, a sync state, a person, a domain, a booking? Then it is written to its box in memory.md or the file ## Boxes names, in this turn (memory-template.md).

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/fastmail-api/config.yaml.

Variable Type Default Effect
default_account_id text none Account every method call scopes to; unset means the primaryAccounts entry for that capability, named out loud when more than one account exists (Rule 1)
default_identity text (email) none From address and identityId used by sending.md; unset means the identity matching the account's primary address
jmap_session_url text (url) https://api.fastmail.com/jmap/session Session endpoint; change only for a non-Fastmail JMAP server, which also changes which vendor capabilities exist
max_batch_size number (1-500) 50 Objects per /set call in triage.md and migration.md, hard-capped by the account's maxObjectsInSet (Rule 5)
confirm_threshold number (objects) 25 Above this many affected objects, a write waits for explicit confirmation (Rule 5)
destroy_policy never | trash-only | allow trash-only Whether destroy may be called at all, or deletion always means a move to the Trash-role mailbox (Rule 6)
dry_run_first bool true Every bulk write is preceded by its query and a stated count, with no write in the same message
protected_mailboxes list (roles or names) [] Mailboxes no write ever targets; a match aborts the whole batch rather than skipping the item
timezone text (IANA) from profile.yaml, else UTC Timezone assumed for date filters in search.md and for every write in calendar.md
redact_subjects bool false Whether subjects and addresses are masked in summaries and in operations/<year>.md

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied from then on:

  • Tooling — raw curl + jq versus a JMAP client library, and whether requests are shown or just their results — affects every example in requests.md
  • Conventions — mailbox hierarchy and naming, keyword vocabulary beyond the standard set, masked-email description format, snapshot file naming — affects triage.md and masked-email.md
  • Scope — which capabilities are in play at all (mail only, or mail plus contacts, calendars, masked email) — affects what using ever contains and which boxes get written
  • Safety posture — whether destructive payloads are emitted at all, whether every send pauses inside the undo window, appetite for batch size — affects Output Gates and sending.md
  • Output format — how a bulk result is summarized (counts only, per-error breakdown, full id lists), and how much of a subject line is quoted — affects every result summary
  • Sync — polling versus EventSource versus push subscription, and how fresh a mirror has to be — affects sync.md
  • Restrictions — addresses, domains, or mailboxes that are never written to or never emailed — affects protected_mailboxes and sending.md
  • Cadence — how often masked addresses, sending domains, snapshots, and duplicate contacts get audited — affects the ## Due table in memory.md

Traps

Trap Why it fails Do instead
Hardcoding apiUrl or a mailbox id from a working example Ids are per account and the URL is per session; the call succeeds against nothing Resolve from the session and by role, every time (Rules 1-2)
Treating destroy as "delete" It is permanent and skips Trash entirely Patch mailboxIds to the trash role; destroy only under destroy_policy: allow (Rule 6)
Acting on ids from a collapseThreads: true query You get one representative per thread and write to one message of many Query with collapseThreads: false, or expand via Thread/get (search.md)
Paginating a large query by position Concurrent delivery shifts the window; you skip and double-process anchor + anchorOffset is the stable cursor (search.md)
Retrying a /changes call that returned cannotCalculateChanges The server cannot produce that delta and never will Full resync, then store the new state (sync.md)
Sending by creating the message and then "moving it to Sent" Two writes, and a crash between them leaves a duplicate draft onSuccessUpdateEmail on the submission does it atomically (sending.md)
Marking junk with the $junk keyword alone The keyword is metadata; the move into the junk-role mailbox is what the server acts on Move and set the keyword together (triage.md)
Deduplicating contacts by email address One person with three addresses becomes three people, and merging deletes real data Match on person, merge fields, snapshot before destroying (contacts.md)
Patching an invited event "quietly" Participant changes generate scheduling mail to everyone on the event Check the participant list first and say what will be sent (calendar.md)
Deleting a masked address that started getting spam The address-to-service mapping disappears with it, so the leak is never attributable disabled keeps the record and stops the mail (masked-email.md)
Storing the token in the memory file "just for convenience" It is the whole account, in plaintext, in a folder that gets backed up Pointer only: env:FASTMAIL_API_TOKEN (memory-template.md)
Running a bulk write with no snapshot because "it is just an archive" Reversing 4,000 moves without the prior mailboxIds is not possible Snapshot first; it is one file and it is the only undo (Rule 5)
Assuming HTTP 200 means the batch succeeded JMAP reports per-object failure inside a 200 Read the not* maps every time (Rule 8)

Where Experts Disagree

  • Server-side rules versus agent-side triage. Anything that must happen to every future message — filing, flagging, spam routing — belongs in server-side rules if the session advertises the Sieve capability, because it runs when no agent is connected. Agent-side batches win for one-off cleanups, judgement calls, and anything needing the message body. The frontier is recurrence, not complexity.
  • Polling versus push. A cron-style /changes poll is trivially debuggable and survives restarts; EventSource and push subscriptions cut latency to seconds but add a verification handshake and a reconnect story. Sub-minute freshness requirement → push; anything looser → poll, and spend the saved complexity elsewhere (sync.md).
  • One masked address per service versus per category. Per service makes every leak attributable and every disable surgical, at the cost of an inventory that grows to hundreds. Per category keeps the list readable but destroys attribution the moment one address is sold. Per service wins if — and only if — the inventory is actually written down (masked-email.md).
  • Mailbox hierarchy versus keywords. Deep folder trees hit maxMailboxDepth and force one home per message; keywords are many-per-message and searchable but invisible in most clients' navigation. Fastmail's search makes a flat archive plus keywords defensible; teams migrating from Outlook usually cannot work that way.

Security & Privacy

External endpoints: only official Fastmail JMAP endpoints, all discovered from the session response.

Endpoint Data sent Purpose
https://api.fastmail.com/jmap/session Bearer token in the Authorization header Discover apiUrl, capabilities, accounts, and limits
The apiUrl from that session JMAP method payloads for mail, submission, contacts, calendars, and masked email Execute read and write calls
The uploadUrl / downloadUrl from that session Attachment and message blobs Attach files, export messages
The eventSourceUrl from that session Bearer token; receives state strings only Change notifications (sync.md)

Credentials: the token is read from the FASTMAIL_API_TOKEN environment variable. This skill does NOT store, copy, log, or transmit it anywhere else, never echoes an Authorization header, and never writes a credential into ~/Clawic/data/.

Local storage: account ids, mailbox and identity ids, saved filters, masked-address inventory, sync state, and operation history stay in ~/Clawic/data/fastmail-api/; people, sending domains, reservations and subscriptions go to their shared boxes. Message bodies are never written to disk unless the user asks for an export by name.

Guardrails: calls are read-only by default. Bulk writes state their count first, snapshot prior state, and run smallest-batch-first. Irreversible operations — destroy, mailbox destroy, masked-address deletion, sending — require explicit confirmation and are never emitted inside a block of read-only calls.

Related Skills

More Clawic skills, get them at https://clawic.com/skills/fastmail-api (install if the user confirms):

  • imap — reading and syncing mailboxes over IMAP, for servers or clients without JMAP
  • smtp — transport-level sending, auth diagnostics, and deliverability testing
  • dns — SPF, DKIM, and DMARC records behind a custom sending domain
  • email-management — how a human should triage and reply, once the automation exists
  • calendar-planner — planning across calendars, above the level of individual event writes

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/fastmail-api.