New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@bobfrankston/iflow-direct

Package Overview
Dependencies
Maintainers
1
Versions
76
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bobfrankston/iflow-direct

Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready

latest
Source
npmnpm
Version
0.1.85
Version published
Maintainers
1
Created
Source

@bobfrankston/iflow-direct

Transport-agnostic IMAP client. Zero Node.js deps — works in Node, Electron, and the browser (via a bridge transport). Caller supplies a TransportFactory, OAuth token provider (if needed), and drives the client.

See MIGRATION.md for the split from legacy @bobfrankston/iflow and the desktop/browser architecture diagram.

Install

npm install @bobfrankston/iflow-direct
# desktop (Node) TCP transport implementation:
npm install @bobfrankston/node-tcp-transport

@bobfrankston/tcp-transport is the interface package (TcpTransport is a type, plus the browser/Android BridgeTcpTransport); the class you instantiate in Node is NodeTcpTransport from @bobfrankston/node-tcp-transport (node:net/tls). @bobfrankston/iflow-node is a back-compat shim re-exporting it as NodeTransport — new code should not use it.

Quick start (Node)

import { NativeImapClient } from "@bobfrankston/iflow-direct";
import { NodeTcpTransport } from "@bobfrankston/node-tcp-transport";

const client = new NativeImapClient(
    { server: "imap.example.com", port: 993, username: "me", password: "..." },
    () => new NodeTcpTransport(),
);
await client.connect();
await client.select("INBOX");
const msgs = await client.fetchMessages("1:50", { source: false });
await client.logout();

For OAuth (Gmail, Office 365), omit password and supply a tokenProvider — an async function returning a current access token; the client calls it at authentication time, so refresh logic lives in the provider:

const client = new NativeImapClient(
    {
        server: "imap.gmail.com", port: 993, username: "me@gmail.com",
        tokenProvider: async () => myOauth.getFreshAccessToken(),
    },
    () => new NodeTcpTransport(),
);

(mailx pairs this with @bobfrankston/oauthsupport, but any source of a valid token works.)

For the legacy API shape (getFolderList, etc.) use CompatImapClient from the same package.

Batch body retrieval

Two APIs for pulling bodies across many UIDs in a single UID FETCH round trip, with per-message streaming as responses arrive (not buffered until the tagged OK).

fetchMessagesStream(range, options, onMessage?)

Streaming variant of fetchMessages. range accepts any IMAP sequence set — single UID ("42"), range ("100:200"), comma list ("1,5,10,42"), or mixed ("1:10,42,50:55"). The server handles all forms.

await client.select("INBOX");
await client.fetchMessagesStream(
    "1,5,10,42,100:150",
    { source: true, headers: false },
    (msg) => {
        // Called once per message as soon as its FETCH response
        // (literals included) is fully received. Tagged OK has not arrived yet.
        console.log(msg.uid, msg.source.length);
    },
);

Returns the parsed messages at the tagged OK; if onMessage is omitted, behaves like fetchMessages (collect-all).

fetchBodiesBatch(folderPath, uids, onBody)

Folder-scoped convenience. Selects the folder (skips if already selected), issues one UID FETCH <comma-list> (UID FLAGS ENVELOPE RFC822.SIZE INTERNALDATE BODY.PEEK[]), streams each (uid, source) through onBody, returns on tagged OK.

await client.fetchBodiesBatch("INBOX", [1, 5, 10, 42], (uid, source) => {
    store.saveBody(uid, source);
});

Intended for prefetch pipelines — e.g. mailx-imap's body-prefetch path — that would otherwise issue one SELECT+FETCH per message.

Streaming guarantees

  • onMessage / onBody fires on the same event-loop turn that the server's FETCH response (with any literal bodies) is fully parsed.
  • Ordering matches server response order. For a comma list, servers typically return in the order requested, but RFC 3501 does not require it — treat order as server-defined.
  • Callback exceptions are caught and logged (verbose mode) so one bad message doesn't abort the batch. Throw only if you want that behavior and wrap your own try/catch.
  • The tagged-OK promise still resolves with the full response list; callers needing both streaming and a terminal "all done" signal should await the returned promise.

Other APIs

NativeImapClient exposes the usual IMAP surface: select, examine, listFolders, getStatus, search, fetchMessage, fetchSinceUid, fetchByDate (both with optional onChunk chunked callbacks), addFlags, removeFlags, copyMessage, moveMessage, deleteMessage, deleteMessages, expunge, appendMessage, getFolderSize, startIdle, logout. See imap-native.ts for signatures.

Folder size (2026-09-16)

getFolderSize(mailbox) returns { messages, bytes, method }. When the server advertises STATUS=SIZE (RFC 8438) it is one STATUS (MESSAGES SIZE) round trip and method is "status". Otherwise it EXAMINEs the folder and sums a size-only UID FETCH 1:* (UID RFC822.SIZE) — no envelopes or headers — and method is "fetch". getStatus(mailbox, items?) takes an optional item list for the same reason; the default is unchanged. CompatImapClient.getFolderSize is the mailbox-scoped wrapper.

Bulk delete by criterion (2026-09-16)

Search, then delete the UIDs in one go. CompatImapClient does the SELECT / CLOSE around each call:

// Everything in Trash received before 2025-01-01
const uids = await client.searchMessages("Trash", { before: new Date("2025-01-01") });
await client.deleteMessagesByUid("Trash", uids);

searchMessages(mailbox, criteria) accepts from/to/cc/subject/body/ text (string or string[]), since/before (Date — IMAP compares the server's INTERNALDATE at day granularity; before is exclusive), the boolean flags seen/unseen/flagged/answered/draft, plus not and or groups. deleteMessagesByUid(mailbox, uids) issues one UID STORE <set> +FLAGS.SILENT (\Deleted) per 1000-UID chunk and a single EXPUNGE, instead of a STORE and an EXPUNGE round trip per message. Native equivalent: deleteMessages(uids) on a selected mailbox.

IDLE auto-suspends when any other command is issued on the same connection and re-enters afterwards — safe to interleave commands with a long-running IDLE, including commands issued from the IDLE onNewMail callback itself (e.g. a mailpuller-style pullMail that reacts to EXISTS by running STATUS + FETCH on the same connection). Suspend sends DONE, awaits the tagged OK, runs the command, then re-enters IDLE. If the caller stops IDLE (via the startIdle stop function) during the command, IDLE is not re-entered.

Connection liveness

NativeImapClient.connected (and CompatImapClient's lazy ensureConnected path, which now delegates to it) is authoritative: it gets cleared on transport error, transport close, inactivity-timeout socket kill, and write failure. Any of those paths also drops IDLE state so auto-suspend doesn't try to DONE a dead socket. Callers that detect connection-style errors (e.g. DNS "hostname not known" from a dead Android socket) can safely retry — the next operation will reconnect from scratch rather than reusing the dead socket.

Configuration

ImapClientConfig fields relevant to throughput/resilience:

FieldDefaultNotes
inactivityTimeout60000ms with no data before connection is declared dead. Timer resets on every byte. Slow Dovecot often needs 180000+.
fetchChunkSize25Initial chunk size for fetchSinceUid/fetchByDate.
fetchChunkSizeMax500Ramps up 4× per chunk. Batch APIs above bypass chunking — caller decides.
verbosefalseLog every command/response line + literal bookkeeping.

Files

See MIGRATION.md § Files in iflow-direct.

Keywords

imap

FAQs

Package last updated on 25 Oct 2026

Related posts