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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.0](https://github.com/postalsys/imapflow/compare/v2.4.3...v2.5.0) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add a transport option and createSocketsTransport() for celld ([f4411aa](https://github.com/postalsys/imapflow/commit/f4411aa55635cb46b0e29a31965816e613d10ab9))
|
|
9
|
+
* add createStreamsTransport() for socket APIs built on web streams ([fec7999](https://github.com/postalsys/imapflow/commit/fec79997db36878a8e3222833837d09f011eac3e))
|
|
10
|
+
|
|
11
|
+
## [2.4.3](https://github.com/postalsys/imapflow/compare/v2.4.2...v2.4.3) (2026-10-15)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* fail the command on a NO or BAD status line without a tag ([5a38e17](https://github.com/postalsys/imapflow/commit/5a38e171ad8405583b090872076f464c4dc6f0e2))
|
|
17
|
+
* send ID after STARTTLS instead of before it ([7d6f37a](https://github.com/postalsys/imapflow/commit/7d6f37a4e8eb2f9b0f921f6b004a1ba5e45b1867))
|
|
18
|
+
* send ID after STARTTLS instead of before it ([197ecdc](https://github.com/postalsys/imapflow/commit/197ecdc49f2f64c45ee98c4e4284534662e26942))
|
|
19
|
+
|
|
3
20
|
## [2.4.2](https://github.com/postalsys/imapflow/compare/v2.4.1...v2.4.2) (2026-10-14)
|
|
4
21
|
|
|
5
22
|
|
package/README.md
CHANGED
|
@@ -26,7 +26,65 @@ npm install imapflow
|
|
|
26
26
|
|
|
27
27
|
ImapFlow requires Node.js 20 or newer. The package ships both an ES module build and a CommonJS build with bundled type declarations, so no separate `@types` package is needed.
|
|
28
28
|
|
|
29
|
-
The ES module build also runs on [Bun](https://bun.sh/) (tested against the latest release) and on [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the `nodejs_compat` compatibility flag. On Workers connect with implicit TLS (`secure: true`, usually port 993) or in cleartext: the runtime can not upgrade an already connected socket, so a STARTTLS negotiation fails with a TLS error, and it does not allow turning certificate validation off, so `tls: { rejectUnauthorized: false }` is rejected with `ERR_OPTION_NOT_IMPLEMENTED`. COMPRESS=DEFLATE, IDLE and the default pino logger work as on Node.js.
|
|
29
|
+
The ES module build also runs on [Bun](https://bun.sh/) (tested against the latest release) and on [Cloudflare Workers](https://developers.cloudflare.com/workers/) with the `nodejs_compat` compatibility flag. On Workers connect with implicit TLS (`secure: true`, usually port 993) or in cleartext: the runtime can not upgrade an already connected socket, so a STARTTLS negotiation fails with a TLS error, and it does not allow turning certificate validation off, so `tls: { rejectUnauthorized: false }` is rejected with `ERR_OPTION_NOT_IMPLEMENTED`. COMPRESS=DEFLATE, IDLE and the default pino logger work as on Node.js. STARTTLS works on Workers when the connection goes through `createSocketsTransport()` (see below).
|
|
30
|
+
|
|
31
|
+
[celld](https://celld.dev/), the self-hosted runtime for Cloudflare-style Workers, has no `node:net`, `node:tls` or `node:dns`. Pass the `connect()` of `cloudflare:sockets` through the `transport` option:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { connect } from 'cloudflare:sockets';
|
|
35
|
+
import { ImapFlow, createSocketsTransport } from 'imapflow';
|
|
36
|
+
|
|
37
|
+
const client = new ImapFlow({
|
|
38
|
+
host: 'imap.example.com',
|
|
39
|
+
port: 993,
|
|
40
|
+
secure: true,
|
|
41
|
+
auth: { user: 'user@example.com', pass: 'secret' },
|
|
42
|
+
transport: createSocketsTransport(connect)
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Implicit TLS, STARTTLS, cleartext, IDLE, `download()` and the default logger work. Not available there:
|
|
47
|
+
|
|
48
|
+
- The runtime verifies every certificate against its own root store, so `tls.rejectUnauthorized: false`, a custom `tls.ca`, client certificates and a `servername` that differs from `host` are refused with `UnsupportedTransportOption`. A TLS connection to an IP address needs a certificate issued for that address.
|
|
49
|
+
- COMPRESS=DEFLATE is skipped, the runtime has no streaming zlib.
|
|
50
|
+
- The `proxy` option can not be combined with a transport.
|
|
51
|
+
- `client.tls` stays `false`, the runtime reports no cipher details.
|
|
52
|
+
- A socket is closed when the request (or Durable Object event) that opened it ends. Run the whole session inside the handler, or inside `ctx.waitUntil()` to keep it going after the response.
|
|
53
|
+
|
|
54
|
+
### Other connections
|
|
55
|
+
|
|
56
|
+
The `transport` option takes any object with a `connect()` method, so other socket APIs plug in the same way. For an API built on web streams (a connection that opens with a promise of a `ReadableStream` and a `WritableStream` of bytes, as `cloudflare:sockets` and Chrome's Direct Sockets do), `createStreamsTransport()` does the wrapping:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { ImapFlow, createStreamsTransport } from 'imapflow';
|
|
60
|
+
|
|
61
|
+
const transport = createStreamsTransport({
|
|
62
|
+
// `secure` says whether ImapFlow wants a TLS connection; throw to refuse what the API can not do
|
|
63
|
+
open: ({ host, port, secure }) => {
|
|
64
|
+
const socket = openSocket(host, port, secure);
|
|
65
|
+
return {
|
|
66
|
+
opened: socket.opened.then(() => ({ readable: socket.readable, writable: socket.writable })),
|
|
67
|
+
close: () => socket.close()
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
// optional: start TLS on an open connection after STARTTLS. Without it ImapFlow does not try
|
|
71
|
+
// STARTTLS, and with doSTARTTLS: true the connection fails with err.tlsFailed set
|
|
72
|
+
upgrade: (socket, options) => startTlsSomehow(socket)
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`opened` may also report `remoteAddress`, `remotePort`, `localAddress` and `localPort`, and for a TLS connection `tls: { authorized, name, version }`, which becomes `client.tls`. If `close()` fails while the streams are locked, it is called again with the locks released.
|
|
77
|
+
|
|
78
|
+
An API built on events instead implements the transport itself: `connect(options, onConnect)` returns a Node.js `Duplex` stream of bytes and calls `onConnect` once the connection is up (or throws to refuse it), and an optional `upgrade(socket, options, onSecure)` starts TLS. The Duplex also needs `setTimeout(ms)`, emitting `timeout` after `ms` without traffic like `net.Socket` does: it is the watchdog behind the `socketTimeout` option.
|
|
79
|
+
|
|
80
|
+
In a browser, ImapFlow runs from the ES module build when the bundler provides what Node.js has and browsers do not:
|
|
81
|
+
|
|
82
|
+
- `node:stream` from [readable-stream](https://www.npmjs.com/package/readable-stream), `node:events` from [events](https://www.npmjs.com/package/events), and the `Buffer` global from [buffer](https://www.npmjs.com/package/buffer)
|
|
83
|
+
- the `process` global from [process](https://www.npmjs.com/package/process), and `setImmediate()` and `clearImmediate()` globals
|
|
84
|
+
- `node:crypto` with an MD5 `createHash()` (message ids on servers without OBJECTID), and `path` with `parse()` (used by a dependency for attachment file names)
|
|
85
|
+
- empty modules for `node:net`, `node:tls`, `node:dns` and `node:zlib` (COMPRESS is then skipped)
|
|
86
|
+
|
|
87
|
+
`test/browser/` in the repository has the complete esbuild setup, with the shims, and runs it in headless Chrome.
|
|
30
88
|
|
|
31
89
|
## Quick Example
|
|
32
90
|
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
20
20
|
readonly LockTimeout: "LockTimeout";
|
|
21
21
|
readonly ETHROTTLE: "ETHROTTLE";
|
|
22
22
|
readonly UnexpectedTag: "UnexpectedTag";
|
|
23
|
+
readonly MissingTag: "MissingTag";
|
|
23
24
|
readonly InvalidResponse: "InvalidResponse";
|
|
24
25
|
readonly ResponseProcessingFailed: "ResponseProcessingFailed";
|
|
25
26
|
readonly STARTTLS_INJECTION: "STARTTLS_INJECTION";
|
|
@@ -46,6 +47,7 @@ export declare const ImapFlowErrorCode: {
|
|
|
46
47
|
readonly EPROXY: "EPROXY";
|
|
47
48
|
readonly UnsupportedProxyAddress: "UnsupportedProxyAddress";
|
|
48
49
|
readonly ERR_INVALID_URL: "ERR_INVALID_URL";
|
|
50
|
+
readonly UnsupportedTransportOption: "UnsupportedTransportOption";
|
|
49
51
|
readonly InstanceReused: "InstanceReused";
|
|
50
52
|
};
|
|
51
53
|
/** One of the {@link ImapFlowErrorCode} values */
|
package/dist/cjs/errors.js
CHANGED
|
@@ -26,6 +26,7 @@ exports.ImapFlowErrorCode = {
|
|
|
26
26
|
// the server
|
|
27
27
|
ETHROTTLE: 'ETHROTTLE',
|
|
28
28
|
UnexpectedTag: 'UnexpectedTag',
|
|
29
|
+
MissingTag: 'MissingTag',
|
|
29
30
|
InvalidResponse: 'InvalidResponse',
|
|
30
31
|
ResponseProcessingFailed: 'ResponseProcessingFailed',
|
|
31
32
|
STARTTLS_INJECTION: 'STARTTLS_INJECTION',
|
|
@@ -57,6 +58,8 @@ exports.ImapFlowErrorCode = {
|
|
|
57
58
|
EPROXY: 'EPROXY',
|
|
58
59
|
UnsupportedProxyAddress: 'UnsupportedProxyAddress',
|
|
59
60
|
ERR_INVALID_URL: 'ERR_INVALID_URL',
|
|
61
|
+
// the transport option
|
|
62
|
+
UnsupportedTransportOption: 'UnsupportedTransportOption',
|
|
60
63
|
// API misuse
|
|
61
64
|
InstanceReused: 'InstanceReused'
|
|
62
65
|
};
|
package/dist/cjs/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/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
|
}
|
|
@@ -781,7 +784,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
781
784
|
*/
|
|
782
785
|
rejectUnparsedCompletion(payload, parserError) {
|
|
783
786
|
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
784
|
-
return;
|
|
787
|
+
return false;
|
|
785
788
|
}
|
|
786
789
|
// Prefer the tag the parser had already extracted before it failed - it went
|
|
787
790
|
// through the same leading-NUL workaround as every parsed response. Fall back
|
|
@@ -793,11 +796,45 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
793
796
|
tag = match && match[1];
|
|
794
797
|
}
|
|
795
798
|
if (!tag || tag !== this.currentRequest.tag) {
|
|
796
|
-
return;
|
|
799
|
+
return false;
|
|
797
800
|
}
|
|
798
801
|
let err = new Error('Failed to parse the server response for this command');
|
|
799
802
|
err.code = parserError.code || 'ParserError';
|
|
800
803
|
err.parserError = parserError;
|
|
804
|
+
this.failInFlight(err);
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Fails the in-flight command when the server answered it with a NO or BAD status line that
|
|
809
|
+
* has no tag at all (Namecheap Private Email: `BAD "Must issue a STARTTLS command first"`).
|
|
810
|
+
* Only one command is ever on the wire, and leaving it pending would stall the connection
|
|
811
|
+
* until the socket timeout. Tags are uppercase hex, so a command can carry the tag BAD:
|
|
812
|
+
* callers check for the command's own tag first.
|
|
813
|
+
*
|
|
814
|
+
* @param payload - Raw bytes of the line.
|
|
815
|
+
* @returns `true` when the line failed the in-flight command
|
|
816
|
+
* @internal
|
|
817
|
+
*/
|
|
818
|
+
rejectTaglessCompletion(payload) {
|
|
819
|
+
if (!this.currentRequest || !this.currentRequest.sent) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
let match = payload.toString('latin1', 0, 1024).match(/^\0*(BAD|NO)(?:\s+([^]*))?$/i);
|
|
823
|
+
if (!match) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
let err = (0, errors_js_1.createImapError)('Command failed', 'MissingTag', {
|
|
827
|
+
command: this.currentRequest.command,
|
|
828
|
+
responseStatus: match[1].toUpperCase(),
|
|
829
|
+
responseText: (match[2] || '').trim().replace(/^"(.*)"$/, '$1')
|
|
830
|
+
});
|
|
831
|
+
this.log.warn({ msg: 'Server sent a status response without a tag', err, cid: this.id });
|
|
832
|
+
this.failInFlight(err);
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
// Rejects the in-flight command and moves on to the next queued one
|
|
836
|
+
/** @internal */
|
|
837
|
+
failInFlight(err) {
|
|
801
838
|
this.rejectCurrentRequest(err);
|
|
802
839
|
this.trySend().catch(sendErr => (0, tools_js_1.logConnectionError)(this, 'Failed to dispatch command', sendErr));
|
|
803
840
|
}
|
|
@@ -847,7 +884,9 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
847
884
|
// timeout fires. The tag is recovered from the raw bytes (a tag is
|
|
848
885
|
// ASTRING-CHAR only, so it survives whatever made the rest unparseable) and
|
|
849
886
|
// the command is failed with the parser error instead of hanging.
|
|
850
|
-
this.rejectUnparsedCompletion(data.payload, err)
|
|
887
|
+
if (!this.rejectUnparsedCompletion(data.payload, err)) {
|
|
888
|
+
this.rejectTaglessCompletion(data.payload);
|
|
889
|
+
}
|
|
851
890
|
return true;
|
|
852
891
|
}
|
|
853
892
|
if (parsed.tag && !['*', '+'].includes(parsed.tag) && parsed.command) {
|
|
@@ -952,7 +991,8 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
952
991
|
// actually written to the socket (invariant 2), so the three cases below are kept apart:
|
|
953
992
|
// the active command completes, a command that has not been written yet is proof of
|
|
954
993
|
// desynchronization (queued behind another command, or current but not yet on the wire),
|
|
955
|
-
// and an entirely unknown tag is recorded but tolerated
|
|
994
|
+
// and an entirely unknown tag is recorded but tolerated, unless the line is a NO or BAD
|
|
995
|
+
// that came without a tag (see rejectTaglessCompletion()).
|
|
956
996
|
if (parsed.tag && !['*', '+'].includes(parsed.tag)) {
|
|
957
997
|
if (this.currentRequest && this.currentRequest.tag === parsed.tag && this.currentRequest.sent) {
|
|
958
998
|
let request = this.takeCurrentRequest();
|
|
@@ -990,7 +1030,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
990
1030
|
this.failProtocol(err);
|
|
991
1031
|
return false;
|
|
992
1032
|
}
|
|
993
|
-
else {
|
|
1033
|
+
else if (!this.rejectTaglessCompletion(data.payload)) {
|
|
994
1034
|
this.countUnknownTag(parsed.tag);
|
|
995
1035
|
}
|
|
996
1036
|
}
|
|
@@ -1148,6 +1188,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1148
1188
|
if (typeof socket.setTimeout === 'function') {
|
|
1149
1189
|
socket.setTimeout(this.socketTimeout);
|
|
1150
1190
|
}
|
|
1191
|
+
else {
|
|
1192
|
+
// a transport socket from plain JavaScript can lack it, which disables the watchdog
|
|
1193
|
+
this.log.warn({ msg: 'The socket has no setTimeout(), socketTimeout is not enforced', cid: this.id });
|
|
1194
|
+
}
|
|
1151
1195
|
}
|
|
1152
1196
|
/** @internal */
|
|
1153
1197
|
setSocketHandlers() {
|
|
@@ -1266,10 +1310,14 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1266
1310
|
/** @internal */
|
|
1267
1311
|
async startSession() {
|
|
1268
1312
|
await this.run('CAPABILITY');
|
|
1313
|
+
await this.upgradeToSTARTTLS();
|
|
1314
|
+
// ID goes after STARTTLS: the client info should not travel in cleartext, and some
|
|
1315
|
+
// servers refuse every command but STARTTLS before the upgrade with an untagged BAD
|
|
1316
|
+
// the client can not tie to ID, which then waits for the socket timeout (Namecheap
|
|
1317
|
+
// Private Email). The capability list was refreshed by the upgrade.
|
|
1269
1318
|
if (this.capabilities.has('ID')) {
|
|
1270
1319
|
this.idRequested = await this.run('ID', this.clientInfo);
|
|
1271
1320
|
}
|
|
1272
|
-
await this.upgradeToSTARTTLS();
|
|
1273
1321
|
await this.authenticate();
|
|
1274
1322
|
if ((!this.idRequested || Object.keys(this.idRequested).length < 2) && this.capabilities.has('ID')) {
|
|
1275
1323
|
// re-request ID after LOGIN
|
|
@@ -1329,6 +1377,12 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1329
1377
|
}
|
|
1330
1378
|
/** @internal */
|
|
1331
1379
|
async compress() {
|
|
1380
|
+
if (typeof node_zlib_1.default.createDeflateRaw !== 'function') {
|
|
1381
|
+
// The runtime has no streaming DEFLATE (celld implements only the synchronous zlib
|
|
1382
|
+
// functions), so the session stays uncompressed rather than failing after COMPRESS
|
|
1383
|
+
this.log.debug({ msg: 'Streaming DEFLATE is not available, skipping COMPRESS', cid: this.id });
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1332
1386
|
if (!(await this.run('COMPRESS'))) {
|
|
1333
1387
|
return; // was not able to negotiate compression
|
|
1334
1388
|
}
|
|
@@ -1449,10 +1503,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1449
1503
|
});
|
|
1450
1504
|
}
|
|
1451
1505
|
/** @internal */
|
|
1452
|
-
_failSTARTTLS() {
|
|
1506
|
+
_failSTARTTLS(reason = 'Server does not support STARTTLS') {
|
|
1453
1507
|
if (this.options.doSTARTTLS === true) {
|
|
1454
1508
|
// STARTTLS configured as requirement
|
|
1455
|
-
let err = new Error(
|
|
1509
|
+
let err = new Error(reason);
|
|
1456
1510
|
err.tlsFailed = true;
|
|
1457
1511
|
throw err;
|
|
1458
1512
|
}
|
|
@@ -1460,7 +1514,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1460
1514
|
// may be a downgrade attack: say so rather than fall back silently (see the doSTARTTLS
|
|
1461
1515
|
// option for the policy).
|
|
1462
1516
|
this.log.warn({
|
|
1463
|
-
msg:
|
|
1517
|
+
msg: reason + ', continuing over an unencrypted connection',
|
|
1464
1518
|
host: this.host,
|
|
1465
1519
|
port: this.port,
|
|
1466
1520
|
cid: this.id
|
|
@@ -1487,7 +1541,13 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1487
1541
|
if (!this.capabilities.has('STARTTLS')) {
|
|
1488
1542
|
return this._failSTARTTLS();
|
|
1489
1543
|
}
|
|
1544
|
+
if (this.options.transport && !this.options.transport.upgrade) {
|
|
1545
|
+
return this._failSTARTTLS('The transport can not start TLS');
|
|
1546
|
+
}
|
|
1490
1547
|
this.expectCapabilityUpdate = true;
|
|
1548
|
+
if (this.options.transport?.prepareUpgrade && this.socket) {
|
|
1549
|
+
this.options.transport.prepareUpgrade(this.socket);
|
|
1550
|
+
}
|
|
1491
1551
|
let canUpgrade = await this.run('STARTTLS');
|
|
1492
1552
|
if (!canUpgrade) {
|
|
1493
1553
|
return this._failSTARTTLS();
|
|
@@ -1589,7 +1649,7 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1589
1649
|
this.upgrading = true;
|
|
1590
1650
|
let tlsSocket;
|
|
1591
1651
|
try {
|
|
1592
|
-
|
|
1652
|
+
let onSecure = () => {
|
|
1593
1653
|
try {
|
|
1594
1654
|
/* c8 ignore start */ // race: connection closed during the TLS handshake window
|
|
1595
1655
|
if (this.isClosed) {
|
|
@@ -1641,7 +1701,11 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1641
1701
|
catch (ex) {
|
|
1642
1702
|
this.emitError(ex);
|
|
1643
1703
|
}
|
|
1644
|
-
}
|
|
1704
|
+
};
|
|
1705
|
+
// a transport without upgrade() never gets here, see the STARTTLS check above
|
|
1706
|
+
tlsSocket = this.options.transport
|
|
1707
|
+
? this.options.transport.upgrade(socketPlain, this.transportOptions(), onSecure)
|
|
1708
|
+
: node_tls_1.default.connect(opts, onSecure);
|
|
1645
1709
|
}
|
|
1646
1710
|
catch (err) {
|
|
1647
1711
|
// tls.connect() refused the upgrade before any handshake (an option the runtime
|
|
@@ -1988,6 +2052,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
1988
2052
|
tlsServername() {
|
|
1989
2053
|
return this.servername ? { servername: this.servername } : {};
|
|
1990
2054
|
}
|
|
2055
|
+
/** @internal */
|
|
2056
|
+
transportOptions() {
|
|
2057
|
+
return Object.assign({ host: this.host, port: this.port, tls: this.options.tls }, this.tlsServername());
|
|
2058
|
+
}
|
|
1991
2059
|
// Normalizes a message range from various input formats into an IMAP-compatible
|
|
1992
2060
|
// sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
|
|
1993
2061
|
// {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
|
|
@@ -2114,6 +2182,10 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2114
2182
|
// these methods take an optional second argument, so make sure that some random IMAP tag is not used as the second argument
|
|
2115
2183
|
this.untaggedHandlers.FETCH = untagged => this.untaggedFetch(untagged);
|
|
2116
2184
|
this.untaggedHandlers.VANISHED = untagged => this.untaggedVanished(untagged);
|
|
2185
|
+
let transport = this.options.transport;
|
|
2186
|
+
if (transport && this.options.proxy) {
|
|
2187
|
+
throw (0, errors_js_1.createImapError)('The proxy option can not be combined with a custom transport', 'UnsupportedTransportOption');
|
|
2188
|
+
}
|
|
2117
2189
|
let socket = false;
|
|
2118
2190
|
if (this.options.proxy) {
|
|
2119
2191
|
try {
|
|
@@ -2230,6 +2302,19 @@ class ImapFlow extends node_events_1.EventEmitter {
|
|
|
2230
2302
|
setImmediate(onConnect);
|
|
2231
2303
|
}
|
|
2232
2304
|
}
|
|
2305
|
+
else if (transport) {
|
|
2306
|
+
try {
|
|
2307
|
+
this.socket = transport.connect({ ...this.transportOptions(), secure: this.secureConnection }, onConnect);
|
|
2308
|
+
}
|
|
2309
|
+
catch (err) {
|
|
2310
|
+
// refused up front: the connection deadline must not stay armed
|
|
2311
|
+
(0, tools_js_1.clearTimer)(this.connectTimeout);
|
|
2312
|
+
this.closeAfter();
|
|
2313
|
+
this.log.error({ err, cid: this.id });
|
|
2314
|
+
reject(err);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2233
2318
|
else {
|
|
2234
2319
|
this.socket = connector.connect(opts, onConnect);
|
|
2235
2320
|
}
|
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
|
+
}
|