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.
- package/.github/workflows/deploy.yml +6 -3
- package/.github/workflows/release.yaml +2 -0
- package/.github/workflows/test.yml +73 -12
- package/.ncurc.js +3 -3
- package/CHANGELOG.md +37 -0
- package/Gruntfile.js +21 -23
- package/bin/emailengine.js +8 -1
- package/config/default.toml +5 -0
- package/config/test.toml +5 -0
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +44 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +104 -72
- 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 +291 -145
- 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/document-store.js +22 -1
- package/lib/email-client/base-client.js +31 -8
- package/lib/email-client/gmail-client.js +119 -112
- package/lib/email-client/imap/mailbox.js +2 -2
- 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 +3 -6
- package/lib/email-client/outlook-client.js +49 -62
- package/lib/export.js +49 -1
- package/lib/feature-flags.js +8 -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/license-beacon.js +367 -0
- package/lib/logger.js +35 -22
- 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/routes-ui.js +2 -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 +28 -6
- package/lib/ui-routes/account-routes.js +7 -4
- package/lib/ui-routes/admin-config-routes.js +20 -6
- package/lib/ui-routes/document-store-routes.js +7 -1
- 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 +23 -19
- package/sbom.json +1 -1
- package/server.js +38 -31
- package/static/licenses.html +171 -391
- package/translations/de.mo +0 -0
- package/translations/de.po +154 -142
- package/translations/et.mo +0 -0
- package/translations/et.po +129 -131
- package/translations/fr.mo +0 -0
- package/translations/fr.po +133 -136
- package/translations/ja.mo +0 -0
- package/translations/ja.po +126 -129
- package/translations/messages.pot +107 -107
- package/translations/nl.mo +0 -0
- package/translations/nl.po +128 -130
- package/translations/pl.mo +0 -0
- package/translations/pl.po +125 -128
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +22 -0
- package/workers/api.js +33 -37
- package/workers/documents.js +2 -22
- package/workers/export.js +73 -92
- 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,50 @@ 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
|
+
|
|
102
|
+
// Per-message errors worth retrying during a batch export: rate limits, a dropped batch response
|
|
103
|
+
// (Outlook reports these as EMISSING_RESPONSE), and the same transient network/5xx/timeout errors
|
|
104
|
+
// isTransientError already covers. Used by the export worker's API-account batch fetch path so a
|
|
105
|
+
// single transient blip does not fail an entire multi-message export.
|
|
106
|
+
function isRetryableError(err) {
|
|
107
|
+
if (err.statusCode === 429 || ['rateLimitExceeded', 'userRateLimitExceeded', 'EMISSING_RESPONSE'].includes(err.code)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return isTransientError(err);
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
async function tryAddToActiveSet(account, exportId) {
|
|
70
114
|
const maxConcurrent = (await settings.get('exportMaxConcurrent')) || DEFAULT_EXPORT_MAX_CONCURRENT;
|
|
71
115
|
const maxGlobalConcurrent = (await settings.get('exportMaxGlobalConcurrent')) || DEFAULT_EXPORT_MAX_GLOBAL_CONCURRENT;
|
|
@@ -493,7 +537,7 @@ class Export {
|
|
|
493
537
|
continue;
|
|
494
538
|
}
|
|
495
539
|
|
|
496
|
-
if (data && (data.status === 'processing' || data.status === 'queued' || data.status === '
|
|
540
|
+
if (data && (data.status === 'processing' || data.status === 'queued' || data.status === 'cancelled')) {
|
|
497
541
|
const job = await exportQueue.getJob(exportId).catch(() => null);
|
|
498
542
|
if (job) {
|
|
499
543
|
await job.remove().catch(() => {});
|
|
@@ -582,5 +626,9 @@ module.exports = {
|
|
|
582
626
|
getExportQueueKey,
|
|
583
627
|
getExportPath,
|
|
584
628
|
getExportMaxAge,
|
|
629
|
+
isTransientError,
|
|
630
|
+
isSkippableError,
|
|
631
|
+
isFolderMissingError,
|
|
632
|
+
isRetryableError,
|
|
585
633
|
DEFAULT_EXPORT_MAX_MESSAGE_SIZE
|
|
586
634
|
};
|
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
|
}
|
|
@@ -34,6 +34,12 @@ for (let key of Object.keys(process.env)) {
|
|
|
34
34
|
module.exports = {
|
|
35
35
|
enabled(key) {
|
|
36
36
|
return FEATURES.has(formatFeatureKey(key));
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Sorted list of the currently enabled feature flag keys. Used by the license beacon
|
|
40
|
+
// to report which EENGINE_FEATURE_* flags are active on this instance.
|
|
41
|
+
listEnabled() {
|
|
42
|
+
return Array.from(FEATURES).sort();
|
|
37
43
|
}
|
|
38
44
|
};
|
|
39
45
|
|
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,
|