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/cjs/imap-flow.js
CHANGED
|
@@ -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 : !
|
|
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' +
|
|
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
|
}
|
|
@@ -886,6 +889,16 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
886
889
|
}
|
|
887
890
|
return true;
|
|
888
891
|
}
|
|
892
|
+
if (parsed.truncatedAttributes) {
|
|
893
|
+
// The row is kept with the item missing rather than dropped with the whole line: the
|
|
894
|
+
// nesting is the message sender's doing, and the message must still reach the caller
|
|
895
|
+
this.log.warn({
|
|
896
|
+
msg: 'FETCH data item exceeded the nesting limit and was dropped',
|
|
897
|
+
seq: parsed.command,
|
|
898
|
+
attributes: parsed.truncatedAttributes,
|
|
899
|
+
cid: this.id
|
|
900
|
+
});
|
|
901
|
+
}
|
|
889
902
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
890
903
|
let payload = { response: parsed.command };
|
|
891
904
|
if (parsed.attributes &&
|
|
@@ -1049,11 +1062,24 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1049
1062
|
async settleRequest(request, parsed, hasTrailingData) {
|
|
1050
1063
|
switch ((parsed.command || '').toUpperCase()) {
|
|
1051
1064
|
case 'OK':
|
|
1052
|
-
case 'BYE':
|
|
1053
1065
|
// hasTrailingData is forwarded so STARTTLS can detect a plaintext
|
|
1054
1066
|
// injection (data buffered after the tagged OK, before the handshake).
|
|
1055
1067
|
await this.resolveParked(request, parsed, hasTrailingData);
|
|
1056
1068
|
break;
|
|
1069
|
+
case 'BYE': {
|
|
1070
|
+
// BYE is always untagged (RFC 9051 section 7.1.5), it is never the completion
|
|
1071
|
+
// result of a command. It used to resolve the command like an OK, so a server (or
|
|
1072
|
+
// a cleartext MITM) answering LOGIN with a tagged BYE marked the session
|
|
1073
|
+
// authenticated. The server says it is closing: the command fails as a lost
|
|
1074
|
+
// connection and the connection is closed, which for a LOGOUT is the outcome it
|
|
1075
|
+
// was after anyway.
|
|
1076
|
+
await this.serverBye(parsed);
|
|
1077
|
+
let err = this.createNoConnectionError(this.byeReason, { rejectedFrom: 'taggedBye', command: request.command });
|
|
1078
|
+
err.response = parsed;
|
|
1079
|
+
err.responseStatus = 'BYE';
|
|
1080
|
+
request.reject(err);
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1057
1083
|
case 'NO':
|
|
1058
1084
|
case 'BAD': {
|
|
1059
1085
|
let txt = (0, tools_js_1.getTextValues)(parsed.attributes)
|
|
@@ -1185,6 +1211,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1185
1211
|
if (typeof socket.setTimeout === 'function') {
|
|
1186
1212
|
socket.setTimeout(this.socketTimeout);
|
|
1187
1213
|
}
|
|
1214
|
+
else {
|
|
1215
|
+
// a transport socket from plain JavaScript can lack it, which disables the watchdog
|
|
1216
|
+
this.log.warn({ msg: 'The socket has no setTimeout(), socketTimeout is not enforced', cid: this.id });
|
|
1217
|
+
}
|
|
1188
1218
|
}
|
|
1189
1219
|
/** @internal */
|
|
1190
1220
|
setSocketHandlers() {
|
|
@@ -1370,6 +1400,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1370
1400
|
}
|
|
1371
1401
|
/** @internal */
|
|
1372
1402
|
async compress() {
|
|
1403
|
+
if (typeof node_zlib_1.default.createDeflateRaw !== 'function') {
|
|
1404
|
+
// The runtime has no streaming DEFLATE (celld implements only the synchronous zlib
|
|
1405
|
+
// functions), so the session stays uncompressed rather than failing after COMPRESS
|
|
1406
|
+
this.log.debug({ msg: 'Streaming DEFLATE is not available, skipping COMPRESS', cid: this.id });
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1373
1409
|
if (!(await this.run('COMPRESS'))) {
|
|
1374
1410
|
return; // was not able to negotiate compression
|
|
1375
1411
|
}
|
|
@@ -1490,10 +1526,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1490
1526
|
});
|
|
1491
1527
|
}
|
|
1492
1528
|
/** @internal */
|
|
1493
|
-
_failSTARTTLS() {
|
|
1529
|
+
_failSTARTTLS(reason = 'Server does not support STARTTLS') {
|
|
1494
1530
|
if (this.options.doSTARTTLS === true) {
|
|
1495
1531
|
// STARTTLS configured as requirement
|
|
1496
|
-
let err = new Error(
|
|
1532
|
+
let err = new Error(reason);
|
|
1497
1533
|
err.tlsFailed = true;
|
|
1498
1534
|
throw err;
|
|
1499
1535
|
}
|
|
@@ -1501,7 +1537,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1501
1537
|
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1502
1538
|
// option for the policy).
|
|
1503
1539
|
this.log.warn({
|
|
1504
|
-
msg:
|
|
1540
|
+
msg: reason + ', continuing over an unencrypted connection',
|
|
1505
1541
|
host: this.host,
|
|
1506
1542
|
port: this.port,
|
|
1507
1543
|
cid: this.id
|
|
@@ -1525,10 +1561,23 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1525
1561
|
// STARTTLS explictly disabled by config
|
|
1526
1562
|
return false;
|
|
1527
1563
|
}
|
|
1564
|
+
if (this.state !== this.states.NOT_AUTHENTICATED) {
|
|
1565
|
+
// STARTTLS is only valid in the not-authenticated state (RFC 9051 section 6.2.1),
|
|
1566
|
+
// which a PREAUTH greeting skips, so there is no way left to protect this session.
|
|
1567
|
+
// Required STARTTLS fails here with that reason: the greeting arrived in cleartext,
|
|
1568
|
+
// and a MITM can answer with PREAUTH precisely to keep it that way.
|
|
1569
|
+
return this._failSTARTTLS('STARTTLS is not possible after a PREAUTH greeting');
|
|
1570
|
+
}
|
|
1528
1571
|
if (!this.capabilities.has('STARTTLS')) {
|
|
1529
1572
|
return this._failSTARTTLS();
|
|
1530
1573
|
}
|
|
1574
|
+
if (this.options.transport && !this.options.transport.upgrade) {
|
|
1575
|
+
return this._failSTARTTLS('The transport can not start TLS');
|
|
1576
|
+
}
|
|
1531
1577
|
this.expectCapabilityUpdate = true;
|
|
1578
|
+
if (this.options.transport?.prepareUpgrade && this.socket) {
|
|
1579
|
+
this.options.transport.prepareUpgrade(this.socket);
|
|
1580
|
+
}
|
|
1532
1581
|
let canUpgrade = await this.run('STARTTLS');
|
|
1533
1582
|
if (!canUpgrade) {
|
|
1534
1583
|
return this._failSTARTTLS();
|
|
@@ -1630,7 +1679,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1630
1679
|
this.upgrading = true;
|
|
1631
1680
|
let tlsSocket;
|
|
1632
1681
|
try {
|
|
1633
|
-
|
|
1682
|
+
let onSecure = () => {
|
|
1634
1683
|
try {
|
|
1635
1684
|
/* c8 ignore start */ // race: connection closed during the TLS handshake window
|
|
1636
1685
|
if (this.isClosed) {
|
|
@@ -1682,7 +1731,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1682
1731
|
catch (ex) {
|
|
1683
1732
|
this.emitError(ex);
|
|
1684
1733
|
}
|
|
1685
|
-
}
|
|
1734
|
+
};
|
|
1735
|
+
// a transport without upgrade() never gets here, see the STARTTLS check above
|
|
1736
|
+
tlsSocket = this.options.transport
|
|
1737
|
+
? this.options.transport.upgrade(socketPlain, this.transportOptions(), onSecure)
|
|
1738
|
+
: node_tls_1.default.connect(opts, onSecure);
|
|
1686
1739
|
}
|
|
1687
1740
|
catch (err) {
|
|
1688
1741
|
// tls.connect() refused the upgrade before any handshake (an option the runtime
|
|
@@ -1959,7 +2012,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1959
2012
|
let tags = [];
|
|
1960
2013
|
let uids = false;
|
|
1961
2014
|
// A malformed VANISHED can carry no attributes at all, and one carrying only the
|
|
1962
|
-
// (EARLIER) tag leaves `uids` false -
|
|
2015
|
+
// (EARLIER) tag leaves `uids` false - iterateRange() handles that and yields nothing
|
|
1963
2016
|
if (!untagged.attributes || !untagged.attributes.length) {
|
|
1964
2017
|
return;
|
|
1965
2018
|
}
|
|
@@ -1970,14 +2023,16 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1970
2023
|
if (untagged.attributes[0] && typeof untagged.attributes[0].value === 'string') {
|
|
1971
2024
|
uids = untagged.attributes[0].value;
|
|
1972
2025
|
}
|
|
1973
|
-
let uidList = (0, tools_js_1.expandRange)(uids);
|
|
1974
2026
|
let earlier = tags.includes('EARLIER');
|
|
1975
2027
|
// RFC 7162 section 3.2.10: unlike VANISHED (EARLIER), a plain VANISHED reports messages the
|
|
1976
2028
|
// client knows about and decrements the message count like the same number of EXPUNGEs would
|
|
1977
2029
|
if (!earlier) {
|
|
1978
|
-
mailbox.exists = Math.max(0, mailbox.exists -
|
|
2030
|
+
mailbox.exists = Math.max(0, mailbox.exists - (0, tools_js_1.countRange)(uids));
|
|
1979
2031
|
}
|
|
1980
|
-
|
|
2032
|
+
// The set is walked one UID at a time rather than expanded first: it is the server's to
|
|
2033
|
+
// choose, and a single "1:16777216" is a few bytes on the wire but sixteen million array
|
|
2034
|
+
// entries expanded, for every such line the server cares to send
|
|
2035
|
+
for (let uid of (0, tools_js_1.iterateRange)(uids)) {
|
|
1981
2036
|
let payload = {
|
|
1982
2037
|
path: mailbox.path,
|
|
1983
2038
|
uid,
|
|
@@ -2029,6 +2084,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2029
2084
|
tlsServername() {
|
|
2030
2085
|
return this.servername ? { servername: this.servername } : {};
|
|
2031
2086
|
}
|
|
2087
|
+
/** @internal */
|
|
2088
|
+
transportOptions() {
|
|
2089
|
+
return Object.assign({ host: this.host, port: this.port, tls: this.options.tls }, this.tlsServername());
|
|
2090
|
+
}
|
|
2032
2091
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
2033
2092
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
2034
2093
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -2155,6 +2214,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2155
2214
|
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
2156
2215
|
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
2157
2216
|
this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
|
|
2217
|
+
let transport = this.options.transport;
|
|
2218
|
+
if (transport && this.options.proxy) {
|
|
2219
|
+
throw (0, errors_js_1.createImapError)('The proxy option can not be combined with a custom transport', 'UnsupportedTransportOption');
|
|
2220
|
+
}
|
|
2158
2221
|
let socket = false;
|
|
2159
2222
|
if (this.options.proxy) {
|
|
2160
2223
|
try {
|
|
@@ -2271,6 +2334,19 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2271
2334
|
setImmediate(onConnect);
|
|
2272
2335
|
}
|
|
2273
2336
|
}
|
|
2337
|
+
else if (transport) {
|
|
2338
|
+
try {
|
|
2339
|
+
this.socket = transport.connect({ ...this.transportOptions(), secure: this.secureConnection }, onConnect);
|
|
2340
|
+
}
|
|
2341
|
+
catch (err) {
|
|
2342
|
+
// refused up front: the connection deadline must not stay armed
|
|
2343
|
+
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2344
|
+
this.closeAfter();
|
|
2345
|
+
this.log.error({ err, cid: this.id });
|
|
2346
|
+
reject(err);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2274
2350
|
else {
|
|
2275
2351
|
this.socket = connector.connect(opts, onConnect);
|
|
2276
2352
|
}
|
package/dist/cjs/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,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;
|