# Connect legacy tools to an agent mailbox over IMAP/SMTP

> Source: <https://dev.to/mqasimca/connect-legacy-tools-to-an-agent-mailbox-over-imapsmtp-14ch>
> Published: 2026-07-17 21:18:08+00:00

Most "AI email" integrations assume everything on the other side speaks REST. You wire up a webhook, you call `POST /messages/send`

, and you move on. That works right up until you remember how much of your stack *doesn't* speak REST and never will: the ticketing system that ingests mail over IMAP, the backup script your predecessor wrote in 2014, the monitoring tool that only knows how to send SMTP, the compliance archiver that polls a mailbox every five minutes. None of those are getting rewritten to call an HTTP API for your demo.

So here's the trick that makes a Nylas **Agent Account** genuinely useful in a real environment: it's not API-only. You can expose the *same* mailbox over **IMAP** and **SMTP submission**, hand the host, port, and credentials to one of those legacy tools, and let it read and send like it's talking to any old mail server. Meanwhile your agent drives that identical mailbox over the v3 API. Both surfaces hit one storage layer. A flag, move, or delete on either side shows up on the other within seconds.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. As usual I'll show both angles for every operation — the raw `curl`

against the API and the `nylas`

command — because half the point of an Agent Account is that you can mix them freely.

An Agent Account is just a **grant**. It has a `grant_id`

, and that `grant_id`

works with every grant-scoped endpoint you already know — Messages, Drafts, Threads, Folders, Attachments, Contacts, Calendars, Events. There's nothing new to learn on the data plane.

The IMAP/SMTP layer doesn't change that model. It adds a second door into the same room:

That last point is the one that surprises people, so I'll be blunt about it later: this is shared mutable state across two protocols. It's a feature, but you should design for it.

You could, in theory, stand up Dovecot, bolt on Postfix, wire in DKIM and SPF and DMARC, warm an IP, and expose that to your legacy tools yourself. People do. It's also a multi-week project with an on-call rotation attached.

With an Agent Account you skip all of it. Nylas hosts the mailbox, runs the IMAP and SMTP submission servers, handles TLS, manages deliverability, and gives you the API on top. The part I like as an SRE: there's no OAuth token to refresh. A connected Gmail or Microsoft grant lives and dies by its refresh token. An Agent Account has no upstream provider, so the grant doesn't silently expire on you at 3 a.m. — it just keeps being a mailbox.

The honest tradeoff: this is a Nylas-hosted mailbox, not your existing Google Workspace or Microsoft 365 account. If your goal is to put an agent on a *human's* real inbox, that's a connected-grant problem, not this. Agent Accounts are for mailboxes the agent (and your tooling) *owns* — `support@`

, `notifications@`

, `intake@`

, the address a workflow lives behind.

You need three things:

`*.nylas.email`

trial subdomain. New domains warm over roughly four weeks before they're sending at full reputation.`grant_id`

.If you don't have the account yet, create one. Over the API it's a `POST /v3/connect/custom`

with `"provider": "nylas"`

:

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Support Bot",
    "settings": {
      "email": "support@yourcompany.com"
    }
  }'
```

The response carries the `grant_id`

. The optional top-level `name`

sets the display name on outbound mail. No refresh token, no redirect dance.

From the CLI it's one line:

```
nylas agent account create support@yourcompany.com --name "Support Bot"
```

The API auto-creates a default workspace and policy for the account, so you don't pass a workspace on create — there's no `--workspace`

flag. If you later want a custom policy governing limits and spam, you attach it to the workspace with `nylas workspace update <workspace-id> --policy-id <policy-id>`

.

New to Agent Accounts in general? The [Agent Accounts docs](https://developer.nylas.com/docs/v3/agent-accounts/) walk through the model end to end. Come back here once you have a `grant_id`

in hand.

Here's the thing that trips people up first: **API access and protocol access use different credentials.** The API authenticates with your Nylas API key and a `grant_id`

. IMAP and SMTP can't use that — a mail client only knows how to send a username and a password. So protocol access has its own credential: an **app password** set directly on the grant.

Until you set one, Nylas rejects every IMAP `LOGIN`

and SMTP `AUTH`

attempt. The API still works fine without it; the app password gates *only* the protocol door. Set it before you hand connection details to any legacy tool.

The password has rules, and Nylas validates them on write:

Nylas stores it as a bcrypt hash, so you can't read it back later — only reset it. Treat it like any other secret: generate it, stash it in your secrets manager, hand it to the client.

You can set the app password at creation time. Over the CLI, pass `--app-password`

:

```
nylas agent account create support@yourcompany.com \
  --name "Support Bot" \
  --app-password "MySecureP4ssword2024"
```

Over the API, drop it into `settings`

on the same `POST /v3/connect/custom`

call:

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Support Bot",
    "settings": {
      "email": "support@yourcompany.com",
      "app_password": "MySecureP4ssword2024"
    }
  }'
```

When the app password leaks, or you're rotating on schedule, you set a new one on the grant you already have.

From the CLI, `agent account update`

takes the same flag and looks the account up by `grant_id`

or by email:

```
nylas agent account update support@yourcompany.com \
  --app-password "MyRotatedP4ssword2025"
```

Over the API, `PATCH /v3/grants/{grant_id}`

does it — but note one sharp edge: a `PATCH`

replaces the *entire* `settings`

object, so you have to send `email`

alongside the new `app_password`

or you'll wipe it out.

```
curl --request PATCH \
  --url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "settings": {
      "email": "support@yourcompany.com",
      "app_password": "MyRotatedP4ssword2025"
    }
  }'
```

Rotating disconnects every active IMAP and SMTP client immediately — each one reconnects after the user (or the config) supplies the new value. Plan rotations for a quiet window if a legacy tool can't gracefully re-auth.

Once the app password is set, point any IMAP/SMTP tool at the Nylas servers for your region. The credentials are the account's own email address and the app password.

| Setting | Value |
|---|---|
| IMAP host |
`mail.us.nylas.email` (US) · `mail.eu.nylas.email` (EU) |
| IMAP port |
`993` (implicit TLS) |
| SMTP host |
`mail.us.nylas.email` (US) · `mail.eu.nylas.email` (EU) |
| SMTP port |
`465` (implicit TLS) or `587` (STARTTLS) |
| Username | The Agent Account email, e.g. `support@yourcompany.com`
|
| Password | The `app_password` you set on the grant |
| Encryption | TLS required on every port |

In a desktop client like Thunderbird, Apple Mail, or Outlook, that's just the manual-setup screen: server `mail.us.nylas.email`

, IMAP 993 SSL/TLS, SMTP 465 SSL/TLS (or 587 STARTTLS), username and password as above.

For a script, it's whatever your language's IMAP library wants. The point is that it's *ordinary* — here's a throwaway Python check that the credentials work and counts what's in the inbox:

``` python
import imaplib

host = "mail.us.nylas.email"
user = "support@yourcompany.com"
app_password = "MySecureP4ssword2024"  # pull from your secrets manager

with imaplib.IMAP4_SSL(host, 993) as imap:
    imap.login(user, app_password)
    imap.select("INBOX")
    typ, data = imap.search(None, "ALL")
    print("messages in inbox:", len(data[0].split()))
```

No SDK, no API key, no `grant_id`

in sight. That's the legacy tool's whole world, and it's enough.

This is the part worth internalizing, so let me make it concrete. The IMAP client above and the API are looking at one mailbox. Watch what happens when each side acts.

List the inbox over the API:

```
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/messages?in=inbox&limit=5" \
  --header "Authorization: Bearer $NYLAS_API_KEY"
```

Or the CLI equivalent, which resolves the Agent Account grant for you:

```
nylas email list --folder INBOX
```

Now flag a message as read from the IMAP side with a `STORE \Seen`

, or move it to another folder. Re-run that same `nylas email list`

or `GET /messages`

and the `unread`

and `folders`

fields already reflect it. There's no propagation delay you need to code around — it's the same row.

It runs the other direction too. Send from the API:

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/messages/send" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "to": [{ "email": "customer@example.com" }],
    "subject": "Re: your ticket",
    "body": "Thanks — we are on it."
  }'
nylas email send --to customer@example.com \
  --subject "Re: your ticket" \
  --body "Thanks — we are on it."
```

That message lands in the **Sent** folder your IMAP client sees, exactly as if the client had submitted it over SMTP. And a message submitted over SMTP shows up in the API's message list the same way. Replying in-thread works from either side; from the CLI, `nylas email reply <message-id> --body "..."`

preserves the `In-Reply-To`

and `References`

headers so the conversation stays threaded.

The folder mapping is the obvious one: IMAP `INBOX`

/`Sent`

/`Drafts`

/`Trash`

/`Junk`

/`Archive`

map to the API folder IDs `inbox`

/`sent`

/`drafts`

/`trash`

/`junk`

/`archive`

. Custom folders you create over either surface appear on the other. IMAP `APPEND`

even deduplicates on the MIME `Message-ID`

, so a message that's already in the mailbox (because SMTP just put it there) won't get a phantom copy.

Because both surfaces share storage, both fire the **same webhooks** — and this is how your agent stays in the loop on what the legacy tool is doing. A human marks a message read in Thunderbird? `message.updated`

. The archiver moves something? `message.updated`

. New inbound mail, whether your agent or the IMAP client sees it first? `message.created`

.

Webhooks are **application-scoped**, not grant-scoped. You subscribe once at the app level and every grant's events arrive at that endpoint, each payload carrying a `grant_id`

you filter on:

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/webhooks" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "trigger_types": ["message.created", "message.updated"],
    "webhook_url": "https://your-app.example.com/nylas/webhooks",
    "description": "Agent mailbox events"
  }'
nylas webhook create \
  --url https://your-app.example.com/nylas/webhooks \
  --triggers message.created,message.updated
```

A few facts to build correctly against, all of which I've watched people get wrong:

`GET /v3/grants/{grant_id}/messages/{message_id}`

when you actually need the content. Also branch on `message.created.truncated`

— that's the variant you get when a body exceeds ~1 MB and gets omitted.`id`

.`id`

is constant across all retries of one event — that's your dedup key. The inner `data.object.id`

(the message id) identifies the message itself; you can additionally guard on it to avoid acting twice on the same message.`X-Nylas-Signature`

header: a hex HMAC-SHA256 of the `crypto.timingSafeEqual`

, check both buffers are the same length first, because it throws on a length mismatch. The CLI's `nylas webhook verify`

does this check locally while you're developing.Agent Accounts also emit deliverability webhooks for outbound mail — `message.delivered`

, `message.bounced`

, `message.complaint`

, and `message.rejected`

. Worth noting: `message.rejected`

fires specifically when an outbound message is rejected because an attachment contained a virus, not as a generic catch-all rejection signal.

A few honest gotchas before you ship this.

**Shared mutable state is real.** Two protocols writing one mailbox means you can race yourself. If your agent auto-archives messages over the API at the same moment a human is reading them in a client, the message moves out from under them. Decide who owns what. A common split: the agent handles triage and labeling, humans handle replies — or the agent only touches messages in a folder humans don't live in.

**Attachments don't go through SMTP from the API side.** When the agent sends *with* an attachment, that's the Messages or Drafts API, not SMTP. You attach either as Base64 strings in the JSON `attachments`

array, or via multipart where the file form field is named `attachment`

— **not** `file`

, which is the single most common mistake. Note that `nylas email send`

has no attachment flag; for attachments from the CLI you stage a draft with `nylas email drafts create --attach`

and send that.

**Outbound size cap.** SMTP submission enforces a 25 MB ceiling on outbound messages at the `DATA`

command. Over it and the send is rejected with an error naming the limit.

**Connection limits exist.** There's a default cap of 20 concurrent IMAP connections per grant, a 30-minute IDLE timeout (the client re-issues IDLE), and a 5-minute timeout on otherwise-idle TCP connections. If your legacy tool opens a connection per worker, count your workers.

**Free-plan ceilings.** The free plan allows 200 messages per account per day, 3 GB of storage per org, and retains the inbox 30 days and spam 7 days. Fine for building; size up before production volume.

**Rotation disconnects clients.** Said it above, repeating it because it's an operational footgun: rotating the app password drops every live session. Coordinate it with whoever owns the legacy tool's config.

The pattern here is small but powerful: an Agent Account gives you a mailbox your agent drives over a clean v3 API, *and* a standard IMAP/SMTP endpoint for every tool that will never call REST — both pointed at the same storage, both firing the same webhooks. You don't have to choose between "modern API" and "works with the stuff I already run." You get both doors into one room.

From here:

`nylas agent`

, `nylas email`

, and `nylas webhook`

flag.Set the app password, point one real client at the mailbox, and watch a flag you flip in the API show up in the client a second later. That moment is when the two-doors model clicks.

When this post is published, link AI agents and crawlers to the retrieval-ready version on `cli.nylas.com`

:
