emailengine-app 2.69.0 → 2.71.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.
Files changed (97) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/.github/workflows/test.yml +73 -12
  4. package/.ncurc.js +3 -3
  5. package/CHANGELOG.md +37 -0
  6. package/Gruntfile.js +21 -23
  7. package/bin/emailengine.js +8 -1
  8. package/config/default.toml +5 -0
  9. package/config/test.toml +5 -0
  10. package/data/google-crawlers.json +1 -1
  11. package/getswagger.sh +44 -4
  12. package/gettext-extract.js +163 -0
  13. package/lib/account.js +104 -72
  14. package/lib/api-routes/account-routes.js +231 -71
  15. package/lib/api-routes/blocklist-routes.js +25 -18
  16. package/lib/api-routes/chat-routes.js +32 -14
  17. package/lib/api-routes/delivery-test-routes.js +30 -5
  18. package/lib/api-routes/export-routes.js +27 -2
  19. package/lib/api-routes/gateway-routes.js +63 -12
  20. package/lib/api-routes/license-routes.js +18 -4
  21. package/lib/api-routes/mailbox-routes.js +33 -7
  22. package/lib/api-routes/message-routes.js +291 -145
  23. package/lib/api-routes/oauth2-app-routes.js +90 -24
  24. package/lib/api-routes/outbox-routes.js +16 -4
  25. package/lib/api-routes/pubsub-routes.js +8 -4
  26. package/lib/api-routes/route-helpers.js +14 -1
  27. package/lib/api-routes/settings-routes.js +51 -25
  28. package/lib/api-routes/stats-routes.js +37 -3
  29. package/lib/api-routes/submit-routes.js +31 -42
  30. package/lib/api-routes/template-routes.js +54 -21
  31. package/lib/api-routes/token-routes.js +67 -67
  32. package/lib/api-routes/webhook-route-routes.js +37 -8
  33. package/lib/autodetect-imap-settings.js +0 -2
  34. package/lib/consts.js +5 -0
  35. package/lib/document-store.js +22 -1
  36. package/lib/email-client/base-client.js +31 -8
  37. package/lib/email-client/gmail-client.js +119 -112
  38. package/lib/email-client/imap/mailbox.js +2 -2
  39. package/lib/email-client/imap/subconnection.js +0 -1
  40. package/lib/email-client/imap/sync-operations.js +1 -1
  41. package/lib/email-client/imap-client.js +36 -17
  42. package/lib/email-client/notification-handler.js +3 -6
  43. package/lib/email-client/outlook-client.js +49 -62
  44. package/lib/export.js +49 -1
  45. package/lib/feature-flags.js +8 -2
  46. package/lib/gateway.js +4 -9
  47. package/lib/get-raw-email.js +5 -5
  48. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  49. package/lib/license-beacon.js +367 -0
  50. package/lib/logger.js +35 -22
  51. package/lib/metrics-collector.js +0 -2
  52. package/lib/oauth2-apps.js +13 -4
  53. package/lib/outbox.js +24 -40
  54. package/lib/redis-operations.js +1 -1
  55. package/lib/routes-ui.js +2 -1
  56. package/lib/schemas.js +403 -83
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +28 -6
  63. package/lib/ui-routes/account-routes.js +7 -4
  64. package/lib/ui-routes/admin-config-routes.js +20 -6
  65. package/lib/ui-routes/document-store-routes.js +7 -1
  66. package/lib/ui-routes/oauth-config-routes.js +0 -2
  67. package/lib/ui-routes/route-helpers.js +0 -2
  68. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  69. package/lib/webhooks.js +8 -4
  70. package/package.json +23 -19
  71. package/sbom.json +1 -1
  72. package/server.js +38 -31
  73. package/static/licenses.html +171 -391
  74. package/translations/de.mo +0 -0
  75. package/translations/de.po +154 -142
  76. package/translations/et.mo +0 -0
  77. package/translations/et.po +129 -131
  78. package/translations/fr.mo +0 -0
  79. package/translations/fr.po +133 -136
  80. package/translations/ja.mo +0 -0
  81. package/translations/ja.po +126 -129
  82. package/translations/messages.pot +107 -107
  83. package/translations/nl.mo +0 -0
  84. package/translations/nl.po +128 -130
  85. package/translations/pl.mo +0 -0
  86. package/translations/pl.po +125 -128
  87. package/update-info.sh +19 -1
  88. package/views/config/logging.hbs +48 -0
  89. package/views/dashboard.hbs +22 -0
  90. package/workers/api.js +33 -37
  91. package/workers/documents.js +2 -22
  92. package/workers/export.js +73 -92
  93. package/workers/imap-proxy.js +3 -23
  94. package/workers/imap.js +2 -22
  95. package/workers/smtp.js +2 -22
  96. package/workers/submit.js +6 -24
  97. package/workers/webhooks.js +2 -22
@@ -46,7 +46,7 @@ const SYSTEM_LABELS = {
46
46
 
47
47
  // User-friendly names for system labels
48
48
  const SYSTEM_NAMES = {
49
- SENT: 'Sent ',
49
+ SENT: 'Sent',
50
50
  INBOX: 'Inbox',
51
51
  TRASH: 'Trash',
52
52
  DRAFT: 'Drafts',
@@ -63,6 +63,12 @@ for (let label of Object.keys(SYSTEM_LABELS)) {
63
63
  SYSTEM_LABELS_REV[SYSTEM_LABELS[label]] = label;
64
64
  }
65
65
 
66
+ // Convert an IMAP special-use label (e.g. '\Important') to its Gmail system label ID, pass every
67
+ // other value through as-is
68
+ function toGmailLabelId(label) {
69
+ return SYSTEM_LABELS_REV.hasOwnProperty(label) ? SYSTEM_LABELS_REV[label] : label;
70
+ }
71
+
66
72
  // Timing constants for Gmail Pub/Sub watch
67
73
  const RENEW_WATCH_TTL = 60 * 60 * 1000; // 1h - how often to check if watch needs renewal
68
74
  const MIN_WATCH_TTL = 24 * 3600 * 1000; // 1day - minimum time before renewing watch
@@ -753,7 +759,7 @@ class GmailClient extends BaseClient {
753
759
  // NB! Might throw if using unsupported search terms
754
760
  const preparedQuery = this.prepareQuery(query.search);
755
761
  if (preparedQuery) {
756
- requestQuery.q = this.prepareQuery(query.search);
762
+ requestQuery.q = preparedQuery;
757
763
  }
758
764
  }
759
765
 
@@ -875,16 +881,13 @@ class GmailClient extends BaseClient {
875
881
 
876
882
  let sourceLabel = path && path !== '\\All' ? await this.getLabel(path) : null;
877
883
  if (path && path !== '\\All' && !sourceLabel) {
878
- let error = new Error('Unknown path');
879
- error.info = {
880
- response: `Mailbox doesn't exist: ${path}`
881
- };
882
- error.code = 'NotFound';
883
- error.statusCode = 404;
884
- throw error;
884
+ throw this.unknownPathError(path);
885
885
  }
886
886
 
887
- // Add TRASH label and remove source label
887
+ // Add TRASH label and remove source label. When deleting from the Trash folder itself both
888
+ // labels are TRASH - updateMessages resolves the add/remove conflict in favor of the add,
889
+ // keeping the request valid (Gmail API accounts only hold the gmail.modify scope, so a
890
+ // permanent batchDelete is not possible)
888
891
  let labelsUpdate = { add: 'TRASH' };
889
892
  if (sourceLabel) {
890
893
  labelsUpdate.delete = sourceLabel.id;
@@ -911,51 +914,28 @@ class GmailClient extends BaseClient {
911
914
  await this.prepare();
912
915
  updates = updates || {};
913
916
 
914
- let addLabelIds = new Set();
915
- let removeLabelIds = new Set();
916
-
917
917
  // Convert IMAP flags to Gmail labels
918
- if (updates.flags) {
919
- let labelUpdates = [];
920
-
921
- for (let flag of [].concat(updates.flags.add || [])) {
922
- labelUpdates.push(this.flagToLabel(flag));
923
- }
924
-
925
- for (let flag of [].concat(updates.flags.delete || [])) {
926
- labelUpdates.push(this.flagToLabel(flag, true));
927
- }
928
-
929
- labelUpdates
930
- .filter(label => label)
931
- .forEach(label => {
932
- if (label.add) {
933
- addLabelIds.add(label.add);
934
- }
935
- if (label.remove) {
936
- removeLabelIds.add(label.remove);
937
- }
938
- });
939
- }
918
+ let { addLabelIds, removeLabelIds } = this.flagsToLabelIds(updates.flags);
940
919
 
941
920
  // Process direct label updates
942
921
  if (updates.labels) {
922
+ this.assertLabelSetSupported(updates.labels);
923
+
943
924
  for (let label of [].concat(updates.labels.add || [])) {
944
- // Convert IMAP special-use to Gmail label
945
- if (SYSTEM_LABELS_REV.hasOwnProperty(label)) {
946
- label = SYSTEM_LABELS_REV[label];
947
- }
948
- addLabelIds.add(label);
925
+ addLabelIds.add(toGmailLabelId(label));
949
926
  }
950
927
 
951
928
  for (let label of [].concat(updates.labels.delete || [])) {
952
- if (SYSTEM_LABELS_REV.hasOwnProperty(label)) {
953
- label = SYSTEM_LABELS_REV[label];
954
- }
955
- removeLabelIds.add(label);
929
+ removeLabelIds.add(toGmailLabelId(label));
956
930
  }
957
931
  }
958
932
 
933
+ // Gmail rejects modify calls where the same label is both added and removed (deleting or
934
+ // moving a message within its current folder re-adds the source label) - the add wins
935
+ for (let label of addLabelIds) {
936
+ removeLabelIds.delete(label);
937
+ }
938
+
959
939
  if (!addLabelIds.size && !removeLabelIds.size) {
960
940
  return updates;
961
941
  }
@@ -1024,6 +1004,17 @@ class GmailClient extends BaseClient {
1024
1004
  await this.prepare();
1025
1005
  updates = updates || {};
1026
1006
 
1007
+ // Reject unsupported operations before any API calls are made
1008
+ if (updates.labels) {
1009
+ this.assertLabelSetSupported(updates.labels);
1010
+ }
1011
+
1012
+ path = [].concat(path || []).join('/');
1013
+
1014
+ if (path && path !== '\\All' && !(await this.getLabel(path))) {
1015
+ throw this.unknownPathError(path);
1016
+ }
1017
+
1027
1018
  // Step 1. Resolve matching messages
1028
1019
  let messages = [];
1029
1020
  let cursor;
@@ -1043,8 +1034,8 @@ class GmailClient extends BaseClient {
1043
1034
  { metadataOnly: true }
1044
1035
  );
1045
1036
 
1046
- if (messageListResult?.messages) {
1047
- messages = messages.concat(messageListResult?.messages);
1037
+ if (messageListResult.messages) {
1038
+ messages = messages.concat(messageListResult.messages);
1048
1039
  if (messages.length >= maxMessages) {
1049
1040
  messages = messages.slice(0, maxMessages);
1050
1041
  break;
@@ -1064,43 +1055,25 @@ class GmailClient extends BaseClient {
1064
1055
  return updates;
1065
1056
  }
1066
1057
 
1067
- let addLabelIds = new Set();
1068
- let removeLabelIds = new Set();
1069
-
1070
1058
  // Convert flags to label operations
1071
- if (updates.flags) {
1072
- let labelUpdates = [];
1073
-
1074
- for (let flag of [].concat(updates.flags.add || [])) {
1075
- labelUpdates.push(this.flagToLabel(flag));
1076
- }
1077
-
1078
- for (let flag of [].concat(updates.flags.delete || [])) {
1079
- labelUpdates.push(this.flagToLabel(flag, true));
1080
- }
1081
-
1082
- labelUpdates
1083
- .filter(label => label)
1084
- .forEach(label => {
1085
- if (label.add) {
1086
- addLabelIds.add(label.add);
1087
- }
1088
- if (label.remove) {
1089
- removeLabelIds.add(label.remove);
1090
- }
1091
- });
1092
- }
1059
+ let { addLabelIds, removeLabelIds } = this.flagsToLabelIds(updates.flags);
1093
1060
 
1094
1061
  if (updates.labels) {
1095
1062
  for (let label of [].concat(updates.labels.add || [])) {
1096
- addLabelIds.add(label);
1063
+ addLabelIds.add(toGmailLabelId(label));
1097
1064
  }
1098
1065
 
1099
1066
  for (let label of [].concat(updates.labels.delete || [])) {
1100
- removeLabelIds.add(label);
1067
+ removeLabelIds.add(toGmailLabelId(label));
1101
1068
  }
1102
1069
  }
1103
1070
 
1071
+ // Gmail rejects batchModify calls where the same label is both added and removed (deleting
1072
+ // or moving messages within their current folder re-adds the source label) - the add wins
1073
+ for (let label of addLabelIds) {
1074
+ removeLabelIds.delete(label);
1075
+ }
1076
+
1104
1077
  if (!addLabelIds.size && !removeLabelIds.size) {
1105
1078
  return { flags: {}, labels: {} };
1106
1079
  }
@@ -1141,13 +1114,7 @@ class GmailClient extends BaseClient {
1141
1114
 
1142
1115
  let label = await this.getLabel(path);
1143
1116
  if (!label) {
1144
- let error = new Error('Unknown path');
1145
- error.info = {
1146
- response: `Mailbox doesn't exist: ${path}`
1147
- };
1148
- error.code = 'NotFound';
1149
- error.statusCode = 404;
1150
- throw error;
1117
+ throw this.unknownPathError(path);
1151
1118
  }
1152
1119
  let labelsUpdate = { add: [label.id] };
1153
1120
 
@@ -1155,13 +1122,7 @@ class GmailClient extends BaseClient {
1155
1122
 
1156
1123
  let sourceLabel = sourcePath ? await this.getLabel(sourcePath) : null;
1157
1124
  if (sourcePath && !sourceLabel) {
1158
- let error = new Error('Unknown path');
1159
- error.info = {
1160
- response: `Mailbox doesn't exist: ${sourcePath}`
1161
- };
1162
- error.code = 'NotFound';
1163
- error.statusCode = 404;
1164
- throw error;
1125
+ throw this.unknownPathError(sourcePath);
1165
1126
  }
1166
1127
 
1167
1128
  if (sourceLabel) {
@@ -1190,24 +1151,12 @@ class GmailClient extends BaseClient {
1190
1151
 
1191
1152
  let targetLabel = await this.getLabel(path);
1192
1153
  if (!targetLabel) {
1193
- let error = new Error('Unknown path');
1194
- error.info = {
1195
- response: `Mailbox doesn't exist: ${path}`
1196
- };
1197
- error.code = 'NotFound';
1198
- error.statusCode = 404;
1199
- throw error;
1154
+ throw this.unknownPathError(path);
1200
1155
  }
1201
1156
 
1202
1157
  let sourceLabel = source ? await this.getLabel(source) : null;
1203
1158
  if (source && !sourceLabel) {
1204
- let error = new Error('Unknown path');
1205
- error.info = {
1206
- response: `Mailbox doesn't exist: ${source}`
1207
- };
1208
- error.code = 'NotFound';
1209
- error.statusCode = 404;
1210
- throw error;
1159
+ throw this.unknownPathError(source);
1211
1160
  }
1212
1161
 
1213
1162
  let labelsUpdate = { add: targetLabel.id };
@@ -1251,7 +1200,7 @@ class GmailClient extends BaseClient {
1251
1200
 
1252
1201
  const contentResponse = {
1253
1202
  headers: {
1254
- 'content-type': attachmentData.mimeType || 'application/octet-stream',
1203
+ 'content-type': attachmentData.contentType || 'application/octet-stream',
1255
1204
  'content-disposition': 'attachment' + filenameParam
1256
1205
  },
1257
1206
  contentType: attachmentData.contentType,
@@ -1421,7 +1370,7 @@ class GmailClient extends BaseClient {
1421
1370
 
1422
1371
  // Map part IDs to content types
1423
1372
  textParts[0].forEach(p => {
1424
- bodyParts.set(p, 'text');
1373
+ bodyParts.set(p, 'plain');
1425
1374
  });
1426
1375
 
1427
1376
  textParts[1].forEach(p => {
@@ -1481,13 +1430,7 @@ class GmailClient extends BaseClient {
1481
1430
 
1482
1431
  let targetLabel = await this.getLabel(path);
1483
1432
  if (!targetLabel) {
1484
- let error = new Error('Unknown path');
1485
- error.info = {
1486
- response: `Mailbox doesn't exist: ${path}`
1487
- };
1488
- error.code = 'NotFound';
1489
- error.statusCode = 404;
1490
- throw error;
1433
+ throw this.unknownPathError(path);
1491
1434
  }
1492
1435
 
1493
1436
  // Generate raw message
@@ -2400,7 +2343,7 @@ class GmailClient extends BaseClient {
2400
2343
  flags.push('\\Flagged');
2401
2344
  }
2402
2345
 
2403
- if (messageData.labelIds?.includes('DRAFTS')) {
2346
+ if (messageData.labelIds?.includes('DRAFT')) {
2404
2347
  flags.push('\\Draft');
2405
2348
  }
2406
2349
 
@@ -2609,6 +2552,21 @@ class GmailClient extends BaseClient {
2609
2552
  return term;
2610
2553
  }
2611
2554
 
2555
+ /**
2556
+ * Builds the canonical 404 error for a mailbox path that does not match any Gmail label
2557
+ * @param {string} path - Requested mailbox path
2558
+ * @returns {Error} Error object with code and statusCode set
2559
+ */
2560
+ unknownPathError(path) {
2561
+ let error = new Error('Unknown path');
2562
+ error.info = {
2563
+ response: `Mailbox doesn't exist: ${path}`
2564
+ };
2565
+ error.code = 'NotFound';
2566
+ error.statusCode = 404;
2567
+ return error;
2568
+ }
2569
+
2612
2570
  /**
2613
2571
  * Converts IMAP flags to Gmail label operations
2614
2572
  * @param {string} flag - IMAP flag
@@ -2625,6 +2583,55 @@ class GmailClient extends BaseClient {
2625
2583
  }
2626
2584
  }
2627
2585
 
2586
+ /**
2587
+ * Converts an IMAP-style flag update request (add/delete/set) into Gmail label ID sets.
2588
+ * Set precedence is handled by BaseClient#normalizeFlagUpdates.
2589
+ * @param {Object} [flagUpdates] - Flag update request ({ add, delete, set })
2590
+ * @returns {Object} Label ID sets ({ addLabelIds: Set, removeLabelIds: Set })
2591
+ */
2592
+ flagsToLabelIds(flagUpdates) {
2593
+ let addLabelIds = new Set();
2594
+ let removeLabelIds = new Set();
2595
+
2596
+ let normalized = this.normalizeFlagUpdates(flagUpdates, ['\\Seen', '\\Flagged']);
2597
+
2598
+ let labelUpdates = [];
2599
+
2600
+ for (let flag of normalized.add) {
2601
+ labelUpdates.push(this.flagToLabel(flag));
2602
+ }
2603
+
2604
+ for (let flag of normalized.delete) {
2605
+ labelUpdates.push(this.flagToLabel(flag, true));
2606
+ }
2607
+
2608
+ labelUpdates
2609
+ .filter(label => label)
2610
+ .forEach(label => {
2611
+ if (label.add) {
2612
+ addLabelIds.add(label.add);
2613
+ }
2614
+ if (label.remove) {
2615
+ removeLabelIds.add(label.remove);
2616
+ }
2617
+ });
2618
+
2619
+ return { addLabelIds, removeLabelIds };
2620
+ }
2621
+
2622
+ /**
2623
+ * Throws if the update request uses `labels.set`, which Gmail API accounts do not support
2624
+ * @param {Object} labels - Label update request
2625
+ */
2626
+ assertLabelSetSupported(labels) {
2627
+ if (labels.set) {
2628
+ let error = new Error('Replacing the full label set is not supported for Gmail API accounts');
2629
+ error.code = 'UnsupportedOperation';
2630
+ error.statusCode = 400;
2631
+ throw error;
2632
+ }
2633
+ }
2634
+
2628
2635
  /**
2629
2636
  * Converts IMAP SEARCH query to Gmail API query
2630
2637
  * @param {Object} search - IMAP search object
@@ -2805,7 +2812,7 @@ class GmailClient extends BaseClient {
2805
2812
  case 'STARRED':
2806
2813
  changes.flags[addedProp].push('\\Flagged');
2807
2814
  break;
2808
- case 'DRAFTS':
2815
+ case 'DRAFT':
2809
2816
  changes.flags[addedProp].push('\\Draft');
2810
2817
  break;
2811
2818
  default:
@@ -15,7 +15,7 @@ const ical = require('ical.js');
15
15
  const addressparser = require('nodemailer/lib/addressparser');
16
16
  const { llmPreProcess } = require('../../llm-pre-process');
17
17
 
18
- const { getESClient } = require('../../document-store');
18
+ const { getESClient, isDocumentStoreEnabled } = require('../../document-store');
19
19
 
20
20
  const {
21
21
  MESSAGE_NEW_NOTIFY,
@@ -1958,7 +1958,7 @@ class Mailbox {
1958
1958
  async publishSyncedEvents(storedStatus) {
1959
1959
  let messageFetchOptions = {};
1960
1960
 
1961
- let documentStoreEnabled = await settings.get('documentStoreEnabled');
1961
+ let documentStoreEnabled = await isDocumentStoreEnabled();
1962
1962
 
1963
1963
  // Configure text fetching
1964
1964
  let notifyText = await settings.get('notifyText');
@@ -133,7 +133,6 @@ class Subconnection extends EventEmitter {
133
133
  if (prevImapClient === this.imapClient) {
134
134
  this.imapClient = null;
135
135
  }
136
- prevImapClient = null;
137
136
  }
138
137
  }
139
138
 
@@ -909,7 +909,7 @@ class SyncOperations {
909
909
  currentPath: currentMailbox ? currentMailbox.path : 'none',
910
910
  loopId
911
911
  });
912
- throw new Error('Mailbox changed during sync operation');
912
+ throw new Error('Mailbox changed during sync operation', { cause: err });
913
913
  }
914
914
 
915
915
  // Refresh mailbox status in case it changed
@@ -1858,11 +1858,13 @@ class IMAPClient extends BaseClient {
1858
1858
 
1859
1859
  this.checkIMAPConnection(connectionOptions);
1860
1860
 
1861
- if (!this.mailboxes.has(normalizePath(path))) {
1861
+ path = await this.resolvePathAlias(path);
1862
+
1863
+ if (!this.mailboxes.has(path)) {
1862
1864
  return false; //?
1863
1865
  }
1864
1866
 
1865
- let mailbox = this.mailboxes.get(normalizePath(path));
1867
+ let mailbox = this.mailboxes.get(path);
1866
1868
 
1867
1869
  return await mailbox.updateMessages(search, updates, connectionOptions);
1868
1870
  }
@@ -1924,11 +1926,13 @@ class IMAPClient extends BaseClient {
1924
1926
 
1925
1927
  this.checkIMAPConnection(connectionOptions);
1926
1928
 
1927
- if (!this.mailboxes.has(normalizePath(source))) {
1929
+ source = await this.resolvePathAlias(source);
1930
+
1931
+ if (!this.mailboxes.has(source)) {
1928
1932
  return false; //?
1929
1933
  }
1930
1934
 
1931
- let mailbox = this.mailboxes.get(normalizePath(source));
1935
+ let mailbox = this.mailboxes.get(source);
1932
1936
 
1933
1937
  let res = await mailbox.moveMessages(search, target, connectionOptions);
1934
1938
 
@@ -1981,11 +1985,13 @@ class IMAPClient extends BaseClient {
1981
1985
 
1982
1986
  this.checkIMAPConnection(connectionOptions);
1983
1987
 
1984
- if (!this.mailboxes.has(normalizePath(path))) {
1988
+ path = await this.resolvePathAlias(path);
1989
+
1990
+ if (!this.mailboxes.has(path)) {
1985
1991
  return false; //?
1986
1992
  }
1987
1993
 
1988
- let mailbox = this.mailboxes.get(normalizePath(path));
1994
+ let mailbox = this.mailboxes.get(path);
1989
1995
  let res = await mailbox.deleteMessages(search, force, connectionOptions);
1990
1996
 
1991
1997
  // force sync target mailbox if messages were moved to trash
@@ -2112,15 +2118,8 @@ class IMAPClient extends BaseClient {
2112
2118
 
2113
2119
  this.checkIMAPConnection(connectionOptions);
2114
2120
 
2115
- let path = normalizePath(options.path);
2116
-
2117
2121
  // Handle special-use folder aliases
2118
- if (['\\Junk', '\\Sent', '\\Trash', '\\Inbox', '\\Drafts', '\\All'].includes(path)) {
2119
- let resolvedPath = await this.getSpecialUseMailbox(path);
2120
- if (resolvedPath) {
2121
- path = resolvedPath.path;
2122
- }
2123
- }
2122
+ let path = await this.resolvePathAlias(options.path);
2124
2123
 
2125
2124
  if (!this.mailboxes.has(path)) {
2126
2125
  return false; //?
@@ -2294,7 +2293,7 @@ class IMAPClient extends BaseClient {
2294
2293
  throw error;
2295
2294
  } else if (err.responseStatus === 'NO') {
2296
2295
  return {
2297
- path,
2296
+ path: [].concat(path || []).join('/'),
2298
2297
  created: false
2299
2298
  };
2300
2299
  } else {
@@ -2357,7 +2356,7 @@ class IMAPClient extends BaseClient {
2357
2356
  error.info = {
2358
2357
  response: err.response && typeof err.response === 'string' && err.response.replace(/^[^\s]*\s*/, '')
2359
2358
  };
2360
- error.code = err.serverResponseCode;
2359
+ error.code = err.serverResponseCode || 'RenameFailed';
2361
2360
  error.statusCode = 400;
2362
2361
  throw error;
2363
2362
  } else {
@@ -2413,6 +2412,26 @@ class IMAPClient extends BaseClient {
2413
2412
  .find(entry => entry.specialUse === specialUse);
2414
2413
  }
2415
2414
 
2415
+ /**
2416
+ * Resolves a special-use folder alias (e.g. "\Sent") to the real mailbox path.
2417
+ * Returns the normalized input path unchanged if it is not an alias or if no
2418
+ * mailbox with the requested special-use flag exists.
2419
+ * @param {string} path - Mailbox path or special-use alias
2420
+ * @returns {string} Normalized mailbox path
2421
+ */
2422
+ async resolvePathAlias(path) {
2423
+ path = normalizePath(path);
2424
+
2425
+ if (['\\Junk', '\\Sent', '\\Trash', '\\Inbox', '\\Drafts', '\\All'].includes(path)) {
2426
+ let resolved = await this.getSpecialUseMailbox(path);
2427
+ if (resolved) {
2428
+ path = normalizePath(resolved.path);
2429
+ }
2430
+ }
2431
+
2432
+ return path;
2433
+ }
2434
+
2416
2435
  /**
2417
2436
  * Uploads a message to a mailbox
2418
2437
  * @param {Object} data - Message data including path, flags, content
@@ -2734,7 +2753,7 @@ class IMAPClient extends BaseClient {
2734
2753
  const emptyResponse = { signatures: [], signaturesSupported: false };
2735
2754
  let accountData = await this.accountObject.loadAccountData();
2736
2755
 
2737
- if (!accountData.oauth2.provider) {
2756
+ if (!accountData.oauth2?.provider) {
2738
2757
  // Not an OAuth2 account
2739
2758
  return emptyResponse;
2740
2759
  }
@@ -3,7 +3,7 @@
3
3
  const { parentPort } = require('worker_threads');
4
4
  const logger = require('../logger');
5
5
  const { webhooks: Webhooks } = require('../webhooks');
6
- const { getESClient } = require('../document-store');
6
+ const { getESClient, isDocumentStoreEnabled } = require('../document-store');
7
7
  const { getThread } = require('../threads');
8
8
  const settings = require('../settings');
9
9
 
@@ -126,7 +126,7 @@ class NotificationHandler {
126
126
  return false;
127
127
  }
128
128
 
129
- return await settings.get('documentStoreEnabled');
129
+ return await isDocumentStoreEnabled();
130
130
  }
131
131
 
132
132
  /**
@@ -157,10 +157,7 @@ class NotificationHandler {
157
157
  }
158
158
  } catch (err) {
159
159
  if (this.logger.notifyError) {
160
- this.logger.notifyError(err, event => {
161
- event.setUser(this.account);
162
- event.addMetadata('ee', { index });
163
- });
160
+ this.logger.notifyError(err, { user: this.account, meta: { index } });
164
161
  }
165
162
  this.logger.error({
166
163
  msg: 'Failed to resolve thread',