Access iCloud Mail over IMAP and SMTP with an app password. Read, send, and manage messages, drafts, threads, and folders.
Reference
iCloud Mail has no REST API. The maton icloud-mail commands speak IMAP and SMTP, handling quoting, folder-name encoding, threading headers and the Sent copy, and checking that a write lands on the right message. Connect a mailbox with maton connection create icloud-mail and an app-specific password from account.apple.com (two-factor authentication must be on). iCloud Mail is reached through the Maton bridge, which runs on your own computer on a home or office network.
Message ID
- A message ID is
<folder>/<uid>, e.g.INBOX/{uid}orWork/Clients/{uid}; it is split at the last/, so nested folders work. Quote IDs whose folder has a space:"Sent Messages/{uid}". - The ID changes when the message moves. iCloud has no server-side message ID, and a UID only means something within its folder.
trash,untrashandmodify --move-toprint the new ID; use that one from then on. Renaming a folder changes the IDs of the messages in it. - A stale ID is an error, never a silent no-op:
message … not found. - Thread IDs are a
Message-ID. iCloud assigns none, so a thread ID is theMessage-IDof the conversation's first message, as replies name it in theirReferencesheader. It is in the THREAD column ofmessage listandthread_idinmessage get --json.thread getalso accepts themessage_idof any message in the conversation. message_idin JSON is theMessage-IDheader, written by whoever sent the message. It is not guaranteed unique, and some messages have none.
Bridges
maton bridge list
maton bridge install
maton bridge statusA bridge on a cloud or hosting network is refused. If there are multiple bridges, specify which one to use so requests route through the intended bridge with --bridge {bridgeId}. Without --bridge, requests use the oldest connected bridge with room, then the next.
Get Mailbox
maton icloud-mail whoamiList Folders
maton icloud-mail folder list--folder takes NAME (eg. INBOX, Sent Messages, Drafts, Deleted Messages, Junk, Archive, Work/Clients). iCloud flags only Sent Messages and Deleted Messages so the CLI recognises the others by name.
Create Folder
maton icloud-mail folder create Receipts
maton icloud-mail folder create Work/ClientsRename Folder
maton icloud-mail folder rename Receipts "Receipts 2026"INBOX and the special folders can't be renamed. Renaming changes the IDs of the messages inside.
Delete Folder
maton icloud-mail folder delete "Receipts 2026"Permanently deletes the folder and all its messages. Messages skip Trash and can't be recovered. INBOX and special folders can't be deleted.
List Messages
maton icloud-mail message listReturns up to 20 unread INBOX messages, newest first. Listing never marks anything read.
All messages:
maton icloud-mail message list --all -L 50Another folder:
maton icloud-mail message list --folder 'Sent Messages' --allWith filters (all must match):
maton icloud-mail message list --all --from boss@example.com --since 2026-10-01
maton icloud-mail message list --all --subject invoice --before 2026-10-15
maton icloud-mail message list --all --text "quarterly report"--from, --to, --subject and --text match text anywhere in the field (--text covers headers and body); --since and --before take YYYY-MM-DD. There is no next-page cursor: -L/--max caps the results, so narrow with filters instead of paging.
Get Message
maton icloud-mail message get {messageId}
maton icloud-mail message get {messageId} --headers
maton icloud-mail message get {messageId} --rawPrints the plaintext body without marking the message read. --headers adds From/To/Cc/Subject/Date, --html prints the HTML part, --raw the original source. --json adds the folder, flags, message_id and thread_id.
Send Message
maton icloud-mail message send --to alice@example.com --subject "Hello" --body "Hi there!"
maton icloud-mail message send --to alice@example.com --cc bob@example.com --subject "Report" --body "Attached." -a report.pdfiCloud Mail keeps no copy of what it sends so the CLI saves one in Sent Messages and prints its ID (sent_id in JSON, alongside message_id). --html sends --body as HTML. reply, reply-all, forward and draft send work the same way.
Reply to Message
maton icloud-mail message reply {messageId} --body "Thanks!"
maton icloud-mail message reply {messageId} --body "Draft reply" --draftThe reply goes to the sender (or their Reply-To address, if set) and includes the threading headers and the quoted original so mail apps keep the conversation together. --draft saves the reply in Drafts instead of sending it and prints its draft ID.
Reply All to Message
maton icloud-mail message reply-all {messageId} --body "Sounds good"
maton icloud-mail message reply-all {messageId} --body "Updated" --remove bob@example.comThe reply goes to everyone in To and Cc, except your own address and any address passed with --remove. When replying to a message you sent, it goes to that message's original recipients.
Forward Message
maton icloud-mail message forward {messageId} --to dave@example.com --body "FYI"The original's header block and body are included. Its attachments are not re-attached; attach files with -a.
Modify Message
maton icloud-mail message modify {messageId} --read
maton icloud-mail message modify {messageId} --unread --flag
maton icloud-mail message modify {messageId} --move-to ArchiveIMAP uses flags and folders rather than labels. The message gets a new ID in its new folder; the output prints it.
Trash / Untrash Message
maton icloud-mail message trash {messageId}
maton icloud-mail message untrash {messageId}
maton icloud-mail message untrash {messageId} --to Archivetrash moves the message to Deleted Messages where it stays until the provider empties it. untrash moves it back to INBOX or to --to if given.
Create Draft
maton icloud-mail draft create --to alice@example.com --subject "Hello" --body "Draft body"List Drafts
maton icloud-mail draft listGet Draft
maton icloud-mail draft get {draftId}Send Draft
maton icloud-mail draft send {draftId}Sends the draft as saved, then deletes it from Drafts. BCC recipients receive it but aren't shown in the delivered message's headers. iCloud Mail keeps no copy of what it sends so the CLI saves one in Sent Messages and prints its ID (sent_id in JSON, alongside message_id).
Delete Draft
maton icloud-mail draft delete {draftId}Permanently deletes the draft; it doesn't go to Trash.
Get Thread
maton icloud-mail thread get {threadId}
maton icloud-mail thread get {threadId} --jsonLists every message in the conversation across all folders including Sent and Trash, oldest first, as summaries; use message get for bodies.
Raw IMAP / SMTP
POST /icloud-mail/commands
Content-Type: application/json
{"commands": "<base64 of the IMAP or SMTP stream>"}stream=$(printf 'M1 EXAMINE INBOX\r\nM2 UID SEARCH UNSEEN\r\n' | base64)
maton api -X POST '/icloud-mail/commands' -f commands="$stream" -q '.data' | base64 --decodeThe request body is exactly {"commands": "<base64 of the stream>"} (any other field is refused), and the response is {"data": "<base64 of the server's reply>"}. A stream whose first word is an SMTP verb (EHLO, MAIL, …) goes to SMTP; anything else goes to IMAP. None of the commands' safeguards apply:
- You own the protocol. Every IMAP command needs a unique tag; strings need quoting or literals (use
{n+}); folder names with non-ASCII characters must be in modified UTF-7 (CaféisCaf&AOk-); every line ends with CRLF, and a bare CR or LF is refused. - No folder is selected when a stream starts.
SELECTorEXAMINEfirst, in the same stream. - Writes to a missing UID succeed silently. Check with
UID FETCH <uid> (UID)in the same stream, and useUID EXPUNGE <uid>, never plainEXPUNGE, which removes every message marked deleted in the folder. - Fetching with
BODY[]marks a message read. UseBODY.PEEK[]. EXAMINEis read-write on iCloud: a plainFETCH … BODY[]marks the message read even in an examined folder.- Keep the session reusable.
LOGIN,AUTHENTICATE,LOGOUT,ENABLE,IDLE,NOTIFY,COMPRESS, CONDSTORE/QRESYNC options, synchronizing literals{n}, andSEARCH … RETURNoptions other thanMIN,MAX,COUNTandALLmake Maton discard the session afterwards, so the next request logs in again, which is slower and counts against the login limits. - iCloud's SMTP server files no Sent copy.
APPENDone toSent Messagesyourself, with the sameMessage-ID. - Choosing a bridge: with more than one,
-H 'Maton-Bridge: <bridge_id>'(an ID frommaton bridge list) pins the request to that computer.
Notes
- Removal actions require review.
folder deleteanddraft deleteare permanent; list or get the target first so you can verify it. - Reading never marks mail read.
message list,message getandthread getleave flags alone; mark a message withmessage modify --read. --sinceand--beforework by day, on the date the server received the message; the time of day is ignored.message_iddoesn't identify a message. TheMessage-IDheader is written by the sending software: it can be missing, repeated, or copied from another message, and both copies of a message sent to yourself carry the same one. Act on messages byid; usemessage_idonly withthread get.- iCloud needs the Maton bridge for reading and sending: Apple refuses mail from cloud servers, so Maton connects through the bridge on your own computer. While that computer sleeps, the connection shows
PENDING(bridge_disconnected) and recovers by itself. - Moves change the ID. UIDs are per folder, so
trash,untrashandmodify --move-togive the message a new ID; use the one printed. (iCloud doesn't advertise IMAPMOVE, so the CLI copies the message and expunges the original.) - Sent copies are written by the CLI. iCloud keeps no copy of what it sends, so the CLI saves one in
Sent Messages. Bcc recipients appear only in that copy. If saving it fails, the message was still sent and the CLI says so: don't send it again. - Threads come from reply headers. A thread ID is the
Message-IDof the conversation's first message, found throughReferences, elseIn-Reply-To, else the message's own; it matches how iCloud Mail on the web groups conversations, never by subject. A message without a validMessage-IDgets nothread_id, and replies to it start a new thread. - A thread ID may belong to no stored message. When a conversation started from Yahoo, Yahoo's server added a
Referencesentry like<id.ref@yahoo.com>, which becomes the thread ID.thread geton it, or on any message'smessage_id, still returns the whole thread. thread getcan miss a deep branch: a reply that carries onlyIn-Reply-To, two or more replies down, isn't found from the root. It is found from its own branch'smessage_id.- Self-sent mail appears twice in
thread getand searches: the delivered copy in INBOX and the CLI's copy inSent Messages, with the sameMessage-ID. - Forwards stay in the thread:
forwardsetsReferences(but notIn-Reply-To) to the original. draft sendfills in missing headers. A draft without aMessage-IDorDategets them before sending, so the delivered copy and the Sent copy match.- Large messages are slow through the bridge, at about your upload speed (around 1 MB/s on a typical connection). Fetch big messages one at a time.
- Folder names: non-ASCII names (
Café) are converted to and from IMAP's encoding, and names with control characters are refused.Work/Clientscan be created without creatingWorkfirst. - Retry reads, check writes. After a 500, 502 or 504, a send or change may already have happened: check the folder (and Sent) before repeating it. 429 and 503 changed nothing; retry after
Retry-After.
Rate Limits
- Requests: 10 per second per Maton account, with bursts up to 50.
- Mail requests in progress: at most 4 at a time per Maton account, across all of its mailboxes; more get 429 for a second.
- Per mailbox: 4 requests in flight. More workers only add 429s: iCloud reaches the account's request rate at about 4 at a time.
- Logins per mailbox: 4 IMAP logins per 8 s and 10 SMTP logins per 20 s; each send logs in to SMTP. Maton reuses IMAP sessions, so this is only reached by reconnect-heavy raw streams or rapid sends.
- Sending: 1000 messages and 1000 recipients per mailbox per day, across all its connections, reset at 00:00 UTC (Apple's published limit for iCloud Mail). A send over the allowance gets 429 before anything goes out.
- Failed logins: a refused app password is retried only after a pause (5 minutes, doubling up to 6 hours).
- Each bridge carries up to 8 IMAP and 4 SMTP connections at once, shared by all of the account's iCloud mailboxes; when full it answers 503 for a few seconds.