emailengine-app 2.61.5 → 2.62.1
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/CHANGELOG.md +88 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account.js +20 -7
- package/lib/api-routes/account-routes.js +28 -5
- package/lib/api-routes/chat-routes.js +1 -1
- package/lib/api-routes/export-routes.js +316 -0
- package/lib/api-routes/message-routes.js +28 -23
- package/lib/api-routes/template-routes.js +28 -7
- package/lib/arf-detect.js +1 -1
- package/lib/autodetect-imap-settings.js +5 -5
- package/lib/consts.js +16 -0
- package/lib/db.js +3 -0
- package/lib/email-client/base-client.js +6 -4
- package/lib/email-client/gmail-client.js +205 -35
- package/lib/email-client/imap/mailbox.js +99 -8
- package/lib/email-client/imap/subconnection.js +5 -5
- package/lib/email-client/imap-client.js +76 -19
- package/lib/email-client/message-builder.js +3 -1
- package/lib/email-client/notification-handler.js +12 -9
- package/lib/email-client/outlook-client.js +364 -73
- package/lib/email-client/smtp-pool-manager.js +1 -1
- package/lib/export.js +528 -0
- package/lib/oauth/gmail.js +24 -16
- package/lib/oauth/mail-ru.js +26 -13
- package/lib/oauth/outlook.js +29 -19
- package/lib/oauth/pubsub/google.js +5 -0
- package/lib/routes-ui.js +268 -9
- package/lib/schemas.js +274 -81
- package/lib/stream-encrypt.js +263 -0
- package/lib/sub-script.js +2 -2
- package/lib/tools.js +194 -12
- package/lib/ui-routes/account-routes.js +23 -0
- package/lib/ui-routes/admin-config-routes.js +13 -6
- package/lib/ui-routes/admin-entities-routes.js +18 -0
- package/lib/webhooks.js +16 -20
- package/package.json +20 -20
- package/sbom.json +1 -1
- package/server.js +66 -7
- package/static/js/ace/ace.js +1 -1
- package/static/js/ace/ext-language_tools.js +1 -1
- package/static/licenses.html +118 -149
- package/translations/de.mo +0 -0
- package/translations/de.po +63 -36
- package/translations/en.mo +0 -0
- package/translations/en.po +64 -37
- package/translations/et.mo +0 -0
- package/translations/et.po +63 -36
- package/translations/fr.mo +0 -0
- package/translations/fr.po +63 -36
- package/translations/ja.mo +0 -0
- package/translations/ja.po +63 -36
- package/translations/messages.pot +84 -51
- package/translations/nl.mo +0 -0
- package/translations/nl.po +63 -36
- package/translations/pl.mo +0 -0
- package/translations/pl.po +63 -36
- package/views/accounts/account.hbs +375 -2
- package/views/config/network.hbs +45 -0
- package/views/config/service.hbs +35 -0
- package/workers/api.js +130 -47
- package/workers/documents.js +3 -2
- package/workers/export.js +933 -0
- package/workers/imap.js +34 -1
- package/workers/submit.js +33 -6
- package/workers/webhooks.js +20 -4
|
@@ -288,7 +288,7 @@ class IMAPClient extends BaseClient {
|
|
|
288
288
|
|
|
289
289
|
// Set up error handling for the command connection
|
|
290
290
|
const onErr = err => {
|
|
291
|
-
commandClient?.log.
|
|
291
|
+
commandClient?.log.warn({ msg: 'IMAP connection error', cid: commandCid, channel: 'command', account: this.account, err });
|
|
292
292
|
commandClient.close();
|
|
293
293
|
this.commandClient = null;
|
|
294
294
|
};
|
|
@@ -301,7 +301,7 @@ class IMAPClient extends BaseClient {
|
|
|
301
301
|
if (this.connections.delete(commandClient)) {
|
|
302
302
|
await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
|
|
303
303
|
}
|
|
304
|
-
commandClient.log.
|
|
304
|
+
commandClient.log.warn({ msg: 'Failed to connect command client', cid: commandCid, channel: 'command', account: this.account, err });
|
|
305
305
|
throw err;
|
|
306
306
|
}
|
|
307
307
|
|
|
@@ -371,8 +371,47 @@ class IMAPClient extends BaseClient {
|
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
// Get stored mailbox status including UID validity
|
|
374
|
-
|
|
374
|
+
let storedStatus = await mailbox.getStoredStatus();
|
|
375
|
+
|
|
376
|
+
// If stored status missing uidValidity, try live IMAP mailbox status
|
|
377
|
+
if (!validUidValidity(storedStatus.uidValidity)) {
|
|
378
|
+
try {
|
|
379
|
+
const liveStatus = mailbox.getMailboxStatus();
|
|
380
|
+
if (validUidValidity(liveStatus.uidValidity)) {
|
|
381
|
+
this.logger.debug({
|
|
382
|
+
msg: 'packUid using live mailbox status (stored uidValidity missing)',
|
|
383
|
+
path: mailbox.path,
|
|
384
|
+
liveUidValidity: liveStatus.uidValidity.toString()
|
|
385
|
+
});
|
|
386
|
+
storedStatus = {
|
|
387
|
+
...storedStatus,
|
|
388
|
+
uidValidity: liveStatus.uidValidity,
|
|
389
|
+
path: liveStatus.path || mailbox.path
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
// Live status not available (mailbox not selected)
|
|
394
|
+
this.logger.debug({
|
|
395
|
+
msg: 'packUid live status fallback unavailable',
|
|
396
|
+
path: mailbox.path,
|
|
397
|
+
error: err.message
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Final validation
|
|
375
403
|
if (!validUidValidity(storedStatus.uidValidity) || !storedStatus.path) {
|
|
404
|
+
this.logger.warn({
|
|
405
|
+
msg: 'packUid failed due to invalid stored status',
|
|
406
|
+
uid,
|
|
407
|
+
mailboxPath: mailbox.path,
|
|
408
|
+
storedUidValidity: storedStatus.uidValidity,
|
|
409
|
+
storedUidValidityType: typeof storedStatus.uidValidity,
|
|
410
|
+
storedPath: storedStatus.path,
|
|
411
|
+
storedUidNext: storedStatus.uidNext,
|
|
412
|
+
storedMessages: storedStatus.messages,
|
|
413
|
+
syncing: mailbox.syncing || false
|
|
414
|
+
});
|
|
376
415
|
return false;
|
|
377
416
|
}
|
|
378
417
|
|
|
@@ -481,7 +520,6 @@ class IMAPClient extends BaseClient {
|
|
|
481
520
|
}
|
|
482
521
|
|
|
483
522
|
await mailbox.clear();
|
|
484
|
-
mailbox = false;
|
|
485
523
|
}
|
|
486
524
|
|
|
487
525
|
/**
|
|
@@ -498,7 +536,9 @@ class IMAPClient extends BaseClient {
|
|
|
498
536
|
|
|
499
537
|
const connectionClient = await this.getImapConnection(connectionOptions, 'getCurrentListing');
|
|
500
538
|
if (!connectionClient) {
|
|
501
|
-
this.imapClient
|
|
539
|
+
if (this.imapClient) {
|
|
540
|
+
this.imapClient.close();
|
|
541
|
+
}
|
|
502
542
|
let error = new Error('Failed to get connection');
|
|
503
543
|
error.code = 'ConnectionError';
|
|
504
544
|
throw error;
|
|
@@ -522,7 +562,9 @@ class IMAPClient extends BaseClient {
|
|
|
522
562
|
let listing = await connectionClient.list(options);
|
|
523
563
|
if (!listing.length) {
|
|
524
564
|
// server bug, the list can never be empty
|
|
525
|
-
this.imapClient
|
|
565
|
+
if (this.imapClient) {
|
|
566
|
+
this.imapClient.close();
|
|
567
|
+
}
|
|
526
568
|
let error = new Error('Server bug: empty mailbox listing');
|
|
527
569
|
error.code = 'ServerBug';
|
|
528
570
|
throw error;
|
|
@@ -879,7 +921,7 @@ class IMAPClient extends BaseClient {
|
|
|
879
921
|
}
|
|
880
922
|
} catch (err) {
|
|
881
923
|
// ended in an unconncted state
|
|
882
|
-
this.logger.
|
|
924
|
+
this.logger.warn({ msg: 'Failed to set up connection, will retry', err });
|
|
883
925
|
|
|
884
926
|
// Calculate delay with capped exponential backoff
|
|
885
927
|
const retryDelay = Math.min(this.reconnectMaxDelay, 1000 * Math.pow(1.5, Math.min(this.reconnectRetries, 10)));
|
|
@@ -898,7 +940,7 @@ class IMAPClient extends BaseClient {
|
|
|
898
940
|
this.reconnectRetries = 0;
|
|
899
941
|
})
|
|
900
942
|
.catch(err => {
|
|
901
|
-
this.logger.
|
|
943
|
+
this.logger.warn({
|
|
902
944
|
msg: 'Connection retry failed',
|
|
903
945
|
err,
|
|
904
946
|
attempt: this.reconnectRetries
|
|
@@ -935,6 +977,10 @@ class IMAPClient extends BaseClient {
|
|
|
935
977
|
|
|
936
978
|
// Sync existing folders that weren't just synced
|
|
937
979
|
for (let mailbox of this.mailboxes.values()) {
|
|
980
|
+
if (!this.imapClient || !this.imapClient.usable) {
|
|
981
|
+
this.logger.debug({ msg: 'IMAP client disconnected during mailbox sync' });
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
938
984
|
if (!synced || !synced.has(mailbox)) {
|
|
939
985
|
await mailbox.sync();
|
|
940
986
|
}
|
|
@@ -949,12 +995,19 @@ class IMAPClient extends BaseClient {
|
|
|
949
995
|
this.state = 'connected';
|
|
950
996
|
await this.setStateVal();
|
|
951
997
|
|
|
998
|
+
// Capture local reference to avoid race conditions during async operations
|
|
999
|
+
const imapClient = this.imapClient;
|
|
1000
|
+
if (!imapClient || !imapClient.usable) {
|
|
1001
|
+
this.logger.debug({ msg: 'IMAP client disconnected or not usable after state update' });
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
952
1005
|
// Store IMAP server capabilities for reference
|
|
953
|
-
const capabilities = (
|
|
1006
|
+
const capabilities = (imapClient.rawCapabilities || []).map(entry => entry && entry.value).filter(entry => entry);
|
|
954
1007
|
const authCapabilities = [];
|
|
955
1008
|
let lastUsedAuthCapability = null;
|
|
956
|
-
if (
|
|
957
|
-
for (let [authCapa, usedAuth] of
|
|
1009
|
+
if (imapClient.authCapabilities) {
|
|
1010
|
+
for (let [authCapa, usedAuth] of imapClient.authCapabilities) {
|
|
958
1011
|
authCapabilities.push(authCapa);
|
|
959
1012
|
if (usedAuth) {
|
|
960
1013
|
lastUsedAuthCapability = authCapa;
|
|
@@ -962,7 +1015,7 @@ class IMAPClient extends BaseClient {
|
|
|
962
1015
|
}
|
|
963
1016
|
}
|
|
964
1017
|
|
|
965
|
-
const serverInfo = Object.assign({},
|
|
1018
|
+
const serverInfo = Object.assign({}, imapClient.serverInfo || {}, {
|
|
966
1019
|
capabilities,
|
|
967
1020
|
authCapabilities,
|
|
968
1021
|
lastUsedAuthCapability
|
|
@@ -980,6 +1033,12 @@ class IMAPClient extends BaseClient {
|
|
|
980
1033
|
return;
|
|
981
1034
|
}
|
|
982
1035
|
|
|
1036
|
+
// Check if client disconnected before mailbox selection
|
|
1037
|
+
if (!this.imapClient || !this.imapClient.usable) {
|
|
1038
|
+
this.logger.debug({ msg: 'IMAP client disconnected before mailbox selection' });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
983
1042
|
this.logger.debug({ msg: 'Syncing completed, selecting main path', path: mainPath });
|
|
984
1043
|
// start waiting for changes
|
|
985
1044
|
await this.select(mainPath);
|
|
@@ -1195,7 +1254,7 @@ class IMAPClient extends BaseClient {
|
|
|
1195
1254
|
prevImapClient.removeAllListeners();
|
|
1196
1255
|
|
|
1197
1256
|
const prevImapErrorHandler = err => {
|
|
1198
|
-
this.logger.
|
|
1257
|
+
this.logger.warn({ msg: 'IMAP connection error', type: 'imapClient', previous: true, account: this.account, err });
|
|
1199
1258
|
};
|
|
1200
1259
|
|
|
1201
1260
|
prevImapClient.once('error', prevImapErrorHandler);
|
|
@@ -1207,12 +1266,11 @@ class IMAPClient extends BaseClient {
|
|
|
1207
1266
|
this.commandClient.close();
|
|
1208
1267
|
}
|
|
1209
1268
|
} catch (err) {
|
|
1210
|
-
this.logger.
|
|
1269
|
+
this.logger.warn({ msg: 'IMAP close error', err });
|
|
1211
1270
|
} finally {
|
|
1212
1271
|
if (prevImapClient === this.imapClient) {
|
|
1213
1272
|
this.imapClient = null;
|
|
1214
1273
|
}
|
|
1215
|
-
prevImapClient = null;
|
|
1216
1274
|
}
|
|
1217
1275
|
}
|
|
1218
1276
|
|
|
@@ -1267,7 +1325,7 @@ class IMAPClient extends BaseClient {
|
|
|
1267
1325
|
|
|
1268
1326
|
// Handle connection errors
|
|
1269
1327
|
imapClient.on('error', err => {
|
|
1270
|
-
imapClient?.log.
|
|
1328
|
+
imapClient?.log.warn({ msg: 'IMAP connection error', type: 'imapClient', account: this.account, err });
|
|
1271
1329
|
if (imapClient !== this.imapClient || this._connecting) {
|
|
1272
1330
|
return;
|
|
1273
1331
|
}
|
|
@@ -1291,7 +1349,7 @@ class IMAPClient extends BaseClient {
|
|
|
1291
1349
|
this.errorReconnectDelay = 2000;
|
|
1292
1350
|
})
|
|
1293
1351
|
.catch(err => {
|
|
1294
|
-
this.logger.
|
|
1352
|
+
this.logger.warn({
|
|
1295
1353
|
msg: 'IMAP reconnection error',
|
|
1296
1354
|
account: this.account,
|
|
1297
1355
|
err,
|
|
@@ -2039,7 +2097,6 @@ class IMAPClient extends BaseClient {
|
|
|
2039
2097
|
if (this.mailboxes.has(normalizePath(path))) {
|
|
2040
2098
|
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
2041
2099
|
await mailbox.clear();
|
|
2042
|
-
mailbox = false;
|
|
2043
2100
|
}
|
|
2044
2101
|
|
|
2045
2102
|
return result;
|
|
@@ -2303,7 +2360,7 @@ class IMAPClient extends BaseClient {
|
|
|
2303
2360
|
let uploadResponse = await connectionClient.append(data.path, raw, data.flags, data.internalDate);
|
|
2304
2361
|
|
|
2305
2362
|
// Return to IDLE if using primary connection
|
|
2306
|
-
if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
2363
|
+
if (this.imapClient && connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
2307
2364
|
// force back to IDLE
|
|
2308
2365
|
this.imapClient.idle().catch(err => {
|
|
2309
2366
|
this.logger.error({ msg: 'IDLE error', err });
|
|
@@ -467,7 +467,9 @@ const SMTP_ERROR_DESCRIPTIONS = {
|
|
|
467
467
|
EDNS: settings => `EmailEngine failed to resolve DNS record for ${settings.host}`,
|
|
468
468
|
ECONNECTION: settings => `EmailEngine failed to establish TCP connection against ${settings.host}`,
|
|
469
469
|
EPROTOCOL: settings => `Unexpected response from ${settings.host}`,
|
|
470
|
-
EAUTH: () => 'Authentication failed'
|
|
470
|
+
EAUTH: () => 'Authentication failed',
|
|
471
|
+
ENOAUTH: () => 'Authentication credentials were not provided',
|
|
472
|
+
EOAUTH2: () => 'OAuth2 token generation or refresh failed'
|
|
471
473
|
};
|
|
472
474
|
|
|
473
475
|
/**
|
|
@@ -19,12 +19,13 @@ const DOCUMENT_SYNC_EVENTS = [MESSAGE_NEW_NOTIFY, MESSAGE_DELETED_NOTIFY, MESSAG
|
|
|
19
19
|
* Default job options for notification queue
|
|
20
20
|
*/
|
|
21
21
|
const DEFAULT_JOB_OPTIONS = {
|
|
22
|
-
removeOnComplete:
|
|
23
|
-
removeOnFail:
|
|
22
|
+
removeOnComplete: { age: 24 * 3600, count: 1000 },
|
|
23
|
+
removeOnFail: { age: 24 * 3600, count: 1000 },
|
|
24
24
|
attempts: 10,
|
|
25
25
|
backoff: {
|
|
26
26
|
type: 'exponential',
|
|
27
|
-
delay: 5000
|
|
27
|
+
delay: 5000,
|
|
28
|
+
jitter: 0.2 // 20% randomization to prevent thundering herd
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
31
|
|
|
@@ -236,18 +237,20 @@ class NotificationHandler {
|
|
|
236
237
|
|
|
237
238
|
/**
|
|
238
239
|
* Builds job options based on queue keep setting
|
|
239
|
-
* @param {boolean} queueKeep - Whether to keep
|
|
240
|
+
* @param {boolean|number} queueKeep - Whether/how many jobs to keep
|
|
240
241
|
* @returns {Object} Job options for notify and documents queues
|
|
241
242
|
*/
|
|
242
243
|
buildJobOptions(queueKeep) {
|
|
244
|
+
const retention = typeof queueKeep === 'number' ? { age: 24 * 3600, count: queueKeep } : queueKeep;
|
|
245
|
+
|
|
243
246
|
const notifyOptions = Object.assign({}, DEFAULT_JOB_OPTIONS, {
|
|
244
|
-
removeOnComplete:
|
|
245
|
-
removeOnFail:
|
|
247
|
+
removeOnComplete: retention,
|
|
248
|
+
removeOnFail: retention
|
|
246
249
|
});
|
|
247
250
|
|
|
248
251
|
const documentOptions = Object.assign({}, DOCUMENT_JOB_OPTIONS, {
|
|
249
|
-
removeOnComplete:
|
|
250
|
-
removeOnFail:
|
|
252
|
+
removeOnComplete: retention,
|
|
253
|
+
removeOnFail: retention
|
|
251
254
|
});
|
|
252
255
|
|
|
253
256
|
return {
|
|
@@ -289,7 +292,7 @@ class NotificationHandler {
|
|
|
289
292
|
}
|
|
290
293
|
|
|
291
294
|
// Get queue retention setting
|
|
292
|
-
const queueKeep = (await settings.get('queueKeep'))
|
|
295
|
+
const queueKeep = (await settings.get('queueKeep')) ?? true;
|
|
293
296
|
|
|
294
297
|
// Process notification based on required destinations
|
|
295
298
|
if (!skipWebhook && addDocumentQueueJob) {
|