# How to list Exchange email messages

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

Exchange on-premises servers are still common in enterprise environments. If your users run self-hosted Exchange (2007 or later), Nylas connects to them through Exchange Web Services (EWS), a separate protocol from the Microsoft Graph API used for Exchange Online and Microsoft 365.

The same [Messages API](/docs/reference/api/messages/) you use for Gmail and Outlook works for Exchange on-prem accounts. This guide covers the EWS-specific details: when to use EWS vs. Microsoft Graph, authentication, message ID behavior, and search capabilities.

## How do I list Exchange messages with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/messages` with your API key. The endpoint returns the 50 most recent Exchange messages by default and up to 200 per page in a unified schema. On EWS, a message's ID changes when it moves between folders, so match on the internet message ID instead. See [List messages](#list-messages) for the request and response.

## EWS vs. Microsoft Graph: which one?

This is the first thing to figure out. The two provider types target different Exchange deployments:

| Provider type         | Connector   | Use when                                                                             |
| --------------------- | ----------- | ------------------------------------------------------------------------------------ |
| Microsoft Graph       | `microsoft` | Exchange Online, Microsoft 365, Office 365, Outlook.com, personal Microsoft accounts |
| Exchange Web Services | `ews`       | Self-hosted Exchange servers (on-premises)                                           |

If the user's mailbox is hosted by Microsoft in the cloud, use the [Microsoft guide](/docs/cookbook/email/messages/list-messages-microsoft/) instead. The `ews` connector is specifically for organizations that run their own Exchange servers.

> **Warn:** 
> **Microsoft announced EWS retirement** and recommends migrating to Microsoft Graph. However, many organizations still run on-premises Exchange servers where EWS is the only option. Nylas continues to support EWS for these environments.

## Why use Nylas instead of EWS directly?

EWS is a SOAP-based XML API. Every request requires building XML SOAP envelopes, every response needs XML parsing, and errors come back as SOAP faults with nested XML structures. You need to handle autodiscovery to find the right server endpoint (which is frequently misconfigured in enterprise environments), manage authentication with support for two-factor app passwords, and work with EWS-specific data formats that don't match any other email provider.

Nylas replaces all of that with a JSON REST API. No XML, no WSDL, no SOAP. Authentication and autodiscovery are handled automatically. Your code stays the same whether you're reading from Exchange on-prem, Exchange Online, Gmail, or any IMAP provider.

If you have deep EWS experience and only target Exchange on-prem, you can integrate directly. For multi-provider support or faster time-to-integration, Nylas is the simpler path.

## 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 Exchange on-premises account
- An EWS connector configured with the appropriate scopes
- The Exchange server accessible from outside the corporate network (not behind a VPN or firewall that blocks external access)


> **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.


### Exchange authentication setup

Create an EWS connector with the scopes your app needs:

| Scope           | Access                                |
| --------------- | ------------------------------------- |
| `ews.messages`  | Email API (messages, drafts, folders) |
| `ews.calendars` | Calendar API                          |
| `ews.contacts`  | Contacts API                          |

During authentication, users sign in with their Exchange credentials, typically the same username and password they use for Windows login. The username format is usually `user@example.com` or `DOMAIN\username`.

If EWS autodiscovery is configured on the server, authentication works automatically. If not, users can click "Additional settings" and manually enter the Exchange server address (e.g., `mail.company.com`).

> **Info:** 
> **Users with two-factor authentication** must generate an app password instead of using their regular password. See [Microsoft's app password documentation](https://support.microsoft.com/en-us/help/12409/) for instructions.

The full setup walkthrough is in the [Exchange on-premises provider guide](/docs/provider-guides/exchange-on-prem/).

### Network requirements

The Exchange server must be accessible from Nylas's infrastructure:

- **EWS must be enabled** on the server and exposed outside the corporate network
- If the server is behind a **firewall**, you'll need to allow Nylas's IP addresses. [Static IP routing](/docs/dev-guide/platform/#static-ips) requires an annual contract. [Contact sales](https://www.nylas.com/contact-sales/) to upgrade your plan
- If EWS isn't enabled, Nylas can fall back to **IMAP** for email-only access, but you lose calendar and contacts support

Accounts in admin groups are not supported.

## 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 Exchange messages from the terminal

Exchange on-premises is the one mailbox type with no modern REST API. It speaks EWS, an XML and SOAP protocol that Microsoft is retiring. The [Nylas CLI](https://cli.nylas.com/docs/commands) hides it entirely: once a grant connects to a self-hosted Exchange server (2007 or later), `nylas email list` prints the inbox from your terminal with no XML in sight.


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.


One EWS quirk matters the moment you script against it: a message's ID changes when it moves between folders. An ID you captured from one `email list` call goes stale once the message moves to another folder or the archive. Match on the stable internet message ID when you need to follow a message across folders.

```bash
# List the inbox as JSON; the id field changes if a message later moves folders
nylas email list --json

# List the account's folders to see where Exchange files mail
nylas email folders list
```

To send from the Exchange account instead of reading, the [Send email from the terminal](https://cli.nylas.com/guides/send-email-from-terminal) guide walks through the send commands.

## Filter messages

You can narrow results with query parameters. Here's what works with Exchange 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`

Exchange supports the `search_query_native` parameter using Microsoft's [Advanced Query Syntax (AQS)](https://learn.microsoft.com/en-us/windows/win32/lwef/-search-2x-wds-aqsreference). You can combine `search_query_native` with any query parameter **except** `thread_id`.

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

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

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

> **Warn:** 
> **AQS queries must be URL-encoded.** For example, `subject:invoice` becomes `subject%3Ainvoice` in the URL. The SDKs handle this automatically, but you'll need to encode manually in curl requests.

> **Warn:** 
> **Exchange doesn't support searching by BCC field.** If you include BCC in a `search_query_native` query, results may be incomplete or return an error.

The Exchange server must have the **AQS parser enabled** and **search indexing active** for `search_query_native` to work. If queries aren't returning expected results, the Exchange admin should verify these settings.

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

## Things to know about Exchange

Exchange on-prem behaves differently from Exchange Online (Microsoft Graph) in several important ways.

### Message IDs change when messages move

This is the most important Exchange-specific behavior. When a message is moved from one folder to another, **its Nylas message ID changes**. This is expected EWS behavior because Exchange assigns new IDs when messages change folders.

If your app stores message IDs, treat them as **folder-specific pointers**, not permanent identifiers. For tracking a message across folder moves, use the `InternetMessageId` header instead. It stays stable regardless of which folder the message is in.

To get the `InternetMessageId`, include the `fields=include_basic_headers` query parameter. This is the right option for EWS — it returns `Message-ID`, `In-Reply-To`, and `References` reliably, whereas `fields=include_headers` only returns headers Nylas generates for MIME on EWS (provider limitation):

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

```js [getHeaders-Node.js SDK]
const message = await nylas.messages.find({
  identifier: grantId,
  messageId: messageId,
  queryParams: {
    fields: "include_basic_headers",
  },
});
```

```python [getHeaders-Python SDK]
message = nylas.messages.find(
    grant_id,
    message_id,
    query_params={
        "fields": "include_basic_headers",
    }
)
```

> **Info:** 
> **Multiple messages can share the same `InternetMessageId`** in Exchange. This happens when copies of a message exist in multiple folders. Use it for correlation, not as a unique key.

### Folder hierarchy with parent_id

Exchange supports nested folders. Nylas returns a flat folder list but includes a `parent_id` field on child folders so you can reconstruct the hierarchy. Use `parent_id` when creating or updating folders to place them in the right location.

Use the [List Folders endpoint](/docs/reference/api/folders/get-folder/) to discover all folders and their hierarchy for an Exchange account.

### Starred messages require Exchange 2010+

The `starred` query parameter only works on Exchange 2010 and later. If you're targeting Exchange 2007, filtering by starred status isn't available.

### Rate limits are admin-configured

Unlike Google and Microsoft's cloud services, Exchange on-prem rate limits are set by the server administrator. Nylas can't predict what they'll be. If the Exchange server throttles a request, Nylas returns a `Retry-After` header with the number of seconds to wait.

For apps that check mailboxes frequently, [webhooks](/docs/v3/notifications/) are the best way to avoid hitting rate limits. Let Nylas notify you of changes instead of polling.

### Date filtering is day-level precision

Exchange processes `received_before` and `received_after` filters at the **day level**, not second-level. Even though Nylas accepts Unix timestamps, Exchange rounds to the nearest day. Results are inclusive of the specified day.

For example, if you filter with `received_after=1706745600` (February 1, 2024 00:00:00 UTC), you'll get all messages from February 1 onward, including messages received earlier that day.

### Search indexing affects query accuracy

Exchange relies on a search index for queries using `to`, `from`, `cc`, `bcc`, and `any_email`. If a message has just arrived but the search index hasn't refreshed yet, it won't appear in filtered results. The search index refresh interval is controlled by the Exchange administrator.

If filtered queries aren't returning recently received messages, this is likely the cause. Unfiltered list requests (no query parameters) always return the latest messages.

### EWS fallback to IMAP

If EWS isn't enabled on the Exchange server, Nylas can still connect via IMAP for email-only access. This means:

- **Email works** with messages, drafts, and folders available
- **Calendar and contacts are not available** because these require EWS
- **The 90-day message cache applies** because IMAP connections use the same caching behavior as other IMAP providers, with `query_imap=true` for older messages

If your users report that calendar or contacts aren't working, verify that EWS is enabled on their Exchange server.

## 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 Exchange email threads](/docs/cookbook/email/threads/list-threads-ews/) to group these messages into conversations
- [List Microsoft messages](/docs/cookbook/email/messages/list-messages-microsoft/) for Microsoft 365 and Outlook accounts on Microsoft Graph
- [List Gmail messages](/docs/cookbook/email/messages/list-messages-google/) for the same task on Google 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` and AQS for Exchange
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [Exchange on-premises provider guide](/docs/provider-guides/exchange-on-prem/) for full Exchange setup including authentication and network requirements
- [Microsoft admin approval](/docs/provider-guides/microsoft/admin-approval/) to configure consent for enterprise organizations (Exchange Online)