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.
@@ -629,7 +629,7 @@ class Control {
629
629
  }
630
630
  else {
631
631
  this.server.closeListeners(false);
632
- this.server.server.close(done);
632
+ this.server.closeImapListeners(done);
633
633
  }
634
634
  });
635
635
  }
@@ -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
+ }
@@ -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/rest.js CHANGED
@@ -69,8 +69,10 @@ function decodeBody(value, encoding) {
69
69
  }
70
70
  return encoding === 'base64' && typeof value === 'string' ? Buffer.from(value, 'base64') : value;
71
71
  }
72
- // the server events that GET /v1/events streams
73
- const EVENT_TYPES = ['session', 'command', 'mailbox', 'expunge', 'flags', 'acl', 'script', 'reset'];
72
+ // the server events that GET /v1/events streams by default
73
+ const EVENT_TYPES = ['session', 'command', 'mailbox', 'append', 'expunge', 'flags', 'acl', 'script', 'reset'];
74
+ // event types that are streamed only when ?types= asks for them, the protocol trace of every session is a lot of data
75
+ const OPT_IN_EVENT_TYPES = ['trace'];
74
76
  const sessionNumber = (origin) => (origin ? origin.sessionNumber : null);
75
77
  /**
76
78
  * Turns the arguments of a server event into JSON data: mailboxes become paths, messages UIDs, sessions numbers
@@ -85,8 +87,13 @@ function eventData(type, args) {
85
87
  const { type: change, path, oldPath, origin } = args[0];
86
88
  return { type: change, path, oldPath, origin: sessionNumber(origin) };
87
89
  }
90
+ case 'append':
88
91
  case 'expunge':
89
92
  return { path: args[0].path, uids: args[1].map(message => message.uid), origin: sessionNumber(args[2]) };
93
+ case 'trace': {
94
+ const { session, direction, data } = args[0];
95
+ return { session, direction, data: Buffer.from(data).toString('base64'), encoding: 'base64' };
96
+ }
90
97
  case 'flags':
91
98
  return {
92
99
  path: args[0].path,
@@ -161,13 +168,16 @@ function getRoutes(server, streams = new Set()) {
161
168
  {
162
169
  method: 'GET',
163
170
  path: '/v1/events',
164
- summary: 'Server events as Server-Sent Events (?types=session,command to choose: ' + EVENT_TYPES.join(', ') + ')',
171
+ summary: 'Server events as Server-Sent Events (?types=session,command to choose: ' +
172
+ EVENT_TYPES.concat(OPT_IN_EVENT_TYPES).join(', ') +
173
+ ', trace only when asked for)',
165
174
  handler: ({ query }) => {
166
175
  const wanted = query.get('types');
167
176
  const types = wanted === null ? EVENT_TYPES : wanted.split(',');
168
- const unknown = types.filter(type => EVENT_TYPES.indexOf(type) < 0);
177
+ const known = EVENT_TYPES.concat(OPT_IN_EVENT_TYPES);
178
+ const unknown = types.filter(type => known.indexOf(type) < 0);
169
179
  if (unknown.length) {
170
- throw (0, store_operations_js_1.storeError)('Unknown event type ' + unknown.join(', ') + ', expected ' + EVENT_TYPES.join(', '), 'INVALID');
180
+ throw (0, store_operations_js_1.storeError)('Unknown event type ' + unknown.join(', ') + ', expected ' + known.join(', '), 'INVALID');
171
181
  }
172
182
  return { stream: eventStream(server, types, streams) };
173
183
  }
@@ -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
  *
@@ -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;
@@ -241,6 +242,7 @@ class IMAPServer extends node_stream_1.Stream {
241
242
  }
242
243
  this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
243
244
  this.loadStorage();
245
+ this.tlsServer = null;
244
246
  this.smtpServer = null;
245
247
  this.restServer = null;
246
248
  }
@@ -291,9 +293,25 @@ class IMAPServer extends node_stream_1.Stream {
291
293
  */
292
294
  async start(port, host) {
293
295
  const imapPort = await listenOn(this.server, port, host);
294
- await Promise.all([this.options.smtp ? this.startSmtp() : null, this.options.rest ? this.startRest() : null]);
296
+ await Promise.all([
297
+ this.options.tls ? this.startTlsListener(host) : null,
298
+ this.options.smtp ? this.startSmtp() : null,
299
+ this.options.rest ? this.startRest() : null
300
+ ]);
295
301
  return imapPort;
296
302
  }
303
+ /**
304
+ * Starts the implicit TLS listener of the `tls` option. Its sessions are sessions of this server like the ones of
305
+ * the main listener, they share the storage and count as encrypted
306
+ *
307
+ * @param {String} [defaultHost] Address to listen on when the `tls` option has no host
308
+ * @return {Promise<Number>} resolves with the port of the TLS listener
309
+ */
310
+ startTlsListener(defaultHost) {
311
+ const { port, host } = this.options.tls || {};
312
+ this.tlsServer = node_tls_1.default.createServer(this.getCredentials(), this.createClient.bind(this));
313
+ return listenOn(this.tlsServer, port, host || defaultHost);
314
+ }
297
315
  /**
298
316
  * Starts the REST API of the `rest` option, see src/rest.ts
299
317
  *
@@ -343,7 +361,7 @@ class IMAPServer extends node_stream_1.Stream {
343
361
  }
344
362
  close(callback) {
345
363
  this.closeListeners(true);
346
- this.server.close(callback);
364
+ this.closeImapListeners(callback);
347
365
  // close() only completes once all connections are gone
348
366
  this.connections.forEach((connection) => {
349
367
  if (connection.socket) {
@@ -351,6 +369,24 @@ class IMAPServer extends node_stream_1.Stream {
351
369
  }
352
370
  });
353
371
  }
372
+ /**
373
+ * Stops accepting IMAP connections, on the main listener and on the TLS listener of the `tls` option
374
+ *
375
+ * @param {Function} [callback] Called once both listeners are closed, which is after their last connection is
376
+ * gone, with the first error
377
+ */
378
+ closeImapListeners(callback) {
379
+ const listeners = this.tlsServer ? [this.server, this.tlsServer] : [this.server];
380
+ this.tlsServer = null;
381
+ let pending = listeners.length;
382
+ let error;
383
+ listeners.forEach(listener => listener.close(err => {
384
+ error = error || err;
385
+ if (--pending === 0) {
386
+ callback?.(error);
387
+ }
388
+ }));
389
+ }
354
390
  /**
355
391
  * Stops the SMTP and REST listeners of `start()`
356
392
  *
@@ -400,6 +436,21 @@ class IMAPServer extends node_stream_1.Stream {
400
436
  handler(connection);
401
437
  });
402
438
  }
439
+ /**
440
+ * Opens a connection to the server in memory, without a network socket (no `listen()` needed). This is how the
441
+ * server runs in a browser, see the README "Run in a browser"
442
+ *
443
+ * @param {Object} [options] `{ secure }`: false makes the connection a plaintext one, where STARTTLS turns on the
444
+ * encryption (nothing is encrypted, the session only counts as encrypted). By default the connection counts as
445
+ * one over TLS
446
+ * @return {MemorySocket} the client end, write commands to it and read the responses from its `data` events
447
+ */
448
+ connect(options) {
449
+ const [serverSocket, clientSocket] = (0, memory_socket_js_1.createSocketPair)();
450
+ serverSocket.encrypted = clientSocket.encrypted = !options || options.secure !== false;
451
+ this.createClient(serverSocket);
452
+ return clientSocket;
453
+ }
403
454
  registerCapability(keyword, handler) {
404
455
  this.capabilities[keyword] =
405
456
  handler ||
@@ -1136,6 +1187,8 @@ class IMAPServer extends node_stream_1.Stream {
1136
1187
  break;
1137
1188
  }
1138
1189
  }
1190
+ // like expunge, before the sessions are told: APPEND, COPY, MOVE, REPLACE, SMTP and the control API all add here
1191
+ this.emit('append', mailbox, [message], this.activeConnection);
1139
1192
  this.notify({
1140
1193
  tag: '*',
1141
1194
  attributes: [
@@ -1438,7 +1491,8 @@ class IMAPConnection {
1438
1491
  this._outputQueue = null;
1439
1492
  this._outputTimer = null;
1440
1493
  this._deferredOutput = null;
1441
- this.secureConnection = !!this.options.secureConnection;
1494
+ // a TLS socket is encrypted, and so can be an in-memory connection of server.connect()
1495
+ this.secureConnection = !!this.options.secureConnection || socket.encrypted === true;
1442
1496
  this._remainder = '';
1443
1497
  this._command = '';
1444
1498
  this._literalRemaining = 0;
@@ -1456,7 +1510,7 @@ class IMAPConnection {
1456
1510
  this.messageGlobal = false;
1457
1511
  this._commandQueue = [];
1458
1512
  this._processing = false;
1459
- if (this.options.debug) {
1513
+ if (this.options.debug && typeof this.socket.pipe === 'function') {
1460
1514
  this.socket.pipe(process.stdout);
1461
1515
  }
1462
1516
  this.socket.on('data', this.receive.bind(this));
@@ -1579,6 +1633,7 @@ class IMAPConnection {
1579
1633
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
1580
1634
  */
1581
1635
  writeLayer(data, transport, callback) {
1636
+ this.trace('out', data);
1582
1637
  if (transport) {
1583
1638
  transport.write(data);
1584
1639
  if (callback) {
@@ -1589,6 +1644,17 @@ class IMAPConnection {
1589
1644
  this.writeRaw(data, callback);
1590
1645
  }
1591
1646
  }
1647
+ /**
1648
+ * Emits the protocol traffic of the session as `trace` events, as the IMAP layer sees it: above TLS and COMPRESS
1649
+ *
1650
+ * @param {String} direction "in" for client input, "out" for server output
1651
+ * @param {Buffer} data The octets
1652
+ */
1653
+ trace(direction, data) {
1654
+ if (this.server.listenerCount('trace')) {
1655
+ this.server.emit('trace', { session: this.sessionNumber, direction, data });
1656
+ }
1657
+ }
1592
1658
  /**
1593
1659
  * Sends output through the script rule that handles its event, if there is one
1594
1660
  *
@@ -1695,7 +1761,8 @@ class IMAPConnection {
1695
1761
  this.watchQuiet(quiet);
1696
1762
  }
1697
1763
  }, Math.max(0, next - (Date.now() - last)));
1698
- this._quietTimer.unref();
1764
+ // browsers have no unref
1765
+ this._quietTimer.unref?.();
1699
1766
  }
1700
1767
  /**
1701
1768
  * Handles data from the socket, through the transport layer if there is one
@@ -1898,6 +1965,9 @@ class IMAPConnection {
1898
1965
  if (this._resetting) {
1899
1966
  return;
1900
1967
  }
1968
+ if (chunk && chunk.length) {
1969
+ this.trace('in', chunk);
1970
+ }
1901
1971
  // everything in one read arrived before anything this read causes to be sent
1902
1972
  this._readCount = (this._readCount || 0) + 1;
1903
1973
  str = (chunk || '').toString('binary');
@@ -2365,6 +2435,16 @@ class IMAPConnection {
2365
2435
  // section 6.2.1). STARTTLS is refused when input is waiting (the noPipelining command option), this only
2366
2436
  // guards against plugins that upgrade the connection otherwise
2367
2437
  this.discardInput();
2438
+ // STARTTLS runs on a live connection
2439
+ const socket = this.socket;
2440
+ if (typeof socket.startTls === 'function') {
2441
+ // a socket without a TLS layer that can count as encrypted (MemorySocket of server.connect())
2442
+ return socket.startTls(() => {
2443
+ this.secureConnection = true;
2444
+ this.upgrading = false;
2445
+ callback();
2446
+ });
2447
+ }
2368
2448
  const secureContext = node_tls_1.default.createSecureContext(this.server.getCredentials());
2369
2449
  const socketOptions = {
2370
2450
  secureContext: secureContext,
@@ -2375,8 +2455,6 @@ class IMAPConnection {
2375
2455
  cb(null, secureContext);
2376
2456
  }
2377
2457
  };
2378
- // STARTTLS runs on a live connection
2379
- const socket = this.socket;
2380
2458
  // the RST of a script rule goes to the TCP socket under the TLS layer, see resetSocket. Node also has it as the
2381
2459
  // _parent of the TLS socket, Bun does not
2382
2460
  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?: {
@@ -622,7 +622,7 @@ class Control {
622
622
  }
623
623
  else {
624
624
  this.server.closeListeners(false);
625
- this.server.server.close(done);
625
+ this.server.closeImapListeners(done);
626
626
  }
627
627
  });
628
628
  }
@@ -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 & {