Sitelet https://github.com/maildev/maildev/blob/main/docs/api.md
Skip to content

Latest commit

 

History

History
599 lines (458 loc) · 13.3 KB

File metadata and controls

599 lines (458 loc) · 13.3 KB

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

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

npm install maildev

MailDev Class

The MailDev class provides the simplest way to run MailDev programmatically.

Constructor Options

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,
  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)

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)

await maildev.stop()

isRunning() - Check if servers are running

if (maildev.isRunning()) {
  console.log('MailDev is running')
}

getServers() - Get server instances

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.

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.

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

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.

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.

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:

import { toSummary } from '@maildev/core'

const summaries = items.map(toSummary)

Getting All Emails

const emails = await smtp.getAllEmails()
console.log(`Total emails: ${emails.length}`)

Getting a Single Email

const email = await smtp.getEmail('email-id')
console.log(email.subject)

Getting Raw Email (EML format)

const stream = await smtp.getRawEmail('email-id')
stream.pipe(fs.createWriteStream('email.eml'))

Deleting Emails

// Delete single email
await smtp.deleteEmail('email-id')

// Delete all emails
await smtp.deleteAllEmails()

Mark All as Read

const count = await smtp.markAllRead()
console.log(`Marked ${count} emails as read`)

Working with Attachments

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

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

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:

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

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

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.

smtp.on('new', (email: Email) => {
  console.log('New email:', email.subject)
})

'delete'

Emitted when an email is deleted.

smtp.on('delete', (data: { id: string }) => {
  console.log('Deleted email:', data.id)
})

'error'

Emitted on server errors.

smtp.on('error', (error: Error) => {
  console.error('SMTP error:', error.message)
})

'close'

Emitted when the server is closed.

smtp.on('close', () => {
  console.log('SMTP server closed')
})

Advanced Usage

Using Individual Packages

For more control, you can use the underlying packages directly.

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

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:

storage.onEvicted(async (email) => {
  await removeMyIndexEntry(email.id)
})

Custom Logger

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:

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:

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:

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):

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):

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)