VulpesOS/gaia/apps/email/js/ext/imap/protocol/sync.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

280 lines
8.9 KiB
JavaScript
Executable file

define(
[
'../imapchew',
'../../allback',
'exports'
],
function(
$imapchew,
allback,
exports
) {
/**
* Fetch parameters to get the headers / bodystructure; exists to reuse the
* object since every fetch is the same.
*
* We are intentionally not using ENVELOPE because Thunderbird intentionally
* defaults to to not using ENVELOPE. Per bienvenu in
* https://bugzilla.mozilla.org/show_bug.cgi?id=402594#c33 "We stopped using it
* by default because servers often had issues with it so it was more trouble
* than it was worth."
*/
var INITIAL_FETCH_PARAMS = [
'BODYSTRUCTURE',
'INTERNALDATE',
'FLAGS',
'BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC SUBJECT REPLY-TO MESSAGE-ID REFERENCES)]',
];
/**
* Fetch parameters to just get the flags.
*/
var FLAG_FETCH_PARAMS = ['FLAGS'];
// Number of bytes to fetch for snippet.
var SNIPPET_BYTES = 256;
// See the `ImapFolderConn` block comment for rationale.
var KNOWN_HEADERS_AGGR_COST = 20,
KNOWN_HEADERS_PER_COST = 1,
NEW_HEADERS_AGGR_COST = 20,
NEW_HEADERS_PER_COST = 5,
NEW_BODIES_PER_COST = 30;
/**
* Given a list of new-to-us UIDs and known-to-us UIDs and their corresponding
* headers, synchronize the flags for the known UIDs' headers and fetch and
* create the header and body objects for the new UIDS.
*
* First we fetch the headers/bodystructures for the new UIDs all in one go;
* all of these headers are going to end up in-memory at the same time, so
* batching won't let us reduce the overhead right now. We process them
* to determine the body parts we should fetch as the results come in. Once
* we have them all, we sort them by date, endTS-to-startTS for the third
* step and start issuing/pipelining the requests.
*
* Second, we issue the flag update requests for the known-to-us UIDs. This
* is done second so it can help avoid wasting the latency of the round-trip
* that would otherwise result between steps one and three. (Although we
* could also mitigate that by issuing some step three requests even as
* the step one requests are coming in; our sorting doesn't have to be
* perfect and may already be reasonably well ordered if UIDs correlate
* with internal date well.)
*
* Third, we fetch the body parts in our newest-to-startTS order, adding
* finalized headers and bodies as we go.
*
* == Usage
*
* var sync = new ImapUidSync({
* initialProgress: 0.25,
* connection: (ImapConnection),
*
* storage: (FolderStorage),
*
* knownHeaders: [],
*
* knownUIDs: [],
* newUIDs: []
* });
*
*/
function Sync(options) {
// storage and connections
this.storage = options.storage;
this.connection = options.connection;
this.knownHeaders = options.knownHeaders || [];
this.knownUIDs = options.knownUIDs || [];
this.newUIDs = options.newUIDs || [];
this._progress = options.initialProgress || 0.25;
this._progressCost =
(this.knownUIDs.length ? KNOWN_HEADERS_AGGR_COST : 0) +
KNOWN_HEADERS_PER_COST * this.knownUIDs.length +
(this.newUIDs.length ? NEW_HEADERS_AGGR_COST : 0) +
NEW_HEADERS_PER_COST * this.newUIDs.length;
// events
this.onprogress = null;
this.oncomplete = null;
this._beginSync();
}
Sync.prototype = {
_updateProgress: function(newProgress) {
this._progress += newProgress;
if (this.onprogress) {
this.onprogress(
0.25 + 0.75 * (this._progress / this._progressCost)
);
}
},
_beginSync: function() {
var latch = allback.latch();
if (this.newUIDs.length) {
this._handleNewUids(latch.defer('new'));
}
if (this.knownUIDs.length) {
this._handleKnownUids(latch.defer('known'));
}
latch.then(function() {
if (!this.oncomplete) {
return;
}
this.oncomplete(this.newUIDs.length, this.knownUIDs.length);
}.bind(this));
},
_handleNewUids: function(callback) {
var pendingSnippets = [];
var self = this;
this.connection.listMessages(
this.newUIDs,
INITIAL_FETCH_PARAMS,
{ byUid: true },
function (err, messages) {
if (err) {
// XXX the UID might have disappeared already? we might need to have
// our initiating command re-do whatever it's up to. Alternatively,
// we could drop back from a bulk fetch to a one-by-one fetch.
console.warn('New UIDs fetch error, ideally harmless:', err);
callback();
return;
}
var latch = allback.latch();
messages.forEach(function(msg) {
// Filter out the \Recent flag; our old imap library didn't
// pass it along (complaining about it being useless), so we
// don't either.
var recentIdx = msg.flags.indexOf('\\Recent');
if (recentIdx !== -1) {
msg.flags.splice(recentIdx, 1);
}
try {
// at this point we can build the first batch of header/body
// information.
var chewRep = $imapchew.chewHeaderAndBodyStructure(
msg,
self.storage.folderId,
self.storage._issueNewHeaderId()
);
chewRep.header.bytesToDownloadForBodyDisplay =
$imapchew.calculateBytesToDownloadForImapBodyDisplay(
chewRep.bodyInfo);
pendingSnippets.push(chewRep);
// flush our body/header information ? should we do some sorting,
// etc.. here or just let the UI update ASAP?
self.storage.addMessageHeader(
chewRep.header, chewRep.bodyInfo, latch.defer());
self.storage.addMessageBody(
chewRep.header, chewRep.bodyInfo, latch.defer());
}
catch (ex) {
// it's fine for us to not add bad messages to the database
// XXX do plumb the logger through here eventually
console.warn('message problem, skipping message', ex, '\n',
ex.stack);
}
}.bind(this));
latch.then(callback); // our caller doesn't care about its args
}.bind(this));
},
_handleKnownUids: function(callback) {
var self = this;
this.connection.listMessages(
self.knownUIDs,
FLAG_FETCH_PARAMS,
{ byUid: true },
function(err, messages) {
if (err) {
console.warn('Known UIDs fetch error, ideally harmless:', err);
callback();
return;
}
var latch = allback.latch();
messages.forEach(function(msg, i) {
console.log('FETCHED', i, 'known id', self.knownHeaders[i].id,
'known srvid', self.knownHeaders[i].srvid,
'actual id', msg.uid);
// Filter out the \Recent flag; our old imap library didn't
// pass it along (complaining about it being useless), so we
// don't either.
var recentIdx = msg.flags.indexOf('\\Recent');
if (recentIdx !== -1) {
msg.flags.splice(recentIdx, 1);
}
// RFC 3501 doesn't require that we get results in the order we
// request them, so use indexOf if things don't line up. (In fact,
// dovecot sorts them, so we might just want to sort ours too.)
if (self.knownHeaders[i].srvid !== msg.uid) {
i = self.knownUIDs.indexOf(msg.uid);
// If it's telling us about a message we don't know about, run away.
if (i === -1) {
console.warn("Server fetch reports unexpected message:", msg.uid);
return;
}
}
var header = self.knownHeaders[i];
// msg.flags is not sorted in browserbox; sort here before comparing.
var sortedExistingFlags = header.flags.slice();
sortedExistingFlags.sort();
msg.flags.sort();
// Update number of unread messages
if (header.flags.indexOf('\\Seen') === -1 &&
msg.flags.indexOf('\\Seen') !== -1) {
self.storage.folderMeta.unreadCount--;
} else if (header.flags.indexOf('\\Seen') !== -1 &&
msg.flags.indexOf('\\Seen') === -1) {
self.storage.folderMeta.unreadCount++;
}
if (header.flags.toString() !== msg.flags.toString()) {
console.warn(' FLAGS: "' + header.flags.toString() + '" VS "' +
msg.flags.toString() + '"');
header.flags = msg.flags;
self.storage.updateMessageHeader(
header.date, header.id, true, header, /* body hint */ null,
latch.defer());
}
else {
self.storage.unchangedMessageHeader(header);
}
});
// the fetch results will be bursty, so just update all at once
self._updateProgress(KNOWN_HEADERS_AGGR_COST +
KNOWN_HEADERS_PER_COST * self.knownUIDs.length);
latch.then(callback); // our caller doesn't care about its args
}.bind(this));
}
};
exports.Sync = Sync;
});