# MailTs
> Native SMTP/IMAP TypeScript SDK — zero runtime dependencies. Queue, DLQ, DKIM, iCal, telemetry — out of the box.
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 and ships as ~25 kB.
## Packages
- `@mailts/core` v0.3.0 — The mail SDK: SMTP, IMAP, queue, templates, middleware, transports
- `@mailts/cli` v0.1.6 — Terminal CLI for sending, reading, queue management
- `@mailts/trap` v1.0.3 — Local SMTP trap server with HTTP preview UI
- `@mailts/testing` v0.1.0 — Vitest helpers (`useTrapServer`) for integration tests
## Installation
```
npm install @mailts/core
```
Requires Node.js 18+.
## 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 },
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 },
},
logger: { level: 'debug', format: 'raw', protocol: true },
devMode: false,
});
```
### 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.',
html: '
Hello!
',
attachments: [
{ filename: 'report.pdf', path: './report.pdf' },
{ filename: 'logo.png', content: buffer, contentType: 'image/png', cid: 'logo' },
],
priority: 'high',
headers: { 'X-Custom': 'value' },
ical: {
summary: 'Team Sync',
start: new Date('2026-06-01T14:00:00'),
end: new Date('2026-06-01T15:00:00'),
timezone: 'America/New_York',
organizer: { name: 'Alice', email: 'alice@example.com' },
attendees: [{ email: 'bob@example.com', name: 'Bob', rsvp: true }],
method: 'REQUEST', // 'REQUEST' | 'CANCEL' | 'REPLY' | 'PUBLISH'
},
});
if (result.ok) {
console.log('Sent:', result.messageId);
} else {
console.error('Failed:', result.error.message, '— retryable:', result.error.retryable);
}
```
### Shorthand helpers
```ts
await mail.notify({ from: '...', to: '...', subject: 'Event', text: '...' });
// Prepends [NOTIFICATION] to subject
await mail.alert({ from: '...', to: '...', subject: 'CPU high', text: '...' });
// Prepends [ALERT] and forces high priority
await mail.ping({ from: '...', to: '...', subject: 'Ping', text: '...' });
// Minimal heartbeat send
```
### Templates
```ts
mail.define('welcome', {
from: { email: 'welcome@example.com', name: 'Acme' },
subject: 'Welcome, {{user.name}}!',
template: 'Hi {{user.name}}, your account is ready.',
});
await mail.trigger('welcome', {
to: 'newuser@example.com',
data: { user: { name: 'Alice' } },
});
// One-shot inline template:
await mail.sendTemplate({
from: 'billing@example.com',
to: 'user@example.com',
subject: 'Invoice #{{invoiceId}}',
template: 'Hi {{name}}, your invoice for ${{amount}} is due.',
data: { name: 'Alice', invoiceId: '1042', amount: '49.00' },
});
// Custom template engine (e.g. Handlebars):
mail.setTemplateEngine({
render: (compiled, data) => Handlebars.template(compiled as HandlebarsTemplateDelegate)(data),
});
```
### Middleware
```ts
mail.use(async (msg, next) => {
msg.headers = { ...msg.headers, 'X-Mailer': 'myapp/1.0' };
await next();
});
```
### Hot-swap credentials
```ts
mail.configure({
smtp: {
host: 'smtp.sendgrid.net',
port: 587,
auth: { type: 'plain', user: 'apikey', pass: process.env.SENDGRID_API_KEY },
},
});
```
### Health checks
```ts
const h = await mail.health();
// { smtp: { ok: true, latencyMs: 42 }, imap: { ok: true, latencyMs: 18 } }
await mail.testConnection(); // boolean — probe SMTP without using the pool
```
### Lifecycle
```ts
await mail.shutdown(); // drain queue, close connections
await mail.dispatch(opts, signal); // bypass queue and middleware, thread AbortSignal
```
## SMTP Auth types
- `'plain'` — PLAIN auth with `user` + `pass`
- `'login'` — LOGIN auth with `user` + `pass`
- `'xoauth2'` — XOAUTH2 with `user` + `token` (short-lived OAuth2 access token)
## IMAP API
```ts
const session = mail.imap;
await session.connect();
// Select and inspect a mailbox
const status = await session.open('INBOX');
// { exists: 120, unseen: 5, uidNext: 121 }
// Check status without selecting (safe for background polling)
const s = await session.getStatus('INBOX', ['MESSAGES', 'UNSEEN', 'UIDNEXT']);
// Fetch messages
const messages = await session.fetch({ seen: false, limit: 10, bodies: true });
// Each message: { uid, envelope, flags, modseq, headers, body }
// CONDSTORE incremental sync
const changed = await session.fetchChanged(lastKnownModSeq);
// Flag operations
await session.markSeen([101, 102]);
await session.markUnseen([103]);
await session.markFlagged([104]);
await session.markUnflagged([104]);
await session.setFlags([105], ['\\Answered'], true); // set arbitrary IMAP flags
// Move, copy, delete
await session.move([110], 'Archive');
await session.copy([111], 'Backup');
await session.delete([112]);
// Append raw message
await session.append('Sent', rawBuffer, ['\\Seen']);
// Mailbox management
await session.listMailboxes();
await session.createMailbox('Projects/Alpha');
await session.renameMailbox('Projects/Alpha', 'Projects/Done');
await session.deleteMailbox('Projects/Done');
// IDLE push notifications
await session.idle((msg) => console.log('New message:', msg.seq));
// Stop idle:
session.stopIdle();
await session.close();
```
## Queue API
```ts
// Enqueue returns a QueueJob — use job.id to track/cancel
const job = mail.queue.enqueue(
{ to: 'user@example.com', subject: 'Hi', text: '...' },
{ priority: 'critical' }, // 'critical' | 'high' | 'normal' | 'low'
);
mail.queue.cancel(job.id); // cancel pending job
mail.queue.interrupt(job.id); // re-queue running job (attempt count unchanged)
mail.queue.abort(job.id); // abort running job (counts as a failed attempt)
mail.queue.cancelAll();
mail.queue.interruptAll();
mail.queue.abortAll();
mail.queue.pause(); // stop picking up new jobs
mail.queue.play(); // resume
await mail.queue.drain(); // wait until all jobs settle
await mail.queue.shutdown(5_000); // drain with timeout, then force-close
const stats = mail.queue.stats();
// { pending: 3, running: 1, completed: 42, failed: 0, dead: 0 }
// Events
mail.queue.on('success', (job, result) => {});
mail.queue.on('retry', (job, error) => {});
mail.queue.on('dead', (job, error) => {});
mail.queue.on('cancelled', (job) => {});
mail.queue.on('interrupted', (job) => {});
mail.queue.on('enqueued', (job) => {});
mail.queue.on('started', (job) => {});
mail.queue.on('drained', () => {});
```
## Logging / telemetry
```ts
// Stream SMTP protocol trace to stdout (raw format, credentials auto-redacted)
mail.logger.stream({ format: 'raw' }).pipe(process.stdout);
// JSON structured logs to a file
mail.logger.stream({ format: 'json' }).pipe(createWriteStream('/tmp/mail.log'));
// Typed event listener
mail.logger.onEvent((e: LogEvent) => {
// e.level, e.phase, e.direction, e.message, e.timestamp, e.meta
if (e.level === 'error') reportToSentry(e);
});
```
Log levels: `'debug'` | `'info'` | `'warn'` | `'error'`
Log formats: `'pretty'` (ANSI-coloured) | `'json'` | `'raw'` (plain text)
Set `protocol: true` in logger config to stream every SMTP/IMAP command and reply.
## HTTP Transports (third-party APIs)
```ts
import { ResendTransport, SendGridTransport, MailgunTransport, PostmarkTransport, SesTransport } from '@mailts/core/transports';
const mail = new MailTs({
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY }),
});
// Same mail.send() interface — swap transport without changing call sites
```
## Local development — @mailts/trap
```ts
import { TrapServer } from '@mailts/trap';
const trap = new TrapServer({ smtpPort: 1025, httpPort: 1080 });
await trap.start();
// Inspect captured mail at http://localhost:1080
```
## Testing — @mailts/testing
```ts
import { useTrapServer } from '@mailts/testing';
const { getTrap } = useTrapServer(); // Vitest lifecycle hook
test('sends welcome email', async () => {
const mail = new MailTs({ smtp: { host: '127.0.0.1', port: getTrap().smtpPort, pool: false } });
await mail.send({ from: 'app@example.com', to: 'user@example.com', subject: 'Welcome', text: 'Hi' });
const [msg] = getTrap().store.getAll();
expect(msg.subject).toBe('Welcome');
});
```
## MailWorker (cross-process queues)
```ts
import { MailWorker, type QueueDriver, type DriverMessage } from '@mailts/core';
class RedisDriver implements QueueDriver {
async dequeue(): Promise { /* ... */ }
async ack(id: string): Promise { /* ... */ }
async nack(id: string, reason?: Error): Promise { /* ... */ }
}
const worker = new MailWorker(new RedisDriver(), { smtp: { /* ... */ } });
await worker.start();
```
## DKIM signing
DKIM is configured under `smtp.dkim` (not at top level):
```ts
const mail = new MailTs({
smtp: {
host: 'smtp.gmail.com',
port: 587,
auth: { type: 'plain', user: 'you@gmail.com', pass: process.env.SMTP_PASS },
dkim: {
domainName: 'example.com',
keySelector: 'mail', // matches mail._domainkey.example.com TXT record
privateKey: process.env.DKIM_PRIVATE_KEY,
},
},
});
```
## iCal / calendar invites
```ts
await mail.send({
from: 'organizer@example.com',
to: 'attendee@example.com',
subject: 'Meeting invite',
text: 'You have a meeting.',
ical: {
summary: 'Team Sync',
start: new Date('2026-06-01T14:00:00'),
end: new Date('2026-06-01T15:00:00'),
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' | 'PUBLISH'
uid: 'meeting-2026-06-01@example.com',
description: 'Quarterly planning session',
},
});
```
## 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