emailengine-app 2.61.4 → 2.62.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/CHANGELOG.md +87 -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/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 +204 -33
- 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 -16
- 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 +362 -69
- package/lib/email-client/smtp-pool-manager.js +1 -1
- package/lib/export.js +528 -0
- package/lib/oauth/gmail.js +21 -13
- package/lib/oauth/mail-ru.js +23 -10
- package/lib/oauth/outlook.js +26 -16
- package/lib/oauth/pubsub/google.js +5 -0
- package/lib/routes-ui.js +236 -2
- package/lib/schemas.js +260 -80
- package/lib/stream-encrypt.js +263 -0
- package/lib/tools.js +30 -4
- package/lib/ui-routes/account-routes.js +24 -1
- package/lib/ui-routes/admin-config-routes.js +11 -4
- package/lib/ui-routes/admin-entities-routes.js +18 -0
- package/lib/webhooks.js +16 -20
- package/package.json +17 -17
- package/sbom.json +1 -1
- package/server.js +41 -5
- package/static/js/ace/ace.js +1 -1
- package/static/js/ace/ext-language_tools.js +1 -1
- package/static/licenses.html +47 -127
- 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 +88 -55
- 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/service.hbs +35 -0
- package/workers/api.js +124 -45
- package/workers/documents.js +1 -0
- package/workers/export.js +926 -0
- package/workers/imap.js +29 -0
- package/workers/submit.js +25 -5
- package/workers/webhooks.js +11 -2
|
@@ -99,6 +99,18 @@ class Mailbox {
|
|
|
99
99
|
|
|
100
100
|
let mailboxInfo = connectionClient.mailbox;
|
|
101
101
|
|
|
102
|
+
// Log diagnostic info if mailbox object is missing or incomplete
|
|
103
|
+
if (!mailboxInfo) {
|
|
104
|
+
this.logger.warn({
|
|
105
|
+
msg: 'IMAP mailbox object is not available',
|
|
106
|
+
path: this.path,
|
|
107
|
+
connectionState: connectionClient.state,
|
|
108
|
+
usable: connectionClient.usable,
|
|
109
|
+
idling: connectionClient.idling
|
|
110
|
+
});
|
|
111
|
+
throw new Error('IMAP mailbox state is not available');
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
let status = {
|
|
103
115
|
path: this.path
|
|
104
116
|
};
|
|
@@ -112,6 +124,21 @@ class Mailbox {
|
|
|
112
124
|
// Total message count
|
|
113
125
|
status.messages = mailboxInfo.exists ? mailboxInfo.exists : 0;
|
|
114
126
|
|
|
127
|
+
// Log diagnostic info if uidValidity is invalid
|
|
128
|
+
if (!status.uidValidity) {
|
|
129
|
+
this.logger.warn({
|
|
130
|
+
msg: 'Invalid uidValidity received from IMAP server',
|
|
131
|
+
path: this.path,
|
|
132
|
+
rawUidValidity: mailboxInfo.uidValidity,
|
|
133
|
+
rawUidValidityType: typeof mailboxInfo.uidValidity,
|
|
134
|
+
uidNext: status.uidNext,
|
|
135
|
+
messages: status.messages,
|
|
136
|
+
mailboxPath: mailboxInfo.path,
|
|
137
|
+
connectionState: connectionClient.state,
|
|
138
|
+
usable: connectionClient.usable
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
115
142
|
return status;
|
|
116
143
|
}
|
|
117
144
|
|
|
@@ -122,6 +149,20 @@ class Mailbox {
|
|
|
122
149
|
async getStoredStatus() {
|
|
123
150
|
let data = await this.connection.redis.hgetall(this.getMailboxKey());
|
|
124
151
|
data = data || {};
|
|
152
|
+
|
|
153
|
+
// Log diagnostic info if stored uidValidity is invalid or missing
|
|
154
|
+
if (!validUidValidity(data.uidValidity)) {
|
|
155
|
+
this.logger.warn({
|
|
156
|
+
msg: 'Invalid or missing uidValidity in stored status',
|
|
157
|
+
path: this.path,
|
|
158
|
+
redisKey: this.getMailboxKey(),
|
|
159
|
+
rawUidValidity: data.uidValidity,
|
|
160
|
+
rawUidValidityType: typeof data.uidValidity,
|
|
161
|
+
hasData: Object.keys(data).length > 0,
|
|
162
|
+
storedKeys: Object.keys(data)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
125
166
|
return {
|
|
126
167
|
path: data.path || this.path,
|
|
127
168
|
uidValidity: validUidValidity(data.uidValidity) ? BigInt(data.uidValidity) : false,
|
|
@@ -135,6 +176,25 @@ class Mailbox {
|
|
|
135
176
|
};
|
|
136
177
|
}
|
|
137
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Packs a UID into external reference format with validation and logging
|
|
181
|
+
* @param {number} uid - Message UID to pack
|
|
182
|
+
* @param {string} notificationType - Type of notification for logging context
|
|
183
|
+
* @returns {string|null} Packed UID or null if packing failed
|
|
184
|
+
*/
|
|
185
|
+
async packUidWithLogging(uid, notificationType) {
|
|
186
|
+
let packedUid = await this.connection.packUid(this, uid);
|
|
187
|
+
if (!packedUid) {
|
|
188
|
+
this.logger.warn({
|
|
189
|
+
msg: `Skipping ${notificationType} notification - packUid returned invalid result`,
|
|
190
|
+
uid,
|
|
191
|
+
path: this.path
|
|
192
|
+
});
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return packedUid;
|
|
196
|
+
}
|
|
197
|
+
|
|
138
198
|
/**
|
|
139
199
|
* Updates known mailbox state in Redis
|
|
140
200
|
* @param {Object} data - Status data to store
|
|
@@ -149,10 +209,32 @@ class Mailbox {
|
|
|
149
209
|
.map(key => {
|
|
150
210
|
switch (key) {
|
|
151
211
|
case 'path':
|
|
152
|
-
case 'uidValidity':
|
|
153
212
|
case 'highestModseq':
|
|
154
213
|
case 'messages':
|
|
214
|
+
return [key, data[key].toString()];
|
|
215
|
+
|
|
216
|
+
case 'uidValidity':
|
|
217
|
+
// Skip storing invalid uidValidity to prevent sync state corruption
|
|
218
|
+
if (!validUidValidity(data[key])) {
|
|
219
|
+
this.logger.warn({
|
|
220
|
+
msg: 'Skipping invalid uidValidity value in updateStoredStatus',
|
|
221
|
+
path: this.path,
|
|
222
|
+
uidValidityValue: data[key]
|
|
223
|
+
});
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return [key, data[key].toString()];
|
|
227
|
+
|
|
155
228
|
case 'uidNext':
|
|
229
|
+
// Skip storing invalid uidNext to prevent sync state corruption
|
|
230
|
+
if (!data[key]) {
|
|
231
|
+
this.logger.warn({
|
|
232
|
+
msg: 'Skipping invalid uidNext value in updateStoredStatus',
|
|
233
|
+
path: this.path,
|
|
234
|
+
uidNextValue: data[key]
|
|
235
|
+
});
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
156
238
|
return [key, data[key].toString()];
|
|
157
239
|
|
|
158
240
|
case 'lastFullSync':
|
|
@@ -644,7 +726,10 @@ class Mailbox {
|
|
|
644
726
|
*/
|
|
645
727
|
|
|
646
728
|
// Generate packed UID for external reference
|
|
647
|
-
let packedUid = await this.
|
|
729
|
+
let packedUid = await this.packUidWithLogging(messageData.uid, 'delete');
|
|
730
|
+
if (!packedUid) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
648
733
|
await this.connection.notify(this, MESSAGE_DELETED_NOTIFY, {
|
|
649
734
|
id: packedUid,
|
|
650
735
|
uid: messageData.uid
|
|
@@ -707,7 +792,10 @@ class Mailbox {
|
|
|
707
792
|
|
|
708
793
|
if (!messageInfo) {
|
|
709
794
|
// Message not found after retries - send missing notification
|
|
710
|
-
let packedUid = await this.
|
|
795
|
+
let packedUid = await this.packUidWithLogging(messageData.uid, 'missing');
|
|
796
|
+
if (!packedUid) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
711
799
|
await this.connection.notify(this, MESSAGE_MISSING_NOTIFY, {
|
|
712
800
|
id: packedUid,
|
|
713
801
|
uid: messageData.uid,
|
|
@@ -766,7 +854,7 @@ class Mailbox {
|
|
|
766
854
|
if (this.mightBeAComplaint(messageInfo)) {
|
|
767
855
|
try {
|
|
768
856
|
// Download relevant attachments for ARF parsing
|
|
769
|
-
for (let attachment of messageInfo.attachments) {
|
|
857
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
770
858
|
if (!['message/feedback-report', 'message/rfc822-headers', 'message/rfc822'].includes(attachment.contentType)) {
|
|
771
859
|
continue;
|
|
772
860
|
}
|
|
@@ -1074,7 +1162,7 @@ class Mailbox {
|
|
|
1074
1162
|
// Fetch inline attachments referenced in HTML
|
|
1075
1163
|
if (messageInfo.attachments?.length && messageInfo.text?.html) {
|
|
1076
1164
|
// fetch inline attachments
|
|
1077
|
-
for (let attachment of messageInfo.attachments) {
|
|
1165
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
1078
1166
|
if (attachment.encodedSize && attachment.encodedSize > MAX_INLINE_ATTACHMENT_SIZE) {
|
|
1079
1167
|
// skip large attachments
|
|
1080
1168
|
continue;
|
|
@@ -1366,7 +1454,7 @@ class Mailbox {
|
|
|
1366
1454
|
let partList = [];
|
|
1367
1455
|
|
|
1368
1456
|
// Collect CID-referenced attachments
|
|
1369
|
-
for (let attachment of messageInfo.attachments) {
|
|
1457
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
1370
1458
|
let contentId = attachment.contentId && attachment.contentId.replace(/^<|>$/g, '');
|
|
1371
1459
|
if (contentId && messageInfo.text.html.indexOf(contentId) >= 0) {
|
|
1372
1460
|
attachmentList.set(contentId, {
|
|
@@ -1786,7 +1874,10 @@ class Mailbox {
|
|
|
1786
1874
|
* @param {Object} changes - What changed
|
|
1787
1875
|
*/
|
|
1788
1876
|
async processChanges(messageData, changes) {
|
|
1789
|
-
let packedUid = await this.
|
|
1877
|
+
let packedUid = await this.packUidWithLogging(messageData.uid, 'update');
|
|
1878
|
+
if (!packedUid) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1790
1881
|
await this.connection.notify(this, MESSAGE_UPDATED_NOTIFY, {
|
|
1791
1882
|
id: packedUid,
|
|
1792
1883
|
uid: messageData.uid,
|
|
@@ -2303,7 +2394,7 @@ class Mailbox {
|
|
|
2303
2394
|
let partList = [];
|
|
2304
2395
|
|
|
2305
2396
|
// Find images referenced by CID
|
|
2306
|
-
for (let attachment of messageInfo.attachments) {
|
|
2397
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
2307
2398
|
let contentId = attachment.contentId && attachment.contentId.replace(/^<|>$/g, '');
|
|
2308
2399
|
if (contentId && messageInfo.text.html.indexOf(contentId) >= 0) {
|
|
2309
2400
|
attachmentList.set(contentId, { attachment, content: null });
|
|
@@ -112,7 +112,7 @@ class Subconnection extends EventEmitter {
|
|
|
112
112
|
this.disabledReason = false;
|
|
113
113
|
} catch (err) {
|
|
114
114
|
// ended in an unconncted state
|
|
115
|
-
this.logger.
|
|
115
|
+
this.logger.warn({ msg: 'Failed to set up subconnection', err });
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -124,11 +124,11 @@ class Subconnection extends EventEmitter {
|
|
|
124
124
|
try {
|
|
125
125
|
prevImapClient.removeAllListeners();
|
|
126
126
|
prevImapClient.once('error', err => {
|
|
127
|
-
this.logger.
|
|
127
|
+
this.logger.warn({ msg: 'IMAP connection error', previous: true, account: this.account, err });
|
|
128
128
|
});
|
|
129
129
|
prevImapClient.close();
|
|
130
130
|
} catch (err) {
|
|
131
|
-
this.logger.
|
|
131
|
+
this.logger.warn({ msg: 'IMAP close error', err });
|
|
132
132
|
} finally {
|
|
133
133
|
if (prevImapClient === this.imapClient) {
|
|
134
134
|
this.imapClient = null;
|
|
@@ -153,13 +153,13 @@ class Subconnection extends EventEmitter {
|
|
|
153
153
|
imapClient.subConnection = true;
|
|
154
154
|
|
|
155
155
|
imapClient.on('error', err => {
|
|
156
|
-
imapClient?.log.
|
|
156
|
+
imapClient?.log.warn({ msg: 'IMAP connection error', account: this.account, err });
|
|
157
157
|
if (imapClient !== this.imapClient || this._connecting) {
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
imapClient.close();
|
|
161
161
|
this.reconnect().catch(err => {
|
|
162
|
-
this.logger.
|
|
162
|
+
this.logger.warn({ msg: 'IMAP reconnection error', account: this.account, err });
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
|
|
@@ -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
|
|
|
@@ -498,7 +537,9 @@ class IMAPClient extends BaseClient {
|
|
|
498
537
|
|
|
499
538
|
const connectionClient = await this.getImapConnection(connectionOptions, 'getCurrentListing');
|
|
500
539
|
if (!connectionClient) {
|
|
501
|
-
this.imapClient
|
|
540
|
+
if (this.imapClient) {
|
|
541
|
+
this.imapClient.close();
|
|
542
|
+
}
|
|
502
543
|
let error = new Error('Failed to get connection');
|
|
503
544
|
error.code = 'ConnectionError';
|
|
504
545
|
throw error;
|
|
@@ -522,7 +563,9 @@ class IMAPClient extends BaseClient {
|
|
|
522
563
|
let listing = await connectionClient.list(options);
|
|
523
564
|
if (!listing.length) {
|
|
524
565
|
// server bug, the list can never be empty
|
|
525
|
-
this.imapClient
|
|
566
|
+
if (this.imapClient) {
|
|
567
|
+
this.imapClient.close();
|
|
568
|
+
}
|
|
526
569
|
let error = new Error('Server bug: empty mailbox listing');
|
|
527
570
|
error.code = 'ServerBug';
|
|
528
571
|
throw error;
|
|
@@ -879,7 +922,7 @@ class IMAPClient extends BaseClient {
|
|
|
879
922
|
}
|
|
880
923
|
} catch (err) {
|
|
881
924
|
// ended in an unconncted state
|
|
882
|
-
this.logger.
|
|
925
|
+
this.logger.warn({ msg: 'Failed to set up connection, will retry', err });
|
|
883
926
|
|
|
884
927
|
// Calculate delay with capped exponential backoff
|
|
885
928
|
const retryDelay = Math.min(this.reconnectMaxDelay, 1000 * Math.pow(1.5, Math.min(this.reconnectRetries, 10)));
|
|
@@ -898,7 +941,7 @@ class IMAPClient extends BaseClient {
|
|
|
898
941
|
this.reconnectRetries = 0;
|
|
899
942
|
})
|
|
900
943
|
.catch(err => {
|
|
901
|
-
this.logger.
|
|
944
|
+
this.logger.warn({
|
|
902
945
|
msg: 'Connection retry failed',
|
|
903
946
|
err,
|
|
904
947
|
attempt: this.reconnectRetries
|
|
@@ -935,6 +978,10 @@ class IMAPClient extends BaseClient {
|
|
|
935
978
|
|
|
936
979
|
// Sync existing folders that weren't just synced
|
|
937
980
|
for (let mailbox of this.mailboxes.values()) {
|
|
981
|
+
if (!this.imapClient || !this.imapClient.usable) {
|
|
982
|
+
this.logger.debug({ msg: 'IMAP client disconnected during mailbox sync' });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
938
985
|
if (!synced || !synced.has(mailbox)) {
|
|
939
986
|
await mailbox.sync();
|
|
940
987
|
}
|
|
@@ -949,12 +996,19 @@ class IMAPClient extends BaseClient {
|
|
|
949
996
|
this.state = 'connected';
|
|
950
997
|
await this.setStateVal();
|
|
951
998
|
|
|
999
|
+
// Capture local reference to avoid race conditions during async operations
|
|
1000
|
+
const imapClient = this.imapClient;
|
|
1001
|
+
if (!imapClient || !imapClient.usable) {
|
|
1002
|
+
this.logger.debug({ msg: 'IMAP client disconnected or not usable after state update' });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
952
1006
|
// Store IMAP server capabilities for reference
|
|
953
|
-
const capabilities = (
|
|
1007
|
+
const capabilities = (imapClient.rawCapabilities || []).map(entry => entry && entry.value).filter(entry => entry);
|
|
954
1008
|
const authCapabilities = [];
|
|
955
1009
|
let lastUsedAuthCapability = null;
|
|
956
|
-
if (
|
|
957
|
-
for (let [authCapa, usedAuth] of
|
|
1010
|
+
if (imapClient.authCapabilities) {
|
|
1011
|
+
for (let [authCapa, usedAuth] of imapClient.authCapabilities) {
|
|
958
1012
|
authCapabilities.push(authCapa);
|
|
959
1013
|
if (usedAuth) {
|
|
960
1014
|
lastUsedAuthCapability = authCapa;
|
|
@@ -962,7 +1016,7 @@ class IMAPClient extends BaseClient {
|
|
|
962
1016
|
}
|
|
963
1017
|
}
|
|
964
1018
|
|
|
965
|
-
const serverInfo = Object.assign({},
|
|
1019
|
+
const serverInfo = Object.assign({}, imapClient.serverInfo || {}, {
|
|
966
1020
|
capabilities,
|
|
967
1021
|
authCapabilities,
|
|
968
1022
|
lastUsedAuthCapability
|
|
@@ -980,6 +1034,12 @@ class IMAPClient extends BaseClient {
|
|
|
980
1034
|
return;
|
|
981
1035
|
}
|
|
982
1036
|
|
|
1037
|
+
// Check if client disconnected before mailbox selection
|
|
1038
|
+
if (!this.imapClient || !this.imapClient.usable) {
|
|
1039
|
+
this.logger.debug({ msg: 'IMAP client disconnected before mailbox selection' });
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
983
1043
|
this.logger.debug({ msg: 'Syncing completed, selecting main path', path: mainPath });
|
|
984
1044
|
// start waiting for changes
|
|
985
1045
|
await this.select(mainPath);
|
|
@@ -1195,7 +1255,7 @@ class IMAPClient extends BaseClient {
|
|
|
1195
1255
|
prevImapClient.removeAllListeners();
|
|
1196
1256
|
|
|
1197
1257
|
const prevImapErrorHandler = err => {
|
|
1198
|
-
this.logger.
|
|
1258
|
+
this.logger.warn({ msg: 'IMAP connection error', type: 'imapClient', previous: true, account: this.account, err });
|
|
1199
1259
|
};
|
|
1200
1260
|
|
|
1201
1261
|
prevImapClient.once('error', prevImapErrorHandler);
|
|
@@ -1207,7 +1267,7 @@ class IMAPClient extends BaseClient {
|
|
|
1207
1267
|
this.commandClient.close();
|
|
1208
1268
|
}
|
|
1209
1269
|
} catch (err) {
|
|
1210
|
-
this.logger.
|
|
1270
|
+
this.logger.warn({ msg: 'IMAP close error', err });
|
|
1211
1271
|
} finally {
|
|
1212
1272
|
if (prevImapClient === this.imapClient) {
|
|
1213
1273
|
this.imapClient = null;
|
|
@@ -1267,7 +1327,7 @@ class IMAPClient extends BaseClient {
|
|
|
1267
1327
|
|
|
1268
1328
|
// Handle connection errors
|
|
1269
1329
|
imapClient.on('error', err => {
|
|
1270
|
-
imapClient?.log.
|
|
1330
|
+
imapClient?.log.warn({ msg: 'IMAP connection error', type: 'imapClient', account: this.account, err });
|
|
1271
1331
|
if (imapClient !== this.imapClient || this._connecting) {
|
|
1272
1332
|
return;
|
|
1273
1333
|
}
|
|
@@ -1291,7 +1351,7 @@ class IMAPClient extends BaseClient {
|
|
|
1291
1351
|
this.errorReconnectDelay = 2000;
|
|
1292
1352
|
})
|
|
1293
1353
|
.catch(err => {
|
|
1294
|
-
this.logger.
|
|
1354
|
+
this.logger.warn({
|
|
1295
1355
|
msg: 'IMAP reconnection error',
|
|
1296
1356
|
account: this.account,
|
|
1297
1357
|
err,
|
|
@@ -2303,7 +2363,7 @@ class IMAPClient extends BaseClient {
|
|
|
2303
2363
|
let uploadResponse = await connectionClient.append(data.path, raw, data.flags, data.internalDate);
|
|
2304
2364
|
|
|
2305
2365
|
// Return to IDLE if using primary connection
|
|
2306
|
-
if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
2366
|
+
if (this.imapClient && connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
2307
2367
|
// force back to IDLE
|
|
2308
2368
|
this.imapClient.idle().catch(err => {
|
|
2309
2369
|
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) {
|