Skip to main content
Glama
icloud-mail-mcp

icloud-mail-mcp

README.md
# icloud-mail-mcp

[![CI](https://github.com/nickhalterman/icloud-mail-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nickhalterman/icloud-mail-mcp/actions/workflows/ci.yml)
![Python 3.13](https://img.shields.io/badge/python-3.13-blue)
![AWS Lambda](https://img.shields.io/badge/AWS-Lambda-orange)
![License: MIT](https://img.shields.io/badge/license-MIT-green)

**Give Claude secure access to your iCloud Mail and Calendar on every device.** A small, serverless [MCP](https://modelcontextprotocol.io) server that lets Claude read, search, organize and draft replies in iCloud Mail, and read your iCloud Calendar. It runs on AWS Lambda for pennies a month and works in Claude chat, the iPhone app and scheduled agents.

Apple doesn't offer a Claude connector for iCloud Mail or Calendar, so I built one.

> "Anything from recruiters this week?" → Claude searches your inbox, reads the relevant threads, and answers.
> "Draft a reply saying Thursday works." → The draft appears in Apple Mail. You review it and hit Send.
> "Trash last week's promos." → Claude lists them, you approve, they're moved to Deleted Messages.
> "What's on my calendar tomorrow?" → Claude reads your iCloud calendars and lists the day.

## How it works

```mermaid
flowchart LR
    A[You<br/>iPhone · Mac · web] --> B[Claude]
    B -- HTTPS + API key --> C[AWS Lambda<br/>Function URL]
    C -- secrets --> D[(SSM Parameter Store)]
    C -- IMAP over TLS --> E[iCloud Mail]
    C -- CalDAV over TLS, read-only --> G[iCloud Calendar]
    E -. drafts sync .-> F[Apple Mail app]
```

Claude calls the server as a [custom connector](https://support.claude.com/en/articles/11175166-about-custom-connectors). Each request spins up a stateless Lambda that checks the API key, loads your iCloud app-specific password from SSM, does its work over IMAP (mail) or CalDAV (calendar), and shuts down. There's no server to maintain.

## Tools

| Tool | What it does | Suggested Claude permission |
|---|---|---|
| `list_mailboxes` | List folders | Always allow |
| `list_recent` | Newest messages from the last N days | Always allow |
| `search_mail` | Filter by text, sender, subject, date, unread | Always allow |
| `read_message` | Full text and attachment list (doesn't mark as read) | Always allow |
| `create_draft` | Save a draft, threaded as a reply if needed. **Never sends.** | Always allow |
| `create_mailbox` | Create a folder | Always allow |
| `set_flags` | Mark read/unread, flag/unflag | Always allow |
| `move_messages` | Move up to 50 messages to another folder | **Needs approval** |
| `trash_messages` | Move up to 50 messages to Deleted Messages | **Needs approval** |
| `list_calendars` | List iCloud calendar names | Always allow |
| `list_events` | Events for a range of days (1-31), repeating events expanded, in your time zone. **Read-only.** | Always allow |

## Security model

This server touches your email, so it's built to fail safe:

- **It can't send email.** There's no SMTP code at all. Drafts wait for you in Apple Mail.
- **It can't change your calendar.** The calendar code only sends CalDAV read requests (`PROPFIND`, `REPORT`). There is no code that creates, edits or deletes events.
- **Your password only goes to Apple.** Calendar requests are refused unless the address is `https://*.icloud.com`.
- **Nothing is permanently deleted.** "Trash" moves mail to Deleted Messages, which iCloud keeps recoverable for 30 days.
- **Blast radius is capped.** Organizing tools act on at most 50 messages per call and report exactly which uids they touched.
- **Reads are read-only.** Mailboxes are opened with `EXAMINE`, and bodies are fetched with `BODY.PEEK`, so reading never changes state.
- **Least-privilege secrets.** Your iCloud app-specific password and the API key live only in SSM as SecureStrings. The Lambda's IAM role can read only its own parameter path. Revoking the app-specific password at Apple cuts access instantly.
- **Locked endpoint.** Every request needs `Authorization: Bearer <key>` (a 64-character random key, checked in constant time).
- **Prompt-injection aware.** The server tells Claude to treat email content as untrusted data, and the destructive tools are meant to sit behind Claude's per-tool approval.
- **Kill switches.** `ALLOW_ORGANIZE=false` makes it read-and-draft only, `ALLOW_DRAFTS=false` makes it strictly read-only, and `ALLOW_CALENDAR=false` removes the calendar tools.

## Deploy on AWS (about 20 minutes)

**You need:** an AWS account, the [AWS CLI](https://aws.amazon.com/cli/) and [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html), Python 3.13, and an iCloud **app-specific password** (account.apple.com → Sign-In and Security → App-Specific Passwords).

**1. Store your secrets** (pick a region, e.g. `us-east-2`):
```bash
export AWS_REGION=us-east-2
aws ssm put-parameter --name /icloud-mail-mcp/icloud/user --type String --value "you@icloud.com"
read -s "PW?iCloud app-specific password: " && echo   # bash: read -s -p "..." PW
aws ssm put-parameter --name /icloud-mail-mcp/icloud/app-password --type SecureString --value "$PW"; unset PW
aws ssm put-parameter --name /icloud-mail-mcp/bridge/api-key --type SecureString --value "$(openssl rand -hex 32)"
```
Use your primary @icloud.com address.

**2. Deploy:**
```bash
sam build
sam deploy --guided --stack-name icloud-mail-mcp
```
Answer `y` when asked whether the Function URL may have no AWS auth; the API key is the lock. The output prints a **ConnectorUrl**. Set **DefaultTimezone** to your IANA time zone (e.g. `America/Chicago`) so "today" on the calendar means your today.

The calendar uses the same app-specific password as mail. If your Apple Account email isn't your @icloud.com address and calendar sign-in is rejected, set the `ICLOUD_CALDAV_USER` environment variable on the function to your Apple Account email.

**3. Verify** (`401` means it's up and locked):
```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST <ConnectorUrl>
```

**4. Connect it to Claude.** claude.ai → Settings → Connectors → **Add custom connector**:
- **URL:** your ConnectorUrl
- **Authentication:** no sign in
- **Request header:** `Authorization` = `Bearer <your key>`

Get your key back with:
```bash
aws ssm get-parameter --name /icloud-mail-mcp/bridge/api-key --with-decryption --query Parameter.Value --output text
```

**Cost:** Lambda, SSM and logs for personal use are typically free tier. The template also adds a monthly budget alert (default $5).

## Alternative: self-host with Docker

It also runs as a normal HTTP server, for example on a homelab behind a Cloudflare Tunnel:
```bash
cp .env.example .env   # fill in iCloud user, app password, API key, tunnel token
docker compose up -d --build
```
The container publishes no ports, so only `cloudflared` can reach it. Set `PUBLIC_HOST` to your tunnel hostname to enable DNS-rebinding protection.

## Development

```bash
pip install -r requirements.txt -r requirements-dev.txt
python tests/test_bridge.py    # HTTP server: auth, host checks, every tool, draft threading
python tests/test_lambda.py    # Lambda mode: Function URL events, warm re-invocations
python tests/test_organize.py  # move / trash / flags / folders, safety limits, MOVE fallback
python tests/test_calendar.py  # CalDAV discovery, time zones, repeating and all-day events, read-only checks
cfn-lint template.yaml
```
Tests run the real server against fake IMAP and CalDAV backends. No AWS account or iCloud login needed.

**Layout:**
- `mail.py`: iCloud IMAP client (read-only selects, batch limits, `MOVE` with `COPY`+`UID EXPUNGE` fallback)
- `icloud_calendar.py`: iCloud CalDAV client (read-only, expands repeating events, iCloud-only hosts)
- `server.py`: MCP tools, API-key middleware, transport security
- `lambda_handler.py`: Lambda entry point (Function URL → ASGI via Mangum, fresh app per request)
- `template.yaml`: AWS SAM: function, URL, least-privilege IAM, logs, budget

## Limitations

- Search terms must be plain ASCII (an iCloud IMAP limitation in this version).
- Works only with iCloud Mail. Other IMAP providers would need different host settings.
- No sending, by design.
- Calendar is read-only, and covers at most 31 days per request. Reminders aren't included.

## Roadmap

- [x] iCloud Calendar over CalDAV (read-only)
- [ ] Download attachments to S3
- [ ] Terraform version of the infrastructure

## License

MIT. See [LICENSE](LICENSE).