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.
- package/.github/workflows/deploy.yml +6 -3
- package/.github/workflows/release.yaml +2 -0
- package/CHANGELOG.md +19 -0
- package/Gruntfile.js +3 -1
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +73 -47
- package/lib/api-routes/account-routes.js +231 -71
- package/lib/api-routes/blocklist-routes.js +25 -18
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +30 -5
- package/lib/api-routes/export-routes.js +27 -2
- package/lib/api-routes/gateway-routes.js +63 -12
- package/lib/api-routes/license-routes.js +18 -4
- package/lib/api-routes/mailbox-routes.js +33 -7
- package/lib/api-routes/message-routes.js +200 -58
- package/lib/api-routes/oauth2-app-routes.js +90 -24
- package/lib/api-routes/outbox-routes.js +16 -4
- package/lib/api-routes/pubsub-routes.js +8 -4
- package/lib/api-routes/route-helpers.js +14 -1
- package/lib/api-routes/settings-routes.js +51 -25
- package/lib/api-routes/stats-routes.js +37 -3
- package/lib/api-routes/submit-routes.js +31 -42
- package/lib/api-routes/template-routes.js +54 -21
- package/lib/api-routes/token-routes.js +67 -67
- package/lib/api-routes/webhook-route-routes.js +37 -8
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +119 -112
- package/lib/email-client/imap/subconnection.js +0 -1
- package/lib/email-client/imap/sync-operations.js +1 -1
- package/lib/email-client/imap-client.js +36 -17
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +49 -62
- package/lib/export.js +37 -1
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
- package/lib/logger.js +24 -21
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/schemas.js +403 -83
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +2 -4
- package/lib/ui-routes/account-routes.js +7 -4
- package/lib/ui-routes/admin-config-routes.js +16 -3
- package/lib/ui-routes/oauth-config-routes.js +0 -2
- package/lib/ui-routes/route-helpers.js +0 -2
- package/lib/ui-routes/unsubscribe-routes.js +0 -2
- package/lib/webhooks.js +8 -4
- package/package.json +9 -8
- package/sbom.json +1 -1
- package/server.js +8 -23
- package/static/licenses.html +152 -292
- package/translations/messages.pot +122 -122
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/workers/api.js +11 -32
- package/workers/documents.js +2 -22
- package/workers/export.js +16 -50
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +2 -22
- package/workers/smtp.js +2 -22
- package/workers/submit.js +6 -24
- 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
|
-
|
|
914
|
-
|
|
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
|
-
|
|
922
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
1055
|
-
|
|
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.
|
|
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
|
-
|
|
3182
|
-
|
|
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 === '
|
|
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
|
};
|
package/lib/feature-flags.js
CHANGED
|
@@ -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|
|
|
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.
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
+
gateway: this.gateway,
|
|
260
255
|
deleted: false
|
|
261
256
|
};
|
|
262
257
|
}
|
package/lib/get-raw-email.js
CHANGED
|
@@ -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.
|
|
269
|
+
let trackOpens = typeof data.trackOpens === 'boolean' ? data.trackOpens : trackingEnabled;
|
|
270
270
|
|
|
271
271
|
return {
|
|
272
272
|
raw: message,
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
let meta = {};
|
|
23
|
+
for (let key of ['msg', 'path', 'cid']) {
|
|
24
|
+
if (object[key]) {
|
|
25
|
+
meta[key] = object[key];
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
setTimeout(() => process.exit(2), 10);
|
|
73
|
-
}
|
|
76
|
+
fatalShutdown(2, err);
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
module.exports = logger;
|
package/lib/metrics-collector.js
CHANGED
package/lib/oauth2-apps.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 };
|
package/lib/redis-operations.js
CHANGED
|
@@ -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 {
|
|
231
|
+
const { values, error } = await this.exec();
|
|
232
232
|
|
|
233
233
|
if (error) {
|
|
234
234
|
throw error;
|