The relay connection only had `secure`, so with `secure: false` nodemailer still upgraded to STARTTLS whenever the server advertised it, and there was no way to talk cleartext to such a server. Pass nodemailer's `ignoreTLS` through as `ignoreTLS` on the relay config, wired to --outgoing-ignore-tls / MAILDEV_OUTGOING_IGNORE_TLS / `outgoingIgnoreTls`.
601 lines
13 KiB
Markdown
601 lines
13 KiB
Markdown
# Programmatic API
|
|
|
|
MailDev v3 provides a modern TypeScript API for embedding into your Node.js applications. The API uses async/await patterns throughout.
|
|
|
|
## Quick Start
|
|
|
|
```typescript
|
|
import { MailDev } from 'maildev'
|
|
|
|
const maildev = new MailDev({
|
|
smtp: 1025,
|
|
web: 1080,
|
|
})
|
|
|
|
await maildev.start()
|
|
|
|
// Access server instances
|
|
const servers = maildev.getServers()
|
|
|
|
// Listen for new emails
|
|
servers.smtp.on('new', (email) => {
|
|
console.log('Received:', email.subject)
|
|
})
|
|
|
|
// Stop when done
|
|
await maildev.stop()
|
|
```
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install maildev
|
|
```
|
|
|
|
## MailDev Class
|
|
|
|
The `MailDev` class provides the simplest way to run MailDev programmatically.
|
|
|
|
### Constructor Options
|
|
|
|
```typescript
|
|
import { MailDev } from 'maildev'
|
|
|
|
const maildev = new MailDev({
|
|
// SMTP Server
|
|
smtp: 1025, // SMTP port (default: 1025)
|
|
ip: '::', // SMTP bind address (default: '::')
|
|
|
|
// Web/API Server
|
|
web: 1080, // Web UI port (default: 1080)
|
|
webIp: '0.0.0.0', // Web bind address (default: '0.0.0.0')
|
|
disableWeb: false, // Disable web interface
|
|
basePathname: '/', // Base path for web interface
|
|
|
|
// Storage
|
|
mailDirectory: '/tmp/maildev', // Persist emails to disk (optional)
|
|
maxEmails: 0, // Keep at most this many emails (0 = unlimited, default)
|
|
|
|
// Authentication
|
|
incomingUser: 'user', // SMTP auth username
|
|
incomingPass: 'pass', // SMTP auth password
|
|
webUser: 'admin', // Web UI username
|
|
webPass: 'admin', // Web UI password
|
|
|
|
// Relay (outgoing mail)
|
|
outgoingHost: 'smtp.example.com',
|
|
outgoingPort: 587,
|
|
outgoingUser: 'user',
|
|
outgoingPass: 'pass',
|
|
outgoingSecure: true,
|
|
outgoingIgnoreTls: false, // Never upgrade the relay connection with STARTTLS
|
|
autoRelay: true, // Auto-forward emails
|
|
|
|
// Logging
|
|
verbose: false,
|
|
silent: false,
|
|
logMailContents: false,
|
|
|
|
// MCP (Claude integration)
|
|
mcp: true, // Enable MCP server at /mcp endpoint
|
|
})
|
|
```
|
|
|
|
### Methods
|
|
|
|
**start()** - Start all servers (async)
|
|
```typescript
|
|
const servers = await maildev.start()
|
|
// servers.smtp - SMTP server instance
|
|
// servers.storage - Storage instance
|
|
// servers.api - API server instance (if not disabled)
|
|
```
|
|
|
|
**stop()** - Stop all servers gracefully (async)
|
|
```typescript
|
|
await maildev.stop()
|
|
```
|
|
|
|
**isRunning()** - Check if servers are running
|
|
```typescript
|
|
if (maildev.isRunning()) {
|
|
console.log('MailDev is running')
|
|
}
|
|
```
|
|
|
|
**getServers()** - Get server instances
|
|
```typescript
|
|
const servers = maildev.getServers()
|
|
```
|
|
|
|
## Ephemeral ports
|
|
|
|
Pass `smtp: 0` (and/or `web: 0`) to have the OS assign a free port, then read
|
|
back the port that was actually bound. This is the reliable way to run one
|
|
MailDev per worker under a parallel test runner without hand-assigning ports.
|
|
|
|
```typescript
|
|
const maildev = new MailDev({ smtp: 0, disableWeb: true })
|
|
const { smtp } = await maildev.start()
|
|
|
|
const { host, port } = smtp.getAddress()
|
|
console.log(`SMTP listening on ${host}:${port}`)
|
|
|
|
// Point your app's mail transport at `port` — e.g. nodemailer:
|
|
// nodemailer.createTransport({ host, port, secure: false })
|
|
```
|
|
|
|
Both the SMTP and API servers expose the bound address:
|
|
|
|
- `smtp.getAddress()` → `{ host, port }` (and `smtp.getPort()`)
|
|
- `servers.api?.getAddress()` → `{ host, port } | null` (and `api.getPort()`)
|
|
|
|
The API server's accessors return `null` until it is listening.
|
|
|
|
```typescript
|
|
const maildev = new MailDev({ smtp: 0, web: 0 })
|
|
const { smtp, api } = await maildev.start()
|
|
|
|
const smtpPort = smtp.getPort() // OS-assigned SMTP port
|
|
const webPort = api?.getPort() // OS-assigned web/API port
|
|
```
|
|
|
|
## Working with Emails
|
|
|
|
Once MailDev is running, you can access emails through the SMTP server instance.
|
|
|
|
### Listening for New Emails
|
|
|
|
```typescript
|
|
const maildev = new MailDev()
|
|
const { smtp } = await maildev.start()
|
|
|
|
smtp.on('new', (email) => {
|
|
console.log('New email received!')
|
|
console.log('From:', email.from[0].address)
|
|
console.log('To:', email.to.map(t => t.address).join(', '))
|
|
console.log('Subject:', email.subject)
|
|
console.log('Text:', email.text)
|
|
console.log('HTML:', email.html)
|
|
})
|
|
```
|
|
|
|
### Limiting how many emails are kept
|
|
|
|
MailDev keeps every email by default (`maxEmails: 0`). Set a positive
|
|
`maxEmails` and it keeps the newest that many messages, discarding the oldest as
|
|
new mail arrives. When emails are persisted to disk, the `.eml` file and any
|
|
attachments are deleted along with the message, so the mail directory stays
|
|
bounded too.
|
|
|
|
```typescript
|
|
const maildev = new MailDev({
|
|
mailDirectory: '/var/mail/maildev',
|
|
maxEmails: 5000,
|
|
})
|
|
```
|
|
|
|
`maxEmails: 0` (the default) keeps everything. Be aware that both memory use and
|
|
the mail directory then grow without limit: with typical messages, 10,000 emails
|
|
is around 150 MB of heap, and nothing is ever removed from disk — so set a
|
|
positive limit for long-running or high-volume use.
|
|
|
|
### Listing a large inbox
|
|
|
|
`smtp.getAllEmails()` materializes every email, bodies included. For listings,
|
|
use `storage.list()` instead — it returns a page of emails plus the counts
|
|
needed to paginate, so the work stays proportional to the page size rather than
|
|
the size of the store.
|
|
|
|
```typescript
|
|
const { items, total, unread } = await servers.storage.list({
|
|
skip: 0,
|
|
limit: 50,
|
|
search: 'welcome', // optional: subject, participants and body text
|
|
sort: 'desc', // 'desc' (default) is newest first
|
|
})
|
|
```
|
|
|
|
To drop the message bodies and headers — as the REST `/api/email/summary`
|
|
endpoint and the web UI do — map the page through `toSummary`:
|
|
|
|
```typescript
|
|
import { toSummary } from '@maildev/core'
|
|
|
|
const summaries = items.map(toSummary)
|
|
```
|
|
|
|
### Getting All Emails
|
|
|
|
```typescript
|
|
const emails = await smtp.getAllEmails()
|
|
console.log(`Total emails: ${emails.length}`)
|
|
```
|
|
|
|
### Getting a Single Email
|
|
|
|
```typescript
|
|
const email = await smtp.getEmail('email-id')
|
|
console.log(email.subject)
|
|
```
|
|
|
|
### Getting Raw Email (EML format)
|
|
|
|
```typescript
|
|
const stream = await smtp.getRawEmail('email-id')
|
|
stream.pipe(fs.createWriteStream('email.eml'))
|
|
```
|
|
|
|
### Deleting Emails
|
|
|
|
```typescript
|
|
// Delete single email
|
|
await smtp.deleteEmail('email-id')
|
|
|
|
// Delete all emails
|
|
await smtp.deleteAllEmails()
|
|
```
|
|
|
|
### Mark All as Read
|
|
|
|
```typescript
|
|
const count = await smtp.markAllRead()
|
|
console.log(`Marked ${count} emails as read`)
|
|
```
|
|
|
|
### Working with Attachments
|
|
|
|
```typescript
|
|
const email = await smtp.getEmail('email-id')
|
|
|
|
for (const attachment of email.attachments) {
|
|
console.log(`Attachment: ${attachment.filename}`)
|
|
console.log(`Type: ${attachment.contentType}`)
|
|
console.log(`Size: ${attachment.size} bytes`)
|
|
}
|
|
|
|
// Get attachment content
|
|
const { contentType, stream } = await smtp.getEmailAttachment('email-id', 'filename.pdf')
|
|
stream.pipe(fs.createWriteStream('filename.pdf'))
|
|
```
|
|
|
|
## Email Object Structure
|
|
|
|
```typescript
|
|
interface Email {
|
|
id: string
|
|
time: Date
|
|
read: boolean
|
|
subject: string
|
|
source: string
|
|
size: number
|
|
sizeHuman: string
|
|
from: Address[]
|
|
to: Address[]
|
|
cc?: Address[]
|
|
bcc?: Address[]
|
|
calculatedBcc?: Address[]
|
|
date?: Date
|
|
html?: string
|
|
text?: string
|
|
headers: Record<string, string | string[]>
|
|
inReplyTo?: string
|
|
priority?: 'high' | 'normal' | 'low'
|
|
attachments: Attachment[]
|
|
envelope: Envelope
|
|
relayedAt?: Date // when the email was last successfully relayed
|
|
relayedTo?: string[] // recipients delivered to on the last successful relay
|
|
}
|
|
|
|
interface Address {
|
|
address: string
|
|
name?: string
|
|
}
|
|
|
|
interface Envelope {
|
|
from: EnvelopeAddress
|
|
to: EnvelopeAddress[]
|
|
host?: string
|
|
remoteAddress?: string
|
|
}
|
|
|
|
interface EnvelopeAddress extends Address {
|
|
args?: boolean | Record<string, unknown>
|
|
}
|
|
|
|
interface Attachment {
|
|
filename: string
|
|
generatedFileName: string
|
|
contentType: string
|
|
contentDisposition: 'inline' | 'attachment'
|
|
contentId?: string
|
|
size?: number
|
|
transferred?: boolean
|
|
}
|
|
```
|
|
|
|
## Relay (Forwarding) Emails
|
|
|
|
MailDev can relay emails to a real SMTP server.
|
|
|
|
### Manual Relay
|
|
|
|
```typescript
|
|
const maildev = new MailDev({
|
|
outgoingHost: 'smtp.gmail.com',
|
|
outgoingPort: 587,
|
|
outgoingUser: 'you@gmail.com',
|
|
outgoingPass: 'app-password',
|
|
outgoingSecure: true,
|
|
})
|
|
|
|
const { smtp } = await maildev.start()
|
|
|
|
// Relay a specific email
|
|
smtp.on('new', async (email) => {
|
|
if (email.to.some(t => t.address === 'important@example.com')) {
|
|
await smtp.relayEmail(email.id)
|
|
console.log('Email relayed!')
|
|
}
|
|
})
|
|
```
|
|
|
|
After a successful relay (manual or auto) the email records `relayedAt` and
|
|
`relayedTo`, so you can tell whether and where a message was delivered:
|
|
|
|
```typescript
|
|
const email = await smtp.getEmail('email-id')
|
|
if (email.relayedAt) {
|
|
console.log(`Relayed at ${email.relayedAt} to ${email.relayedTo?.join(', ')}`)
|
|
}
|
|
```
|
|
|
|
With `FileStorage` this status is persisted to disk and restored on startup;
|
|
with in-memory storage it lives only for the lifetime of the process.
|
|
|
|
### Auto-Relay
|
|
|
|
```typescript
|
|
const maildev = new MailDev({
|
|
outgoingHost: 'smtp.example.com',
|
|
outgoingPort: 587,
|
|
outgoingUser: 'user',
|
|
outgoingPass: 'pass',
|
|
autoRelay: true, // Relay all emails automatically
|
|
})
|
|
```
|
|
|
|
### Auto-Relay to Specific Address
|
|
|
|
```typescript
|
|
const maildev = new MailDev({
|
|
outgoingHost: 'smtp.example.com',
|
|
outgoingPort: 587,
|
|
outgoingUser: 'user',
|
|
outgoingPass: 'pass',
|
|
autoRelay: 'catch-all@example.com', // Override recipient
|
|
})
|
|
```
|
|
|
|
## Events
|
|
|
|
The SMTP server emits the following events:
|
|
|
|
### 'new'
|
|
Emitted when a new email is received.
|
|
|
|
```typescript
|
|
smtp.on('new', (email: Email) => {
|
|
console.log('New email:', email.subject)
|
|
})
|
|
```
|
|
|
|
### 'delete'
|
|
Emitted when an email is deleted.
|
|
|
|
```typescript
|
|
smtp.on('delete', (data: { id: string }) => {
|
|
console.log('Deleted email:', data.id)
|
|
})
|
|
```
|
|
|
|
### 'error'
|
|
Emitted on server errors.
|
|
|
|
```typescript
|
|
smtp.on('error', (error: Error) => {
|
|
console.error('SMTP error:', error.message)
|
|
})
|
|
```
|
|
|
|
### 'close'
|
|
Emitted when the server is closed.
|
|
|
|
```typescript
|
|
smtp.on('close', () => {
|
|
console.log('SMTP server closed')
|
|
})
|
|
```
|
|
|
|
## Advanced Usage
|
|
|
|
### Using Individual Packages
|
|
|
|
For more control, you can use the underlying packages directly.
|
|
|
|
```typescript
|
|
import { MemoryStorage } from '@maildev/core'
|
|
import { createSMTPServer } from '@maildev/smtp'
|
|
import { createAPIServer } from '@maildev/api'
|
|
|
|
// Create storage
|
|
const storage = new MemoryStorage()
|
|
await storage.initialize()
|
|
|
|
// Create SMTP server
|
|
const smtp = createSMTPServer({
|
|
port: 1025,
|
|
host: '::',
|
|
storage,
|
|
mailDir: '/tmp/maildev',
|
|
})
|
|
|
|
await smtp.start()
|
|
|
|
// Create API server
|
|
const api = createAPIServer({
|
|
port: 1080,
|
|
storage,
|
|
smtp,
|
|
mcp: { enabled: true },
|
|
})
|
|
|
|
await api.start()
|
|
```
|
|
|
|
### Using FileStorage for Persistence
|
|
|
|
```typescript
|
|
import { FileStorage } from '@maildev/core'
|
|
|
|
const storage = new FileStorage({
|
|
mailDirectory: '/var/mail/maildev',
|
|
maxEmails: 1000, // optional: cap the store (0/omitted = unlimited)
|
|
})
|
|
await storage.initialize()
|
|
```
|
|
|
|
When `maxEmails` is exceeded, the oldest email is dropped and its files are
|
|
deleted. To clean up anything else you wrote alongside an email, register an
|
|
evict handler — `save()` awaits it, so once it resolves the email is fully gone:
|
|
|
|
```typescript
|
|
storage.onEvicted(async (email) => {
|
|
await removeMyIndexEntry(email.id)
|
|
})
|
|
```
|
|
|
|
### Custom Logger
|
|
|
|
```typescript
|
|
import { MailDev, createLogger } from 'maildev'
|
|
|
|
const logger = createLogger({
|
|
verbose: true,
|
|
silent: false,
|
|
})
|
|
|
|
// The MailDev class uses the logger internally
|
|
const maildev = new MailDev({
|
|
verbose: true,
|
|
})
|
|
```
|
|
|
|
### Middleware Integration
|
|
|
|
You can run MailDev behind a proxy or within an existing Express/Fastify app:
|
|
|
|
```typescript
|
|
const maildev = new MailDev({
|
|
basePathname: '/maildev',
|
|
web: 3001,
|
|
})
|
|
|
|
await maildev.start()
|
|
// MailDev UI now available at http://localhost:3001/maildev
|
|
```
|
|
|
|
Then proxy requests to MailDev:
|
|
|
|
```typescript
|
|
import express from 'express'
|
|
import { createProxyMiddleware } from 'http-proxy-middleware'
|
|
|
|
const app = express()
|
|
|
|
app.use('/maildev', createProxyMiddleware({
|
|
target: 'http://localhost:3001',
|
|
ws: true,
|
|
}))
|
|
|
|
app.listen(3000)
|
|
// MailDev accessible at http://localhost:3000/maildev
|
|
```
|
|
|
|
## TypeScript Support
|
|
|
|
MailDev v3 is written in TypeScript and exports all types:
|
|
|
|
```typescript
|
|
import type {
|
|
MailDevConfig,
|
|
Email,
|
|
EmailSummary,
|
|
Address,
|
|
Attachment,
|
|
Storage,
|
|
ListOptions,
|
|
ListResult,
|
|
} from 'maildev'
|
|
|
|
import type {
|
|
SMTPServer,
|
|
SMTPServerOptions,
|
|
RelayConfig,
|
|
} from '@maildev/smtp'
|
|
|
|
import type {
|
|
APIServer,
|
|
APIServerOptions,
|
|
} from '@maildev/api'
|
|
```
|
|
|
|
## Migration from v2
|
|
|
|
### Key Changes
|
|
|
|
1. **Promise-based API**: All methods are now async/await
|
|
2. **No callbacks**: Replace callback patterns with promises
|
|
3. **Package structure**: Core functionality split into `@maildev/core`, `@maildev/smtp`, `@maildev/api`
|
|
4. **TypeScript**: Full type definitions included
|
|
5. **Events unchanged**: Event names and payloads are compatible
|
|
|
|
### Example Migration
|
|
|
|
**v2 (callbacks):**
|
|
```javascript
|
|
const MailDev = require('maildev')
|
|
|
|
const maildev = new MailDev()
|
|
|
|
maildev.listen(function(err) {
|
|
if (err) throw err
|
|
console.log('MailDev running')
|
|
})
|
|
|
|
maildev.on('new', function(email) {
|
|
console.log('New email:', email.subject)
|
|
})
|
|
|
|
maildev.getAllEmail(function(err, emails) {
|
|
console.log('Total:', emails.length)
|
|
})
|
|
```
|
|
|
|
**v3 (async/await):**
|
|
```typescript
|
|
import { MailDev } from 'maildev'
|
|
|
|
const maildev = new MailDev()
|
|
|
|
const { smtp } = await maildev.start()
|
|
console.log('MailDev running')
|
|
|
|
smtp.on('new', (email) => {
|
|
console.log('New email:', email.subject)
|
|
})
|
|
|
|
const emails = await smtp.getAllEmails()
|
|
console.log('Total:', emails.length)
|
|
```
|