@bobfrankston/iflow-direct 0.1.35 → 0.1.39
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/imap-compat.d.ts +6 -0
- package/imap-compat.js +14 -0
- package/imap-native.d.ts +50 -11
- package/imap-native.js +156 -75
- package/package.json +3 -3
package/imap-compat.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ export declare class CompatImapClient {
|
|
|
66
66
|
getMessagesCount(mailbox: string): Promise<number>;
|
|
67
67
|
/** Get all UIDs in a mailbox */
|
|
68
68
|
getUids(mailbox: string): Promise<number[]>;
|
|
69
|
+
/** Get UIDs whose INTERNALDATE is on/after `since`. Bounded version of
|
|
70
|
+
* getUids — returns only the date window the caller cares about
|
|
71
|
+
* instead of the entire folder. Lets set-diff reconciliation scope
|
|
72
|
+
* itself to "messages from the last N days" rather than enumerating
|
|
73
|
+
* every UID in a 134k-message folder. */
|
|
74
|
+
getUidsSince(mailbox: string, since: Date): Promise<number[]>;
|
|
69
75
|
/** Fetch messages — supports two calling conventions for compatibility:
|
|
70
76
|
*
|
|
71
77
|
* New: fetchMessages(mailbox, "100:200") — UID range string
|
package/imap-compat.js
CHANGED
|
@@ -168,6 +168,20 @@ export class CompatImapClient {
|
|
|
168
168
|
await this.native.closeMailbox();
|
|
169
169
|
return uids;
|
|
170
170
|
}
|
|
171
|
+
/** Get UIDs whose INTERNALDATE is on/after `since`. Bounded version of
|
|
172
|
+
* getUids — returns only the date window the caller cares about
|
|
173
|
+
* instead of the entire folder. Lets set-diff reconciliation scope
|
|
174
|
+
* itself to "messages from the last N days" rather than enumerating
|
|
175
|
+
* every UID in a 134k-message folder. */
|
|
176
|
+
async getUidsSince(mailbox, since) {
|
|
177
|
+
await this.ensureConnected();
|
|
178
|
+
await this.native.select(mailbox);
|
|
179
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
180
|
+
const d = `${since.getUTCDate()}-${months[since.getUTCMonth()]}-${since.getUTCFullYear()}`;
|
|
181
|
+
const uids = await this.native.search(`SINCE ${d}`);
|
|
182
|
+
await this.native.closeMailbox();
|
|
183
|
+
return uids;
|
|
184
|
+
}
|
|
171
185
|
async fetchMessages(mailbox, rangeOrEnd, countOrOptions, maybeOptions) {
|
|
172
186
|
let range;
|
|
173
187
|
let options;
|
package/imap-native.d.ts
CHANGED
|
@@ -48,15 +48,29 @@ export declare class NativeImapClient {
|
|
|
48
48
|
private transport;
|
|
49
49
|
private transportFactory;
|
|
50
50
|
private config;
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
51
|
+
/** Byte-level receive buffer. IMAP literals (`{N}`) are octets — message
|
|
52
|
+
* bodies, MIME parts with binary content, non-UTF-8 charsets — and the
|
|
53
|
+
* parser MUST work in bytes to recover them with byte fidelity and
|
|
54
|
+
* without paying O(n²) re-encoding cost on every chunk arrival. The
|
|
55
|
+
* prior implementation kept `this.buffer` as a JS string, accumulated
|
|
56
|
+
* UTF-8-decoded text from the transport, then `TextEncoder.encode`'d
|
|
57
|
+
* the whole unread region on every TCP chunk while waiting for a
|
|
58
|
+
* literal — pegged the event loop on multi-message FETCH responses.
|
|
59
|
+
*
|
|
60
|
+
* Invariants:
|
|
61
|
+
* - `buffer[0 .. bufferLength)` holds valid received bytes
|
|
62
|
+
* - `buffer[bufferOffset .. bufferLength)` is unread
|
|
63
|
+
* - capacity = `buffer.length` ≥ `bufferLength`; we grow on demand
|
|
64
|
+
* - never substring — bufferOffset advances past consumed bytes;
|
|
65
|
+
* compactBufferIfNeeded reclaims space when consumed >= half. */
|
|
58
66
|
private buffer;
|
|
67
|
+
private bufferLength;
|
|
59
68
|
private bufferOffset;
|
|
69
|
+
/** Cached decoder so per-line / per-literal decode doesn't re-construct
|
|
70
|
+
* one. `fatal: false` (default) replaces invalid UTF-8 sequences with
|
|
71
|
+
* U+FFFD — same lenient behavior as JavaScript's implicit string
|
|
72
|
+
* decoding the parser used to rely on. */
|
|
73
|
+
private utf8Decoder;
|
|
60
74
|
private pendingCommand;
|
|
61
75
|
private capabilities;
|
|
62
76
|
private _connected;
|
|
@@ -213,22 +227,47 @@ export declare class NativeImapClient {
|
|
|
213
227
|
* EXISTS callback (e.g. mailpuller's pullMail) would be ignored by the
|
|
214
228
|
* server until the inactivity timer killed the socket.
|
|
215
229
|
*/
|
|
230
|
+
/** Per-client command serialization. IMAP allows only one command in
|
|
231
|
+
* flight at a time on a single connection (RFC 3501 §2.2.1). The
|
|
232
|
+
* client's `pendingCommand` field tracks that command — if a second
|
|
233
|
+
* caller concurrently invokes sendCommand, it overwrites
|
|
234
|
+
* pendingCommand and the first caller's await is orphaned. We saw
|
|
235
|
+
* this in production: the outbox poller's `withConnection`-queued
|
|
236
|
+
* task ran on the same shared ops client as syncFolder's INBOX work,
|
|
237
|
+
* and INBOX silently hung forever (no heartbeat fire, no wall-clock,
|
|
238
|
+
* no reject — the promise just had no command tracking it anymore).
|
|
239
|
+
*
|
|
240
|
+
* This chain serializes ALL sendCommand callers against this client
|
|
241
|
+
* regardless of whether the caller bothered to coordinate.
|
|
242
|
+
* Defensive — the right thing for the upper layer to do is also use
|
|
243
|
+
* the queue (withConnection in mailx-imap), but the IMAP client
|
|
244
|
+
* shouldn't allow itself to be corrupted by a careless caller. */
|
|
245
|
+
private commandChain;
|
|
216
246
|
private sendCommand;
|
|
217
247
|
private sendCommandCore;
|
|
218
248
|
private waitForContinuation;
|
|
219
249
|
private waitForTagged;
|
|
220
250
|
private handleData;
|
|
251
|
+
/** Append received bytes to the receive buffer, growing capacity if
|
|
252
|
+
* needed. O(n) over the chunk size, never over the buffer total —
|
|
253
|
+
* doubling growth amortizes to O(1) per byte across the response. */
|
|
254
|
+
private appendToBuffer;
|
|
221
255
|
/** Single canonical "command finished or abandoned" cleanup. Every
|
|
222
256
|
* path that ends a command's lifecycle — resolve, reject, timeout,
|
|
223
257
|
* socket close, command-replace — calls this. Was a leak source:
|
|
224
258
|
* closure-scoped timers in sendCommandCore couldn't be cleared from
|
|
225
259
|
* handleData's inline-replacement timer or from IDLE suspend/resume. */
|
|
226
260
|
private clearAllCommandTimers;
|
|
227
|
-
/** Compact the buffer when bufferOffset has advanced past half
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
261
|
+
/** Compact the buffer when bufferOffset has advanced past half the used
|
|
262
|
+
* region. copyWithin moves the unread bytes to the front in-place
|
|
263
|
+
* (no new allocation). Amortized O(1) per byte; never the O(n²)
|
|
264
|
+
* substring-each-line pathology that blocked the event loop on a
|
|
265
|
+
* large LIST response. */
|
|
231
266
|
private compactBufferIfNeeded;
|
|
267
|
+
/** Find the next CRLF (0x0D 0x0A) in the unread region, returning the
|
|
268
|
+
* byte index of the 0x0D, or -1 if not found. Manual byte loop —
|
|
269
|
+
* Uint8Array doesn't have a native indexOf for byte sequences. */
|
|
270
|
+
private indexOfCRLF;
|
|
232
271
|
private processBuffer;
|
|
233
272
|
private handleUntaggedResponse;
|
|
234
273
|
private parseFetchResponses;
|
package/imap-native.js
CHANGED
|
@@ -11,15 +11,29 @@ export class NativeImapClient {
|
|
|
11
11
|
transport;
|
|
12
12
|
transportFactory;
|
|
13
13
|
config;
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
14
|
+
/** Byte-level receive buffer. IMAP literals (`{N}`) are octets — message
|
|
15
|
+
* bodies, MIME parts with binary content, non-UTF-8 charsets — and the
|
|
16
|
+
* parser MUST work in bytes to recover them with byte fidelity and
|
|
17
|
+
* without paying O(n²) re-encoding cost on every chunk arrival. The
|
|
18
|
+
* prior implementation kept `this.buffer` as a JS string, accumulated
|
|
19
|
+
* UTF-8-decoded text from the transport, then `TextEncoder.encode`'d
|
|
20
|
+
* the whole unread region on every TCP chunk while waiting for a
|
|
21
|
+
* literal — pegged the event loop on multi-message FETCH responses.
|
|
22
|
+
*
|
|
23
|
+
* Invariants:
|
|
24
|
+
* - `buffer[0 .. bufferLength)` holds valid received bytes
|
|
25
|
+
* - `buffer[bufferOffset .. bufferLength)` is unread
|
|
26
|
+
* - capacity = `buffer.length` ≥ `bufferLength`; we grow on demand
|
|
27
|
+
* - never substring — bufferOffset advances past consumed bytes;
|
|
28
|
+
* compactBufferIfNeeded reclaims space when consumed >= half. */
|
|
29
|
+
buffer = new Uint8Array(8192);
|
|
30
|
+
bufferLength = 0;
|
|
22
31
|
bufferOffset = 0;
|
|
32
|
+
/** Cached decoder so per-line / per-literal decode doesn't re-construct
|
|
33
|
+
* one. `fatal: false` (default) replaces invalid UTF-8 sequences with
|
|
34
|
+
* U+FFFD — same lenient behavior as JavaScript's implicit string
|
|
35
|
+
* decoding the parser used to rely on. */
|
|
36
|
+
utf8Decoder = new TextDecoder("utf-8");
|
|
23
37
|
pendingCommand = null;
|
|
24
38
|
capabilities = new Set();
|
|
25
39
|
_connected = false;
|
|
@@ -73,7 +87,7 @@ export class NativeImapClient {
|
|
|
73
87
|
catch { /* ignore */ }
|
|
74
88
|
this.transport = this.transportFactory();
|
|
75
89
|
this._connected = false;
|
|
76
|
-
this.
|
|
90
|
+
this.bufferLength = 0;
|
|
77
91
|
this.bufferOffset = 0;
|
|
78
92
|
this.pendingCommand = null;
|
|
79
93
|
this.selectedMailbox = null;
|
|
@@ -788,22 +802,52 @@ export class NativeImapClient {
|
|
|
788
802
|
* EXISTS callback (e.g. mailpuller's pullMail) would be ignored by the
|
|
789
803
|
* server until the inactivity timer killed the socket.
|
|
790
804
|
*/
|
|
805
|
+
/** Per-client command serialization. IMAP allows only one command in
|
|
806
|
+
* flight at a time on a single connection (RFC 3501 §2.2.1). The
|
|
807
|
+
* client's `pendingCommand` field tracks that command — if a second
|
|
808
|
+
* caller concurrently invokes sendCommand, it overwrites
|
|
809
|
+
* pendingCommand and the first caller's await is orphaned. We saw
|
|
810
|
+
* this in production: the outbox poller's `withConnection`-queued
|
|
811
|
+
* task ran on the same shared ops client as syncFolder's INBOX work,
|
|
812
|
+
* and INBOX silently hung forever (no heartbeat fire, no wall-clock,
|
|
813
|
+
* no reject — the promise just had no command tracking it anymore).
|
|
814
|
+
*
|
|
815
|
+
* This chain serializes ALL sendCommand callers against this client
|
|
816
|
+
* regardless of whether the caller bothered to coordinate.
|
|
817
|
+
* Defensive — the right thing for the upper layer to do is also use
|
|
818
|
+
* the queue (withConnection in mailx-imap), but the IMAP client
|
|
819
|
+
* shouldn't allow itself to be corrupted by a careless caller. */
|
|
820
|
+
commandChain = Promise.resolve();
|
|
791
821
|
async sendCommand(tag, command, onUntagged) {
|
|
792
|
-
const
|
|
822
|
+
const prev = this.commandChain.catch(() => { });
|
|
823
|
+
let myResolve;
|
|
824
|
+
let myReject;
|
|
825
|
+
const myPromise = new Promise((res, rej) => { myResolve = res; myReject = rej; });
|
|
826
|
+
this.commandChain = myPromise.catch(() => { });
|
|
827
|
+
await prev;
|
|
793
828
|
try {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
829
|
+
const idleState = await this.suspendIdleForCommand();
|
|
830
|
+
try {
|
|
831
|
+
const out = await this.sendCommandCore(tag, command, onUntagged);
|
|
832
|
+
myResolve(out);
|
|
833
|
+
return out;
|
|
834
|
+
}
|
|
835
|
+
finally {
|
|
836
|
+
if (idleState) {
|
|
837
|
+
try {
|
|
838
|
+
await this.resumeIdleAfterCommand(idleState);
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
if (this.verbose)
|
|
842
|
+
console.error(` [imap] IDLE auto-resume failed: ${err.message}`);
|
|
843
|
+
}
|
|
804
844
|
}
|
|
805
845
|
}
|
|
806
846
|
}
|
|
847
|
+
catch (e) {
|
|
848
|
+
myReject(e);
|
|
849
|
+
throw e;
|
|
850
|
+
}
|
|
807
851
|
}
|
|
808
852
|
sendCommandCore(tag, command, onUntagged) {
|
|
809
853
|
return new Promise((resolve, reject) => {
|
|
@@ -920,9 +964,25 @@ export class NativeImapClient {
|
|
|
920
964
|
}
|
|
921
965
|
}, this.inactivityTimeout);
|
|
922
966
|
}
|
|
923
|
-
this.
|
|
967
|
+
this.appendToBuffer(data);
|
|
924
968
|
this.processBuffer();
|
|
925
969
|
}
|
|
970
|
+
/** Append received bytes to the receive buffer, growing capacity if
|
|
971
|
+
* needed. O(n) over the chunk size, never over the buffer total —
|
|
972
|
+
* doubling growth amortizes to O(1) per byte across the response. */
|
|
973
|
+
appendToBuffer(data) {
|
|
974
|
+
const need = this.bufferLength + data.length;
|
|
975
|
+
if (need > this.buffer.length) {
|
|
976
|
+
let cap = Math.max(this.buffer.length * 2, 8192);
|
|
977
|
+
while (cap < need)
|
|
978
|
+
cap *= 2;
|
|
979
|
+
const grown = new Uint8Array(cap);
|
|
980
|
+
grown.set(this.buffer.subarray(0, this.bufferLength));
|
|
981
|
+
this.buffer = grown;
|
|
982
|
+
}
|
|
983
|
+
this.buffer.set(data, this.bufferLength);
|
|
984
|
+
this.bufferLength += data.length;
|
|
985
|
+
}
|
|
926
986
|
/** Single canonical "command finished or abandoned" cleanup. Every
|
|
927
987
|
* path that ends a command's lifecycle — resolve, reject, timeout,
|
|
928
988
|
* socket close, command-replace — calls this. Was a leak source:
|
|
@@ -942,78 +1002,99 @@ export class NativeImapClient {
|
|
|
942
1002
|
this.wallClockTimer = null;
|
|
943
1003
|
}
|
|
944
1004
|
}
|
|
945
|
-
/** Compact the buffer when bufferOffset has advanced past half
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1005
|
+
/** Compact the buffer when bufferOffset has advanced past half the used
|
|
1006
|
+
* region. copyWithin moves the unread bytes to the front in-place
|
|
1007
|
+
* (no new allocation). Amortized O(1) per byte; never the O(n²)
|
|
1008
|
+
* substring-each-line pathology that blocked the event loop on a
|
|
1009
|
+
* large LIST response. */
|
|
949
1010
|
compactBufferIfNeeded() {
|
|
950
|
-
if (this.bufferOffset > 0 && this.bufferOffset >= this.
|
|
951
|
-
this.buffer
|
|
1011
|
+
if (this.bufferOffset > 0 && this.bufferOffset >= this.bufferLength / 2) {
|
|
1012
|
+
this.buffer.copyWithin(0, this.bufferOffset, this.bufferLength);
|
|
1013
|
+
this.bufferLength -= this.bufferOffset;
|
|
952
1014
|
this.bufferOffset = 0;
|
|
953
1015
|
}
|
|
954
1016
|
}
|
|
1017
|
+
/** Find the next CRLF (0x0D 0x0A) in the unread region, returning the
|
|
1018
|
+
* byte index of the 0x0D, or -1 if not found. Manual byte loop —
|
|
1019
|
+
* Uint8Array doesn't have a native indexOf for byte sequences. */
|
|
1020
|
+
indexOfCRLF(from) {
|
|
1021
|
+
const buf = this.buffer;
|
|
1022
|
+
const end = this.bufferLength - 1;
|
|
1023
|
+
for (let i = from; i < end; i++) {
|
|
1024
|
+
if (buf[i] === 0x0D && buf[i + 1] === 0x0A)
|
|
1025
|
+
return i;
|
|
1026
|
+
}
|
|
1027
|
+
return -1;
|
|
1028
|
+
}
|
|
955
1029
|
processBuffer() {
|
|
956
1030
|
while (true) {
|
|
957
|
-
// Check for literal {
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
1031
|
+
// Check for literal — `{N}` announces N octets of arbitrary data
|
|
1032
|
+
// (message body, MIME part, attachment chunk). The buffer is
|
|
1033
|
+
// bytes; the check is a direct length compare and the slice is
|
|
1034
|
+
// O(N) over the literal's own size — never O(buffer²).
|
|
961
1035
|
if (this.pendingCommand?.literalBytes != null) {
|
|
962
1036
|
const neededBytes = this.pendingCommand.literalBytes;
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const encoded = new TextEncoder().encode(unread);
|
|
968
|
-
const bufferBytes = encoded.byteLength;
|
|
969
|
-
if (bufferBytes >= neededBytes) {
|
|
970
|
-
const literal_bytes = encoded.subarray(0, neededBytes);
|
|
971
|
-
const rest_bytes = encoded.subarray(neededBytes);
|
|
972
|
-
let literal = new TextDecoder().decode(literal_bytes);
|
|
973
|
-
// Reset buffer to just the rest — bufferOffset goes back
|
|
974
|
-
// to 0 since we replaced the buffer content.
|
|
975
|
-
this.buffer = new TextDecoder().decode(rest_bytes);
|
|
976
|
-
this.bufferOffset = 0;
|
|
977
|
-
// For non-BODY literals (e.g. display names in ENVELOPE), wrap in quotes
|
|
978
|
-
// so tokenizeParenList treats them as a single token
|
|
979
|
-
if (!this.pendingCommand.currentLiteralKey) {
|
|
980
|
-
literal = `"${literal.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, "")}"`;
|
|
1037
|
+
const available = this.bufferLength - this.bufferOffset;
|
|
1038
|
+
if (available < neededBytes) {
|
|
1039
|
+
if (this.verbose && this.pendingCommand.literalBytes > 0) {
|
|
1040
|
+
console.log(` [imap] waiting for literal: need ${neededBytes} bytes, have ${available}`);
|
|
981
1041
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1042
|
+
break; // Wait for more data — cheap, no decode.
|
|
1043
|
+
}
|
|
1044
|
+
// Slice the literal bytes; advance the read cursor by N.
|
|
1045
|
+
// The bytes themselves stay in the underlying buffer for the
|
|
1046
|
+
// moment, but bufferOffset advances past them and the next
|
|
1047
|
+
// compactBufferIfNeeded will reclaim the space.
|
|
1048
|
+
const literalBytes = this.buffer.subarray(this.bufferOffset, this.bufferOffset + neededBytes);
|
|
1049
|
+
this.bufferOffset += neededBytes;
|
|
1050
|
+
// Decode literal once (UTF-8 with replacement). For BODY[]
|
|
1051
|
+
// literals carrying non-UTF-8 charsets this is still lossy —
|
|
1052
|
+
// the proper fix is to keep literals as bytes through to the
|
|
1053
|
+
// body store, tracked separately. The win HERE is that the
|
|
1054
|
+
// decode happens once per literal, not once per chunk that
|
|
1055
|
+
// arrives during the literal's transit.
|
|
1056
|
+
let literal = this.utf8Decoder.decode(literalBytes);
|
|
1057
|
+
// For non-BODY literals (e.g. display names in ENVELOPE), wrap in quotes
|
|
1058
|
+
// so tokenizeParenList treats them as a single token
|
|
1059
|
+
if (!this.pendingCommand.currentLiteralKey) {
|
|
1060
|
+
literal = `"${literal.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, "")}"`;
|
|
998
1061
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1062
|
+
this.pendingCommand.literalBuffer = (this.pendingCommand.literalBuffer || "") + literal;
|
|
1063
|
+
this.pendingCommand.literalBytes = undefined;
|
|
1064
|
+
// Store the literal data by its BODY key for parseFetchResponses
|
|
1065
|
+
if (this.pendingCommand.currentLiteralKey) {
|
|
1066
|
+
if (!this.pendingCommand.literals)
|
|
1067
|
+
this.pendingCommand.literals = new Map();
|
|
1068
|
+
this.pendingCommand.literals.set(this.pendingCommand.currentLiteralKey, literal);
|
|
1069
|
+
this.pendingCommand.currentLiteralKey = undefined;
|
|
1070
|
+
this.pendingCommand.currentLiteralSize = undefined;
|
|
1001
1071
|
}
|
|
1002
|
-
|
|
1072
|
+
if (this.verbose)
|
|
1073
|
+
console.log(` [imap] literal consumed, ${neededBytes} bytes, buffer remaining: ${this.bufferLength - this.bufferOffset}`);
|
|
1074
|
+
// Allow the consumed-prefix to be reclaimed when half-full —
|
|
1075
|
+
// long literal-heavy responses would otherwise grow the
|
|
1076
|
+
// backing buffer indefinitely between commands.
|
|
1077
|
+
this.compactBufferIfNeeded();
|
|
1078
|
+
continue;
|
|
1003
1079
|
}
|
|
1004
|
-
// Find next CRLF starting from the read offset
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1080
|
+
// Find next CRLF starting from the read offset on bytes (not
|
|
1081
|
+
// chars). bufferOffset advances past consumed bytes; we only
|
|
1082
|
+
// search the unread region so per-line work is O(line length),
|
|
1083
|
+
// never O(response size).
|
|
1084
|
+
const lineEnd = this.indexOfCRLF(this.bufferOffset);
|
|
1008
1085
|
if (lineEnd < 0) {
|
|
1009
1086
|
// No complete line yet. Compact the buffer if we've advanced
|
|
1010
1087
|
// far enough that the unconsumed-prefix is dragging.
|
|
1011
1088
|
this.compactBufferIfNeeded();
|
|
1012
1089
|
break;
|
|
1013
1090
|
}
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
|
|
1091
|
+
// Slice the line bytes (including trailing CRLF) and decode
|
|
1092
|
+
// once. IMAP control lines are 7-bit ASCII per RFC 3501; UTF-8
|
|
1093
|
+
// decode is a superset and produces the same characters. The
|
|
1094
|
+
// existing string-based parser (parseResponseLine, tokenizers,
|
|
1095
|
+
// ENVELOPE/FLAGS handling) consumes the decoded line below.
|
|
1096
|
+
const lineBytes = this.buffer.subarray(this.bufferOffset, lineEnd + 2);
|
|
1097
|
+
const line = this.utf8Decoder.decode(lineBytes);
|
|
1017
1098
|
this.bufferOffset = lineEnd + 2;
|
|
1018
1099
|
// Periodic compaction so the consumed prefix doesn't keep
|
|
1019
1100
|
// growing forever in long-running connections (IDLE, big sync).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/iflow-direct",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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.6"
|
|
23
23
|
},
|
|
24
24
|
"exports": {
|
|
25
25
|
".": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
".transformedSnapshot": {
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
53
|
+
"@bobfrankston/tcp-transport": "^0.1.6"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|