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

376 lines
12 KiB
JavaScript
Executable file

define([
'logic', '../a64', '../accountmixins', '../mailslice',
'../searchfilter', '../util', '../db/folder_info_rep', 'require', 'exports'],
function(logic, $a64, $acctmixins, $mailslice,
$searchfilter, $util, $folder_info, require, exports) {
var bsearchForInsert = $util.bsearchForInsert;
function cmpFolderPubPath(a, b) {
return a.path.localeCompare(b.path);
}
/**
* A base class for IMAP and POP accounts.
*
* A lot of the functionality related to handling folders,
* orchestrating jobs, etc., is common to both IMAP and POP accounts.
* This class factors out the common functionality, allowing the
* ImapAccount and Pop3Account classes to only provide
* protocol-specific code.
*
* @param {Class} FolderSyncer The class to instantiate for folder sync.
*
* The rest of the parameters match those passed to Pop3Account and
* ImapAccount.
*/
function CompositeIncomingAccount(
FolderSyncer,
universe, compositeAccount, accountId, credentials,
connInfo, folderInfos, dbConn, existingProtoConn) {
this.universe = universe;
this.compositeAccount = compositeAccount;
this.id = accountId;
this.accountDef = compositeAccount.accountDef;
this.enabled = true;
this._alive = true;
this._credentials = credentials;
this._connInfo = connInfo;
this._db = dbConn;
// Yes, the pluralization is suspect, but unambiguous.
/** @dictof[@key[FolderId] @value[FolderStorage] */
var folderStorages = this._folderStorages = {};
/** @dictof[@key[FolderId] @value[FolderMeta] */
var folderPubs = this.folders = [];
// This is a class, not an instance, hence the camel case.
this.FolderSyncer = FolderSyncer;
/**
* The list of dead folder id's that we need to nuke the storage for when
* we next save our account status to the database.
*/
this._deadFolderIds = null;
/**
* The canonical folderInfo object we persist to the database.
*/
this._folderInfos = folderInfos;
/**
* @dict[
* @param[nextFolderNum Number]{
* The next numeric folder number to be allocated.
* }
* @param[nextMutationNum Number]{
* The next mutation id to be allocated.
* }
* @param[lastFolderSyncAt DateMS]{
* When was the last time we ran `syncFolderList`?
* }
* @param[capability @listof[String]]{
* The post-login capabilities from the server.
* }
* @param[overflowMap @dictof[
* @key[uidl String]
* @value[@dict[
* @key[size Number]
* ]]
* ]]{
* The list of messages that will NOT be downloaded by a sync
* automatically, but instead need to be fetched with a "Download
* more messages..." operation. (POP3 only.)
* }
* @param[uidlMap @dictof[
* @key[uidl String]
* @value[headerID String]
* ]]{
* A mapping of UIDLs to message header IDs. (POP3 only.)
* }
* ]{
* Meta-information about the account derived from probing the account.
* This information gets flushed on database upgrades.
* }
*/
this.meta = this._folderInfos.$meta;
/**
* @listof[SerializedMutation]{
* The list of recently issued mutations against us. Mutations are added
* as soon as they are requested and remain until evicted based on a hard
* numeric limit. The limit is driven by our unit tests rather than our
* UI which currently only allows a maximum of 1 (high-level) undo. The
* status of whether the mutation has been run is tracked on the mutation
* but does not affect its presence or position in the list.
*
* Right now, the `MailUniverse` is in charge of this and we just are a
* convenient place to stash the data.
* }
*/
this.mutations = this._folderInfos.$mutations;
for (var folderId in folderInfos) {
if (folderId[0] === '$')
continue;
var folderInfo = folderInfos[folderId];
folderStorages[folderId] =
new $mailslice.FolderStorage(this, folderId, folderInfo, this._db,
FolderSyncer);
folderPubs.push(folderInfo.$meta);
}
this.folders.sort(function(a, b) {
return a.path.localeCompare(b.path);
});
// Ensure we have an inbox. This is a folder that must exist with a standard
// name, so we can create it without talking to the server.
var inboxFolder = this.getFirstFolderWithType('inbox');
if (!inboxFolder) {
this._learnAboutFolder('INBOX', 'INBOX', null, 'inbox', '/', 0, true);
}
}
exports.CompositeIncomingAccount = CompositeIncomingAccount;
CompositeIncomingAccount.prototype = {
////////////////////////////////////////////////////////////////
// ACCOUNT OVERRIDES
runOp: $acctmixins.runOp,
getFirstFolderWithType: $acctmixins.getFirstFolderWithType,
getFolderByPath: $acctmixins.getFolderByPath,
saveAccountState: $acctmixins.saveAccountState,
runAfterSaves: $acctmixins.runAfterSaves,
/**
* Make a given folder known to us, creating state tracking instances, etc.
*
* @param {Boolean} suppressNotification
* Don't report this folder to subscribed slices. This is used in cases
* where the account has not been made visible to the front-end yet and/or
* syncFolderList hasn't yet run, but something subscribed to the "all
* accounts" unified folder slice could end up seeing something before it
* should. This is a ret-con'ed comment, so maybe do some auditing before
* adding new call-sites that use this, especially if it's not used for
* offline-only folders at account creation/app startup.
*/
_learnAboutFolder: function(name, path, parentId, type, delim, depth,
suppressNotification) {
var folderId = this.id + '/' + $a64.encodeInt(this.meta.nextFolderNum++);
var folderInfo = this._folderInfos[folderId] = {
$meta: $folder_info.makeFolderMeta({
id: folderId,
name: name,
type: type,
path: path,
parentId: parentId,
delim: delim,
depth: depth,
lastSyncedAt: 0,
version: $mailslice.FOLDER_DB_VERSION
}),
$impl: {
nextId: 0,
nextHeaderBlock: 0,
nextBodyBlock: 0,
},
accuracy: [],
headerBlocks: [],
bodyBlocks: [],
serverIdHeaderBlockMapping: null, // IMAP/POP3 does not need the mapping
};
this._folderStorages[folderId] =
new $mailslice.FolderStorage(this, folderId, folderInfo, this._db,
this.FolderSyncer);
var folderMeta = folderInfo.$meta;
var idx = bsearchForInsert(this.folders, folderMeta, cmpFolderPubPath);
this.folders.splice(idx, 0, folderMeta);
if (!suppressNotification)
this.universe.__notifyAddedFolder(this, folderMeta);
return folderMeta;
},
_forgetFolder: function(folderId, suppressNotification) {
var folderInfo = this._folderInfos[folderId],
folderMeta = folderInfo.$meta;
delete this._folderInfos[folderId];
var folderStorage = this._folderStorages[folderId];
delete this._folderStorages[folderId];
var idx = this.folders.indexOf(folderMeta);
this.folders.splice(idx, 1);
if (this._deadFolderIds === null)
this._deadFolderIds = [];
this._deadFolderIds.push(folderId);
folderStorage.youAreDeadCleanupAfterYourself();
if (!suppressNotification)
this.universe.__notifyRemovedFolder(this, folderMeta);
},
/**
* Completely reset the state of a folder. For use by unit tests and in the
* case of UID validity rolls. No notification is generated, although slices
* are repopulated.
*
* FYI: There is a nearly identical method in ActiveSync's account
* implementation.
*/
_recreateFolder: function(folderId, callback) {
logic(this, 'recreateFolder', { folderId: folderId });
var folderInfo = this._folderInfos[folderId];
folderInfo.$impl = {
nextId: 0,
nextHeaderBlock: 0,
nextBodyBlock: 0,
};
folderInfo.accuracy = [];
folderInfo.headerBlocks = [];
folderInfo.bodyBlocks = [];
if (this._deadFolderIds === null)
this._deadFolderIds = [];
this._deadFolderIds.push(folderId);
var self = this;
this.saveAccountState(null, function() {
var newStorage =
new $mailslice.FolderStorage(self, folderId, folderInfo, self._db,
self.FolderSyncer);
for (var iter in Iterator(self._folderStorages[folderId]._slices)) {
var slice = iter[1];
slice._storage = newStorage;
slice.reset();
newStorage.sliceOpenMostRecent(slice);
}
self._folderStorages[folderId]._slices = [];
self._folderStorages[folderId] = newStorage;
callback(newStorage);
}, 'recreateFolder');
},
/**
* We are being told that a synchronization pass completed, and that we may
* want to consider persisting our state.
*/
__checkpointSyncCompleted: function(callback, betterReason) {
this.saveAccountState(null, callback, betterReason || 'checkpointSync');
},
/**
* Delete an existing folder WITHOUT ANY ABILITY TO UNDO IT. Current
* UX does not desire this, but the unit tests do.
*
* XXX: This is not quite right for POP3; address when we expose
* deleting folders to the UI when we need to create jobs too.
*
* Callback is like the createFolder one, why not.
*/
deleteFolder: function(folderId, callback) {
if (!this._folderInfos.hasOwnProperty(folderId))
throw new Error("No such folder: " + folderId);
if (!this.universe.online) {
if (callback)
callback('offline');
return;
}
var folderMeta = this._folderInfos[folderId].$meta;
var rawConn = null, self = this;
function gotConn(conn) {
rawConn = conn;
rawConn.delBox(folderMeta.path, deletionCallback);
}
function deletionCallback(err) {
if (err)
done('unknown');
else
done(null);
}
function done(errString) {
if (rawConn) {
self.__folderDoneWithConnection(rawConn, false, false);
rawConn = null;
}
if (!errString) {
logic(self, 'deleteFolder', { path: folderMeta.path });
self._forgetFolder(folderId);
}
if (callback)
callback(errString, folderMeta);
}
this.__folderDemandsConnection(null, 'deleteFolder', gotConn);
},
getFolderStorageForFolderId: function(folderId) {
if (this._folderStorages.hasOwnProperty(folderId))
return this._folderStorages[folderId];
throw new Error('No folder with id: ' + folderId);
},
getFolderStorageForMessageSuid: function(messageSuid) {
var folderId = messageSuid.substring(0, messageSuid.lastIndexOf('/'));
if (this._folderStorages.hasOwnProperty(folderId))
return this._folderStorages[folderId];
throw new Error('No folder with id: ' + folderId);
},
getFolderMetaForFolderId: function(folderId) {
if (this._folderInfos.hasOwnProperty(folderId))
return this._folderInfos[folderId].$meta;
return null;
},
/**
* Create a view slice on the messages in a folder, starting from the most
* recent messages and synchronizing further as needed.
*/
sliceFolderMessages: function(folderId, bridgeHandle) {
var storage = this._folderStorages[folderId],
slice = new $mailslice.MailSlice(bridgeHandle, storage);
storage.sliceOpenMostRecent(slice);
},
searchFolderMessages: function(folderId, bridgeHandle, phrase, whatToSearch) {
var storage = this._folderStorages[folderId],
slice = new $searchfilter.SearchSlice(bridgeHandle, storage, phrase,
whatToSearch);
storage.sliceOpenSearch(slice);
return slice;
},
shutdownFolders: function() {
// - kill all folder storages (for their loggers)
for (var iFolder = 0; iFolder < this.folders.length; iFolder++) {
var folderPub = this.folders[iFolder],
folderStorage = this._folderStorages[folderPub.id];
folderStorage.shutdown();
}
},
scheduleMessagePurge: function(folderId, callback) {
this.universe.purgeExcessMessages(this.compositeAccount, folderId,
callback);
},
/**
* We receive this notification from our _backoffEndpoint.
*/
onEndpointStateChange: function(state) {
switch (state) {
case 'healthy':
this.universe.__removeAccountProblem(this.compositeAccount,
'connection', 'incoming');
break;
case 'unreachable':
case 'broken':
this.universe.__reportAccountProblem(this.compositeAccount,
'connection', 'incoming');
break;
}
},
};
}); // end define