Human Usage
This document is for developers using @ghostpaw/email directly in code.
It assumes a human developer or operator is working with the Mailbox class
and the typed surfaces it exposes: read, write, and network.
If you are wiring this package into an agent or LLM harness, read LLM.md
instead. That document covers the soul, tools, and skills layers.
What This Surface Is For
@ghostpaw/email is strongest when used as a complete mailbox engine, not as
a thin IMAP wrapper.
A complete human usage pattern looks like:
- construct a
Mailboxwith account config - connect (initialises the local schema, upserts the account, opens the IMAP session)
- sync to materialise the local cache
- read from the cache for fast, offline-capable access
- write flag changes and composed messages back through the protocol
- watch for new messages via IDLE when staying connected
- disconnect when done
Setup
import { Mailbox } from '@ghostpaw/email';
import type { EmailConfig } from '@ghostpaw/email';
const config: EmailConfig = {
imap: { host: 'imap.example.com', port: 993, tls: true },
smtp: { host: 'smtp.example.com', port: 465, tls: true },
auth: { user: 'user@example.com', pass: 'secret' },
storage: 'mail.db',
};
const mailbox = new Mailbox(config);
await mailbox.connect();
connect() opens the SQLite database at the storage path (or :memory: if
omitted), runs initSchema, upserts the account row, and authenticates the
IMAP session, all in one call.
For OAuth2 providers (Gmail, Outlook), use an access-token auth config with a
refreshFn callback:
const config: EmailConfig = {
imap: { host: 'imap.gmail.com', port: 993, tls: true },
smtp: { host: 'smtp.gmail.com', port: 465, tls: true },
auth: {
user: 'user@gmail.com',
accessToken: currentToken,
refreshFn: async () => refreshedToken,
},
storage: 'gmail.db',
};
For SASL mechanisms, use the generic mechanism form:
const config: EmailConfig = {
imap: { host: 'imap.provider.com', port: 993, tls: true },
smtp: { host: 'smtp.provider.com', port: 465, tls: true },
auth: {
mechanism: 'XOAUTH2',
credentials: { user: 'user@provider.com', token: bearerToken },
},
};
Connection Lifecycle
// Connect: opens DB, initialises schema, upserts account, authenticates IMAP.
await mailbox.connect();
// Sync all subscribed folders (incremental by default).
const result = await mailbox.network.sync();
console.log(`${result.totalNew} new, ${result.totalExpunged} expunged`);
// Refresh the folder list from the server (detects new folders, role changes).
const folders = await mailbox.network.refreshFolders();
// Disconnect gracefully (closes IMAP session and SQLite database).
await mailbox.disconnect();
After disconnecting, a new Mailbox instance must be created to reconnect.
Within an active session, use mailbox.network.reconnect() for transient
failures (exponential back-off, up to 5 attempts).
Reading Local State
All read.* calls are synchronous and operate against the local cache. No
network is required.
Folders
const folders = mailbox.read.folders();
// [{ id, accountId, path, role, uidValidity, uidNext, unseenCount, ... }]
const status = mailbox.read.folderStatus('INBOX');
// { messages, unseen, uidNext, uidValidity, highestModSeq }
Messages
const messages = mailbox.read.messages('INBOX', {
limit: 50,
sort: 'date',
order: 'desc',
});
// Returns Message[] — header-only, no body content.
Message Detail (with body)
getMessage is async because it fetches the body on demand if not cached:
const detail = await mailbox.read.getMessage('INBOX', uid);
// Returns MessageDetail — includes textPlain, textHtml, attachments[].
Threads
const threads = mailbox.read.threads('INBOX', { limit: 20 });
// Returns Thread[] — each thread has messages[] sorted by date.
const thread = mailbox.read.getThread('thread-id-string');
// Returns one Thread with full ThreadMessage[] (includes depth for nesting).
Attachments
const metas = mailbox.read.listAttachments('INBOX', uid);
// Returns AttachmentMeta[] — filename, mimeType, size, partPath, inline.
// Fetch binary bytes (async — triggers IMAP FETCH if not cached).
const att = await mailbox.read.getAttachment('INBOX', uid, '2');
// Returns Attachment — AttachmentMeta + data: Buffer.
Search
const results = mailbox.read.search('quarterly report', {
folder: 'INBOX',
since: new Date('2026-01-01'),
unreadOnly: true,
limit: 25,
});
// Returns Message[] matching FTS5 query.
Stats
const stats = mailbox.read.stats();
// { folders: [{ path, role, total, unread }], totalMessages, totalUnread,
// lastSyncedAt, storageUsed }
Remote Operations
Some read operations require a live IMAP session:
// Fetch a message body from the server (useful when local cache is stale).
const body = await mailbox.network.fetchBody('INBOX', uid);
// Fetch a specific MIME attachment part.
const att = await mailbox.network.fetchAttachment('INBOX', uid, '1.2');
// Run IMAP SEARCH on the server (returns UIDs).
const uids = await mailbox.network.searchRemote('INBOX', {
unseen: true,
since: new Date('2026-01-01'),
});
Writing Flag Changes
All write.* calls are async. Flag operations send IMAP STORE commands and
update the local cache on success.
// Mark as read / unread.
await mailbox.write.markRead('INBOX', [42, 43]);
await mailbox.write.markUnread('INBOX', [42]);
// Star / unstar (sets/clears \Flagged).
await mailbox.write.star('INBOX', [42]);
await mailbox.write.unstar('INBOX', [42]);
// Mark as answered (sets \Answered flag).
await mailbox.write.markAnswered('INBOX', [42]);
Organizing Messages
// Copy to another folder (leaves original in place).
await mailbox.write.copyTo('INBOX', [42], 'Archive/2026');
// Move (COPY + STORE \Deleted + EXPUNGE).
await mailbox.write.moveTo('INBOX', [42], 'Archive/2026');
// Archive to server-detected Archive folder.
await mailbox.write.archive('INBOX', [42]);
// Trash to server-detected Trash folder.
await mailbox.write.trash('INBOX', [42]);
// Move to Junk.
await mailbox.write.moveToJunk('INBOX', [42]);
// Mark as not junk and restore.
await mailbox.write.markNotJunk('Junk', [42], 'INBOX');
Labels (Gmail and compatible)
await mailbox.write.addLabels('INBOX', [42], ['receipts', 'finance']);
await mailbox.write.removeLabels('INBOX', [42], ['receipts']);
await mailbox.write.setLabels('INBOX', [42], ['finance']); // replaces all
Composing And Sending
Direct send
const { messageId } = await mailbox.write.send({
to: [{ address: 'bob@example.com' }],
subject: 'Project update',
text: 'Here is the latest status.',
attachments: [
{ filename: 'status.pdf', mimeType: 'application/pdf', content: pdfBuffer },
],
});
The from field is optional. When omitted, the sender address is derived from
the identities array in EmailConfig, or from the IMAP auth credentials.
Reply (preserves threading headers)
const { messageId } = await mailbox.write.reply('INBOX', uid, {
text: 'Thanks, the timeline works for me.',
});
// Automatically sets In-Reply-To, References, and prepends attribution + quoted body.
Forward
// Inline forward (prepends forwarded-message header block + original body).
const { messageId } = await mailbox.write.forward('INBOX', uid, {
to: [{ address: 'carol@example.com' }],
mode: 'inline',
text: 'FYI — see below.',
});
// Attachment forward (wraps original as .eml attachment).
await mailbox.write.forward('INBOX', uid, {
to: [{ address: 'carol@example.com' }],
mode: 'attachment',
});
Draft lifecycle
// Save a draft to the Drafts folder.
const { uid: draftUid } = await mailbox.write.saveDraft({
to: [{ address: 'bob@example.com' }],
subject: 'Work in progress',
text: 'Draft body.',
});
// Update the draft.
const { uid: newUid } = await mailbox.write.updateDraft(draftUid, {
subject: 'Work in progress',
to: [{ address: 'bob@example.com' }],
text: 'Revised body.',
});
// Send the draft (SMTP send + append to Sent + expunge from Drafts).
const { messageId } = await mailbox.write.sendDraft(newUid);
Export and import
// Export a message as raw RFC 2822 bytes.
const eml: Buffer = await mailbox.write.exportEml('INBOX', uid);
// Import a .eml file into a folder.
const { uid: importedUid } = await mailbox.write.importEml('Archive', eml, ['\\Seen']);
Folder Management
await mailbox.write.createFolder('Archive/2026');
await mailbox.write.renameFolder('Archive/2026', 'Archive/2026-Q1');
await mailbox.write.deleteFolder('Archive/2026-Q1');
await mailbox.write.subscribeFolder('Updates');
await mailbox.write.unsubscribeFolder('Updates');
Always call mailbox.network.refreshFolders() after structural folder changes
to update the local folder cache.
Watch (IDLE)
watch is an async generator. It yields WatchEvent values as the server
pushes new EXISTS, EXPUNGE, or FLAGS notifications:
const controller = new AbortController();
for await (const event of mailbox.network.watch({ signal: controller.signal })) {
if (event.type === 'new') {
console.log(`${event.messages.length} new messages in ${event.folder}`);
}
if (event.type === 'flags') {
console.log(`Flags changed for uid ${event.uid} in ${event.folder}`);
}
if (event.type === 'expunge') {
console.log(`Message ${event.uid} expunged from ${event.folder}`);
}
}
// Stop watching.
controller.abort();
Sync Options
// Incremental sync of all subscribed folders (QRESYNC when available).
await mailbox.network.sync();
// Sync only specific folders.
await mailbox.network.sync({ folders: ['INBOX', 'Sent'] });
// Fetch missing bodies during sync.
await mailbox.network.sync({ bodies: 'missing' });
// Fetch all bodies (use sparingly — can be slow on large mailboxes).
await mailbox.network.sync({ bodies: 'all' });
Error Handling
The engine raises typed errors:
EmailAuthError: authentication failure: fix credentials, do not retry blindlyEmailConnectionError: network / TLS failure: retry withreconnect()EmailProtocolError: server-side NO or BAD response to a commandEmailNotFoundError: requested message or folder does not exist locallyEmailUnsupportedError: server does not support the requested extension
import { EmailAuthError, EmailConnectionError } from '@ghostpaw/email';
try {
await mailbox.connect();
} catch (err) {
if (err instanceof EmailAuthError) {
// Credential problem — do not retry.
console.error('Authentication failed:', err.message);
} else if (err instanceof EmailConnectionError) {
// Network problem — create a new Mailbox and try again.
console.error('Connection failed:', err.message);
}
}
The Human Operating Loop
A disciplined human developer drives the mailbox through derived views, not through memory or ad-hoc queries:
- Construct a
MailboxwithEmailConfigand callconnect()on startup. - Use
network.sync()to materialise the local cache. - Use
read.folders()andread.messages()to build views from local state. - Use
read.getMessage()on demand when body content is needed. - Use
write.*for intentional mutations: flag changes, sends, organise. - Use
network.watch()for real-time updates when staying connected. - Use
network.sync()periodically to pull state changes when IDLE is not in use. - Call
disconnect()to close the session and database when done.
Local reads are always fast. Network operations are always explicit.