Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
310 lines
9.6 KiB
JavaScript
Executable file
310 lines
9.6 KiB
JavaScript
Executable file
define([
|
|
'logic',
|
|
'../errbackoff',
|
|
'../composite/incoming',
|
|
'./sync',
|
|
'../errorutils',
|
|
'./jobs',
|
|
'../drafts/draft_rep',
|
|
'../disaster-recovery',
|
|
'module',
|
|
'require',
|
|
'exports'],
|
|
function(
|
|
logic,
|
|
errbackoff,
|
|
incoming,
|
|
pop3sync,
|
|
errorutils,
|
|
pop3jobs,
|
|
draftRep,
|
|
DisasterRecovery,
|
|
module,
|
|
require,
|
|
exports
|
|
) {
|
|
|
|
var CompositeIncomingAccount = incoming.CompositeIncomingAccount;
|
|
|
|
/**
|
|
* Define a POP3 account. Much of the functionality here is similar
|
|
* to IMAP; Pop3Account inherits the shared parts from
|
|
* CompositeIncomingAccount.
|
|
*/
|
|
function Pop3Account(universe, compositeAccount, accountId, credentials,
|
|
connInfo, folderInfos, dbConn, existingProtoConn) {
|
|
logic.defineScope(this, 'Account', { accountId: accountId,
|
|
accountType: 'pop3' });
|
|
|
|
CompositeIncomingAccount.apply(
|
|
this, [pop3sync.Pop3FolderSyncer].concat(Array.slice(arguments)));
|
|
|
|
// Set up connection information. We can't make much use of
|
|
// connection pooling since the POP3 protocol only allows one client
|
|
// to access a mailbox at a given time, so there's no connection pool.
|
|
this._conn = null;
|
|
this._pendingConnectionRequests = [];
|
|
this._backoffEndpoint = errbackoff.createEndpoint('pop3:' + this.id, this);
|
|
|
|
// If we have an existing connection from setting up the account, we
|
|
// can reuse that during the first sync.
|
|
if (existingProtoConn) {
|
|
DisasterRecovery.associateSocketWithAccount(
|
|
existingProtoConn.socket, this);
|
|
this._conn = existingProtoConn;
|
|
}
|
|
|
|
// Immediately ensure that we have any required local-only folders,
|
|
// as those can be created even while offline.
|
|
this.ensureEssentialOfflineFolders();
|
|
|
|
this._jobDriver = new pop3jobs.Pop3JobDriver(
|
|
this, this._folderInfos.$mutationState);
|
|
}
|
|
exports.Account = exports.Pop3Account = Pop3Account;
|
|
Pop3Account.prototype = Object.create(CompositeIncomingAccount.prototype);
|
|
var properties = {
|
|
type: 'pop3',
|
|
supportsServerFolders: false,
|
|
toString: function() {
|
|
return '[Pop3Account: ' + this.id + ']';
|
|
},
|
|
|
|
/**
|
|
* Call the callback with a live, authenticated connection. Clients
|
|
* should call done() when finished with the connection. (In our
|
|
* case, pop3/sync.js has a lazyWithConnection wrapper which
|
|
* abstracts the `done` callback.)
|
|
* @param {function(err, conn, done)} cb
|
|
*/
|
|
withConnection: function(cb, whyLabel) {
|
|
// This implementation serializes withConnection requests so that
|
|
// we don't step on requests' toes. While Pop3Client wouldn't mix
|
|
// up the requests themselves, interleaving different operations
|
|
// could result in undesired consequences.
|
|
this._pendingConnectionRequests.push(cb);
|
|
var done = function() {
|
|
var req = this._pendingConnectionRequests.shift();
|
|
if (req) {
|
|
var next = function(err) {
|
|
if (err) {
|
|
req(err);
|
|
done();
|
|
} else {
|
|
req(null, this._conn, done);
|
|
}
|
|
}.bind(this);
|
|
if (!this._conn || this._conn.state === 'disconnected') {
|
|
this._makeConnection(next, whyLabel);
|
|
} else {
|
|
next();
|
|
}
|
|
}
|
|
}.bind(this);
|
|
|
|
if (this._pendingConnectionRequests.length === 1) {
|
|
done();
|
|
}
|
|
},
|
|
|
|
/** @override */
|
|
__folderDoneWithConnection: function(conn) {
|
|
// IMAP uses this function to perform folder-specific connection cleanup.
|
|
// We don't need to do anything here.
|
|
},
|
|
|
|
/**
|
|
* Create a new POP3 connection, and call the callback when we
|
|
* have established the connection (or with an error if we failed).
|
|
* Since POP3 only uses one connection at a time, this function also
|
|
* assigns the given connection to this._conn.
|
|
*
|
|
* Don't use this function directly; instead use `withConnection` or
|
|
* a higher-level wrapper.
|
|
*
|
|
* @param {function(err, conn)} callback
|
|
* @param {String} whyLabel A descriptive name for the connection.
|
|
*/
|
|
_makeConnection: function(callback, whyLabel) {
|
|
// Mark a pending connection synchronously; the require call will
|
|
// not return until at least the next turn of the event loop, so
|
|
// we need to know that there's a pending connection request in
|
|
// progress.
|
|
this._conn = true;
|
|
// Dynamically load the probe/pop3 code to speed up startup.
|
|
require(['./pop3', './probe'], function(pop3, pop3probe) {
|
|
logic(this, 'createConnection', { label: whyLabel });
|
|
var opts = {
|
|
host: this._connInfo.hostname,
|
|
port: this._connInfo.port,
|
|
crypto: this._connInfo.crypto,
|
|
|
|
preferredAuthMethod: this._connInfo.preferredAuthMethod,
|
|
|
|
username: this._credentials.username,
|
|
password: this._credentials.password,
|
|
};
|
|
|
|
var conn = this._conn = new pop3.Pop3Client(opts, function(err) {
|
|
if (err) {
|
|
// Failed to get the connection:
|
|
console.error('Connect error:', err.name, 'formal:', err, 'on',
|
|
this._connInfo.hostname, this._connInfo.port);
|
|
|
|
err = pop3probe.normalizePop3Error(err);
|
|
|
|
if (errorutils.shouldReportProblem(err)) {
|
|
this.universe.__reportAccountProblem(
|
|
this.compositeAccount, err, 'incoming');
|
|
}
|
|
|
|
callback && callback(err, null);
|
|
conn.close();
|
|
|
|
// Track this failure for backoff purposes.
|
|
if (errorutils.shouldRetry(err)) {
|
|
if (this._backoffEndpoint.noteConnectFailureMaybeRetry(
|
|
errorutils.wasErrorFromReachableState(err))) {
|
|
this._backoffEndpoint.scheduleConnectAttempt(
|
|
this._makeConnection.bind(this));
|
|
} else {
|
|
this._backoffEndpoint.noteBrokenConnection();
|
|
}
|
|
} else {
|
|
this._backoffEndpoint.noteBrokenConnection();
|
|
}
|
|
}
|
|
// Succeeded:
|
|
else {
|
|
this._backoffEndpoint.noteConnectSuccess();
|
|
callback && callback(null, conn);
|
|
}
|
|
}.bind(this));
|
|
|
|
DisasterRecovery.associateSocketWithAccount(conn.socket, this);
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Save an attachment-stripped version of the sent draft to our sent folder.
|
|
*/
|
|
saveSentMessage: function(composer) {
|
|
var sentFolder = this.getFirstFolderWithType('sent');
|
|
if (!sentFolder) {
|
|
return;
|
|
}
|
|
|
|
var sentStorage = this.getFolderStorageForFolderId(sentFolder.id);
|
|
var id = sentStorage._issueNewHeaderId();
|
|
var suid = sentStorage.folderId + '/' + id;
|
|
|
|
var sentPieces = draftRep.cloneDraftMessageForSentFolderWithoutAttachments(
|
|
composer.header, composer.body, { id: id, suid: suid });
|
|
|
|
this.universe.saveSentDraft(sentFolder.id,
|
|
sentPieces.header, sentPieces.body);
|
|
},
|
|
|
|
/**
|
|
* Delete the given folder. (This always happens locally.)
|
|
*/
|
|
deleteFolder: function(folderId, callback) {
|
|
if (!this._folderInfos.hasOwnProperty(folderId)) {
|
|
throw new Error("No such folder: " + folderId);
|
|
}
|
|
var folderMeta = this._folderInfos[folderId].$meta;
|
|
logic(self, 'deleteFolder', { path: folderMeta.path });
|
|
self._forgetFolder(folderId);
|
|
callback && callback(null, folderMeta);
|
|
},
|
|
|
|
/**
|
|
* Shut down the account and close the connection.
|
|
*/
|
|
shutdown: function(callback) {
|
|
CompositeIncomingAccount.prototype.shutdownFolders.call(this);
|
|
|
|
this._backoffEndpoint.shutdown();
|
|
|
|
if (this._conn && this._conn.close) {
|
|
this._conn.close();
|
|
}
|
|
callback && callback();
|
|
},
|
|
|
|
/**
|
|
* Attempt to create a new, authenticated connection using the
|
|
* current credentials. If a current connection is already
|
|
* established, terminates the existing connection first.
|
|
*
|
|
* @param {function(err)} callback
|
|
*/
|
|
checkAccount: function(callback) {
|
|
// Disconnect first so as to properly check credentials.
|
|
if (this._conn !== null) {
|
|
if (this._conn.state !== 'disconnected') {
|
|
this._conn.close();
|
|
}
|
|
this._conn = null;
|
|
}
|
|
logic(this, 'checkAccount_begin');
|
|
this.withConnection(function(err) {
|
|
logic(this, 'checkAccount_end', { error: err });
|
|
callback(err);
|
|
}.bind(this), 'checkAccount');
|
|
},
|
|
|
|
/**
|
|
* Ensure that local-only folders exist. This is run immediately
|
|
* upon account initialization. Since POP3 doesn't support server
|
|
* folders, all folders are local-only, so this function does all
|
|
* the hard work.
|
|
*/
|
|
ensureEssentialOfflineFolders: function() {
|
|
// Create required folders if necessary.
|
|
[ 'sent', 'localdrafts', 'trash', 'outbox' ].forEach(function(folderType) {
|
|
if (!this.getFirstFolderWithType(folderType)) {
|
|
this._learnAboutFolder(
|
|
/* name: */ folderType,
|
|
/* path: */ folderType,
|
|
/* parentId: */ null,
|
|
/* type: */ folderType,
|
|
/* delim: */ '',
|
|
/* depth: */ 0,
|
|
/* suppressNotification: */ true);
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* POP3 doesn't support server folders, so all folder creation is
|
|
* done in `ensureEssentialOfflineFolders`.
|
|
|
|
* @param {function} callback
|
|
* Called immediately, for homogeneity with other account types.
|
|
*/
|
|
ensureEssentialOnlineFolders: function(callback) {
|
|
// All the important work is already done. Yay POP3!
|
|
callback && callback();
|
|
},
|
|
|
|
|
|
/**
|
|
* Destroy the account when the account has been deleted.
|
|
*/
|
|
accountDeleted: function() {
|
|
this._alive = false;
|
|
this.shutdown();
|
|
},
|
|
|
|
};
|
|
|
|
// Inherit Pop3Account from CompositeIncomingAccount:
|
|
// XXX: Use mix.js when it lands in the streaming patch.
|
|
for (var k in properties) {
|
|
Object.defineProperty(Pop3Account.prototype, k,
|
|
Object.getOwnPropertyDescriptor(properties, k));
|
|
}
|
|
|
|
|
|
}); // end define
|