@bobfrankston/iflow-direct 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/imap-compat.d.ts +5 -0
- package/imap-compat.js +10 -0
- package/imap-native.d.ts +21 -1
- package/imap-native.js +54 -4
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @bobfrankston/iflow-direct
|
|
2
|
+
|
|
3
|
+
Transport-agnostic IMAP client. Zero Node.js deps — works in Node, Electron, and
|
|
4
|
+
the browser (via a bridge transport). Caller supplies a `TransportFactory`, OAuth
|
|
5
|
+
token provider (if needed), and drives the client.
|
|
6
|
+
|
|
7
|
+
See [MIGRATION.md](./MIGRATION.md) for the split from legacy `@bobfrankston/iflow`
|
|
8
|
+
and the desktop/browser architecture diagram.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @bobfrankston/iflow-direct
|
|
14
|
+
# desktop TCP transport:
|
|
15
|
+
npm install @bobfrankston/tcp-transport
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start (desktop)
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { NativeImapClient } from "@bobfrankston/iflow-direct";
|
|
22
|
+
import { TcpTransport } from "@bobfrankston/tcp-transport";
|
|
23
|
+
|
|
24
|
+
const client = new NativeImapClient(
|
|
25
|
+
{ server: "imap.example.com", port: 993, username: "me", password: "..." },
|
|
26
|
+
() => new TcpTransport(),
|
|
27
|
+
);
|
|
28
|
+
await client.connect();
|
|
29
|
+
await client.select("INBOX");
|
|
30
|
+
const msgs = await client.fetchMessages("1:50", { source: false });
|
|
31
|
+
await client.logout();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For the legacy API shape (`getFolderList`, etc.) use `CompatImapClient` from the
|
|
35
|
+
same package.
|
|
36
|
+
|
|
37
|
+
## Batch body retrieval
|
|
38
|
+
|
|
39
|
+
Two APIs for pulling bodies across many UIDs in a single `UID FETCH` round trip,
|
|
40
|
+
with per-message streaming as responses arrive (not buffered until the tagged OK).
|
|
41
|
+
|
|
42
|
+
### `fetchMessagesStream(range, options, onMessage?)`
|
|
43
|
+
|
|
44
|
+
Streaming variant of `fetchMessages`. `range` accepts any IMAP sequence set —
|
|
45
|
+
single UID (`"42"`), range (`"100:200"`), comma list (`"1,5,10,42"`), or mixed
|
|
46
|
+
(`"1:10,42,50:55"`). The server handles all forms.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
await client.select("INBOX");
|
|
50
|
+
await client.fetchMessagesStream(
|
|
51
|
+
"1,5,10,42,100:150",
|
|
52
|
+
{ source: true, headers: false },
|
|
53
|
+
(msg) => {
|
|
54
|
+
// Called once per message as soon as its FETCH response
|
|
55
|
+
// (literals included) is fully received. Tagged OK has not arrived yet.
|
|
56
|
+
console.log(msg.uid, msg.source.length);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Returns the parsed messages at the tagged OK; if `onMessage` is omitted, behaves
|
|
62
|
+
like `fetchMessages` (collect-all).
|
|
63
|
+
|
|
64
|
+
### `fetchBodiesBatch(folderPath, uids, onBody)`
|
|
65
|
+
|
|
66
|
+
Folder-scoped convenience. Selects the folder (skips if already selected), issues
|
|
67
|
+
one `UID FETCH <comma-list> (UID FLAGS ENVELOPE RFC822.SIZE INTERNALDATE BODY.PEEK[])`,
|
|
68
|
+
streams each `(uid, source)` through `onBody`, returns on tagged OK.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
await client.fetchBodiesBatch("INBOX", [1, 5, 10, 42], (uid, source) => {
|
|
72
|
+
store.saveBody(uid, source);
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Intended for prefetch pipelines — e.g. mailx-imap's body-prefetch path — that
|
|
77
|
+
would otherwise issue one SELECT+FETCH per message.
|
|
78
|
+
|
|
79
|
+
### Streaming guarantees
|
|
80
|
+
|
|
81
|
+
- `onMessage` / `onBody` fires on the same event-loop turn that the server's
|
|
82
|
+
FETCH response (with any literal bodies) is fully parsed.
|
|
83
|
+
- Ordering matches server response order. For a comma list, servers typically
|
|
84
|
+
return in the order requested, but RFC 3501 does not require it — treat order
|
|
85
|
+
as server-defined.
|
|
86
|
+
- Callback exceptions are caught and logged (verbose mode) so one bad message
|
|
87
|
+
doesn't abort the batch. Throw only if you want that behavior and wrap your
|
|
88
|
+
own try/catch.
|
|
89
|
+
- The tagged-OK promise still resolves with the full response list; callers
|
|
90
|
+
needing both streaming and a terminal "all done" signal should `await` the
|
|
91
|
+
returned promise.
|
|
92
|
+
|
|
93
|
+
## Other APIs
|
|
94
|
+
|
|
95
|
+
`NativeImapClient` exposes the usual IMAP surface: `select`, `examine`,
|
|
96
|
+
`listFolders`, `getStatus`, `search`, `fetchMessage`, `fetchSinceUid`,
|
|
97
|
+
`fetchByDate` (both with optional `onChunk` chunked callbacks), `addFlags`,
|
|
98
|
+
`removeFlags`, `copyMessage`, `moveMessage`, `deleteMessage`, `expunge`,
|
|
99
|
+
`appendMessage`, `startIdle`, `logout`. See `imap-native.ts` for signatures.
|
|
100
|
+
|
|
101
|
+
IDLE auto-suspends when any other command is issued on the same connection and
|
|
102
|
+
re-enters afterwards — safe to interleave commands with a long-running IDLE.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
`ImapClientConfig` fields relevant to throughput/resilience:
|
|
107
|
+
|
|
108
|
+
| Field | Default | Notes |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `inactivityTimeout` | 60000 | ms with no data before connection is declared dead. Timer resets on every byte. Slow Dovecot often needs 180000+. |
|
|
111
|
+
| `fetchChunkSize` | 25 | Initial chunk size for `fetchSinceUid`/`fetchByDate`. |
|
|
112
|
+
| `fetchChunkSizeMax` | 500 | Ramps up 4× per chunk. Batch APIs above bypass chunking — caller decides. |
|
|
113
|
+
| `verbose` | false | Log every command/response line + literal bookkeeping. |
|
|
114
|
+
|
|
115
|
+
## Files
|
|
116
|
+
|
|
117
|
+
See [MIGRATION.md § Files in iflow-direct](./MIGRATION.md#files-in-iflow-direct).
|
package/imap-compat.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export declare class CompatImapClient {
|
|
|
38
38
|
fetchMessagesSinceUid(mailbox: string, sinceUid: number, options?: {
|
|
39
39
|
source?: boolean;
|
|
40
40
|
}): Promise<FetchedMessage[]>;
|
|
41
|
+
/** Batch-fetch bodies for many UIDs in one folder on one connection. Streams
|
|
42
|
+
* each body through `onBody` as it arrives. No per-message round trips —
|
|
43
|
+
* one SELECT, one UID FETCH, streaming response. Required by mailx-imap's
|
|
44
|
+
* batch prefetch path. */
|
|
45
|
+
fetchBodiesBatch(mailbox: string, uids: number[], onBody: (uid: number, source: string) => void): Promise<void>;
|
|
41
46
|
/** Fetch messages by date range. Optional onChunk callback for incremental processing. */
|
|
42
47
|
fetchMessageByDate(mailbox: string, start: Date, end?: Date, options?: {
|
|
43
48
|
source?: boolean;
|
package/imap-compat.js
CHANGED
|
@@ -93,6 +93,16 @@ export class CompatImapClient {
|
|
|
93
93
|
await this.native.closeMailbox();
|
|
94
94
|
return msgs.map(m => new FetchedMessage(m));
|
|
95
95
|
}
|
|
96
|
+
/** Batch-fetch bodies for many UIDs in one folder on one connection. Streams
|
|
97
|
+
* each body through `onBody` as it arrives. No per-message round trips —
|
|
98
|
+
* one SELECT, one UID FETCH, streaming response. Required by mailx-imap's
|
|
99
|
+
* batch prefetch path. */
|
|
100
|
+
async fetchBodiesBatch(mailbox, uids, onBody) {
|
|
101
|
+
if (uids.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
await this.ensureConnected();
|
|
104
|
+
await this.native.fetchBodiesBatch(mailbox, uids, onBody);
|
|
105
|
+
}
|
|
96
106
|
/** Fetch messages by date range. Optional onChunk callback for incremental processing. */
|
|
97
107
|
async fetchMessageByDate(mailbox, start, end, options, onChunk) {
|
|
98
108
|
await this.ensureConnected();
|
package/imap-native.d.ts
CHANGED
|
@@ -79,11 +79,31 @@ export declare class NativeImapClient {
|
|
|
79
79
|
createMailbox(mailbox: string): Promise<void>;
|
|
80
80
|
deleteMailbox(mailbox: string): Promise<void>;
|
|
81
81
|
renameMailbox(from: string, to: string): Promise<void>;
|
|
82
|
-
/** Fetch messages by UID range */
|
|
82
|
+
/** Fetch messages by UID range. Range may be sequence set (e.g. "1:100"), comma list ("1,5,10"), or mix ("1:10,42,50:55"). */
|
|
83
83
|
fetchMessages(range: string, options?: {
|
|
84
84
|
source?: boolean;
|
|
85
85
|
headers?: boolean;
|
|
86
86
|
}): Promise<NativeFetchedMessage[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Fetch messages by UID range/list, streaming each parsed message through `onMessage`
|
|
89
|
+
* as soon as its server response (including any literal bodies) is received, instead
|
|
90
|
+
* of buffering the entire batch until the tagged OK. Returns all parsed messages at end.
|
|
91
|
+
*
|
|
92
|
+
* Pass a comma list (`"1,5,10,42"`) or sequence set (`"100:200"`) — the server handles either.
|
|
93
|
+
*/
|
|
94
|
+
fetchMessagesStream(range: string, options?: {
|
|
95
|
+
source?: boolean;
|
|
96
|
+
headers?: boolean;
|
|
97
|
+
}, onMessage?: (msg: NativeFetchedMessage) => void): Promise<NativeFetchedMessage[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Folder-scoped batch body fetch. Selects `folderPath`, issues a single
|
|
100
|
+
* `UID FETCH <uids> (BODY.PEEK[] ...)`, streams each body through `onBody`
|
|
101
|
+
* as it arrives, and returns when the tagged OK is received.
|
|
102
|
+
*
|
|
103
|
+
* Eliminates per-message SELECT+FETCH round trips for callers like mailx-imap's
|
|
104
|
+
* prefetch path.
|
|
105
|
+
*/
|
|
106
|
+
fetchBodiesBatch(folderPath: string, uids: number[], onBody: (uid: number, source: string) => void): Promise<void>;
|
|
87
107
|
/** Fetch messages since a UID */
|
|
88
108
|
fetchSinceUid(sinceUid: number, options?: {
|
|
89
109
|
source?: boolean;
|
package/imap-native.js
CHANGED
|
@@ -281,16 +281,56 @@ export class NativeImapClient {
|
|
|
281
281
|
throw new Error(`RENAME failed: ${tagged?.text || "unknown"}`);
|
|
282
282
|
}
|
|
283
283
|
// ── Message Operations ──
|
|
284
|
-
/** Fetch messages by UID range */
|
|
284
|
+
/** Fetch messages by UID range. Range may be sequence set (e.g. "1:100"), comma list ("1,5,10"), or mix ("1:10,42,50:55"). */
|
|
285
285
|
async fetchMessages(range, options = {}) {
|
|
286
|
+
return this.fetchMessagesStream(range, options);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Fetch messages by UID range/list, streaming each parsed message through `onMessage`
|
|
290
|
+
* as soon as its server response (including any literal bodies) is received, instead
|
|
291
|
+
* of buffering the entire batch until the tagged OK. Returns all parsed messages at end.
|
|
292
|
+
*
|
|
293
|
+
* Pass a comma list (`"1,5,10,42"`) or sequence set (`"100:200"`) — the server handles either.
|
|
294
|
+
*/
|
|
295
|
+
async fetchMessagesStream(range, options = {}, onMessage) {
|
|
286
296
|
const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE"];
|
|
287
297
|
if (options.headers !== false)
|
|
288
298
|
items.push("BODY.PEEK[HEADER]");
|
|
289
299
|
if (options.source)
|
|
290
300
|
items.push("BODY.PEEK[]");
|
|
291
301
|
const tag = proto.nextTag();
|
|
292
|
-
const
|
|
293
|
-
|
|
302
|
+
const streamed = [];
|
|
303
|
+
const cb = onMessage
|
|
304
|
+
? (resp) => {
|
|
305
|
+
if (resp.tag !== "*" || resp.type !== "FETCH")
|
|
306
|
+
return;
|
|
307
|
+
const parsed = this.parseFetchResponses([resp]);
|
|
308
|
+
for (const msg of parsed) {
|
|
309
|
+
streamed.push(msg);
|
|
310
|
+
onMessage(msg);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
: undefined;
|
|
314
|
+
const responses = await this.sendCommand(tag, proto.fetchCommand(tag, range, items), cb);
|
|
315
|
+
return onMessage ? streamed : this.parseFetchResponses(responses);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Folder-scoped batch body fetch. Selects `folderPath`, issues a single
|
|
319
|
+
* `UID FETCH <uids> (BODY.PEEK[] ...)`, streams each body through `onBody`
|
|
320
|
+
* as it arrives, and returns when the tagged OK is received.
|
|
321
|
+
*
|
|
322
|
+
* Eliminates per-message SELECT+FETCH round trips for callers like mailx-imap's
|
|
323
|
+
* prefetch path.
|
|
324
|
+
*/
|
|
325
|
+
async fetchBodiesBatch(folderPath, uids, onBody) {
|
|
326
|
+
if (uids.length === 0)
|
|
327
|
+
return;
|
|
328
|
+
if (this.selectedMailbox !== folderPath)
|
|
329
|
+
await this.select(folderPath);
|
|
330
|
+
await this.fetchMessagesStream(uids.join(","), { source: true, headers: false }, (msg) => {
|
|
331
|
+
if (msg.uid && msg.source)
|
|
332
|
+
onBody(msg.uid, msg.source);
|
|
333
|
+
});
|
|
294
334
|
}
|
|
295
335
|
/** Fetch messages since a UID */
|
|
296
336
|
async fetchSinceUid(sinceUid, options = {}, onChunk) {
|
|
@@ -527,7 +567,7 @@ export class NativeImapClient {
|
|
|
527
567
|
fetchChunkSizeMax;
|
|
528
568
|
/** Active command timer — reset by handleData on every data arrival */
|
|
529
569
|
commandTimer = null;
|
|
530
|
-
sendCommand(tag, command) {
|
|
570
|
+
sendCommand(tag, command, onUntagged) {
|
|
531
571
|
return new Promise((resolve, reject) => {
|
|
532
572
|
if (this.verbose && !command.includes("LOGIN") && !command.includes("AUTHENTICATE")) {
|
|
533
573
|
console.log(` [imap] > ${command.trimEnd()}`);
|
|
@@ -546,6 +586,7 @@ export class NativeImapClient {
|
|
|
546
586
|
clearTimeout(this.commandTimer); this.commandTimer = null; resolve(responses); },
|
|
547
587
|
reject: (err) => { if (this.commandTimer)
|
|
548
588
|
clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); },
|
|
589
|
+
onUntagged,
|
|
549
590
|
};
|
|
550
591
|
this.transport.write(command).catch((err) => { if (this.commandTimer)
|
|
551
592
|
clearTimeout(this.commandTimer); this.commandTimer = null; reject(err); });
|
|
@@ -696,6 +737,15 @@ export class NativeImapClient {
|
|
|
696
737
|
// Collect untagged responses for the pending command
|
|
697
738
|
if (resp.tag === "*" && this.pendingCommand) {
|
|
698
739
|
this.pendingCommand.responses.push(resp);
|
|
740
|
+
if (this.pendingCommand.onUntagged) {
|
|
741
|
+
try {
|
|
742
|
+
this.pendingCommand.onUntagged(resp);
|
|
743
|
+
}
|
|
744
|
+
catch (err) {
|
|
745
|
+
if (this.verbose)
|
|
746
|
+
console.error(` [imap] onUntagged callback threw: ${err.message}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
699
749
|
continue;
|
|
700
750
|
}
|
|
701
751
|
// Continuation response
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/iflow-direct",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "Bob Frankston",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
22
|
+
"@bobfrankston/tcp-transport": "^0.1.3"
|
|
23
23
|
},
|
|
24
24
|
"exports": {
|
|
25
25
|
".": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
".transformedSnapshot": {
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
50
|
+
"@bobfrankston/tcp-transport": "^0.1.3"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}
|