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 +8 -0
- package/README.md +59 -1
- package/dist/cjs/errors.d.ts +1 -0
- package/dist/cjs/errors.js +2 -0
- package/dist/cjs/imap-flow.d.ts +9 -9
- package/dist/cjs/imap-flow.js +53 -9
- 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 +1 -0
- package/dist/esm/errors.js +2 -0
- package/dist/esm/imap-flow.d.ts +9 -9
- package/dist/esm/imap-flow.js +51 -9
- 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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The `transport` option over a socket API built on web streams: the socket opens with a promise
|
|
3
|
+
// that resolves to a ReadableStream and a WritableStream of bytes. `cloudflare:sockets` (celld,
|
|
4
|
+
// Cloudflare Workers) is one, see sockets-transport.ts; Chrome's Direct Sockets and in-memory
|
|
5
|
+
// connections fit the same shape. The streams are wrapped into a Duplex that behaves like the
|
|
6
|
+
// net.Socket ImapFlow drives.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createStreamsTransport = createStreamsTransport;
|
|
9
|
+
const node_stream_1 = require("node:stream");
|
|
10
|
+
const tools_js_1 = require("./tools.js");
|
|
11
|
+
function closeQuietly(socket) {
|
|
12
|
+
try {
|
|
13
|
+
Promise.resolve(socket.close()).catch(tools_js_1.noop);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// already closed
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
class StreamsDuplex extends node_stream_1.Duplex {
|
|
20
|
+
constructor(api, socket, onConnect, plain) {
|
|
21
|
+
super({ allowHalfOpen: false });
|
|
22
|
+
// Taken once the socket is open: locking the streams of a socket returned by startTls()
|
|
23
|
+
// before its handshake completes stalls the handshake on celld
|
|
24
|
+
this._reader = null;
|
|
25
|
+
this._writer = null;
|
|
26
|
+
this._pumping = false;
|
|
27
|
+
this._readRequested = false;
|
|
28
|
+
// A runtime may only start TLS on a socket without a read in flight (with one, celld never
|
|
29
|
+
// completes the handshake), while the read loop always keeps one. So once the client
|
|
30
|
+
// announced a STARTTLS (holdAfterStarttls), reading stops after the tagged line that answers
|
|
31
|
+
// it, the only command in flight then, and resumes only when that line is not an OK.
|
|
32
|
+
this._awaitingStarttls = false;
|
|
33
|
+
this._lineTail = '';
|
|
34
|
+
// Set when the partial line got too long to be the one looked for and was dropped: the rest
|
|
35
|
+
// of it, up to the next line break, is skipped too
|
|
36
|
+
this._tailDropped = false;
|
|
37
|
+
this._readsHeld = false;
|
|
38
|
+
// Set once startTls() handed the connection to a TLS socket: the read that ends then is not
|
|
39
|
+
// the end of the connection, and closing this socket would close the TLS one
|
|
40
|
+
this._upgraded = false;
|
|
41
|
+
this._timeout = 0;
|
|
42
|
+
this._timer = null;
|
|
43
|
+
// Traffic only records the time, the one timer re-arms itself for the rest of the idle period
|
|
44
|
+
this._lastActivity = 0;
|
|
45
|
+
this._api = api;
|
|
46
|
+
this._socket = socket;
|
|
47
|
+
this._plain = plain || null;
|
|
48
|
+
socket.opened.then(info => {
|
|
49
|
+
if (this.destroyed) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
this._reader = info.readable.getReader();
|
|
54
|
+
this._writer = info.writable.getWriter();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// streams already locked by someone else
|
|
58
|
+
this.destroy(err);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.remoteAddress = info.remoteAddress;
|
|
62
|
+
this.remotePort = info.remotePort;
|
|
63
|
+
this.localAddress = info.localAddress;
|
|
64
|
+
this.localPort = info.localPort;
|
|
65
|
+
const tls = info.tls;
|
|
66
|
+
if (tls) {
|
|
67
|
+
this.authorized = tls.authorized;
|
|
68
|
+
this.getCipher = () => ({ name: tls.name || '', standardName: tls.standardName || tls.name || '', version: tls.version || '' });
|
|
69
|
+
}
|
|
70
|
+
this._touch();
|
|
71
|
+
onConnect();
|
|
72
|
+
if (this._readRequested) {
|
|
73
|
+
void this._pump();
|
|
74
|
+
}
|
|
75
|
+
}, (err) => this.destroy(err));
|
|
76
|
+
socket.closed?.catch((err) => {
|
|
77
|
+
if (!this._upgraded) {
|
|
78
|
+
this.destroy(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
_read() {
|
|
83
|
+
if (this._readsHeld) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!this._reader) {
|
|
87
|
+
this._readRequested = true;
|
|
88
|
+
}
|
|
89
|
+
else if (!this._pumping) {
|
|
90
|
+
void this._pump();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async _pump() {
|
|
94
|
+
const reader = this._reader;
|
|
95
|
+
this._pumping = true;
|
|
96
|
+
try {
|
|
97
|
+
for (;;) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (this._upgraded || this.destroyed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (done || !value) {
|
|
103
|
+
this.push(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this._touch();
|
|
107
|
+
const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
108
|
+
const pushed = this.push(chunk);
|
|
109
|
+
if (this._awaitingStarttls && this._starttlsCompleted(chunk)) {
|
|
110
|
+
this._readsHeld = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!pushed) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (!this._upgraded) {
|
|
120
|
+
this.destroy(err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this._pumping = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Whether the chunk holds the tagged response of the STARTTLS command and it is an OK. Lines
|
|
128
|
+
// end with LF, CR is optional, as for the parser
|
|
129
|
+
_starttlsCompleted(chunk) {
|
|
130
|
+
const skipFirst = this._tailDropped;
|
|
131
|
+
const lines = (this._lineTail + chunk.toString('latin1')).split('\n');
|
|
132
|
+
const tail = lines.pop();
|
|
133
|
+
if (skipFirst && !lines.length) {
|
|
134
|
+
// still inside the dropped line
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this._tailDropped = tail.length > 1024;
|
|
138
|
+
this._lineTail = this._tailDropped ? '' : tail;
|
|
139
|
+
for (let i = skipFirst ? 1 : 0; i < lines.length; i++) {
|
|
140
|
+
const tagged = /^(?![*+] )\S+ (OK|NO|BAD)\b/i.exec(lines[i]);
|
|
141
|
+
if (tagged) {
|
|
142
|
+
this._awaitingStarttls = false;
|
|
143
|
+
return tagged[1].toUpperCase() === 'OK';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
/** Stops reading after the tagged response of the STARTTLS command about to be sent, if it is an OK */
|
|
149
|
+
holdAfterStarttls() {
|
|
150
|
+
this._awaitingStarttls = true;
|
|
151
|
+
this._lineTail = '';
|
|
152
|
+
this._tailDropped = false;
|
|
153
|
+
}
|
|
154
|
+
_write(chunk, _encoding, callback) {
|
|
155
|
+
if (!this._writer) {
|
|
156
|
+
// ImapFlow writes only after onConnect, this is a misuse
|
|
157
|
+
callback(new Error('Socket is not open yet'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this._touch();
|
|
161
|
+
this._writer.write(chunk).then(() => callback(), callback);
|
|
162
|
+
}
|
|
163
|
+
_destroy(err, callback) {
|
|
164
|
+
this._clearTimer();
|
|
165
|
+
if (!this._upgraded) {
|
|
166
|
+
this._close();
|
|
167
|
+
}
|
|
168
|
+
if (this._plain) {
|
|
169
|
+
// the plain connection went with the TLS one, unless the API keeps it open
|
|
170
|
+
this._plain._close();
|
|
171
|
+
this._plain.destroy();
|
|
172
|
+
}
|
|
173
|
+
callback(err);
|
|
174
|
+
}
|
|
175
|
+
// Closes the connection. An API that refuses to close while its streams are locked (Direct
|
|
176
|
+
// Sockets) gets a second attempt with the locks released; releasing them right away would
|
|
177
|
+
// fail the read in flight, which ends the request on celld
|
|
178
|
+
_close() {
|
|
179
|
+
const retry = () => {
|
|
180
|
+
for (const stream of [this._reader, this._writer]) {
|
|
181
|
+
try {
|
|
182
|
+
stream?.releaseLock();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// an older streams implementation refuses with a read in flight
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
closeQuietly(this._socket);
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
const closing = this._socket.close();
|
|
192
|
+
if (typeof closing?.then === 'function') {
|
|
193
|
+
closing.then(undefined, retry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
retry();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Like net.Socket#setTimeout(): emits 'timeout' after `ms` without traffic, 0 disables */
|
|
201
|
+
setTimeout(ms) {
|
|
202
|
+
this._timeout = ms;
|
|
203
|
+
this._clearTimer();
|
|
204
|
+
this._touch();
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
setKeepAlive() {
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
/** Hands the connection over to a TLS socket, this one only stays as its transport */
|
|
211
|
+
startTls(options, onSecure) {
|
|
212
|
+
this._upgraded = true;
|
|
213
|
+
this._clearTimer();
|
|
214
|
+
// workerd refuses to start TLS while the streams are locked, nothing is in flight on them
|
|
215
|
+
this._reader?.releaseLock();
|
|
216
|
+
this._writer?.releaseLock();
|
|
217
|
+
let tlsSocket;
|
|
218
|
+
try {
|
|
219
|
+
tlsSocket = this._api.upgrade(this._socket, options);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
// refused before any handshake, the connection is closed with this socket
|
|
223
|
+
this._upgraded = false;
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
return new StreamsDuplex(this._api, tlsSocket, onSecure, this);
|
|
227
|
+
}
|
|
228
|
+
_touch() {
|
|
229
|
+
this._lastActivity = Date.now();
|
|
230
|
+
if (!this._timer) {
|
|
231
|
+
this._arm(this._timeout);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
_arm(ms) {
|
|
235
|
+
if (this._timeout > 0 && !this.destroyed && !this._upgraded) {
|
|
236
|
+
this._timer = (0, tools_js_1.unrefTimer)(setTimeout(() => {
|
|
237
|
+
this._timer = null;
|
|
238
|
+
const idle = Date.now() - this._lastActivity;
|
|
239
|
+
if (idle >= this._timeout) {
|
|
240
|
+
this.emit('timeout');
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this._arm(this._timeout - idle);
|
|
244
|
+
}
|
|
245
|
+
}, ms));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
_clearTimer() {
|
|
249
|
+
(0, tools_js_1.clearTimer)(this._timer);
|
|
250
|
+
this._timer = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Builds a transport for the `transport` option over a socket API built on web streams. A new
|
|
255
|
+
* API needs only `open()`, and `upgrade()` if it can start TLS on an open connection:
|
|
256
|
+
*
|
|
257
|
+
* ```js
|
|
258
|
+
* const transport = createStreamsTransport({
|
|
259
|
+
* open: ({ host, port, secure }) => {
|
|
260
|
+
* const socket = openSomeSocket(host, port, secure);
|
|
261
|
+
* return { opened: socket.ready.then(() => ({ readable: socket.readable, writable: socket.writable })), close: () => socket.close() };
|
|
262
|
+
* }
|
|
263
|
+
* });
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
266
|
+
* The transport stops reading once the server accepted a STARTTLS, so the API never has to start
|
|
267
|
+
* TLS with a read in flight, and releases the stream locks before `upgrade()` is called.
|
|
268
|
+
*
|
|
269
|
+
* @param api - Opens (and upgrades) connections
|
|
270
|
+
* @returns A transport for the `transport` option
|
|
271
|
+
*/
|
|
272
|
+
function createStreamsTransport(api) {
|
|
273
|
+
const transport = {
|
|
274
|
+
connect: (options, onConnect) => new StreamsDuplex(api, api.open(options), onConnect)
|
|
275
|
+
};
|
|
276
|
+
if (api.upgrade) {
|
|
277
|
+
transport.prepareUpgrade = socket => socket.holdAfterStarttls();
|
|
278
|
+
transport.upgrade = (socket, options, onSecure) => socket.startTls(options, onSecure);
|
|
279
|
+
}
|
|
280
|
+
return transport;
|
|
281
|
+
}
|
package/dist/cjs/tools.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ export { AuthenticationFailure } from './errors.js';
|
|
|
7
7
|
export declare const EXPANDED_RANGE_LIMIT = 16777216;
|
|
8
8
|
export declare const MAX_UINT32_DIGITS = 10;
|
|
9
9
|
export declare const noop: () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
|
|
12
|
+
* Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
|
|
13
|
+
* colon (an IPv6 literal, a hostname never contains one).
|
|
14
|
+
*
|
|
15
|
+
* @param host - Hostname or IP address
|
|
16
|
+
* @returns true for an IP literal
|
|
17
|
+
*/
|
|
18
|
+
export declare function isIpLiteral(host: string): boolean;
|
|
10
19
|
/**
|
|
11
20
|
* A stream decoder returned by getDecoder(). The Japanese decoder reports the `limited`
|
|
12
21
|
* flag once it has buffered all it will accept; a streaming iconv decoder never sets it.
|
package/dist/cjs/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.noop = exports.MAX_UINT32_DIGITS = exports.EXPANDED_RANGE_LIMIT = exports.AuthenticationFailure = void 0;
|
|
8
|
+
exports.isIpLiteral = isIpLiteral;
|
|
8
9
|
exports.buildConnectionError = buildConnectionError;
|
|
9
10
|
exports.restampConnectionError = restampConnectionError;
|
|
10
11
|
exports.guardedPromise = guardedPromise;
|
|
@@ -122,6 +123,17 @@ const IMAP4REV2_FOLDED_CAPABILITIES = new Set([
|
|
|
122
123
|
// Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
|
|
123
124
|
const noop = () => { };
|
|
124
125
|
exports.noop = noop;
|
|
126
|
+
/**
|
|
127
|
+
* Tells an IP literal from a hostname without `node:net`, which some runtimes do not implement.
|
|
128
|
+
* Only used to decide whether a host can be a TLS servername: a dotted quad, or anything with a
|
|
129
|
+
* colon (an IPv6 literal, a hostname never contains one).
|
|
130
|
+
*
|
|
131
|
+
* @param host - Hostname or IP address
|
|
132
|
+
* @returns true for an IP literal
|
|
133
|
+
*/
|
|
134
|
+
function isIpLiteral(host) {
|
|
135
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(':');
|
|
136
|
+
}
|
|
125
137
|
// The fields buildConnectionError() stamps to say *where* a connection error was rejected, as
|
|
126
138
|
// opposed to what went wrong. restampConnectionError() clears them, because they belong to the
|
|
127
139
|
// site that built the error rather than to the failure it describes.
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectionOptions, CipherNameAndProtocol } from 'node:tls';
|
|
2
|
-
import type { Readable } from 'node:stream';
|
|
2
|
+
import type { Duplex, Readable } from 'node:stream';
|
|
3
3
|
/**
|
|
4
4
|
* Logger interface. Any object with `debug`, `info`, `warn` and `error` methods can be used,
|
|
5
5
|
* for example a pino or bunyan instance. `trace` and `fatal` are used when present.
|
|
@@ -95,6 +95,14 @@ export interface ImapFlowOptions {
|
|
|
95
95
|
* Node as-is. Proxy DNS and negotiation run inside `connectionTimeout`.
|
|
96
96
|
*/
|
|
97
97
|
proxy?: string | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Opens the connection through this transport instead of `node:net` and `node:tls`, for
|
|
100
|
+
* runtimes without those modules. `createSocketsTransport()` builds one from the
|
|
101
|
+
* `connect()` of the `cloudflare:sockets` API (celld, Cloudflare Workers),
|
|
102
|
+
* `createStreamsTransport()` from any socket API built on web streams. Can not be combined
|
|
103
|
+
* with `proxy`.
|
|
104
|
+
*/
|
|
105
|
+
transport?: ImapTransport | undefined;
|
|
98
106
|
/** If true, then use QRESYNC instead of CONDSTORE. EXPUNGE notifications will include UID instead of sequence number */
|
|
99
107
|
qresync?: boolean | undefined;
|
|
100
108
|
/** If set, then breaks and restarts IDLE every maxIdleTime ms */
|
|
@@ -882,3 +890,70 @@ export interface ImapFlowEvents {
|
|
|
882
890
|
/** The server sent an ALERT: text meant for the user, such as a planned shutdown or an account problem */
|
|
883
891
|
alert: [alert: AlertEvent];
|
|
884
892
|
}
|
|
893
|
+
/** Where {@link ImapTransport} connects to, and the `tls` options of the client */
|
|
894
|
+
export interface ImapTransportOptions {
|
|
895
|
+
host: string;
|
|
896
|
+
port: number;
|
|
897
|
+
/** The SNI name, absent for an IP literal host without the `servername` option */
|
|
898
|
+
servername?: string | undefined;
|
|
899
|
+
/** The `tls` option of the client */
|
|
900
|
+
tls?: ConnectionOptions | undefined;
|
|
901
|
+
}
|
|
902
|
+
/** What {@link ImapTransport.connect} gets: the destination, and whether to open a TLS connection */
|
|
903
|
+
export interface ImapTransportConnectOptions extends ImapTransportOptions {
|
|
904
|
+
/** Open a TLS connection (the `secure` option), otherwise a cleartext one */
|
|
905
|
+
secure: boolean;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* The socket a {@link ImapTransport} returns: a Duplex stream of Buffers that emits `error`,
|
|
909
|
+
* `end` and `close`, and `timeout` once `setTimeout(ms)` was called and `ms` passed without
|
|
910
|
+
* traffic (0 disables it, like `net.Socket#setTimeout()`), the watchdog behind the
|
|
911
|
+
* `socketTimeout` option. The rest of what a `net.Socket` has is optional; ImapFlow uses it when
|
|
912
|
+
* it is there (the addresses for logging, the cipher of a TLS connection for `client.tls`).
|
|
913
|
+
*/
|
|
914
|
+
export type ImapTransportSocket = Duplex & {
|
|
915
|
+
setTimeout(ms: number): unknown;
|
|
916
|
+
setKeepAlive?: ((enable?: boolean, initialDelay?: number) => unknown) | undefined;
|
|
917
|
+
remoteAddress?: string | undefined;
|
|
918
|
+
remotePort?: number | undefined;
|
|
919
|
+
localAddress?: string | undefined;
|
|
920
|
+
localPort?: number | undefined;
|
|
921
|
+
getCipher?: (() => CipherNameAndProtocol | null) | undefined;
|
|
922
|
+
authorized?: boolean | undefined;
|
|
923
|
+
};
|
|
924
|
+
/**
|
|
925
|
+
* Opens and upgrades the socket carrying the session, see the `transport` option.
|
|
926
|
+
* `createStreamsTransport()` builds one over any socket API built on web streams,
|
|
927
|
+
* `createSocketsTransport()` over `cloudflare:sockets`.
|
|
928
|
+
*/
|
|
929
|
+
export interface ImapTransport {
|
|
930
|
+
/**
|
|
931
|
+
* Opens a connection, cleartext or TLS. Connection errors are emitted on the returned
|
|
932
|
+
* socket; a connection refused up front (an option the transport can not honor) may throw.
|
|
933
|
+
*
|
|
934
|
+
* @param options - Destination, TLS options and whether to open a TLS connection
|
|
935
|
+
* @param onConnect - Called once the connection (and the TLS handshake) is established
|
|
936
|
+
*/
|
|
937
|
+
connect(options: ImapTransportConnectOptions, onConnect: () => void): ImapTransportSocket;
|
|
938
|
+
/**
|
|
939
|
+
* Called right before the STARTTLS command is sent on the cleartext socket, so a transport
|
|
940
|
+
* that can only start TLS without a read in flight can stop reading once it is answered.
|
|
941
|
+
* STARTTLS is the only command in flight then: the next tagged response line (one that does
|
|
942
|
+
* not start with `* ` or `+ `) answers it, and if it is an OK, the server sends nothing more
|
|
943
|
+
* before the TLS handshake.
|
|
944
|
+
*
|
|
945
|
+
* @param socket - The cleartext socket returned by connect()
|
|
946
|
+
*/
|
|
947
|
+
prepareUpgrade?: ((socket: ImapTransportSocket) => void) | undefined;
|
|
948
|
+
/**
|
|
949
|
+
* Starts TLS on a connected cleartext socket after STARTTLS. May throw if the upgrade is
|
|
950
|
+
* refused before any handshake, handshake errors are emitted on the returned socket. A
|
|
951
|
+
* transport that can not start TLS leaves it out: ImapFlow then treats the server as one
|
|
952
|
+
* without STARTTLS (see the `doSTARTTLS` option).
|
|
953
|
+
*
|
|
954
|
+
* @param socket - The cleartext socket returned by connect()
|
|
955
|
+
* @param options - Destination and TLS options
|
|
956
|
+
* @param onSecure - Called once the TLS handshake is complete
|
|
957
|
+
*/
|
|
958
|
+
upgrade?: ((socket: ImapTransportSocket, options: ImapTransportOptions, onSecure: () => void) => ImapTransportSocket) | undefined;
|
|
959
|
+
}
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -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 */
|
package/dist/esm/errors.js
CHANGED
|
@@ -54,6 +54,8 @@ export const ImapFlowErrorCode = {
|
|
|
54
54
|
EPROXY: 'EPROXY',
|
|
55
55
|
UnsupportedProxyAddress: 'UnsupportedProxyAddress',
|
|
56
56
|
ERR_INVALID_URL: 'ERR_INVALID_URL',
|
|
57
|
+
// the transport option
|
|
58
|
+
UnsupportedTransportOption: 'UnsupportedTransportOption',
|
|
57
59
|
// API misuse
|
|
58
60
|
InstanceReused: 'InstanceReused'
|
|
59
61
|
};
|
package/dist/esm/imap-flow.d.ts
CHANGED
|
@@ -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
|
|
31
|
-
*
|
|
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 =
|
|
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
|
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
|
}
|
|
@@ -1144,6 +1145,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1144
1145
|
if (typeof socket.setTimeout === 'function') {
|
|
1145
1146
|
socket.setTimeout(this.socketTimeout);
|
|
1146
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
|
+
}
|
|
1147
1152
|
}
|
|
1148
1153
|
/** @internal */
|
|
1149
1154
|
setSocketHandlers() {
|
|
@@ -1329,6 +1334,12 @@ export class ImapFlow extends EventEmitter {
|
|
|
1329
1334
|
}
|
|
1330
1335
|
/** @internal */
|
|
1331
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
|
+
}
|
|
1332
1343
|
if (!(await this.run('COMPRESS'))) {
|
|
1333
1344
|
return; // was not able to negotiate compression
|
|
1334
1345
|
}
|
|
@@ -1449,10 +1460,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1449
1460
|
});
|
|
1450
1461
|
}
|
|
1451
1462
|
/** @internal */
|
|
1452
|
-
_failSTARTTLS() {
|
|
1463
|
+
_failSTARTTLS(reason = 'Server does not support STARTTLS') {
|
|
1453
1464
|
if (this.options.doSTARTTLS === true) {
|
|
1454
1465
|
// STARTTLS configured as requirement
|
|
1455
|
-
let err = new Error(
|
|
1466
|
+
let err = new Error(reason);
|
|
1456
1467
|
err.tlsFailed = true;
|
|
1457
1468
|
throw err;
|
|
1458
1469
|
}
|
|
@@ -1460,7 +1471,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1460
1471
|
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1461
1472
|
// option for the policy).
|
|
1462
1473
|
this.log.warn({
|
|
1463
|
-
msg:
|
|
1474
|
+
msg: reason + ', continuing over an unencrypted connection',
|
|
1464
1475
|
host: this.host,
|
|
1465
1476
|
port: this.port,
|
|
1466
1477
|
cid: this.id
|
|
@@ -1487,7 +1498,13 @@ export class ImapFlow extends EventEmitter {
|
|
|
1487
1498
|
if (!this.capabilities.has('STARTTLS')) {
|
|
1488
1499
|
return this._failSTARTTLS();
|
|
1489
1500
|
}
|
|
1501
|
+
if (this.options.transport && !this.options.transport.upgrade) {
|
|
1502
|
+
return this._failSTARTTLS('The transport can not start TLS');
|
|
1503
|
+
}
|
|
1490
1504
|
this.expectCapabilityUpdate = true;
|
|
1505
|
+
if (this.options.transport?.prepareUpgrade && this.socket) {
|
|
1506
|
+
this.options.transport.prepareUpgrade(this.socket);
|
|
1507
|
+
}
|
|
1491
1508
|
let canUpgrade = await this.run('STARTTLS');
|
|
1492
1509
|
if (!canUpgrade) {
|
|
1493
1510
|
return this._failSTARTTLS();
|
|
@@ -1589,7 +1606,7 @@ export class ImapFlow extends EventEmitter {
|
|
|
1589
1606
|
this.upgrading = true;
|
|
1590
1607
|
let tlsSocket;
|
|
1591
1608
|
try {
|
|
1592
|
-
|
|
1609
|
+
let onSecure = () => {
|
|
1593
1610
|
try {
|
|
1594
1611
|
/* c8 ignore start */ // race: connection closed during the TLS handshake window
|
|
1595
1612
|
if (this.isClosed) {
|
|
@@ -1641,7 +1658,11 @@ export class ImapFlow extends EventEmitter {
|
|
|
1641
1658
|
catch (ex) {
|
|
1642
1659
|
this.emitError(ex);
|
|
1643
1660
|
}
|
|
1644
|
-
}
|
|
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);
|
|
1645
1666
|
}
|
|
1646
1667
|
catch (err) {
|
|
1647
1668
|
// tls.connect() refused the upgrade before any handshake (an option the runtime
|
|
@@ -1988,6 +2009,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
1988
2009
|
tlsServername() {
|
|
1989
2010
|
return this.servername ? { servername: this.servername } : {};
|
|
1990
2011
|
}
|
|
2012
|
+
/** @internal */
|
|
2013
|
+
transportOptions() {
|
|
2014
|
+
return Object.assign({ host: this.host, port: this.port, tls: this.options.tls }, this.tlsServername());
|
|
2015
|
+
}
|
|
1991
2016
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1992
2017
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1993
2018
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -2114,6 +2139,10 @@ export class ImapFlow extends EventEmitter {
|
|
|
2114
2139
|
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
2115
2140
|
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
2116
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
|
+
}
|
|
2117
2146
|
let socket = false;
|
|
2118
2147
|
if (this.options.proxy) {
|
|
2119
2148
|
try {
|
|
@@ -2230,6 +2259,19 @@ export class ImapFlow extends EventEmitter {
|
|
|
2230
2259
|
setImmediate(onConnect);
|
|
2231
2260
|
}
|
|
2232
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
|
+
}
|
|
2233
2275
|
else {
|
|
2234
2276
|
this.socket = connector.connect(opts, onConnect);
|
|
2235
2277
|
}
|
package/dist/esm/package-info.js
CHANGED