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