emailengine-app 2.61.0 → 2.61.2
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 +16 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account/account-state.js +248 -0
- package/lib/account.js +17 -178
- package/lib/api-routes/account-routes.js +1006 -0
- package/lib/api-routes/message-routes.js +1377 -0
- package/lib/consts.js +12 -2
- package/lib/email-client/base-client.js +282 -771
- package/lib/email-client/gmail/gmail-api.js +243 -0
- package/lib/email-client/gmail-client.js +145 -53
- package/lib/email-client/imap/mailbox.js +24 -698
- package/lib/email-client/imap/sync-operations.js +812 -0
- package/lib/email-client/imap-client.js +3 -1
- package/lib/email-client/message-builder.js +566 -0
- package/lib/email-client/notification-handler.js +314 -0
- package/lib/email-client/outlook/graph-api.js +326 -0
- package/lib/email-client/outlook-client.js +159 -113
- package/lib/email-client/smtp-pool-manager.js +196 -0
- package/lib/imapproxy/imap-server.js +3 -12
- package/lib/oauth/gmail.js +4 -4
- package/lib/oauth/mail-ru.js +30 -5
- package/lib/oauth/outlook.js +57 -3
- package/lib/oauth/pubsub/google.js +30 -11
- package/lib/oauth/scope-checker.js +202 -0
- package/lib/oauth2-apps.js +8 -4
- package/lib/redis-operations.js +484 -0
- package/lib/routes-ui.js +283 -2582
- package/lib/tools.js +5 -196
- package/lib/ui-routes/account-routes.js +1931 -0
- package/lib/ui-routes/admin-config-routes.js +1233 -0
- package/lib/ui-routes/admin-entities-routes.js +2367 -0
- package/lib/ui-routes/oauth-routes.js +992 -0
- package/lib/utils/network.js +237 -0
- package/package.json +12 -12
- package/sbom.json +1 -1
- package/static/js/app.js +5 -5
- package/static/licenses.html +91 -21
- package/translations/de.mo +0 -0
- package/translations/de.po +85 -82
- package/translations/en.mo +0 -0
- package/translations/en.po +63 -71
- package/translations/et.mo +0 -0
- package/translations/et.po +84 -82
- package/translations/fr.mo +0 -0
- package/translations/fr.po +85 -82
- package/translations/ja.mo +0 -0
- package/translations/ja.po +84 -82
- package/translations/messages.pot +67 -80
- package/translations/nl.mo +0 -0
- package/translations/nl.po +86 -82
- package/translations/pl.mo +0 -0
- package/translations/pl.po +84 -82
- package/views/account/security.hbs +4 -4
- package/views/accounts/account.hbs +13 -13
- package/views/accounts/register/imap-server.hbs +12 -12
- package/views/config/document-store/pre-processing/index.hbs +4 -2
- package/views/config/oauth/app.hbs +6 -7
- package/views/config/oauth/index.hbs +2 -2
- package/views/config/service.hbs +3 -4
- package/views/dashboard.hbs +5 -7
- package/views/error.hbs +22 -7
- package/views/gateways/gateway.hbs +2 -2
- package/views/partials/add_account_modal.hbs +7 -10
- package/views/partials/document_store_header.hbs +1 -1
- package/views/partials/editor_scope_info.hbs +0 -1
- package/views/partials/oauth_config_header.hbs +1 -1
- package/views/partials/side_menu.hbs +3 -3
- package/views/partials/webhook_form.hbs +2 -2
- package/views/templates/index.hbs +1 -1
- package/views/templates/template.hbs +8 -8
- package/views/tokens/index.hbs +6 -6
- package/views/tokens/new.hbs +1 -1
- package/views/webhooks/index.hbs +4 -4
- package/views/webhooks/webhook.hbs +7 -7
- package/workers/api.js +148 -2436
- package/workers/smtp.js +2 -1
- package/workers/webhooks.js +6 -0
- package/lib/imapproxy/imap-core/test/client.js +0 -46
- package/lib/imapproxy/imap-core/test/fixtures/append.eml +0 -1196
- package/lib/imapproxy/imap-core/test/fixtures/chunks.js +0 -44
- package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +0 -32
- package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +0 -6
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +0 -2740
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +0 -1411
- package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +0 -85
- package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +0 -582
- package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +0 -599
- package/lib/imapproxy/imap-core/test/fixtures/simple.eml +0 -42
- package/lib/imapproxy/imap-core/test/fixtures/simple.json +0 -164
- package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +0 -671
- package/lib/imapproxy/imap-core/test/imap-compiler-test.js +0 -272
- package/lib/imapproxy/imap-core/test/imap-indexer-test.js +0 -236
- package/lib/imapproxy/imap-core/test/imap-parser-test.js +0 -922
- package/lib/imapproxy/imap-core/test/memory-notifier.js +0 -129
- package/lib/imapproxy/imap-core/test/prepare.sh +0 -74
- package/lib/imapproxy/imap-core/test/protocol-test.js +0 -1756
- package/lib/imapproxy/imap-core/test/search-test.js +0 -1356
- package/lib/imapproxy/imap-core/test/test-client.js +0 -152
- package/lib/imapproxy/imap-core/test/test-server.js +0 -623
- package/lib/imapproxy/imap-core/test/tools-test.js +0 -22
- package/test/api-test.js +0 -899
- package/test/autoreply-test.js +0 -327
- package/test/bounce-test.js +0 -151
- package/test/complaint-test.js +0 -256
- package/test/fixtures/autoreply/LICENSE +0 -27
- package/test/fixtures/autoreply/rfc3834-01.eml +0 -23
- package/test/fixtures/autoreply/rfc3834-02.eml +0 -24
- package/test/fixtures/autoreply/rfc3834-03.eml +0 -26
- package/test/fixtures/autoreply/rfc3834-04.eml +0 -48
- package/test/fixtures/autoreply/rfc3834-05.eml +0 -19
- package/test/fixtures/autoreply/rfc3834-06.eml +0 -59
- package/test/fixtures/bounces/163.eml +0 -2521
- package/test/fixtures/bounces/fastmail.eml +0 -242
- package/test/fixtures/bounces/gmail.eml +0 -252
- package/test/fixtures/bounces/hotmail.eml +0 -655
- package/test/fixtures/bounces/mailru.eml +0 -121
- package/test/fixtures/bounces/outlook.eml +0 -1107
- package/test/fixtures/bounces/postfix.eml +0 -101
- package/test/fixtures/bounces/rambler.eml +0 -116
- package/test/fixtures/bounces/workmail.eml +0 -142
- package/test/fixtures/bounces/yahoo.eml +0 -139
- package/test/fixtures/bounces/zoho.eml +0 -83
- package/test/fixtures/bounces/zonemta.eml +0 -100
- package/test/fixtures/complaints/LICENSE +0 -27
- package/test/fixtures/complaints/amazonses.eml +0 -72
- package/test/fixtures/complaints/dmarc.eml +0 -59
- package/test/fixtures/complaints/hotmail.eml +0 -49
- package/test/fixtures/complaints/optout.eml +0 -40
- package/test/fixtures/complaints/standard-arf.eml +0 -68
- package/test/fixtures/complaints/yahoo.eml +0 -68
- package/test/oauth2-apps-test.js +0 -301
- package/test/sendonly-test.js +0 -160
- package/test/test-config.js +0 -34
- package/test/webhooks-server.js +0 -39
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { compareExisting, calculateFetchBackoff, readEnvValue } = require('../../tools');
|
|
5
|
+
const config = require('@zone-eu/wild-config');
|
|
6
|
+
const { DEFAULT_FETCH_BATCH_SIZE } = require('../../consts');
|
|
7
|
+
|
|
8
|
+
// Configurable batch size for fetching messages (default: 250)
|
|
9
|
+
const FETCH_BATCH_SIZE = Number(readEnvValue('EENGINE_FETCH_BATCH_SIZE') || config.service.fetchBatchSize) || DEFAULT_FETCH_BATCH_SIZE;
|
|
10
|
+
|
|
11
|
+
// Do not check for flag updates using full sync more often than this value (30 minutes)
|
|
12
|
+
const FULL_SYNC_DELAY = 30 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculates the next range of sequence numbers to fetch based on the last fetched range
|
|
16
|
+
* @param {Number} totalMessages - Total number of messages in the mailbox
|
|
17
|
+
* @param {String} lastRange - Last fetched range in format "start:end" or "start:*"
|
|
18
|
+
* @returns {String|false} Next range to fetch or false if no more messages
|
|
19
|
+
*/
|
|
20
|
+
function getFetchRange(totalMessages, lastRange) {
|
|
21
|
+
let lastEndMarker = lastRange ? lastRange.split(':').pop() : false;
|
|
22
|
+
if (lastEndMarker === '*') {
|
|
23
|
+
// Already fetched to the end
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
let lastSeq = lastRange ? Number(lastEndMarker) : 0;
|
|
27
|
+
let startSeq = lastSeq + 1;
|
|
28
|
+
if (startSeq > totalMessages) {
|
|
29
|
+
// No more messages to fetch
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
let endMarker = startSeq + FETCH_BATCH_SIZE - 1;
|
|
33
|
+
if (endMarker >= totalMessages) {
|
|
34
|
+
// Use * to fetch to the end
|
|
35
|
+
endMarker = '*';
|
|
36
|
+
}
|
|
37
|
+
return `${startSeq}:${endMarker}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Determines if a full sync should be skipped based on timing
|
|
42
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
43
|
+
* @returns {Boolean} True if full sync was performed recently
|
|
44
|
+
*/
|
|
45
|
+
function isRecentFullSync(storedStatus) {
|
|
46
|
+
return storedStatus.lastFullSync && storedStatus.lastFullSync >= new Date(Date.now() - FULL_SYNC_DELAY);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks if UIDVALIDITY has changed (mailbox was recreated)
|
|
51
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
52
|
+
* @param {Object} mailboxStatus - Current mailbox status from IMAP
|
|
53
|
+
* @returns {Boolean} True if UIDVALIDITY changed
|
|
54
|
+
*/
|
|
55
|
+
function hasUidValidityChanged(storedStatus, mailboxStatus) {
|
|
56
|
+
return 'uidValidity' in storedStatus && mailboxStatus.uidValidity !== storedStatus.uidValidity;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks if MODSEQ indicates no changes
|
|
61
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
62
|
+
* @param {Object} mailboxStatus - Current mailbox status from IMAP
|
|
63
|
+
* @returns {Boolean} True if MODSEQ is unchanged
|
|
64
|
+
*/
|
|
65
|
+
function hasNoModseqChanges(storedStatus, mailboxStatus) {
|
|
66
|
+
return storedStatus.highestModseq && storedStatus.highestModseq === mailboxStatus.highestModseq;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Determines if partial sync can be used based on CONDSTORE support
|
|
71
|
+
* @param {Object} imapClient - IMAP client with enabled extensions
|
|
72
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
73
|
+
* @param {Object} mailboxStatus - Current mailbox status from IMAP
|
|
74
|
+
* @returns {Boolean} True if partial sync with CONDSTORE is appropriate
|
|
75
|
+
*/
|
|
76
|
+
function canUseCondstorePartialSync(imapClient, storedStatus, mailboxStatus) {
|
|
77
|
+
return (
|
|
78
|
+
imapClient.enabled.has('CONDSTORE') &&
|
|
79
|
+
storedStatus.highestModseq < mailboxStatus.highestModseq &&
|
|
80
|
+
storedStatus.messages <= mailboxStatus.messages &&
|
|
81
|
+
mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determines if partial sync can be used based on message count
|
|
87
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
88
|
+
* @param {Object} mailboxStatus - Current mailbox status from IMAP
|
|
89
|
+
* @returns {Boolean} True if only new messages were added
|
|
90
|
+
*/
|
|
91
|
+
function canUseSimplePartialSync(storedStatus, mailboxStatus) {
|
|
92
|
+
return storedStatus.messages < mailboxStatus.messages && mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Determines if sync can be skipped due to no changes
|
|
97
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
98
|
+
* @param {Object} mailboxStatus - Current mailbox status from IMAP
|
|
99
|
+
* @returns {Boolean} True if no sync is needed
|
|
100
|
+
*/
|
|
101
|
+
function canSkipSync(storedStatus, mailboxStatus) {
|
|
102
|
+
return storedStatus.messages === mailboxStatus.messages && storedStatus.uidNext === mailboxStatus.uidNext && isRecentFullSync(storedStatus);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Handles the synchronization of IMAP mailboxes
|
|
107
|
+
*/
|
|
108
|
+
class SyncOperations {
|
|
109
|
+
/**
|
|
110
|
+
* Creates a new SyncOperations instance
|
|
111
|
+
* @param {Object} mailbox - The parent Mailbox instance
|
|
112
|
+
*/
|
|
113
|
+
constructor(mailbox) {
|
|
114
|
+
this.mailbox = mailbox;
|
|
115
|
+
this.connection = mailbox.connection;
|
|
116
|
+
this.logger = mailbox.logger;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Performs full synchronization based on indexer type
|
|
121
|
+
* @returns {Promise} Resolves when sync is complete
|
|
122
|
+
*/
|
|
123
|
+
async fullSync() {
|
|
124
|
+
const imapIndexer = this.mailbox.imapIndexer;
|
|
125
|
+
|
|
126
|
+
this.logger.trace({ msg: 'Running full sync', imapIndexer });
|
|
127
|
+
|
|
128
|
+
if (imapIndexer === 'fast') {
|
|
129
|
+
return this.runFastSync();
|
|
130
|
+
}
|
|
131
|
+
return this.runFullSync();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Performs partial synchronization based on indexer type
|
|
136
|
+
* @param {Object} storedStatus - Current stored mailbox status
|
|
137
|
+
* @returns {Promise} Resolves when sync is complete
|
|
138
|
+
*/
|
|
139
|
+
async partialSync(storedStatus) {
|
|
140
|
+
const imapIndexer = this.mailbox.imapIndexer;
|
|
141
|
+
|
|
142
|
+
this.logger.trace({ msg: 'Running partial sync', imapIndexer });
|
|
143
|
+
|
|
144
|
+
if (imapIndexer === 'fast') {
|
|
145
|
+
return this.runFastSync(storedStatus);
|
|
146
|
+
}
|
|
147
|
+
return this.runPartialSync(storedStatus);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Determines the appropriate sync strategy based on mailbox state
|
|
152
|
+
* @param {Object} storedStatus - Stored mailbox status
|
|
153
|
+
* @param {Object} mailboxStatus - Current mailbox status from IMAP
|
|
154
|
+
* @returns {Object} Sync decision with type and reason
|
|
155
|
+
*/
|
|
156
|
+
determineSyncStrategy(storedStatus, mailboxStatus) {
|
|
157
|
+
// No changes if MODSEQ hasn't changed
|
|
158
|
+
if (hasNoModseqChanges(storedStatus, mailboxStatus)) {
|
|
159
|
+
return { type: 'none', reason: 'modseq_unchanged' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// No changes if mailbox is empty
|
|
163
|
+
if (storedStatus.messages === 0 && mailboxStatus.messages === 0) {
|
|
164
|
+
return { type: 'none', reason: 'empty_mailbox' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Partial sync if CONDSTORE indicates only new messages or flag changes
|
|
168
|
+
if (canUseCondstorePartialSync(this.connection.imapClient, storedStatus, mailboxStatus)) {
|
|
169
|
+
return { type: 'partial', reason: 'condstore_changes' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Partial sync if only new messages
|
|
173
|
+
if (canUseSimplePartialSync(storedStatus, mailboxStatus)) {
|
|
174
|
+
return { type: 'partial', reason: 'new_messages_only' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Skip if nothing changed and recent full sync
|
|
178
|
+
if (canSkipSync(storedStatus, mailboxStatus)) {
|
|
179
|
+
return { type: 'none', reason: 'recent_full_sync' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Full sync for all other cases
|
|
183
|
+
return { type: 'full', reason: 'changes_detected' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Fast sync mode - only tracks new messages, doesn't maintain full message list
|
|
188
|
+
* More efficient for large mailboxes where we only care about new messages
|
|
189
|
+
* @param {Object} storedStatus - Current stored mailbox status
|
|
190
|
+
*/
|
|
191
|
+
async runFastSync(storedStatus) {
|
|
192
|
+
storedStatus = storedStatus || (await this.mailbox.getStoredStatus());
|
|
193
|
+
let mailboxStatus = this.mailbox.getMailboxStatus();
|
|
194
|
+
|
|
195
|
+
let lock = await this.mailbox.getMailboxLock(null, { description: 'Fast sync' });
|
|
196
|
+
this.connection.syncing = true;
|
|
197
|
+
this.mailbox.syncing = true;
|
|
198
|
+
try {
|
|
199
|
+
if (!this.connection.imapClient) {
|
|
200
|
+
throw new Error('IMAP connection not available');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let knownUidNext = typeof storedStatus.uidNext === 'number' ? storedStatus.uidNext || 1 : 1;
|
|
204
|
+
|
|
205
|
+
if (knownUidNext && mailboxStatus.messages) {
|
|
206
|
+
// detected new emails
|
|
207
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
208
|
+
|
|
209
|
+
let imapClient = this.connection.imapClient;
|
|
210
|
+
|
|
211
|
+
// If we have not yet scanned this folder, then start by finding the earliest matching email
|
|
212
|
+
if (typeof storedStatus.uidNext !== 'number' && this.connection.notifyFrom && this.connection.notifyFrom < new Date()) {
|
|
213
|
+
// Find first message after notifyFrom date
|
|
214
|
+
let matchingMessages = await imapClient.search({ since: this.connection.notifyFrom }, { uid: true });
|
|
215
|
+
if (matchingMessages) {
|
|
216
|
+
let earliestUid = matchingMessages[0];
|
|
217
|
+
if (earliestUid) {
|
|
218
|
+
knownUidNext = earliestUid;
|
|
219
|
+
} else if (mailboxStatus.uidNext) {
|
|
220
|
+
// no match, start from newest
|
|
221
|
+
knownUidNext = mailboxStatus.uidNext;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let range = `${knownUidNext}:*`;
|
|
227
|
+
let opts = {
|
|
228
|
+
uid: true
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Fetch messages with retry logic
|
|
232
|
+
let fetchCompleted = false;
|
|
233
|
+
let fetchRetryCount = 0;
|
|
234
|
+
|
|
235
|
+
while (!fetchCompleted) {
|
|
236
|
+
try {
|
|
237
|
+
let messages = [];
|
|
238
|
+
|
|
239
|
+
// Fetch all messages in range
|
|
240
|
+
for await (let messageData of imapClient.fetch(range, fields, opts)) {
|
|
241
|
+
if (!messageData || !messageData.uid) {
|
|
242
|
+
//TODO: support partial responses
|
|
243
|
+
this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range, fields, opts } });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ignore Recent flag
|
|
248
|
+
messageData.flags.delete('\\Recent');
|
|
249
|
+
|
|
250
|
+
messages.push(messageData);
|
|
251
|
+
}
|
|
252
|
+
// ensure that messages are sorted by UID
|
|
253
|
+
messages = messages.sort((a, b) => a.uid - b.uid);
|
|
254
|
+
|
|
255
|
+
// Process each new message
|
|
256
|
+
for (let messageData of messages) {
|
|
257
|
+
// Update uidNext if this is a new message
|
|
258
|
+
let updated = await this.connection.redis.hUpdateBigger(
|
|
259
|
+
this.mailbox.getMailboxKey(),
|
|
260
|
+
'uidNext',
|
|
261
|
+
messageData.uid + 1,
|
|
262
|
+
messageData.uid + 1
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (updated) {
|
|
266
|
+
// new email! Queue for processing
|
|
267
|
+
await this.connection.redis.zadd(
|
|
268
|
+
this.mailbox.getNotificationsKey(),
|
|
269
|
+
messageData.uid,
|
|
270
|
+
JSON.stringify({
|
|
271
|
+
uid: messageData.uid,
|
|
272
|
+
flags: messageData.flags,
|
|
273
|
+
internalDate:
|
|
274
|
+
(messageData.internalDate &&
|
|
275
|
+
typeof messageData.internalDate.toISOString === 'function' &&
|
|
276
|
+
messageData.internalDate.toISOString()) ||
|
|
277
|
+
null
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// clear failure flag
|
|
285
|
+
await this.connection.redis.hdel(this.connection.getAccountKey(), 'syncError');
|
|
286
|
+
} catch (err) {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
fetchCompleted = true;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
try {
|
|
292
|
+
// set failure flag
|
|
293
|
+
await this.connection.redis.hSetExists(
|
|
294
|
+
this.connection.getAccountKey(),
|
|
295
|
+
'syncError',
|
|
296
|
+
JSON.stringify({
|
|
297
|
+
path: this.mailbox.path,
|
|
298
|
+
time: new Date().toISOString(),
|
|
299
|
+
error: {
|
|
300
|
+
error: err.message,
|
|
301
|
+
responseStatus: err.responseStatus,
|
|
302
|
+
responseText: err.responseText
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
// ignore
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Retry with exponential backoff
|
|
311
|
+
if (!imapClient.usable) {
|
|
312
|
+
// nothing to do here, connection closed
|
|
313
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, err });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
318
|
+
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s`, err });
|
|
319
|
+
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
320
|
+
|
|
321
|
+
if (!imapClient.usable) {
|
|
322
|
+
// nothing to do here, connection closed
|
|
323
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, err });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await this.mailbox.updateStoredStatus(this.mailbox.getMailboxStatus());
|
|
331
|
+
|
|
332
|
+
await this.mailbox.publishSyncedEvents(storedStatus);
|
|
333
|
+
} finally {
|
|
334
|
+
lock.release();
|
|
335
|
+
this.connection.syncing = false;
|
|
336
|
+
this.mailbox.syncing = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Partial sync - fetches only changed messages using MODSEQ or UID range
|
|
342
|
+
* Used for incremental updates when we know something changed
|
|
343
|
+
* @param {Object} storedStatus - Current stored mailbox status
|
|
344
|
+
*/
|
|
345
|
+
async runPartialSync(storedStatus) {
|
|
346
|
+
storedStatus = storedStatus || (await this.mailbox.getStoredStatus());
|
|
347
|
+
let mailboxStatus = this.mailbox.getMailboxStatus();
|
|
348
|
+
|
|
349
|
+
let lock = await this.mailbox.getMailboxLock(null, { description: 'Partial sync' });
|
|
350
|
+
this.connection.syncing = true;
|
|
351
|
+
this.mailbox.syncing = true;
|
|
352
|
+
try {
|
|
353
|
+
if (!this.connection.imapClient) {
|
|
354
|
+
throw new Error('IMAP connection not available');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
358
|
+
let range = '1:*';
|
|
359
|
+
let opts = {
|
|
360
|
+
uid: true
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Use CONDSTORE if available for efficient change detection
|
|
364
|
+
if (this.connection.imapClient.enabled.has('CONDSTORE') && storedStatus.highestModseq) {
|
|
365
|
+
// Only fetch messages changed since last known MODSEQ
|
|
366
|
+
opts.changedSince = storedStatus.highestModseq;
|
|
367
|
+
} else if (storedStatus.uidNext) {
|
|
368
|
+
// Fall back to fetching new messages only
|
|
369
|
+
range = `${storedStatus.uidNext}:*`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (mailboxStatus.messages) {
|
|
373
|
+
// only fetch messages if there are some
|
|
374
|
+
let fetchCompleted = false;
|
|
375
|
+
let fetchRetryCount = 0;
|
|
376
|
+
while (!fetchCompleted) {
|
|
377
|
+
// Get fresh imapClient reference inside retry loop
|
|
378
|
+
let imapClient = this.connection.imapClient;
|
|
379
|
+
if (!imapClient || !imapClient.usable) {
|
|
380
|
+
this.logger.error({ msg: 'IMAP client not available for partial sync' });
|
|
381
|
+
throw new Error('IMAP connection not available');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
// Fetch and process each message
|
|
386
|
+
for await (let messageData of imapClient.fetch(range, fields, opts)) {
|
|
387
|
+
if (!messageData || !messageData.uid) {
|
|
388
|
+
//TODO: support partial responses
|
|
389
|
+
this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range, fields, opts } });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ignore Recent flag
|
|
394
|
+
messageData.flags.delete('\\Recent');
|
|
395
|
+
|
|
396
|
+
let storedMessage = await this.mailbox.entryListGet(messageData.uid, { uid: true });
|
|
397
|
+
|
|
398
|
+
let changes;
|
|
399
|
+
if (!storedMessage) {
|
|
400
|
+
// New message
|
|
401
|
+
let seq = await this.mailbox.entryListSet(messageData);
|
|
402
|
+
if (seq) {
|
|
403
|
+
// Queue for processing
|
|
404
|
+
await this.connection.redis.zadd(
|
|
405
|
+
this.mailbox.getNotificationsKey(),
|
|
406
|
+
messageData.uid,
|
|
407
|
+
JSON.stringify({
|
|
408
|
+
uid: messageData.uid,
|
|
409
|
+
flags: messageData.flags,
|
|
410
|
+
internalDate:
|
|
411
|
+
(messageData.internalDate &&
|
|
412
|
+
typeof messageData.internalDate.toISOString === 'function' &&
|
|
413
|
+
messageData.internalDate.toISOString()) ||
|
|
414
|
+
null
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} else if ((changes = compareExisting(storedMessage.entry, messageData))) {
|
|
419
|
+
// Existing message with changes
|
|
420
|
+
let seq = await this.mailbox.entryListSet(messageData);
|
|
421
|
+
if (seq) {
|
|
422
|
+
await this.mailbox.processChanges(messageData, changes);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
// clear failure flag
|
|
428
|
+
await this.connection.redis.hdel(this.connection.getAccountKey(), 'syncError');
|
|
429
|
+
} catch (err) {
|
|
430
|
+
// ignore
|
|
431
|
+
}
|
|
432
|
+
fetchCompleted = true;
|
|
433
|
+
} catch (err) {
|
|
434
|
+
try {
|
|
435
|
+
// set failure flag
|
|
436
|
+
await this.connection.redis.hSetExists(
|
|
437
|
+
this.connection.getAccountKey(),
|
|
438
|
+
'syncError',
|
|
439
|
+
JSON.stringify({
|
|
440
|
+
path: this.mailbox.path,
|
|
441
|
+
time: new Date().toISOString(),
|
|
442
|
+
error: {
|
|
443
|
+
error: err.message,
|
|
444
|
+
responseStatus: err.responseStatus,
|
|
445
|
+
responseText: err.responseText
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
// ignore
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Retry with exponential backoff
|
|
454
|
+
if (!imapClient.usable) {
|
|
455
|
+
// nothing to do here, connection closed
|
|
456
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying` });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
461
|
+
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s` });
|
|
462
|
+
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
463
|
+
|
|
464
|
+
if (!imapClient.usable) {
|
|
465
|
+
// nothing to do here, connection closed
|
|
466
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying` });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await this.mailbox.updateStoredStatus(this.mailbox.getMailboxStatus());
|
|
474
|
+
|
|
475
|
+
await this.mailbox.publishSyncedEvents(storedStatus);
|
|
476
|
+
} finally {
|
|
477
|
+
lock.release();
|
|
478
|
+
this.connection.syncing = false;
|
|
479
|
+
this.mailbox.syncing = false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Full sync - fetches all messages and detects additions, deletions, and changes
|
|
485
|
+
* Most thorough but slowest sync method
|
|
486
|
+
*/
|
|
487
|
+
async runFullSync() {
|
|
488
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
489
|
+
let opts = {};
|
|
490
|
+
|
|
491
|
+
let lock = await this.mailbox.getMailboxLock(null, { description: 'Full sync' });
|
|
492
|
+
this.connection.syncing = true;
|
|
493
|
+
this.mailbox.syncing = true;
|
|
494
|
+
try {
|
|
495
|
+
// Generate unique ID for this sync loop to track batch ordering
|
|
496
|
+
const loopId = crypto.randomUUID();
|
|
497
|
+
|
|
498
|
+
// Wait for next tick to ensure ImapFlow has processed all untagged responses from SELECT
|
|
499
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
500
|
+
|
|
501
|
+
let mailboxStatus = this.mailbox.getMailboxStatus();
|
|
502
|
+
|
|
503
|
+
this.logger.debug({
|
|
504
|
+
msg: 'Starting full sync',
|
|
505
|
+
code: 'full_sync_start',
|
|
506
|
+
loopId,
|
|
507
|
+
mailboxStatus,
|
|
508
|
+
imapClientExists: mailboxStatus.messages
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Track highest sequence number seen
|
|
512
|
+
let seqMax = 0;
|
|
513
|
+
let changes;
|
|
514
|
+
|
|
515
|
+
// Get current message count for deletion detection
|
|
516
|
+
let storedMaxSeqOld = await this.connection.redis.zcard(this.mailbox.getMessagesKey());
|
|
517
|
+
|
|
518
|
+
let responseCounters = {
|
|
519
|
+
empty: 0,
|
|
520
|
+
partial: 0,
|
|
521
|
+
messages: 0
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
if (mailboxStatus.messages) {
|
|
525
|
+
this.logger.debug({
|
|
526
|
+
msg: 'Running FETCH',
|
|
527
|
+
code: 'run_fetch',
|
|
528
|
+
query: { fields, opts },
|
|
529
|
+
expectedMessages: mailboxStatus.messages,
|
|
530
|
+
mailbox: mailboxStatus,
|
|
531
|
+
maxBatchSize: FETCH_BATCH_SIZE,
|
|
532
|
+
expectedBatches: Math.ceil(mailboxStatus.messages / FETCH_BATCH_SIZE)
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Process messages in batches to avoid memory issues
|
|
536
|
+
let range = false;
|
|
537
|
+
let lastHighestUid = 0;
|
|
538
|
+
let batchNumber = 0;
|
|
539
|
+
// process messages in batches
|
|
540
|
+
while ((range = getFetchRange(mailboxStatus.messages, range))) {
|
|
541
|
+
batchNumber++;
|
|
542
|
+
this.logger.debug({
|
|
543
|
+
msg: 'Processing batch',
|
|
544
|
+
code: 'fetch_batch',
|
|
545
|
+
loopId,
|
|
546
|
+
batchNumber,
|
|
547
|
+
range,
|
|
548
|
+
totalMessages: mailboxStatus.messages,
|
|
549
|
+
previousRange: batchNumber > 1 ? 'calculated' : 'initial'
|
|
550
|
+
});
|
|
551
|
+
let fetchCompleted = false;
|
|
552
|
+
let fetchRetryCount = 0;
|
|
553
|
+
while (!fetchCompleted) {
|
|
554
|
+
// Get fresh imapClient reference inside retry loop
|
|
555
|
+
// This ensures we use the current connection state
|
|
556
|
+
const imapClient = this.connection.imapClient;
|
|
557
|
+
if (!imapClient || !imapClient.usable) {
|
|
558
|
+
this.logger.error({ msg: 'IMAP client not available for FETCH' });
|
|
559
|
+
throw new Error('IMAP connection not available');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
this.logger.debug({
|
|
564
|
+
msg: 'Starting FETCH command',
|
|
565
|
+
code: 'fetch_start',
|
|
566
|
+
loopId,
|
|
567
|
+
batchNumber,
|
|
568
|
+
range,
|
|
569
|
+
retryCount: fetchRetryCount,
|
|
570
|
+
totalMessages: mailboxStatus.messages
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
for await (let messageData of imapClient.fetch(range, fields, opts)) {
|
|
574
|
+
if (!messageData) {
|
|
575
|
+
this.logger.debug({ msg: 'Empty FETCH response', code: 'empty_fetch', query: { range, fields, opts } });
|
|
576
|
+
responseCounters.empty++;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!messageData.uid || (fields.flags && !messageData.flags)) {
|
|
581
|
+
// TODO: support partial responses
|
|
582
|
+
// For now, without UID or FLAGS there's nothing to do
|
|
583
|
+
this.logger.debug({
|
|
584
|
+
msg: 'Partial FETCH response',
|
|
585
|
+
code: 'partial_fetch',
|
|
586
|
+
query: { range, fields, opts },
|
|
587
|
+
responseKeys: Object.keys(messageData)
|
|
588
|
+
});
|
|
589
|
+
responseCounters.partial++;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (messageData.uid <= lastHighestUid) {
|
|
594
|
+
// already processed in the previous batch
|
|
595
|
+
// probably an older email was deleted which shifted message entries
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
lastHighestUid = messageData.uid;
|
|
599
|
+
|
|
600
|
+
responseCounters.messages++;
|
|
601
|
+
|
|
602
|
+
if (fields.internalDate && !messageData.internalDate) {
|
|
603
|
+
this.logger.debug({
|
|
604
|
+
msg: 'Missing INTERNALDATE',
|
|
605
|
+
code: 'fetch_date_missing',
|
|
606
|
+
query: { range, fields, opts },
|
|
607
|
+
responseKeys: Object.keys(messageData)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ignore Recent flag
|
|
612
|
+
messageData.flags.delete('\\Recent');
|
|
613
|
+
|
|
614
|
+
if (messageData.seq > seqMax) {
|
|
615
|
+
seqMax = messageData.seq;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let storedMessage = await this.mailbox.entryListGet(messageData.uid, { uid: true });
|
|
619
|
+
if (!storedMessage) {
|
|
620
|
+
// New message
|
|
621
|
+
let seq = await this.mailbox.entryListSet(messageData);
|
|
622
|
+
if (seq) {
|
|
623
|
+
await this.connection.redis.zadd(
|
|
624
|
+
this.mailbox.getNotificationsKey(),
|
|
625
|
+
messageData.uid,
|
|
626
|
+
JSON.stringify({
|
|
627
|
+
uid: messageData.uid,
|
|
628
|
+
flags: messageData.flags,
|
|
629
|
+
internalDate:
|
|
630
|
+
(messageData.internalDate &&
|
|
631
|
+
typeof messageData.internalDate.toISOString === 'function' &&
|
|
632
|
+
messageData.internalDate.toISOString()) ||
|
|
633
|
+
null
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
// Check for deleted messages between stored and current sequence
|
|
639
|
+
let diff = storedMessage.seq - messageData.seq;
|
|
640
|
+
if (diff) {
|
|
641
|
+
this.logger.trace({ msg: 'Deleted range', inloop: true, diff, start: messageData.seq });
|
|
642
|
+
}
|
|
643
|
+
// Process deletions
|
|
644
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
645
|
+
let seq = messageData.seq + i;
|
|
646
|
+
let deletedEntry = await this.mailbox.entryListExpunge(seq);
|
|
647
|
+
if (deletedEntry) {
|
|
648
|
+
await this.mailbox.processDeleted(deletedEntry);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Check for changes
|
|
653
|
+
if ((changes = compareExisting(storedMessage.entry, messageData))) {
|
|
654
|
+
let seq = await this.mailbox.entryListSet(messageData);
|
|
655
|
+
if (seq) {
|
|
656
|
+
await this.mailbox.processChanges(messageData, changes);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
// clear failure flag
|
|
664
|
+
await this.connection.redis.hdel(this.connection.getAccountKey(), 'syncError');
|
|
665
|
+
} catch (err) {
|
|
666
|
+
// ignore
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
this.logger.debug({
|
|
670
|
+
msg: 'FETCH completed successfully',
|
|
671
|
+
code: 'fetch_success',
|
|
672
|
+
loopId,
|
|
673
|
+
batchNumber,
|
|
674
|
+
range,
|
|
675
|
+
retryCount: fetchRetryCount
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
fetchCompleted = true;
|
|
679
|
+
} catch (err) {
|
|
680
|
+
this.logger.error({
|
|
681
|
+
msg: 'FETCH failed',
|
|
682
|
+
code: 'fetch_error',
|
|
683
|
+
loopId,
|
|
684
|
+
batchNumber,
|
|
685
|
+
range,
|
|
686
|
+
retryCount: fetchRetryCount,
|
|
687
|
+
totalMessages: mailboxStatus.messages,
|
|
688
|
+
error: err.message,
|
|
689
|
+
responseStatus: err.responseStatus,
|
|
690
|
+
responseText: err.responseText
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (!imapClient.usable) {
|
|
694
|
+
// nothing to do here, connection closed
|
|
695
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, loopId });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
// set failure flag
|
|
701
|
+
await this.connection.redis.hSetExists(
|
|
702
|
+
this.connection.getAccountKey(),
|
|
703
|
+
'syncError',
|
|
704
|
+
JSON.stringify({
|
|
705
|
+
path: this.mailbox.path,
|
|
706
|
+
time: new Date().toISOString(),
|
|
707
|
+
error: {
|
|
708
|
+
error: err.message,
|
|
709
|
+
responseStatus: err.responseStatus,
|
|
710
|
+
responseText: err.responseText
|
|
711
|
+
}
|
|
712
|
+
})
|
|
713
|
+
);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
// ignore
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Retry with exponential backoff
|
|
719
|
+
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
720
|
+
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s`, loopId, batchNumber });
|
|
721
|
+
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
722
|
+
|
|
723
|
+
if (!imapClient.usable) {
|
|
724
|
+
// nothing to do here, connection closed
|
|
725
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, loopId });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Verify we're still on the correct mailbox after the delay
|
|
730
|
+
// Another operation might have changed the mailbox while we were waiting
|
|
731
|
+
const currentMailbox = this.connection.imapClient.mailbox;
|
|
732
|
+
if (!currentMailbox || currentMailbox.path !== this.mailbox.path) {
|
|
733
|
+
this.logger.error({
|
|
734
|
+
msg: 'Mailbox changed during retry delay, aborting sync',
|
|
735
|
+
expectedPath: this.mailbox.path,
|
|
736
|
+
currentPath: currentMailbox ? currentMailbox.path : 'none',
|
|
737
|
+
loopId
|
|
738
|
+
});
|
|
739
|
+
throw new Error('Mailbox changed during sync operation');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Refresh mailbox status in case it changed
|
|
743
|
+
const oldMailboxMessages = mailboxStatus.messages;
|
|
744
|
+
mailboxStatus = this.mailbox.getMailboxStatus();
|
|
745
|
+
|
|
746
|
+
this.logger.debug({
|
|
747
|
+
msg: 'Refreshed mailbox status after error',
|
|
748
|
+
code: 'mailbox_status_refresh',
|
|
749
|
+
loopId,
|
|
750
|
+
batchNumber,
|
|
751
|
+
oldMessages: oldMailboxMessages,
|
|
752
|
+
newMessages: mailboxStatus.messages,
|
|
753
|
+
range
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Delete any messages that weren't seen in this sync
|
|
761
|
+
let storedMaxSeq = await this.connection.redis.zcard(this.mailbox.getMessagesKey());
|
|
762
|
+
let diff = storedMaxSeq - seqMax;
|
|
763
|
+
if (diff) {
|
|
764
|
+
this.logger.trace({
|
|
765
|
+
msg: 'Deleted range',
|
|
766
|
+
inloop: false,
|
|
767
|
+
diff,
|
|
768
|
+
start: seqMax + 1,
|
|
769
|
+
messagesKey: this.mailbox.getMessagesKey(),
|
|
770
|
+
zcard: storedMaxSeq,
|
|
771
|
+
zcardOld: storedMaxSeqOld,
|
|
772
|
+
responseCounters
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Process remaining deletions
|
|
777
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
778
|
+
let seq = seqMax + i + 1;
|
|
779
|
+
let deletedEntry = await this.mailbox.entryListExpunge(seq);
|
|
780
|
+
if (deletedEntry) {
|
|
781
|
+
await this.mailbox.processDeleted(deletedEntry);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Update status with full sync timestamp
|
|
786
|
+
let status = this.mailbox.getMailboxStatus();
|
|
787
|
+
status.lastFullSync = new Date();
|
|
788
|
+
|
|
789
|
+
await this.mailbox.updateStoredStatus(status);
|
|
790
|
+
let storedStatus = await this.mailbox.getStoredStatus();
|
|
791
|
+
|
|
792
|
+
await this.mailbox.publishSyncedEvents(storedStatus);
|
|
793
|
+
} finally {
|
|
794
|
+
this.connection.syncing = false;
|
|
795
|
+
this.mailbox.syncing = false;
|
|
796
|
+
lock.release();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
module.exports = {
|
|
802
|
+
SyncOperations,
|
|
803
|
+
getFetchRange,
|
|
804
|
+
isRecentFullSync,
|
|
805
|
+
hasUidValidityChanged,
|
|
806
|
+
hasNoModseqChanges,
|
|
807
|
+
canUseCondstorePartialSync,
|
|
808
|
+
canUseSimplePartialSync,
|
|
809
|
+
canSkipSync,
|
|
810
|
+
FETCH_BATCH_SIZE,
|
|
811
|
+
FULL_SYNC_DELAY
|
|
812
|
+
};
|