Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
376 lines
12 KiB
JavaScript
Executable file
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
|