@bobfrankston/iflow-direct
v0.1.51
Published
Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready
Maintainers
Readme
@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 TCP transport:
npm install @bobfrankston/tcp-transportQuick start (desktop)
import { NativeImapClient } from "@bobfrankston/iflow-direct";
import { TcpTransport } from "@bobfrankston/tcp-transport";
const client = new NativeImapClient(
{ server: "imap.example.com", port: 993, username: "me", password: "..." },
() => new TcpTransport(),
);
await client.connect();
await client.select("INBOX");
const msgs = await client.fetchMessages("1:50", { source: false });
await client.logout();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/onBodyfires 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
awaitthe 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, expunge,
appendMessage, startIdle, logout. See imap-native.ts for signatures.
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:
| Field | Default | Notes |
|---|---|---|
| inactivityTimeout | 60000 | ms with no data before connection is declared dead. Timer resets on every byte. Slow Dovecot often needs 180000+. |
| fetchChunkSize | 25 | Initial chunk size for fetchSinceUid/fetchByDate. |
| fetchChunkSizeMax | 500 | Ramps up 4× per chunk. Batch APIs above bypass chunking — caller decides. |
| verbose | false | Log every command/response line + literal bookkeeping. |
