DEV Community

Qasim
Qasim

Posted on

Connect legacy tools to an agent mailbox over IMAP/SMTP

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.

What you actually get

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:

  • One mailbox, two protocols. The API and the IMAP/SMTP server are two front-ends over the same backend. There is no sync job, no eventual-consistency window worth worrying about, no "API mailbox" versus "client mailbox." It's one mailbox.
  • Legacy tools just work. Anything that can authenticate to an IMAP server with a username and password can read this mailbox. Anything that can submit over SMTP can send from it.
  • Your agent stays on the API. It keeps doing structured things — filtering, classifying, replying in-thread — over REST, and every action it takes is visible to the IMAP client within seconds.

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.

Why this beats running your own IMAP server

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) ownssupport@, notifications@, intake@, the address a workflow lives behind.

Before you begin

You need three things:

  1. A Nylas API key for your application.
  2. A registered domain — either your own custom domain or a Nylas *.nylas.email trial subdomain. New domains warm over roughly four weeks before they're sending at full reputation.
  3. An Agent Account on that domain, and its 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"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 walk through the model end to end. Come back here once you have a grant_id in hand.

Set the app password — the one gate for protocol access

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:

  • 18 to 40 characters.
  • Printable ASCII only (codes 33–126) — no spaces, no control characters.
  • At least one uppercase letter, one lowercase letter, and one digit.

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"
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Rotating the password on an existing account

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"
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

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.

Connect a real client or script

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:

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()))
Enter fullscreen mode Exit fullscreen mode

No SDK, no API key, no grant_id in sight. That's the legacy tool's whole world, and it's enough.

The same mailbox, proven both ways

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"
Enter fullscreen mode Exit fullscreen mode

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

nylas email list --folder INBOX
Enter fullscreen mode Exit fullscreen mode

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."
  }'
Enter fullscreen mode Exit fullscreen mode
nylas email send --to customer@example.com \
  --subject "Re: your ticket" \
  --body "Thanks — we are on it."
Enter fullscreen mode Exit fullscreen mode

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.

What fires when the client touches the mailbox

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"
  }'
Enter fullscreen mode Exit fullscreen mode
nylas webhook create \
  --url https://your-app.example.com/nylas/webhooks \
  --triggers message.created,message.updated
Enter fullscreen mode Exit fullscreen mode

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

  • Don't rely on the webhook payload for the message body — fetch it by id. The Nylas docs differ on whether the body is inline; the safe move is to treat the notification as a pointer and pull the full message with 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.
  • Dedupe on the top-level notification id. Delivery is at-least-once: the same event can arrive up to three times. The top-level notification 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.
  • Verify the signature. Each webhook carries an X-Nylas-Signature header: a hex HMAC-SHA256 of the raw request body using your webhook secret. Compare it with a constant-time compare — and if you're using Node's 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.

Guardrails and the things that'll bite you

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

What's next

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:

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.

AI-answer pages for agents

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

Top comments (0)