VulpesOS/gaia/apps/email/js/ext/jobmixins.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

945 lines
31 KiB
JavaScript
Executable file

/**
* Mix-ins for account job functionality where the code is reused.
**/
define(
[
'./worker-router',
'./util',
'./allback',
'./wakelocks',
'./date',
'./syncbase',
'./mailslice',
'./headerCounter',
'logic',
'exports',
'require'
],
function(
$router,
$util,
$allback,
$wakelocks,
$date,
$sync,
$mailslice,
$count,
logic,
exports,
require
) {
var sendMessage = $router.registerCallbackType('devicestorage');
exports.local_do_modtags = function(op, doneCallback, undo) {
var self = this;
var addTags = undo ? op.removeTags : op.addTags,
removeTags = undo ? op.addTags : op.removeTags;
var mutationsPerformed = 0;
this._partitionAndAccessFoldersSequentially(
op.messages,
false,
function perFolder(ignoredConn, storage, headers, namers, callWhenDone) {
var waitingOn = headers.length;
function next() {
if (--waitingOn === 0)
callWhenDone();
}
for (var iHeader = 0; iHeader < headers.length; iHeader++) {
var header = headers[iHeader];
var iTag, tag, existing, modified = false;
if (addTags) {
for (iTag = 0; iTag < addTags.length; iTag++) {
tag = addTags[iTag];
// The list should be small enough that native stuff is better
// than JS bsearch.
existing = header.flags.indexOf(tag);
if (existing !== -1)
continue;
header.flags.push(tag);
mutationsPerformed++;
if (tag === '\\Seen') {
storage.folderMeta.unreadCount--;
}
header.flags.sort(); // (maintain sorted invariant)
modified = true;
}
}
if (removeTags) {
for (iTag = 0; iTag < removeTags.length; iTag++) {
tag = removeTags[iTag];
existing = header.flags.indexOf(tag);
if (existing === -1)
continue;
header.flags.splice(existing, 1);
mutationsPerformed++;
if (tag === '\\Seen') {
storage.folderMeta.unreadCount++;
}
modified = true;
}
}
storage.updateMessageHeader(header.date, header.id, false,
header, /* body hint */ null, next);
}
},
function() {
// If we didn't actually do anything, then we don't actually need to do
// anything on the server either and we can skip it.
//
// Note that this does get us into some edge cases around the semantics of
// undo. Before this change, "undo" always just meant "do the opposite
// modification of what I said" which is notably different from "undo the
// things you actually just did" which gave rise to the (currently
// unfixed) https://bugzil.la/997496. And so with this change, we'll do
// the "right" thing, but in other cases we'll still do the "wrong" thing.
if (mutationsPerformed === 0) {
op.serverStatus = 'skip';
}
doneCallback(null, null, true);
},
null, // connection loss does not happen for local-only ops
undo,
'modtags');
};
exports.local_undo_modtags = function(op, callback) {
// Undoing is just a question of flipping the add and remove lists.
return this.local_do_modtags(op, callback, true);
};
exports.local_do_move = function(op, doneCallback, targetFolderId) {
// create a scratch field to store the guid's for check purposes
op.guids = {};
var nukeServerIds = !this.resilientServerIds;
var stateDelta = this._stateDelta, addWait = 0, self = this;
if (!stateDelta.moveMap)
stateDelta.moveMap = {};
if (!stateDelta.serverIdMap)
stateDelta.serverIdMap = {};
if (!targetFolderId)
targetFolderId = op.targetFolder;
this._partitionAndAccessFoldersSequentially(
op.messages, false,
function perFolder(ignoredConn, sourceStorage, headers, namers,
perFolderDone) {
// -- open the target folder for processing
function targetOpened_nowProcess(ignoredConn, _targetStorage) {
targetStorage = _targetStorage;
processNext();
}
// -- get the body for the next header (or be done)
function processNext() {
if (iNextHeader >= headers.length) {
perFolderDone();
return;
}
header = headers[iNextHeader++];
sourceStorage.getMessageBody(header.suid, header.date,
gotBody_nowDelete);
}
// -- delete the header and body from the source
function gotBody_nowDelete(_body) {
body = _body;
// We need an entry in the server id map if we are moving/deleting it.
// We don't need this if we're moving a message to the folder it's
// already in, but it doesn't hurt anything.
if (header.srvid)
stateDelta.serverIdMap[header.suid] = header.srvid;
if (sourceStorage === targetStorage ||
// localdraft messages aren't real, and so must not be
// moved and are only eligible for nuke deletion. But they
// _can_ be moved to the outbox, and vice versa!
(sourceStorage.folderMeta.type === 'localdrafts' &&
targetStorage.folderMeta.type !== 'outbox') ||
(sourceStorage.folderMeta.type === 'outbox' &&
targetStorage.folderMeta.type !== 'localdrafts')) {
if (op.type === 'move') {
// A move from a folder to itself is a no-op.
processNext();
}
else { // op.type === 'delete'
// If the op is a delete and the source and destination folders
// match, we're deleting from trash, so just perma-delete it.
sourceStorage.deleteMessageHeaderAndBodyUsingHeader(
header, processNext);
}
}
else {
sourceStorage.deleteMessageHeaderAndBodyUsingHeader(
header, deleted_nowAdd);
}
}
// -- add the header/body to the target folder
function deleted_nowAdd() {
var sourceSuid = header.suid;
// - update id fields
header.id = targetStorage._issueNewHeaderId();
header.suid = targetStorage.folderId + '/' + header.id;
if (nukeServerIds)
header.srvid = null;
stateDelta.moveMap[sourceSuid] = header.suid;
addWait = 2;
targetStorage.addMessageHeader(header, body, added);
targetStorage.addMessageBody(header, body, added);
}
function added() {
if (--addWait !== 0)
return;
processNext();
}
var iNextHeader = 0, targetStorage = null, header = null, body = null,
addWait = 0;
// If the source folder and the target folder are the same, don't try
// to access the target folder!
if (sourceStorage.folderId === targetFolderId) {
targetStorage = sourceStorage;
processNext();
}
else {
self._accessFolderForMutation(targetFolderId, false,
targetOpened_nowProcess, null,
'local move target');
}
},
function() {
// Pass along the moveMap as the move's result, so that the
// frontend can directly obtain a reference to the moved
// message. This is used when tapping a message in the outbox
// folder (wherein we expect it to be moved to localdrafts and
// immediately edited).
doneCallback(null, stateDelta.moveMap, true);
},
null, // connection loss does not happen for local-only ops
false,
'local move source');
};
// XXX implement!
exports.local_undo_move = function(op, doneCallback, targetFolderId) {
doneCallback(null);
};
exports.local_do_delete = function(op, doneCallback) {
var trashFolder = this.account.getFirstFolderWithType('trash');
if (!trashFolder) {
this.account.ensureEssentialOnlineFolders();
doneCallback('defer');
return;
}
this.local_do_move(op, doneCallback, trashFolder.id);
};
exports.local_undo_delete = function(op, doneCallback) {
var trashFolder = this.account.getFirstFolderWithType('trash');
if (!trashFolder) {
// the absence of the trash folder when it must have previously existed is
// confusing.
doneCallback('unknown');
return;
}
this.local_undo_move(op, doneCallback, trashFolder.id);
};
exports.do_download = function(op, callback) {
var self = this;
var idxLastSlash = op.messageSuid.lastIndexOf('/'),
folderId = op.messageSuid.substring(0, idxLastSlash);
var folderConn, folderStorage;
// Once we have the connection, get the current state of the body rep.
var gotConn = function gotConn(_folderConn, _folderStorage) {
folderConn = _folderConn;
folderStorage = _folderStorage;
folderStorage.getMessageHeader(op.messageSuid, op.messageDate, gotHeader);
};
var deadConn = function deadConn() {
callback('aborted-retry');
};
// Now that we have the body, we can know the part numbers and eliminate /
// filter out any redundant download requests. Issue all the fetches at
// once.
var partsToDownload = [], storePartsTo = [], registerDownload = [],
header, bodyInfo, uid;
var gotHeader = function gotHeader(_headerInfo) {
header = _headerInfo;
uid = header.srvid;
folderStorage.getMessageBody(op.messageSuid, op.messageDate, gotBody);
};
var gotBody = function gotBody(_bodyInfo) {
bodyInfo = _bodyInfo;
var i, partInfo;
for (i = 0; i < op.relPartIndices.length; i++) {
partInfo = bodyInfo.relatedParts[op.relPartIndices[i]];
if (partInfo.file)
continue;
partsToDownload.push(partInfo);
storePartsTo.push('idb');
registerDownload.push(false);
}
for (i = 0; i < op.attachmentIndices.length; i++) {
partInfo = bodyInfo.attachments[op.attachmentIndices[i]];
if (partInfo.file)
continue;
partsToDownload.push(partInfo);
// right now all attachments go in sdcard
storePartsTo.push('sdcard');
registerDownload.push(op.registerAttachments[i]);
}
folderConn.downloadMessageAttachments(uid, partsToDownload, gotParts);
};
var downloadErr = null;
var gotParts = function gotParts(err, bodyBlobs) {
if (bodyBlobs.length !== partsToDownload.length) {
callback(err, null, false);
return;
}
downloadErr = err;
var pendingCbs = 1;
function next() {
if (!--pendingCbs) {
done();
}
}
for (var i = 0; i < partsToDownload.length; i++) {
// Because we should be under a mutex, this part should still be the
// live representation and we can mutate it.
var partInfo = partsToDownload[i],
blob = bodyBlobs[i],
storeTo = storePartsTo[i];
if (blob) {
partInfo.sizeEstimate = blob.size;
partInfo.type = blob.type;
if (storeTo === 'idb') {
partInfo.file = blob;
} else {
pendingCbs++;
saveToDeviceStorage(
self, blob, storeTo, registerDownload[i],
partInfo.name, partInfo, next);
}
}
}
next();
};
function done() {
folderStorage.updateMessageBody(
header, bodyInfo,
{ flushBecause: 'blobs' },
{
changeDetails: {
attachments: op.attachmentIndices
}
},
function() {
callback(downloadErr, null, true);
});
};
self._accessFolderForMutation(folderId, true, gotConn, deadConn,
'download');
}
/**
* Save an attachment to device storage, making the filename unique if we
* encounter a collision.
*/
var saveToDeviceStorage = exports.saveToDeviceStorage =
function(scope, blob, storeTo, registerDownload, filename, partInfo, cb,
isRetry) {
var self = this;
var callback = function(success, error, savedFilename, registered) {
if (success) {
logic(scope, 'savedAttachment', { storeTo: storeTo,
type: blob.type,
size: blob.size });
console.log('saved attachment to', storeTo, savedFilename,
'type:', blob.type, 'registered:', registered);
partInfo.file = [storeTo, savedFilename];
cb();
} else {
logic(scope, 'saveFailure', { storeTo: storeTo,
type: blob.type,
size: blob.size,
error: error,
filename: filename });
console.warn('failed to save attachment to', storeTo, filename,
'type:', blob.type);
// if we failed to unique the file after appending junk, just give up
if (isRetry) {
cb(error);
return;
}
// retry by appending a super huge timestamp to the file before its
// extension.
var idxLastPeriod = filename.lastIndexOf('.');
if (idxLastPeriod === -1)
idxLastPeriod = filename.length;
filename = filename.substring(0, idxLastPeriod) + '-' + $date.NOW() +
filename.substring(idxLastPeriod);
saveToDeviceStorage(scope, blob, storeTo, registerDownload,
filename, partInfo, cb, true);
}
};
sendMessage('save', [storeTo, blob, filename, registerDownload], callback);
}
exports.local_do_download = function(op, callback) {
// Downloads are inherently online operations.
callback(null);
};
exports.check_download = function(op, callback) {
// If we downloaded the file and persisted it successfully, this job would be
// marked done because of the atomicity guarantee on our commits.
callback(null, 'coherent-notyet');
};
exports.local_undo_download = function(op, callback) {
callback(null);
};
exports.undo_download = function(op, callback) {
callback(null);
};
exports.local_do_downloadBodies = function(op, callback) {
callback(null);
};
exports.do_downloadBodies = function(op, callback) {
var aggrErr = null, totalDownloaded = 0;
this._partitionAndAccessFoldersSequentially(
op.messages,
true,
function perFolder(folderConn, storage, headers, namers, callWhenDone) {
folderConn.downloadBodies(headers, op.options, function(err, numDownloaded) {
totalDownloaded += numDownloaded;
if (err && !aggrErr) {
aggrErr = err;
}
callWhenDone();
});
},
function allDone() {
callback(aggrErr, null,
// save if we might have done work.
totalDownloaded > 0);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
false, // reverse?
'downloadBodies',
true // require headers
);
};
exports.check_downloadBodies = function(op, callback) {
// If we had downloaded the bodies and persisted them successfully, this job
// would be marked done because of the atomicity guarantee on our commits. It
// is possible this request might only be partially serviced, in which case we
// will avoid redundant body fetches, but redundant folder selection is
// possible if this request spans multiple folders.
callback(null, 'coherent-notyet');
};
exports.check_downloadBodyReps = function(op, callback) {
// If we downloaded all of the body parts and persisted them successfully,
// this job would be marked done because of the atomicity guarantee on our
// commits. But it's not, so there's more to do.
callback(null, 'coherent-notyet');
};
exports.do_downloadBodyReps = function(op, callback) {
var self = this;
var idxLastSlash = op.messageSuid.lastIndexOf('/'),
folderId = op.messageSuid.substring(0, idxLastSlash);
var folderConn, folderStorage;
// Once we have the connection, get the current state of the body rep.
var gotConn = function gotConn(_folderConn, _folderStorage) {
folderConn = _folderConn;
folderStorage = _folderStorage;
folderStorage.getMessageHeader(op.messageSuid, op.messageDate, gotHeader);
};
var deadConn = function deadConn() {
callback('aborted-retry');
};
var gotHeader = function gotHeader(header) {
// header may have been deleted by the time we get here...
if (!header) {
callback();
return;
}
// Check to see if we've already downloaded the bodyReps for this
// message. If so, no need to even try to fetch them again. This
// allows us to enforce an idempotency guarantee regarding how
// many times body change notifications will be fired.
folderStorage.getMessageBody(header.suid, header.date,
function(body) {
if (!folderStorage.messageBodyRepsDownloaded(body)) {
folderConn.downloadBodyReps(header, onDownloadReps);
} else {
// passing flushed = true because we don't need to save anything
onDownloadReps(null, body, /* flushed = */ true);
}
});
};
var onDownloadReps = function onDownloadReps(err, bodyInfo, flushed) {
if (err) {
console.error('Error downloading reps', err);
// fail we cannot download for some reason?
callback('unknown');
return;
}
// Since we downloaded something, we do want to save what we downloaded,
// but only if the downloader didn't already force a save while flushing.
var save = !flushed;
callback(null, bodyInfo, save);
};
self._accessFolderForMutation(folderId, true, gotConn, deadConn,
'downloadBodyReps');
};
exports.local_do_downloadBodyReps = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// sendOutboxMessages
/**
* Send some messages from the outbox. At a high level, you can
* pretend that "sendOutboxMessages" just kicks off a process to send
* all the messages in the outbox.
*
* As an implementation detail, to keep memory requirements low, this
* job is designed to send only one message at a time; it
* self-schedules future jobs to walk through the list of outbox
* messages, one at a time.
*
* In pseudocode:
*
* CLIENT: "Hey, please kick off a sendOutboxMessages job."
* OUTBOX JOB 1: "Okay, I'll send the first message."
* CLIENT: "thanks"
* OUTBOX JOB 1: "Okay, done. Oh, there are more messages. Scheduling
* a future job to send the next message."
* CLIENT: "ok"
* OUTBOX JOB 1: *dies*
* CLIENT: *goes off to do other things*
* OUTBOX JOB 2: "on it, sending another message"
*
* This allows other jobs to interleave the sending process, to avoid
* introducing long delays in a world where we only run one job
* concurrently.
*
* This job accepts a `beforeMessage` parameter; if that parameter is
* null (the normal case), we'll attempt to send the newest message.
* After the first message has been sent, we will _self-schedule_ a
* second sendOutboxMessages job to continue sending the rest of the
* available messages (one per job).
*
* We set `header.sendStatus` to an object representing the current
* state of the send operation. If the send fails, we'll remove the
* flag and indicate that there was an error sending, unless the app
* crashes, in which case we'll try to resend upon startup again (see
* `outboxNeedsFreshSync`).
*/
exports.do_sendOutboxMessages = function(op, callback) {
var account = this.account;
var outboxFolder = account.getFirstFolderWithType('outbox');
if (!outboxFolder) {
callback('moot'); // This shouldn't happen, we should always have an outbox.
return;
}
// If we temporarily paused outbox syncing, don't do anything.
if (!account.outboxSyncEnabled) {
console.log('outbox: Outbox syncing temporarily disabled; not syncing.');
callback(null);
return;
}
var outboxNeedsFreshSync = account.outboxNeedsFreshSync;
if (outboxNeedsFreshSync) {
console.log('outbox: This is the first outbox sync for this account.');
account.outboxNeedsFreshSync = false;
}
// Hold both a CPU and WiFi wake lock for the duration of the send
// operation. We'll pass this in to the Composer instance for each
// message, so that the SMTP/ActiveSync sending process can renew
// the wake lock from time to time as the send continues.
var wakeLock = new $wakelocks.SmartWakeLock({
locks: ['cpu', 'wifi']
});
this._accessFolderForMutation(
outboxFolder.id, /* needConn = */ false,
function(nullFolderConn, folderStorage) {
require(['jobs/outbox'], function ($outbox) {
$outbox.sendNextAvailableOutboxMessage(
account.compositeAccount || account, // Requires the main account.
folderStorage,
op.beforeMessage,
op.emitNotifications,
outboxNeedsFreshSync,
wakeLock
).then(function(result) {
var moreExpected = result.moreExpected;
var messageNamer = result.messageNamer;
wakeLock.unlock('send complete');
// If there may be more messages to send, schedule another
// sync to send the next available message.
if (moreExpected) {
account.universe.sendOutboxMessages(account, {
beforeMessage: messageNamer
});
}
// Otherwise, we're done. Mark the outbox as "synced".
else {
account.universe.notifyOutboxSyncDone(account);
folderStorage.markSyncRange(
$sync.OLDEST_SYNC_DATE, null, 'XXX', $date.NOW());
}
// Since we modified the folders, save the account.
callback(null, /* result = */ null, /* save = */ true);
}).catch(function(e) {
console.error('Exception while sending a message.',
'Send failure: ' + e, e.stack);
wakeLock.unlock(e);
callback('aborted-retry');
});
});
},
/* no conn => no deathback required */ null,
'sendOutboxMessages');
};
exports.check_sendOutboxMessages = function(op, callback) {
callback(null, 'moot');
};
exports.local_undo_sendOutboxMessages = function(op, callback) {
callback(null); // You cannot undo sendOutboxMessages.
};
exports.local_do_setOutboxSyncEnabled = function(op, callback) {
// Set a flag on the account to prevent us from kicking off further
// sends while the outbox is being edited on the client. The account
// referenced by `this.account` is actually the receive piece in a
// composite account; this flag is initialized in accountmixins.js.
this.account.outboxSyncEnabled = op.outboxSyncEnabled;
callback(null);
};
////////////////////////////////////////////////////////////////
exports.postJobCleanup = function(error) {
if (!error) {
var deltaMap, fullMap;
// - apply updates to the serverIdMap map
if (this._stateDelta.serverIdMap) {
deltaMap = this._stateDelta.serverIdMap;
fullMap = this._state.suidToServerId;
for (var suid in deltaMap) {
var srvid = deltaMap[suid];
if (srvid === null)
delete fullMap[suid];
else
fullMap[suid] = srvid;
}
}
// - apply updates to the move map
if (this._stateDelta.moveMap) {
deltaMap = this._stateDelta.moveMap;
fullMap = this._state.moveMap;
for (var oldSuid in deltaMap) {
var newSuid = deltaMap[oldSuid];
fullMap[oldSuid] = newSuid;
}
}
}
for (var i = 0; i < this._heldMutexReleasers.length; i++) {
this._heldMutexReleasers[i](error);
}
this._heldMutexReleasers = [];
this._stateDelta.serverIdMap = null;
this._stateDelta.moveMap = null;
};
exports.allJobsDone = function() {
this._state.suidToServerId = {};
this._state.moveMap = {};
};
/**
* Partition messages identified by namers by folder, then invoke the callback
* once per folder, passing in the loaded message header objects for each
* folder.
*
* This method will filter out removed headers (which would otherwise be null).
* Its possible that entire folders will be skipped if no headers requested are
* now present.
*
* Connection loss by default causes this method to stop trying to traverse
* folders, calling callOnConnLoss and callWhenDone in that order. If you want
* to do something more clever, extend this method so that you can return a
* sentinel value or promise or something and do your clever thing.
*
* @args[
* @param[messageNamers @listof[MessageNamer]]
* @param[needConn Boolean]{
* True if we should try and get a connection from the server. Local ops
* should pass false, server ops should pass true. This additionally
* determines whether we provide headers to the operation (!needConn),
* or server id's for messages (needConn).
* }
* @param[callInFolder @func[
* @args[
* @param[folderConn ImapFolderConn]
* @param[folderStorage FolderStorage]
* @param[headersOrServerIds @oneof[
* @listof[HeaderInfo]
* @listof[ServerID]]
* ]
* @param[messageNamers @listof[MessageNamer]]
* @param[callWhenDoneWithFolder Function]
* ]
* ]]
* @param[callWhenDone @func[
* @args[err @oneof[null 'connection-list']]
* ]]{
* The function to invoke when all of the folders have been processed or the
* connection has been lost and we're giving up. This will be invoked after
* `callOnConnLoss` in the event of a conncetion loss.
* }
* @param[callOnConnLoss Function]{
* This function we invoke when we lose a connection. Traditionally, you would
* use this to flag an error in your function that you would then return when
* we invoke `callWhenDone`. Then your check function will be invoked and you
* can laboriously check what actually happened on the server, etc.
* }
* @param[reverse #:optional Boolean]{
* Should we walk the partitions in reverse order?
* }
* @param[label String]{
* The label to use to name the usage of the folder connection.
* }
* @param[requireHeaders Boolean]{
* True if connection & headers are needed.
* }
* ]
*/
exports._partitionAndAccessFoldersSequentially = function(
allMessageNamers,
needConn,
callInFolder,
callWhenDone,
callOnConnLoss,
reverse,
label,
requireHeaders) {
var partitions = $util.partitionMessagesByFolderId(allMessageNamers);
var folderConn, storage, self = this,
folderId = null, folderMessageNamers = null, serverIds = null,
iNextPartition = 0, curPartition = null, modsToGo = 0,
// Set to true immediately before calling callWhenDone; causes us to
// immediately bail out of any of our callbacks in order to avoid
// continuing beyond the point when we should have stopped.
terminated = false;
if (reverse)
partitions.reverse();
var openNextFolder = function openNextFolder() {
if (terminated)
return;
if (iNextPartition >= partitions.length) {
terminated = true;
callWhenDone(null);
return;
}
// Cleanup the last folder (if there was one)
if (iNextPartition) {
folderConn = null;
// The folder's mutex should be last; if the callee acquired any
// additional mutexes in the last round, it should have freed it then
// too.
var releaser = self._heldMutexReleasers.pop();
if (releaser)
releaser();
folderConn = null;
}
curPartition = partitions[iNextPartition++];
folderMessageNamers = curPartition.messages;
serverIds = null;
if (curPartition.folderId !== folderId) {
folderId = curPartition.folderId;
self._accessFolderForMutation(folderId, needConn, gotFolderConn,
connDied, label);
}
};
var connDied = function connDied() {
if (terminated)
return;
if (callOnConnLoss) {
try {
callOnConnLoss();
}
catch (ex) {
self.log.error('callbackErr', { ex: ex });
}
}
terminated = true;
callWhenDone('connection-lost');
};
var gotFolderConn = function gotFolderConn(_folderConn, _storage) {
if (terminated)
return;
folderConn = _folderConn;
storage = _storage;
// - Get headers or resolve current server id from name map
if (needConn && !requireHeaders) {
var neededHeaders = [],
suidToServerId = self._state.suidToServerId;
serverIds = [];
for (var i = 0; i < folderMessageNamers.length; i++) {
var namer = folderMessageNamers[i];
var srvid = suidToServerId[namer.suid];
if (srvid) {
serverIds.push(srvid);
}
else {
serverIds.push(null);
neededHeaders.push(namer);
}
}
if (!neededHeaders.length) {
try {
callInFolder(folderConn, storage, serverIds, folderMessageNamers,
openNextFolder);
}
catch (ex) {
console.error('PAAFS error:', ex, '\n', ex.stack);
}
}
else {
storage.getMessageHeaders(neededHeaders, gotNeededHeaders);
}
}
else {
storage.getMessageHeaders(folderMessageNamers, gotHeaders);
}
};
var gotNeededHeaders = function gotNeededHeaders(headers) {
if (terminated)
return;
var iNextServerId = serverIds.indexOf(null);
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
// It's possible that by the time this job actually gets a chance to run
// that the header is no longer in the folder. This is rare but not
// particularly exceptional.
if (header) {
var srvid = header.srvid;
serverIds[iNextServerId] = srvid;
// A header that exists but does not have a server id is exceptional and
// bad, although logic should handle it because of the above dead-header
// case. suidToServerId should really have provided this information to
// us.
if (!srvid)
console.warn('Header', headers[i].suid, 'missing server id in job!');
}
iNextServerId = serverIds.indexOf(null, iNextServerId + 1);
}
// its entirely possible that we need headers but there are none so we can
// skip entering this folder as the job cannot do anything with an empty
// header.
if (!serverIds.length) {
openNextFolder();
return;
}
try {
callInFolder(folderConn, storage, serverIds, folderMessageNamers,
openNextFolder);
}
catch (ex) {
console.error('PAAFS error:', ex, '\n', ex.stack);
}
};
var gotHeaders = function gotHeaders(headers) {
if (terminated)
return;
// its unlikely but entirely possible that all pending headers have been
// removed somehow between when the job was queued and now.
if (!headers.length) {
openNextFolder();
return;
}
// Sort the headers in ascending-by-date order so that slices hear about
// changes from oldest to newest. That way, they won't get upset about being
// asked to expand into the past.
headers.sort(function(a, b) { return a.date > b.date; });
try {
callInFolder(folderConn, storage, headers, folderMessageNamers,
openNextFolder);
}
catch (ex) {
console.error('PAAFS error:', ex, '\n', ex.stack);
}
};
openNextFolder();
};
exports.local_do_upgradeDB = function (op, doneCallback) {
var storage = this.account.getFolderStorageForFolderId(op.folderId);
var filter = function(header) {
return header.flags &&
header.flags.indexOf('\\Seen') === -1;
};
$count.countHeaders(storage, filter, function(num) {
storage._dirty = true;
storage.folderMeta.version = $mailslice.FOLDER_DB_VERSION;
storage.folderMeta.unreadCount = num;
doneCallback(/* no error */ null, /* no result */ null,
/* yes save */ true);
});
};
}); // end define