imapkit 5.0.1 → 5.1.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/README.md +18 -0
- package/dist/browser/imapkit.js +105 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/memory-socket.d.ts +69 -0
- package/dist/cjs/memory-socket.js +187 -0
- package/dist/cjs/plugins/idle.js +2 -2
- package/dist/cjs/server.d.ts +13 -0
- package/dist/cjs/server.js +31 -5
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/memory-socket.d.ts +69 -0
- package/dist/esm/memory-socket.js +182 -0
- package/dist/esm/plugins/idle.js +2 -2
- package/dist/esm/server.d.ts +13 -0
- package/dist/esm/server.js +31 -5
- package/package.json +8 -1
package/dist/cjs/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { TAG_REGEX, IMAPServer, IMAPConnection, ImapKitError, quirks, storageSch
|
|
|
6
6
|
export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
|
|
7
7
|
export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
|
|
8
8
|
export type { Quirk } from './quirks.js';
|
|
9
|
+
export type { MemorySocket } from './memory-socket.js';
|
|
9
10
|
export type { Control, MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter, ControlRoute, RouteRequest } from './control.js';
|
|
10
11
|
export type { FlagMode, UidMode, UidValidityOptions } from './store-operations.js';
|
|
11
12
|
declare const imapkit: typeof createServer & {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* One end of an in-memory connection, see `server.connect()`. It has the parts of `net.Socket` that the server and a
|
|
4
|
+
* client use: `write(data, [encoding], [callback])`, `end()`, `destroy()`, `pause()`, `resume()`, `setEncoding()`,
|
|
5
|
+
* the `data`, `end`, `close` and `error` events, and `encrypted`, which makes the server treat the connection like
|
|
6
|
+
* one over TLS (as `tls.TLSSocket#encrypted` does). Written data reaches the other end on a later event loop turn,
|
|
7
|
+
* one `data` event per write
|
|
8
|
+
*/
|
|
9
|
+
export declare class MemorySocket extends EventEmitter {
|
|
10
|
+
/** the other end of the connection */
|
|
11
|
+
peer: MemorySocket | null;
|
|
12
|
+
destroyed: boolean;
|
|
13
|
+
/** true if the connection counts as encrypted (implicit TLS, or after STARTTLS) */
|
|
14
|
+
encrypted: boolean;
|
|
15
|
+
_paused: boolean;
|
|
16
|
+
_ending: boolean;
|
|
17
|
+
_encoding: BufferEncoding | null;
|
|
18
|
+
/** received data that waits while the socket is paused */
|
|
19
|
+
_received: Buffer[];
|
|
20
|
+
constructor();
|
|
21
|
+
/**
|
|
22
|
+
* Sends data to the other end
|
|
23
|
+
*
|
|
24
|
+
* @param data Data to send, a string is encoded with `encoding` (utf8 by default)
|
|
25
|
+
* @param encoding Encoding of a string, or the callback
|
|
26
|
+
* @param callback Called once the other end received the data
|
|
27
|
+
* @return true, there is no backpressure
|
|
28
|
+
*/
|
|
29
|
+
write(data: Buffer | Uint8Array | string, encoding?: BufferEncoding | (() => void), callback?: () => void): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Ends the connection after the data written so far reached the other end, both ends close then
|
|
32
|
+
*
|
|
33
|
+
* @param data Data to send before the end
|
|
34
|
+
*/
|
|
35
|
+
end(data?: Buffer | Uint8Array | string): this;
|
|
36
|
+
/**
|
|
37
|
+
* Closes both ends right away, data that did not reach the other end yet is lost
|
|
38
|
+
*
|
|
39
|
+
* @param error Error to emit on this end
|
|
40
|
+
*/
|
|
41
|
+
destroy(error?: Error): this;
|
|
42
|
+
/**
|
|
43
|
+
* Closes the connection like a TCP RST: the other end gets an ECONNRESET error (`close: 'reset'` of script rules)
|
|
44
|
+
*/
|
|
45
|
+
resetAndDestroy(): this;
|
|
46
|
+
/**
|
|
47
|
+
* STARTTLS without TLS: both ends count as encrypted from now on (`upgradeConnection` in src/server.ts)
|
|
48
|
+
*
|
|
49
|
+
* @param callback Called once the connection counts as encrypted
|
|
50
|
+
*/
|
|
51
|
+
startTls(callback: () => void): void;
|
|
52
|
+
pause(): this;
|
|
53
|
+
resume(): this;
|
|
54
|
+
/**
|
|
55
|
+
* Makes the `data` events carry strings instead of Buffers
|
|
56
|
+
*
|
|
57
|
+
* @param encoding Encoding of the strings, e.g. "utf8" or "binary"
|
|
58
|
+
*/
|
|
59
|
+
setEncoding(encoding: BufferEncoding): this;
|
|
60
|
+
deliver(chunk: Buffer): void;
|
|
61
|
+
private emitData;
|
|
62
|
+
close(error?: Error): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates the two ends of an in-memory connection
|
|
66
|
+
*
|
|
67
|
+
* @return `[serverSocket, clientSocket]`
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSocketPair(): [MemorySocket, MemorySocket];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemorySocket = void 0;
|
|
4
|
+
exports.createSocketPair = createSocketPair;
|
|
5
|
+
const node_events_1 = require("node:events");
|
|
6
|
+
/**
|
|
7
|
+
* One end of an in-memory connection, see `server.connect()`. It has the parts of `net.Socket` that the server and a
|
|
8
|
+
* client use: `write(data, [encoding], [callback])`, `end()`, `destroy()`, `pause()`, `resume()`, `setEncoding()`,
|
|
9
|
+
* the `data`, `end`, `close` and `error` events, and `encrypted`, which makes the server treat the connection like
|
|
10
|
+
* one over TLS (as `tls.TLSSocket#encrypted` does). Written data reaches the other end on a later event loop turn,
|
|
11
|
+
* one `data` event per write
|
|
12
|
+
*/
|
|
13
|
+
class MemorySocket extends node_events_1.EventEmitter {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.peer = null;
|
|
17
|
+
this.destroyed = false;
|
|
18
|
+
this.encrypted = false;
|
|
19
|
+
this._paused = false;
|
|
20
|
+
this._ending = false;
|
|
21
|
+
this._encoding = null;
|
|
22
|
+
this._received = [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sends data to the other end
|
|
26
|
+
*
|
|
27
|
+
* @param data Data to send, a string is encoded with `encoding` (utf8 by default)
|
|
28
|
+
* @param encoding Encoding of a string, or the callback
|
|
29
|
+
* @param callback Called once the other end received the data
|
|
30
|
+
* @return true, there is no backpressure
|
|
31
|
+
*/
|
|
32
|
+
write(data, encoding, callback) {
|
|
33
|
+
if (typeof encoding === 'function') {
|
|
34
|
+
callback = encoding;
|
|
35
|
+
encoding = undefined;
|
|
36
|
+
}
|
|
37
|
+
if (this.destroyed || this._ending) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// no copy, like a real socket the data is not changed after it was written
|
|
41
|
+
const chunk = typeof data === 'string' ? Buffer.from(data, encoding || 'utf8') : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
42
|
+
nextTurn(() => {
|
|
43
|
+
this.peer?.deliver(chunk);
|
|
44
|
+
callback?.();
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Ends the connection after the data written so far reached the other end, both ends close then
|
|
50
|
+
*
|
|
51
|
+
* @param data Data to send before the end
|
|
52
|
+
*/
|
|
53
|
+
end(data) {
|
|
54
|
+
if (data !== undefined) {
|
|
55
|
+
this.write(data);
|
|
56
|
+
}
|
|
57
|
+
if (this.destroyed || this._ending) {
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
this._ending = true;
|
|
61
|
+
// queued after the writes, so the other end gets all data first
|
|
62
|
+
nextTurn(() => {
|
|
63
|
+
const peer = this.peer;
|
|
64
|
+
if (peer && !peer.destroyed) {
|
|
65
|
+
peer.emit('end');
|
|
66
|
+
peer.close();
|
|
67
|
+
}
|
|
68
|
+
this.close();
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Closes both ends right away, data that did not reach the other end yet is lost
|
|
74
|
+
*
|
|
75
|
+
* @param error Error to emit on this end
|
|
76
|
+
*/
|
|
77
|
+
destroy(error) {
|
|
78
|
+
if (this.destroyed) {
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
const peer = this.peer;
|
|
82
|
+
this.close(error);
|
|
83
|
+
if (peer && !peer.destroyed) {
|
|
84
|
+
peer.close();
|
|
85
|
+
}
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Closes the connection like a TCP RST: the other end gets an ECONNRESET error (`close: 'reset'` of script rules)
|
|
90
|
+
*/
|
|
91
|
+
resetAndDestroy() {
|
|
92
|
+
const peer = this.peer;
|
|
93
|
+
this.destroy();
|
|
94
|
+
if (peer) {
|
|
95
|
+
const error = new Error('read ECONNRESET');
|
|
96
|
+
error.code = 'ECONNRESET';
|
|
97
|
+
nextTurn(() => peer.emit('error', error));
|
|
98
|
+
}
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* STARTTLS without TLS: both ends count as encrypted from now on (`upgradeConnection` in src/server.ts)
|
|
103
|
+
*
|
|
104
|
+
* @param callback Called once the connection counts as encrypted
|
|
105
|
+
*/
|
|
106
|
+
startTls(callback) {
|
|
107
|
+
this.encrypted = true;
|
|
108
|
+
if (this.peer) {
|
|
109
|
+
this.peer.encrypted = true;
|
|
110
|
+
}
|
|
111
|
+
callback();
|
|
112
|
+
}
|
|
113
|
+
pause() {
|
|
114
|
+
this._paused = true;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
resume() {
|
|
118
|
+
this._paused = false;
|
|
119
|
+
while (this._received.length && !this._paused && !this.destroyed) {
|
|
120
|
+
this.emitData(this._received.shift());
|
|
121
|
+
}
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Makes the `data` events carry strings instead of Buffers
|
|
126
|
+
*
|
|
127
|
+
* @param encoding Encoding of the strings, e.g. "utf8" or "binary"
|
|
128
|
+
*/
|
|
129
|
+
setEncoding(encoding) {
|
|
130
|
+
this._encoding = encoding;
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
deliver(chunk) {
|
|
134
|
+
if (this.destroyed) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (this._paused) {
|
|
138
|
+
this._received.push(chunk);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.emitData(chunk);
|
|
142
|
+
}
|
|
143
|
+
emitData(chunk) {
|
|
144
|
+
this.emit('data', this._encoding ? chunk.toString(this._encoding) : chunk);
|
|
145
|
+
}
|
|
146
|
+
close(error) {
|
|
147
|
+
if (this.destroyed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.destroyed = true;
|
|
151
|
+
this._received = [];
|
|
152
|
+
nextTurn(() => {
|
|
153
|
+
if (error) {
|
|
154
|
+
this.emit('error', error);
|
|
155
|
+
}
|
|
156
|
+
this.emit('close', !!error);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.MemorySocket = MemorySocket;
|
|
161
|
+
/**
|
|
162
|
+
* Creates the two ends of an in-memory connection
|
|
163
|
+
*
|
|
164
|
+
* @return `[serverSocket, clientSocket]`
|
|
165
|
+
*/
|
|
166
|
+
function createSocketPair() {
|
|
167
|
+
const serverSocket = new MemorySocket();
|
|
168
|
+
const clientSocket = new MemorySocket();
|
|
169
|
+
serverSocket.peer = clientSocket;
|
|
170
|
+
clientSocket.peer = serverSocket;
|
|
171
|
+
return [serverSocket, clientSocket];
|
|
172
|
+
}
|
|
173
|
+
// one ordered queue for both ends, so the data and events of a connection keep the order they were sent in
|
|
174
|
+
const queue = [];
|
|
175
|
+
function nextTurn(fn) {
|
|
176
|
+
queue.push(fn);
|
|
177
|
+
if (queue.length === 1) {
|
|
178
|
+
setImmediate(runQueue);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function runQueue() {
|
|
182
|
+
// what runs now may queue more, that waits for the next turn
|
|
183
|
+
const batch = queue.splice(0, queue.length);
|
|
184
|
+
for (const fn of batch) {
|
|
185
|
+
fn();
|
|
186
|
+
}
|
|
187
|
+
}
|
package/dist/cjs/plugins/idle.js
CHANGED
|
@@ -23,8 +23,8 @@ function idlePlugin(server) {
|
|
|
23
23
|
connection.end();
|
|
24
24
|
}
|
|
25
25
|
}, 30 * 60 * 1000);
|
|
26
|
-
// An idling client should not keep the process alive
|
|
27
|
-
idleTimer.unref();
|
|
26
|
+
// An idling client should not keep the process alive (browsers have no unref)
|
|
27
|
+
idleTimer.unref?.();
|
|
28
28
|
const clearIdleTimer = () => clearTimeout(idleTimer);
|
|
29
29
|
const socket = connection.socket;
|
|
30
30
|
if (socket) {
|
package/dist/cjs/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ResolvedCommandOptions } from './command-states.js';
|
|
|
6
6
|
import { Control } from './control.js';
|
|
7
7
|
import type { RestServer } from './rest.js';
|
|
8
8
|
import type { SessionInfo } from './control.js';
|
|
9
|
+
import type { MemorySocket } from './memory-socket.js';
|
|
9
10
|
import { ServerScript } from './script.js';
|
|
10
11
|
import type { DeferredOutput, OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
|
|
11
12
|
import type { AppendCheck, AppendCheckOptions, AppendDataHandler, AppendMessage, CapabilityCheck, CheckResult, ClosedCheck, CommandCheck, CommandContext, CommandOptions, CommandHandler, ConnectionHandler, ConnectionState, CopyHandler, FetchHandler, IMAPResponse, IMAPServerOptions, ListedMailbox, LiteralFilter, Mailbox, MailboxHandler, MailboxStatus, Message, MessageFilter, MessageHandler, MessageRange, Namespace, Notification, NotifyEvent, NotifyFilter, OutputHandler, ParsedCommand, RangeLimit, Refusal, SearchAccessCheck, SearchHandler, SearchLimit, ServerStorage, StatusHandler, StorageMailbox, StorageMessage, StoreHandler, SubscriptionStandIn, Transport, UrlAccessCheck, UserData } from './types.js';
|
|
@@ -158,6 +159,18 @@ declare class IMAPServer extends Stream {
|
|
|
158
159
|
};
|
|
159
160
|
address(): ReturnType<net.Server['address']>;
|
|
160
161
|
createClient(socket: net.Socket): void;
|
|
162
|
+
/**
|
|
163
|
+
* Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
|
|
164
|
+
* server runs in a browser, see the README "Run in a browser"
|
|
165
|
+
*
|
|
166
|
+
* @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
|
|
167
|
+
* encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
|
|
168
|
+
* one over TLS
|
|
169
|
+
* @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
|
|
170
|
+
*/
|
|
171
|
+
connect(options?: {
|
|
172
|
+
secure?: boolean | undefined;
|
|
173
|
+
}): MemorySocket;
|
|
161
174
|
registerCapability(keyword: string, handler?: CapabilityCheck | null): void;
|
|
162
175
|
/**
|
|
163
176
|
* Sets the handler of a command
|
package/dist/cjs/server.js
CHANGED
|
@@ -59,6 +59,7 @@ const quirks_js_1 = require("./quirks.js");
|
|
|
59
59
|
const storage_schema_js_1 = require("./storage-schema.js");
|
|
60
60
|
const rest_js_1 = require("./rest.js");
|
|
61
61
|
const bundledCert = __importStar(require("./cert.js"));
|
|
62
|
+
const memory_socket_js_1 = require("./memory-socket.js");
|
|
62
63
|
const script_js_1 = require("./script.js");
|
|
63
64
|
// longest command line (not counting literals) accepted from a client
|
|
64
65
|
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
@@ -400,6 +401,21 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
400
401
|
handler(connection);
|
|
401
402
|
});
|
|
402
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
|
|
406
|
+
* server runs in a browser, see the README "Run in a browser"
|
|
407
|
+
*
|
|
408
|
+
* @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
|
|
409
|
+
* encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
|
|
410
|
+
* one over TLS
|
|
411
|
+
* @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
|
|
412
|
+
*/
|
|
413
|
+
connect(options) {
|
|
414
|
+
const [serverSocket, clientSocket] = (0, memory_socket_js_1.createSocketPair)();
|
|
415
|
+
serverSocket.encrypted = clientSocket.encrypted = !options || options.secure !== false;
|
|
416
|
+
this.createClient(serverSocket);
|
|
417
|
+
return clientSocket;
|
|
418
|
+
}
|
|
403
419
|
registerCapability(keyword, handler) {
|
|
404
420
|
this.capabilities[keyword] =
|
|
405
421
|
handler ||
|
|
@@ -1438,7 +1454,8 @@ class IMAPConnection {
|
|
|
1438
1454
|
this._outputQueue = null;
|
|
1439
1455
|
this._outputTimer = null;
|
|
1440
1456
|
this._deferredOutput = null;
|
|
1441
|
-
|
|
1457
|
+
// a TLS socket is encrypted, and so can be an in-memory connection of server.connect()
|
|
1458
|
+
this.secureConnection = !!this.options.secureConnection || socket.encrypted === true;
|
|
1442
1459
|
this._remainder = '';
|
|
1443
1460
|
this._command = '';
|
|
1444
1461
|
this._literalRemaining = 0;
|
|
@@ -1456,7 +1473,7 @@ class IMAPConnection {
|
|
|
1456
1473
|
this.messageGlobal = false;
|
|
1457
1474
|
this._commandQueue = [];
|
|
1458
1475
|
this._processing = false;
|
|
1459
|
-
if (this.options.debug) {
|
|
1476
|
+
if (this.options.debug && typeof this.socket.pipe === 'function') {
|
|
1460
1477
|
this.socket.pipe(process.stdout);
|
|
1461
1478
|
}
|
|
1462
1479
|
this.socket.on('data', this.receive.bind(this));
|
|
@@ -1695,7 +1712,8 @@ class IMAPConnection {
|
|
|
1695
1712
|
this.watchQuiet(quiet);
|
|
1696
1713
|
}
|
|
1697
1714
|
}, Math.max(0, next - (Date.now() - last)));
|
|
1698
|
-
|
|
1715
|
+
// browsers have no unref
|
|
1716
|
+
this._quietTimer.unref?.();
|
|
1699
1717
|
}
|
|
1700
1718
|
/**
|
|
1701
1719
|
* Handles data from the socket, through the transport layer if there is one
|
|
@@ -2365,6 +2383,16 @@ class IMAPConnection {
|
|
|
2365
2383
|
// section 6.2.1). STARTTLS is refused when input is waiting (the noPipelining command option), this only
|
|
2366
2384
|
// guards against plugins that upgrade the connection otherwise
|
|
2367
2385
|
this.discardInput();
|
|
2386
|
+
// STARTTLS runs on a live connection
|
|
2387
|
+
const socket = this.socket;
|
|
2388
|
+
if (typeof socket.startTls === 'function') {
|
|
2389
|
+
// a socket without a TLS layer that can count as encrypted (MemorySocket of server.connect())
|
|
2390
|
+
return socket.startTls(() => {
|
|
2391
|
+
this.secureConnection = true;
|
|
2392
|
+
this.upgrading = false;
|
|
2393
|
+
callback();
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2368
2396
|
const secureContext = node_tls_1.default.createSecureContext(this.server.getCredentials());
|
|
2369
2397
|
const socketOptions = {
|
|
2370
2398
|
secureContext: secureContext,
|
|
@@ -2375,8 +2403,6 @@ class IMAPConnection {
|
|
|
2375
2403
|
cb(null, secureContext);
|
|
2376
2404
|
}
|
|
2377
2405
|
};
|
|
2378
|
-
// STARTTLS runs on a live connection
|
|
2379
|
-
const socket = this.socket;
|
|
2380
2406
|
// the RST of a script rule goes to the TCP socket under the TLS layer, see resetSocket. Node also has it as the
|
|
2381
2407
|
// _parent of the TLS socket, Bun does not
|
|
2382
2408
|
this.tcpSocket = socket;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { TAG_REGEX, IMAPServer, IMAPConnection, ImapKitError, quirks, storageSch
|
|
|
6
6
|
export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
|
|
7
7
|
export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
|
|
8
8
|
export type { Quirk } from './quirks.js';
|
|
9
|
+
export type { MemorySocket } from './memory-socket.js';
|
|
9
10
|
export type { Control, MailboxInfo, MessageInfo, SessionInfo, NewMessage, UserOptions, SessionFilter, ControlRoute, RouteRequest } from './control.js';
|
|
10
11
|
export type { FlagMode, UidMode, UidValidityOptions } from './store-operations.js';
|
|
11
12
|
declare const imapkit: typeof createServer & {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* One end of an in-memory connection, see `server.connect()`. It has the parts of `net.Socket` that the server and a
|
|
4
|
+
* client use: `write(data, [encoding], [callback])`, `end()`, `destroy()`, `pause()`, `resume()`, `setEncoding()`,
|
|
5
|
+
* the `data`, `end`, `close` and `error` events, and `encrypted`, which makes the server treat the connection like
|
|
6
|
+
* one over TLS (as `tls.TLSSocket#encrypted` does). Written data reaches the other end on a later event loop turn,
|
|
7
|
+
* one `data` event per write
|
|
8
|
+
*/
|
|
9
|
+
export declare class MemorySocket extends EventEmitter {
|
|
10
|
+
/** the other end of the connection */
|
|
11
|
+
peer: MemorySocket | null;
|
|
12
|
+
destroyed: boolean;
|
|
13
|
+
/** true if the connection counts as encrypted (implicit TLS, or after STARTTLS) */
|
|
14
|
+
encrypted: boolean;
|
|
15
|
+
_paused: boolean;
|
|
16
|
+
_ending: boolean;
|
|
17
|
+
_encoding: BufferEncoding | null;
|
|
18
|
+
/** received data that waits while the socket is paused */
|
|
19
|
+
_received: Buffer[];
|
|
20
|
+
constructor();
|
|
21
|
+
/**
|
|
22
|
+
* Sends data to the other end
|
|
23
|
+
*
|
|
24
|
+
* @param data Data to send, a string is encoded with `encoding` (utf8 by default)
|
|
25
|
+
* @param encoding Encoding of a string, or the callback
|
|
26
|
+
* @param callback Called once the other end received the data
|
|
27
|
+
* @return true, there is no backpressure
|
|
28
|
+
*/
|
|
29
|
+
write(data: Buffer | Uint8Array | string, encoding?: BufferEncoding | (() => void), callback?: () => void): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Ends the connection after the data written so far reached the other end, both ends close then
|
|
32
|
+
*
|
|
33
|
+
* @param data Data to send before the end
|
|
34
|
+
*/
|
|
35
|
+
end(data?: Buffer | Uint8Array | string): this;
|
|
36
|
+
/**
|
|
37
|
+
* Closes both ends right away, data that did not reach the other end yet is lost
|
|
38
|
+
*
|
|
39
|
+
* @param error Error to emit on this end
|
|
40
|
+
*/
|
|
41
|
+
destroy(error?: Error): this;
|
|
42
|
+
/**
|
|
43
|
+
* Closes the connection like a TCP RST: the other end gets an ECONNRESET error (`close: 'reset'` of script rules)
|
|
44
|
+
*/
|
|
45
|
+
resetAndDestroy(): this;
|
|
46
|
+
/**
|
|
47
|
+
* STARTTLS without TLS: both ends count as encrypted from now on (`upgradeConnection` in src/server.ts)
|
|
48
|
+
*
|
|
49
|
+
* @param callback Called once the connection counts as encrypted
|
|
50
|
+
*/
|
|
51
|
+
startTls(callback: () => void): void;
|
|
52
|
+
pause(): this;
|
|
53
|
+
resume(): this;
|
|
54
|
+
/**
|
|
55
|
+
* Makes the `data` events carry strings instead of Buffers
|
|
56
|
+
*
|
|
57
|
+
* @param encoding Encoding of the strings, e.g. "utf8" or "binary"
|
|
58
|
+
*/
|
|
59
|
+
setEncoding(encoding: BufferEncoding): this;
|
|
60
|
+
deliver(chunk: Buffer): void;
|
|
61
|
+
private emitData;
|
|
62
|
+
close(error?: Error): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates the two ends of an in-memory connection
|
|
66
|
+
*
|
|
67
|
+
* @return `[serverSocket, clientSocket]`
|
|
68
|
+
*/
|
|
69
|
+
export declare function createSocketPair(): [MemorySocket, MemorySocket];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* One end of an in-memory connection, see `server.connect()`. It has the parts of `net.Socket` that the server and a
|
|
4
|
+
* client use: `write(data, [encoding], [callback])`, `end()`, `destroy()`, `pause()`, `resume()`, `setEncoding()`,
|
|
5
|
+
* the `data`, `end`, `close` and `error` events, and `encrypted`, which makes the server treat the connection like
|
|
6
|
+
* one over TLS (as `tls.TLSSocket#encrypted` does). Written data reaches the other end on a later event loop turn,
|
|
7
|
+
* one `data` event per write
|
|
8
|
+
*/
|
|
9
|
+
export class MemorySocket extends EventEmitter {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.peer = null;
|
|
13
|
+
this.destroyed = false;
|
|
14
|
+
this.encrypted = false;
|
|
15
|
+
this._paused = false;
|
|
16
|
+
this._ending = false;
|
|
17
|
+
this._encoding = null;
|
|
18
|
+
this._received = [];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sends data to the other end
|
|
22
|
+
*
|
|
23
|
+
* @param data Data to send, a string is encoded with `encoding` (utf8 by default)
|
|
24
|
+
* @param encoding Encoding of a string, or the callback
|
|
25
|
+
* @param callback Called once the other end received the data
|
|
26
|
+
* @return true, there is no backpressure
|
|
27
|
+
*/
|
|
28
|
+
write(data, encoding, callback) {
|
|
29
|
+
if (typeof encoding === 'function') {
|
|
30
|
+
callback = encoding;
|
|
31
|
+
encoding = undefined;
|
|
32
|
+
}
|
|
33
|
+
if (this.destroyed || this._ending) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// no copy, like a real socket the data is not changed after it was written
|
|
37
|
+
const chunk = typeof data === 'string' ? Buffer.from(data, encoding || 'utf8') : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
38
|
+
nextTurn(() => {
|
|
39
|
+
this.peer?.deliver(chunk);
|
|
40
|
+
callback?.();
|
|
41
|
+
});
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Ends the connection after the data written so far reached the other end, both ends close then
|
|
46
|
+
*
|
|
47
|
+
* @param data Data to send before the end
|
|
48
|
+
*/
|
|
49
|
+
end(data) {
|
|
50
|
+
if (data !== undefined) {
|
|
51
|
+
this.write(data);
|
|
52
|
+
}
|
|
53
|
+
if (this.destroyed || this._ending) {
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
this._ending = true;
|
|
57
|
+
// queued after the writes, so the other end gets all data first
|
|
58
|
+
nextTurn(() => {
|
|
59
|
+
const peer = this.peer;
|
|
60
|
+
if (peer && !peer.destroyed) {
|
|
61
|
+
peer.emit('end');
|
|
62
|
+
peer.close();
|
|
63
|
+
}
|
|
64
|
+
this.close();
|
|
65
|
+
});
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Closes both ends right away, data that did not reach the other end yet is lost
|
|
70
|
+
*
|
|
71
|
+
* @param error Error to emit on this end
|
|
72
|
+
*/
|
|
73
|
+
destroy(error) {
|
|
74
|
+
if (this.destroyed) {
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
const peer = this.peer;
|
|
78
|
+
this.close(error);
|
|
79
|
+
if (peer && !peer.destroyed) {
|
|
80
|
+
peer.close();
|
|
81
|
+
}
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Closes the connection like a TCP RST: the other end gets an ECONNRESET error (`close: 'reset'` of script rules)
|
|
86
|
+
*/
|
|
87
|
+
resetAndDestroy() {
|
|
88
|
+
const peer = this.peer;
|
|
89
|
+
this.destroy();
|
|
90
|
+
if (peer) {
|
|
91
|
+
const error = new Error('read ECONNRESET');
|
|
92
|
+
error.code = 'ECONNRESET';
|
|
93
|
+
nextTurn(() => peer.emit('error', error));
|
|
94
|
+
}
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* STARTTLS without TLS: both ends count as encrypted from now on (`upgradeConnection` in src/server.ts)
|
|
99
|
+
*
|
|
100
|
+
* @param callback Called once the connection counts as encrypted
|
|
101
|
+
*/
|
|
102
|
+
startTls(callback) {
|
|
103
|
+
this.encrypted = true;
|
|
104
|
+
if (this.peer) {
|
|
105
|
+
this.peer.encrypted = true;
|
|
106
|
+
}
|
|
107
|
+
callback();
|
|
108
|
+
}
|
|
109
|
+
pause() {
|
|
110
|
+
this._paused = true;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
resume() {
|
|
114
|
+
this._paused = false;
|
|
115
|
+
while (this._received.length && !this._paused && !this.destroyed) {
|
|
116
|
+
this.emitData(this._received.shift());
|
|
117
|
+
}
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Makes the `data` events carry strings instead of Buffers
|
|
122
|
+
*
|
|
123
|
+
* @param encoding Encoding of the strings, e.g. "utf8" or "binary"
|
|
124
|
+
*/
|
|
125
|
+
setEncoding(encoding) {
|
|
126
|
+
this._encoding = encoding;
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
deliver(chunk) {
|
|
130
|
+
if (this.destroyed) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (this._paused) {
|
|
134
|
+
this._received.push(chunk);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.emitData(chunk);
|
|
138
|
+
}
|
|
139
|
+
emitData(chunk) {
|
|
140
|
+
this.emit('data', this._encoding ? chunk.toString(this._encoding) : chunk);
|
|
141
|
+
}
|
|
142
|
+
close(error) {
|
|
143
|
+
if (this.destroyed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.destroyed = true;
|
|
147
|
+
this._received = [];
|
|
148
|
+
nextTurn(() => {
|
|
149
|
+
if (error) {
|
|
150
|
+
this.emit('error', error);
|
|
151
|
+
}
|
|
152
|
+
this.emit('close', !!error);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Creates the two ends of an in-memory connection
|
|
158
|
+
*
|
|
159
|
+
* @return `[serverSocket, clientSocket]`
|
|
160
|
+
*/
|
|
161
|
+
export function createSocketPair() {
|
|
162
|
+
const serverSocket = new MemorySocket();
|
|
163
|
+
const clientSocket = new MemorySocket();
|
|
164
|
+
serverSocket.peer = clientSocket;
|
|
165
|
+
clientSocket.peer = serverSocket;
|
|
166
|
+
return [serverSocket, clientSocket];
|
|
167
|
+
}
|
|
168
|
+
// one ordered queue for both ends, so the data and events of a connection keep the order they were sent in
|
|
169
|
+
const queue = [];
|
|
170
|
+
function nextTurn(fn) {
|
|
171
|
+
queue.push(fn);
|
|
172
|
+
if (queue.length === 1) {
|
|
173
|
+
setImmediate(runQueue);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function runQueue() {
|
|
177
|
+
// what runs now may queue more, that waits for the next turn
|
|
178
|
+
const batch = queue.splice(0, queue.length);
|
|
179
|
+
for (const fn of batch) {
|
|
180
|
+
fn();
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/esm/plugins/idle.js
CHANGED
|
@@ -20,8 +20,8 @@ export default function idlePlugin(server) {
|
|
|
20
20
|
connection.end();
|
|
21
21
|
}
|
|
22
22
|
}, 30 * 60 * 1000);
|
|
23
|
-
// An idling client should not keep the process alive
|
|
24
|
-
idleTimer.unref();
|
|
23
|
+
// An idling client should not keep the process alive (browsers have no unref)
|
|
24
|
+
idleTimer.unref?.();
|
|
25
25
|
const clearIdleTimer = () => clearTimeout(idleTimer);
|
|
26
26
|
const socket = connection.socket;
|
|
27
27
|
if (socket) {
|