# MailTs — full reference > Complete, single-file reference for AI assistants. Index: https://mailts.anishhs.com/llms.txt > Native SMTP/IMAP TypeScript SDK with Google & Microsoft OAuth, Gmail & Graph APIs and durable queues — zero runtime dependencies. Stable 1.0 API. MailTs (`@mailts/core`) is a TypeScript-first email SDK built on Node.js built-ins (`node:net`, `node:tls`, `node:crypto`, `node:stream`, `node:http`, `node:https`). It has zero runtime npm dependencies (~40 kB gzipped). From 1.0.0 the public API follows semantic versioning; APIs tagged `@experimental` (Microsoft Graph, Microsoft app-only OAuth) and Low-level protocol helpers are excluded. ## Packages - `@mailts/core` v1.0.0 — The mail SDK: SMTP, IMAP, OAuth, Gmail & Graph APIs, queue, transports - `@mailts/cli` v0.2.0 — Terminal CLI: configure, test, send, read, queue management, AI agent skill - `@mailts/trap` v1.1.0 — Local SMTP trap server with web UI and REST API - `@mailts/testing` v0.2.0 — Vitest helpers (`useTrapServer`) for integration tests ## Installation ``` npm install @mailts/core ``` Requires Node.js 22+ (the SQLite queue, `queue.persist`, needs 22.13+). Teach an AI coding agent (Claude Code and other Agent Skills readers) to use mailts: ``` npx @mailts/cli skill install # ./.claude/skills/mailts npx @mailts/cli skill install --global # ~/.claude/skills/mailts ``` ## Rules that prevent common bugs - `send()` never throws for delivery problems — branch on `result.ok`; `result.error` has `.code` and `.retryable`. - `mail.imap` returns a NEW session on every access — keep one reference and reuse it. - `fetch({ bodies: true })` never marks mail as read; pass `markSeen: true` to set `\Seen`. - Attachments given by `path` are rejected unless `attachmentPolicy` is `'allow'` or `{ root }`; otherwise pass `content`. - `requireTLS` defaults to true when authenticating (loopback hosts exempt); minimum TLS 1.2. - `shutdown()` delivers pending in-memory jobs and keeps persistent ones; pass `{ pending: 'cancel' }` to discard. - For XOAUTH2 use `auth: { type: 'xoauth2', user, getToken }` — called per connect and refreshed once on rejection. - DKIM is configured under `smtp.dkim`, not at the top level. ## Core API ### MailTs constructor ```ts import { MailTs } from '@mailts/core'; const mail = new MailTs({ smtp: { host: 'smtp.gmail.com', port: 587, auth: { type: 'plain', user: 'you@gmail.com', pass: process.env.SMTP_PASS }, pool: { maxConnections: 5, maxMessages: 100, idleTimeout: 30_000 }, // or pool: false for scripts dkim: { domainName: 'example.com', keySelector: 'mail', privateKey: process.env.DKIM_PRIVATE_KEY }, }, imap: { host: 'imap.gmail.com', port: 993, secure: true, auth: { type: 'plain', user: 'you@gmail.com', pass: process.env.IMAP_PASS }, }, queue: { concurrency: 5, maxRetries: 3, retryBackoff: 'exponential', jitter: true, deadLetter: { enabled: true } }, attachmentPolicy: { root: '/srv/uploads' }, // 'deny' | 'allow' | { root }; unset = path attachments rejected logger: { level: 'debug', format: 'raw', protocol: true }, telemetry: { onSend: (opts, result, ms) => {}, onQueueDead: (job) => {} }, devMode: false, }); ``` With no config, `new MailTs()` loads `~/.mailts/config.json` and `.mailtsrc` / `.mailtsrc.json` (local wins), expanding `${ENV_VAR}`. ### Sending email ```ts const result = await mail.send({ from: { email: 'you@gmail.com', name: 'Your App' }, to: 'recipient@example.com', cc: 'cc@example.com', bcc: ['bcc1@example.com', 'bcc2@example.com'], replyTo: 'support@example.com', subject: 'Hello from mailts!', text: 'Plain-text fallback.', // generated from html when omitted html: '

Hello!

', attachments: [ { filename: 'logo.png', content: buffer, contentType: 'image/png', cid: 'logo' }, { filename: 'report.pdf', path: 'report.pdf' }, // needs attachmentPolicy ], priority: 'high', headers: { 'X-Custom': 'value' }, }); if (result.ok) console.log('Sent:', result.messageId, result.accepted, result.rejected); else console.error('Failed:', result.error.message, '— retryable:', result.error.retryable); const built = await mail.build(options); // raw RFC 5322 bytes (built.raw) without sending await mail.send(options, { saveToSent: true }); // also append to the Sent mailbox over IMAP await mail.saveToSent(options, { mailbox: 'Sent' }); ``` ### Replies, forwards, unsubscribe ```ts import { buildReply, buildForward, isOneClickUnsubscribe } from '@mailts/core'; const [orig] = await session.fetch({ uids: [uid], bodies: true, headers: ['References'] }); await mail.send(buildReply(orig, { from: 'me@x.com', text: 'Thanks!', replyAll: true }), { saveToSent: true }); await mail.send(buildForward(orig, { from: 'me@x.com', to: 'boss@x.com', text: 'FYI' })); // List-Unsubscribe + List-Unsubscribe-Post (RFC 8058), DKIM-signed await mail.send({ ...newsletter, unsubscribe: { url: `https://app.com/u/${token}`, mailto: 'unsubscribe@app.com' } }); isOneClickUnsubscribe({ method: req.method, contentType: req.headers['content-type'], body }); ``` ### Smart inbox content ```ts import { schemaOrg } from '@mailts/core'; await mail.send({ to, subject: 'Your Acme order #1234', html, structuredData: [ // schema.org JSON-LD in — Gmail order card + button schemaOrg.order({ merchant: 'Acme', orderNumber: '1234', price: 39.9, priceCurrency: 'EUR', status: 'processing' }), schemaOrg.viewAction({ url: 'https://shop.example/orders/1234', name: 'View order' }), ], }); // Also: parcelDelivery, flightReservation, lodgingReservation, eventReservation, foodReservation, // discountOffer, promotionCard; `amp` (AMP for Email, needs html); `adaptiveCard` (Outlook, needs originator) ``` ### iCal invites ```ts await mail.send({ from: 'organizer@example.com', to: 'attendee@example.com', subject: 'Team Sync', ical: { summary: 'Team Sync', start: new Date(2026, 5, 1, 14, 0), // wall-clock time in `timezone` end: new Date(2026, 5, 1, 15, 0), timezone: 'America/New_York', organizer: { name: 'Alice', email: 'alice@example.com' }, attendees: [{ email: 'bob@example.com', name: 'Bob', rsvp: true }], location: 'Conference Room A', method: 'REQUEST', // 'REQUEST' | 'CANCEL' | 'REPLY' | 'COUNTER' }, }); ``` ### Templates, middleware, helpers ```ts mail.define('welcome', { from: 'welcome@example.com', subject: 'Welcome, {{user.name}}!', template: 'Hi {{user.name}}' }); await mail.trigger('welcome', { to: 'newuser@example.com', data: { user: { name: 'Alice' } } }); await mail.sendTemplate({ from, to, subject: 'Invoice #{{id}}', template: 'Hi {{name}}', data: { id: '1042', name: 'Alice' } }); mail.setTemplateEngine({ compile: (src) => Handlebars.compile(src), render: (tpl, data) => (tpl as HandlebarsTemplateDelegate)(data) }); mail.use(async (msg, next) => { // runs for direct and queued sends msg.headers = { ...msg.headers, 'X-Mailer': 'myapp/1.0' }; await next(); }); await mail.notify(opts); // [NOTIFICATION] subject prefix await mail.alert(opts); // [ALERT] prefix + high priority mail.configure({ smtp: newSmtp }); // hot-swap credentials await mail.health(); // { smtp: { ok, latencyMs }, imap: { ok, latencyMs }, timestamp } await mail.testConnection(); // boolean await mail.shutdown(); // never drops queued mail; close pools ``` ## OAuth (`@mailts/core/oauth`) ```ts import { authorizeWithLoopback, buildAuthorizationUrl, exchangeCode, createPkce, createState, google, microsoft, googleTokenProvider, microsoftTokenProvider, mailConfigFor, googleServiceAccountProvider, microsoftAppOnlyProvider, } from '@mailts/core/oauth'; // CLI / desktop: browser sign-in, redirect to 127.0.0.1, PKCE const tokens = await authorizeWithLoopback({ provider: google, clientId, clientSecret }); const getToken = googleTokenProvider({ clientId, clientSecret, refreshToken: tokens.refreshToken! }); const mail = new MailTs(mailConfigFor(google, { user: tokens.email!, getToken })); // IMAP + SMTP presets // Microsoft rotates refresh tokens — persist the new one const ms = microsoftTokenProvider({ clientId, refreshToken, onRefreshToken: (rt) => save(rt) }); // Web apps: buildAuthorizationUrl({ provider, clientId, redirectUri, codeChallenge, state }) → exchangeCode(...) in the callback // App-only: googleServiceAccountProvider({ credentials, subject }), microsoftAppOnlyProvider({ tenant, clientId, certificate }) (experimental) // microsoft({ api: 'graph' }) for Graph scopes; refreshAccessToken() throws OAuthError with oauthCode 'invalid_grant' when sign-in is needed ``` ## IMAP API ```ts const session = mail.imap; // keep this reference — connects lazily, reconnects after drops const status = await session.open('INBOX'); // { exists, unseen, uidNext, highestModSeq, ... } const counts = await session.getStatus('INBOX', ['MESSAGES', 'UNSEEN']); const messages = await session.fetch({ seen: false, limit: 10, bodies: true }); // never marks read // message: { uid, envelope, flags, headers, body?: { text, html, attachments }, structure? } await session.fetch({ limit: 50, structure: true }); // BODYSTRUCTURE only await session.fetch({ limit: 50, textOnly: true }); // only text/plain + text/html sections const tree = await session.fetchStructure(101); const bytes = await session.fetchSection(101, '2'); const raw = await session.fetchRaw(101); // full source import { parseMessage } from '@mailts/core'; const parsed = parseMessage(raw); // also for .eml files const uids = await session.search({ from: 'alerts@example.com', since: new Date('2026-06-01'), flagged: true }); const changed = await session.fetchChanged(lastModSeq, 'INBOX'); // CONDSTORE await session.markSeen([101]); await session.markFlagged([104]); await session.setFlags([105], ['\\Answered'], true); await session.move([110], 'Archive'); await session.copy([111], 'Backup'); await session.delete([112]); await session.appendMessage('Drafts', { from, to, subject, text }, ['\\Draft']); await session.append('Sent', rawBuffer, ['\\Seen']); const sent = await session.findMailbox('\\Sent'); const watcher = await session.watch('INBOX'); // dedicated connection, by UID, reconnects + catches up watcher.on('new', async (uids) => { await session.fetch({ uids, textOnly: true }); }); await watcher.stop(); await session.close(); ``` ### IMAP connection pool (multi-tenant servers) ```ts import { ImapPool } from '@mailts/core'; const pool = new ImapPool({ maxPerAccount: 2, maxSessions: 200, idleTimeoutMs: 5 * 60_000 }); const unread = await pool.use(account.id, () => imapConfigFor(account), (s) => s.fetch({ seen: false, limit: 20 })); await pool.close(account.id); await pool.closeAll(); ``` ### One mailbox API (IMAP, Gmail API, Microsoft Graph) ```ts import { imapMailbox, GmailMailbox, GraphMailbox, type Mailbox } from '@mailts/core/mailbox'; const box: Mailbox = imapMailbox(mail.imap); // or new GmailMailbox({ user, getToken }) / new GraphMailbox({ user, getToken }) (experimental) const unread = await box.fetch({ search: { seen: false }, limit: 20, bodies: true }); await box.setSeen(unread.map(m => m.id), true); await box.move([unread[0].id], 'Archive'); const w = await box.watch('INBOX'); ``` ## Queue API ```ts const job = mail.queue.enqueue( { from, to: 'user@example.com', subject: 'Hi', text: '...' }, { priority: 'critical', idempotencyKey: 'welcome:42', sendAt: new Date(Date.now() + 3_600_000) }, ); new MailTs({ smtp, queue: { persist: true, rateLimit: { perMinute: 30, by: 'sender' } } }); // crash-safe SQLite mail.queue.pause(); mail.queue.play(); mail.queue.cancel(job.id); // remove — no retry, no DLQ mail.queue.interrupt(job.id); // back to the front, attempt not counted mail.queue.abort(job.id); // counts as a failed attempt await mail.queue.drain(); await mail.queue.shutdown({ timeoutMs: 5_000 }); // or { pending: 'drain' | 'keep' | 'cancel' } const { pending, scheduled, running, succeeded, dead, cancelled } = mail.queue.stats(); mail.queue.dlq.getAll(); mail.queue.on('success', (job, result) => {}); mail.queue.on('retry', (job, attempt, delay) => {}); mail.queue.on('dead', (job, errors) => {}); mail.queue.on('cancelled', (job) => {}); mail.queue.on('interrupted', (job) => {}); mail.queue.on('throttled', (job, waitMs) => {}); mail.queue.on('drained', () => {}); ``` ## MailWorker (external queues: Redis, SQS, Postgres) ```ts import { MailWorker, decodeOptions, type QueueDriver, type DriverMessage } from '@mailts/core'; class RedisDriver implements QueueDriver { async dequeue(): Promise { /* { id, data: decodeOptions(...), priority?, idempotencyKey? } */ } async ack(id: string) {} async nack(id: string, reason?: Error) {} async release(id: string) {} // optional: unstarted message at shutdown } const worker = new MailWorker(new RedisDriver(), { smtp: { /* ... */ }, queue: { concurrency: 5 } }); await worker.start(); await worker.shutdown(5_000); ``` ## HTTP transports ```ts import { ResendTransport, SendGridTransport, MailgunTransport, PostmarkTransport, SesTransport, GmailTransport, GraphTransport } from '@mailts/core/transports'; const mail = new MailTs({ transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY }) }); // GmailTransport({ user, getToken }) / GraphTransport({ user, getToken }) (experimental) send over OAuth where SMTP is disabled. // Errors are TransportError: 408/429/5xx retryable (Retry-After honoured), 401/403 EAUTH. ``` ## Errors All extend `MailTsError` with `.code` and `.retryable`: `SmtpAuthError` (EAUTH), `SmtpRejectError` (EREJECT), `SmtpConnError` (ECONN, retryable), `SmtpTimeoutError` (ETIMEOUT, retryable), `SmtpTlsError` (ETLS), `ImapError` (EIMAP), `ImapAuthError` (EAUTH), `ImapConnError` (ECONN, retryable), `OAuthError` (EAUTH), `QueueError` (EQUEUE), `TransportError`, `LimitError` (ELIMIT), `ConfigError` (ECONFIG), `MimeError` (EMIME), `TemplateError` (ETEMPLATE). ## Logging ```ts mail.logger.stream({ format: 'raw' }).pipe(process.stdout); // protocol trace, AUTH redacted mail.logger.stream({ format: 'json' }).pipe(createWriteStream('/tmp/mail.log')); mail.logger.onEvent((e) => { if (e.level === 'error') report(e); }); ``` ## CLI — @mailts/cli (v0.2.0, Node 22+, needs @mailts/core 1.x) ```bash npm install -g @mailts/cli # or run any command with: npx @mailts/cli mailts configure # wizard → ~/.mailts/config.json mailts configure --local # → .mailtsrc in the current directory mailts test --host smtp.gmail.com --port 465 --encryption SSL_TLS # live protocol trace (MAILTS_PASSWORD env) mailts send --to you@example.com --subject "Hi" --text "Hello" [--html file|string] [--attachments a.pdf,b.png] mailts send --host 127.0.0.1 --port 1025 --to dev@example.com --text "trap test" # override SMTP server mailts send --alias welcome --to newuser@example.com mailts read [--mailbox INBOX] [--limit 10] [--unseen] mailts queue status [--json] # needs queue.persist; pending/scheduled/running/succeeded/dead/cancelled mailts queue cancel|interrupt|abort # picked up by the running app within 5 s mailts queue dlq list [--json] | dlq retry | dlq clear mailts trap [--smtp-port 1025] [--http-port 1080] [--host 127.0.0.1] [--persist ] [--no-open] [--quiet] ``` ### Agent skill — `mailts skill` Installs the mailts Agent Skill (SKILL.md + references: sending, reading, OAuth, queues, production) so AI coding agents such as Claude Code use @mailts/core correctly. It loads only for email tasks. ```bash npx @mailts/cli skill install # ./.claude/skills/mailts (commit it to share with the team) mailts skill install --global # ~/.claude/skills/mailts (every project) mailts skill install --dir # /mailts (other agents / layouts) mailts skill uninstall | show | path # --force replaces a folder mailts did not create ``` Source: https://github.com/anishhs-gh/mailts/tree/master/packages/cli/skill/mailts ## Local development — @mailts/trap (v1.1.0, Node 22+, zero dependencies) Captures every message sent to it; works with any SMTP client. CLI: `mailts-trap` (or `npx @mailts/trap`, `mailts trap`). ```ts import { TrapServer } from '@mailts/trap'; const trap = new TrapServer({ smtpPort: 1025, httpPort: 1080, maxMessages: 100, persist: false }); // persist: true → ~/.mailts-trap/ await trap.start(); // Point SMTP at 127.0.0.1:1025 (loopback is exempt from requireTLS); inspect at http://localhost:1080 const [latest] = trap.store.getAll(); // newest first; trap.store.getById(id) await trap.stop(); ``` REST API: `GET /api/messages`, `GET /api/messages/:id` (marks read), `GET /api/messages/:id/raw`, `GET /api/messages/:id/attachments/:n`, `DELETE /api/messages/:id`, `DELETE /api/messages`, `GET /api/stats` (`{ total, unread, storageBytes }`), `GET /api/events` (Server-Sent Events). ## Testing — @mailts/testing (v0.2.0, Node 22+, peers vitest + @mailts/core 1.x) Requires `globals: true` in the Vitest config. The only API is `useTrapServer(options?)` returning `{ server, waitForMessage, messages, clear }` — there is no `getTrap()`. ```ts import { useTrapServer } from '@mailts/testing'; import { MailTs } from '@mailts/core'; const trap = useTrapServer({ smtpPort: 2025, httpPort: 2080 }); // starts/stops per suite, clears after each test test('sends welcome email', async () => { const mail = new MailTs({ smtp: { host: '127.0.0.1', port: 2025, pool: false } }); await mail.send({ from: 'app@example.com', to: 'user@example.com', subject: 'Welcome', text: 'Hi' }); const msg = await trap.waitForMessage({ subject: 'Welcome' }); // also: trap.messages(), trap.clear(), trap.server expect(msg.to[0]?.email).toBe('user@example.com'); }); ``` ## Version history (@mailts/core) - 1.0.0 (2026-10-03) — stable API; OAuth, Mailbox API, Gmail/Graph transports, ImapPool, watch(), build/saveToSent, replies, unsubscribe, smart inbox content, queue idempotency/rate limits/sendAt; Node 22+. Breaking vs 0.4: fetch no longer marks read, shutdown() keeps/delivers pending mail, path attachments need attachmentPolicy, TLS required before login, idle() delivers { uid }. - 0.4.0 (2026-06-22) — BODYSTRUCTURE, fetchStructure/fetchSection/fetchText, search(), working MIME bodies. - 0.3.0 (2026-05-12) — priorities, cancel/interrupt/abort, shutdown(timeoutMs), MailWorker + QueueDriver, AbortSignal. - 0.2.0 (2026-05-06) — health checks, telemetry hooks, SQLite queue persistence. - 0.1.0 (2026-04-25) — first release. Migration guide: https://github.com/anishhs-gh/mailts/blob/master/MIGRATION.md ## Links - GitHub: https://github.com/anishhs-gh/mailts - npm (@mailts/core): https://www.npmjs.com/package/@mailts/core - Documentation: https://mailts.anishhs.com - API reference: https://mailts.anishhs.com/#/api - Examples: https://mailts.anishhs.com/#/examples - Changelog: https://mailts.anishhs.com/#/changelog - Upgrading from 0.4: https://github.com/anishhs-gh/mailts/blob/master/MIGRATION.md