@molecule/api-imap
v1.0.1
Published
IMAP client core for per-user mailboxes (distinct from outbound transactional bonds).
Readme
@molecule/api-imap
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
IMAP client wrapper for molecule.dev — fetches folders, lists messages,
fetches message bodies, marks read/unread, moves, and deletes. Used by
the email-client flagship app to render per-user mailboxes.
Wraps the imapflow driver — this package is intentionally a single
utility, not an abstract core, because there is only one IMAP protocol;
provider variation is per-server config (host/port/auth), not per-bond.
Quick Start
import { connectImap } from '@molecule/api-imap'
const client = await connectImap({
host: 'imap.gmail.com',
port: 993,
secure: true,
auth: { user: '[email protected]', accessToken: googleAccessToken },
})
const folders = await client.listFolders()
await client.selectFolder('INBOX')
const recent = await client.listMessages({ folder: 'INBOX', limit: 25 })
for (const summary of recent) {
console.log(summary.uid, summary.subject, summary.flags)
}
const full = await client.fetchMessage(recent[0].uid)
console.log(full.subject, full.text ?? full.html, full.attachments.length)
await client.markRead(full.uid)
await client.disconnect()Type
utility
Installation
npm install @molecule/api-imap imapflowAPI
Interfaces
Attachment
A single attachment on a {@link FullMessage}. The body is exposed as a
Uint8Array so callers can stream it to S3 / disk / SSE without depending
on Node's Buffer type.
interface Attachment {
/** Content-ID (used to inline reference attachments from HTML), if present. */
contentId?: string
/** Filename from the Content-Disposition header, if present. */
filename?: string
/** MIME type (e.g. `image/png`, `application/pdf`). */
contentType: string
/** Decoded content body. */
content: Uint8Array
/** Decoded content size in bytes. */
size: number
/** `true` if the attachment is inline (referenced by `cid:`), not a separate file. */
inline: boolean
}ConnectImapHooks
Hooks for testing — pass {@link connectImap} a factory to substitute a
custom driver constructor in place of the real imapflow.ImapFlow.
interface ConnectImapHooks {
/** Driver constructor override — defaults to `imapflow.ImapFlow`. */
factory?: ImapFlowFactory
}DiscoveredPart
A single discovered body part with its IMAP part-number identifier and decoded metadata.
interface DiscoveredPart {
part: string
type: string
encoding?: string
filename?: string
contentId?: string
disposition?: string
inline: boolean
size: number
}FullMessage
Full message returned by {@link ImapClient.fetchMessage}. Body parts are decoded into UTF-8 strings; attachments retain their raw decoded bytes.
interface FullMessage {
/** Server-assigned UID. */
uid: number
/** From: addresses (envelope). */
from: ImapAddress[]
/** To: addresses (envelope). */
to: ImapAddress[]
/** Cc: addresses (envelope), if present. */
cc?: ImapAddress[]
/** Subject. */
subject: string
/** Internal date stamp. */
date: Date
/** Plaintext body (decoded), if the message has a `text/plain` part. */
text?: string
/** HTML body (decoded), if the message has a `text/html` part. */
html?: string
/** All non-inline-text attachments, in encounter order. */
attachments: Attachment[]
/**
* Full RFC 822 header lines, lower-cased and folded. Useful for inspecting
* `dkim-signature`, `received`, `list-unsubscribe`, etc. without re-parsing.
*/
headers: Record<string, string | string[]>
}ImapAddress
A parsed IMAP envelope address — display name + RFC 5322 mailbox.
interface ImapAddress {
/** Display name (e.g. `"Jane Doe"`) or `undefined` if absent. */
name?: string
/** Mailbox in `local@domain` form. */
address: string
}ImapBodyNode
Structural shape of an IMAP BODYSTRUCTURE node as returned by
imapflow. Represented recursively — multipart parts contain
childNodes, leaf parts do not.
interface ImapBodyNode {
part?: string
type?: string
parameters?: Record<string, string>
id?: string
encoding?: string
size?: number
disposition?: string
dispositionParameters?: Record<string, string>
childNodes?: ImapBodyNode[]
}ImapClient
The handle returned by {@link connectImap}. All methods are async; calling any after {@link disconnect} throws.
Designed to be composable: ImapClient is an opaque interface so the
underlying driver can be swapped if needed (e.g. for a server-side mock).
interface ImapClient {
/** List all folders (subscribed + unsubscribed) the user can see. */
listFolders(): Promise<ImapFolder[]>
/**
* Select a folder. Subsequent UID-scoped operations
* ({@link fetchMessage}, {@link markRead}, …) operate on the most-recently
* selected folder.
*
* @param name - Folder path (e.g. `INBOX`, `INBOX/Travel`).
*/
selectFolder(name: string): Promise<void>
/** List message summaries in a folder. */
listMessages(options: ListMessagesOptions): Promise<MessageSummary[]>
/** Fetch a full message by UID (within the currently-selected folder). */
fetchMessage(uid: number): Promise<FullMessage>
/** Set the `\Seen` flag. */
markRead(uid: number): Promise<void>
/** Clear the `\Seen` flag. */
markUnread(uid: number): Promise<void>
/** Move a message to another folder. */
moveMessage(uid: number, toFolder: string): Promise<void>
/** Permanently delete a message (UID EXPUNGE). */
deleteMessage(uid: number): Promise<void>
/** Close the IMAP connection cleanly (LOGOUT). Idempotent. */
disconnect(): Promise<void>
}ImapConfig
Configuration for {@link connectImap}. All TLS and host details are passed
straight through to the underlying imapflow driver — callers should set
secure: true for implicit-TLS ports (993) and secure: false for
STARTTLS-on-143.
interface ImapConfig {
/** IMAP server hostname (e.g. `imap.gmail.com`). */
host: string
/** IMAP server port (993 implicit-TLS, 143 STARTTLS, etc.). */
port: number
/** When `true` (default for 993), use implicit TLS from the first byte. */
secure?: boolean
/** Authentication credentials — password or OAuth2 access token. */
auth: ImapAuth
/** Optional TLS overrides — e.g. `{ rejectUnauthorized: false }` for self-signed dev servers. */
tls?: {
rejectUnauthorized?: boolean
}
}ImapDownloadManyEntry
Body-part identifier accepted by downloadMany.
interface ImapDownloadManyEntry {
meta: {
contentType?: string
charset?: string
disposition?: string
filename?: string
encoding?: string
}
content: Uint8Array | null
}ImapEnvelope
Structural shape of an IMAP envelope as returned by imapflow.
interface ImapEnvelope {
date?: Date | string
subject?: string
from?: ImapEnvelopeAddress[]
to?: ImapEnvelopeAddress[]
cc?: ImapEnvelopeAddress[]
bcc?: ImapEnvelopeAddress[]
replyTo?: ImapEnvelopeAddress[]
messageId?: string
inReplyTo?: string
}ImapEnvelopeAddress
Structural shape of an IMAP envelope address as returned by imapflow.
interface ImapEnvelopeAddress {
name?: string
address?: string
}ImapFetchMessage
Structural shape of the message object returned by
{@link ImapFlowLike.fetchOne} / fetchAll.
interface ImapFetchMessage {
uid: number
seq?: number
flags?: Set<string> | string[]
envelope?: ImapEnvelope
internalDate?: Date | string
bodyStructure?: ImapBodyNode
bodyParts?: Map<string, Uint8Array> | Record<string, Uint8Array>
headers?: Uint8Array | string
size?: number
}ImapFlowLike
Hand-rolled structural slice of imapflow.ImapFlow covering exactly the
methods we call. Anything not on this interface MUST NOT be reached for
by the wrapper.
interface ImapFlowLike {
connect(): Promise<void>
logout(): Promise<void>
close(): void
list(): Promise<ImapListResponse[]>
mailboxOpen(path: string): Promise<unknown>
search(query: ImapSearchQuery, options?: { uid?: boolean }): Promise<number[] | false>
fetchOne(
seq: string,
query: Record<string, unknown>,
options?: { uid?: boolean },
): Promise<ImapFetchMessage | false>
fetchAll(
range: number[] | string,
query: Record<string, unknown>,
options?: { uid?: boolean },
): Promise<ImapFetchMessage[]>
messageFlagsAdd(range: number[], flags: string[], options?: { uid?: boolean }): Promise<boolean>
messageFlagsRemove(
range: number[],
flags: string[],
options?: { uid?: boolean },
): Promise<boolean>
messageMove(range: number[], destination: string, options?: { uid?: boolean }): Promise<unknown>
messageDelete(range: number[], options?: { uid?: boolean }): Promise<boolean>
downloadMany(
range: number,
parts: string[],
options?: { uid?: boolean },
): Promise<Record<string, ImapDownloadManyEntry>>
}ImapFolder
A normalized IMAP folder / mailbox entry returned by {@link ImapClient.listFolders}.
interface ImapFolder {
/** Server-supplied path (delimiter-separated, e.g. `INBOX/Travel`). */
path: string
/** Last segment of `path` for display. */
name: string
/** Hierarchy delimiter the server uses (typically `/` or `.`). */
delimiter: string
/**
* Special-use attribute if the server reports one (`\Inbox`, `\Sent`,
* `\Drafts`, `\Trash`, `\Junk`, `\Archive`, `\All`, `\Flagged`).
*/
specialUse?: string
/** `true` if this folder can be selected with {@link ImapClient.selectFolder}. */
subscribed: boolean
}ImapListResponse
Structural shape of one entry returned by {@link ImapFlowLike.list}.
interface ImapListResponse {
path: string
name?: string
delimiter?: string
specialUse?: string
subscribed?: boolean
}ImapSearchQuery
Search query shape accepted by {@link ImapFlowLike.search}. Only the fields used by this wrapper are listed.
interface ImapSearchQuery {
since?: Date
text?: string
}ListMessagesOptions
Filter options for {@link ImapClient.listMessages}.
interface ListMessagesOptions {
/** Folder path to list. Required. */
folder: string
/** Maximum number of messages to return (default `50`). */
limit?: number
/** Number of messages to skip from the most-recent end (default `0`). */
offset?: number
/** Only return messages with internal-date >= this value. */
since?: Date
/** Free-text search applied across `From`, `To`, `Subject`, and body. */
search?: string
}MessageSummary
Compact summary of a message, returned by {@link ImapClient.listMessages}. Does NOT include body / html / attachments — call {@link ImapClient.fetchMessage} for those.
interface MessageSummary {
/** Server-assigned UID (stable within a folder). */
uid: number
/** From: addresses (envelope). */
from: ImapAddress[]
/** To: addresses (envelope). */
to: ImapAddress[]
/** Subject — empty string if the server returned none. */
subject: string
/** Internal date the server stamped the message with. */
date: Date
/** `true` if BODYSTRUCTURE indicates at least one non-inline attachment. */
hasAttachments: boolean
/**
* IMAP flags currently set on the message (e.g. `\Seen`, `\Flagged`,
* `\Answered`). Backslash prefixes are preserved verbatim.
*/
flags: string[]
}Types
ImapAuth
Authentication strategies for an IMAP connection.
Either a username + password pair (most common — IMAP servers, app passwords, etc.) or a username + OAuth2 access token (XOAUTH2 — used by Gmail, Outlook 365, and other major providers when basic auth is disabled).
type ImapAuth = { user: string; pass: string } | { user: string; accessToken: string }ImapErrorCode
Error code surfaced by {@link ImapError}. Map these to translated user-facing strings in the calling handler — this utility is intentionally locale-bond-free (handler-error pattern).
type ImapErrorCode =
| 'auth-failed'
| 'connection-failed'
| 'folder-not-found'
| 'message-not-found'
| 'not-connected'
| 'no-folder-selected'
| 'protocol-error'ImapFlowFactory
Constructor signature used to build an {@link ImapFlowLike}. The
top-level entry point (connectImap) accepts an optional override of
this so tests can inject a mock without monkey-patching the module.
type ImapFlowFactory = (options: {
host: string
port: number
secure?: boolean
auth: { user: string; pass?: string; accessToken?: string }
tls?: { rejectUnauthorized?: boolean }
logger?: false
}) => ImapFlowLikeClasses
ImapError
Strongly-typed error thrown by {@link connectImap} and any {@link ImapClient} method.
Functions
classifyTextPart(part)
Determine whether a {@link DiscoveredPart} is a text body part (plain or
HTML) — used to populate {@link import('./types.js').FullMessage}'s
text / html fields.
function classifyTextPart(part: DiscoveredPart): 'text' | 'html' | undefinedpart— The discovered part.
Returns: 'text', 'html', or undefined.
connectImap(config, hooks)
Connect to an IMAP server and return an {@link ImapClient} backed by
imapflow.
Locale bonds are intentionally not used — error messages on the thrown
{@link ImapError} are developer-facing English (handler-error pattern).
Consumers should map error.code to translated user-facing strings in
the calling handler.
function connectImap(config: ImapConfig, hooks?: ConnectImapHooks): Promise<ImapClient>config— Server + auth configuration.hooks— Optional test hooks (driver factory).
Returns: A connected {@link ImapClient}.
detectHasAttachments(root)
Walk a BODYSTRUCTURE tree and decide whether the message has at least
one non-inline attachment.
function detectHasAttachments(root: ImapBodyNode | undefined): booleanroot— Root body-structure node from a fetch response.
Returns: true if the message has any attachment-disposition part with a non-empty filename.
flattenBodyStructure(root)
Walk an IMAP BODYSTRUCTURE tree depth-first and yield each leaf part
with its IMAP part-number identifier (1, 1.1, 2, …).
Multipart container nodes are skipped — only leaf parts (text, html, attachments) are returned.
function flattenBodyStructure(root: ImapBodyNode | undefined): DiscoveredPart[]root— RootBODYSTRUCTUREnode from a fetch response.
Returns: Flat array of discovered leaf parts in encounter order.
isAttachmentPart(part)
Determine whether a {@link DiscoveredPart} should be returned as a structured attachment.
function isAttachmentPart(part: DiscoveredPart): booleanpart— The discovered part.
Returns: true if the part should be exposed as an attachment.
normalizeAddresses(raw)
Convert an imapflow envelope-address array into the normalized
{@link ImapAddress} shape, dropping entries that have no address.
function normalizeAddresses(raw: ImapEnvelopeAddress[] | undefined): ImapAddress[]raw— Driver address list.
Returns: Normalized addresses (always an array, possibly empty).
normalizeDate(raw)
Coerce an envelope date field (which may be Date | string | undefined)
into a Date. Falls back to the unix epoch if parsing fails so callers
never have to handle NaN-dates.
function normalizeDate(raw: string | Date | undefined): Dateraw— Driver date value.
Returns: Parsed Date (epoch on parse failure).
normalizeEnvelope(envelope)
Normalize imapflow's envelope shape into the trio of fields we expose
on a {@link import('./types.js').FullMessage}.
function normalizeEnvelope(envelope: ImapEnvelope | undefined): {
from: ImapAddress[]
to: ImapAddress[]
cc: ImapAddress[]
subject: string
date: Date
}envelope— Driver envelope (may be partially populated).
Returns: Normalized fields.
normalizeFlags(raw)
Coerce an imapflow flags representation (which may be a Set<string>,
an array, or undefined) into a stable string array.
function normalizeFlags(raw: string[] | Set<string> | undefined): string[]raw— Driver flags container.
Returns: Flag list (possibly empty).
normalizeFolder(raw)
Convert an imapflow list-response into our normalized
{@link ImapFolder} shape.
function normalizeFolder(raw: ImapListResponse): ImapFolderraw— Single driver list entry.
Returns: Normalized folder.
normalizeSubject(raw)
Normalize an envelope's subject — imapflow may report undefined when
the message has no Subject header. We always return a string so callers
can render uniformly.
function normalizeSubject(raw: string | undefined): stringraw— Envelope subject.
Returns: Subject string (empty when missing).
parseHeaders(raw)
Parse a raw RFC 822 header block (CRLF-separated) into a map of lower-cased header names to their values. Folded lines (continuation lines starting with whitespace) are unfolded. Headers that appear more than once are aggregated into an array.
function parseHeaders(
raw: string | Uint8Array<ArrayBufferLike> | undefined,
): Record<string, string | string[]>raw— Raw header block as a string orUint8Array. UTF-8 is assumed when aUint8Arrayis supplied.
Returns: Header map keyed by lower-cased name.
Injection Notes
Runtime Dependencies
imapflow
Locale bonds are intentionally not used — error messages on the thrown
{@link ImapError} are developer-facing English (handler-error pattern).
Consumers should map error.code to translated user-facing strings in
the calling handler.
The wrapper exposes a small, stable surface; advanced imapflow features
(IDLE, mailbox locks, raw search/fetch) are intentionally not surfaced —
if you need them, drop down to imapflow directly.
Network reality: IMAP is raw TLS/TCP (typically port 993) from the API
process to the mail host — it does NOT traverse HTTP_PROXY-style egress
proxies. In deployments with default-deny egress (e.g. molecule.dev
sandboxes, where only HTTP(S) via the egress proxy is permitted),
connectImap fails at connect with a timeout; direct outbound TCP to the
IMAP host must be allowed. Credential reality: Gmail and Microsoft reject
plain account passwords for IMAP — use an OAuth2 access token
(auth: { user, accessToken }, sent as XOAUTH2) or a provider-issued app
password. Credentials are per-call config; nothing is read from env vars.
