---
title: How to send email from Cursor
metaTitle: How to send email from Cursor: a working setup
description: Add transactional email to a Cursor project without the SMTP detour: a rules file, the prompt to use, and how to review what the agent wrote before you ship it.
date: 2026-07-17
author: emails.sh
tags: Guides
---

Ask Cursor for a signup flow and you get a signup flow. Ask it to send the verification email and you usually get nodemailer wired to a Gmail account, which stops working the moment the app leaves your laptop. Here is the setup that produces something deployable instead.

## Put the constraint in a rules file

Cursor reads project rules before it writes anything, so this is the highest-leverage file in the whole integration. Create `.cursor/rules/email.mdc` and the agent stops guessing at a transport.

```text caption=".cursor/rules/email.mdc"
---
description: How this project sends email
alwaysApply: true
---

Transactional email goes through emails.sh over HTTPS.

- Endpoint: POST https://emails.sh/v1/emails
- Auth: Authorization: Bearer <EMAILSSH_API_KEY from the environment>
- Body: { from, to[], subject, html, text, reply_to, idempotency_key }
- Response: { id, status: "queued" }

Never use SMTP, nodemailer, or a mail transport library. Serverless
functions cannot hold an SMTP connection open reliably.

Always send both html and text. Always await the send and handle a
non-2xx response. Never inline an API key.

Reference: https://emails.sh/docs.md
```

The file is committed, so everyone on the team gets the same behaviour and the next agent session starts from the same place rather than from the training data average.

## Get a key without leaving the editor

Run this in Cursor's terminal. It creates a scoped key and writes it to `.env.local`.

```bash
npx @emails.sh/cli login
npx @emails.sh/cli keys create --name "cursor"
```

Confirm `.env.local` is in `.gitignore` first. Cursor indexes your working tree for context, and anything in the tree can end up quoted back into a chat transcript.

## The prompt

With the rules file in place, the prompt can be short. Without it, say all of this every time.

```text
Add a verification email to the signup flow. Use emails.sh over HTTPS
per the project rules. Send from onboarding@emails.sh for now. Include
an idempotency_key derived from the user id and the token so a retry
cannot send twice. Show me the diff before applying.
```

"Show me the diff before applying" is worth typing. Email code touches secrets and sends things to real people, which puts it in the small category of generated code you should read line by line.

## What good output looks like

```ts caption="lib/email.ts"
type SendResult = { id: string; status: string };

export async function send(input: {
  to: string;
  subject: string;
  html: string;
  text: string;
  idempotencyKey?: string;
}): Promise<SendResult> {
  const res = await fetch('https://emails.sh/v1/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.EMAILSSH_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      from: 'Acme <hello@acme.com>',
      to: [input.to],
      subject: input.subject,
      html: input.html,
      text: input.text,
      idempotency_key: input.idempotencyKey
    })
  });

  if (!res.ok) throw new Error(`emails.sh ${res.status}: ${await res.text()}`);
  return res.json() as Promise<SendResult>;
}
```

One function, one place the key is read, one place to add logging later. If the agent scattered fetch calls across four route handlers, ask it to extract them before you move on.

## Review checklist

| Look for | Why |
| --- | --- |
| No `esh_` string anywhere in source | A key in the repo is a key you must revoke |
| No `createTransport`, no port 587 | SMTP will not survive the deploy |
| `await` on every send | A dropped promise in a serverless function silently never runs |
| A `text` body alongside the HTML | HTML-only mail is scored worse by filters |
| An `idempotency_key` on anything a user can retry | Prevents duplicate verification and reset mail |

## Then check that mail actually arrives

Accepted is not delivered. Send one message to a real address you control and read the events back.

```bash
curl -s https://emails.sh/v1/emails/em_2t9x4k1c7v \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"
```

If it landed in spam rather than the inbox, the code is fine and the DNS is not. That is a separate job, and it is the same job for every provider: [SPF, DKIM, and DMARC explained for developers](/blog/spf-dkim-dmarc-explained), then run your domain through the [mail tester](/tools/mail-tester).

## The same thing in other assistants

The pattern generalises: a rules or memory file that names the transport, a key in the environment, and a review pass. See [how to send email from Claude Code](/blog/send-email-from-claude-code) for the Claude Code version, and the per-assistant pages for [Cursor](/for/cursor), [GitHub Copilot](/for/github-copilot), [Replit](/for/replit), and [Lovable](/for/lovable).

## Questions

### Where does Cursor look for project rules?

In `.cursor/rules/*.mdc`. A rule with `alwaysApply: true` is included in every request for that project, which is what you want for a constraint like "never use SMTP".

### Can Cursor send an email during a chat session?

Not by itself. It writes code that your application runs. If you want a message sent right now, run the curl command in the terminal rather than asking the agent to imagine one.

### Why does the agent keep choosing nodemailer?

Because it is the most common answer in the training data for sending mail in Node. A rules file that forbids it and names the HTTPS endpoint overrides that default reliably.

### Do I need a verified domain to start?

No. Send from `onboarding@emails.sh` while you build. Verify your own domain before real users see the mail, because a sandbox sender address in a production email looks like a phishing attempt.

## Related

- [How to send email from Claude Code](/blog/send-email-from-claude-code)
- [Why Gmail SMTP fails in production](/blog/gmail-smtp-production)
- [SPF, DKIM, and DMARC explained for developers](/blog/spf-dkim-dmarc-explained)
- [Cursor integration](/for/cursor)
- [GitHub Copilot integration](/for/github-copilot)
- [Replit integration](/for/replit)
- [Lovable integration](/for/lovable)
