# How to list iCloud email messages

Source: https://developer.nylas.com/docs/cookbook/email/messages/list-messages-icloud/

Apple doesn't offer a public email API for iCloud Mail. There's no REST interface, no SDK, and no developer portal for mail. The only programmatic access is raw IMAP, and even that requires each user to manually create an app-specific password through their Apple ID settings. Nylas handles all of that and exposes iCloud through the same [Messages API](/docs/reference/api/messages/) you use for Gmail and Outlook.

This guide covers listing messages from iCloud accounts, including the app-specific password requirement, the 90-day message cache, and iCloud-specific behaviors you should know about.

## Is there an iCloud Mail API?

No. Apple publishes no public REST API or SDK for iCloud Mail, and there's no developer portal for it. The only programmatic access is IMAP for reading and SMTP for sending, and each user must generate a 16-character app-specific password from their Apple ID account page before any client can connect.

That constraint is why most iCloud integrations go through a layer that wraps IMAP. Reading mail still happens over IMAP, which exposes the last 90 days through the [message cache](#the-90-day-message-cache), with older messages reachable on demand. This guide uses a unified REST layer so the same call that lists Gmail or Outlook messages also lists iCloud mail across all 6 providers.

## How do I list iCloud Mail messages with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/messages` using the iCloud connector and your API key. The endpoint returns the 50 most recent messages by default and up to 200 per page. iCloud is IMAP-backed, so only messages within the 90-day sync cache are searchable server-side. See [List messages](#list-messages) for the request and response.

## Why use Nylas instead of IMAP directly?

iCloud's biggest friction point for developers is the app-specific password requirement. There's no way to generate these programmatically, so every user must log in to their Apple ID, open the security settings, and manually create a password before your app can connect. On top of that, you carry the full IMAP burden.

That means building the same infrastructure as any other IMAP integration: connection management, MIME parsing, sync state tracking, and a separate SMTP connection for sending.

Nylas handles the IMAP connection and guides users through the app password flow during authentication. You get a REST API with JSON responses, automatic sync and caching, and the same code works across iCloud, Gmail, Outlook, Yahoo, and every other provider.

If you're comfortable with raw IMAP and only targeting iCloud, you can connect directly. For multi-provider apps or faster development, Nylas saves you significant time.

## Before you begin

You'll need:

- A [Nylas application](/docs/v3/getting-started/) with a valid API key
- A [grant](/docs/v3/auth/) for an iCloud Mail account
- An iCloud connector configured in your Nylas application


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


### iCloud authentication setup

iCloud requires **app-specific passwords** for third-party email access. Unlike Google or Microsoft OAuth, there's no way to generate these programmatically. Each user must create one manually in their Apple ID settings.

Nylas supports two authentication flows for iCloud:

| Method                              | Best for                                                               |
| ----------------------------------- | ---------------------------------------------------------------------- |
| Hosted OAuth                        | Production apps where Nylas guides users through the app password flow |
| Bring Your Own (BYO) Authentication | Custom auth pages where you collect credentials directly               |

With either method, users need to:

1. Go to [appleid.apple.com](https://appleid.apple.com/) and sign in
2. Navigate to **Sign-In and Security** then **App-Specific Passwords**
3. Generate a new app password
4. Use that password (not their regular iCloud password) when authenticating

> **Warn:** 
> **App-specific passwords can't be generated through the API.** Your app's onboarding flow should include clear instructions telling users how to create one. Users who enter their regular iCloud password will fail authentication.

The full setup walkthrough is in the [iCloud provider guide](/docs/provider-guides/icloud/) and the [app passwords guide](/docs/provider-guides/app-passwords/).

## List messages

Make a [List Messages request](/docs/reference/api/messages/get-messages/) with the grant ID. By default, Nylas returns the 50 most recent messages. These examples limit results to 5:

```bash
curl --compressed --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=5" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json'

```

```json
{
  "request_id": "d0c951b9-61db-4daa-ab19-cd44afeeabac",
  "data": [
    {
      "starred": false,
      "unread": true,
      "folders": ["UNREAD", "CATEGORY_PERSONAL", "INBOX"],
      "grant_id": "1",
      "date": 1706811644,
      "attachments": [
        {
          "id": "1",
          "grant_id": "1",
          "filename": "invite.ics",
          "size": 2504,
          "content_type": "text/calendar; charset=\"UTF-8\"; method=REQUEST"
        },
        {
          "id": "2",
          "grant_id": "1",
          "filename": "invite.ics",
          "size": 2504,
          "content_type": "application/ics; name=\"invite.ics\"",
          "is_inline": false,
          "content_disposition": "attachment; filename=\"invite.ics\""
        }
      ],
      "from": [
        {
          "name": "Nylas DevRel",
          "email": "nylasdev@nylas.com"
        }
      ],
      "id": "1",
      "object": "message",
      "snippet": "Send Email with Nylas APIs",
      "subject": "Learn how to Send Email with Nylas APIs",
      "thread_id": "1",
      "to": [
        {
          "name": "Nyla",
          "email": "nyla@nylas.com"
        }
      ],
      "created_at": 1706811644,
      "body": "Learn how to send emails using the Nylas APIs!"
    }
  ],
  "next_cursor": "123"
}


```

```js [listMessages-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

async function fetchRecentEmails() {
  try {
    const messages = await nylas.messages.list({
      identifier: "<NYLAS_GRANT_ID>",
      queryParams: {
        limit: 5,
      },
    });

    console.log("Messages:", messages);
  } catch (error) {
    console.error("Error fetching emails:", error);
  }
}

fetchRecentEmails();


```

```python [listMessages-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"

messages = nylas.messages.list(
  grant_id,
  query_params={
    "limit": 5
  }
)

print(messages)

```


```ruby [listMessages-Ruby SDK]
require 'nylas'

nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')
query_params = { limit: 5 }
messages, _ = nylas.messages.list(identifier: '<NYLAS_GRANT_ID>', query_params: query_params)

messages.each {|message|
  puts "[#{Time.at(message[:date]).strftime("%d/%m/%Y at %H:%M:%S")}] \
      #{message[:subject]}"
}
```

```java [listMessages-Java SDK]
import com.nylas.NylasClient;
import com.nylas.models.*;
import java.text.SimpleDateFormat;

public class ListMessages {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
    ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder().limit(5).build();
    ListResponse<Message> message = nylas.messages().list("<NYLAS_GRANT_ID>", queryParams);

    for(Message email : message.getData()) {
      String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").
          format(new java.util.Date((email.getDate() * 1000L)));

      System.out.println("[" + date + "] | " + email.getSubject());
    }
  }
}
```

```kt [listMessages-Kotlin SDK]
import com.nylas.NylasClient
import com.nylas.models.*
import java.text.SimpleDateFormat
import java.util.*

fun main(args: Array<String>) {
    val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")
    val queryParams = ListMessagesQueryParams(limit = 5)
    val messages = nylas.messages().list("<NYLAS_GRANT_ID>", queryParams).data

    for (message in messages) {
        val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .format(Date(message.date.toLong() * 1000))
        println("[$date] | ${message.subject}")
    }
}
```


## List iCloud messages from the terminal

Apple ships no email API, so iCloud Mail is IMAP-only and every user must create a 16-character app-specific password before any client connects. Once that grant exists, the [Nylas CLI](https://cli.nylas.com/docs/commands) reads the mailbox like any other connected account, with no IMAP client code on your side. The `nylas email list` command pulls the inbox straight from your terminal.


The Nylas CLI mirrors the Messages API, so you can read the same inbox from your terminal without writing any code. After `nylas init` and `nylas auth login`, the `email list` command returns the 10 most recent inbox messages by default and pages through everything automatically once `--limit` goes over 200:

```bash
# List the 10 most recent inbox messages
nylas email list

# Show only unread messages from a specific sender
nylas email list --unread --from billing@vendor.com

# Fetch everything across all folders, paginated automatically
nylas email list --all --all-folders --max 500
```

To find specific messages rather than list them, `email search` runs a full-text query with its own filters for sender (`--from`), date range (`--after` and `--before`), and attachments (`--has-attachment`). Search returns 20 results by default and only fetches more than one page once `--limit` goes over 200:

```bash
# Full-text search restricted to one sender, attachments only
nylas email search "invoice" --from billing@vendor.com --has-attachment
```

Both commands accept `--json`, so you can pipe results into `jq` or a script. See the [`email list`](https://cli.nylas.com/docs/commands/email-list) and [`email search`](https://cli.nylas.com/docs/commands/email-search) command reference for every flag.


iCloud folder names aren't fixed. They vary by account and can be locale-specific, so a German account stores trash under `Papierkorb` rather than `Deleted Messages`. The Apple Mail display name also won't always match the IMAP folder on the server. List each account's real names first, then read one by name:

```bash
# Folder names vary per account, so list the real ones first
nylas email folders list

# Read a folder by its actual iCloud name
nylas email list --folder "Sent Messages"
```

iCloud keeps roughly 90 days of mail searchable through the cache. iCloud sending runs through the same CLI. See [Send email from the terminal](https://cli.nylas.com/guides/send-email-from-terminal) for the send commands.

## Filter messages

You can narrow results with query parameters. Here's what works with iCloud accounts:


| Parameter         | What it does                  | Example                       |
| ----------------- | ----------------------------- | ----------------------------- |
| `subject`         | Match on subject line         | `?subject=Weekly standup`     |
| `from`            | Filter by sender              | `?from=alex@example.com`      |
| `to`              | Filter by recipient           | `?to=team@company.com`        |
| `unread`          | Unread only                   | `?unread=true`                |
| `in`              | Filter by folder or label ID  | `?in=INBOX`                   |
| `received_after`  | After a Unix timestamp        | `?received_after=1706000000`  |
| `received_before` | Before a Unix timestamp       | `?received_before=1706100000` |
| `has_attachment`  | Only results with attachments | `?has_attachment=true`        |


Here's how to combine filters. This pulls unread messages from a specific sender:


```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?from=alex@example.com&unread=true&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [filterMessages-Node.js SDK]
const messages = await nylas.messages.list({
  identifier: grantId,
  queryParams: {
    from: "alex@example.com",
    unread: true,
    limit: 10,
  },
});
```

```python [filterMessages-Python SDK]
messages = nylas.messages.list(
    grant_id,
    query_params={
        "from": "alex@example.com",
        "unread": True,
        "limit": 10,
    }
)
```


### Search with `search_query_native`

iCloud supports the `search_query_native` parameter for IMAP-style search. Like Yahoo, iCloud isn't subject to the `in`, `limit`, and `page_token` co-parameter restriction that Google and Microsoft enforce, so you can pair it with standard filters like `from` and `subject`.

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?search_query_native=subject:invoice&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [nativeSearchIcloud-Node.js SDK]
const messages = await nylas.messages.list({
  identifier: grantId,
  queryParams: {
    searchQueryNative: "subject:invoice",
    limit: 10,
  },
});
```

```python [nativeSearchIcloud-Python SDK]
messages = nylas.messages.list(
    grant_id,
    query_params={
        "search_query_native": "subject:invoice",
        "limit": 10,
    }
)
```

> **Warn:** 
> **Some IMAP providers don't fully support the `SEARCH` operator.** If `search_query_native` returns unexpected results or a `400` error, fall back to standard query parameters like `subject`, `from`, and `to` instead.

See the [search best practices guide](/docs/dev-guide/best-practices/search/) for more on `search_query_native` across providers.

## Things to know about iCloud

iCloud is IMAP-based, which means it shares some behaviors with Yahoo and other IMAP providers. But Apple has its own quirks too.

### The 90-day message cache

Nylas maintains a rolling cache of messages from the last 90 days for all IMAP-based providers, including iCloud. Anything received or created within that window is synced and available through the API. For older messages, set `query_imap=true` to query iCloud's IMAP server directly. This is slower but gives you access to the full mailbox.


```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?query_imap=true&in=INBOX&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [queryImap-Node.js SDK]
const messages = await nylas.messages.list({
  identifier: grantId,
  queryParams: {
    queryImap: true,
    in: "INBOX",
    limit: 10,
  },
});
```

```python [queryImap-Python SDK]
messages = nylas.messages.list(
    grant_id,
    query_params={
        "query_imap": True,
        "in": "INBOX",
        "limit": 10,
    }
)
```

When using `query_imap`, you must include the `in` parameter to specify which folder to search.


### Webhooks don't cover old messages

Nylas [webhooks](/docs/v3/notifications/) only fire for changes to messages within the 90-day cache window. If a user modifies or deletes a message older than 90 days, you won't receive a notification. Plan your sync strategy accordingly if your app needs to track changes across the full mailbox.


### iCloud folder names

iCloud uses standard IMAP folder names, but the exact names can vary. Some accounts use locale-specific names (for example, "Papierkorb" instead of "Trash" on German accounts), and the display name in Apple Mail may not always match the IMAP folder name on the server.

Common folder names on English-language accounts:

| Apple Mail UI name | Folder name        |
| ------------------ | ------------------ |
| Inbox              | `INBOX`            |
| Sent               | `Sent Messages`    |
| Drafts             | `Drafts`           |
| Trash              | `Deleted Messages` |
| Junk               | `Junk`             |
| Archive            | `Archive`          |

Don't hardcode these. Always use the [List Folders endpoint](/docs/reference/api/folders/get-folder/) to discover the exact folder names for each account.

### Sending rate limits

Apple publishes specific sending limits for iCloud Mail:

| Limit                  | Value |
| ---------------------- | ----- |
| Messages per day       | 1,000 |
| Recipients per day     | 1,000 |
| Recipients per message | 500   |
| Max message size       | 20 MB |

These are sending limits, not read limits. Nylas handles retries if you hit throttling on reads, but if your app sends email through iCloud accounts, keep these limits in mind. Source: [Apple support documentation](https://support.apple.com/en-gb/102198).

### No unsubscribe headers

iCloud doesn't support `List-Unsubscribe-Post` or `List-Unsubscribe` custom headers when sending email. If your app sends subscription-style email through iCloud accounts, you'll need to handle unsubscribe links in the message body instead. This limitation is shared with some Microsoft Graph accounts.

### Contacts are disabled by default

The Contacts API is disabled by default for iCloud grants. If your app needs to access iCloud contacts, contact [Nylas Support](https://support.nylas.com/) to enable it. When enabled, contacts are parsed from email headers (From, To, CC, BCC, Reply-To fields) rather than synced from an address book.

### Sync and threading

iCloud accounts use IMAP for reading and SMTP for sending. This is invisible to your code, but it affects a few behaviors:

- **Sync relies on IMAP idle.** Nylas maintains low-bandwidth connections to monitor the Inbox and Sent folders. Other folders are checked periodically, so changes outside Inbox and Sent may take a few minutes to appear.
- **Message IDs** are IMAP UIDs, which are numeric values unique within a folder but not globally unique across the account. If you need a stable identifier across folders, use the `Message-ID` header.
- **Thread grouping** relies on subject-line and `In-Reply-To` header matching. This works well for most conversations but isn't as precise as Gmail's native threading.

### iCloud connector vs. generic IMAP

You can authenticate iCloud accounts two ways in Nylas:

| Method           | Provider type | What you get                            |
| ---------------- | ------------- | --------------------------------------- |
| iCloud connector | `icloud`      | Email through IMAP plus calendar through CalDAV |
| Generic IMAP     | `imap`        | Email only, no calendar access          |

Use the dedicated iCloud connector if your app needs calendar access alongside email. The generic IMAP connector works for email-only use cases but doesn't include CalDAV support.

## Paginate through results


The Messages API returns paginated responses. When there are more results, the response includes a `next_cursor` value. Pass it back as `page_token` to get the next page:

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=10&page_token=<NEXT_CURSOR>" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [paginateMessages-Node.js SDK]
let pageCursor = undefined;

do {
  const result = await nylas.messages.list({
    identifier: grantId,
    queryParams: {
      limit: 10,
      pageToken: pageCursor,
    },
  });

  // Process result.data here

  pageCursor = result.nextCursor;
} while (pageCursor);
```

```python [paginateMessages-Python SDK]
page_cursor = None

while True:
    query = {"limit": 10}
    if page_cursor:
        query["page_token"] = page_cursor

    result = nylas.messages.list(grant_id, query_params=query)

    # Process result.data here

    page_cursor = result.next_cursor
    if not page_cursor:
        break
```

Keep paginating until the response comes back without a `next_cursor`.


## What's next

- [List iCloud email threads](/docs/cookbook/email/threads/list-threads-icloud/) to group these messages into conversations
- [List Gmail messages](/docs/cookbook/email/messages/list-messages-google/) for the same task on Google accounts
- [List Microsoft messages](/docs/cookbook/email/messages/list-messages-microsoft/) for the same task on Microsoft 365 accounts
- [Messages API reference](/docs/reference/api/messages/) for full endpoint documentation and all available parameters
- [Using the Messages API](/docs/v3/email/messages/) for search, modification, and deletion
- [Threads](/docs/v3/email/threads/) to group related messages into conversations
- [Search best practices](/docs/dev-guide/best-practices/search/) for advanced search with `search_query_native` across providers
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [iCloud provider guide](/docs/provider-guides/icloud/) for full iCloud setup including authentication
- [App passwords guide](/docs/provider-guides/app-passwords/) for generating app-specific passwords for iCloud and other providers
- [IMAP provider guide](/docs/provider-guides/imap/) for general IMAP configuration and behavior