imapflow 2.4.3 → 2.5.0

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.
@@ -0,0 +1,44 @@
1
+ import { type StreamsReader, type StreamsWriter } from './streams-transport.js';
2
+ import type { ImapTransport } from './types.js';
3
+ /** The part of a `cloudflare:sockets` Socket the transport uses */
4
+ export interface SocketsApiSocket {
5
+ readable: {
6
+ getReader(): StreamsReader;
7
+ };
8
+ writable: {
9
+ getWriter(): StreamsWriter;
10
+ };
11
+ opened: Promise<{
12
+ remoteAddress?: string | undefined;
13
+ localAddress?: string | undefined;
14
+ } | undefined>;
15
+ closed: Promise<void>;
16
+ close(): Promise<void>;
17
+ startTls(): SocketsApiSocket;
18
+ }
19
+ /** The `connect()` function of the `cloudflare:sockets` API */
20
+ export type SocketsConnect = (address: {
21
+ hostname: string;
22
+ port: number;
23
+ }, options?: {
24
+ secureTransport?: 'off' | 'on' | 'starttls' | undefined;
25
+ allowHalfOpen?: boolean | undefined;
26
+ } | undefined) => SocketsApiSocket;
27
+ /**
28
+ * Builds a transport for the `transport` option from the `connect()` of the `cloudflare:sockets`
29
+ * API, for celld and other runtimes without `node:net` and `node:tls`:
30
+ *
31
+ * ```js
32
+ * import { connect } from 'cloudflare:sockets';
33
+ * const client = new ImapFlow({ host, port: 993, secure: true, auth, transport: createSocketsTransport(connect) });
34
+ * ```
35
+ *
36
+ * The runtime verifies the server certificate against its own root store, so the transport
37
+ * refuses `tls.rejectUnauthorized: false`, a custom `tls.ca`, client certificates and a
38
+ * `servername` that differs from `host`. The socket carries no cipher details, so `client.tls`
39
+ * stays `false` on a secure connection.
40
+ *
41
+ * @param connect - The `connect` export of `cloudflare:sockets`
42
+ * @returns A transport for the `transport` option
43
+ */
44
+ export declare function createSocketsTransport(connect: SocketsConnect): ImapTransport;
@@ -0,0 +1,86 @@
1
+ // The `transport` option over the `connect()` of the `cloudflare:sockets` API, for runtimes that
2
+ // have it but no usable `node:net` or `node:tls` (celld, Cloudflare Workers). A thin adapter of
3
+ // the API to createStreamsTransport().
4
+ //
5
+ // The runtime verifies every TLS certificate against its own root store and takes the SNI name
6
+ // from the hostname it connects to, so TLS options that would change either are refused rather
7
+ // than silently ignored.
8
+ import { createImapError } from './errors.js';
9
+ import { createStreamsTransport } from './streams-transport.js';
10
+ // TLS options the runtime can not honor: it always verifies the certificate against its own
11
+ // roots, and has no client certificates
12
+ const UNSUPPORTED_TLS_OPTIONS = ['ca', 'cert', 'key', 'pfx', 'passphrase', 'checkServerIdentity'];
13
+ function refuse(option) {
14
+ throw createImapError(`The ${option} option is not supported by this transport`, 'UnsupportedTransportOption');
15
+ }
16
+ function assertOptions(options) {
17
+ const tlsOptions = options.tls || {};
18
+ if (tlsOptions.rejectUnauthorized === false) {
19
+ refuse('tls.rejectUnauthorized=false');
20
+ }
21
+ if (options.servername && options.servername !== options.host) {
22
+ refuse('servername');
23
+ }
24
+ const unsupported = UNSUPPORTED_TLS_OPTIONS.find(key => tlsOptions[key] !== undefined);
25
+ if (unsupported) {
26
+ refuse('tls.' + unsupported);
27
+ }
28
+ }
29
+ // "1.2.3.4:993" or "[::1]:993"
30
+ function splitAddress(address) {
31
+ const match = typeof address === 'string' && address.match(/^\[?(.*?)\]?:(\d+)$/);
32
+ return match ? { address: match[1], port: Number(match[2]) } : {};
33
+ }
34
+ function wrap(raw) {
35
+ return {
36
+ raw,
37
+ opened: raw.opened.then(info => {
38
+ const remote = splitAddress(info?.remoteAddress);
39
+ const local = splitAddress(info?.localAddress);
40
+ return {
41
+ readable: raw.readable,
42
+ writable: raw.writable,
43
+ remoteAddress: remote.address,
44
+ remotePort: remote.port,
45
+ localAddress: local.address,
46
+ localPort: local.port
47
+ };
48
+ }),
49
+ closed: raw.closed,
50
+ close: () => raw.close()
51
+ };
52
+ }
53
+ /**
54
+ * Builds a transport for the `transport` option from the `connect()` of the `cloudflare:sockets`
55
+ * API, for celld and other runtimes without `node:net` and `node:tls`:
56
+ *
57
+ * ```js
58
+ * import { connect } from 'cloudflare:sockets';
59
+ * const client = new ImapFlow({ host, port: 993, secure: true, auth, transport: createSocketsTransport(connect) });
60
+ * ```
61
+ *
62
+ * The runtime verifies the server certificate against its own root store, so the transport
63
+ * refuses `tls.rejectUnauthorized: false`, a custom `tls.ca`, client certificates and a
64
+ * `servername` that differs from `host`. The socket carries no cipher details, so `client.tls`
65
+ * stays `false` on a secure connection.
66
+ *
67
+ * @param connect - The `connect` export of `cloudflare:sockets`
68
+ * @returns A transport for the `transport` option
69
+ */
70
+ export function createSocketsTransport(connect) {
71
+ return createStreamsTransport({
72
+ open(options) {
73
+ // TLS options matter only for a TLS connection, a cleartext one gets them checked if
74
+ // it is upgraded
75
+ if (options.secure) {
76
+ assertOptions(options);
77
+ }
78
+ // Cleartext sockets are opened in starttls mode, which costs nothing until an upgrade
79
+ return wrap(connect({ hostname: options.host, port: options.port }, { secureTransport: options.secure ? 'on' : 'starttls' }));
80
+ },
81
+ upgrade(socket, options) {
82
+ assertOptions(options);
83
+ return wrap(socket.raw.startTls());
84
+ }
85
+ });
86
+ }
@@ -0,0 +1,84 @@
1
+ import type { ImapTransport, ImapTransportConnectOptions, ImapTransportOptions } from './types.js';
2
+ /** The reader of {@link StreamsSocketInfo.readable} */
3
+ export interface StreamsReader {
4
+ read(): Promise<{
5
+ done: boolean;
6
+ value?: Uint8Array | undefined;
7
+ }>;
8
+ releaseLock(): void;
9
+ }
10
+ /** The writer of {@link StreamsSocketInfo.writable} */
11
+ export interface StreamsWriter {
12
+ write(chunk: Uint8Array): Promise<void>;
13
+ releaseLock(): void;
14
+ }
15
+ /** What a {@link StreamsSocket} resolves with once it is open */
16
+ export interface StreamsSocketInfo {
17
+ /** Bytes from the server, a ReadableStream of Uint8Array */
18
+ readable: {
19
+ getReader(): StreamsReader;
20
+ };
21
+ /** Bytes to the server, a WritableStream of Uint8Array */
22
+ writable: {
23
+ getWriter(): StreamsWriter;
24
+ };
25
+ remoteAddress?: string | undefined;
26
+ remotePort?: number | undefined;
27
+ localAddress?: string | undefined;
28
+ localPort?: number | undefined;
29
+ /** Details of a TLS connection, reported as `client.tls`. Left out when the API has none */
30
+ tls?: StreamsTlsInfo | undefined;
31
+ }
32
+ /** What a socket API knows about a TLS connection */
33
+ export interface StreamsTlsInfo {
34
+ /** Whether the server certificate was verified */
35
+ authorized: boolean;
36
+ /** Cipher name, e.g. `TLS_AES_256_GCM_SHA384` */
37
+ name?: string | undefined;
38
+ standardName?: string | undefined;
39
+ /** Protocol version, e.g. `TLSv1.3` */
40
+ version?: string | undefined;
41
+ }
42
+ /** A connection of a web streams socket API, as {@link StreamsTransportApi} opens it */
43
+ export interface StreamsSocket {
44
+ /** Resolves once the connection (and the TLS handshake, if any) is established, rejects if it fails */
45
+ opened: Promise<StreamsSocketInfo>;
46
+ /** Rejects if the connection fails after it was opened */
47
+ closed?: Promise<unknown> | undefined;
48
+ /** Closes the connection */
49
+ close(): unknown;
50
+ }
51
+ /** The socket API that {@link createStreamsTransport} drives */
52
+ export interface StreamsTransportApi<S extends StreamsSocket = StreamsSocket> {
53
+ /**
54
+ * Opens a connection, cleartext or TLS as `options.secure` says. Throws to refuse it, for
55
+ * example with an `UnsupportedTransportOption` error for TLS options the API can not honor.
56
+ */
57
+ open(options: ImapTransportConnectOptions): S;
58
+ /**
59
+ * Starts TLS on an open cleartext connection after STARTTLS and returns the TLS connection,
60
+ * the old one stays open as its transport. Throws to refuse the upgrade. Leave it out when
61
+ * the API can not upgrade a connection: ImapFlow then does not try STARTTLS.
62
+ */
63
+ upgrade?: ((socket: S, options: ImapTransportOptions) => S) | undefined;
64
+ }
65
+ /**
66
+ * Builds a transport for the `transport` option over a socket API built on web streams. A new
67
+ * API needs only `open()`, and `upgrade()` if it can start TLS on an open connection:
68
+ *
69
+ * ```js
70
+ * const transport = createStreamsTransport({
71
+ * open: ({ host, port, secure }) => {
72
+ * const socket = openSomeSocket(host, port, secure);
73
+ * return { opened: socket.ready.then(() => ({ readable: socket.readable, writable: socket.writable })), close: () => socket.close() };
74
+ * }
75
+ * });
76
+ * ```
77
+ *
78
+ * The transport stops reading once the server accepted a STARTTLS, so the API never has to start
79
+ * TLS with a read in flight, and releases the stream locks before `upgrade()` is called.
80
+ *
81
+ * @param api - Opens (and upgrades) connections
82
+ * @returns A transport for the `transport` option
83
+ */
84
+ export declare function createStreamsTransport<S extends StreamsSocket>(api: StreamsTransportApi<S>): ImapTransport;
@@ -0,0 +1,278 @@
1
+ // The `transport` option over a socket API built on web streams: the socket opens with a promise
2
+ // that resolves to a ReadableStream and a WritableStream of bytes. `cloudflare:sockets` (celld,
3
+ // Cloudflare Workers) is one, see sockets-transport.ts; Chrome's Direct Sockets and in-memory
4
+ // connections fit the same shape. The streams are wrapped into a Duplex that behaves like the
5
+ // net.Socket ImapFlow drives.
6
+ import { Duplex } from 'node:stream';
7
+ import { clearTimer, noop, unrefTimer } from './tools.js';
8
+ function closeQuietly(socket) {
9
+ try {
10
+ Promise.resolve(socket.close()).catch(noop);
11
+ }
12
+ catch {
13
+ // already closed
14
+ }
15
+ }
16
+ class StreamsDuplex extends Duplex {
17
+ constructor(api, socket, onConnect, plain) {
18
+ super({ allowHalfOpen: false });
19
+ // Taken once the socket is open: locking the streams of a socket returned by startTls()
20
+ // before its handshake completes stalls the handshake on celld
21
+ this._reader = null;
22
+ this._writer = null;
23
+ this._pumping = false;
24
+ this._readRequested = false;
25
+ // A runtime may only start TLS on a socket without a read in flight (with one, celld never
26
+ // completes the handshake), while the read loop always keeps one. So once the client
27
+ // announced a STARTTLS (holdAfterStarttls), reading stops after the tagged line that answers
28
+ // it, the only command in flight then, and resumes only when that line is not an OK.
29
+ this._awaitingStarttls = false;
30
+ this._lineTail = '';
31
+ // Set when the partial line got too long to be the one looked for and was dropped: the rest
32
+ // of it, up to the next line break, is skipped too
33
+ this._tailDropped = false;
34
+ this._readsHeld = false;
35
+ // Set once startTls() handed the connection to a TLS socket: the read that ends then is not
36
+ // the end of the connection, and closing this socket would close the TLS one
37
+ this._upgraded = false;
38
+ this._timeout = 0;
39
+ this._timer = null;
40
+ // Traffic only records the time, the one timer re-arms itself for the rest of the idle period
41
+ this._lastActivity = 0;
42
+ this._api = api;
43
+ this._socket = socket;
44
+ this._plain = plain || null;
45
+ socket.opened.then(info => {
46
+ if (this.destroyed) {
47
+ return;
48
+ }
49
+ try {
50
+ this._reader = info.readable.getReader();
51
+ this._writer = info.writable.getWriter();
52
+ }
53
+ catch (err) {
54
+ // streams already locked by someone else
55
+ this.destroy(err);
56
+ return;
57
+ }
58
+ this.remoteAddress = info.remoteAddress;
59
+ this.remotePort = info.remotePort;
60
+ this.localAddress = info.localAddress;
61
+ this.localPort = info.localPort;
62
+ const tls = info.tls;
63
+ if (tls) {
64
+ this.authorized = tls.authorized;
65
+ this.getCipher = () => ({ name: tls.name || '', standardName: tls.standardName || tls.name || '', version: tls.version || '' });
66
+ }
67
+ this._touch();
68
+ onConnect();
69
+ if (this._readRequested) {
70
+ void this._pump();
71
+ }
72
+ }, (err) => this.destroy(err));
73
+ socket.closed?.catch((err) => {
74
+ if (!this._upgraded) {
75
+ this.destroy(err);
76
+ }
77
+ });
78
+ }
79
+ _read() {
80
+ if (this._readsHeld) {
81
+ return;
82
+ }
83
+ if (!this._reader) {
84
+ this._readRequested = true;
85
+ }
86
+ else if (!this._pumping) {
87
+ void this._pump();
88
+ }
89
+ }
90
+ async _pump() {
91
+ const reader = this._reader;
92
+ this._pumping = true;
93
+ try {
94
+ for (;;) {
95
+ const { done, value } = await reader.read();
96
+ if (this._upgraded || this.destroyed) {
97
+ return;
98
+ }
99
+ if (done || !value) {
100
+ this.push(null);
101
+ return;
102
+ }
103
+ this._touch();
104
+ const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
105
+ const pushed = this.push(chunk);
106
+ if (this._awaitingStarttls && this._starttlsCompleted(chunk)) {
107
+ this._readsHeld = true;
108
+ return;
109
+ }
110
+ if (!pushed) {
111
+ return;
112
+ }
113
+ }
114
+ }
115
+ catch (err) {
116
+ if (!this._upgraded) {
117
+ this.destroy(err);
118
+ }
119
+ }
120
+ finally {
121
+ this._pumping = false;
122
+ }
123
+ }
124
+ // Whether the chunk holds the tagged response of the STARTTLS command and it is an OK. Lines
125
+ // end with LF, CR is optional, as for the parser
126
+ _starttlsCompleted(chunk) {
127
+ const skipFirst = this._tailDropped;
128
+ const lines = (this._lineTail + chunk.toString('latin1')).split('\n');
129
+ const tail = lines.pop();
130
+ if (skipFirst && !lines.length) {
131
+ // still inside the dropped line
132
+ return false;
133
+ }
134
+ this._tailDropped = tail.length > 1024;
135
+ this._lineTail = this._tailDropped ? '' : tail;
136
+ for (let i = skipFirst ? 1 : 0; i < lines.length; i++) {
137
+ const tagged = /^(?![*+] )\S+ (OK|NO|BAD)\b/i.exec(lines[i]);
138
+ if (tagged) {
139
+ this._awaitingStarttls = false;
140
+ return tagged[1].toUpperCase() === 'OK';
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+ /** Stops reading after the tagged response of the STARTTLS command about to be sent, if it is an OK */
146
+ holdAfterStarttls() {
147
+ this._awaitingStarttls = true;
148
+ this._lineTail = '';
149
+ this._tailDropped = false;
150
+ }
151
+ _write(chunk, _encoding, callback) {
152
+ if (!this._writer) {
153
+ // ImapFlow writes only after onConnect, this is a misuse
154
+ callback(new Error('Socket is not open yet'));
155
+ return;
156
+ }
157
+ this._touch();
158
+ this._writer.write(chunk).then(() => callback(), callback);
159
+ }
160
+ _destroy(err, callback) {
161
+ this._clearTimer();
162
+ if (!this._upgraded) {
163
+ this._close();
164
+ }
165
+ if (this._plain) {
166
+ // the plain connection went with the TLS one, unless the API keeps it open
167
+ this._plain._close();
168
+ this._plain.destroy();
169
+ }
170
+ callback(err);
171
+ }
172
+ // Closes the connection. An API that refuses to close while its streams are locked (Direct
173
+ // Sockets) gets a second attempt with the locks released; releasing them right away would
174
+ // fail the read in flight, which ends the request on celld
175
+ _close() {
176
+ const retry = () => {
177
+ for (const stream of [this._reader, this._writer]) {
178
+ try {
179
+ stream?.releaseLock();
180
+ }
181
+ catch {
182
+ // an older streams implementation refuses with a read in flight
183
+ }
184
+ }
185
+ closeQuietly(this._socket);
186
+ };
187
+ try {
188
+ const closing = this._socket.close();
189
+ if (typeof closing?.then === 'function') {
190
+ closing.then(undefined, retry);
191
+ }
192
+ }
193
+ catch {
194
+ retry();
195
+ }
196
+ }
197
+ /** Like net.Socket#setTimeout(): emits 'timeout' after `ms` without traffic, 0 disables */
198
+ setTimeout(ms) {
199
+ this._timeout = ms;
200
+ this._clearTimer();
201
+ this._touch();
202
+ return this;
203
+ }
204
+ setKeepAlive() {
205
+ return this;
206
+ }
207
+ /** Hands the connection over to a TLS socket, this one only stays as its transport */
208
+ startTls(options, onSecure) {
209
+ this._upgraded = true;
210
+ this._clearTimer();
211
+ // workerd refuses to start TLS while the streams are locked, nothing is in flight on them
212
+ this._reader?.releaseLock();
213
+ this._writer?.releaseLock();
214
+ let tlsSocket;
215
+ try {
216
+ tlsSocket = this._api.upgrade(this._socket, options);
217
+ }
218
+ catch (err) {
219
+ // refused before any handshake, the connection is closed with this socket
220
+ this._upgraded = false;
221
+ throw err;
222
+ }
223
+ return new StreamsDuplex(this._api, tlsSocket, onSecure, this);
224
+ }
225
+ _touch() {
226
+ this._lastActivity = Date.now();
227
+ if (!this._timer) {
228
+ this._arm(this._timeout);
229
+ }
230
+ }
231
+ _arm(ms) {
232
+ if (this._timeout > 0 && !this.destroyed && !this._upgraded) {
233
+ this._timer = unrefTimer(setTimeout(() => {
234
+ this._timer = null;
235
+ const idle = Date.now() - this._lastActivity;
236
+ if (idle >= this._timeout) {
237
+ this.emit('timeout');
238
+ }
239
+ else {
240
+ this._arm(this._timeout - idle);
241
+ }
242
+ }, ms));
243
+ }
244
+ }
245
+ _clearTimer() {
246
+ clearTimer(this._timer);
247
+ this._timer = null;
248
+ }
249
+ }
250
+ /**
251
+ * Builds a transport for the `transport` option over a socket API built on web streams. A new
252
+ * API needs only `open()`, and `upgrade()` if it can start TLS on an open connection:
253
+ *
254
+ * ```js
255
+ * const transport = createStreamsTransport({
256
+ * open: ({ host, port, secure }) => {
257
+ * const socket = openSomeSocket(host, port, secure);
258
+ * return { opened: socket.ready.then(() => ({ readable: socket.readable, writable: socket.writable })), close: () => socket.close() };
259
+ * }
260
+ * });
261
+ * ```
262
+ *
263
+ * The transport stops reading once the server accepted a STARTTLS, so the API never has to start
264
+ * TLS with a read in flight, and releases the stream locks before `upgrade()` is called.
265
+ *
266
+ * @param api - Opens (and upgrades) connections
267
+ * @returns A transport for the `transport` option
268
+ */
269
+ export function createStreamsTransport(api) {
270
+ const transport = {
271
+ connect: (options, onConnect) => new StreamsDuplex(api, api.open(options), onConnect)
272
+ };
273
+ if (api.upgrade) {
274
+ transport.prepareUpgrade = socket => socket.holdAfterStarttls();
275
+ transport.upgrade = (socket, options, onSecure) => socket.startTls(options, onSecure);
276
+ }
277
+ return transport;
278
+ }
@@ -7,6 +7,15 @@ export { AuthenticationFailure } from './errors.js';
7
7
  export declare const EXPANDED_RANGE_LIMIT = 16777216;
8
8
  export declare const MAX_UINT32_DIGITS = 10;
9
9
  export declare const noop: () => void;
10
+ /**
11
+ * Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
12
+ * Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
13
+ * colon (an IPv6 literal, a hostname never contains one).
14
+ *
15
+ * @param host - Hostname or IP address
16
+ * @returns true for an IP literal
17
+ */
18
+ export declare function isIpLiteral(host: string): boolean;
10
19
  /**
11
20
  * A stream decoder returned by getDecoder(). The Japanese decoder reports the `limited`
12
21
  * flag once it has buffered all it will accept; a streaming iconv decoder never sets it.
package/dist/esm/tools.js CHANGED
@@ -64,6 +64,17 @@ const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
64
64
  ]);
65
65
  // Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
66
66
  export const noop = () => { };
67
+ /**
68
+ * Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
69
+ * Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
70
+ * colon (an IPv6 literal, a hostname never contains one).
71
+ *
72
+ * @param host - Hostname or IP address
73
+ * @returns true for an IP literal
74
+ */
75
+ export function isIpLiteral(host) {
76
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(':');
77
+ }
67
78
  // The fields buildConnectionError() stamps to say *where* a connection error was rejected, as
68
79
  // opposed to what went wrong. restampConnectionError() clears them, because they belong to the
69
80
  // site that built the error rather than to the failure it describes.
@@ -1,5 +1,5 @@
1
1
  import type { ConnectionOptions, CipherNameAndProtocol } from 'node:tls';
2
- import type { Readable } from 'node:stream';
2
+ import type { Duplex, Readable } from 'node:stream';
3
3
  /**
4
4
  * Logger interface. Any object with `debug`, `info`, `warn` and `error` methods can be used,
5
5
  * for example a pino or bunyan instance. `trace` and `fatal` are used when present.
@@ -95,6 +95,14 @@ export interface ImapFlowOptions {
95
95
  * Node as-is. Proxy DNS and negotiation run inside `connectionTimeout`.
96
96
  */
97
97
  proxy?: string | undefined;
98
+ /**
99
+ * Opens the connection through this transport instead of `node:net` and `node:tls`, for
100
+ * runtimes without those modules. `createSocketsTransport()` builds one from the
101
+ * `connect()` of the `cloudflare:sockets` API (celld, Cloudflare Workers),
102
+ * `createStreamsTransport()` from any socket API built on web streams. Can not be combined
103
+ * with `proxy`.
104
+ */
105
+ transport?: ImapTransport | undefined;
98
106
  /** If true, then use QRESYNC instead of CONDSTORE. EXPUNGE notifications will include UID instead of sequence number */
99
107
  qresync?: boolean | undefined;
100
108
  /** If set, then breaks and restarts IDLE every maxIdleTime ms */
@@ -882,3 +890,70 @@ export interface ImapFlowEvents {
882
890
  /** The server sent an ALERT: text meant for the user, such as a planned shutdown or an account problem */
883
891
  alert: [alert: AlertEvent];
884
892
  }
893
+ /** Where {@link ImapTransport} connects to, and the `tls` options of the client */
894
+ export interface ImapTransportOptions {
895
+ host: string;
896
+ port: number;
897
+ /** The SNI name, absent for an IP literal host without the `servername` option */
898
+ servername?: string | undefined;
899
+ /** The `tls` option of the client */
900
+ tls?: ConnectionOptions | undefined;
901
+ }
902
+ /** What {@link ImapTransport.connect} gets: the destination, and whether to open a TLS connection */
903
+ export interface ImapTransportConnectOptions extends ImapTransportOptions {
904
+ /** Open a TLS connection (the `secure` option), otherwise a cleartext one */
905
+ secure: boolean;
906
+ }
907
+ /**
908
+ * The socket a {@link ImapTransport} returns: a Duplex stream of Buffers that emits `error`,
909
+ * `end` and `close`, and `timeout` once `setTimeout(ms)` was called and `ms` passed without
910
+ * traffic (0 disables it, like `net.Socket#setTimeout()`), the watchdog behind the
911
+ * `socketTimeout` option. The rest of what a `net.Socket` has is optional; ImapFlow uses it when
912
+ * it is there (the addresses for logging, the cipher of a TLS connection for `client.tls`).
913
+ */
914
+ export type ImapTransportSocket = Duplex & {
915
+ setTimeout(ms: number): unknown;
916
+ setKeepAlive?: ((enable?: boolean, initialDelay?: number) => unknown) | undefined;
917
+ remoteAddress?: string | undefined;
918
+ remotePort?: number | undefined;
919
+ localAddress?: string | undefined;
920
+ localPort?: number | undefined;
921
+ getCipher?: (() => CipherNameAndProtocol | null) | undefined;
922
+ authorized?: boolean | undefined;
923
+ };
924
+ /**
925
+ * Opens and upgrades the socket carrying the session, see the `transport` option.
926
+ * `createStreamsTransport()` builds one over any socket API built on web streams,
927
+ * `createSocketsTransport()` over `cloudflare:sockets`.
928
+ */
929
+ export interface ImapTransport {
930
+ /**
931
+ * Opens a connection, cleartext or TLS. Connection errors are emitted on the returned
932
+ * socket; a connection refused up front (an option the transport can not honor) may throw.
933
+ *
934
+ * @param options - Destination, TLS options and whether to open a TLS connection
935
+ * @param onConnect - Called once the connection (and the TLS handshake) is established
936
+ */
937
+ connect(options: ImapTransportConnectOptions, onConnect: () => void): ImapTransportSocket;
938
+ /**
939
+ * Called right before the STARTTLS command is sent on the cleartext socket, so a transport
940
+ * that can only start TLS without a read in flight can stop reading once it is answered.
941
+ * STARTTLS is the only command in flight then: the next tagged response line (one that does
942
+ * not start with `* ` or `+ `) answers it, and if it is an OK, the server sends nothing more
943
+ * before the TLS handshake.
944
+ *
945
+ * @param socket - The cleartext socket returned by connect()
946
+ */
947
+ prepareUpgrade?: ((socket: ImapTransportSocket) => void) | undefined;
948
+ /**
949
+ * Starts TLS on a connected cleartext socket after STARTTLS. May throw if the upgrade is
950
+ * refused before any handshake, handshake errors are emitted on the returned socket. A
951
+ * transport that can not start TLS leaves it out: ImapFlow then treats the server as one
952
+ * without STARTTLS (see the `doSTARTTLS` option).
953
+ *
954
+ * @param socket - The cleartext socket returned by connect()
955
+ * @param options - Destination and TLS options
956
+ * @param onSecure - Called once the TLS handshake is complete
957
+ */
958
+ upgrade?: ((socket: ImapTransportSocket, options: ImapTransportOptions, onSecure: () => void) => ImapTransportSocket) | undefined;
959
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -37,13 +37,17 @@
37
37
  "build": "node scripts/build.js",
38
38
  "typecheck": "node scripts/build.js --generate-only && tsc -p tsconfig.json",
39
39
  "pretest": "npm run build",
40
- "test": "node --import tsx --test --test-force-exit --test-concurrency=1 $(find test -name '*-test.ts' -not -path '*/integration/*' -not -path '*/cloudflare/*')",
40
+ "test": "node --import tsx --test --test-force-exit --test-concurrency=1 $(find test -name '*-test.ts' -not -path '*/integration/*' -not -path '*/cloudflare/*' -not -path '*/celld/*' -not -path '*/browser/*')",
41
41
  "pretest:coverage": "npm run build",
42
- "test:coverage": "c8 node --import tsx --test --test-force-exit --test-concurrency=1 $(find test -name '*-test.ts' -not -path '*/integration/*' -not -path '*/cloudflare/*')",
42
+ "test:coverage": "c8 node --import tsx --test --test-force-exit --test-concurrency=1 $(find test -name '*-test.ts' -not -path '*/integration/*' -not -path '*/cloudflare/*' -not -path '*/celld/*' -not -path '*/browser/*')",
43
43
  "pretest:bun": "npm run build",
44
- "test:bun": "bun test --timeout 60000 $(find ./test -name '*-test.ts' -not -path '*/integration/*' -not -path '*/cloudflare/*')",
44
+ "test:bun": "bun test --timeout 60000 $(find ./test -name '*-test.ts' -not -path '*/integration/*' -not -path '*/cloudflare/*' -not -path '*/celld/*' -not -path '*/browser/*')",
45
45
  "pretest:workers": "npm run build",
46
46
  "test:workers": "node --import tsx --test test/cloudflare/cloudflare-test.ts",
47
+ "pretest:celld": "npm run build",
48
+ "test:celld": "node --import tsx --test --test-force-exit test/celld/celld-test.ts",
49
+ "pretest:browser": "npm run build",
50
+ "test:browser": "node --import tsx --test --test-force-exit test/browser/browser-test.ts",
47
51
  "test:rev2": "bash test/integration/run-rev2-tests.sh",
48
52
  "test:mutation": "node scripts/mutation-test.js",
49
53
  "format": "prettier --write \"**/*.{js,cjs,ts,json,md,yml,yaml}\"",
@@ -71,12 +75,19 @@
71
75
  "homepage": "https://imapflow.com/",
72
76
  "devDependencies": {
73
77
  "@types/node": "20.19.43",
78
+ "buffer": "6.0.3",
74
79
  "c8": "12.0.0",
80
+ "esbuild": "0.28.2",
75
81
  "eslint": "10.12.0",
76
82
  "eslint-config-prettier": "10.1.8",
83
+ "events": "3.3.0",
77
84
  "globals": "17.13.0",
78
- "imapkit": "5.0.1",
85
+ "imapkit": "5.1.0",
86
+ "playwright-core": "1.64.1",
79
87
  "prettier": "3.9.10",
88
+ "process": "0.11.10",
89
+ "readable-stream": "4.8.0",
90
+ "string_decoder": "1.3.0",
80
91
  "tsx": "4.23.15",
81
92
  "types-node-legacy": "npm:@types/node@20.0.0",
82
93
  "typescript": "6.0.3",