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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
273
gaia/apps/email/js/ext/composite/account.js
Executable file
273
gaia/apps/email/js/ext/composite/account.js
Executable file
|
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* Configurator for fake
|
||||
**/
|
||||
|
||||
define(
|
||||
[
|
||||
'logic',
|
||||
'../accountcommon',
|
||||
'../a64',
|
||||
'../accountmixins',
|
||||
'../imap/account',
|
||||
'../pop3/account',
|
||||
'../smtp/account',
|
||||
'../allback',
|
||||
'exports'
|
||||
],
|
||||
function(
|
||||
logic,
|
||||
$accountcommon,
|
||||
$a64,
|
||||
$acctmixins,
|
||||
$imapacct,
|
||||
$pop3acct,
|
||||
$smtpacct,
|
||||
allback,
|
||||
exports
|
||||
) {
|
||||
|
||||
var PIECE_ACCOUNT_TYPE_TO_CLASS = {
|
||||
'imap': $imapacct.ImapAccount,
|
||||
'pop3': $pop3acct.Pop3Account,
|
||||
'smtp': $smtpacct.SmtpAccount,
|
||||
};
|
||||
|
||||
/**
|
||||
* Composite account type to expose account piece types with individual
|
||||
* implementations (ex: imap, smtp) together as a single account. This is
|
||||
* intended to be a very thin layer that shields consuming code from the
|
||||
* fact that IMAP and SMTP are not actually bundled tightly together.
|
||||
*/
|
||||
function CompositeAccount(universe, accountDef, folderInfo, dbConn,
|
||||
receiveProtoConn) {
|
||||
this.universe = universe;
|
||||
this.id = accountDef.id;
|
||||
this.accountDef = accountDef;
|
||||
logic.defineScope(this, 'Account', { accountId: this.id });
|
||||
|
||||
// Currently we don't persist the disabled state of an account because it's
|
||||
// easier for the UI to be edge-triggered right now and ensure that the
|
||||
// triggering occurs once each session.
|
||||
this._enabled = true;
|
||||
this.problems = [];
|
||||
|
||||
// For oauth2, hold on to a "last renew attempt" timestamp. However, since it
|
||||
// uses performance.now() that can be reset depending on clock time and
|
||||
// environment (shared worker always resets to 0 for instance), always reset
|
||||
// the value here to 0. It is just a transient timestamp that is useful
|
||||
// during the lifetime of the app.
|
||||
if (accountDef.credentials && accountDef.credentials.oauth2) {
|
||||
accountDef.credentials.oauth2._transientLastRenew = 0;
|
||||
}
|
||||
|
||||
this.identities = accountDef.identities;
|
||||
|
||||
if (!PIECE_ACCOUNT_TYPE_TO_CLASS.hasOwnProperty(accountDef.receiveType)) {
|
||||
logic(this, 'badAccountType', { type: accountDef.receiveType });
|
||||
}
|
||||
if (!PIECE_ACCOUNT_TYPE_TO_CLASS.hasOwnProperty(accountDef.sendType)) {
|
||||
logic(this, 'badAccountType', { type: accountDef.sendType });
|
||||
}
|
||||
|
||||
this._receivePiece =
|
||||
new PIECE_ACCOUNT_TYPE_TO_CLASS[accountDef.receiveType](
|
||||
universe, this,
|
||||
accountDef.id, accountDef.credentials, accountDef.receiveConnInfo,
|
||||
folderInfo, dbConn, receiveProtoConn);
|
||||
this._sendPiece =
|
||||
new PIECE_ACCOUNT_TYPE_TO_CLASS[accountDef.sendType](
|
||||
universe, this,
|
||||
accountDef.id, accountDef.credentials,
|
||||
accountDef.sendConnInfo, dbConn);
|
||||
|
||||
// expose public lists that are always manipulated in place.
|
||||
this.folders = this._receivePiece.folders;
|
||||
this.meta = this._receivePiece.meta;
|
||||
this.mutations = this._receivePiece.mutations;
|
||||
|
||||
// Mix in any fields common to all accounts.
|
||||
$acctmixins.accountConstructorMixin.call(
|
||||
this, this._receivePiece, this._sendPiece);
|
||||
}
|
||||
|
||||
exports.Account = exports.CompositeAccount = CompositeAccount;
|
||||
CompositeAccount.prototype = {
|
||||
toString: function() {
|
||||
return '[CompositeAccount: ' + this.id + ']';
|
||||
},
|
||||
get supportsServerFolders() {
|
||||
return this._receivePiece.supportsServerFolders;
|
||||
},
|
||||
toBridgeWire: function() {
|
||||
return {
|
||||
id: this.accountDef.id,
|
||||
name: this.accountDef.name,
|
||||
type: this.accountDef.type,
|
||||
|
||||
defaultPriority: this.accountDef.defaultPriority,
|
||||
|
||||
enabled: this.enabled,
|
||||
problems: this.problems,
|
||||
|
||||
syncRange: this.accountDef.syncRange,
|
||||
syncInterval: this.accountDef.syncInterval,
|
||||
notifyOnNew: this.accountDef.notifyOnNew,
|
||||
playSoundOnSend: this.accountDef.playSoundOnSend,
|
||||
|
||||
identities: this.identities,
|
||||
|
||||
credentials: {
|
||||
username: this.accountDef.credentials.username,
|
||||
outgoingUsername: this.accountDef.credentials.outgoingUsername,
|
||||
// no need to send the password to the UI.
|
||||
// send all the oauth2 stuff we've got, though.
|
||||
oauth2: this.accountDef.credentials.oauth2
|
||||
},
|
||||
|
||||
servers: [
|
||||
{
|
||||
type: this.accountDef.receiveType,
|
||||
connInfo: this.accountDef.receiveConnInfo,
|
||||
activeConns: this._receivePiece.numActiveConns || 0,
|
||||
},
|
||||
{
|
||||
type: this.accountDef.sendType,
|
||||
connInfo: this.accountDef.sendConnInfo,
|
||||
activeConns: this._sendPiece.numActiveConns || 0,
|
||||
}
|
||||
],
|
||||
};
|
||||
},
|
||||
toBridgeFolder: function() {
|
||||
return {
|
||||
id: this.accountDef.id,
|
||||
name: this.accountDef.name,
|
||||
path: this.accountDef.name,
|
||||
type: 'account',
|
||||
};
|
||||
},
|
||||
|
||||
get enabled() {
|
||||
return this._enabled;
|
||||
},
|
||||
set enabled(val) {
|
||||
this._enabled = this._receivePiece.enabled = val;
|
||||
},
|
||||
|
||||
saveAccountState: function(reuseTrans, callback, reason) {
|
||||
return this._receivePiece.saveAccountState(reuseTrans, callback, reason);
|
||||
},
|
||||
|
||||
get _saveAccountIsImminent() {
|
||||
return this.__saveAccountIsImminent;
|
||||
},
|
||||
set _saveAccountIsImminent(val) {
|
||||
this.___saveAccountIsImminent =
|
||||
this._receivePiece._saveAccountIsImminent = val;
|
||||
},
|
||||
|
||||
runAfterSaves: function(callback) {
|
||||
return this._receivePiece.runAfterSaves(callback);
|
||||
},
|
||||
|
||||
allOperationsCompleted: function() {
|
||||
if (this._receivePiece.allOperationsCompleted) {
|
||||
this._receivePiece.allOperationsCompleted();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check that the account is healthy in that we can login at all.
|
||||
* We'll check both the incoming server and the SMTP server; for
|
||||
* simplicity, the errors are returned as follows:
|
||||
*
|
||||
* callback(incomingErr, outgoingErr);
|
||||
*
|
||||
* If you don't want to check both pieces, you should just call
|
||||
* checkAccount on the receivePiece or sendPiece as appropriate.
|
||||
*/
|
||||
checkAccount: function(callback) {
|
||||
var latch = allback.latch();
|
||||
this._receivePiece.checkAccount(latch.defer('incoming'));
|
||||
this._sendPiece.checkAccount(latch.defer('outgoing'));
|
||||
latch.then(function(results) {
|
||||
callback(results.incoming[0], results.outgoing[0]);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shutdown the account; see `MailUniverse.shutdown` for semantics.
|
||||
*/
|
||||
shutdown: function(callback) {
|
||||
this._sendPiece.shutdown();
|
||||
this._receivePiece.shutdown(callback);
|
||||
},
|
||||
|
||||
accountDeleted: function() {
|
||||
this._sendPiece.accountDeleted();
|
||||
this._receivePiece.accountDeleted();
|
||||
},
|
||||
|
||||
deleteFolder: function(folderId, callback) {
|
||||
return this._receivePiece.deleteFolder(folderId, callback);
|
||||
},
|
||||
|
||||
sliceFolderMessages: function(folderId, bridgeProxy) {
|
||||
return this._receivePiece.sliceFolderMessages(folderId, bridgeProxy);
|
||||
},
|
||||
|
||||
searchFolderMessages: function(folderId, bridgeHandle, phrase, whatToSearch) {
|
||||
return this._receivePiece.searchFolderMessages(
|
||||
folderId, bridgeHandle, phrase, whatToSearch);
|
||||
},
|
||||
|
||||
syncFolderList: function(callback) {
|
||||
return this._receivePiece.syncFolderList(callback);
|
||||
},
|
||||
|
||||
sendMessage: function(composer, callback) {
|
||||
return this._sendPiece.sendMessage(
|
||||
composer,
|
||||
function(err, errDetails) {
|
||||
if (!err) {
|
||||
// The saving is done asynchronously as a best-effort.
|
||||
this._receivePiece.saveSentMessage(composer);
|
||||
}
|
||||
callback(err, errDetails, null);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getFolderStorageForFolderId: function(folderId) {
|
||||
return this._receivePiece.getFolderStorageForFolderId(folderId);
|
||||
},
|
||||
|
||||
getFolderMetaForFolderId: function(folderId) {
|
||||
return this._receivePiece.getFolderMetaForFolderId(folderId);
|
||||
},
|
||||
|
||||
runOp: function(op, mode, callback) {
|
||||
return this._receivePiece.runOp(op, mode, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Kick off jobs to create required folders, both locally and on the
|
||||
* server. See imap/account.js and activesync/account.js for documentation.
|
||||
*
|
||||
* @param {function} callback
|
||||
* Called when all jobs have run.
|
||||
*/
|
||||
ensureEssentialOnlineFolders: function(callback) {
|
||||
return this._receivePiece.ensureEssentialOnlineFolders(callback);
|
||||
},
|
||||
|
||||
getFirstFolderWithType: $acctmixins.getFirstFolderWithType,
|
||||
|
||||
upgradeFolderStoragesIfNeeded: function() {
|
||||
for (var key in this._receivePiece._folderStorages) {
|
||||
var storage = this._receivePiece._folderStorages[key];
|
||||
storage.upgradeIfNeeded();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}); // end define
|
||||
343
gaia/apps/email/js/ext/composite/configurator.js
Executable file
343
gaia/apps/email/js/ext/composite/configurator.js
Executable file
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* Configurator for imap+smtp and pop3+smtp.
|
||||
**/
|
||||
|
||||
define(
|
||||
[
|
||||
'logic',
|
||||
'../accountcommon',
|
||||
'../a64',
|
||||
'../allback',
|
||||
'./account',
|
||||
'../date',
|
||||
'require',
|
||||
'exports'
|
||||
],
|
||||
function(
|
||||
logic,
|
||||
$accountcommon,
|
||||
$a64,
|
||||
$allback,
|
||||
$account,
|
||||
$date,
|
||||
require,
|
||||
exports
|
||||
) {
|
||||
|
||||
var allbackMaker = $allback.allbackMaker;
|
||||
|
||||
exports.account = $account;
|
||||
exports.configurator = {
|
||||
tryToCreateAccount: function(universe, userDetails, domainInfo,
|
||||
callback) {
|
||||
var credentials, incomingInfo, smtpConnInfo, incomingType;
|
||||
if (domainInfo) {
|
||||
incomingType = (domainInfo.type === 'imap+smtp' ? 'imap' : 'pop3');
|
||||
var password = null;
|
||||
// If the account has an outgoingPassword, use that; otherwise
|
||||
// use the main password. We must take care to treat null values
|
||||
// as potentially valid in the future, if we allow password-free
|
||||
// account configurations.
|
||||
if (userDetails.outgoingPassword !== undefined) {
|
||||
password = userDetails.outgoingPassword;
|
||||
} else {
|
||||
password = userDetails.password;
|
||||
}
|
||||
credentials = {
|
||||
username: domainInfo.incoming.username,
|
||||
password: userDetails.password,
|
||||
outgoingUsername: domainInfo.outgoing.username,
|
||||
outgoingPassword: password,
|
||||
};
|
||||
if (domainInfo.oauth2Tokens) {
|
||||
// We need to save off all the information so:
|
||||
// - the front-end can reauthorize exclusively from this info.
|
||||
// - the back-end can refresh its token
|
||||
// - on upgrades so we can know if our scope isn't good enough. (Note
|
||||
// that we're not saving off the secret group; upgrades would need to
|
||||
// factor in the auth or token endpoints.)
|
||||
credentials.oauth2 = {
|
||||
authEndpoint: domainInfo.oauth2Settings.authEndpoint,
|
||||
tokenEndpoint: domainInfo.oauth2Settings.tokenEndpoint,
|
||||
scope: domainInfo.oauth2Settings.scope,
|
||||
clientId: domainInfo.oauth2Secrets.clientId,
|
||||
clientSecret: domainInfo.oauth2Secrets.clientSecret,
|
||||
refreshToken: domainInfo.oauth2Tokens.refreshToken,
|
||||
accessToken: domainInfo.oauth2Tokens.accessToken,
|
||||
expireTimeMS: domainInfo.oauth2Tokens.expireTimeMS,
|
||||
// Treat the access token like it was recently retrieved; although we
|
||||
// generally expect the XOAUTH2 case should go through without
|
||||
// failure, in the event something is wrong, immediately re-fetching
|
||||
// a new accessToken is not going to be useful for us.
|
||||
_transientLastRenew: $date.PERFNOW()
|
||||
};
|
||||
}
|
||||
incomingInfo = {
|
||||
hostname: domainInfo.incoming.hostname,
|
||||
port: domainInfo.incoming.port,
|
||||
crypto: (typeof domainInfo.incoming.socketType === 'string' ?
|
||||
domainInfo.incoming.socketType.toLowerCase() :
|
||||
domainInfo.incoming.socketType),
|
||||
};
|
||||
|
||||
if (incomingType === 'pop3') {
|
||||
incomingInfo.preferredAuthMethod = null;
|
||||
}
|
||||
smtpConnInfo = {
|
||||
emailAddress: userDetails.emailAddress, // used for probing
|
||||
hostname: domainInfo.outgoing.hostname,
|
||||
port: domainInfo.outgoing.port,
|
||||
crypto: (typeof domainInfo.outgoing.socketType === 'string' ?
|
||||
domainInfo.outgoing.socketType.toLowerCase() :
|
||||
domainInfo.outgoing.socketType),
|
||||
};
|
||||
}
|
||||
|
||||
// Note: For OAUTH accounts, the credentials may be updated
|
||||
// in-place if a new access token was required. We don't need to
|
||||
// explicitly save those changes here because we define the
|
||||
// account with the same object below.
|
||||
var incomingPromise = new Promise(function(resolve, reject) {
|
||||
if (incomingType === 'imap') {
|
||||
require(['../imap/probe'], function(probe) {
|
||||
probe.probeAccount(credentials, incomingInfo).then(resolve, reject);
|
||||
});
|
||||
} else {
|
||||
require(['../pop3/probe'], function(probe) {
|
||||
probe.probeAccount(credentials, incomingInfo).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var outgoingPromise = new Promise(function(resolve, reject) {
|
||||
require(['../smtp/probe'], function(probe) {
|
||||
probe.probeAccount(credentials, smtpConnInfo).then(resolve, reject);
|
||||
});
|
||||
});
|
||||
|
||||
// Note: Promise.all() will fire the catch handler as soon as one
|
||||
// of the promises is rejected. While this means we will only see
|
||||
// the first error that returns, it actually works well for our
|
||||
// semantics, as we only notify the user about one side's problems
|
||||
// at a time.
|
||||
Promise.all([incomingPromise, outgoingPromise])
|
||||
.then(function(results) {
|
||||
var incomingConn = results[0].conn;
|
||||
var defineAccount;
|
||||
|
||||
if (incomingType === 'imap') {
|
||||
defineAccount = this._defineImapAccount;
|
||||
} else if (incomingType === 'pop3') {
|
||||
incomingInfo.preferredAuthMethod = incomingConn.authMethod;
|
||||
defineAccount = this._definePop3Account;
|
||||
}
|
||||
defineAccount.call(this,
|
||||
universe, userDetails, credentials,
|
||||
incomingInfo, smtpConnInfo, incomingConn,
|
||||
callback);
|
||||
}.bind(this))
|
||||
.catch(function(ambiguousErr) {
|
||||
// One of the account sides failed. Normally we leave the
|
||||
// IMAP/POP3 side open for reuse, but if the SMTP
|
||||
// configuration falied we must close the incoming connection.
|
||||
// (If the incoming side failed as well, we won't receive the
|
||||
// `.then` callback.)
|
||||
incomingPromise.then(function incomingOkButOutgoingFailed(result) {
|
||||
result.conn.close();
|
||||
// the error is no longer ambiguous; it was SMTP
|
||||
callback(ambiguousErr, /* conn: */ null,
|
||||
{ server: smtpConnInfo.hostname });
|
||||
}).catch(function incomingFailed(incomingErr) {
|
||||
callback(incomingErr, /* conn: */ null,
|
||||
{ server: incomingInfo.hostname });
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
recreateAccount: function(universe, oldVersion, oldAccountInfo, callback) {
|
||||
var oldAccountDef = oldAccountInfo.def;
|
||||
|
||||
var credentials = {
|
||||
username: oldAccountDef.credentials.username,
|
||||
password: oldAccountDef.credentials.password,
|
||||
// (if these two keys are null, keep them that way:)
|
||||
outgoingUsername: oldAccountDef.credentials.outgoingUsername,
|
||||
outgoingPassword: oldAccountDef.credentials.outgoingPassword,
|
||||
authMechanism: oldAccountDef.credentials.authMechanism,
|
||||
oauth2: oldAccountDef.credentials.oauth2
|
||||
};
|
||||
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
|
||||
var oldType = oldAccountDef.type || 'imap+smtp';
|
||||
var accountDef = {
|
||||
id: accountId,
|
||||
name: oldAccountDef.name,
|
||||
|
||||
type: oldType,
|
||||
receiveType: oldType.split('+')[0],
|
||||
sendType: 'smtp',
|
||||
|
||||
syncRange: oldAccountDef.syncRange,
|
||||
syncInterval: oldAccountDef.syncInterval || 0,
|
||||
notifyOnNew: oldAccountDef.hasOwnProperty('notifyOnNew') ?
|
||||
oldAccountDef.notifyOnNew : true,
|
||||
playSoundOnSend: oldAccountDef.hasOwnProperty('playSoundOnSend') ?
|
||||
oldAccountDef.playSoundOnSend : true,
|
||||
|
||||
credentials: credentials,
|
||||
receiveConnInfo: {
|
||||
hostname: oldAccountDef.receiveConnInfo.hostname,
|
||||
port: oldAccountDef.receiveConnInfo.port,
|
||||
crypto: oldAccountDef.receiveConnInfo.crypto,
|
||||
preferredAuthMethod:
|
||||
oldAccountDef.receiveConnInfo.preferredAuthMethod || null,
|
||||
},
|
||||
sendConnInfo: {
|
||||
hostname: oldAccountDef.sendConnInfo.hostname,
|
||||
port: oldAccountDef.sendConnInfo.port,
|
||||
crypto: oldAccountDef.sendConnInfo.crypto,
|
||||
},
|
||||
|
||||
identities: $accountcommon.recreateIdentities(universe, accountId,
|
||||
oldAccountDef.identities)
|
||||
};
|
||||
|
||||
this._loadAccount(universe, accountDef,
|
||||
oldAccountInfo.folderInfo, null, function(account) {
|
||||
callback(null, account, null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Define an account now that we have verified the credentials are good and
|
||||
* the server meets our minimal functionality standards. We are also
|
||||
* provided with the protocol connection that was used to perform the check
|
||||
* so we can immediately put it to work.
|
||||
*/
|
||||
_defineImapAccount: function(universe, userDetails, credentials,
|
||||
incomingInfo, smtpConnInfo, imapProtoConn,
|
||||
callback) {
|
||||
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
|
||||
var accountDef = {
|
||||
id: accountId,
|
||||
name: userDetails.accountName || userDetails.emailAddress,
|
||||
defaultPriority: $date.NOW(),
|
||||
|
||||
type: 'imap+smtp',
|
||||
receiveType: 'imap',
|
||||
sendType: 'smtp',
|
||||
|
||||
syncRange: 'auto',
|
||||
syncInterval: userDetails.syncInterval || 0,
|
||||
notifyOnNew: userDetails.hasOwnProperty('notifyOnNew') ?
|
||||
userDetails.notifyOnNew : true,
|
||||
playSoundOnSend: userDetails.hasOwnProperty('playSoundOnSend') ?
|
||||
userDetails.playSoundOnSend : true,
|
||||
|
||||
credentials: credentials,
|
||||
receiveConnInfo: incomingInfo,
|
||||
sendConnInfo: smtpConnInfo,
|
||||
|
||||
identities: [
|
||||
{
|
||||
id: accountId + '/' +
|
||||
$a64.encodeInt(universe.config.nextIdentityNum++),
|
||||
name: userDetails.displayName,
|
||||
address: userDetails.emailAddress,
|
||||
replyTo: null,
|
||||
signature: null,
|
||||
signatureEnabled: false
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
this._loadAccount(universe, accountDef, null,
|
||||
imapProtoConn, function(account) {
|
||||
callback(null, account, null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Define an account now that we have verified the credentials are good and
|
||||
* the server meets our minimal functionality standards. We are also
|
||||
* provided with the protocol connection that was used to perform the check
|
||||
* so we can immediately put it to work.
|
||||
*/
|
||||
_definePop3Account: function(universe, userDetails, credentials,
|
||||
incomingInfo, smtpConnInfo, pop3ProtoConn,
|
||||
callback) {
|
||||
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
|
||||
var accountDef = {
|
||||
id: accountId,
|
||||
name: userDetails.accountName || userDetails.emailAddress,
|
||||
defaultPriority: $date.NOW(),
|
||||
|
||||
type: 'pop3+smtp',
|
||||
receiveType: 'pop3',
|
||||
sendType: 'smtp',
|
||||
|
||||
syncRange: 'auto',
|
||||
syncInterval: userDetails.syncInterval || 0,
|
||||
notifyOnNew: userDetails.hasOwnProperty('notifyOnNew') ?
|
||||
userDetails.notifyOnNew : true,
|
||||
playSoundOnSend: userDetails.hasOwnProperty('playSoundOnSend') ?
|
||||
userDetails.playSoundOnSend : true,
|
||||
|
||||
credentials: credentials,
|
||||
receiveConnInfo: incomingInfo,
|
||||
sendConnInfo: smtpConnInfo,
|
||||
|
||||
identities: [
|
||||
{
|
||||
id: accountId + '/' +
|
||||
$a64.encodeInt(universe.config.nextIdentityNum++),
|
||||
name: userDetails.displayName,
|
||||
address: userDetails.emailAddress,
|
||||
replyTo: null,
|
||||
signature: null,
|
||||
signatureEnabled: false
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this._loadAccount(universe, accountDef, null,
|
||||
pop3ProtoConn, function(account) {
|
||||
callback(null, account, null);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the account def and folder info for our new (or recreated) account and
|
||||
* then load it.
|
||||
*/
|
||||
_loadAccount: function(universe, accountDef, oldFolderInfo, protoConn,
|
||||
callback) {
|
||||
var folderInfo;
|
||||
if (accountDef.receiveType === 'imap') {
|
||||
folderInfo = {
|
||||
$meta: {
|
||||
nextFolderNum: 0,
|
||||
nextMutationNum: 0,
|
||||
lastFolderSyncAt: 0,
|
||||
capability: (oldFolderInfo && oldFolderInfo.$meta.capability) ||
|
||||
protoConn.capability
|
||||
},
|
||||
$mutations: [],
|
||||
$mutationState: {},
|
||||
};
|
||||
} else { // POP3
|
||||
folderInfo = {
|
||||
$meta: {
|
||||
nextFolderNum: 0,
|
||||
nextMutationNum: 0,
|
||||
lastFolderSyncAt: 0,
|
||||
},
|
||||
$mutations: [],
|
||||
$mutationState: {},
|
||||
};
|
||||
}
|
||||
universe.saveAccountDef(accountDef, folderInfo);
|
||||
universe._loadAccount(accountDef, folderInfo, protoConn, callback);
|
||||
},
|
||||
};
|
||||
|
||||
}); // end define
|
||||
376
gaia/apps/email/js/ext/composite/incoming.js
Executable file
376
gaia/apps/email/js/ext/composite/incoming.js
Executable file
|
|
@ -0,0 +1,376 @@
|
|||
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
|
||||
Loading…
Reference in a new issue