imapkit 5.1.0 → 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
  }
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
  }
@@ -83,6 +83,8 @@ declare class IMAPServer extends Stream {
83
83
  sessionCounter: number;
84
84
  /** the control API, see src/control.ts */
85
85
  control: Control;
86
+ /** the implicit TLS listener that `start()` runs for the `tls` option */
87
+ tlsServer: tls.Server | null;
86
88
  /** the SMTP server that `start()` runs for the `smtp` option */
87
89
  smtpServer: SMTPListener | null;
88
90
  /** the HTTP server of the REST API that `start()` runs for the `rest` option */
@@ -107,6 +109,14 @@ declare class IMAPServer extends Stream {
107
109
  * @return {Promise<Number>} resolves with the port the server listens on
108
110
  */
109
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>;
110
120
  /**
111
121
  * Starts the REST API of the `rest` option, see src/rest.ts
112
122
  *
@@ -140,6 +150,13 @@ declare class IMAPServer extends Stream {
140
150
  listen(port: number, hostname: string, listeningListener?: () => void): void;
141
151
  listen(options: net.ListenOptions, listeningListener?: () => void): void;
142
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;
143
160
  /**
144
161
  * Stops the SMTP and REST listeners of `start()`
145
162
  *
@@ -677,6 +694,13 @@ declare class IMAPConnection {
677
694
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
678
695
  */
679
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;
680
704
  /**
681
705
  * Sends output through the script rule that handles its event, if there is one
682
706
  *
@@ -242,6 +242,7 @@ class IMAPServer extends node_stream_1.Stream {
242
242
  }
243
243
  this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
244
244
  this.loadStorage();
245
+ this.tlsServer = null;
245
246
  this.smtpServer = null;
246
247
  this.restServer = null;
247
248
  }
@@ -292,9 +293,25 @@ class IMAPServer extends node_stream_1.Stream {
292
293
  */
293
294
  async start(port, host) {
294
295
  const imapPort = await listenOn(this.server, port, host);
295
- 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
+ ]);
296
301
  return imapPort;
297
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
+ }
298
315
  /**
299
316
  * Starts the REST API of the `rest` option, see src/rest.ts
300
317
  *
@@ -344,7 +361,7 @@ class IMAPServer extends node_stream_1.Stream {
344
361
  }
345
362
  close(callback) {
346
363
  this.closeListeners(true);
347
- this.server.close(callback);
364
+ this.closeImapListeners(callback);
348
365
  // close() only completes once all connections are gone
349
366
  this.connections.forEach((connection) => {
350
367
  if (connection.socket) {
@@ -352,6 +369,24 @@ class IMAPServer extends node_stream_1.Stream {
352
369
  }
353
370
  });
354
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
+ }
355
390
  /**
356
391
  * Stops the SMTP and REST listeners of `start()`
357
392
  *
@@ -1152,6 +1187,8 @@ class IMAPServer extends node_stream_1.Stream {
1152
1187
  break;
1153
1188
  }
1154
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);
1155
1192
  this.notify({
1156
1193
  tag: '*',
1157
1194
  attributes: [
@@ -1596,6 +1633,7 @@ class IMAPConnection {
1596
1633
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
1597
1634
  */
1598
1635
  writeLayer(data, transport, callback) {
1636
+ this.trace('out', data);
1599
1637
  if (transport) {
1600
1638
  transport.write(data);
1601
1639
  if (callback) {
@@ -1606,6 +1644,17 @@ class IMAPConnection {
1606
1644
  this.writeRaw(data, callback);
1607
1645
  }
1608
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
+ }
1609
1658
  /**
1610
1659
  * Sends output through the script rule that handles its event, if there is one
1611
1660
  *
@@ -1916,6 +1965,9 @@ class IMAPConnection {
1916
1965
  if (this._resetting) {
1917
1966
  return;
1918
1967
  }
1968
+ if (chunk && chunk.length) {
1969
+ this.trace('in', chunk);
1970
+ }
1919
1971
  // everything in one read arrived before anything this read causes to be sent
1920
1972
  this._readCount = (this._readCount || 0) + 1;
1921
1973
  str = (chunk || '').toString('binary');
@@ -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
  }
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
  }
@@ -83,6 +83,8 @@ declare class IMAPServer extends Stream {
83
83
  sessionCounter: number;
84
84
  /** the control API, see src/control.ts */
85
85
  control: Control;
86
+ /** the implicit TLS listener that `start()` runs for the `tls` option */
87
+ tlsServer: tls.Server | null;
86
88
  /** the SMTP server that `start()` runs for the `smtp` option */
87
89
  smtpServer: SMTPListener | null;
88
90
  /** the HTTP server of the REST API that `start()` runs for the `rest` option */
@@ -107,6 +109,14 @@ declare class IMAPServer extends Stream {
107
109
  * @return {Promise<Number>} resolves with the port the server listens on
108
110
  */
109
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>;
110
120
  /**
111
121
  * Starts the REST API of the `rest` option, see src/rest.ts
112
122
  *
@@ -140,6 +150,13 @@ declare class IMAPServer extends Stream {
140
150
  listen(port: number, hostname: string, listeningListener?: () => void): void;
141
151
  listen(options: net.ListenOptions, listeningListener?: () => void): void;
142
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;
143
160
  /**
144
161
  * Stops the SMTP and REST listeners of `start()`
145
162
  *
@@ -677,6 +694,13 @@ declare class IMAPConnection {
677
694
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
678
695
  */
679
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;
680
704
  /**
681
705
  * Sends output through the script rule that handles its event, if there is one
682
706
  *
@@ -201,6 +201,7 @@ class IMAPServer extends Stream {
201
201
  }
202
202
  this.systemFlags = [].concat(this.options.systemFlags || ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']);
203
203
  this.loadStorage();
204
+ this.tlsServer = null;
204
205
  this.smtpServer = null;
205
206
  this.restServer = null;
206
207
  }
@@ -251,9 +252,25 @@ class IMAPServer extends Stream {
251
252
  */
252
253
  async start(port, host) {
253
254
  const imapPort = await listenOn(this.server, port, host);
254
- 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
+ ]);
255
260
  return imapPort;
256
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
+ }
257
274
  /**
258
275
  * Starts the REST API of the `rest` option, see src/rest.ts
259
276
  *
@@ -303,7 +320,7 @@ class IMAPServer extends Stream {
303
320
  }
304
321
  close(callback) {
305
322
  this.closeListeners(true);
306
- this.server.close(callback);
323
+ this.closeImapListeners(callback);
307
324
  // close() only completes once all connections are gone
308
325
  this.connections.forEach((connection) => {
309
326
  if (connection.socket) {
@@ -311,6 +328,24 @@ class IMAPServer extends Stream {
311
328
  }
312
329
  });
313
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
+ }
314
349
  /**
315
350
  * Stops the SMTP and REST listeners of `start()`
316
351
  *
@@ -1111,6 +1146,8 @@ class IMAPServer extends Stream {
1111
1146
  break;
1112
1147
  }
1113
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);
1114
1151
  this.notify({
1115
1152
  tag: '*',
1116
1153
  attributes: [
@@ -1554,6 +1591,7 @@ class IMAPConnection {
1554
1591
  * @param {Function} [callback] Called once the data was handed to the system (a transport: on the next turn)
1555
1592
  */
1556
1593
  writeLayer(data, transport, callback) {
1594
+ this.trace('out', data);
1557
1595
  if (transport) {
1558
1596
  transport.write(data);
1559
1597
  if (callback) {
@@ -1564,6 +1602,17 @@ class IMAPConnection {
1564
1602
  this.writeRaw(data, callback);
1565
1603
  }
1566
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
+ }
1567
1616
  /**
1568
1617
  * Sends output through the script rule that handles its event, if there is one
1569
1618
  *
@@ -1874,6 +1923,9 @@ class IMAPConnection {
1874
1923
  if (this._resetting) {
1875
1924
  return;
1876
1925
  }
1926
+ if (chunk && chunk.length) {
1927
+ this.trace('in', chunk);
1928
+ }
1877
1929
  // everything in one read arrived before anything this read causes to be sent
1878
1930
  this._readCount = (this._readCount || 0) + 1;
1879
1931
  str = (chunk || '').toString('binary');
@@ -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.1.0",
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",
@@ -95,16 +95,16 @@
95
95
  "@types/smtp-server": "3.5.13",
96
96
  "buffer": "6.0.3",
97
97
  "esbuild": "0.28.2",
98
- "eslint": "10.12.0",
98
+ "eslint": "10.13.0",
99
99
  "eslint-config-prettier": "10.1.8",
100
100
  "events": "3.3.0",
101
101
  "globals": "17.13.0",
102
- "imapflow": "2.2.9",
103
- "prettier": "3.9.9",
104
- "smtp-server": "3.19.17",
102
+ "imapflow": "2.5.6",
103
+ "prettier": "3.9.10",
104
+ "smtp-server": "3.20.2",
105
105
  "tsx": "4.23.15",
106
106
  "typescript": "6.0.3",
107
- "typescript-eslint": "8.71.1"
107
+ "typescript-eslint": "8.71.2"
108
108
  },
109
109
  "engines": {
110
110
  "node": ">=20.0.0"