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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,310 @@
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

View file

@ -0,0 +1,188 @@
define(['module', 'exports', 'logic', '../allback', 'mix',
'../jobmixins', '../drafts/jobs', './pop3'],
function(module, exports, logic, allback, mix,
jobmixins, draftsJobs, pop3) {
/**
* Manage the jobs for a POP3 account. POP3 does not support
* server-side folders, so many of these operations are local-only.
* Operations not implemented in the Pop3JobDriver are ignored and
* assumed unsupported. For instance, issuing a "move" command will
* execute local_do_move, but not do_move. It is assumed that unit
* tests will ensure we've implemented all required jobs.
*/
function Pop3JobDriver(account, state) {
this.account = account;
this.resilientServerIds = true; // once assigned, the server never changes IDs
this._heldMutexReleasers = [];
logic.defineScope(this, 'Pop3JobDriver', { accountId: account.id });
// For tracking state as used in jobmixins:
this._stateDelta = {};
this._state = state;
if (!state.hasOwnProperty('suidToServerId')) {
state.suidToServerId = {};
state.moveMap = {};
}
}
exports.Pop3JobDriver = Pop3JobDriver;
Pop3JobDriver.prototype = {
/**
* Request access to a POP3 folder for mutation. This acquires a
* write mutex on the FolderStorage. The callback will be invoked
* with the folder and the raw connection.
*
* There is no need to explicitly release the connection when done;
* it will be automatically released when the mutex is released if
* desirable.
*
* This function is used by jobmixins.
*/
_accessFolderForMutation: function(
folderId, needConn, callback, deathback, label) {
var storage = this.account.getFolderStorageForFolderId(folderId);
storage.runMutexed(label, function(releaseMutex) {
this._heldMutexReleasers.push(releaseMutex);
try {
// The folderSyncer is like IMAP/ActiveSync's folderConn.
callback(storage.folderSyncer, storage);
} catch (ex) {
logic(this, 'callbackErr', { ex: ex });
}
}.bind(this));
},
/**
* Create a folder locally. (Again, no remote folders, so
* do_createFolder is not implemented.)
*/
local_do_createFolder: function(op, callback) {
var path, delim, parentFolderId = null, depth = 0;
if (op.parentFolderId) {
if (!this.account._folderInfos.hasOwnProperty(op.parentFolderId)) {
throw new Error("No such folder: " + op.parentFolderId);
}
var parentFolder = this.account._folderInfos[op.parentFolderId];
delim = parentFolder.$meta.delim;
path = parentFolder.$meta.path + delim;
parentFolderId = parentFolder.$meta.id;
depth = parentFolder.depth + 1;
}
else {
path = '';
delim = '/';
}
if (typeof(op.folderName) === 'string')
path += op.folderName;
else
path += op.folderName.join(delim);
if (op.containOnlyOtherFolders) {
path += delim;
}
if (this.account.getFolderByPath(path)) {
callback(null);
} else {
var folderMeta = self.account._learnAboutFolder(
op.folderName, path, parentFolderId, 'normal', delim, depth);
callback(null, folderMeta);
}
},
/**
* Delete old messages from disk. Since we currently leave mail on
* the server, we won't incur permanent data loss.
*/
local_do_purgeExcessMessages: function(op, callback) {
this._accessFolderForMutation(
op.folderId, false,
function withMutex(_ignoredConn, storage) {
storage.purgeExcessMessages(function(numDeleted, cutTS) {
// Indicate that we want a save performed if any messages got deleted.
callback(null, null, numDeleted > 0);
});
},
null,
'purgeExcessMessages');
},
local_do_saveSentDraft: function(op, callback) {
var self = this;
this._accessFolderForMutation(
op.folderId, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
var latch = allback.latch();
folderStorage.addMessageHeader(op.headerInfo, op.bodyInfo,
latch.defer());
folderStorage.addMessageBody(op.headerInfo, op.bodyInfo, latch.defer());
latch.then(function(results) {
// header/body insertion can't fail
callback(null, null, /* save */ true);
});
},
/* no conn => no deathback required */ null,
'saveSentDraft');
},
do_syncFolderList: function(op, doneCallback) {
this.account.meta.lastFolderSyncAt = Date.now();
doneCallback(null);
},
/** No-op to silence warnings. Perhaps implement someday. */
do_modtags: function(op, doneCallback) {
doneCallback(null);
},
/** No-op to silence warnings. Perhaps implement someday. */
undo_modtags: function(op, doneCallback) {
doneCallback(null);
},
// Local modifications (move, delete, and modtags) simply reuse the
// jobmixins implementation. No server-side implementations are
// necessary here, so they are omitted.
local_do_modtags: jobmixins.local_do_modtags,
local_undo_modtags: jobmixins.local_undo_modtags,
local_do_move: jobmixins.local_do_move,
local_undo_move: jobmixins.local_undo_move,
local_do_delete: jobmixins.local_do_delete,
local_undo_delete: jobmixins.local_undo_delete,
// Download operations (for attachments) are irrelevant because
// downloading the bodies always fetches the attachments too.
local_do_downloadBodies: jobmixins.local_do_downloadBodies,
do_downloadBodies: jobmixins.do_downloadBodies,
check_downloadBodies: jobmixins.check_downloadBodies,
check_downloadBodyReps: jobmixins.check_downloadBodyReps,
do_downloadBodyReps: jobmixins.do_downloadBodyReps,
local_do_downloadBodyReps: jobmixins.local_do_downloadBodyReps,
local_do_sendOutboxMessages: jobmixins.local_do_sendOutboxMessages,
do_sendOutboxMessages: jobmixins.do_sendOutboxMessages,
check_sendOutboxMessages: jobmixins.check_sendOutboxMessages,
local_undo_sendOutboxMessages: jobmixins.local_undo_sendOutboxMessages,
undo_sendOutboxMessages: jobmixins.undo_sendOutboxMessages,
local_do_setOutboxSyncEnabled: jobmixins.local_do_setOutboxSyncEnabled,
local_do_upgradeDB: jobmixins.local_do_upgradeDB,
// These utility functions are necessary.
postJobCleanup: jobmixins.postJobCleanup,
allJobsDone: jobmixins.allJobsDone,
_partitionAndAccessFoldersSequentially:
jobmixins._partitionAndAccessFoldersSequentially
};
mix(Pop3JobDriver.prototype, draftsJobs.draftsMixins);
}); // end define

View file

@ -0,0 +1,136 @@
'use strict';
// XXX: This is copied from shared/js/mime_mapper.js until the
// download manager ships.
/**
* MimeMapper helps gaia apps to decide the mapping of mimetype and extension.
* The use cases often happen when apps need to know about the exact
* mimetypes or extensions, such as to delegate the open web activity, we must
* have suitable mimetypes or extensions to request the right activity
*
* The mapping is basically created according to:
* http://en.wikipedia.org/wiki/Internet_media_type
*
* The supported formats are considered base on the deviceStorage properties:
* http://dxr.mozilla.org/mozilla-central/toolkit/content/
* devicestorage.properties
*
*/
define(function() {
return {
// This list only contains the extensions we currently supported
// We should make it more complete for further usages
_typeToExtensionMap: {
// Image
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/bmp': 'bmp',
// Audio
'audio/mpeg': 'mp3',
'audio/mp4': 'm4a',
'audio/ogg': 'ogg',
'audio/webm': 'webm',
'audio/3gpp': '3gp',
'audio/amr': 'amr',
// Video
'video/mp4': 'mp4',
'video/mpeg': 'mpg',
'video/ogg': 'ogg',
'video/webm': 'webm',
'video/3gpp': '3gp',
// Application
// If we want to support some types, like pdf, just add
// 'application/pdf': 'pdf'
'application/vcard': 'vcf',
// Text
'text/vcard': 'vcf',
'text/x-vcard': 'vcf'
},
// This list only contains the mimetypes we currently supported
// We should make it more complete for further usages
_extensionToTypeMap: {
// Image
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpe': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'bmp': 'image/bmp',
// Audio
'mp3': 'audio/mpeg',
'm4a': 'audio/mp4',
'm4b': 'audio/mp4',
'm4p': 'audio/mp4',
'm4r': 'audio/mp4',
'aac': 'audio/aac',
'opus': 'audio/ogg',
'amr': 'audio/amr',
// Video
'mp4': 'video/mp4',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'ogv': 'video/ogg',
'ogx': 'video/ogg',
'webm': 'video/webm',
'3gp': 'video/3gpp',
'ogg': 'video/ogg',
// Application
// If we want to support some extensions, like pdf, just add
// 'pdf': 'application/pdf'
// Text
'vcf': 'text/vcard'
},
_parseExtension: function(filename) {
var array = filename.split('.');
return array.length > 1 ? array.pop() : '';
},
isSupportedType: function(mimetype) {
return (mimetype in this._typeToExtensionMap);
},
isSupportedExtension: function(extension) {
return (extension in this._extensionToTypeMap);
},
isFilenameMatchesType: function(filename, mimetype) {
var extension = this._parseExtension(filename);
var guessedType = this.guessTypeFromExtension(extension);
return (guessedType == mimetype);
},
guessExtensionFromType: function(mimetype) {
return this._typeToExtensionMap[mimetype];
},
guessTypeFromExtension: function(extension) {
return this._extensionToTypeMap[extension];
},
// If mimetype is not in the supported list, we will try to
// predict the possible valid mimetype based on extension.
guessTypeFromFileProperties: function(filename, mimetype) {
var extension = this._parseExtension(filename);
var type = this.isSupportedType(mimetype) ?
mimetype : this.guessTypeFromExtension(extension);
return type || '';
},
// if mimetype is not supported, preserve the original extension
// and add the predict result as new extension.
// If both filename and mimetype are not supported, return the original
// filename.
ensureFilenameMatchesType: function(filename, mimetype) {
if (!this.isFilenameMatchesType(filename, mimetype)) {
var guessedExt = this.guessExtensionFromType(mimetype);
if (guessedExt) {
filename += '.' + guessedExt;
}
}
return filename;
}
};
});

View file

@ -0,0 +1,972 @@
define(['module', 'exports', 'logic', 'tcp-socket', 'md5',
'./transport', 'mimeparser', 'imap/imapchew',
'syncbase', 'date',
'mimefuncs',
'./mime_mapper', 'allback'],
function(module, exports, logic, tcpSocket, md5,
transport, MimeParser, imapchew,
syncbase, dateMod, mimefuncs, mimeMapper, allback) {
/**
* The Pop3Client modules and classes are organized according to
* their function, as follows, from low-level to high-level:
*
* [Pop3Parser] parses raw protocol data from the server.
* [Pop3Protocol] handles the request/response semantics
* along with the Request and Response classes,
* which are mostly for internal use. Pop3Protocol
* does not deal with I/O at all.
* [Pop3Client] hooks together the Protocol and a socket, and
* handles high-level details like listing messages.
*
* In general, this tries to share as much code as possible with
* IMAP/ActiveSync. We reuse imapchew.js to normalize POP3 MIME
* messages in the same way as IMAP, to avoid spurious errors trying
* to write yet another translation layer. All of the MIME parsing
* happens in this file; transport.js contains purely wire-level
* logic.
*
* Each Pop3Client is responsible for one connection only;
* Pop3Account in GELAM is responsible for managing connection lifetime.
*
* As of this writing (Nov 2013), there was only one other
* reasonably complete POP3 JavaScript implementation, available at
* <https://github.com/ditesh/node-poplib>. It would have probably
* worked, but since the protocol is simple, it seemed like a better
* idea to avoid patching over Node-isms more than necessary (e.g.
* avoiding Buffers, node socket-isms, etc.). Additionally, that
* library only contained protocol-level details, so we would have
* only really saved some code in transport.js.
*
* For error conditions, this class always normalizes errors into
* the format as documented in the constructor below.
* All external callbacks get passed node-style (err, ...).
*/
// Allow setTimeout and clearTimeout to be shimmed for unit tests.
var setTimeout = window.setTimeout.bind(window);
var clearTimeout = window.clearTimeout.bind(window);
exports.setTimeoutFunctions = function(set, clear) {
setTimeout = set;
clearTimeout = clear;
}
/***************************************************************************
* Pop3Client
*
* Connect to a POP3 server. `cb` is always invoked, with (err) if
* the connction attempt failed. Options are as follows:
*
* @param {string} host
* @param {string} username
* @param {string} password
* @param {string} port
* @param {boolean|'plain'|'ssl'|'starttls'} crypto
* @param {int} connTimeout optional connection timeout
* @param {'apop'|'sasl'|'user-pass'} preferredAuthMethod first method to try
* @param {boolean} debug True to dump the protocol to the console.
*
* The connection's current state is available at `.state`, with the
* following values:
*
* 'disconnected', 'greeting', 'starttls', 'authorization', 'ready'
*
* All callback errors are normalized to the following form:
*
* var err = {
* scope: 'connection|authentication|mailbox|message',
* name: '...',
* message: '...',
* request: Pop3Client.Request (if applicable),
* exception: (A socket error, if available),
* };
*
*/
var Pop3Client = exports.Pop3Client = function(options, cb) {
// for clarity, list the available options:
this.options = options = options || {};
options.host = options.host || null;
options.username = options.username || null;
options.password = options.password || null;
options.port = options.port || null;
options.crypto = options.crypto || false;
options.connTimeout = options.connTimeout || 30000;
options.debug = options.debug || false;
options.authMethods = ['apop', 'sasl', 'user-pass'];
logic.defineScope(this, 'Pop3Client');
if (options.preferredAuthMethod) {
// if we prefer a certain auth method, try that first.
var idx = options.authMethods.indexOf(options.preferredAuthMethod);
if (idx !== -1) {
options.authMethods.splice(idx, 1);
}
options.authMethods.unshift(options.preferredAuthMethod);
}
// Normalize the crypto option:
if (options.crypto === true) {
options.crypto = 'ssl';
} else if (!options.crypto) {
options.crypto = 'plain';
}
if (!options.port) {
options.port = {
'plain': 110,
'starttls': 110,
'ssl': 995
}[options.crypto];
if (!options.port) {
throw new Error('Invalid crypto option for Pop3Client: ' +
options.crypto);
}
}
// The public state of the connection (the only one we really care
// about is 'disconnected')
this.state = 'disconnected';
this.authMethod = null; // Upon successful login, the method that worked.
// Keep track of the message IDs and UIDLs the server has reported
// during this session (these values could change in each
// session, though they probably won't):
this.idToUidl = {};
this.uidlToId = {};
this.idToSize = {};
// An array of {uidl: "", size: 0, number: } for each message
// retrieved as a result of calling LIST
this._messageList = null;
this._greetingLine = null; // contains APOP auth info, if available
this.protocol = new transport.Pop3Protocol();
this.socket = tcpSocket.open(options.host, options.port, {
useSecureTransport: (options.crypto === 'ssl' ||
options.crypto === true)
});
var connectTimeout = setTimeout(function() {
this.state = 'disconnected';
if (connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
cb && cb({
scope: 'connection',
request: null,
name: 'unresponsive-server',
message: 'Could not connect to ' + options.host + ':' + options.port +
' with ' + options.crypto + ' encryption.',
});
}.bind(this), options.connTimeout);
// Hook the protocol and socket together:
this.socket.ondata = this.protocol.onreceive.bind(this.protocol);
this.protocol.onsend = this.socket.send.bind(this.socket);
this.socket.onopen = function() {
console.log('pop3:onopen');
if (connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
this.state = 'greeting';
// No further processing is needed here. We wait for the server
// to send a +OK greeting before we try to authenticate.
}.bind(this);
this.socket.onerror = function(evt) {
var err = evt && evt.data || evt;
console.log('pop3:onerror', err);
if (connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
cb && cb({
scope: 'connection',
request: null,
name: 'unresponsive-server',
message: 'Socket exception: ' + JSON.stringify(err),
exception: err,
});
}.bind(this);
// sync cares about listening for us closing; it has no way to be informed
// by disaster recovery otherwise
this.onclose = null;
this.socket.onclose = function() {
console.log('pop3:onclose');
this.protocol.onclose();
this.close();
if (this.onclose) {
this.onclose();
}
}.bind(this);
// To track requests/responses in the presence of a server
// greeting, store an empty request here. Our request/response
// matching logic will pair the server's greeting with this
// request.
this.protocol.pendingRequests.push(
new transport.Request(null, [], false, function(err, rsp) {
if (err) {
cb && cb({
scope: 'connection',
request: null,
name: 'unresponsive-server',
message: err.getStatusLine(),
response: err,
});
return;
}
// Store the greeting line, it might be needed in authentication
this._greetingLine = rsp.getLineAsString(0);
this._maybeUpgradeConnection(function(err) {
if (err) { cb && cb(err); return; }
this._thenAuthorize(function(err) {
if (!err) {
this.state = 'ready';
}
cb && cb(err);
});
}.bind(this));
}.bind(this)));
}
/**
* Disconnect from the server forcibly. Do not issue a QUIT command.
*/
Pop3Client.prototype.close =
Pop3Client.prototype.die = function() {
if (this.state !== 'disconnected') {
this.state = 'disconnected';
this.socket.close();
// No need to do anything further; we'll tear down when we
// receive the socket's "close" event.
}
}
/**
* Fetch the capabilities from the server. If the connection
* supports STLS and we've specified 'starttls' as the crypto
* option, we upgrade the connection here.
*/
// XXX: UNUSED FOR NOW. Maybe we'll use it later.
Pop3Client.prototype._getCapabilities = function(cb) {
this.protocol.sendRequest('CAPA', [], true, function(err, rsp) {
if (err) {
// It's unlikely this server's going to do much, but we'll try.
this.capabilities = {};
} else {
var lines = rsp.getDataLines();
for (var i = 0; i < lines.length; i++) {
var words = lines[i].split(' ');
this.capabilities[words[0]] = words.slice(1);
}
}
}.bind(this));
}
/**
* If we're trying to use TLS, upgrade now.
*
* This is followed by ._thenAuthorize().
*/
Pop3Client.prototype._maybeUpgradeConnection = function(cb) {
if (this.options.crypto === 'starttls') {
this.state = 'starttls';
this.protocol.sendRequest('STLS', [], false, function(err, rsp) {
if (err) {
cb && cb({
scope: 'connection',
request: err.request,
name: 'bad-security',
message: err.getStatusLine(),
response: err,
});
return;
}
this.socket.upgradeToSecure();
cb();
}.bind(this));
} else {
cb();
}
}
/**
* Set the current state to 'authorization' and attempts to
* authenticate the user with any available authentication method.
* We try APOP first if the server supports it, since we can avoid
* replay attacks and authenticate in one roundtrip. Otherwise, we
* try SASL AUTH PLAIN, which POP3 servers are (in theory) required
* to support if they support SASL at all. Lastly, we fall back to
* plain-old USER/PASS authentication if that's all we have left.
*
* Presently, if one authentication method fails for any reason, we
* simply try the next. We could be smarter and drop out on
* detecting a bad-user-or-pass error.
*/
Pop3Client.prototype._thenAuthorize = function(cb) {
this.state = 'authorization';
this.authMethod = this.options.authMethods.shift();
var user = this.options.username;
var pass = this.options.password;
var secret;
switch(this.authMethod) {
case 'apop':
var match = /<.*?>/.exec(this._greetingLine || "");
var apopTimestamp = match && match[0];
if (!apopTimestamp) {
// if the server doesn't support APOP, try the next method.
this._thenAuthorize(cb);
} else {
secret = md5(apopTimestamp + pass).toLowerCase();
this.protocol.sendRequest(
'APOP', [user, secret], false, function(err, rsp) {
if (err) {
this._greetingLine = null; // try without APOP
this._thenAuthorize(cb);
} else {
cb(); // ready!
}
}.bind(this));
}
break;
case 'sasl':
secret = btoa(user + '\x00' + user + '\x00' + pass);
this.protocol.sendRequest(
'AUTH', ['PLAIN', secret], false, function(err, rsp) {
if (err) {
this._thenAuthorize(cb);
} else {
cb(); // ready!
}
}.bind(this));
break;
case 'user-pass':
default:
this.protocol.sendRequest('USER', [user], false, function(err, rsp) {
if (err) {
cb && cb({
scope: 'authentication',
request: err.request,
name: 'bad-user-or-pass',
message: err.getStatusLine(),
response: err,
});
return;
}
this.protocol.sendRequest('PASS', [pass], false, function(err, rsp) {
if (err) {
cb && cb({
scope: 'authentication',
request: null, // No request logging here; may leak password.
name: 'bad-user-or-pass',
message: err.getStatusLine(),
response: err,
});
return;
}
cb();
}.bind(this));
}.bind(this));
break;
}
}
/*********************************************************************
* MESSAGE FETCHING
*
* POP3 does not support granular partial retrieval; we can only
* download a given number of _lines_ of the message (including
* headers). Thus, in order to download snippets of messages (rather
* than just the entire body), we have to guess at how many lines
* it'll take to get enough MIME data to be able to parse out a
* text/plain snippet.
*
* For now, we'll try to download a few KB of the message, which
* should give plenty of data to form a snippet. We're aiming for a
* sweet spot, because if the message is small enough, we can just
* download the whole thing and be done.
*/
/**
* Issue a QUIT command to the server, persisting any DELE message
* deletions you've enqueued. This also closes the connection.
*/
Pop3Client.prototype.quit = function(cb) {
this.state = 'disconnected';
this.protocol.sendRequest('QUIT', [], false, function(err, rsp) {
this.close();
if (err) {
cb && cb({
scope: 'mailbox',
request: err.request,
name: 'server-problem',
message: err.getStatusLine(),
response: err,
});
} else {
cb && cb();
}
}.bind(this));
}
/**
* Load a mapping of server message numbers to UIDLs, so that we
* can interact with messages stably across sessions. Additionally,
* this fetches a LIST of the messages so that we have a list of
* message sizes in addition to their UIDLs.
*/
Pop3Client.prototype._loadMessageList = function(cb) {
// if we've already loaded IDs this session, we don't need to
// compute them again, because POP3 shows a frozen state of your
// mailbox until you disconnect.
if (this._messageList) {
cb(null, this._messageList);
return;
}
// First, get UIDLs for each message.
this.protocol.sendRequest('UIDL', [], true, function(err, rsp) {
if (err) {
cb && cb({
scope: 'mailbox',
request: err.request,
name: 'server-problem',
message: err.getStatusLine(),
response: err,
});
return;
}
var lines = rsp.getDataLines();
for (var i = 0; i < lines.length; i++) {
var words = lines[i].split(' ');
var number = words[0];
var uidl = words[1];
this.idToUidl[number] = uidl;
this.uidlToId[uidl] = number
}
// because POP3 servers process requests serially, the next LIST
// will not run until after this completes.
}.bind(this));
// Then, get a list of messages so that we can track their size.
this.protocol.sendRequest('LIST', [], true, function(err, rsp) {
if (err) {
cb && cb({
scope: 'mailbox',
request: err.request,
name: 'server-problem',
message: err.getStatusLine(),
response: err,
});
return;
}
var lines = rsp.getDataLines();
var allMessages = [];
for (var i = 0; i < lines.length; i++) {
var words = lines[i].split(' ');
var number = words[0];
var size = parseInt(words[1], 10);
this.idToSize[number] = size;
// Push the message onto the front, so that the last line
// becomes the first message in allMessages. Most POP3 servers
// seem to return messages in ascending date order, so we want
// to process the newest messages first. (Tested with Dovecot,
// Gmail, and AOL.) The resulting list here contains the most
// recent message first.
allMessages.unshift({
uidl: this.idToUidl[number],
size: size,
number: number
});
}
this._messageList = allMessages;
cb && cb(null, allMessages);
}.bind(this));
}
/**
* Fetch the headers and snippets for all messages. Only retrieves
* messages for which filterFunc(uidl) returns true.
*
* @param {object} opts
* @param {function(uidl)} opts.filter Only store messages matching filter
* @param {function(evt)} opts.progress Progress callback
* @param {int} opts.checkpointInterval Call `checkpoint` every N messages
* @param {int} opts.maxMessages Download _at most_ this many
* messages during this listMessages invocation. If we find that
* we would have to download more than this many messages, mark
* the rest as "overflow" messages that could be downloaded in a
* future sync iteration. (Default is infinite.)
* @param {function(next)} opts.checkpoint Callback to periodically save state
* @param {function(err, numSynced, overflowMessages)} cb
* Upon completion, returns the following data:
*
* numSynced: The number of messages synced.
*
* overflowMessages: An array of objects with the following structure:
*
* { uidl: "", size: 0 }
*
* Each message in overflowMessages was NOT downloaded. Instead,
* you should store those UIDLs for future retrieval as part of
* a "Download More Messages" operation.
*/
Pop3Client.prototype.listMessages = function(opts, cb) {
var filterFunc = opts.filter;
var progressCb = opts.progress;
var checkpointInterval = opts.checkpointInterval || null;
var maxMessages = opts.maxMessages || Infinity;
var checkpoint = opts.checkpoint;
var overflowMessages = [];
// Get a mapping of number->UIDL.
this._loadMessageList(function(err, unfilteredMessages) {
if (err) { cb && cb(err); return; }
// Calculate which messages we would need to download.
var totalBytes = 0;
var bytesFetched = 0;
var messages = [];
var seenCount = 0;
// Filter out unwanted messages.
for (var i = 0; i < unfilteredMessages.length; i++) {
var msgInfo = unfilteredMessages[i];
if (!filterFunc || filterFunc(msgInfo.uidl)) {
if (messages.length < maxMessages) {
totalBytes += msgInfo.size;
messages.push(msgInfo);
} else {
overflowMessages.push(msgInfo);
}
} else {
seenCount++;
}
}
console.log('POP3: listMessages found ' +
messages.length + ' new, ' +
overflowMessages.length + ' overflow, and ' +
seenCount + ' seen messages. New UIDLs:');
messages.forEach(function(m) {
console.log('POP3: ' + m.size + ' bytes: ' + m.uidl);
});
var totalMessages = messages.length;
// If we don't provide a checkpoint interval, just do all
// messages at once.
if (!checkpointInterval) {
checkpointInterval = totalMessages;
}
var firstErr = null;
// Download all of the messages in batches.
var nextBatch = function() {
console.log('POP3: Next batch. Messages left: ' + messages.length);
// If there are no more messages or our connection died, we're done.
if (!messages.length || this.protocol.closed) {
console.log('POP3: Sync complete. ' +
totalMessages + ' messages synced, ' +
overflowMessages.length + ' overflow messages.');
cb && cb(firstErr, totalMessages, overflowMessages);
return;
}
var batch = messages.splice(0, checkpointInterval);
var latch = allback.latch();
// Trigger a download for every message in the batch.
batch.forEach(function(m, idx) {
var messageDone = latch.defer(m.number);
this.downloadPartialMessageByNumber(m.number, function(err, msg) {
bytesFetched += m.size;
if (err) {
if (!firstErr) {
firstErr = err;
}
} else {
progressCb && progressCb({
totalBytes: totalBytes,
bytesFetched: bytesFetched,
size: m.size,
message: msg
});
}
messageDone(err);
});
}.bind(this));
// When all messages in this batch have completed, trigger the
// next batch to begin download. If `checkpoint` is provided,
// we'll wait for it to tell us to continue (so that we can
// save the database periodically or perform other
// housekeeping during sync).
latch.then(function(results) {
// figure out if we actually did work so we actually need to save.
var anySaved = false;
for (var num in results) {
console.log('result', num, results[num]);
if (!results[num][0]) {
anySaved = true;
break;
}
}
if (checkpoint && anySaved) {
console.log('POP3: Checkpoint.');
checkpoint(nextBatch);
} else {
nextBatch();
}
});
}.bind(this);
// Kick it off, maestro.
nextBatch();
}.bind(this));
}
/**
* Retrieve the full body (+ attachments) of a message given a UIDL.
*
* @param {string} uidl The message's UIDL as reported by the server.
*/
Pop3Client.prototype.downloadMessageByUidl = function(uidl, cb) {
this._loadMessageList(function(err) {
if (err) {
cb && cb(err);
} else {
this.downloadMessageByNumber(this.uidlToId[uidl], cb);
}
}.bind(this));
}
/**
* Retrieve a portion of one message. The returned message is
* normalized to the format needed by GELAM according to
* `parseMime`.
*
* @param {string} number The message number (on the server)
* @param {function(err, msg)} cb
*/
// XXX: TODO: There are some roundtrips between strings and buffers
// here. This is generally safe (converting to and from UTF-8), but
// it creates unnecessary garbage. Clean this up when we switch over
// to jsmime.
Pop3Client.prototype.downloadPartialMessageByNumber = function(number, cb) {
// Based on SNIPPET_SIZE_GOAL, calculate approximately how many
// lines we'll need to fetch in order to roughly retrieve
// SNIPPET_SIZE_GOAL bytes.
var numLines = Math.floor(syncbase.POP3_SNIPPET_SIZE_GOAL / 80);
this.protocol.sendRequest('TOP', [number, numLines],
true, function(err, rsp) {
if(err) {
cb && cb({
scope: 'message',
request: err.request,
name: 'server-problem',
message: err.getStatusLine(),
response: err,
});
return;
}
var fullSize = this.idToSize[number];
var data = rsp.getDataAsString();
var isSnippet = (!fullSize || data.length < fullSize);
// If we didn't get enough data, msg.body.bodyReps may be empty.
// The values we use for retrieving snippets are
// sufficiently large that we really shouldn't run into this
// case in nearly all cases. We assume that the UI will
// handle this (exceptional) case reasonably.
cb(null, this.parseMime(data, isSnippet, number));
}.bind(this));
}
/**
* Retrieve a message in its entirety, given a server-centric number.
*
* @param {string} number The message number (on the server)
* @param {function(err, msg)} cb
*/
Pop3Client.prototype.downloadMessageByNumber = function(number, cb) {
this.protocol.sendRequest('RETR', [number], true, function(err, rsp) {
if(err) {
cb && cb({
scope: 'message',
request: err.request,
name: 'server-problem',
message: err.getStatusLine(),
response: err,
});
return;
}
cb(null, this.parseMime(rsp.getDataAsString(), false, number));
}.bind(this));
}
/**
* Retrieve a header from a MimeNode given a lowercase headerName.
*/
function safeHeader(node, headerName, defaultValue) {
var allHeaders = node.headers[headerName];
if (allHeaders && allHeaders[0]) {
return allHeaders[0].value;
} else {
return defaultValue || null;
}
}
function safeHeaderParams(node, headerName) {
var allHeaders = node.headers[headerName];
if (allHeaders && allHeaders[0]) {
return allHeaders[0].params || {};
} else {
return {};
}
}
/**
* Convert a MimeParser-intermediate MIME tree to a structure
* format as parsable with imapchew. This allows us to reuse much of
* the parsing code and maintain parity between IMAP and POP3.
*/
function mimeTreeToStructure(node, partId, partMap, partialNode) {
var typeInfo = {};
typeInfo.part = partId || '1';
typeInfo.type = node.contentType.value;
typeInfo.parameters = safeHeaderParams(node, 'content-type');
var dispositionValue = safeHeader(node, 'content-disposition');
if (dispositionValue) {
typeInfo.disposition = dispositionValue;
typeInfo.dispositionParameters =
safeHeaderParams(node, 'content-disposition');
}
typeInfo.id = safeHeader(node, 'content-id');
typeInfo.encoding = 'binary'; // we already decoded it
typeInfo.size = node.content && node.content.length || 0;
typeInfo.description = null; // unsupported (unnecessary)
typeInfo.lines = null; // unsupported (unnecessary)
typeInfo.md5 = null; // unsupported (unnecessary)
typeInfo.childNodes = [];
// If the node was not a multipart node (i.e. it's supposed to
// have content), it's just an empty node. MimeParser leaves
// 'content' undefined, but actually we want an empty array.
if (node.content == null && !/^multipart\//.test(typeInfo.type)) {
node.content = new Uint8Array();
}
if (node.content != null) {
partMap[typeInfo.part] = node.content;
// If this node was only partially downloaded, note it as such
// in a special key on partMap. We'll use this key to later
// indicate that this part's size should be calculated based on
// the bytes we have not downloaded yet.
if (partialNode === node) {
partMap['partial'] = typeInfo.part;
}
}
if (node._childNodes.length) {
for (var i = 0; i < node._childNodes.length; i++) {
var child = node._childNodes[i];
typeInfo.childNodes.push(mimeTreeToStructure(
child, typeInfo.part + '.' + (i + 1), partMap, partialNode));
}
}
return typeInfo;
}
// This function is made visible for test logic external to this module.
Pop3Client.parseMime = function(content) {
return Pop3Client.prototype.parseMime.call(this, content);
}
Pop3Client.prototype.parseMime = function(mimeContent, isSnippet, number) {
var mp = new MimeParser();
var lastNode;
mp.write(mimefuncs.charset.encode(mimeContent, 'utf-8'));
mp.end();
// mimeparser does not generate onbody events for partial pieces of body
// so we find the "last" node through tree-traversal:
lastNode = mp.node;
while (lastNode._currentChild && lastNode !== lastNode._currentChild) {
lastNode = lastNode._currentChild;
}
var rootNode = mp.node;
var partialNode = (isSnippet ? lastNode : null);
var estSize = (number && this.idToSize[number]) || mimeContent.length;
var content;
var dateHeader = safeHeader(rootNode, 'date'), dateTS;
// If we got a date, clamp it to now if it's trying to live in the future
// or it's simply invalid. Our rational for clamping is that we don't
// want spammers to be able to permanently lodge their mails at the top of
// the inbox or to otherwise upset our careful invariants.
var now = dateMod.NOW();
if (dateHeader) {
dateTS = Date.parse(dateHeader);
if (isNaN(dateTS) || dateTS > now) {
dateTS = now;
}
} else {
// If we don't have a date, then just use now as the date. The rationale
// for this is that we are already trusting the message's claimed
// composition date, so it's not like this can be maliciously abused.
dateTS = now;
}
var headerList = [];
for (var key in rootNode.headers) {
headerList.push(key + ': ' + rootNode.headers[key][0].initial + '\r\n');
}
var partMap = {}; // partId -> content
var msg = {
uid: number && this.idToUidl[number], // the server-given ID
'header.fields[]': headerList.join(''),
internaldate: dateTS && imapchew.formatImapDateTime(new Date(dateTS)),
flags: [],
bodystructure: mimeTreeToStructure(rootNode, '1', partMap, partialNode)
};
var rep = imapchew.chewHeaderAndBodyStructure(msg, null, null);
var bodyRepIdx = imapchew.selectSnippetBodyRep(rep.header, rep.bodyInfo);
// Calculate the proper size for all of the parts. Any part we've
// seen will have been fully downloaded, so we have the whole
// thing. We must just attribute the rest of the size to the one
// unfinished part, whose partId is stored in partMap['partial'].
var partSizes = {};
var usedSize = 0;
var partialPartKey = partMap['partial'];
for (var k in partMap) {
if (k === 'partial') { continue; };
if (k !== partialPartKey) {
usedSize += partMap[k].length;
partSizes[k] = partMap[k].length;
}
}
if (partialPartKey) {
partSizes[partialPartKey] = estSize - usedSize;
}
for (var i = 0; i < rep.bodyInfo.bodyReps.length; i++) {
var bodyRep = rep.bodyInfo.bodyReps[i];
content = mimefuncs.charset.decode(partMap[bodyRep.part], 'utf-8');
var req = {
// If bytes is null, imapchew.updateMessageWithFetch knows
// that we've fetched the entire thing. Passing in [-1, -1] as a
// range tells imapchew that we're not done downloading it yet.
bytes: (partialPartKey === bodyRep.part ? [-1, -1] : null),
bodyRepIndex: i,
createSnippet: i === bodyRepIdx,
};
if (content != null) {
bodyRep.size = partSizes[bodyRep.part];
var res = {
bytesFetched: content.length,
text: content
};
imapchew.updateMessageWithFetch(rep.header, rep.bodyInfo, req, res);
}
}
// Convert attachments and related parts to Blobs if we've
// downloaded the whole thing:
for (var i = 0; i < rep.bodyInfo.relatedParts.length; i++) {
var relatedPart = rep.bodyInfo.relatedParts[i];
relatedPart.sizeEstimate = partSizes[relatedPart.part];
content = partMap[relatedPart.part];
if (content != null && partialPartKey !== relatedPart.part) {
relatedPart.file = new Blob([content], {type: relatedPart.type});
}
}
for (var i = 0; i < rep.bodyInfo.attachments.length; i++) {
var att = rep.bodyInfo.attachments[i];
content = partMap[att.part];
att.sizeEstimate = partSizes[att.part];
if (content != null && partialPartKey !== att.part &&
mimeMapper.isSupportedType(att.type)) {
att.file = new Blob([content], {type: att.type});
}
}
// If it's a snippet and we aren't sure that we have attachments,
// guess based on what we know.
if (isSnippet &&
!rep.header.hasAttachments &&
(safeHeader(rootNode, 'x-ms-has-attach') ||
/multipart\/mixed/.test(rootNode.contentType.value) ||
estSize > syncbase.POP3_INFER_ATTACHMENTS_SIZE)) {
rep.header.hasAttachments = true;
}
// If we haven't downloaded the entire message, we need to have
// some way to tell the UI that we actually haven't downloaded all
// of the bodyReps yet. We add this fake bodyRep here, indicating
// that it isn't fully downloaded, so that when the user triggers
// downloadBodyReps, we actually try to fetch the message. In
// POP3, we _don't_ know that we have all bodyReps until we've
// downloaded the whole thing. There could be parts hidden in the
// data we haven't downloaded yet.
rep.bodyInfo.bodyReps.push({
type: 'fake', // not 'text' nor 'html', so it won't be rendered
part: 'fake',
sizeEstimate: 0,
amountDownloaded: 0,
isDownloaded: !isSnippet,
content: null,
size: 0,
});
// POP3 can't display the completely-downloaded-body until we've
// downloaded the entire message, including attachments. So
// unfortunately, no matter how much we've already downloaded, if
// we haven't downloaded the whole thing, we can't start from the
// middle.
rep.header.bytesToDownloadForBodyDisplay = (isSnippet ? estSize : 0);
// to fill: suid, id
return rep;
}
/**
* Display a buffer in a debug-friendly printable format, with
* CRLFs escaped for easy protocol verification.
*/
function bufferToPrintable(line) {
var s = '';
if (Array.isArray(line)) {
line.forEach(function(l) {
s += bufferToPrintable(l) + '\n';
});
return s;
}
for (var i = 0; i < line.length; i++) {
var c = String.fromCharCode(line[i]);
if (c === '\r') { s += '\\r'; }
else if (c === '\n') { s += '\\n'; }
else { s += c; }
}
return s;
}
}); // end define

View file

@ -0,0 +1,181 @@
define([
'./pop3',
'../syncbase',
'logic',
'../errorutils',
'exports'
], function(pop3, syncbase, logic, errorutils, exports) {
var scope = logic.scope('Pop3Prober');
/**
* Validate connection information for an account and verify that the
* server on the other end is something we are capable of sustaining
* an account with.
*
* If we succeed at logging in, hand off the established connection to
* our caller so they can reuse the connection.
*/
exports.probeAccount = function(credentials, connInfo) {
var opts = {
host: connInfo.hostname,
port: connInfo.port,
crypto: connInfo.crypto,
username: credentials.username,
password: credentials.password,
connTimeout: syncbase.CONNECT_TIMEOUT_MS
};
logic(scope, 'connecting', { connInfo: connInfo });
var resolve, reject;
var promise = new Promise(function(_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
// We need the server to support UIDL and TOP. To test that it does,
// first we assume that there's a message in the mailbox. If so, we
// can just run `UIDL 1` and `TOP 1` to see if it works. If there
// aren't any messages, we'll just run `UIDL` by itself to see if
// that works. If so, great. If it errors out in any other path, the
// server doesn't have the support we need and we must give up.
var conn = new pop3.Pop3Client(opts, function(err) {
if (err) { reject(err); return; }
conn.protocol.sendRequest('UIDL', ['1'], false, function(err, rsp) {
if (rsp) {
conn.protocol.sendRequest('TOP', ['1', '0'], true, function(err, rsp) {
if (rsp) {
// both UIDL and TOP work. Awesome!
resolve(conn);
} else if (err.err) {
// Uh, this server must not support TOP. That sucks.
logic(scope, 'server-not-great', { why: 'no TOP' });
reject('pop-server-not-great');
} else {
// if the error was socket-level or something, let it pass
// through untouched
reject(rsp.err);
}
});
} else {
// Either their inbox is empty or they don't support UIDL.
conn.protocol.sendRequest('UIDL', [], true, function(err, rsp) {
if (rsp) {
// It looks like they support UIDL, so let's go for it.
resolve(conn);
} else if (err.err) {
// They must not support UIDL. Not good enough.
logic(scope, 'server-not-great', { why: 'no UIDL' });
reject('pop-server-not-great');
} else {
// if the error was socket-level or something, let it pass
// through untouched
reject(rsp.err);
}
});
}
});
});
return promise
.then(function(conn) {
logic(scope, 'success');
return {
conn: conn,
timezoneOffset: null
};
})
.catch(function(err) {
err = normalizePop3Error(err);
logic(scope, 'error', { error: err });
if (conn) {
conn.close();
}
return Promise.reject(err);
});
}
// These strings were taken verbatim from failed Gmail POP connection logs:
var GMAIL_POP_DISABLED_RE = /\[SYS\/PERM\] Your account is not enabled for POP/;
var GMAIL_DOMAIN_DISABLED_RE =
/\[SYS\/PERM\] POP access is disabled for your domain\./;
function analyzePop3LibraryError(err) {
if (!err || !err.name) {
return null;
}
// All POP3 library callback errors are normalized to the following form:
//
// var err = {
// scope: 'connection|authentication|mailbox|message',
// name: '...',
// message: '...',
// request: Pop3Client.Request (if applicable),
// exception: (A socket error, if available),
// };
if (err.name === 'bad-user-or-pass' &&
err.message && GMAIL_POP_DISABLED_RE.test(err.message)) {
return 'pop3-disabled';
}
else if (err.name === 'bad-user-or-pass' &&
err.message && GMAIL_DOMAIN_DISABLED_RE.test(err.message)) {
return 'pop3-disabled';
}
// If there was a socket exception and the exception looks like
// a security exception, note that it was a security-related
// problem rather than just a bad server connection.
else if (err.name === 'unresponsive-server' && err.exception &&
err.exception.name && /security/i.test(err.exception.name)) {
return 'bad-security';
}
// In two cases (bad auth, and unresponsive server), we might get
// a more detailed status message from the server that saysa that
// our account (or the entire server) is temporarily unavailble.
// Per RFC 3206, these statuses indicate that the server is
// unavailable right now but will be later.
else if ((err.name === 'unresponsive-server' ||
err.name === 'bad-user-or-pass') &&
err.message && /\[(LOGIN-DELAY|SYS|IN-USE)/i.test(err.message)) {
return 'server-maintenance';
} else {
return err.name;
}
}
var normalizePop3Error = exports.normalizePop3Error = function(err) {
var reportAs = (analyzePop3LibraryError(err) ||
errorutils.analyzeException(err) ||
'unknown');
logic(scope, 'normalized-error', { error: err, reportAs: reportAs });
return reportAs;
};
/**
* Notes on transient failures:
*
* LOGIN-DELAY: RFC2449 defines the LOGIN-DELAY capability to tell the
* client how often it should check and introduces it as a response
* code if the client checks too frequently. See
* http://tools.ietf.org/html/rfc2449#section-8.1.1
*
* SYS: RFC3206 provides disambiguation between system failures and
* auth failures. SYS/TEMP is something that should go away on its
* own. SYS/PERM is for errors that probably require a human involved.
* We aren't planning on actually telling the user what the SYS/PERM
* problem was so they can contact tech support, so we lump them in
* the same bucket. See http://tools.ietf.org/html/rfc3206#section-4
*
* IN-USE: Someone else is already in the maildrop, probably another
* POP3 client. If optimizing for multiple POP3-using devices was
* something we wanted to optimize for we would indicate a desire to
* retry here with a more extensive back-off strategy. See
* http://tools.ietf.org/html/rfc2449#section-8.1.2
*/
}); // end define

View file

@ -0,0 +1,662 @@
define(['logic', '../util', 'module', 'require', 'exports',
'../mailchew', '../syncbase', '../date', '../jobmixins',
'../allback', './pop3'],
function(logic, util, module, require, exports,
mailchew, sync, date, jobmixins,
allback, pop3) {
var PASTWARDS = 1;
/**
* Manage the synchronization process for POP3 accounts. In IMAP and
* ActiveSync, the work of this class is split in two (a `folderConn`
* and syncer), but since POP3 has no concept of folders, the syncer
* manages everything itself.
*
* This class still gets created for each folder for compatibiliy with
* IMAP/ActiveSync, but we fast-path out of sync operations if the
* folder we're looking at isn't the inbox.
*/
function Pop3FolderSyncer(account, storage) {
this.account = account;
this.storage = storage;
logic.defineScope(this, 'Pop3FolderSyncer', {
accountId: account.id,
folderId: storage.folderId
});
// Only sync folders if this is the inbox. Other folders are client-side only.
this.isInbox = (storage.folderMeta.type === 'inbox');
}
exports.Pop3FolderSyncer = Pop3FolderSyncer;
/**
* Wrap a function with connection handling, as follows:
* - If a successful connection can be established, fn gets called with
* a connection and the rest of the arguments. The argument at index
* cbIndex is wrapped to automatically call the connection `done`
* callback.
* - If connection fails, the argument at index cbIndex is called with
* the connection error.
*
* @param {boolean} getNew If a fresh connection should always be made.
* @param {int} cbIndex Index of the parent function's callback in args
* @param {string} whyLabel Description for why we need the connection
*/
function lazyWithConnection(getNew, cbIndex, whyLabel, fn) {
return function pop3LazyWithConnection() {
var args = Array.slice(arguments);
require([], function () {
var next = function() {
// Only the inbox actually needs a connection. Using the
// connection in a non-inbox folder is an error.
if (!this.isInbox) {
fn.apply(this, [null].concat(args));
return;
}
this.account.withConnection(function (err, conn, done) {
var callback = args[cbIndex];
if (err) {
callback && callback(err);
} else {
args[cbIndex] = function lazyDone(err) {
done();
callback && callback(err);
};
fn.apply(this, [conn].concat(args));
}
}.bind(this), whyLabel);
}.bind(this);
// if we require a fresh connection, close out the old one first.
if (getNew && this.account._conn &&
this.account._conn.state !== 'disconnected') {
this.account._conn.quit(next);
} else {
next();
}
}.bind(this));
};
};
Pop3FolderSyncer.prototype = {
syncable: true,
get canGrowSync() {
// Only the inbox can be grown in POP3.
return this.isInbox;
},
/**
* Given a list of messages, download snippets for those that don't
* already have snippets. You need to pass an options argument so we
* only download a snippet. If you don't do that, you are doing
* something wrong. downloadBodyReps is the one that is for full
* body part/message downloading. XXX rename this family of methods.
*/
downloadBodies: lazyWithConnection(/* getNew = */ false, /* cbIndex = */ 2,
/* whyLabel = */ 'downloadBodies',
function(conn, headers, options, callback) {
var latch = allback.latch();
var storage = this.storage;
for (var i = 0; i < headers.length; i++) {
if (headers[i] && headers[i].snippet == null) {
this.downloadBodyReps(headers[i], options, latch.defer(i));
}
}
latch.then(function(results) {
var err = null; // pull out the first error, if it exists
for (var k in results) {
err = results[k][0];
}
callback(err, headers.length);
});
}),
/**
* Download the full body of a message. POP3 does not distinguish
* between message bodies and attachments, so we must retrieve them
* all in one go.
*/
downloadBodyReps: lazyWithConnection(/* getNew = */ false, /* cbIndex = */ 2,
/* whyLabel = */ 'downloadBodyReps',
function(conn, header, options, callback) {
if (options instanceof Function) {
callback = options;
options = {};
}
console.log('POP3: Downloading bodyReps for UIDL ' + header.srvid);
conn.downloadMessageByUidl(header.srvid, function(err, message) {
if (err) { callback(err); return; }
// Don't overwrite the header, because it contains useful
// identifiers like `suid` and things we want. Plus, with POP3,
// the server's headers will always be the same.
// However, we do need header.bytesToDownloadForBodyDisplay:
header.bytesToDownloadForBodyDisplay =
message.header.bytesToDownloadForBodyDisplay;
console.log('POP3: Storing message ' + header.srvid +
' with ' + header.bytesToDownloadForBodyDisplay +
' bytesToDownload.');
// Force a flush if there were any attachments so that any memory-backed
// Blobs get replaced with their post-save disk-backed equivalent so they
// can be garbage collected.
var flush = message.bodyInfo.attachments.length > 0;
this.storeMessage(header, message.bodyInfo, { flush: flush }, function() {
callback && callback(null, message.bodyInfo, flush);
});
}.bind(this));
}),
downloadMessageAttachments: function(uid, partInfos, callback, progress) {
// We already retrieved the attachments in downloadBodyReps, so
// this function should never be invoked (because callers would
// have seen that all relevant partInfos have set `isDownloaded`
// to true). Either way, there's nothing to do here.
console.log('POP3: ERROR: downloadMessageAttachments called and ' +
'POP3 shouldn\'t do that.');
callback(null, null);
},
/**
* Store a message. Depending on whether or not we've seen the
* message before, we'll either add it as a new message in storage
* or update the existing one.
*
* Our current POP3 implementation does not automatically delete
* messages from the server when they've been fetched, so we need to
* track which messages we've downloaded already and which ones are
* new. Unfortunately, this means that our sync with the server will
* take progressively longer as the server accumulates more messages
* in its store.
*
* Some servers might potentially "window" messages, such that the
* oldest messages in the message list might just drop off the
* server's list. If so, this code doesn't change; new messages will
* continue to be newly stored, and old messages will still be
* known.
*
* @param {HeaderInfo} header Message header.
* @param {BodyInfo} bodyInfo Body information, reps, etc.
* @param {Object} options
* @param {Boolean} options.flush Force a flush so the message gets reloaded,
* replacing memory-backed Blobs with
* disk-backed ones?
* @param {function()} callback
*/
storeMessage: function(header, bodyInfo, options, callback) {
callback = callback || function() {};
var event = {
changeDetails: {}
};
var knownId = this.getMessageIdForUidl(header.srvid);
if (header.id == null) { // might be zero.
if (knownId == null) {
header.id = this.storage._issueNewHeaderId();
} else {
header.id = knownId;
}
header.suid = this.storage.folderId + '/' + header.id;
header.guid = header.guid || header.srvid;
}
// Save all included attachments before actually storing the
// message. Downloaded attachments must be converted from a blob
// to a file on disk.
var latch = allback.latch();
var self = this;
for (var i = 0; i < bodyInfo.attachments.length; i++) {
var att = bodyInfo.attachments[i];
if (att.file instanceof Blob) {
// We want to save attachments to device storage (sdcard),
// rather than IndexedDB, for now. It's a v3 thing to use IndexedDB
// as a cache.
console.log('Saving attachment', att.file);
// Always register all POP3 downloads with the download manager since
// the user didn't have to explicitly trigger download for each
// attachment.
var registerDownload = true;
jobmixins.saveToDeviceStorage(
self, att.file, 'sdcard', registerDownload, att.name, att,
latch.defer());
// When saveToDeviceStorage completes, att.file will
// be a reference to the file on the sdcard.
}
}
latch.then(function() {
// Once the attachments have been downloaded, we can store the
// message. Here, we wait to call back from storeMessage() until
// we've saved _both_ the header and body.
latch = allback.latch();
if (knownId == null) {
self.storeMessageUidlForMessageId(header.srvid, header.id);
self.storage.addMessageHeader(header, bodyInfo, latch.defer());
self.storage.addMessageBody(header, bodyInfo, latch.defer());
} else {
self.storage.updateMessageHeader(
header.date, header.id, true, header, bodyInfo, latch.defer());
event.changeDetails.attachments = range(bodyInfo.attachments.length);
event.changeDetails.bodyReps = range(bodyInfo.bodyReps.length);
var updateOptions = {};
if (options.flush) {
updateOptions.flushBecause = 'blobs';
}
self.storage.updateMessageBody(
header, bodyInfo, updateOptions, event, latch.defer());
}
latch.then(function() {
callback(null, bodyInfo);
});
});
},
/**
* Return the folderMeta for the INBOX, upon which we store the
* uidlMap and overflowMap. Cache it for performance, since this
* function gets invoked frequently.
*/
get inboxMeta() {
// Override this getter to provide direct access in the future.
return (this.inboxMeta = this.account.getFolderMetaForFolderId(
this.account.getFirstFolderWithType('inbox').id));
},
/**
* Retrieve the message's id (header.id) given a server's UIDL.
*
* CAUTION: Zero is a valid message ID. I made the mistake of doing
* boolean comparisons on header IDs and that is a BAD IDEA. <3
* Hence the `== null` checks in a few places in this file.
*/
getMessageIdForUidl: function(uidl) {
if (uidl == null) {
return null;
}
this.inboxMeta.uidlMap = this.inboxMeta.uidlMap || {};
return this.inboxMeta.uidlMap[uidl];
},
/**
* Store the given message UIDL so that we know it has already been
* downloaded. If the message was previously marked as overflow,
* remove it from the overflow map because we know about it now.
*/
storeMessageUidlForMessageId: function(uidl, headerId) {
this.inboxMeta.uidlMap = this.inboxMeta.uidlMap || {};
this.inboxMeta.uidlMap[uidl] = headerId;
if (this.inboxMeta.overflowMap) {
delete this.inboxMeta.overflowMap[uidl];
}
},
/**
* Mark the given message UIDL as being an "overflow message"; that
* is, it was NOT downloaded and should be made available to
* download during a "download more messages..." operation.
*
* This data is stored in INBOX's folderMeta like so:
*
* overflowMap: {
* "(message uidl)": { size: 0 },
* ...
* }
*/
storeOverflowMessageUidl: function(uidl, size) {
this.inboxMeta.overflowMap = this.inboxMeta.overflowMap || {};
this.inboxMeta.overflowMap[uidl] = { size: size };
},
/**
* Return true if there are overflow messages. (If so, we're NOT
* synced to the dawn of time.)
*/
hasOverflowMessages: function() {
if (!this.inboxMeta.overflowMap) { return false; }
for (var key in this.inboxMeta.overflowMap) {
return true; // if there's even a single key, we have some!
}
return false;
},
/**
* Return whether or not the given UIDL is in the overflow map.
*/
isUidlInOverflowMap: function(uidl) {
if (!this.inboxMeta.overflowMap) { return false; }
return !!this.inboxMeta.overflowMap[uidl];
},
/**
* Sync the inbox for the first time. Since we set `ignoreHeaders`
* to true, we'll notify mail slices to update after the entire sync
* completes, so that all messages show up at once rather than one
* at a time.
*/
initialSync: function(slice, initialDays, syncCb, doneCb, progressCb) {
syncCb('sync', true /* ignoreHeaders */);
this.sync('initial', slice, doneCb, progressCb);
},
/**
* Sync the inbox for a refresh. This is the same as initialSync for
* POP3, except that we notify slices immediately upon receiving
* each new message individually.
*/
refreshSync: function(
slice, dir, startTS, endTS, origStartTS, doneCb, progressCb) {
this.sync('refresh', slice, doneCb, progressCb);
},
/**
* The unit tests issue "delete on server but not locally" commands.
* In order to mimic operations where we modify non-INBOX folders on
* the server and expect to learn about them from the client on
* sync, we queue up "server-only" modifications and execute them
* upon sync. This allows us to reuse much of the existing tests for
* certain folder operations, and becomes a no-op in production.
*
* @return {Boolean} true if a save is needed because we're actually doing
* something.
*/
_performTestAdditionsAndDeletions: function(cb) {
var meta = this.storage.folderMeta;
var numAdds = 0;
var latch = allback.latch();
var saveNeeded = false;
if (meta._TEST_pendingHeaderDeletes) {
meta._TEST_pendingHeaderDeletes.forEach(function(namer) {
saveNeeded = true;
this.storage.deleteMessageHeaderAndBody(namer.suid, namer.date,
latch.defer());
}, this);
meta._TEST_pendingHeaderDeletes = null;
}
if (meta._TEST_pendingAdds) {
meta._TEST_pendingAdds.forEach(function(msg) {
saveNeeded = true;
this.storeMessage(msg.header, msg.bodyInfo, {}, latch.defer());
}, this);
meta._TEST_pendingAdds = null;
}
latch.then(function(results) { cb(); });
return saveNeeded;
},
/**
* If we have overflow messages, fetch them here.
*/
growSync: function(slice, growthDirection, anchorTS, syncStepDays,
doneCallback, progressCallback) {
if (growthDirection !== PASTWARDS || !this.hasOverflowMessages()) {
return false;
}
// For simplicity, we ignore anchorTS and syncStepDays, because
// POP3's limitations make it difficult to infer anything about
// the messages we're going to download now. All we can do here is
// download another batch of overflow messages.
this.sync('grow', slice, doneCallback, progressCallback);
return true;
},
allConsumersDead: function() {
// Nothing to do here.
},
shutdown: function() {
// Nothing to do here either.
},
/**
* Pull down new headers from the server, attempting to fetch
* snippets for the messages.
*
* Pop3Client (in pop3.js) contains the variables used to determine
* how much of each message to fetch. Since POP3 only lets us
* download a certain number of _lines_ from the message, Pop3Client
* selects an appropriate snippet size (say, 4KB) and attempts to
* fetch approximately that much data for each message. That value
* is/should be high enough that we get snippets for nearly all
* messages, unless a message is particularly strange.
*
* Additionally, we don't delete messages from the server. This
* means that when we attempt to list messages, we'll see new
* messages along with messages we've seen before. To ensure we only
* retrieve messages we don't know about, we keep track of message
* unique IDs (UIDLs) and only download new messages.
*
* OVERFLOW MESSAGE HANDLING:
*
* We don't want to overwhelm a sync with a ridiculous number of
* messages if the spool has a lot of new messagse. Instead of
* blindly downloading all headers right away, we store excess
* "overflow" messages for future "grow" syncs, e.g. when the user
* clicks "Get More Messages" in the message list.
*
* This works as follows:
*
* If we're syncing normally, mark any excess messages as overflow
* messages and don't download them. This is handled largely by
* Pop3Client by the maxMessages option to listMessages(). We ignore
* any messages already marked as overflow for the purposes of the
* sync filter.
*
* If this is a grow sync, i.e. we want to download some overflow
* messages, we set the download filter to _only_ include overflow
* UIDLs. We may still have _more_ overflow messages, but that's
* okay, because they'll just be stored in the overflowMap like
* normal, for a future "grow" sync. Any messages we do download are
* marked as stored and removed from the overflowMap (in
* `this.storeMessageUidlForMessageId`).
*/
sync: lazyWithConnection(/* getNew = */ true, /* cbIndex = */ 2,
/* whyLabel = */ 'sync',
function(conn, syncType, slice, realDoneCallback, progressCallback) {
// if we could not establish a connection, abort the sync.
var self = this;
logic(self, 'sync:begin', { syncType: syncType });
// Avoid invoking realDoneCallback multiple times. Cleanup when we switch
// sync to promises/tasks.
var doneFired = false;
var doneCallback = function(err) {
if (doneFired) {
logic(self, 'sync:duplicateDone', { syncType: syncType, err: err });
return;
}
logic(self, 'sync:end', { syncType: syncType, err: err });
doneFired = true;
// coerce the rich error object to a string error code; currently
// refreshSlice only likes 'unknown' and 'aborted' so just run with
// unknown.
realDoneCallback(err ? 'unknown' : null);
};
// Only fetch info for messages we don't already know about.
var filterFunc;
if (syncType !== 'grow') {
// In a regular sync, download any message that we don't know
// about that isn't in the overflow map.
filterFunc = function(uidl) {
return self.getMessageIdForUidl(uidl) == null && // might be 0
!self.isUidlInOverflowMap(uidl);
};
} else /* (syncType === 'grow') */ {
// In a 'grow' sync, ONLY download overflow messages.
filterFunc = this.isUidlInOverflowMap.bind(this);
}
var bytesStored = 0;
var numMessagesSynced = 0;
var latch = allback.latch();
// We only want to trigger a save if work is actually being done. This is
// ugly/complicated because in order to let POP3 use the existing IMAP tests
// that did things in other folders, a test-only bypass route was created
// that has us actually add the messages
var saveNeeded;
if (!this.isInbox) {
slice.desiredHeaders = (this._TEST_pendingAdds &&
this._TEST_pendingAdds.length);
saveNeeded = this._performTestAdditionsAndDeletions(latch.defer());
} else {
saveNeeded = true;
logic(this, 'sync_begin');
var fetchDoneCb = latch.defer();
var closeExpected = false;
// register for a close notification so if disaster recovery closes the
// connection we still get a chance to report the error without breaking
// sync. This is the lowest priority onclose handler so all the other
// more specific error handlers will get a chance to fire. However, some
// like to defer to future turns of the event loop so we we use setTimeout
// to defer through at least two turns of the event loop.
conn.onclose = function() {
if (closeExpected) {
return;
}
closeExpected = true;
// see the above. This is horrible but we hate POP3 and these error
// handling cases are edge-casey and this actually does improve our test
// coverage. (test_pop3_dead_connection.js's first two test clauses
// were written before I added this onclose handler.)
window.setTimeout(function() {
window.setTimeout(function() {
doneCallback('closed');
}, 0);
}, 0);
};
// Fetch messages, ensuring that we don't actually store them all in
// memory so as not to burden memory unnecessarily.
conn.listMessages({
filter: filterFunc,
checkpointInterval: sync.POP3_SAVE_STATE_EVERY_N_MESSAGES,
maxMessages: sync.POP3_MAX_MESSAGES_PER_SYNC,
checkpoint: function(next) {
// Every N messages, wait for everything to be stored to
// disk and saved in the database. Then proceed.
this.account.__checkpointSyncCompleted(next, 'syncBatch');
}.bind(this),
progress: function fetchProgress(evt) {
// Store each message as it is retrieved.
var totalBytes = evt.totalBytes;
var message = evt.message;
var messageCb = latch.defer();
this.storeMessage(message.header, message.bodyInfo, {}, function() {
bytesStored += evt.size;
numMessagesSynced++;
progressCallback(0.1 + 0.7 * bytesStored / totalBytes);
messageCb();
});
}.bind(this),
}, function fetchDone(err, numSynced, overflowMessages) {
// Upon downloading all of the messages, we MUST issue a QUIT
// command. This will tear down the connection, however if we
// don't, we will never receive notifications of new messages.
// If we deleted any messages on the server (which we don't),
// the QUIT command is what would actually cause those to be
// persisted. In the future, when we support server-side
// deletion, we should ensure that this QUIT does not
// inadvertently commit unintended deletions.
closeExpected = true;
conn.quit();
if (err) {
doneCallback(err);
return;
}
// If there were excess messages, mark them for later download.
if (overflowMessages.length) {
overflowMessages.forEach(function(message) {
this.storeOverflowMessageUidl(message.uidl, message.size);
}, this);
logic(this, 'overflowMessages', { count: overflowMessages.length });
}
// When all of the messages have been persisted to disk, indicate
// that we've successfully synced. Refresh our view of the world.
fetchDoneCb();
}.bind(this));
}
latch.then((function onSyncDone() {
// Because POP3 has no concept of syncing discrete time ranges,
// we have to trick the storage into marking everything synced
// _except_ the dawn of time. This has to be slightly later than
// a value that would be interpreted as the dawn of time -- in
// this case, it has to be one day plus one. Ideally, this
// should be abstracted a little better; it's mostly IMAP that
// needs more involved logic.
this.storage.markSyncRange(
sync.OLDEST_SYNC_DATE + date.DAY_MILLIS + 1,
date.NOW(), 'XXX', date.NOW());
if (!this.hasOverflowMessages()) {
this.storage.markSyncedToDawnOfTime();
}
if (this.isInbox) {
logic(this, 'sync_end');
}
// Don't notify completion until the save completes, if relevant.
if (saveNeeded) {
this.account.__checkpointSyncCompleted(doDoneStuff, 'syncComplete');
} else {
doDoneStuff();
}
}).bind(this));
var doDoneStuff = function() {
if (syncType === 'initial') {
// If it's the first time we've synced, we've set
// ignoreHeaders to true, which means that slices don't know
// about new messages. We'll reset ignoreHeaders to false
// here, and then instruct the database to load messages
// again.
//
// We're waiting for the database to settle. Since POP3
// doesn't guarantee message ordering (in terms of listing
// messages in your maildrop), if we just blindly updated the
// current slice, the UI might frantically update as new
// messages come in. So for the initial sync, just batch them
// all in.
this.storage._curSyncSlice.ignoreHeaders = false;
this.storage._curSyncSlice.waitingOnData = 'db';
this.storage.getMessagesInImapDateRange(
sync.OLDEST_SYNC_DATE, null,
sync.INITIAL_FILL_SIZE, sync.INITIAL_FILL_SIZE,
// Don't trigger a refresh; we just synced. Accordingly,
// releaseMutex can be null.
this.storage.onFetchDBHeaders.bind(
this.storage, this.storage._curSyncSlice,
false, doneCallback, null));
} else {
doneCallback(null);
}
}.bind(this);
}),
};
/** Return an array with the integers [0, end). */
function range(end) {
var ret = [];
for (var i = 0; i < end; i++) {
ret.push(i);
}
return ret;
}
}); // end define

View file

@ -0,0 +1,362 @@
define(['mimefuncs', 'exports'], function(mimefuncs, exports) {
/**
* This file contains the following classes:
*
* - Pop3Parser: Parses incoming POP3 requests
* - Pop3Protocol: Uses the Pop3Parser to match requests up with responses
* - Request: Encapsulates a request to the server
* - Response: Encapsulates a response from the server
*
* The Pop3Client (in pop3.js) hooks together a socket and an
* instance of Pop3Protocol to form a complete client. See pop3.js
* for a more detailed description of the hierarchy.
*/
var setTimeout = window.setTimeout.bind(window);
var clearTimeout = window.clearTimeout.bind(window);
var MAX_LINE_LENGTH = 512; // per POP3 spec, including CRLF
var CR = '\r'.charCodeAt(0);
var LF = '\n'.charCodeAt(0);
var PERIOD = '.'.charCodeAt(0);
var PLUS = '+'.charCodeAt(0);
var MINUS = '-'.charCodeAt(0);
var SPACE = ' '.charCodeAt(0);
var textEncoder = new TextEncoder('utf-8', { fatal: false });
function concatBuffers(a, b) {
var buffer = new Uint8Array(a.length + b.length);
buffer.set(a, 0);
buffer.set(b, a.length);
return buffer;
}
/**
* Pop3Parser receives binary data (presumably from a socket) and
* parse it according to the POP3 spec:
*
* var parser = new Pop3Parser();
* parser.push(myBinaryData);
* var rsp = parser.extractResponse(false);
* if (rsp) {
* // do something with the response
* }
*/
function Pop3Parser() {
this.buffer = new Uint8Array(0); // data not yet parsed into lines
this.unprocessedLines = [];
}
/**
* Add new data to be parsed. To actually parse the incoming data
* (to see if there is enough data to extract a full response), call
* `.extractResponse()`.
*
* @param {Uint8Array} data
*/
Pop3Parser.prototype.push = function(data) {
// append the data to be processed
var buffer = this.buffer = concatBuffers(this.buffer, data);
// pull out full lines
for (var i = 0; i < buffer.length - 1; i++) {
if (buffer[i] === CR && buffer[i + 1] === LF) {
var end = i + 1;
if (end > MAX_LINE_LENGTH) {
// Sadly, servers do this, so we can't bail here.
}
this.unprocessedLines.push(buffer.subarray(0, end + 1));
buffer = this.buffer = buffer.subarray(end + 1);
i = -1;
}
}
}
/**
* Attempt to parse and return a single message from the buffered
* data. Since the POP3 protocol does not provide a foolproof way to
* determine whether a given message is multiline without tracking
* request state, you must specify whether or not the response is
* expected to be multiline.
*
* Multiple responses may be available; you should call
* `.extractResponse()` repeatedly until no more responses are
* available. This method returns null if there was not enough data
* to parse and return a response.
*
* @param {boolean} multiline true to parse a multiline response.
* @return {Response|null}
*/
Pop3Parser.prototype.extractResponse = function(multiline) {
if (!this.unprocessedLines.length) {
return null;
}
if (this.unprocessedLines[0][0] !== PLUS) {
multiline = false; // Negative responses are never multiline.
}
if (!multiline) {
return new Response([this.unprocessedLines.shift()], false);
} else {
var endLineIndex = -1;
for (var i = 1; i < this.unprocessedLines.length; i++) {
var line = this.unprocessedLines[i];
if (line.byteLength === 3 &&
line[0] === PERIOD && line[1] === CR && line[2] === LF) {
endLineIndex = i;
break;
}
}
if (endLineIndex === -1) {
return null;
}
var lines = this.unprocessedLines.splice(0, endLineIndex + 1);
lines.pop(); // remove final ".\r\n" line
// the first line cannot be stuffed (it's the command OK/ERR
// response). Other lines may be period-stuffed.
for (var i = 1; i < endLineIndex; i++) {
if (lines[i][0] === PERIOD) {
lines[i] = lines[i].subarray(1);
}
}
return new Response(lines, true);
}
}
/**
* Represent a POP3 response (both success and failure). You should
* not have to instantiate this class directly; Pop3Parser returns
* these objects from `Pop3Parser.extractResponse()`.
*
* @param {UInt8Array[]} lines
* @param {boolean} isMultiline
*/
function Response(lines, isMultiline) {
this.lines = lines; // list of UInt8Arrays
this.isMultiline = isMultiline;
this.ok = (this.lines[0][0] === PLUS);
this.err = !this.ok;
this.request = null;
}
/**
* Return the description text for the status line as a string.
*/
Response.prototype.getStatusLine = function() {
return this.getLineAsString(0).replace(/^(\+OK|-ERR) /, '');
}
/**
* Return the line at `index` as a string.
*
* @param {int} index
* @return {String}
*/
Response.prototype.getLineAsString = function(index) {
return mimefuncs.fromTypedArray(this.lines[index]);
}
/**
* Return an array of strings, one for each line, including CRLFs.
* If you want to parse the data from a response, use
* `.getDataLines()`.
*
* @return {String[]}
*/
Response.prototype.getLinesAsString = function() {
var lines = [];
for (var i = 0; i < this.lines.length; i++) {
lines.push(this.getLineAsString(i));
}
return lines;
}
/**
* Return an array of strings, _excluding_ CRLFs, starting from the
* line after the +OK/-ERR line.
*/
Response.prototype.getDataLines = function() {
var lines = [];
for (var i = 1; i < this.lines.length; i++) {
var line = this.getLineAsString(i);
lines.push(line.slice(0, line.length - 2)); // strip CRLF
}
return lines;
}
/**
* Return the data portion of a multiline response as a string,
* with the lines' CRLFs intact.
*/
Response.prototype.getDataAsString = function() {
var lines = [];
for (var i = 1; i < this.lines.length; i++) {
lines.push(this.getLineAsString(i));
}
return lines.join(''); // lines already have '\r\n'
}
/**
* Return a string representation of the message, primarily for
* debugging purposes.
*/
Response.prototype.toString = function() {
return this.getLinesAsString().join('\r\n');
}
/**
* Represent a POP3 request, with enough data to allow the parser
* to parse out a response and invoke a callback upon receiving a
* response.
*
* @param {string} command The command, like RETR, USER, etc.
* @param {string[]} args Arguments to the command, as an array.
* @param {boolean} expectMultiline Whether or not the response will
* be multiline.
* @param {function(err, rsp)} cb The callback to invoke when a
* response is received.
*/
function Request(command, args, expectMultiline, cb) {
this.command = command;
this.args = args;
this.expectMultiline = expectMultiline;
this.onresponse = cb || null;
}
exports.Request = Request;
/**
* Encode the request into a byte array suitable for transport over
* a socket.
*/
Request.prototype.toByteArray = function() {
return textEncoder.encode(
this.command + (this.args.length ? ' ' + this.args.join(' ') : '') + '\r\n');
}
/**
* Trigger the response callback with '-ERR desc\r\n'.
*/
Request.prototype._respondWithError = function(desc) {
var rsp = new Response([textEncoder.encode(
'-ERR ' + desc + '\r\n')], false);
rsp.request = this;
this.onresponse(rsp, null);
}
/**
* Couple a POP3 parser with a request/response model, such that
* you can easily hook Pop3Protocol up to a socket (or other
* transport) to get proper request/response semantics.
*
* You must attach a handler to `.onsend`, which should fire data
* across the wire. Similarly, you should call `.onreceive(data)` to
* pass data back in from the socket.
*/
function Pop3Protocol() {
this.parser = new Pop3Parser();
this.onsend = function(data) {
throw new Error("You must implement Pop3Protocol.onsend to send data.");
};
this.unsentRequests = []; // if not pipelining, queue requests one at a time
this.pipeline = false;
this.pendingRequests = [];
this.closed = false;
}
exports.Response = Response;
exports.Pop3Protocol = Pop3Protocol;
/**
* Send a request to the server. Upon receiving a response, the
* callback will be invoked, node-style, with an err or a response.
* Negative replies (-ERR) are returned as an error to the callback;
* positive replies (+OK) as a response. Socket errors are returned
* as an error to the callback.
*
* @param {string} cmd The command like USER, RETR, etc.
* @param {string[]} args An array of arguments to the command.
* @param {boolean} expectMultiline Whether or not the response will
* be multiline.
* @param {function(err, rsp)} cb The callback to invoke upon
* receipt of a response.
*/
Pop3Protocol.prototype.sendRequest = function(
cmd, args, expectMultiline, cb) {
var req;
if (cmd instanceof Request) {
req = cmd;
} else {
req = new Request(cmd, args, expectMultiline, cb);
}
if (this.closed) {
req._respondWithError('(request sent after connection closed)');
return;
}
if (this.pipeline || this.pendingRequests.length === 0) {
this.onsend(req.toByteArray());
this.pendingRequests.push(req);
} else {
this.unsentRequests.push(req);
}
}
/**
* Call this function to send received data to the parser. This
* method automatically calls the appropriate response callback for
* its respective request.
*/
Pop3Protocol.prototype.onreceive = function(evt) {
this.parser.push(new Uint8Array(evt.data));
var response;
while (true) {
var req = this.pendingRequests[0];
response = this.parser.extractResponse(req && req.expectMultiline);
if (!response) {
break;
} else if (!req) {
// It's unclear how to handle this in the most nondestructive way;
// if we receive an unsolicited response, something has gone horribly
// wrong, and it's unlikely that we'll be able to recover.
console.error('Unsolicited response from server: ' + response);
break;
}
response.request = req;
this.pendingRequests.shift();
if (this.unsentRequests.length) {
this.sendRequest(this.unsentRequests.shift());
}
if (req.onresponse) {
if (response.err) {
req.onresponse(response, null);
} else {
req.onresponse(null, response);
}
}
}
}
/**
* Call this function when the socket attached to this protocol is
* closed. Any current requests that have been enqueued but not yet
* responded to will be sent a dummy "-ERR" response, indicating
* that the underlying connection closed without actually
* responding. This avoids the case where we hang if we never
* receive a response from the server.
*/
Pop3Protocol.prototype.onclose = function() {
this.closed = true;
var requestsToRespond = this.pendingRequests.concat(this.unsentRequests);
this.pendingRequests = [];
this.unsentRequests = [];
for (var i = 0; i < requestsToRespond.length; i++) {
var req = requestsToRespond[i];
req._respondWithError('(connection closed, no response)');
}
}
});