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
@@ -14,28 +14,8 @@ const GB_COLLECT_DELAY = 6 * 3600 * 1000; // 6h
14
14
  const GB_FAILURE_DELAY = 3 * 1000;
15
15
  const GB_EMPTY_DELAY = 10 * 1000;
16
16
 
17
- const Bugsnag = require('@bugsnag/js');
18
- if (readEnvValue('BUGSNAG_API_KEY')) {
19
- Bugsnag.start({
20
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
21
- appVersion: packageData.version,
22
- logger: {
23
- debug(...args) {
24
- logger.debug({ msg: args.shift(), worker: 'documents', source: 'bugsnag', args: args.length ? args : undefined });
25
- },
26
- info(...args) {
27
- logger.debug({ msg: args.shift(), worker: 'documents', source: 'bugsnag', args: args.length ? args : undefined });
28
- },
29
- warn(...args) {
30
- logger.warn({ msg: args.shift(), worker: 'documents', source: 'bugsnag', args: args.length ? args : undefined });
31
- },
32
- error(...args) {
33
- logger.error({ msg: args.shift(), worker: 'documents', source: 'bugsnag', args: args.length ? args : undefined });
34
- }
35
- }
36
- });
37
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
38
- }
17
+ const { initSentry } = require('../lib/sentry');
18
+ initSentry('documents');
39
19
 
40
20
  const { redis, queueConf } = require('../lib/db');
41
21
  const { Worker } = require('bullmq');
package/workers/export.js CHANGED
@@ -20,30 +20,10 @@ const {
20
20
  const { getDuration, readEnvValue, threadStats, maybeReloadHttpProxyAgent } = require('../lib/tools');
21
21
  const { webhooks: Webhooks } = require('../lib/webhooks');
22
22
  const settings = require('../lib/settings');
23
- const { Export } = require('../lib/export');
24
-
25
- const Bugsnag = require('@bugsnag/js');
26
- if (readEnvValue('BUGSNAG_API_KEY')) {
27
- Bugsnag.start({
28
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
29
- appVersion: packageData.version,
30
- logger: {
31
- debug(...args) {
32
- logger.debug({ msg: args.shift(), worker: 'export', source: 'bugsnag', args: args.length ? args : undefined });
33
- },
34
- info(...args) {
35
- logger.debug({ msg: args.shift(), worker: 'export', source: 'bugsnag', args: args.length ? args : undefined });
36
- },
37
- warn(...args) {
38
- logger.warn({ msg: args.shift(), worker: 'export', source: 'bugsnag', args: args.length ? args : undefined });
39
- },
40
- error(...args) {
41
- logger.error({ msg: args.shift(), worker: 'export', source: 'bugsnag', args: args.length ? args : undefined });
42
- }
43
- }
44
- });
45
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
46
- }
23
+ const { Export, isTransientError, isSkippableError, isFolderMissingError, isRetryableError } = require('../lib/export');
24
+
25
+ const { initSentry } = require('../lib/sentry');
26
+ initSentry('export');
47
27
 
48
28
  const { redis, queueConf } = require('../lib/db');
49
29
  const { Worker } = require('bullmq');
@@ -74,23 +54,6 @@ const IMAP_MESSAGE_RETRY_BASE_DELAY = 2000;
74
54
  const ACCOUNT_CHECK_INTERVAL = 60 * 1000;
75
55
  const LOCK_EXTENSION_INTERVAL = 5 * 60 * 1000;
76
56
 
77
- function isTransientError(err) {
78
- if (['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'EPIPE', 'EHOSTUNREACH'].includes(err.code)) {
79
- return true;
80
- }
81
- if (err.statusCode >= 500 && err.statusCode < 600) {
82
- return true;
83
- }
84
- if (err.code === 'Timeout' || err.message?.includes('timeout')) {
85
- return true;
86
- }
87
- return false;
88
- }
89
-
90
- function isSkippableError(err) {
91
- return err.code === 'MessageNotFound' || err.statusCode === 404 || err.message?.includes('Failed to generate message ID');
92
- }
93
-
94
57
  let callQueue = new Map();
95
58
  let mids = 0;
96
59
 
@@ -229,10 +192,9 @@ async function indexMessages(job, exportData) {
229
192
  }
230
193
 
231
194
  const folderPath = foldersToProcess[i];
232
- let retries = FOLDER_INDEX_MAX_RETRIES;
233
- let lastError = null;
195
+ let attempt = 0;
234
196
 
235
- while (retries > 0) {
197
+ while (true) {
236
198
  try {
237
199
  const remaining = maxMessages ? maxMessages - totalIndexed : 0;
238
200
  const queued = await indexFolder(accountObject, account, exportId, folderPath, startDate, endDate, indexingStartTime, remaining);
@@ -248,40 +210,43 @@ async function indexMessages(job, exportData) {
248
210
  totalIndexed
249
211
  });
250
212
 
251
- lastError = null;
252
213
  break;
253
214
  } catch (err) {
254
- lastError = err;
255
- retries--;
256
- if (retries > 0) {
257
- const attemptNumber = FOLDER_INDEX_MAX_RETRIES - retries;
258
- const delay = FOLDER_INDEX_RETRY_DELAY_MS * Math.pow(2, attemptNumber - 1);
259
- logger.warn({
260
- msg: 'Folder indexing failed, retrying',
215
+ attempt++;
216
+ // A folder that was deleted mid-export is handled inside indexFolder (treated as empty),
217
+ // so any error reaching here is a real failure. Only transient errors are worth retrying;
218
+ // a permanent error (deleted account, auth failure, unexpected server response) is not
219
+ // specific to this folder and would repeat for the rest, so fail the whole export. Silently
220
+ // skipping folders and marking an incomplete export "completed" hides data loss from the caller.
221
+ if (!isTransientError(err) || attempt >= FOLDER_INDEX_MAX_RETRIES) {
222
+ logger.error({
223
+ msg: 'Failed to index folder, failing export',
261
224
  account,
262
225
  exportId,
263
226
  folder: folderPath,
264
- retriesLeft: retries,
265
- delayMs: delay,
227
+ attempts: attempt,
266
228
  err
267
229
  });
268
- await new Promise(resolve => setTimeout(resolve, delay));
230
+ err.message = `Failed to index folder "${folderPath}": ${err.message}`;
231
+ throw err;
269
232
  }
270
- }
271
- }
272
233
 
273
- if (lastError) {
274
- logger.warn({
275
- msg: 'Failed to index folder after retries',
276
- account,
277
- exportId,
278
- folder: folderPath,
279
- maxRetries: FOLDER_INDEX_MAX_RETRIES,
280
- err: lastError
281
- });
234
+ const delay = FOLDER_INDEX_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
235
+ logger.warn({
236
+ msg: 'Folder indexing failed, retrying',
237
+ account,
238
+ exportId,
239
+ folder: folderPath,
240
+ attempt,
241
+ maxRetries: FOLDER_INDEX_MAX_RETRIES,
242
+ delayMs: delay,
243
+ err
244
+ });
245
+ await new Promise(resolve => setTimeout(resolve, delay));
246
+ }
282
247
  }
283
248
 
284
- // Count the folder as scanned whether or not it succeeded so progress reaches foldersTotal.
249
+ // The folder indexed successfully (failures throw above); count it toward progress.
285
250
  await Export.update(account, exportId, { foldersScanned: i + 1 });
286
251
 
287
252
  if (maxMessages && totalIndexed >= maxMessages) {
@@ -341,7 +306,18 @@ async function indexFolder(accountObject, account, exportId, folderPath, startDa
341
306
  cursor
342
307
  };
343
308
 
344
- const result = await accountObject.listMessages(listOptions);
309
+ let result;
310
+ try {
311
+ result = await accountObject.listMessages(listOptions);
312
+ } catch (err) {
313
+ // Other errors (e.g. a deleted account) propagate
314
+ if (isFolderMissingError(err)) {
315
+ // The folder disappeared mid-export - treat it as empty instead of failing the export
316
+ logger.warn({ msg: 'Export folder was not found, skipping', account, exportId, folder: folderPath, err });
317
+ return queued;
318
+ }
319
+ throw err;
320
+ }
345
321
 
346
322
  for (const msg of result.messages || []) {
347
323
  if (maxMessages && queued >= maxMessages) {
@@ -446,14 +422,14 @@ async function exportMessages(job, exportData) {
446
422
 
447
423
  let lastAccountCheck = Date.now();
448
424
  // Refreshes the job lock and export key expiry on a fixed cadence; called from the main loop and
449
- // from inside the rate-limit backoff so a batch stuck retrying does not let the lock lapse.
425
+ // from inside the retry backoff so a batch stuck retrying does not let the lock lapse.
450
426
  const maybeExtendLease = createLeaseExtender(job, account, exportId);
451
427
 
452
428
  const accountData = await accountObject.loadAccountData(account);
453
429
  const isApiAccount = await accountObject.isApiClient(accountData);
454
430
  const MESSAGE_FETCH_BATCH_SIZE = 10; // Batch size for parallel message fetching
455
- const MAX_RATE_LIMIT_RETRIES = 5; // Max retries for rate-limited messages
456
- const RATE_LIMIT_BASE_DELAY = 5000; // Base delay for rate limit backoff (5 seconds)
431
+ const MAX_BATCH_RETRIES = 5; // Max retries for rate-limited or transient per-message errors within a batch
432
+ const BATCH_RETRY_BASE_DELAY = 5000; // Base delay for batch retry backoff (5 seconds)
457
433
 
458
434
  async function processMessage(message, entry) {
459
435
  message.path = entry.folder;
@@ -557,7 +533,7 @@ async function exportMessages(job, exportData) {
557
533
  }
558
534
 
559
535
  let fetchBatch = entriesToFetch.slice(i, i + MESSAGE_FETCH_BATCH_SIZE);
560
- let rateLimitRetry = 0;
536
+ let batchRetry = 0;
561
537
 
562
538
  while (fetchBatch.length > 0) {
563
539
  const messageIds = fetchBatch.map(e => e.messageId);
@@ -568,14 +544,17 @@ async function exportMessages(job, exportData) {
568
544
  resultMap.set(result.messageId, result);
569
545
  }
570
546
 
571
- const rateLimitedEntries = [];
547
+ const retryEntries = [];
572
548
 
573
549
  for (const entry of fetchBatch) {
574
550
  const result = resultMap.get(entry.messageId);
575
551
 
576
552
  if (result && result.error) {
577
553
  const err = result.error;
578
- const isRateLimited = err.statusCode === 429 || err.code === 'rateLimitExceeded' || err.code === 'userRateLimitExceeded';
554
+ // A single transient blip (rate limit, dropped batch response, network/5xx)
555
+ // must not fail an entire multi-message export; only give up once the retry
556
+ // budget is exhausted. See isRetryableError for the full classification.
557
+ const isRetryable = isRetryableError(err);
579
558
 
580
559
  if (isSkippableError(err)) {
581
560
  logger.warn({
@@ -587,8 +566,8 @@ async function exportMessages(job, exportData) {
587
566
  reason: err.message || err.code
588
567
  });
589
568
  await Export.incrementSkipped(account, exportId);
590
- } else if (isRateLimited && rateLimitRetry < MAX_RATE_LIMIT_RETRIES) {
591
- rateLimitedEntries.push(entry);
569
+ } else if (isRetryable && batchRetry < MAX_BATCH_RETRIES) {
570
+ retryEntries.push(entry);
592
571
  } else {
593
572
  const error = new Error(err.message);
594
573
  error.code = err.code;
@@ -618,21 +597,21 @@ async function exportMessages(job, exportData) {
618
597
  break;
619
598
  }
620
599
 
621
- if (rateLimitedEntries.length > 0) {
622
- rateLimitRetry++;
623
- const delay = RATE_LIMIT_BASE_DELAY * Math.pow(2, rateLimitRetry - 1) + Math.random() * 1000;
600
+ if (retryEntries.length > 0) {
601
+ batchRetry++;
602
+ const delay = BATCH_RETRY_BASE_DELAY * Math.pow(2, batchRetry - 1) + Math.random() * 1000;
624
603
  logger.warn({
625
- msg: 'Rate limited during export, retrying batch',
604
+ msg: 'Retrying failed messages during export batch',
626
605
  account,
627
606
  exportId,
628
- rateLimitedCount: rateLimitedEntries.length,
629
- attempt: rateLimitRetry,
630
- maxAttempts: MAX_RATE_LIMIT_RETRIES,
607
+ retryCount: retryEntries.length,
608
+ attempt: batchRetry,
609
+ maxAttempts: MAX_BATCH_RETRIES,
631
610
  delayMs: Math.round(delay)
632
611
  });
633
612
  await new Promise(resolve => setTimeout(resolve, delay));
634
613
  await maybeExtendLease();
635
- fetchBatch = rateLimitedEntries;
614
+ fetchBatch = retryEntries;
636
615
  } else {
637
616
  break;
638
617
  }
@@ -833,6 +812,11 @@ const exportWorker = new Worker(
833
812
  lockDuration: 10 * 60 * 1000,
834
813
  stalledInterval: 2 * 60 * 1000,
835
814
  maxStalledCount: 5,
815
+ // Do not start consuming jobs at construction. Startup recovery (markInterruptedAsFailed +
816
+ // cleanup) must finish first, otherwise the worker can pick up a queued export and start
817
+ // processing it while recovery concurrently marks that same export failed and deletes its
818
+ // file. run() is called from the startup IIFE once recovery completes.
819
+ autorun: false,
836
820
  ...queueConf
837
821
  }
838
822
  );
@@ -883,6 +867,11 @@ function onCommand(command) {
883
867
  logger.error({ msg: 'Failed to clean up export files', err });
884
868
  }
885
869
 
870
+ // Now that interrupted exports have been reconciled and orphaned files cleaned, start consuming
871
+ // jobs. Mirrors BullMQ's own autorun: a fatal run() failure is surfaced as an 'error' event,
872
+ // which (no listener) crashes the worker thread so the main process can restart it.
873
+ exportWorker.run().catch(error => exportWorker.emit('error', error));
874
+
886
875
  setInterval(() => {
887
876
  try {
888
877
  parentPort.postMessage({ cmd: 'heartbeat' });
@@ -960,11 +949,3 @@ parentPort.on('message', message => {
960
949
  });
961
950
 
962
951
  logger.info({ msg: 'Started export worker thread', version: packageData.version });
963
-
964
- module.exports = {
965
- isTransientError,
966
- isSkippableError,
967
- IMAP_MESSAGE_MAX_RETRIES,
968
- IMAP_MESSAGE_RETRY_BASE_DELAY,
969
- ACCOUNT_CHECK_INTERVAL
970
- };
@@ -5,32 +5,12 @@ const { parentPort } = require('worker_threads');
5
5
  const packageData = require('../package.json');
6
6
  const logger = require('../lib/logger');
7
7
 
8
- const { readEnvValue, threadStats } = require('../lib/tools');
8
+ const { threadStats } = require('../lib/tools');
9
9
 
10
10
  const { run } = require('../lib/imapproxy/imap-server');
11
11
 
12
- const Bugsnag = require('@bugsnag/js');
13
- if (readEnvValue('BUGSNAG_API_KEY')) {
14
- Bugsnag.start({
15
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
16
- appVersion: packageData.version,
17
- logger: {
18
- debug(...args) {
19
- logger.debug({ msg: args.shift(), worker: 'imapProxy', source: 'bugsnag', args: args.length ? args : undefined });
20
- },
21
- info(...args) {
22
- logger.debug({ msg: args.shift(), worker: 'imapProxy', source: 'bugsnag', args: args.length ? args : undefined });
23
- },
24
- warn(...args) {
25
- logger.warn({ msg: args.shift(), worker: 'imapProxy', source: 'bugsnag', args: args.length ? args : undefined });
26
- },
27
- error(...args) {
28
- logger.error({ msg: args.shift(), worker: 'imapProxy', source: 'bugsnag', args: args.length ? args : undefined });
29
- }
30
- }
31
- });
32
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
33
- }
12
+ const { initSentry } = require('../lib/sentry');
13
+ initSentry('imapProxy');
34
14
 
35
15
  async function onCommand(command) {
36
16
  switch (command.cmd) {
package/workers/imap.js CHANGED
@@ -9,28 +9,8 @@ const { REDIS_PREFIX } = require('../lib/consts');
9
9
 
10
10
  const { getDuration, getBoolean, emitChangeEvent, readEnvValue, hasEnvValue, threadStats, maybeReloadHttpProxyAgent } = require('../lib/tools');
11
11
 
12
- const Bugsnag = require('@bugsnag/js');
13
- if (readEnvValue('BUGSNAG_API_KEY')) {
14
- Bugsnag.start({
15
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
16
- appVersion: packageData.version,
17
- logger: {
18
- debug(...args) {
19
- logger.debug({ msg: args.shift(), worker: 'imap', source: 'bugsnag', args: args.length ? args : undefined });
20
- },
21
- info(...args) {
22
- logger.debug({ msg: args.shift(), worker: 'imap', source: 'bugsnag', args: args.length ? args : undefined });
23
- },
24
- warn(...args) {
25
- logger.warn({ msg: args.shift(), worker: 'imap', source: 'bugsnag', args: args.length ? args : undefined });
26
- },
27
- error(...args) {
28
- logger.error({ msg: args.shift(), worker: 'imap', source: 'bugsnag', args: args.length ? args : undefined });
29
- }
30
- }
31
- });
32
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
33
- }
12
+ const { initSentry } = require('../lib/sentry');
13
+ initSentry('imap');
34
14
 
35
15
  const { IMAPClient } = require('../lib/email-client/imap-client');
36
16
  const { GmailClient } = require('../lib/email-client/gmail-client');
package/workers/smtp.js CHANGED
@@ -9,28 +9,8 @@ const logger = require('../lib/logger');
9
9
  const { getDuration, emitChangeEvent, readEnvValue, threadStats, loadTlsConfig, getByteSize } = require('../lib/tools');
10
10
  const { matchIp } = require('../lib/utils/network');
11
11
 
12
- const Bugsnag = require('@bugsnag/js');
13
- if (readEnvValue('BUGSNAG_API_KEY')) {
14
- Bugsnag.start({
15
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
16
- appVersion: packageData.version,
17
- logger: {
18
- debug(...args) {
19
- logger.debug({ msg: args.shift(), worker: 'smtp', source: 'bugsnag', args: args.length ? args : undefined });
20
- },
21
- info(...args) {
22
- logger.debug({ msg: args.shift(), worker: 'smtp', source: 'bugsnag', args: args.length ? args : undefined });
23
- },
24
- warn(...args) {
25
- logger.warn({ msg: args.shift(), worker: 'smtp', source: 'bugsnag', args: args.length ? args : undefined });
26
- },
27
- error(...args) {
28
- logger.error({ msg: args.shift(), worker: 'smtp', source: 'bugsnag', args: args.length ? args : undefined });
29
- }
30
- }
31
- });
32
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
33
- }
12
+ const { initSentry } = require('../lib/sentry');
13
+ initSentry('smtp');
34
14
 
35
15
  const { SMTPServer } = require('smtp-server');
36
16
  const util = require('util');
package/workers/submit.js CHANGED
@@ -11,28 +11,8 @@ const { getDuration, readEnvValue, threadStats, maybeReloadHttpProxyAgent } = re
11
11
  const { webhooks: Webhooks } = require('../lib/webhooks');
12
12
  const settings = require('../lib/settings');
13
13
 
14
- const Bugsnag = require('@bugsnag/js');
15
- if (readEnvValue('BUGSNAG_API_KEY')) {
16
- Bugsnag.start({
17
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
18
- appVersion: packageData.version,
19
- logger: {
20
- debug(...args) {
21
- logger.debug({ msg: args.shift(), worker: 'submit', source: 'bugsnag', args: args.length ? args : undefined });
22
- },
23
- info(...args) {
24
- logger.debug({ msg: args.shift(), worker: 'submit', source: 'bugsnag', args: args.length ? args : undefined });
25
- },
26
- warn(...args) {
27
- logger.warn({ msg: args.shift(), worker: 'submit', source: 'bugsnag', args: args.length ? args : undefined });
28
- },
29
- error(...args) {
30
- logger.error({ msg: args.shift(), worker: 'submit', source: 'bugsnag', args: args.length ? args : undefined });
31
- }
32
- }
33
- });
34
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
35
- }
14
+ const { initSentry } = require('../lib/sentry');
15
+ initSentry('submit');
36
16
 
37
17
  const util = require('util');
38
18
  const { redis, queueConf, submitQueue } = require('../lib/db');
@@ -222,13 +202,15 @@ const submitWorker = new Worker(
222
202
  }
223
203
 
224
204
  let backoffDelay = Number(job.opts.backoff && job.opts.backoff.delay) || 0;
225
- let nextAttempt = job.attemptsMade < job.opts.attempts ? Math.round(job.processedOn + Math.pow(2, job.attemptsMade) * backoffDelay) : false;
205
+ // job.attemptsMade is not yet incremented for the ongoing attempt, so the
206
+ // next retry (if this attempt fails) is delayed by 2^attemptsMade * base
207
+ let nextAttempt = job.attemptsMade + 1 < job.opts.attempts ? Math.round(job.processedOn + Math.pow(2, job.attemptsMade) * backoffDelay) : false;
226
208
 
227
209
  queueEntry.job = {
228
210
  id: job.id,
229
211
  attemptsMade: job.attemptsMade,
230
212
  attempts: job.opts.attempts,
231
- nextAttempt: new Date(nextAttempt).toISOString()
213
+ nextAttempt: nextAttempt ? new Date(nextAttempt).toISOString() : false
232
214
  };
233
215
 
234
216
  let res = await accountObject.submitMessage(queueEntry);
@@ -13,28 +13,8 @@ const { GooglePubSub } = require('../lib/oauth/pubsub/google');
13
13
  const { readEnvValue, threadStats, getDuration, httpAgent, getServiceSecret, maybeReloadHttpProxyAgent } = require('../lib/tools');
14
14
  const { sendWebhookRequest } = require('../lib/webhook-request');
15
15
 
16
- const Bugsnag = require('@bugsnag/js');
17
- if (readEnvValue('BUGSNAG_API_KEY')) {
18
- Bugsnag.start({
19
- apiKey: readEnvValue('BUGSNAG_API_KEY'),
20
- appVersion: packageData.version,
21
- logger: {
22
- debug(...args) {
23
- logger.debug({ msg: args.shift(), worker: 'webhooks', source: 'bugsnag', args: args.length ? args : undefined });
24
- },
25
- info(...args) {
26
- logger.debug({ msg: args.shift(), worker: 'webhooks', source: 'bugsnag', args: args.length ? args : undefined });
27
- },
28
- warn(...args) {
29
- logger.warn({ msg: args.shift(), worker: 'webhooks', source: 'bugsnag', args: args.length ? args : undefined });
30
- },
31
- error(...args) {
32
- logger.error({ msg: args.shift(), worker: 'webhooks', source: 'bugsnag', args: args.length ? args : undefined });
33
- }
34
- }
35
- });
36
- logger.notifyError = Bugsnag.notify.bind(Bugsnag);
37
- }
16
+ const { initSentry } = require('../lib/sentry');
17
+ initSentry('webhooks');
38
18
 
39
19
  const { redis, queueConf } = require('../lib/db');
40
20
  const { Worker } = require('bullmq');