imapflow 2.4.3 → 2.5.1
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 +15 -0
- package/README.md +59 -1
- package/dist/cjs/commands/authenticate.js +32 -1
- package/dist/cjs/commands/fetch.js +10 -3
- package/dist/cjs/commands/idle.js +31 -1
- package/dist/cjs/commands/list.js +14 -0
- package/dist/cjs/errors.d.ts +2 -0
- package/dist/cjs/errors.js +4 -0
- package/dist/cjs/handler/imap-parser.js +3 -0
- package/dist/cjs/handler/parser-instance.d.ts +2 -0
- package/dist/cjs/handler/parser-instance.js +3 -1
- package/dist/cjs/handler/token-parser.d.ts +21 -2
- package/dist/cjs/handler/token-parser.js +76 -6
- package/dist/cjs/handler/types.d.ts +5 -0
- package/dist/cjs/imap-flow.d.ts +9 -9
- package/dist/cjs/imap-flow.js +90 -14
- 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 +30 -0
- package/dist/cjs/tools.js +130 -52
- package/dist/cjs/types.d.ts +78 -2
- package/dist/esm/commands/authenticate.js +32 -1
- package/dist/esm/commands/fetch.js +11 -4
- package/dist/esm/commands/idle.js +31 -1
- package/dist/esm/commands/list.js +15 -1
- package/dist/esm/errors.d.ts +2 -0
- package/dist/esm/errors.js +4 -0
- package/dist/esm/handler/imap-parser.js +3 -0
- package/dist/esm/handler/parser-instance.d.ts +2 -0
- package/dist/esm/handler/parser-instance.js +3 -1
- package/dist/esm/handler/token-parser.d.ts +21 -2
- package/dist/esm/handler/token-parser.js +76 -6
- package/dist/esm/handler/types.d.ts +5 -0
- package/dist/esm/imap-flow.d.ts +9 -9
- package/dist/esm/imap-flow.js +88 -14
- 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 +30 -0
- package/dist/esm/tools.js +126 -51
- package/dist/esm/types.d.ts +78 -2
- 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,
|
|
18
|
+
import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, mergeFetchRows, packMessageRange, normalizePath, iterateRange, countRange, 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
|
}
|
|
@@ -845,6 +846,16 @@ export class ImapFlow extends EventEmitter {
|
|
|
845
846
|
}
|
|
846
847
|
return true;
|
|
847
848
|
}
|
|
849
|
+
if (parsed.truncatedAttributes) {
|
|
850
|
+
// The row is kept with the item missing rather than dropped with the whole line: the
|
|
851
|
+
// nesting is the message sender's doing, and the message must still reach the caller
|
|
852
|
+
this.log.warn({
|
|
853
|
+
msg: 'FETCH data item exceeded the nesting limit and was dropped',
|
|
854
|
+
seq: parsed.command,
|
|
855
|
+
attributes: parsed.truncatedAttributes,
|
|
856
|
+
cid: this.id
|
|
857
|
+
});
|
|
858
|
+
}
|
|
848
859
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
849
860
|
let payload = { response: parsed.command };
|
|
850
861
|
if (parsed.attributes &&
|
|
@@ -1008,11 +1019,24 @@ export class ImapFlow extends EventEmitter {
|
|
|
1008
1019
|
async settleRequest(request, parsed, hasTrailingData) {
|
|
1009
1020
|
switch ((parsed.command || '').toUpperCase()) {
|
|
1010
1021
|
case 'OK':
|
|
1011
|
-
case 'BYE':
|
|
1012
1022
|
// hasTrailingData is forwarded so STARTTLS can detect a plaintext
|
|
1013
1023
|
// injection (data buffered after the tagged OK, before the handshake).
|
|
1014
1024
|
await this.resolveParked(request, parsed, hasTrailingData);
|
|
1015
1025
|
break;
|
|
1026
|
+
case 'BYE': {
|
|
1027
|
+
// BYE is always untagged (RFC 9051 section 7.1.5), it is never the completion
|
|
1028
|
+
// result of a command. It used to resolve the command like an OK, so a server (or
|
|
1029
|
+
// a cleartext MITM) answering LOGIN with a tagged BYE marked the session
|
|
1030
|
+
// authenticated. The server says it is closing: the command fails as a lost
|
|
1031
|
+
// connection and the connection is closed, which for a LOGOUT is the outcome it
|
|
1032
|
+
// was after anyway.
|
|
1033
|
+
await this.serverBye(parsed);
|
|
1034
|
+
let err = this.createNoConnectionError(this.byeReason, { rejectedFrom: 'taggedBye', command: request.command });
|
|
1035
|
+
err.response = parsed;
|
|
1036
|
+
err.responseStatus = 'BYE';
|
|
1037
|
+
request.reject(err);
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1016
1040
|
case 'NO':
|
|
1017
1041
|
case 'BAD': {
|
|
1018
1042
|
let txt = getTextValues(parsed.attributes)
|
|
@@ -1144,6 +1168,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1144
1168
|
if (typeof socket.setTimeout === 'function') {
|
|
1145
1169
|
socket.setTimeout(this.socketTimeout);
|
|
1146
1170
|
}
|
|
1171
|
+
else {
|
|
1172
|
+
// a transport socket from plain JavaScript can lack it, which disables the watchdog
|
|
1173
|
+
this.log.warn({ msg: 'The socket has no setTimeout(), socketTimeout is not enforced', cid: this.id });
|
|
1174
|
+
}
|
|
1147
1175
|
}
|
|
1148
1176
|
/** @internal */
|
|
1149
1177
|
setSocketHandlers() {
|
|
@@ -1329,6 +1357,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
1329
1357
|
}
|
|
1330
1358
|
/** @internal */
|
|
1331
1359
|
async compress() {
|
|
1360
|
+
if (typeof zlib.createDeflateRaw !== 'function') {
|
|
1361
|
+
// The runtime has no streaming DEFLATE (celld implements only the synchronous zlib
|
|
1362
|
+
// functions), so the session stays uncompressed rather than failing after COMPRESS
|
|
1363
|
+
this.log.debug({ msg: 'Streaming DEFLATE is not available, skipping COMPRESS', cid: this.id });
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1332
1366
|
if (!(await this.run('COMPRESS'))) {
|
|
1333
1367
|
return; // was not able to negotiate compression
|
|
1334
1368
|
}
|
|
@@ -1449,10 +1483,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1449
1483
|
});
|
|
1450
1484
|
}
|
|
1451
1485
|
/** @internal */
|
|
1452
|
-
_failSTARTTLS() {
|
|
1486
|
+
_failSTARTTLS(reason = 'Server does not support STARTTLS') {
|
|
1453
1487
|
if (this.options.doSTARTTLS === true) {
|
|
1454
1488
|
// STARTTLS configured as requirement
|
|
1455
|
-
let err = new Error(
|
|
1489
|
+
let err = new Error(reason);
|
|
1456
1490
|
err.tlsFailed = true;
|
|
1457
1491
|
throw err;
|
|
1458
1492
|
}
|
|
@@ -1460,7 +1494,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1460
1494
|
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1461
1495
|
// option for the policy).
|
|
1462
1496
|
this.log.warn({
|
|
1463
|
-
msg:
|
|
1497
|
+
msg: reason + ', continuing over an unencrypted connection',
|
|
1464
1498
|
host: this.host,
|
|
1465
1499
|
port: this.port,
|
|
1466
1500
|
cid: this.id
|
|
@@ -1484,10 +1518,23 @@ export class ImapFlow extends EventEmitter {
|
|
|
1484
1518
|
// STARTTLS explictly disabled by config
|
|
1485
1519
|
return false;
|
|
1486
1520
|
}
|
|
1521
|
+
if (this.state !== this.states.NOT_AUTHENTICATED) {
|
|
1522
|
+
// STARTTLS is only valid in the not-authenticated state (RFC 9051 section 6.2.1),
|
|
1523
|
+
// which a PREAUTH greeting skips, so there is no way left to protect this session.
|
|
1524
|
+
// Required STARTTLS fails here with that reason: the greeting arrived in cleartext,
|
|
1525
|
+
// and a MITM can answer with PREAUTH precisely to keep it that way.
|
|
1526
|
+
return this._failSTARTTLS('STARTTLS is not possible after a PREAUTH greeting');
|
|
1527
|
+
}
|
|
1487
1528
|
if (!this.capabilities.has('STARTTLS')) {
|
|
1488
1529
|
return this._failSTARTTLS();
|
|
1489
1530
|
}
|
|
1531
|
+
if (this.options.transport && !this.options.transport.upgrade) {
|
|
1532
|
+
return this._failSTARTTLS('The transport can not start TLS');
|
|
1533
|
+
}
|
|
1490
1534
|
this.expectCapabilityUpdate = true;
|
|
1535
|
+
if (this.options.transport?.prepareUpgrade && this.socket) {
|
|
1536
|
+
this.options.transport.prepareUpgrade(this.socket);
|
|
1537
|
+
}
|
|
1491
1538
|
let canUpgrade = await this.run('STARTTLS');
|
|
1492
1539
|
if (!canUpgrade) {
|
|
1493
1540
|
return this._failSTARTTLS();
|
|
@@ -1589,7 +1636,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1589
1636
|
this.upgrading = true;
|
|
1590
1637
|
let tlsSocket;
|
|
1591
1638
|
try {
|
|
1592
|
-
|
|
1639
|
+
let onSecure = () => {
|
|
1593
1640
|
try {
|
|
1594
1641
|
/* c8 ignore start */ // race: connection closed during the TLS handshake window
|
|
1595
1642
|
if (this.isClosed) {
|
|
@@ -1641,7 +1688,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
1641
1688
|
catch (ex) {
|
|
1642
1689
|
this.emitError(ex);
|
|
1643
1690
|
}
|
|
1644
|
-
}
|
|
1691
|
+
};
|
|
1692
|
+
// a transport without upgrade() never gets here, see the STARTTLS check above
|
|
1693
|
+
tlsSocket = this.options.transport
|
|
1694
|
+
? this.options.transport.upgrade(socketPlain, this.transportOptions(), onSecure)
|
|
1695
|
+
: tls.connect(opts, onSecure);
|
|
1645
1696
|
}
|
|
1646
1697
|
catch (err) {
|
|
1647
1698
|
// tls.connect() refused the upgrade before any handshake (an option the runtime
|
|
@@ -1918,7 +1969,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1918
1969
|
let tags = [];
|
|
1919
1970
|
let uids = false;
|
|
1920
1971
|
// A malformed VANISHED can carry no attributes at all, and one carrying only the
|
|
1921
|
-
// (EARLIER) tag leaves `uids` false -
|
|
1972
|
+
// (EARLIER) tag leaves `uids` false - iterateRange() handles that and yields nothing
|
|
1922
1973
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
1923
1974
|
return;
|
|
1924
1975
|
}
|
|
@@ -1929,14 +1980,16 @@ export class ImapFlow extends EventEmitter {
|
|
|
1929
1980
|
if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
|
|
1930
1981
|
uids = untagged.attributes[0].value;
|
|
1931
1982
|
}
|
|
1932
|
-
let uidList = expandRange(uids);
|
|
1933
1983
|
let earlier = tags.includes('EARLIER');
|
|
1934
1984
|
// RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
|
|
1935
1985
|
// client knows about and decrements the message count like the same number of EXPUNGEs would
|
|
1936
1986
|
if (!earlier) {
|
|
1937
|
-
mailbox.exists = Math.max(0, mailbox.exists -
|
|
1987
|
+
mailbox.exists = Math.max(0, mailbox.exists - countRange(uids));
|
|
1938
1988
|
}
|
|
1939
|
-
|
|
1989
|
+
// The set is walked one UID at a time rather than expanded first: it is the server's to
|
|
1990
|
+
// choose, and a single "1:16777216" is a few bytes on the wire but sixteen million array
|
|
1991
|
+
// entries expanded, for every such line the server cares to send
|
|
1992
|
+
for (let uid of iterateRange(uids)) {
|
|
1940
1993
|
let payload = {
|
|
1941
1994
|
path: mailbox.path,
|
|
1942
1995
|
uid,
|
|
@@ -1988,6 +2041,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1988
2041
|
tlsServername() {
|
|
1989
2042
|
return this.servername ? { servername: this.servername } : {};
|
|
1990
2043
|
}
|
|
2044
|
+
/** @internal */
|
|
2045
|
+
transportOptions() {
|
|
2046
|
+
return Object.assign({ host: this.host, port: this.port, tls: this.options.tls }, this.tlsServername());
|
|
2047
|
+
}
|
|
1991
2048
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1992
2049
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1993
2050
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -2114,6 +2171,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
2114
2171
|
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
2115
2172
|
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
2116
2173
|
this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
|
|
2174
|
+
let transport = this.options.transport;
|
|
2175
|
+
if (transport && this.options.proxy) {
|
|
2176
|
+
throw createImapError('The proxy option can not be combined with a custom transport', 'UnsupportedTransportOption');
|
|
2177
|
+
}
|
|
2117
2178
|
let socket = false;
|
|
2118
2179
|
if (this.options.proxy) {
|
|
2119
2180
|
try {
|
|
@@ -2230,6 +2291,19 @@ export class ImapFlow extends EventEmitter {
|
|
|
2230
2291
|
setImmediate(onConnect);
|
|
2231
2292
|
}
|
|
2232
2293
|
}
|
|
2294
|
+
else if (transport) {
|
|
2295
|
+
try {
|
|
2296
|
+
this.socket = transport.connect({ ...this.transportOptions(), secure: this.secureConnection }, onConnect);
|
|
2297
|
+
}
|
|
2298
|
+
catch (err) {
|
|
2299
|
+
// refused up front: the connection deadline must not stay armed
|
|
2300
|
+
clearTimer(this.connectTimeout);
|
|
2301
|
+
this.closeAfter();
|
|
2302
|
+
this.log.error({ err, cid: this.id });
|
|
2303
|
+
reject(err);
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2233
2307
|
else {
|
|
2234
2308
|
this.socket = connector.connect(opts, onConnect);
|
|
2235
2309
|
}
|
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;
|