npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.ts JSDoc, 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 imapflow

API

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
}) => ImapFlowLike

Classes

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' | undefined
  • part — 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): boolean
  • root — 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 — Root BODYSTRUCTURE node 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): boolean
  • part — 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): Date
  • raw — 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): ImapFolder
  • raw — 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): string
  • raw — 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 or Uint8Array. UTF-8 is assumed when a Uint8Array is 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.