Receive iCloud Mail (Apple Mail) emails live with a webhook
iCloud Mail can forward new email while your Mac is asleep. Point that server-side forwarding at a MailKite receiving address and your application gets signed JSON events. This walkthrough shows the forwarding settings, a durable Python receiver, selective rules and what Apple Mail itself does not control. MailKite is the platform we build.
The runnable source and delivery checks live in the shared receiving-email demo repository.
Use this downloadable receiver to accept the forwarded messages. It saves the signed event before acknowledging it and leaves repeated deliveries at the SQLite primary key.
import json
import os
import sqlite3
from flask import Flask, request
from mailkite import verify_webhook
app = Flask(__name__)
SECRET = os.environ["MAILKITE_WEBHOOK_SECRET"]
DB = os.environ.get("INBOX_DB", "inbox.sqlite")
with sqlite3.connect(DB) as db:
db.execute("CREATE TABLE IF NOT EXISTS inbox (id TEXT PRIMARY KEY, payload TEXT NOT NULL)")
@app.post("/hooks/mailkite")
def receive():
raw = request.get_data()
signature = request.headers.get("x-mailkite-signature", "")
if not verify_webhook(signature, raw, SECRET):
return "bad signature", 401
try:
event = json.loads(raw)
except (ValueError, UnicodeDecodeError):
return "bad JSON", 400
if not isinstance(event, dict):
return "bad event", 400
if event.get("type") != "email.received":
return {"status": "ok"}
if not isinstance(event.get("id"), str) or not event["id"]:
return "missing id", 400
try:
with sqlite3.connect(DB, timeout=5) as db:
db.execute("INSERT INTO inbox (id, payload) VALUES (?, ?) ON CONFLICT(id) DO NOTHING",
(event["id"], raw.decode("utf-8")))
except sqlite3.Error:
return "storage unavailable", 503
return {"status": "ok"} # The transaction has committed before this response.
if __name__ == "__main__":
app.run(port=int(os.environ.get("PORT", "3000")))
First distinguish the mailbox from the mail app
Apple Mail is a client. It can show an iCloud account, Gmail, Microsoft 365 or a mailbox hosted elsewhere. Forwarding is a setting of the provider that receives the message, so “receive Apple emails” needs a more precise scope: this article covers iCloud Mail.
If your Apple Mail app displays a Gmail account, use the Gmail forwarding guide. If it displays another provider, use that provider’s server-side forwarding settings. A Mac-local rule is a different mechanism and may depend on the desktop application running.
Apple’s iCloud forwarding instructions, retrieved October 14, 2026, also cover mail sent to custom-domain addresses hosted in iCloud Mail. There is no need to move those addresses to another MX host for this forwarding approach.
Make a separate receiving destination
Add a receiving domain or available hosted subdomain in MailKite and complete verification. Pick an address for this integration, such as icloud@your-receiving-domain. Keep iCloud as the host of your original mailbox; the new address is simply where it forwards a copy.
Create an exact-address MailKite webhook route for this destination before obtaining its signing secret or starting the receiver. Point it at your public HTTPS /hooks/mailkite URL; a development tunnel can forward that URL to local port 3000. Install mailkite-dev and Flask in a virtual environment, supply this route’s secret as MAILKITE_WEBHOOK_SECRET through your environment, then run receive.py. Production needs a real application server and persistent storage.
The iCloud Mail guide includes dependency commands. Download the synthetic checker as check.mjs. With Node.js 18+ and the same webhook secret set in a second terminal, check the running receiver:
node check.mjs http://127.0.0.1:3000/hooks/mailkite
It exercises valid and duplicate events, tampered bodies, missing signatures and signed malformed JSON without sending email. The dashboard’s domain test targets a catch-all *@domain webhook, not your exact-address route. After the local check, send a real email directly to the exact MailKite receiving address and confirm its delivery and stored payload before changing iCloud forwarding. This tests mail routing separately from the HTTP handler and from iCloud’s forwarding configuration.
Download the inbox inspector and read the Python receiver’s database locally:
python3 inspect-inbox.py inbox.sqlite
Use your actual database path if you set INBOX_DB. The inspector prints the latest ten messages and their bodies; keep that output and the database private. Never expose the script as a web route. It displays content, not a row-count assertion.
Set forwarding on iCloud.com
Sign in to iCloud Mail, open the settings menu at the top of the Mailboxes list, and choose Settings → Mail Forwarding. Select Forward my email to and enter the MailKite receiving address.
Leave Delete messages after forwarding unchecked for this walkthrough. That keeps a mailbox copy while you verify the integration. Apple notes that retained copies consume iCloud storage. Deleting those copies is a separate mailbox-retention choice, not a requirement for webhook delivery.
Now send a new external message to the iCloud address. Check that it arrives in iCloud, reaches MailKite and produces a successful webhook delivery. Then inspect the receiver’s stored event. The email was forwarded by iCloud’s servers; closing Apple Mail or putting your laptop to sleep does not remove that forwarding configuration.
Use a rule for a specific workflow
An iCloud Mail rule can forward messages matching chosen criteria. Apple documents one forwarding address per rule. This is appropriate when an application should process a subset, such as mail to one alias, rather than every personal message.
Test the rule with fresh matching and non-matching messages. Keep an identifiable receiving address per integration so you can distinguish which mailbox feed produced an event. The original MIME headers may retain the original recipient; MailKite’s webhook to field identifies the envelope destination it routed on. Use that receiving destination and your route configuration as the integration boundary.
Avoid a circular route that sends the forwarded message back to the original mailbox. If you later add replies, test their routing separately rather than treating an inbound receiver as permission to send from an @icloud.com address.
The webhook is a copy, not an iCloud synchronization API
The app receives parsed message content after the forward reaches MailKite. “Live” means there is no polling timer in this application path. It still depends on normal email transport and forwarding delay.
This does not import historical mail, mirror folders, mark the original message as read, or grant an iCloud sending identity. If your application needs a full mailbox protocol, an authorized IMAP integration is a different choice. For a domain you control, direct inbound delivery through Postmark, SES receipt rules or your own SMTP receiver can avoid a personal-mailbox forwarding hop.
Use the SDK verifier to authenticate the HTTP request, then make a separate decision about the original sender. Forwarding can affect email-authentication results. Null verdicts mean unknown; a valid signature is not a substitute for your sender policy.
Keep acknowledgement separate from completing the task
The receiver commits a SQLite row before it returns 200. The stable event id is unique, so a retry doesn’t add another row. Replay a real delivery and check that its id still has one stored row; the synthetic checker’s duplicate response alone does not prove storage deduplication. If storage fails, the response is 503 and the app has not falsely claimed acceptance.
That is the acceptance boundary, not the entire application. A worker must process stored rows and record completion; external actions need their own idempotency mechanism. Keep the SQLite file on persistent disk and protect it like mailbox data. Download required attachments before their signed URLs expire, after accepting the event rather than holding the webhook open.
The receiving reference documents the event fields and delivery reference covers replay. For another runtime, choose from the language-specific guides. The code in this article is the downloadable Python receiver.