emailengine-app 2.61.5 → 2.62.1

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/data/google-crawlers.json +1 -1
  3. package/lib/account.js +20 -7
  4. package/lib/api-routes/account-routes.js +28 -5
  5. package/lib/api-routes/chat-routes.js +1 -1
  6. package/lib/api-routes/export-routes.js +316 -0
  7. package/lib/api-routes/message-routes.js +28 -23
  8. package/lib/api-routes/template-routes.js +28 -7
  9. package/lib/arf-detect.js +1 -1
  10. package/lib/autodetect-imap-settings.js +5 -5
  11. package/lib/consts.js +16 -0
  12. package/lib/db.js +3 -0
  13. package/lib/email-client/base-client.js +6 -4
  14. package/lib/email-client/gmail-client.js +205 -35
  15. package/lib/email-client/imap/mailbox.js +99 -8
  16. package/lib/email-client/imap/subconnection.js +5 -5
  17. package/lib/email-client/imap-client.js +76 -19
  18. package/lib/email-client/message-builder.js +3 -1
  19. package/lib/email-client/notification-handler.js +12 -9
  20. package/lib/email-client/outlook-client.js +364 -73
  21. package/lib/email-client/smtp-pool-manager.js +1 -1
  22. package/lib/export.js +528 -0
  23. package/lib/oauth/gmail.js +24 -16
  24. package/lib/oauth/mail-ru.js +26 -13
  25. package/lib/oauth/outlook.js +29 -19
  26. package/lib/oauth/pubsub/google.js +5 -0
  27. package/lib/routes-ui.js +268 -9
  28. package/lib/schemas.js +274 -81
  29. package/lib/stream-encrypt.js +263 -0
  30. package/lib/sub-script.js +2 -2
  31. package/lib/tools.js +194 -12
  32. package/lib/ui-routes/account-routes.js +23 -0
  33. package/lib/ui-routes/admin-config-routes.js +13 -6
  34. package/lib/ui-routes/admin-entities-routes.js +18 -0
  35. package/lib/webhooks.js +16 -20
  36. package/package.json +20 -20
  37. package/sbom.json +1 -1
  38. package/server.js +66 -7
  39. package/static/js/ace/ace.js +1 -1
  40. package/static/js/ace/ext-language_tools.js +1 -1
  41. package/static/licenses.html +118 -149
  42. package/translations/de.mo +0 -0
  43. package/translations/de.po +63 -36
  44. package/translations/en.mo +0 -0
  45. package/translations/en.po +64 -37
  46. package/translations/et.mo +0 -0
  47. package/translations/et.po +63 -36
  48. package/translations/fr.mo +0 -0
  49. package/translations/fr.po +63 -36
  50. package/translations/ja.mo +0 -0
  51. package/translations/ja.po +63 -36
  52. package/translations/messages.pot +84 -51
  53. package/translations/nl.mo +0 -0
  54. package/translations/nl.po +63 -36
  55. package/translations/pl.mo +0 -0
  56. package/translations/pl.po +63 -36
  57. package/views/accounts/account.hbs +375 -2
  58. package/views/config/network.hbs +45 -0
  59. package/views/config/service.hbs +35 -0
  60. package/workers/api.js +130 -47
  61. package/workers/documents.js +3 -2
  62. package/workers/export.js +933 -0
  63. package/workers/imap.js +34 -1
  64. package/workers/submit.js +33 -6
  65. package/workers/webhooks.js +20 -4
@@ -288,7 +288,7 @@ class IMAPClient extends BaseClient {
288
288
 
289
289
  // Set up error handling for the command connection
290
290
  const onErr = err => {
291
- commandClient?.log.error({ msg: 'IMAP connection error', cid: commandCid, channel: 'command', account: this.account, err });
291
+ commandClient?.log.warn({ msg: 'IMAP connection error', cid: commandCid, channel: 'command', account: this.account, err });
292
292
  commandClient.close();
293
293
  this.commandClient = null;
294
294
  };
@@ -301,7 +301,7 @@ class IMAPClient extends BaseClient {
301
301
  if (this.connections.delete(commandClient)) {
302
302
  await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
303
303
  }
304
- commandClient.log.error({ msg: 'Failed to connect command client', cid: commandCid, channel: 'command', account: this.account, err });
304
+ commandClient.log.warn({ msg: 'Failed to connect command client', cid: commandCid, channel: 'command', account: this.account, err });
305
305
  throw err;
306
306
  }
307
307
 
@@ -371,8 +371,47 @@ class IMAPClient extends BaseClient {
371
371
  }
372
372
 
373
373
  // Get stored mailbox status including UID validity
374
- const storedStatus = await mailbox.getStoredStatus();
374
+ let storedStatus = await mailbox.getStoredStatus();
375
+
376
+ // If stored status missing uidValidity, try live IMAP mailbox status
377
+ if (!validUidValidity(storedStatus.uidValidity)) {
378
+ try {
379
+ const liveStatus = mailbox.getMailboxStatus();
380
+ if (validUidValidity(liveStatus.uidValidity)) {
381
+ this.logger.debug({
382
+ msg: 'packUid using live mailbox status (stored uidValidity missing)',
383
+ path: mailbox.path,
384
+ liveUidValidity: liveStatus.uidValidity.toString()
385
+ });
386
+ storedStatus = {
387
+ ...storedStatus,
388
+ uidValidity: liveStatus.uidValidity,
389
+ path: liveStatus.path || mailbox.path
390
+ };
391
+ }
392
+ } catch (err) {
393
+ // Live status not available (mailbox not selected)
394
+ this.logger.debug({
395
+ msg: 'packUid live status fallback unavailable',
396
+ path: mailbox.path,
397
+ error: err.message
398
+ });
399
+ }
400
+ }
401
+
402
+ // Final validation
375
403
  if (!validUidValidity(storedStatus.uidValidity) || !storedStatus.path) {
404
+ this.logger.warn({
405
+ msg: 'packUid failed due to invalid stored status',
406
+ uid,
407
+ mailboxPath: mailbox.path,
408
+ storedUidValidity: storedStatus.uidValidity,
409
+ storedUidValidityType: typeof storedStatus.uidValidity,
410
+ storedPath: storedStatus.path,
411
+ storedUidNext: storedStatus.uidNext,
412
+ storedMessages: storedStatus.messages,
413
+ syncing: mailbox.syncing || false
414
+ });
376
415
  return false;
377
416
  }
378
417
 
@@ -481,7 +520,6 @@ class IMAPClient extends BaseClient {
481
520
  }
482
521
 
483
522
  await mailbox.clear();
484
- mailbox = false;
485
523
  }
486
524
 
487
525
  /**
@@ -498,7 +536,9 @@ class IMAPClient extends BaseClient {
498
536
 
499
537
  const connectionClient = await this.getImapConnection(connectionOptions, 'getCurrentListing');
500
538
  if (!connectionClient) {
501
- this.imapClient.close();
539
+ if (this.imapClient) {
540
+ this.imapClient.close();
541
+ }
502
542
  let error = new Error('Failed to get connection');
503
543
  error.code = 'ConnectionError';
504
544
  throw error;
@@ -522,7 +562,9 @@ class IMAPClient extends BaseClient {
522
562
  let listing = await connectionClient.list(options);
523
563
  if (!listing.length) {
524
564
  // server bug, the list can never be empty
525
- this.imapClient.close();
565
+ if (this.imapClient) {
566
+ this.imapClient.close();
567
+ }
526
568
  let error = new Error('Server bug: empty mailbox listing');
527
569
  error.code = 'ServerBug';
528
570
  throw error;
@@ -879,7 +921,7 @@ class IMAPClient extends BaseClient {
879
921
  }
880
922
  } catch (err) {
881
923
  // ended in an unconncted state
882
- this.logger.error({ msg: 'Failed to set up connection, will retry', err });
924
+ this.logger.warn({ msg: 'Failed to set up connection, will retry', err });
883
925
 
884
926
  // Calculate delay with capped exponential backoff
885
927
  const retryDelay = Math.min(this.reconnectMaxDelay, 1000 * Math.pow(1.5, Math.min(this.reconnectRetries, 10)));
@@ -898,7 +940,7 @@ class IMAPClient extends BaseClient {
898
940
  this.reconnectRetries = 0;
899
941
  })
900
942
  .catch(err => {
901
- this.logger.error({
943
+ this.logger.warn({
902
944
  msg: 'Connection retry failed',
903
945
  err,
904
946
  attempt: this.reconnectRetries
@@ -935,6 +977,10 @@ class IMAPClient extends BaseClient {
935
977
 
936
978
  // Sync existing folders that weren't just synced
937
979
  for (let mailbox of this.mailboxes.values()) {
980
+ if (!this.imapClient || !this.imapClient.usable) {
981
+ this.logger.debug({ msg: 'IMAP client disconnected during mailbox sync' });
982
+ return;
983
+ }
938
984
  if (!synced || !synced.has(mailbox)) {
939
985
  await mailbox.sync();
940
986
  }
@@ -949,12 +995,19 @@ class IMAPClient extends BaseClient {
949
995
  this.state = 'connected';
950
996
  await this.setStateVal();
951
997
 
998
+ // Capture local reference to avoid race conditions during async operations
999
+ const imapClient = this.imapClient;
1000
+ if (!imapClient || !imapClient.usable) {
1001
+ this.logger.debug({ msg: 'IMAP client disconnected or not usable after state update' });
1002
+ return;
1003
+ }
1004
+
952
1005
  // Store IMAP server capabilities for reference
953
- const capabilities = (this.imapClient.rawCapabilities || []).map(entry => entry && entry.value).filter(entry => entry);
1006
+ const capabilities = (imapClient.rawCapabilities || []).map(entry => entry && entry.value).filter(entry => entry);
954
1007
  const authCapabilities = [];
955
1008
  let lastUsedAuthCapability = null;
956
- if (this.imapClient.authCapabilities) {
957
- for (let [authCapa, usedAuth] of this.imapClient.authCapabilities) {
1009
+ if (imapClient.authCapabilities) {
1010
+ for (let [authCapa, usedAuth] of imapClient.authCapabilities) {
958
1011
  authCapabilities.push(authCapa);
959
1012
  if (usedAuth) {
960
1013
  lastUsedAuthCapability = authCapa;
@@ -962,7 +1015,7 @@ class IMAPClient extends BaseClient {
962
1015
  }
963
1016
  }
964
1017
 
965
- const serverInfo = Object.assign({}, this.imapClient.serverInfo || {}, {
1018
+ const serverInfo = Object.assign({}, imapClient.serverInfo || {}, {
966
1019
  capabilities,
967
1020
  authCapabilities,
968
1021
  lastUsedAuthCapability
@@ -980,6 +1033,12 @@ class IMAPClient extends BaseClient {
980
1033
  return;
981
1034
  }
982
1035
 
1036
+ // Check if client disconnected before mailbox selection
1037
+ if (!this.imapClient || !this.imapClient.usable) {
1038
+ this.logger.debug({ msg: 'IMAP client disconnected before mailbox selection' });
1039
+ return;
1040
+ }
1041
+
983
1042
  this.logger.debug({ msg: 'Syncing completed, selecting main path', path: mainPath });
984
1043
  // start waiting for changes
985
1044
  await this.select(mainPath);
@@ -1195,7 +1254,7 @@ class IMAPClient extends BaseClient {
1195
1254
  prevImapClient.removeAllListeners();
1196
1255
 
1197
1256
  const prevImapErrorHandler = err => {
1198
- this.logger.error({ msg: 'IMAP connection error', type: 'imapClient', previous: true, account: this.account, err });
1257
+ this.logger.warn({ msg: 'IMAP connection error', type: 'imapClient', previous: true, account: this.account, err });
1199
1258
  };
1200
1259
 
1201
1260
  prevImapClient.once('error', prevImapErrorHandler);
@@ -1207,12 +1266,11 @@ class IMAPClient extends BaseClient {
1207
1266
  this.commandClient.close();
1208
1267
  }
1209
1268
  } catch (err) {
1210
- this.logger.error({ msg: 'IMAP close error', err });
1269
+ this.logger.warn({ msg: 'IMAP close error', err });
1211
1270
  } finally {
1212
1271
  if (prevImapClient === this.imapClient) {
1213
1272
  this.imapClient = null;
1214
1273
  }
1215
- prevImapClient = null;
1216
1274
  }
1217
1275
  }
1218
1276
 
@@ -1267,7 +1325,7 @@ class IMAPClient extends BaseClient {
1267
1325
 
1268
1326
  // Handle connection errors
1269
1327
  imapClient.on('error', err => {
1270
- imapClient?.log.error({ msg: 'IMAP connection error', type: 'imapClient', account: this.account, err });
1328
+ imapClient?.log.warn({ msg: 'IMAP connection error', type: 'imapClient', account: this.account, err });
1271
1329
  if (imapClient !== this.imapClient || this._connecting) {
1272
1330
  return;
1273
1331
  }
@@ -1291,7 +1349,7 @@ class IMAPClient extends BaseClient {
1291
1349
  this.errorReconnectDelay = 2000;
1292
1350
  })
1293
1351
  .catch(err => {
1294
- this.logger.error({
1352
+ this.logger.warn({
1295
1353
  msg: 'IMAP reconnection error',
1296
1354
  account: this.account,
1297
1355
  err,
@@ -2039,7 +2097,6 @@ class IMAPClient extends BaseClient {
2039
2097
  if (this.mailboxes.has(normalizePath(path))) {
2040
2098
  let mailbox = this.mailboxes.get(normalizePath(path));
2041
2099
  await mailbox.clear();
2042
- mailbox = false;
2043
2100
  }
2044
2101
 
2045
2102
  return result;
@@ -2303,7 +2360,7 @@ class IMAPClient extends BaseClient {
2303
2360
  let uploadResponse = await connectionClient.append(data.path, raw, data.flags, data.internalDate);
2304
2361
 
2305
2362
  // Return to IDLE if using primary connection
2306
- if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
2363
+ if (this.imapClient && connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
2307
2364
  // force back to IDLE
2308
2365
  this.imapClient.idle().catch(err => {
2309
2366
  this.logger.error({ msg: 'IDLE error', err });
@@ -467,7 +467,9 @@ const SMTP_ERROR_DESCRIPTIONS = {
467
467
  EDNS: settings => `EmailEngine failed to resolve DNS record for ${settings.host}`,
468
468
  ECONNECTION: settings => `EmailEngine failed to establish TCP connection against ${settings.host}`,
469
469
  EPROTOCOL: settings => `Unexpected response from ${settings.host}`,
470
- EAUTH: () => 'Authentication failed'
470
+ EAUTH: () => 'Authentication failed',
471
+ ENOAUTH: () => 'Authentication credentials were not provided',
472
+ EOAUTH2: () => 'OAuth2 token generation or refresh failed'
471
473
  };
472
474
 
473
475
  /**
@@ -19,12 +19,13 @@ const DOCUMENT_SYNC_EVENTS = [MESSAGE_NEW_NOTIFY, MESSAGE_DELETED_NOTIFY, MESSAG
19
19
  * Default job options for notification queue
20
20
  */
21
21
  const DEFAULT_JOB_OPTIONS = {
22
- removeOnComplete: true,
23
- removeOnFail: true,
22
+ removeOnComplete: { age: 24 * 3600, count: 1000 },
23
+ removeOnFail: { age: 24 * 3600, count: 1000 },
24
24
  attempts: 10,
25
25
  backoff: {
26
26
  type: 'exponential',
27
- delay: 5000
27
+ delay: 5000,
28
+ jitter: 0.2 // 20% randomization to prevent thundering herd
28
29
  }
29
30
  };
30
31
 
@@ -236,18 +237,20 @@ class NotificationHandler {
236
237
 
237
238
  /**
238
239
  * Builds job options based on queue keep setting
239
- * @param {boolean} queueKeep - Whether to keep completed/failed jobs
240
+ * @param {boolean|number} queueKeep - Whether/how many jobs to keep
240
241
  * @returns {Object} Job options for notify and documents queues
241
242
  */
242
243
  buildJobOptions(queueKeep) {
244
+ const retention = typeof queueKeep === 'number' ? { age: 24 * 3600, count: queueKeep } : queueKeep;
245
+
243
246
  const notifyOptions = Object.assign({}, DEFAULT_JOB_OPTIONS, {
244
- removeOnComplete: queueKeep,
245
- removeOnFail: queueKeep
247
+ removeOnComplete: retention,
248
+ removeOnFail: retention
246
249
  });
247
250
 
248
251
  const documentOptions = Object.assign({}, DOCUMENT_JOB_OPTIONS, {
249
- removeOnComplete: queueKeep,
250
- removeOnFail: queueKeep
252
+ removeOnComplete: retention,
253
+ removeOnFail: retention
251
254
  });
252
255
 
253
256
  return {
@@ -289,7 +292,7 @@ class NotificationHandler {
289
292
  }
290
293
 
291
294
  // Get queue retention setting
292
- const queueKeep = (await settings.get('queueKeep')) || true;
295
+ const queueKeep = (await settings.get('queueKeep')) ?? true;
293
296
 
294
297
  // Process notification based on required destinations
295
298
  if (!skipWebhook && addDocumentQueueJob) {