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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.0](https://github.com/postalsys/imapflow/compare/v2.4.3...v2.5.0) (2026-10-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * add a transport option and createSocketsTransport() for celld ([f4411aa](https://github.com/postalsys/imapflow/commit/f4411aa55635cb46b0e29a31965816e613d10ab9))
9
+ * add createStreamsTransport() for socket APIs built on web streams ([fec7999](https://github.com/postalsys/imapflow/commit/fec79997db36878a8e3222833837d09f011eac3e))
10
+
3
11
  ## [2.4.3](https://github.com/postalsys/imapflow/compare/v2.4.2...v2.4.3) (2026-10-15)
4
12
 
5
13
 
package/README.md CHANGED
@@ -26,7 +26,65 @@ npm install imapflow
26
26
 
27
27
  ImapFlow requires Node.js 20 or newer. The package ships both an ES module build and a CommonJS build with bundled type declarations, so no separate `@types` package is needed.
28
28
 
29
- The ES module build also runs on [Bun](https://bun.sh/) (tested against the latest release) and on [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the `nodejs_compat` compatibility flag. On Workers connect with implicit TLS (`secure: true`, usually port 993) or in cleartext: the runtime can not upgrade an already connected socket, so a STARTTLS negotiation fails with a TLS error, and it does not allow turning certificate validation off, so `tls: { rejectUnauthorized: false }` is rejected with `ERR_OPTION_NOT_IMPLEMENTED`. COMPRESS=DEFLATE, IDLE and the default pino logger work as on Node.js.
29
+ The ES module build also runs on [Bun](https://bun.sh/) (tested against the latest release) and on [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the `nodejs_compat` compatibility flag. On Workers connect with implicit TLS (`secure: true`, usually port 993) or in cleartext: the runtime can not upgrade an already connected socket, so a STARTTLS negotiation fails with a TLS error, and it does not allow turning certificate validation off, so `tls: { rejectUnauthorized: false }` is rejected with `ERR_OPTION_NOT_IMPLEMENTED`. COMPRESS=DEFLATE, IDLE and the default pino logger work as on Node.js. STARTTLS works on Workers when the connection goes through `createSocketsTransport()` (see below).
30
+
31
+ [celld](https://celld.dev/), the self-hosted runtime for Cloudflare-style Workers, has no `node:net`, `node:tls` or `node:dns`. Pass the `connect()` of `cloudflare:sockets` through the `transport` option:
32
+
33
+ ```js
34
+ import { connect } from 'cloudflare:sockets';
35
+ import { ImapFlow, createSocketsTransport } from 'imapflow';
36
+
37
+ const client = new ImapFlow({
38
+ host: 'imap.example.com',
39
+ port: 993,
40
+ secure: true,
41
+ auth: { user: 'user@example.com', pass: 'secret' },
42
+ transport: createSocketsTransport(connect)
43
+ });
44
+ ```
45
+
46
+ Implicit TLS, STARTTLS, cleartext, IDLE, `download()` and the default logger work. Not available there:
47
+
48
+ - The runtime verifies every certificate against its own root store, so `tls.rejectUnauthorized: false`, a custom `tls.ca`, client certificates and a `servername` that differs from `host` are refused with `UnsupportedTransportOption`. A TLS connection to an IP address needs a certificate issued for that address.
49
+ - COMPRESS=DEFLATE is skipped, the runtime has no streaming zlib.
50
+ - The `proxy` option can not be combined with a transport.
51
+ - `client.tls` stays `false`, the runtime reports no cipher details.
52
+ - A socket is closed when the request (or Durable Object event) that opened it ends. Run the whole session inside the handler, or inside `ctx.waitUntil()` to keep it going after the response.
53
+
54
+ ### Other connections
55
+
56
+ The `transport` option takes any object with a `connect()` method, so other socket APIs plug in the same way. For an API built on web streams (a connection that opens with a promise of a `ReadableStream` and a `WritableStream` of bytes, as `cloudflare:sockets` and Chrome's Direct Sockets do), `createStreamsTransport()` does the wrapping:
57
+
58
+ ```js
59
+ import { ImapFlow, createStreamsTransport } from 'imapflow';
60
+
61
+ const transport = createStreamsTransport({
62
+ // `secure` says whether ImapFlow wants a TLS connection; throw to refuse what the API can not do
63
+ open: ({ host, port, secure }) => {
64
+ const socket = openSocket(host, port, secure);
65
+ return {
66
+ opened: socket.opened.then(() => ({ readable: socket.readable, writable: socket.writable })),
67
+ close: () => socket.close()
68
+ };
69
+ },
70
+ // optional: start TLS on an open connection after STARTTLS. Without it ImapFlow does not try
71
+ // STARTTLS, and with doSTARTTLS: true the connection fails with err.tlsFailed set
72
+ upgrade: (socket, options) => startTlsSomehow(socket)
73
+ });
74
+ ```
75
+
76
+ `opened` may also report `remoteAddress`, `remotePort`, `localAddress` and `localPort`, and for a TLS connection `tls: { authorized, name, version }`, which becomes `client.tls`. If `close()` fails while the streams are locked, it is called again with the locks released.
77
+
78
+ An API built on events instead implements the transport itself: `connect(options, onConnect)` returns a Node.js `Duplex` stream of bytes and calls `onConnect` once the connection is up (or throws to refuse it), and an optional `upgrade(socket, options, onSecure)` starts TLS. The Duplex also needs `setTimeout(ms)`, emitting `timeout` after `ms` without traffic like `net.Socket` does: it is the watchdog behind the `socketTimeout` option.
79
+
80
+ In a browser, ImapFlow runs from the ES module build when the bundler provides what Node.js has and browsers do not:
81
+
82
+ - `node:stream` from [readable-stream](https://www.npmjs.com/package/readable-stream), `node:events` from [events](https://www.npmjs.com/package/events), and the `Buffer` global from [buffer](https://www.npmjs.com/package/buffer)
83
+ - the `process` global from [process](https://www.npmjs.com/package/process), and `setImmediate()` and `clearImmediate()` globals
84
+ - `node:crypto` with an MD5 `createHash()` (message ids on servers without OBJECTID), and `path` with `parse()` (used by a dependency for attachment file names)
85
+ - empty modules for `node:net`, `node:tls`, `node:dns` and `node:zlib` (COMPRESS is then skipped)
86
+
87
+ `test/browser/` in the repository has the complete esbuild setup, with the shims, and runs it in headless Chrome.
30
88
 
31
89
  ## Quick Example
32
90
 
@@ -47,6 +47,7 @@ export declare const ImapFlowErrorCode: {
47
47
  readonly EPROXY: "EPROXY";
48
48
  readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
49
49
  readonly ERR_INVALID_URL: "ERR_INVALID_URL";
50
+ readonly UnsupportedTransportOption: "UnsupportedTransportOption";
50
51
  readonly InstanceReused: "InstanceReused";
51
52
  };
52
53
  /** One of the {@link ImapFlowErrorCode} values */
@@ -58,6 +58,8 @@ exports.ImapFlowErrorCode = {
58
58
  EPROXY: 'EPROXY',
59
59
  UnsupportedProxyAddress: 'UnsupportedProxyAddress',
60
60
  ERR_INVALID_URL: 'ERR_INVALID_URL',
61
+ // the transport option
62
+ UnsupportedTransportOption: 'UnsupportedTransportOption',
61
63
  // API misuse
62
64
  InstanceReused: 'InstanceReused'
63
65
  };
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * @module imapflow
3
3
  */
4
- import tls from 'node:tls';
5
- import net from 'node:net';
6
4
  import { EventEmitter } from 'node:events';
7
5
  import { PassThrough, type Readable } from 'node:stream';
8
6
  import { AuthenticationFailure } from './errors.js';
9
- import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo } from './types.js';
7
+ import type { AppendResponseObject, CopyResponseObject, DownloadManyOptions, DownloadManyResult, DownloadObject, DownloadNotFound, DownloadOptions, ESearchResult, FetchMessageObject, FetchOptions, FetchQueryObject, IdInfoObject, ImapFlowEvents, ImapFlowOptions, InternalLogger, ListOptions, ListResponse, ListTreeResponse, MailboxCreateResponse, MailboxDeleteResponse, MailboxLockObject, MailboxLockOptions, MailboxObject, MailboxOpenOptions, MailboxRenameResponse, MessageRange, MessageRangeOptions, NamespaceObject, NamespacesObject, QuotaResponse, SearchObject, SearchOptions, SearchReturnOption, SequenceString, StatusObject, StatusQuery, StoreOptions, TlsInfo, ImapTransportSocket } from './types.js';
10
8
  export type * from './types.js';
11
9
  export type { ImapFlowError } from './errors.js';
12
10
  export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
11
+ export { createSocketsTransport } from './sockets-transport.js';
12
+ export type { SocketsApiSocket, SocketsConnect } from './sockets-transport.js';
13
+ export { createStreamsTransport } from './streams-transport.js';
14
+ export type { StreamsReader, StreamsSocket, StreamsSocketInfo, StreamsTlsInfo, StreamsTransportApi, StreamsWriter } from './streams-transport.js';
13
15
  export type { ImapAttribute, ImapAttributeList, ImapAttributeNode, ImapResponse } from './handler/types.js';
14
16
  declare const stateValues: {
15
17
  readonly NOT_AUTHENTICATED: 1;
@@ -27,13 +29,11 @@ export type ConnectionStates = {
27
29
  readonly [K in keyof typeof stateValues]: ConnectionState;
28
30
  };
29
31
  /**
30
- * The socket carrying the IMAP session. A TLS socket exposes `getCipher()` and `authorized`, a
31
- * cleartext socket does not, and the code checks for them at runtime.
32
+ * The socket carrying the IMAP session: a `net.Socket` or `tls.TLSSocket` on Node.js, or what a
33
+ * transport returns. Typed as {@link ImapTransportSocket}, so the client only relies on what
34
+ * every transport provides and checks for the optional members at runtime.
32
35
  */
33
- export type ImapSocket = net.Socket & {
34
- getCipher?: (() => tls.CipherNameAndProtocol) | undefined;
35
- authorized?: boolean | undefined;
36
- };
36
+ export type ImapSocket = ImapTransportSocket;
37
37
  /**
38
38
  * Where outgoing bytes are written: the socket itself, or the PassThrough that feeds the
39
39
  * DEFLATE stream once compression is active
@@ -39,10 +39,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.ImapFlow = exports.ImapFlowErrorCode = exports.AuthenticationFailure = void 0;
42
+ exports.ImapFlow = exports.createStreamsTransport = exports.createSocketsTransport = exports.ImapFlowErrorCode = exports.AuthenticationFailure = void 0;
43
43
  const node_tls_1 = __importDefault(require("node:tls"));
44
44
  const node_net_1 = __importDefault(require("node:net"));
45
- const node_crypto_1 = __importDefault(require("node:crypto"));
46
45
  const node_zlib_1 = __importDefault(require("node:zlib"));
47
46
  const node_events_1 = require("node:events");
48
47
  const node_stream_1 = require("node:stream");
@@ -59,6 +58,10 @@ const tools_js_1 = require("./tools.js");
59
58
  var errors_js_2 = require("./errors.js");
60
59
  Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
61
60
  Object.defineProperty(exports, "ImapFlowErrorCode", { enumerable: true, get: function () { return errors_js_2.ImapFlowErrorCode; } });
61
+ var sockets_transport_js_1 = require("./sockets-transport.js");
62
+ Object.defineProperty(exports, "createSocketsTransport", { enumerable: true, get: function () { return sockets_transport_js_1.createSocketsTransport; } });
63
+ var streams_transport_js_1 = require("./streams-transport.js");
64
+ Object.defineProperty(exports, "createStreamsTransport", { enumerable: true, get: function () { return streams_transport_js_1.createStreamsTransport; } });
62
65
  const GREETING_TIMEOUT = 16 * 1000;
63
66
  // Status responses that can carry an ALERT response code
64
67
  const ALERT_RESPONSES = new Set(['OK', 'NO', 'BAD', 'BYE', 'PREAUTH']);
@@ -234,7 +237,7 @@ class ImapFlow extends node_events_1.EventEmitter {
234
237
  // which is POP3 - a client created without an explicit port could never connect.
235
238
  this.port = Number(this.options.port) || (this.secureConnection ? 993 : 143);
236
239
  this.host = this.options.host || 'localhost';
237
- this.servername = this.options.servername ? this.options.servername : !node_net_1.default.isIP(this.host) ? this.host : false;
240
+ this.servername = this.options.servername ? this.options.servername : !(0, tools_js_1.isIpLiteral)(this.host) ? this.host : false;
238
241
  if (typeof this.options.secure === 'undefined' && this.port === 993) {
239
242
  // if secure option is not set but port is 993, then default to secure
240
243
  this.secureConnection = true;
@@ -361,7 +364,7 @@ class ImapFlow extends node_events_1.EventEmitter {
361
364
  }
362
365
  /** @internal */
363
366
  getRandomId() {
364
- let rid = BigInt('0x' + node_crypto_1.default.randomBytes(13).toString('hex')).toString(36);
367
+ let rid = BigInt('0x' + Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(13))).toString('hex')).toString(36);
365
368
  if (rid.length < 20) {
366
369
  rid = '0'.repeat(20 - rid.length) + rid;
367
370
  }
@@ -1185,6 +1188,10 @@ class ImapFlow extends node_events_1.EventEmitter {
1185
1188
  if (typeof socket.setTimeout === 'function') {
1186
1189
  socket.setTimeout(this.socketTimeout);
1187
1190
  }
1191
+ else {
1192
+ // a transport socket from plain JavaScript can lack it, which disables the watchdog
1193
+ this.log.warn({ msg: 'The socket has no setTimeout(), socketTimeout is not enforced', cid: this.id });
1194
+ }
1188
1195
  }
1189
1196
  /** @internal */
1190
1197
  setSocketHandlers() {
@@ -1370,6 +1377,12 @@ class ImapFlow extends node_events_1.EventEmitter {
1370
1377
  }
1371
1378
  /** @internal */
1372
1379
  async compress() {
1380
+ if (typeof node_zlib_1.default.createDeflateRaw !== 'function') {
1381
+ // The runtime has no streaming DEFLATE (celld implements only the synchronous zlib
1382
+ // functions), so the session stays uncompressed rather than failing after COMPRESS
1383
+ this.log.debug({ msg: 'Streaming DEFLATE is not available, skipping COMPRESS', cid: this.id });
1384
+ return;
1385
+ }
1373
1386
  if (!(await this.run('COMPRESS'))) {
1374
1387
  return; // was not able to negotiate compression
1375
1388
  }
@@ -1490,10 +1503,10 @@ class ImapFlow extends node_events_1.EventEmitter {
1490
1503
  });
1491
1504
  }
1492
1505
  /** @internal */
1493
- _failSTARTTLS() {
1506
+ _failSTARTTLS(reason = 'Server does not support STARTTLS') {
1494
1507
  if (this.options.doSTARTTLS === true) {
1495
1508
  // STARTTLS configured as requirement
1496
- let err = new Error('Server does not support STARTTLS');
1509
+ let err = new Error(reason);
1497
1510
  err.tlsFailed = true;
1498
1511
  throw err;
1499
1512
  }
@@ -1501,7 +1514,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1501
1514
  // may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
1502
1515
  // option for the policy).
1503
1516
  this.log.warn({
1504
- msg: 'Server does not support STARTTLS, continuing over an unencrypted connection',
1517
+ msg: reason + ', continuing over an unencrypted connection',
1505
1518
  host: this.host,
1506
1519
  port: this.port,
1507
1520
  cid: this.id
@@ -1528,7 +1541,13 @@ class ImapFlow extends node_events_1.EventEmitter {
1528
1541
  if (!this.capabilities.has('STARTTLS')) {
1529
1542
  return this._failSTARTTLS();
1530
1543
  }
1544
+ if (this.options.transport && !this.options.transport.upgrade) {
1545
+ return this._failSTARTTLS('The transport can not start TLS');
1546
+ }
1531
1547
  this.expectCapabilityUpdate = true;
1548
+ if (this.options.transport?.prepareUpgrade && this.socket) {
1549
+ this.options.transport.prepareUpgrade(this.socket);
1550
+ }
1532
1551
  let canUpgrade = await this.run('STARTTLS');
1533
1552
  if (!canUpgrade) {
1534
1553
  return this._failSTARTTLS();
@@ -1630,7 +1649,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1630
1649
  this.upgrading = true;
1631
1650
  let tlsSocket;
1632
1651
  try {
1633
- tlsSocket = node_tls_1.default.connect(opts, () => {
1652
+ let onSecure = () => {
1634
1653
  try {
1635
1654
  /* c8 ignore start */ // race: connection closed during the TLS handshake window
1636
1655
  if (this.isClosed) {
@@ -1682,7 +1701,11 @@ class ImapFlow extends node_events_1.EventEmitter {
1682
1701
  catch (ex) {
1683
1702
  this.emitError(ex);
1684
1703
  }
1685
- });
1704
+ };
1705
+ // a transport without upgrade() never gets here, see the STARTTLS check above
1706
+ tlsSocket = this.options.transport
1707
+ ? this.options.transport.upgrade(socketPlain, this.transportOptions(), onSecure)
1708
+ : node_tls_1.default.connect(opts, onSecure);
1686
1709
  }
1687
1710
  catch (err) {
1688
1711
  // tls.connect() refused the upgrade before any handshake (an option the runtime
@@ -2029,6 +2052,10 @@ class ImapFlow extends node_events_1.EventEmitter {
2029
2052
  tlsServername() {
2030
2053
  return this.servername ? { servername: this.servername } : {};
2031
2054
  }
2055
+ /** @internal */
2056
+ transportOptions() {
2057
+ return Object.assign({ host: this.host, port: this.port, tls: this.options.tls }, this.tlsServername());
2058
+ }
2032
2059
  // Normalizes a message range from various input formats into an IMAP-compatible
2033
2060
  // sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
2034
2061
  // {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
@@ -2155,6 +2182,10 @@ class ImapFlow extends node_events_1.EventEmitter {
2155
2182
  // these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
2156
2183
  this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
2157
2184
  this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
2185
+ let transport = this.options.transport;
2186
+ if (transport && this.options.proxy) {
2187
+ throw (0, errors_js_1.createImapError)('The proxy option can not be combined with a custom transport', 'UnsupportedTransportOption');
2188
+ }
2158
2189
  let socket = false;
2159
2190
  if (this.options.proxy) {
2160
2191
  try {
@@ -2271,6 +2302,19 @@ class ImapFlow extends node_events_1.EventEmitter {
2271
2302
  setImmediate(onConnect);
2272
2303
  }
2273
2304
  }
2305
+ else if (transport) {
2306
+ try {
2307
+ this.socket = transport.connect({ ...this.transportOptions(), secure: this.secureConnection }, onConnect);
2308
+ }
2309
+ catch (err) {
2310
+ // refused up front: the connection deadline must not stay armed
2311
+ (0, tools_js_1.clearTimer)(this.connectTimeout);
2312
+ this.closeAfter();
2313
+ this.log.error({ err, cid: this.id });
2314
+ reject(err);
2315
+ return;
2316
+ }
2317
+ }
2274
2318
  else {
2275
2319
  this.socket = connector.connect(opts, onConnect);
2276
2320
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.4.3";
2
+ export declare const version = "2.5.0";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = "imapflow";
6
- exports.version = "2.4.3";
6
+ exports.version = "2.5.0";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -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,89 @@
1
+ "use strict";
2
+ // The `transport` option over the `connect()` of the `cloudflare:sockets` API, for runtimes that
3
+ // have it but no usable `node:net` or `node:tls` (celld, Cloudflare Workers). A thin adapter of
4
+ // the API to createStreamsTransport().
5
+ //
6
+ // The runtime verifies every TLS certificate against its own root store and takes the SNI name
7
+ // from the hostname it connects to, so TLS options that would change either are refused rather
8
+ // than silently ignored.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createSocketsTransport = createSocketsTransport;
11
+ const errors_js_1 = require("./errors.js");
12
+ const streams_transport_js_1 = require("./streams-transport.js");
13
+ // TLS options the runtime can not honor: it always verifies the certificate against its own
14
+ // roots, and has no client certificates
15
+ const UNSUPPORTED_TLS_OPTIONS = ['ca', 'cert', 'key', 'pfx', 'passphrase', 'checkServerIdentity'];
16
+ function refuse(option) {
17
+ throw (0, errors_js_1.createImapError)(`The ${option} option is not supported by this transport`, 'UnsupportedTransportOption');
18
+ }
19
+ function assertOptions(options) {
20
+ const tlsOptions = options.tls || {};
21
+ if (tlsOptions.rejectUnauthorized === false) {
22
+ refuse('tls.rejectUnauthorized=false');
23
+ }
24
+ if (options.servername && options.servername !== options.host) {
25
+ refuse('servername');
26
+ }
27
+ const unsupported = UNSUPPORTED_TLS_OPTIONS.find(key => tlsOptions[key] !== undefined);
28
+ if (unsupported) {
29
+ refuse('tls.' + unsupported);
30
+ }
31
+ }
32
+ // "1.2.3.4:993" or "[::1]:993"
33
+ function splitAddress(address) {
34
+ const match = typeof address === 'string' && address.match(/^\[?(.*?)\]?:(\d+)$/);
35
+ return match ? { address: match[1], port: Number(match[2]) } : {};
36
+ }
37
+ function wrap(raw) {
38
+ return {
39
+ raw,
40
+ opened: raw.opened.then(info => {
41
+ const remote = splitAddress(info?.remoteAddress);
42
+ const local = splitAddress(info?.localAddress);
43
+ return {
44
+ readable: raw.readable,
45
+ writable: raw.writable,
46
+ remoteAddress: remote.address,
47
+ remotePort: remote.port,
48
+ localAddress: local.address,
49
+ localPort: local.port
50
+ };
51
+ }),
52
+ closed: raw.closed,
53
+ close: () => raw.close()
54
+ };
55
+ }
56
+ /**
57
+ * Builds a transport for the `transport` option from the `connect()` of the `cloudflare:sockets`
58
+ * API, for celld and other runtimes without `node:net` and `node:tls`:
59
+ *
60
+ * ```js
61
+ * import { connect } from 'cloudflare:sockets';
62
+ * const client = new ImapFlow({ host, port: 993, secure: true, auth, transport: createSocketsTransport(connect) });
63
+ * ```
64
+ *
65
+ * The runtime verifies the server certificate against its own root store, so the transport
66
+ * refuses `tls.rejectUnauthorized: false`, a custom `tls.ca`, client certificates and a
67
+ * `servername` that differs from `host`. The socket carries no cipher details, so `client.tls`
68
+ * stays `false` on a secure connection.
69
+ *
70
+ * @param connect - The `connect` export of `cloudflare:sockets`
71
+ * @returns A transport for the `transport` option
72
+ */
73
+ function createSocketsTransport(connect) {
74
+ return (0, streams_transport_js_1.createStreamsTransport)({
75
+ open(options) {
76
+ // TLS options matter only for a TLS connection, a cleartext one gets them checked if
77
+ // it is upgraded
78
+ if (options.secure) {
79
+ assertOptions(options);
80
+ }
81
+ // Cleartext sockets are opened in starttls mode, which costs nothing until an upgrade
82
+ return wrap(connect({ hostname: options.host, port: options.port }, { secureTransport: options.secure ? 'on' : 'starttls' }));
83
+ },
84
+ upgrade(socket, options) {
85
+ assertOptions(options);
86
+ return wrap(socket.raw.startTls());
87
+ }
88
+ });
89
+ }
@@ -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;