emailengine-app 2.69.0 → 2.70.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 (74) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/CHANGELOG.md +19 -0
  4. package/Gruntfile.js +3 -1
  5. package/data/google-crawlers.json +1 -1
  6. package/getswagger.sh +40 -4
  7. package/gettext-extract.js +163 -0
  8. package/lib/account.js +73 -47
  9. package/lib/api-routes/account-routes.js +231 -71
  10. package/lib/api-routes/blocklist-routes.js +25 -18
  11. package/lib/api-routes/chat-routes.js +32 -14
  12. package/lib/api-routes/delivery-test-routes.js +30 -5
  13. package/lib/api-routes/export-routes.js +27 -2
  14. package/lib/api-routes/gateway-routes.js +63 -12
  15. package/lib/api-routes/license-routes.js +18 -4
  16. package/lib/api-routes/mailbox-routes.js +33 -7
  17. package/lib/api-routes/message-routes.js +200 -58
  18. package/lib/api-routes/oauth2-app-routes.js +90 -24
  19. package/lib/api-routes/outbox-routes.js +16 -4
  20. package/lib/api-routes/pubsub-routes.js +8 -4
  21. package/lib/api-routes/route-helpers.js +14 -1
  22. package/lib/api-routes/settings-routes.js +51 -25
  23. package/lib/api-routes/stats-routes.js +37 -3
  24. package/lib/api-routes/submit-routes.js +31 -42
  25. package/lib/api-routes/template-routes.js +54 -21
  26. package/lib/api-routes/token-routes.js +67 -67
  27. package/lib/api-routes/webhook-route-routes.js +37 -8
  28. package/lib/autodetect-imap-settings.js +0 -2
  29. package/lib/consts.js +5 -0
  30. package/lib/email-client/base-client.js +28 -6
  31. package/lib/email-client/gmail-client.js +119 -112
  32. package/lib/email-client/imap/subconnection.js +0 -1
  33. package/lib/email-client/imap/sync-operations.js +1 -1
  34. package/lib/email-client/imap-client.js +36 -17
  35. package/lib/email-client/notification-handler.js +1 -4
  36. package/lib/email-client/outlook-client.js +49 -62
  37. package/lib/export.js +37 -1
  38. package/lib/feature-flags.js +2 -2
  39. package/lib/gateway.js +4 -9
  40. package/lib/get-raw-email.js +5 -5
  41. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  42. package/lib/logger.js +24 -21
  43. package/lib/metrics-collector.js +0 -2
  44. package/lib/oauth2-apps.js +13 -4
  45. package/lib/outbox.js +24 -40
  46. package/lib/redis-operations.js +1 -1
  47. package/lib/schemas.js +403 -83
  48. package/lib/sentry.js +139 -0
  49. package/lib/settings.js +9 -3
  50. package/lib/stream-encrypt.js +1 -1
  51. package/lib/templates.js +1 -1
  52. package/lib/tokens.js +5 -3
  53. package/lib/tools.js +2 -4
  54. package/lib/ui-routes/account-routes.js +7 -4
  55. package/lib/ui-routes/admin-config-routes.js +16 -3
  56. package/lib/ui-routes/oauth-config-routes.js +0 -2
  57. package/lib/ui-routes/route-helpers.js +0 -2
  58. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  59. package/lib/webhooks.js +8 -4
  60. package/package.json +9 -8
  61. package/sbom.json +1 -1
  62. package/server.js +8 -23
  63. package/static/licenses.html +152 -292
  64. package/translations/messages.pot +122 -122
  65. package/update-info.sh +19 -1
  66. package/views/config/logging.hbs +48 -0
  67. package/workers/api.js +11 -32
  68. package/workers/documents.js +2 -22
  69. package/workers/export.js +16 -50
  70. package/workers/imap-proxy.js +3 -23
  71. package/workers/imap.js +2 -22
  72. package/workers/smtp.js +2 -22
  73. package/workers/submit.js +6 -24
  74. package/workers/webhooks.js +2 -22
@@ -910,23 +910,35 @@ class OutlookClient extends BaseClient {
910
910
 
911
911
  let folder;
912
912
  if (!force) {
913
- try {
914
- folder = await this.resolveFolder('\\Trash');
915
-
916
- // If we're already in trash, force delete
917
- if (!force && folder?.specialUse === '\\Trash') {
918
- force = true;
919
- }
913
+ // Fetch the listing once for both folder resolutions
914
+ let mailboxListing = (await this.getCachedMailboxListing()) || (await this.getMailboxListing());
920
915
 
921
- if (!force && !folder) {
922
- throw new Error('Trash folder was not found');
923
- }
916
+ // If the source folder is already the Trash folder, delete permanently
917
+ let sourceFolder;
918
+ try {
919
+ sourceFolder = await this.resolveFolder(path, { mailboxListing });
924
920
  } catch (err) {
925
921
  this.logger.error({
926
- msg: 'Failed to resolve folder for Trash',
922
+ msg: 'Failed to resolve source folder',
923
+ path,
927
924
  err
928
925
  });
929
926
  }
927
+
928
+ if (sourceFolder?.specialUse === '\\Trash') {
929
+ force = true;
930
+ } else {
931
+ folder = await this.resolveFolder('\\Trash', { mailboxListing });
932
+ if (!folder) {
933
+ let error = new Error('Trash folder was not found');
934
+ error.info = {
935
+ response: 'Failed to resolve the Trash folder for the account'
936
+ };
937
+ error.code = 'TrashNotFound';
938
+ error.statusCode = 404;
939
+ throw error;
940
+ }
941
+ }
930
942
  }
931
943
 
932
944
  // Step 1. Resolve matching messages
@@ -1049,27 +1061,15 @@ class OutlookClient extends BaseClient {
1049
1061
  }
1050
1062
 
1051
1063
  /**
1052
- * Update message flags (Graph API only supports \Seen and \Flagged)
1064
+ * Converts an IMAP-style flag update request into Graph API message properties.
1065
+ * Only \Seen and \Flagged can be represented in the Graph API. Set precedence is
1066
+ * handled by BaseClient#normalizeFlagUpdates.
1067
+ * @param {Object} [flags] - Flag update request ({ add, delete, set })
1068
+ * @returns {Object} Graph API PATCH properties ({ isRead, flag })
1053
1069
  */
1054
- async updateMessage(emailId, updates) {
1055
- await this.prepare();
1056
- updates = updates || {};
1057
-
1058
- let addFlags = updates.flags?.add || [];
1059
- let deleteFlags = updates.flags?.delete || [];
1060
-
1061
- // Handle flag set operations
1062
- if (updates.flags?.set) {
1063
- for (let flag of ['\\Seen', '\\Flagged']) {
1064
- if (updates.flags.set.includes(flag)) {
1065
- addFlags.push(flag);
1066
- } else {
1067
- deleteFlags.push(flag);
1068
- }
1069
- }
1070
- }
1070
+ buildFlagUpdates(flags) {
1071
+ let { add: addFlags, delete: deleteFlags } = this.normalizeFlagUpdates(flags, ['\\Seen', '\\Flagged']);
1071
1072
 
1072
- // Map IMAP flags to Graph API properties
1073
1073
  let flagUpdates = {};
1074
1074
 
1075
1075
  if (addFlags.includes('\\Seen')) {
@@ -1086,6 +1086,19 @@ class OutlookClient extends BaseClient {
1086
1086
  flagUpdates.flag = { flagStatus: 'notFlagged' };
1087
1087
  }
1088
1088
 
1089
+ return flagUpdates;
1090
+ }
1091
+
1092
+ /**
1093
+ * Update message flags (Graph API only supports \Seen and \Flagged)
1094
+ */
1095
+ async updateMessage(emailId, updates) {
1096
+ await this.prepare();
1097
+ updates = updates || {};
1098
+
1099
+ // Map IMAP flags to Graph API properties
1100
+ let flagUpdates = this.buildFlagUpdates(updates.flags);
1101
+
1089
1102
  // Handle label (category) operations
1090
1103
  if (updates.labels) {
1091
1104
  let categories;
@@ -1178,36 +1191,8 @@ class OutlookClient extends BaseClient {
1178
1191
 
1179
1192
  updates = updates || {};
1180
1193
 
1181
- let addFlags = updates.flags?.add || [];
1182
- let deleteFlags = updates.flags?.delete || [];
1183
-
1184
- // Handle flag set operations
1185
- if (updates.flags?.set) {
1186
- for (let flag of ['\\Seen', '\\Flagged']) {
1187
- if (updates.flags.set.includes(flag)) {
1188
- addFlags.push(flag);
1189
- } else {
1190
- deleteFlags.push(flag);
1191
- }
1192
- }
1193
- }
1194
-
1195
1194
  // Map IMAP flags to Graph API properties
1196
- let flagUpdates = {};
1197
-
1198
- if (addFlags.includes('\\Seen')) {
1199
- flagUpdates.isRead = true;
1200
- }
1201
- if (deleteFlags.includes('\\Seen')) {
1202
- flagUpdates.isRead = false;
1203
- }
1204
-
1205
- if (addFlags.includes('\\Flagged')) {
1206
- flagUpdates.flag = { flagStatus: 'flagged' };
1207
- }
1208
- if (deleteFlags.includes('\\Flagged')) {
1209
- flagUpdates.flag = { flagStatus: 'notFlagged' };
1210
- }
1195
+ let flagUpdates = this.buildFlagUpdates(updates.flags);
1211
1196
 
1212
1197
  // Step 1. Resolve matching messages
1213
1198
  let emailIds = search.emailIds || (await this.searchEmailIds(path, search));
@@ -1579,7 +1564,7 @@ class OutlookClient extends BaseClient {
1579
1564
 
1580
1565
  const contentResponse = {
1581
1566
  headers: {
1582
- 'content-type': attachmentData.mimeType || 'application/octet-stream',
1567
+ 'content-type': attachmentData.contentType || 'application/octet-stream',
1583
1568
  'content-disposition': 'attachment' + filenameParam
1584
1569
  },
1585
1570
  contentType: attachmentData.contentType,
@@ -3178,8 +3163,9 @@ class OutlookClient extends BaseClient {
3178
3163
 
3179
3164
  path = [].concat(path || []).join('/');
3180
3165
 
3181
- let cachedListing = await this.getCachedMailboxListing();
3182
- let mailboxListing = cachedListing || (await this.getMailboxListing());
3166
+ // Callers that resolve multiple folders can pass a pre-fetched listing to avoid
3167
+ // repeated cache reads or Graph API folder crawls
3168
+ let mailboxListing = options.mailboxListing || (await this.getCachedMailboxListing()) || (await this.getMailboxListing());
3183
3169
 
3184
3170
  if (options.byId) {
3185
3171
  return mailboxListing.find(entry => entry.id === path);
@@ -3749,6 +3735,7 @@ class OutlookClient extends BaseClient {
3749
3735
  bcc: extended && messageData.bccRecipients?.length ? messageData.bccRecipients.map(entry => entry.emailAddress).filter(entry => entry) : undefined,
3750
3736
 
3751
3737
  messageId: messageData.internetMessageId,
3738
+ inReplyTo: messageData.inReplyTo || undefined,
3752
3739
 
3753
3740
  headers: (extended && messageData.headers) || undefined,
3754
3741
 
package/lib/export.js CHANGED
@@ -66,6 +66,39 @@ function createError(message, code, statusCode) {
66
66
  return err;
67
67
  }
68
68
 
69
+ // Error classifiers used by the export worker to decide between retrying, skipping a single
70
+ // message, or failing the whole export job.
71
+
72
+ function isTransientError(err) {
73
+ if (['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'EPIPE', 'EHOSTUNREACH'].includes(err.code)) {
74
+ return true;
75
+ }
76
+ if (err.statusCode >= 500 && err.statusCode < 600) {
77
+ return true;
78
+ }
79
+ if (err.code === 'Timeout' || err.message?.includes('timeout')) {
80
+ return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ // Account.assertMessageFound reports a missing message as a Boom 404 that carries the
86
+ // machine-readable code and status only in err.output, while client/RPC errors set plain
87
+ // err.code/err.statusCode - check both shapes
88
+ function isSkippableError(err) {
89
+ let errCode = err.output?.payload?.code || err.code;
90
+ let statusCode = err.statusCode || err.output?.statusCode;
91
+ return errCode === 'MessageNotFound' || statusCode === 404 || err.message?.includes('Failed to generate message ID');
92
+ }
93
+
94
+ // Account.listMessages reports unknown folders as FolderNotFound (Boom error with the code in
95
+ // the payload), the Gmail and Outlook backends throw NotFound directly. Outlook bulk delete can
96
+ // also throw TrashNotFound, but the export worker never deletes messages, so it is not matched.
97
+ function isFolderMissingError(err) {
98
+ let errCode = err.output?.payload?.code || err.code;
99
+ return ['FolderNotFound', 'NotFound'].includes(errCode);
100
+ }
101
+
69
102
  async function tryAddToActiveSet(account, exportId) {
70
103
  const maxConcurrent = (await settings.get('exportMaxConcurrent')) || DEFAULT_EXPORT_MAX_CONCURRENT;
71
104
  const maxGlobalConcurrent = (await settings.get('exportMaxGlobalConcurrent')) || DEFAULT_EXPORT_MAX_GLOBAL_CONCURRENT;
@@ -493,7 +526,7 @@ class Export {
493
526
  continue;
494
527
  }
495
528
 
496
- if (data && (data.status === 'processing' || data.status === 'queued' || data.status === 'indexing' || data.status === 'cancelled')) {
529
+ if (data && (data.status === 'processing' || data.status === 'queued' || data.status === 'cancelled')) {
497
530
  const job = await exportQueue.getJob(exportId).catch(() => null);
498
531
  if (job) {
499
532
  await job.remove().catch(() => {});
@@ -582,5 +615,8 @@ module.exports = {
582
615
  getExportQueueKey,
583
616
  getExportPath,
584
617
  getExportMaxAge,
618
+ isTransientError,
619
+ isSkippableError,
620
+ isFolderMissingError,
585
621
  DEFAULT_EXPORT_MAX_MESSAGE_SIZE
586
622
  };
@@ -21,11 +21,11 @@ for (let key of Object.keys(process.env)) {
21
21
  if (/^EENGINE_FEATURE_/i.test(key)) {
22
22
  let feature = formatFeatureKey(key.substring('EENGINE_FEATURE_'.length));
23
23
  if (feature) {
24
- let value = /^y|1|t/i.test((process.env[key] || '').toString().trim());
24
+ let value = /^(y|yes|true|t|1)$/i.test((process.env[key] || '').toString().trim());
25
25
  if (value) {
26
26
  FEATURES.add(feature);
27
27
  } else {
28
- FEATURES.remove(feature);
28
+ FEATURES.delete(feature);
29
29
  }
30
30
  }
31
31
  }
package/lib/gateway.js CHANGED
@@ -179,13 +179,8 @@ class Gateway {
179
179
  throw error;
180
180
  }
181
181
 
182
- let state = false;
183
- if (result[0][1] && result[0][1].gateway) {
184
- // existing user
185
- state = 'existing';
186
- } else {
187
- state = 'new';
188
- }
182
+ // HGET returns the previous value if the gateway entry already existed
183
+ let state = result[0][1] ? 'existing' : 'new';
189
184
 
190
185
  return { gateway: this.gateway, state };
191
186
  }
@@ -243,7 +238,7 @@ class Gateway {
243
238
  let result = await this.redis.multi().del(this.getGatewayKey()).srem(`${REDIS_PREFIX}gateways`, this.gateway).exec();
244
239
  if (!result) {
245
240
  return {
246
- account: this.account,
241
+ gateway: this.gateway,
247
242
  deleted: false
248
243
  };
249
244
  }
@@ -256,7 +251,7 @@ class Gateway {
256
251
 
257
252
  if (!result[0] || !result[0][1]) {
258
253
  return {
259
- account: this.account,
254
+ gateway: this.gateway,
260
255
  deleted: false
261
256
  };
262
257
  }
@@ -229,15 +229,15 @@ async function processMessage(data, licenseInfo) {
229
229
  }
230
230
 
231
231
  if (data.headers) {
232
- for (let key of Object.keys(headers)) {
232
+ for (let key of Object.keys(data.headers)) {
233
233
  let casedKey = key.replace(/^.|-./g, c => c.toUpperCase());
234
- switch (key) {
234
+ switch (key.toLowerCase()) {
235
235
  case 'in-reply-to':
236
236
  case 'references':
237
- headers.update(casedKey, headers[key]);
237
+ headers.update(casedKey, data.headers[key]);
238
238
  break;
239
239
  default:
240
- headers.add(casedKey, headers[key]);
240
+ headers.add(casedKey, data.headers[key]);
241
241
  break;
242
242
  }
243
243
  }
@@ -266,7 +266,7 @@ async function processMessage(data, licenseInfo) {
266
266
  });
267
267
 
268
268
  let trackClicks = typeof data.trackClicks === 'boolean' ? data.trackClicks : trackingEnabled;
269
- let trackOpens = typeof data.trackOpens === 'boolean' ? data.trackClicks : trackingEnabled;
269
+ let trackOpens = typeof data.trackOpens === 'boolean' ? data.trackOpens : trackingEnabled;
270
270
 
271
271
  return {
272
272
  raw: message,
@@ -860,7 +860,6 @@ class IMAPConnection extends EventEmitter {
860
860
  if (existsResponse && !changed) {
861
861
  // send cached EXISTS response
862
862
  this.writeStream.write(existsResponse);
863
- existsResponse = false;
864
863
  }
865
864
 
866
865
  if (changed) {
package/lib/logger.js CHANGED
@@ -19,22 +19,13 @@ let logger = pino({
19
19
  log(object) {
20
20
  if (object.err && ['TypeError', 'RangeError'].includes(object.err.name)) {
21
21
  if (logger.notifyError) {
22
- logger.notifyError(object.err, event => {
23
- if (object.account) {
24
- event.setUser(object.account);
22
+ let meta = {};
23
+ for (let key of ['msg', 'path', 'cid']) {
24
+ if (object[key]) {
25
+ meta[key] = object[key];
25
26
  }
26
- let meta = {};
27
- let hasMeta = false;
28
- for (let key of ['msg', 'path', 'cid']) {
29
- if (object[key]) {
30
- meta[key] = object[key];
31
- hasMeta = true;
32
- }
33
- }
34
- if (hasMeta) {
35
- event.addMetadata('ee', meta);
36
- }
37
- });
27
+ }
28
+ logger.notifyError(object.err, { user: object.account, meta });
38
29
  }
39
30
  }
40
31
  return object;
@@ -49,6 +40,22 @@ if (threadId) {
49
40
  logger = logger.child({ tid: threadId });
50
41
  }
51
42
 
43
+ // An error that reaches the global handlers leaves the process in an unknown state,
44
+ // so the process must always exit. If error tracking is enabled, report the error
45
+ // first and allow a short flush window for the delivery.
46
+ function fatalShutdown(code, err) {
47
+ if (logger.notifyError) {
48
+ logger.notifyError(err);
49
+ }
50
+
51
+ let exit = () => process.exit(code);
52
+ if (logger.flushNotifications) {
53
+ logger.flushNotifications().then(exit, exit);
54
+ } else {
55
+ setTimeout(exit, 10);
56
+ }
57
+ }
58
+
52
59
  process.on('uncaughtException', err => {
53
60
  logger.fatal({
54
61
  msg: 'uncaughtException',
@@ -56,9 +63,7 @@ process.on('uncaughtException', err => {
56
63
  err
57
64
  });
58
65
 
59
- if (!logger.notifyError) {
60
- setTimeout(() => process.exit(1), 10);
61
- }
66
+ fatalShutdown(1, err);
62
67
  });
63
68
 
64
69
  process.on('unhandledRejection', err => {
@@ -68,9 +73,7 @@ process.on('unhandledRejection', err => {
68
73
  err
69
74
  });
70
75
 
71
- if (!logger.notifyError) {
72
- setTimeout(() => process.exit(2), 10);
73
- }
76
+ fatalShutdown(2, err);
74
77
  });
75
78
 
76
79
  module.exports = logger;
@@ -91,8 +91,6 @@ class MetricsCollector {
91
91
  * Collect metrics in background (sequential to avoid CPU spikes)
92
92
  */
93
93
  async collectInBackground() {
94
- const startTime = Date.now();
95
-
96
94
  // Start with main thread info
97
95
  let threadsInfo = [
98
96
  Object.assign(
@@ -339,10 +339,6 @@ class OAuth2AppsHandler {
339
339
  apps: []
340
340
  };
341
341
 
342
- if (idList.length <= startPos) {
343
- return response;
344
- }
345
-
346
342
  //let keys = idList.slice(startPos, startPos + pageSize);
347
343
  let keys = idList;
348
344
 
@@ -406,6 +402,8 @@ class OAuth2AppsHandler {
406
402
  });
407
403
  }
408
404
 
405
+ // Recount after undecodable entries were skipped and query filters were applied
406
+ response.total = response.apps.length;
409
407
  response.pages = Math.ceil(response.apps.length / pageSize);
410
408
  response.apps = response.apps.slice(startPos, startPos + pageSize);
411
409
 
@@ -747,6 +745,17 @@ class OAuth2AppsHandler {
747
745
 
748
746
  async update(id, data, opts) {
749
747
  opts = opts || {};
748
+
749
+ // `tenant` is a UI-style alias for an Outlook directory tenant ID. Only honor it when
750
+ // the caller explicitly selected it via authority='tenant' (the UI form convention) -
751
+ // a stray tenant value alone must not overwrite the stored authority.
752
+ if (data && typeof data === 'object' && 'tenant' in data) {
753
+ if (data.tenant && data.authority === 'tenant') {
754
+ data.authority = data.tenant;
755
+ }
756
+ delete data.tenant;
757
+ }
758
+
750
759
  if (LEGACY_KEYS.includes(id)) {
751
760
  // legacy
752
761
  return await this.updateLegacyApp(id, data);
package/lib/outbox.js CHANGED
@@ -3,6 +3,28 @@
3
3
  const { submitQueue, redis } = require('./db');
4
4
  const { REDIS_PREFIX } = require('../lib/consts');
5
5
 
6
+ function formatQueueEntry(job) {
7
+ let scheduled = job.timestamp + (Number(job.opts.delay) || 0);
8
+
9
+ let backoffDelay = Number(job.opts.backoff && job.opts.backoff.delay) || 0;
10
+ // attemptsMade already includes the failed attempt, so the retry delay is 2^(attemptsMade-1) * base
11
+ let nextAttempt = job.attemptsMade ? Math.round(job.processedOn + Math.pow(2, job.attemptsMade - 1) * backoffDelay) : scheduled;
12
+
13
+ if (job.opts.attempts <= job.attemptsMade) {
14
+ nextAttempt = false;
15
+ }
16
+
17
+ return Object.assign(job.data, {
18
+ created: new Date(Number(job.data.created || job.timestamp)).toISOString(),
19
+ //status: job.name,
20
+ progress: job.progress,
21
+ attemptsMade: job.attemptsMade,
22
+ attempts: job.opts.attempts,
23
+ scheduled: new Date(scheduled).toISOString(),
24
+ nextAttempt: nextAttempt ? new Date(nextAttempt).toISOString() : false
25
+ });
26
+ }
27
+
6
28
  async function list(options) {
7
29
  options = options || {};
8
30
  let page = Number(options.page) || 0;
@@ -23,26 +45,7 @@ async function list(options) {
23
45
  try {
24
46
  let job = await submitQueue.getJob(jobId);
25
47
  if (job) {
26
- let scheduled = job.timestamp + (Number(job.opts.delay) || 0);
27
-
28
- let backoffDelay = Number(job.opts.backoff && job.opts.backoff.delay) || 0;
29
- let nextAttempt = job.attemptsMade ? Math.round(job.processedOn + Math.pow(2, job.attemptsMade) * backoffDelay) : scheduled;
30
-
31
- if (job.opts.attempts <= job.attemptsMade) {
32
- nextAttempt = false;
33
- }
34
-
35
- messages.push(
36
- Object.assign(job.data, {
37
- created: new Date(Number(job.created || job.timestamp)).toISOString(),
38
- //status: job.name,
39
- progress: job.progress,
40
- attemptsMade: job.attemptsMade,
41
- attempts: job.opts.attempts,
42
- scheduled: new Date(scheduled).toISOString(),
43
- nextAttempt: nextAttempt ? new Date(nextAttempt).toISOString() : false
44
- })
45
- );
48
+ messages.push(formatQueueEntry(job));
46
49
  }
47
50
  } catch (err) {
48
51
  logger.error({ msg: 'Failed to retrieve message info from outbox', jobId, err });
@@ -105,15 +108,6 @@ async function get(options) {
105
108
  return false;
106
109
  }
107
110
 
108
- let scheduled = job.timestamp + (Number(job.opts.delay) || 0);
109
-
110
- let backoffDelay = Number(job.opts.backoff && job.opts.backoff.delay) || 0;
111
- let nextAttempt = job.attemptsMade ? Math.round(job.processedOn + Math.pow(2, job.attemptsMade) * backoffDelay) : scheduled;
112
-
113
- if (job.opts.attempts <= job.attemptsMade) {
114
- nextAttempt = false;
115
- }
116
-
117
111
  try {
118
112
  let queueEntryBuf = await redis.hgetBuffer(`${REDIS_PREFIX}iaq:${job.data.account}`, job.data.queueId);
119
113
  if (!queueEntryBuf) {
@@ -124,17 +118,7 @@ async function get(options) {
124
118
  throw err;
125
119
  }
126
120
 
127
- let response = Object.assign(job.data, {
128
- created: new Date(Number(job.created || job.timestamp)).toISOString(),
129
- //status: job.name,
130
- progress: job.progress,
131
- attemptsMade: job.attemptsMade,
132
- attempts: job.opts.attempts,
133
- scheduled: new Date(scheduled).toISOString(),
134
- nextAttempt: nextAttempt ? new Date(nextAttempt).toISOString() : false
135
- });
136
-
137
- return response;
121
+ return formatQueueEntry(job);
138
122
  }
139
123
 
140
124
  module.exports = { list, del, get };
@@ -228,7 +228,7 @@ class RedisTransaction {
228
228
  * @returns {Promise<Array>} Array of values from successful commands
229
229
  */
230
230
  async execOrThrow() {
231
- const { results, values, error } = await this.exec();
231
+ const { values, error } = await this.exec();
232
232
 
233
233
  if (error) {
234
234
  throw error;