imapkit 5.0.1 → 5.2.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.
@@ -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
+ }
@@ -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) {
package/dist/esm/rest.js CHANGED
@@ -60,8 +60,10 @@ function decodeBody(value, encoding) {
60
60
  }
61
61
  return encoding === 'base64' && typeof value === 'string' ? Buffer.from(value, 'base64') : value;
62
62
  }
63
- // the server events that GET /v1/events streams
64
- const EVENT_TYPES = ['session', 'command', 'mailbox', 'expunge', 'flags', 'acl', 'script', 'reset'];
63
+ // the server events that GET /v1/events streams by default
64
+ const EVENT_TYPES = ['session', 'command', 'mailbox', 'append', 'expunge', 'flags', 'acl', 'script', 'reset'];
65
+ // event types that are streamed only when ?types= asks for them, the protocol trace of every session is a lot of data
66
+ const OPT_IN_EVENT_TYPES = ['trace'];
65
67
  const sessionNumber = (origin) => (origin ? origin.sessionNumber : null);
66
68
  /**
67
69
  * Turns the arguments of a server event into JSON data: mailboxes become paths, messages UIDs, sessions numbers
@@ -76,8 +78,13 @@ function eventData(type, args) {
76
78
  const { type: change, path, oldPath, origin } = args[0];
77
79
  return { type: change, path, oldPath, origin: sessionNumber(origin) };
78
80
  }
81
+ case 'append':
79
82
  case 'expunge':
80
83
  return { path: args[0].path, uids: args[1].map(message => message.uid), origin: sessionNumber(args[2]) };
84
+ case 'trace': {
85
+ const { session, direction, data } = args[0];
86
+ return { session, direction, data: Buffer.from(data).toString('base64'), encoding: 'base64' };
87
+ }
81
88
  case 'flags':
82
89
  return {
83
90
  path: args[0].path,
@@ -152,13 +159,16 @@ function getRoutes(server, streams = new Set()) {
152
159
  {
153
160
  method: 'GET',
154
161
  path: '/v1/events',
155
- summary: 'Server events as Server-Sent Events (?types=session,command to choose: ' + EVENT_TYPES.join(', ') + ')',
162
+ summary: 'Server events as Server-Sent Events (?types=session,command to choose: ' +
163
+ EVENT_TYPES.concat(OPT_IN_EVENT_TYPES).join(', ') +
164
+ ', trace only when asked for)',
156
165
  handler: ({ query }) => {
157
166
  const wanted = query.get('types');
158
167
  const types = wanted === null ? EVENT_TYPES : wanted.split(',');
159
- const unknown = types.filter(type => EVENT_TYPES.indexOf(type) < 0);
168
+ const known = EVENT_TYPES.concat(OPT_IN_EVENT_TYPES);
169
+ const unknown = types.filter(type => known.indexOf(type) < 0);
160
170
  if (unknown.length) {
161
- throw storeError('Unknown event type ' + unknown.join(', ') + ', expected ' + EVENT_TYPES.join(', '), 'INVALID');
171
+ throw storeError('Unknown event type ' + unknown.join(', ') + ', expected ' + known.join(', '), 'INVALID');
162
172
  }
163
173
  return { stream: eventStream(server, types, streams) };
164
174
  }
@@ -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';
@@ -82,6 +83,8 @@ declare class IMAPServer extends Stream {
82
83
  sessionCounter: number;
83
84
  /** the control API, see src/control.ts */
84
85
  control: Control;
86
+ /** the implicit TLS listener that `start()` runs for the `tls` option */
87
+ tlsServer: tls.Server | null;
85
88
  /** the SMTP server that `start()` runs for the `smtp` option */
86
89
  smtpServer: SMTPListener | null;
87
90
  /** the HTTP server of the REST API that `start()` runs for the `rest` option */
@@ -106,6 +109,14 @@ declare class IMAPServer extends Stream {
106
109
  * @return {Promise<Number>} resolves with the port the server listens on
107
110
  */
108
111
  start(port?: number, host?: string): Promise<number>;
112
+ /**
113
+ * Starts the implicit TLS listener of the `tls` option. Its sessions are sessions of this server like the ones of
114
+ * the main listener, they share the storage and count as encrypted
115
+ *
116
+ * @param {String} [defaultHost] Address to listen on when the `tls` option has no host
117
+ * @return {Promise<Number>} resolves with the port of the TLS listener
118
+ */
119
+ startTlsListener(defaultHost?: string): Promise<number>;
109
120
  /**
110
121
  * Starts the REST API of the `rest` option, see src/rest.ts
111
122
  *
@@ -139,6 +150,13 @@ declare class IMAPServer extends Stream {
139
150
  listen(port: number, hostname: string, listeningListener?: () => void): void;
140
151
  listen(options: net.ListenOptions, listeningListener?: () => void): void;
141
152
  close(callback?: (err?: Error) => void): void;
153
+ /**
154
+ * Stops accepting IMAP connections, on the main listener and on the TLS listener of the `tls` option
155
+ *
156
+ * @param {Function} [callback] Called once both listeners are closed, which is after their last connection is
157
+ * gone, with the first error
158
+ */
159
+ closeImapListeners(callback?: (err?: Error) => void): void;
142
160
  /**
143
161
  * Stops the SMTP and REST listeners of `start()`
144
162
  *
@@ -158,6 +176,18 @@ declare class IMAPServer extends Stream {
158
176
  };
159
177
  address(): ReturnType<net.Server['address']>;
160
178
  createClient(socket: net.Socket): void;
179
+ /**
180
+ * Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
181
+ * server runs in a browser, see the README "Run in a browser"
182
+ *
183
+ * @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
184
+ * encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
185
+ * one over TLS
186
+ * @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
187
+ */
188
+ connect(options?: {
189
+ secure?: boolean | undefined;
190
+ }): MemorySocket;
161
191
  registerCapability(keyword: string, handler?: CapabilityCheck | null): void;
162
192
  /**
163
193
  * Sets the handler of a command
@@ -664,6 +694,13 @@ declare class IMAPConnection {
664
694
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
665
695
  */
666
696
  writeLayer(data: Buffer, transport: Transport | null, callback?: () => void): void;
697
+ /**
698
+ * Emits the protocol traffic of the session as `trace` events, as the IMAP layer sees it: above TLS and COMPRESS
699
+ *
700
+ * @param {String} direction "in" for client input, "out" for server output
701
+ * @param {Buffer} data The octets
702
+ */
703
+ trace(direction: 'in' | 'out', data: Buffer): void;
667
704
  /**
668
705
  * Sends output through the script rule that handles its event, if there is one
669
706
  *
@@ -19,6 +19,7 @@ import { resolveQuirks } from './quirks.js';
19
19
  import { validateStorage } from './storage-schema.js';
20
20
  import { createRestServer } from './rest.js';
21
21
  import * as bundledCert from './cert.js';
22
+ import { createSocketPair } from './memory-socket.js';
22
23
  import { ServerScript, sendOutput, handleLine, handleQuiet, literalResponse, releaseDeferred } from './script.js';
23
24
  // longest command line (not counting literals) accepted from a client
24
25
  const MAX_LINE_LENGTH = 1024 * 1024;
@@ -200,6 +201,7 @@ class IMAPServer extends Stream {
200
201
  }
201
202
  this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
202
203
  this.loadStorage();
204
+ this.tlsServer = null;
203
205
  this.smtpServer = null;
204
206
  this.restServer = null;
205
207
  }
@@ -250,9 +252,25 @@ class IMAPServer extends Stream {
250
252
  */
251
253
  async start(port, host) {
252
254
  const imapPort = await listenOn(this.server, port, host);
253
- await Promise.all([this.options.smtp ? this.startSmtp() : null, this.options.rest ? this.startRest() : null]);
255
+ await Promise.all([
256
+ this.options.tls ? this.startTlsListener(host) : null,
257
+ this.options.smtp ? this.startSmtp() : null,
258
+ this.options.rest ? this.startRest() : null
259
+ ]);
254
260
  return imapPort;
255
261
  }
262
+ /**
263
+ * Starts the implicit TLS listener of the `tls` option. Its sessions are sessions of this server like the ones of
264
+ * the main listener, they share the storage and count as encrypted
265
+ *
266
+ * @param {String} [defaultHost] Address to listen on when the `tls` option has no host
267
+ * @return {Promise<Number>} resolves with the port of the TLS listener
268
+ */
269
+ startTlsListener(defaultHost) {
270
+ const { port, host } = this.options.tls || {};
271
+ this.tlsServer = tls.createServer(this.getCredentials(), this.createClient.bind(this));
272
+ return listenOn(this.tlsServer, port, host || defaultHost);
273
+ }
256
274
  /**
257
275
  * Starts the REST API of the `rest` option, see src/rest.ts
258
276
  *
@@ -302,7 +320,7 @@ class IMAPServer extends Stream {
302
320
  }
303
321
  close(callback) {
304
322
  this.closeListeners(true);
305
- this.server.close(callback);
323
+ this.closeImapListeners(callback);
306
324
  // close() only completes once all connections are gone
307
325
  this.connections.forEach((connection) => {
308
326
  if (connection.socket) {
@@ -310,6 +328,24 @@ class IMAPServer extends Stream {
310
328
  }
311
329
  });
312
330
  }
331
+ /**
332
+ * Stops accepting IMAP connections, on the main listener and on the TLS listener of the `tls` option
333
+ *
334
+ * @param {Function} [callback] Called once both listeners are closed, which is after their last connection is
335
+ * gone, with the first error
336
+ */
337
+ closeImapListeners(callback) {
338
+ const listeners = this.tlsServer ? [this.server, this.tlsServer] : [this.server];
339
+ this.tlsServer = null;
340
+ let pending = listeners.length;
341
+ let error;
342
+ listeners.forEach(listener => listener.close(err => {
343
+ error = error || err;
344
+ if (--pending === 0) {
345
+ callback?.(error);
346
+ }
347
+ }));
348
+ }
313
349
  /**
314
350
  * Stops the SMTP and REST listeners of `start()`
315
351
  *
@@ -359,6 +395,21 @@ class IMAPServer extends Stream {
359
395
  handler(connection);
360
396
  });
361
397
  }
398
+ /**
399
+ * Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
400
+ * server runs in a browser, see the README "Run in a browser"
401
+ *
402
+ * @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
403
+ * encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
404
+ * one over TLS
405
+ * @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
406
+ */
407
+ connect(options) {
408
+ const [serverSocket, clientSocket] = createSocketPair();
409
+ serverSocket.encrypted = clientSocket.encrypted = !options || options.secure !== false;
410
+ this.createClient(serverSocket);
411
+ return clientSocket;
412
+ }
362
413
  registerCapability(keyword, handler) {
363
414
  this.capabilities[keyword] =
364
415
  handler ||
@@ -1095,6 +1146,8 @@ class IMAPServer extends Stream {
1095
1146
  break;
1096
1147
  }
1097
1148
  }
1149
+ // like expunge, before the sessions are told: APPEND, COPY, MOVE, REPLACE, SMTP and the control API all add here
1150
+ this.emit('append', mailbox, [message], this.activeConnection);
1098
1151
  this.notify({
1099
1152
  tag: '*',
1100
1153
  attributes: [
@@ -1396,7 +1449,8 @@ class IMAPConnection {
1396
1449
  this._outputQueue = null;
1397
1450
  this._outputTimer = null;
1398
1451
  this._deferredOutput = null;
1399
- this.secureConnection = !!this.options.secureConnection;
1452
+ // a TLS socket is encrypted, and so can be an in-memory connection of server.connect()
1453
+ this.secureConnection = !!this.options.secureConnection || socket.encrypted === true;
1400
1454
  this._remainder = '';
1401
1455
  this._command = '';
1402
1456
  this._literalRemaining = 0;
@@ -1414,7 +1468,7 @@ class IMAPConnection {
1414
1468
  this.messageGlobal = false;
1415
1469
  this._commandQueue = [];
1416
1470
  this._processing = false;
1417
- if (this.options.debug) {
1471
+ if (this.options.debug && typeof this.socket.pipe === 'function') {
1418
1472
  this.socket.pipe(process.stdout);
1419
1473
  }
1420
1474
  this.socket.on('data', this.receive.bind(this));
@@ -1537,6 +1591,7 @@ class IMAPConnection {
1537
1591
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
1538
1592
  */
1539
1593
  writeLayer(data, transport, callback) {
1594
+ this.trace('out', data);
1540
1595
  if (transport) {
1541
1596
  transport.write(data);
1542
1597
  if (callback) {
@@ -1547,6 +1602,17 @@ class IMAPConnection {
1547
1602
  this.writeRaw(data, callback);
1548
1603
  }
1549
1604
  }
1605
+ /**
1606
+ * Emits the protocol traffic of the session as `trace` events, as the IMAP layer sees it: above TLS and COMPRESS
1607
+ *
1608
+ * @param {String} direction "in" for client input, "out" for server output
1609
+ * @param {Buffer} data The octets
1610
+ */
1611
+ trace(direction, data) {
1612
+ if (this.server.listenerCount('trace')) {
1613
+ this.server.emit('trace', { session: this.sessionNumber, direction, data });
1614
+ }
1615
+ }
1550
1616
  /**
1551
1617
  * Sends output through the script rule that handles its event, if there is one
1552
1618
  *
@@ -1653,7 +1719,8 @@ class IMAPConnection {
1653
1719
  this.watchQuiet(quiet);
1654
1720
  }
1655
1721
  }, Math.max(0, next - (Date.now() - last)));
1656
- this._quietTimer.unref();
1722
+ // browsers have no unref
1723
+ this._quietTimer.unref?.();
1657
1724
  }
1658
1725
  /**
1659
1726
  * Handles data from the socket, through the transport layer if there is one
@@ -1856,6 +1923,9 @@ class IMAPConnection {
1856
1923
  if (this._resetting) {
1857
1924
  return;
1858
1925
  }
1926
+ if (chunk && chunk.length) {
1927
+ this.trace('in', chunk);
1928
+ }
1859
1929
  // everything in one read arrived before anything this read causes to be sent
1860
1930
  this._readCount = (this._readCount || 0) + 1;
1861
1931
  str = (chunk || '').toString('binary');
@@ -2323,6 +2393,16 @@ class IMAPConnection {
2323
2393
  // section 6.2.1). STARTTLS is refused when input is waiting (the noPipelining command option), this only
2324
2394
  // guards against plugins that upgrade the connection otherwise
2325
2395
  this.discardInput();
2396
+ // STARTTLS runs on a live connection
2397
+ const socket = this.socket;
2398
+ if (typeof socket.startTls === 'function') {
2399
+ // a socket without a TLS layer that can count as encrypted (MemorySocket of server.connect())
2400
+ return socket.startTls(() => {
2401
+ this.secureConnection = true;
2402
+ this.upgrading = false;
2403
+ callback();
2404
+ });
2405
+ }
2326
2406
  const secureContext = tls.createSecureContext(this.server.getCredentials());
2327
2407
  const socketOptions = {
2328
2408
  secureContext: secureContext,
@@ -2333,8 +2413,6 @@ class IMAPConnection {
2333
2413
  cb(null, secureContext);
2334
2414
  }
2335
2415
  };
2336
- // STARTTLS runs on a live connection
2337
- const socket = this.socket;
2338
2416
  // the RST of a script rule goes to the TCP socket under the TLS layer, see resetSocket. Node also has it as the
2339
2417
  // _parent of the TLS socket, Bun does not
2340
2418
  this.tcpSocket = socket;
@@ -228,6 +228,12 @@ export interface IMAPServerOptions {
228
228
  plugins?: (string | Plugin)[] | string | Plugin | undefined;
229
229
  users?: Record<string, UserData> | undefined;
230
230
  secureConnection?: boolean | undefined;
231
+ /** starts a second IMAP listener for implicit TLS (IMAPS) with `server.start()`, its sessions share the storage
232
+ * with the main listener. Without a host it listens on the host given to `start()` */
233
+ tls?: {
234
+ port?: number | undefined;
235
+ host?: string | undefined;
236
+ } | undefined;
231
237
  /** starts an SMTP server with `server.start()` that appends every received message to INBOX, needs the optional
232
238
  * smtp-server package */
233
239
  smtp?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapkit",
3
- "version": "5.0.1",
3
+ "version": "5.2.0",
4
4
  "description": "Scriptable, strictly RFC compliant in-memory IMAP server for testing IMAP clients",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -18,6 +18,10 @@
18
18
  "import": "./dist/esm/index.js",
19
19
  "require": "./dist/cjs/index.js"
20
20
  },
21
+ "./browser": {
22
+ "types": "./dist/esm/index.d.ts",
23
+ "default": "./dist/browser/imapkit.js"
24
+ },
21
25
  "./lib/server": {
22
26
  "import": "./dist/esm/index.js",
23
27
  "require": "./dist/cjs/index.js"
@@ -89,15 +93,18 @@
89
93
  "@eslint/js": "10.0.1",
90
94
  "@types/node": "20.19.43",
91
95
  "@types/smtp-server": "3.5.13",
92
- "eslint": "10.12.0",
96
+ "buffer": "6.0.3",
97
+ "esbuild": "0.28.2",
98
+ "eslint": "10.13.0",
93
99
  "eslint-config-prettier": "10.1.8",
100
+ "events": "3.3.0",
94
101
  "globals": "17.13.0",
95
- "imapflow": "2.2.9",
96
- "prettier": "3.9.9",
97
- "smtp-server": "3.19.17",
102
+ "imapflow": "2.5.6",
103
+ "prettier": "3.9.10",
104
+ "smtp-server": "3.20.2",
98
105
  "tsx": "4.23.15",
99
106
  "typescript": "6.0.3",
100
- "typescript-eslint": "8.71.1"
107
+ "typescript-eslint": "8.71.2"
101
108
  },
102
109
  "engines": {
103
110
  "node": ">=20.0.0"