imapflow 2.4.2 → 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 +17 -0
- package/README.md +59 -1
- package/dist/cjs/errors.d.ts +2 -0
- package/dist/cjs/errors.js +3 -0
- package/dist/cjs/imap-flow.d.ts +9 -9
- package/dist/cjs/imap-flow.js +100 -15
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/sockets-transport.d.ts +44 -0
- package/dist/cjs/sockets-transport.js +89 -0
- package/dist/cjs/streams-transport.d.ts +84 -0
- package/dist/cjs/streams-transport.js +281 -0
- package/dist/cjs/tools.d.ts +9 -0
- package/dist/cjs/tools.js +12 -0
- package/dist/cjs/types.d.ts +76 -1
- package/dist/esm/errors.d.ts +2 -0
- package/dist/esm/errors.js +3 -0
- package/dist/esm/imap-flow.d.ts +9 -9
- package/dist/esm/imap-flow.js +98 -15
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/sockets-transport.d.ts +44 -0
- package/dist/esm/sockets-transport.js +86 -0
- package/dist/esm/streams-transport.d.ts +84 -0
- package/dist/esm/streams-transport.js +278 -0
- package/dist/esm/tools.d.ts +9 -0
- package/dist/esm/tools.js +11 -0
- package/dist/esm/types.d.ts +76 -1
- package/package.json +16 -5
package/dist/esm/imap-flow.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import tls from 'node:tls';
|
|
5
5
|
import net from 'node:net';
|
|
6
|
-
import crypto from 'node:crypto';
|
|
7
6
|
import zlib from 'node:zlib';
|
|
8
7
|
import { EventEmitter } from 'node:events';
|
|
9
8
|
import { PassThrough } from 'node:stream';
|
|
@@ -16,8 +15,10 @@ import { ConnectionDeadline } from './connection-deadline.js';
|
|
|
16
15
|
import { downloadMessage, downloadMessageParts } from './download.js';
|
|
17
16
|
import { AuthenticationFailure, createImapError } from './errors.js';
|
|
18
17
|
import imapCommands from './imap-commands.js';
|
|
19
|
-
import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, mergeFetchRows, packMessageRange, normalizePath, expandRange, getColorFlags, hasCapability, isRev2Active, logConnectionError, unrefTimer, clearTimer, parseUintValue, getStringList, getTextValues, emitSafe, buildConnectionError, guardedPromise, guardedReject, MAX_UINT32_DIGITS } from './tools.js';
|
|
18
|
+
import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, mergeFetchRows, packMessageRange, normalizePath, expandRange, getColorFlags, hasCapability, isRev2Active, logConnectionError, unrefTimer, clearTimer, parseUintValue, getStringList, getTextValues, emitSafe, buildConnectionError, guardedPromise, guardedReject, isIpLiteral, MAX_UINT32_DIGITS } from './tools.js';
|
|
20
19
|
export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
|
|
20
|
+
export { createSocketsTransport } from './sockets-transport.js';
|
|
21
|
+
export { createStreamsTransport } from './streams-transport.js';
|
|
21
22
|
const GREETING_TIMEOUT = 16 * 1000;
|
|
22
23
|
// Status responses that can carry an ALERT response code
|
|
23
24
|
const ALERT_RESPONSES = new Set(['OK', 'NO', 'BAD', 'BYE', 'PREAUTH']);
|
|
@@ -193,7 +194,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
193
194
|
// which is POP3 - a client created without an explicit port could never connect.
|
|
194
195
|
this.port = Number(this.options.port) || (this.secureConnection ? 993 : 143);
|
|
195
196
|
this.host = this.options.host || 'localhost';
|
|
196
|
-
this.servername = this.options.servername ? this.options.servername : !
|
|
197
|
+
this.servername = this.options.servername ? this.options.servername : !isIpLiteral(this.host) ? this.host : false;
|
|
197
198
|
if (typeof this.options.secure === 'undefined' && this.port === 993) {
|
|
198
199
|
// if secure option is not set but port is 993, then default to secure
|
|
199
200
|
this.secureConnection = true;
|
|
@@ -320,7 +321,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
320
321
|
}
|
|
321
322
|
/** @internal */
|
|
322
323
|
getRandomId() {
|
|
323
|
-
let rid = BigInt('0x' + crypto.
|
|
324
|
+
let rid = BigInt('0x' + Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(13))).toString('hex')).toString(36);
|
|
324
325
|
if (rid.length < 20) {
|
|
325
326
|
rid = '0'.repeat(20 - rid.length) + rid;
|
|
326
327
|
}
|
|
@@ -740,7 +741,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
740
741
|
*/
|
|
741
742
|
rejectUnparsedCompletion(payload, parserError) {
|
|
742
743
|
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
743
|
-
return;
|
|
744
|
+
return false;
|
|
744
745
|
}
|
|
745
746
|
// Prefer the tag the parser had already extracted before it failed - it went
|
|
746
747
|
// through the same leading-NUL workaround as every parsed response. Fall back
|
|
@@ -752,11 +753,45 @@ export class ImapFlow extends EventEmitter {
|
|
|
752
753
|
tag = match && match[1];
|
|
753
754
|
}
|
|
754
755
|
if (!tag || tag !== this.currentRequest.tag) {
|
|
755
|
-
return;
|
|
756
|
+
return false;
|
|
756
757
|
}
|
|
757
758
|
let err = new Error('Failed to parse the server response for this command');
|
|
758
759
|
err.code = parserError.code || 'ParserError';
|
|
759
760
|
err.parserError = parserError;
|
|
761
|
+
this.failInFlight(err);
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Fails the in-flight command when the server answered it with a NO or BAD status line that
|
|
766
|
+
* has no tag at all (Namecheap Private Email: `BAD "Must issue a STARTTLS command first"`).
|
|
767
|
+
* Only one command is ever on the wire, and leaving it pending would stall the connection
|
|
768
|
+
* until the socket timeout. Tags are uppercase hex, so a command can carry the tag BAD:
|
|
769
|
+
* callers check for the command's own tag first.
|
|
770
|
+
*
|
|
771
|
+
* @param payload - Raw bytes of the line.
|
|
772
|
+
* @returns `true` when the line failed the in-flight command
|
|
773
|
+
* @internal
|
|
774
|
+
*/
|
|
775
|
+
rejectTaglessCompletion(payload) {
|
|
776
|
+
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
let match = payload.toString('latin1', 0, 1024).match(/^\0*(BAD|NO)(?:\s+([^]*))?$/i);
|
|
780
|
+
if (!match) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
let err = createImapError('Command failed', 'MissingTag', {
|
|
784
|
+
command: this.currentRequest.command,
|
|
785
|
+
responseStatus: match[1].toUpperCase(),
|
|
786
|
+
responseText: (match[2] || '').trim().replace(/^"(.*)"$/, '$1')
|
|
787
|
+
});
|
|
788
|
+
this.log.warn({ msg: 'Server sent a status response without a tag', err, cid: this.id });
|
|
789
|
+
this.failInFlight(err);
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
// Rejects the in-flight command and moves on to the next queued one
|
|
793
|
+
/** @internal */
|
|
794
|
+
failInFlight(err) {
|
|
760
795
|
this.rejectCurrentRequest(err);
|
|
761
796
|
this.trySend().catch(sendErr => logConnectionError(this, 'Failed to dispatch command', sendErr));
|
|
762
797
|
}
|
|
@@ -806,7 +841,9 @@ export class ImapFlow extends EventEmitter {
|
|
|
806
841
|
// timeout fires. The tag is recovered from the raw bytes (a tag is
|
|
807
842
|
// ASTRING-CHAR only, so it survives whatever made the rest unparseable) and
|
|
808
843
|
// the command is failed with the parser error instead of hanging.
|
|
809
|
-
this.rejectUnparsedCompletion(data.payload, err)
|
|
844
|
+
if (!this.rejectUnparsedCompletion(data.payload, err)) {
|
|
845
|
+
this.rejectTaglessCompletion(data.payload);
|
|
846
|
+
}
|
|
810
847
|
return true;
|
|
811
848
|
}
|
|
812
849
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
@@ -911,7 +948,8 @@ export class ImapFlow extends EventEmitter {
|
|
|
911
948
|
// actually written to the socket (invariant 2), so the three cases below are kept apart:
|
|
912
949
|
// the active command completes, a command that has not been written yet is proof of
|
|
913
950
|
// desynchronization (queued behind another command, or current but not yet on the wire),
|
|
914
|
-
// and an entirely unknown tag is recorded but tolerated
|
|
951
|
+
// and an entirely unknown tag is recorded but tolerated, unless the line is a NO or BAD
|
|
952
|
+
// that came without a tag (see rejectTaglessCompletion()).
|
|
915
953
|
if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
|
|
916
954
|
if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
|
|
917
955
|
let request = this.takeCurrentRequest();
|
|
@@ -949,7 +987,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
949
987
|
this.failProtocol(err);
|
|
950
988
|
return false;
|
|
951
989
|
}
|
|
952
|
-
else {
|
|
990
|
+
else if (!this.rejectTaglessCompletion(data.payload)) {
|
|
953
991
|
this.countUnknownTag(parsed.tag);
|
|
954
992
|
}
|
|
955
993
|
}
|
|
@@ -1107,6 +1145,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1107
1145
|
if (typeof socket.setTimeout === 'function') {
|
|
1108
1146
|
socket.setTimeout(this.socketTimeout);
|
|
1109
1147
|
}
|
|
1148
|
+
else {
|
|
1149
|
+
// a transport socket from plain JavaScript can lack it, which disables the watchdog
|
|
1150
|
+
this.log.warn({ msg: 'The socket has no setTimeout(), socketTimeout is not enforced', cid: this.id });
|
|
1151
|
+
}
|
|
1110
1152
|
}
|
|
1111
1153
|
/** @internal */
|
|
1112
1154
|
setSocketHandlers() {
|
|
@@ -1225,10 +1267,14 @@ export class ImapFlow extends EventEmitter {
|
|
|
1225
1267
|
/** @internal */
|
|
1226
1268
|
async startSession() {
|
|
1227
1269
|
await this.run('CAPABILITY');
|
|
1270
|
+
await this.upgradeToSTARTTLS();
|
|
1271
|
+
// ID goes after STARTTLS: the client info should not travel in cleartext, and some
|
|
1272
|
+
// servers refuse every command but STARTTLS before the upgrade with an untagged BAD
|
|
1273
|
+
// the client can not tie to ID, which then waits for the socket timeout (Namecheap
|
|
1274
|
+
// Private Email). The capability list was refreshed by the upgrade.
|
|
1228
1275
|
if (this.capabilities.has('ID')) {
|
|
1229
1276
|
this.idRequested = await this.run('ID', this.clientInfo);
|
|
1230
1277
|
}
|
|
1231
|
-
await this.upgradeToSTARTTLS();
|
|
1232
1278
|
await this.authenticate();
|
|
1233
1279
|
if ((!this.idRequested || Object.keys(this.idRequested).length < 2) && this.capabilities.has('ID')) {
|
|
1234
1280
|
// re-request ID after LOGIN
|
|
@@ -1288,6 +1334,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
1288
1334
|
}
|
|
1289
1335
|
/** @internal */
|
|
1290
1336
|
async compress() {
|
|
1337
|
+
if (typeof zlib.createDeflateRaw !== 'function') {
|
|
1338
|
+
// The runtime has no streaming DEFLATE (celld implements only the synchronous zlib
|
|
1339
|
+
// functions), so the session stays uncompressed rather than failing after COMPRESS
|
|
1340
|
+
this.log.debug({ msg: 'Streaming DEFLATE is not available, skipping COMPRESS', cid: this.id });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1291
1343
|
if (!(await this.run('COMPRESS'))) {
|
|
1292
1344
|
return; // was not able to negotiate compression
|
|
1293
1345
|
}
|
|
@@ -1408,10 +1460,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1408
1460
|
});
|
|
1409
1461
|
}
|
|
1410
1462
|
/** @internal */
|
|
1411
|
-
_failSTARTTLS() {
|
|
1463
|
+
_failSTARTTLS(reason = 'Server does not support STARTTLS') {
|
|
1412
1464
|
if (this.options.doSTARTTLS === true) {
|
|
1413
1465
|
// STARTTLS configured as requirement
|
|
1414
|
-
let err = new Error(
|
|
1466
|
+
let err = new Error(reason);
|
|
1415
1467
|
err.tlsFailed = true;
|
|
1416
1468
|
throw err;
|
|
1417
1469
|
}
|
|
@@ -1419,7 +1471,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1419
1471
|
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1420
1472
|
// option for the policy).
|
|
1421
1473
|
this.log.warn({
|
|
1422
|
-
msg:
|
|
1474
|
+
msg: reason + ', continuing over an unencrypted connection',
|
|
1423
1475
|
host: this.host,
|
|
1424
1476
|
port: this.port,
|
|
1425
1477
|
cid: this.id
|
|
@@ -1446,7 +1498,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
1446
1498
|
if (!this.capabilities.has('STARTTLS')) {
|
|
1447
1499
|
return this._failSTARTTLS();
|
|
1448
1500
|
}
|
|
1501
|
+
if (this.options.transport && !this.options.transport.upgrade) {
|
|
1502
|
+
return this._failSTARTTLS('The transport can not start TLS');
|
|
1503
|
+
}
|
|
1449
1504
|
this.expectCapabilityUpdate = true;
|
|
1505
|
+
if (this.options.transport?.prepareUpgrade && this.socket) {
|
|
1506
|
+
this.options.transport.prepareUpgrade(this.socket);
|
|
1507
|
+
}
|
|
1450
1508
|
let canUpgrade = await this.run('STARTTLS');
|
|
1451
1509
|
if (!canUpgrade) {
|
|
1452
1510
|
return this._failSTARTTLS();
|
|
@@ -1548,7 +1606,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1548
1606
|
this.upgrading = true;
|
|
1549
1607
|
let tlsSocket;
|
|
1550
1608
|
try {
|
|
1551
|
-
|
|
1609
|
+
let onSecure = () => {
|
|
1552
1610
|
try {
|
|
1553
1611
|
/* c8 ignore start */ // race: connection closed during the TLS handshake window
|
|
1554
1612
|
if (this.isClosed) {
|
|
@@ -1600,7 +1658,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
1600
1658
|
catch (ex) {
|
|
1601
1659
|
this.emitError(ex);
|
|
1602
1660
|
}
|
|
1603
|
-
}
|
|
1661
|
+
};
|
|
1662
|
+
// a transport without upgrade() never gets here, see the STARTTLS check above
|
|
1663
|
+
tlsSocket = this.options.transport
|
|
1664
|
+
? this.options.transport.upgrade(socketPlain, this.transportOptions(), onSecure)
|
|
1665
|
+
: tls.connect(opts, onSecure);
|
|
1604
1666
|
}
|
|
1605
1667
|
catch (err) {
|
|
1606
1668
|
// tls.connect() refused the upgrade before any handshake (an option the runtime
|
|
@@ -1947,6 +2009,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1947
2009
|
tlsServername() {
|
|
1948
2010
|
return this.servername ? { servername: this.servername } : {};
|
|
1949
2011
|
}
|
|
2012
|
+
/** @internal */
|
|
2013
|
+
transportOptions() {
|
|
2014
|
+
return Object.assign({ host: this.host, port: this.port, tls: this.options.tls }, this.tlsServername());
|
|
2015
|
+
}
|
|
1950
2016
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1951
2017
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1952
2018
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -2073,6 +2139,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
2073
2139
|
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
2074
2140
|
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
2075
2141
|
this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
|
|
2142
|
+
let transport = this.options.transport;
|
|
2143
|
+
if (transport && this.options.proxy) {
|
|
2144
|
+
throw createImapError('The proxy option can not be combined with a custom transport', 'UnsupportedTransportOption');
|
|
2145
|
+
}
|
|
2076
2146
|
let socket = false;
|
|
2077
2147
|
if (this.options.proxy) {
|
|
2078
2148
|
try {
|
|
@@ -2189,6 +2259,19 @@ export class ImapFlow extends EventEmitter {
|
|
|
2189
2259
|
setImmediate(onConnect);
|
|
2190
2260
|
}
|
|
2191
2261
|
}
|
|
2262
|
+
else if (transport) {
|
|
2263
|
+
try {
|
|
2264
|
+
this.socket = transport.connect({ ...this.transportOptions(), secure: this.secureConnection }, onConnect);
|
|
2265
|
+
}
|
|
2266
|
+
catch (err) {
|
|
2267
|
+
// refused up front: the connection deadline must not stay armed
|
|
2268
|
+
clearTimer(this.connectTimeout);
|
|
2269
|
+
this.closeAfter();
|
|
2270
|
+
this.log.error({ err, cid: this.id });
|
|
2271
|
+
reject(err);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2192
2275
|
else {
|
|
2193
2276
|
this.socket = connector.connect(opts, onConnect);
|
|
2194
2277
|
}
|
package/dist/esm/package-info.js
CHANGED
|
@@ -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;
|