---
title: "Email — Adapters, SMTP, Resend, Attachments"
aliases: [payload-email, payloadcms-email, nodemailer-payload]
tags: [payloadcms, email, nodemailer, resend, smtp, attachments]
sources: [raw/email__overview.md]
created: 2026-05-15
updated: 2026-05-15
---

## Overview

Payload uses an **adapter pattern** for email. Pass an adapter into `email` property of `buildConfig`. Without it, Payload logs a warning on startup and on every `sendEmail` call.

Two official adapters:

| Adapter | Package | Best for |
|---------|---------|----------|
| **Nodemailer** | `@payloadcms/email-nodemailer` | SMTP, SendGrid, any Nodemailer transport; easy v2→v3 migration |
| **Resend** | `@payloadcms/email-resend` | Vercel / serverless — much lighter than Nodemailer |

Both require at minimum: `defaultFromAddress` + `defaultFromName`.

---

## Nodemailer Adapter

### SMTP via `transportOptions`

```ts
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'

export default buildConfig({
  email: nodemailerAdapter({
    defaultFromAddress: 'info@example.com',
    defaultFromName: 'My App',
    transportOptions: {
      host: process.env.SMTP_HOST,
      port: 587,
      auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
    },
  }),
})
```

### Pre-created transport via `transport`

```ts
import nodemailer from 'nodemailer'

email: nodemailerAdapter({
  defaultFromAddress: 'info@example.com',
  defaultFromName: 'My App',
  transport: nodemailer.createTransport({ /* ... */ }),
})
```

### SendGrid custom transport

```ts
import nodemailerSendgrid from 'nodemailer-sendgrid'

email: nodemailerAdapter({
  defaultFromAddress: 'info@example.com',
  defaultFromName: 'My App',
  transportOptions: nodemailerSendgrid({ apiKey: process.env.SENDGRID_API_KEY }),
})
```

### Dev mode (no config)

Calling `nodemailerAdapter()` with no args uses **ethereal.email** — logs credentials to console. Safe for local development.

```ts
email: nodemailerAdapter()
```

---

## Resend Adapter

Preferred for **Vercel / serverless**. Requires only an API key.

```ts
import { resendAdapter } from '@payloadcms/email-resend'

email: resendAdapter({
  defaultFromAddress: 'dev@example.com',
  defaultFromName: 'My App',
  apiKey: process.env.RESEND_API_KEY || '',
})
```

---

## Sending Email

Available anywhere `payload` instance is accessible:

```ts
await payload.sendEmail({
  to: 'user@example.com',
  subject: 'Hello',
  html: '<p>Body</p>',   // or: text: 'Body'
})
```

---

## Attachments

### Nodemailer — file path or Buffer

```ts
await payload.sendEmail({
  to: 'user@example.com',
  subject: 'Report',
  html: '<p>See attached.</p>',
  attachments: [
    { filename: 'invoice.pdf', path: '/var/data/invoice.pdf', contentType: 'application/pdf' },
    { filename: 'report.csv', content: Buffer.from('col1,col2\nA,B\n'), contentType: 'text/csv' },
  ],
})
```

Supports anything Nodemailer supports: streams, Buffers, URLs, inline CID images.

### Resend — remote URL or Base64

```ts
// Remote URL (Resend fetches it)
attachments: [{ path: 'https://example.com/invoice.pdf', filename: 'invoice.pdf' }]

// Local file — must be Base64
import { readFile } from 'node:fs/promises'
const pdf = await readFile('/var/data/invoice.pdf')
attachments: [{ filename: 'invoice.pdf', content: pdf.toString('base64') }]
```

---

## Attaching Files from Payload Media Collections

### Local storage + Nodemailer

```ts
const doc = await payload.findByID({ collection: 'media', id })
await payload.sendEmail({
  attachments: [{ filename: doc.filename, path: doc.url, contentType: doc.mimeType }],
})
```

### Cloud storage (S3/Azure/GCS) + Nodemailer

```ts
const response = await fetch(doc.url)
const buffer = Buffer.from(await response.arrayBuffer())
attachments: [{ filename: doc.filename, content: buffer, contentType: doc.mimeType }]
```

### Resend + cloud storage

```ts
// Resend fetches the URL directly — no download needed
attachments: [{ filename: doc.filename, path: doc.url }]
```

### Resend + local storage

```ts
import { readFile } from 'node:fs/promises'
const buf = await readFile(doc.url)
attachments: [{ filename: doc.filename, content: buf.toString('base64') }]
```

---

## Key Takeaways

- **Adapter pattern** — swap Nodemailer ↔ Resend without changing send-call code
- **Resend for serverless** — significantly lighter bundle than Nodemailer; use on Vercel
- **Nodemailer for SMTP/SendGrid** — `transportOptions` object or pre-built `transport`; any Nodemailer transport works
- **Dev shortcut** — `nodemailerAdapter()` with no args → ethereal.email auto-config
- **Attachments differ by adapter**: Nodemailer accepts file paths/Buffers/streams; Resend expects Base64 for local files, URL for remote
- **Multiple providers** — Payload only supports one `email` config, but you can use additional transports manually inside hooks
- **Auth emails** — password reset, email verification use same adapter; see [[wiki/payloadcms/authentication-email|Authentication — Email Verification & Password Reset]]

---

## Related

- [[wiki/payloadcms/authentication-email|Authentication — Email Verification & Password Reset]]
- [[wiki/payloadcms/hooks|Hooks]] — trigger custom emails via collection hooks
- [[wiki/payloadcms/upload|Upload & Media]] — attaching media collection files to emails
- [[wiki/payloadcms/configuration|Payload Config — Overview]] — `buildConfig` top-level options

---

## Sources

- `raw/email__overview.md` — compiled 2026-05-15
- https://payloadcms.com/docs/email/overview
