fetch() rewrite

fork
mscdex 12 years ago
parent 9e4223019f
commit 2ae7371f22

@ -4,7 +4,8 @@ var assert = require('assert'),
Socket = require('net').Socket,
EventEmitter = require('events').EventEmitter,
utf7 = require('utf7').imap,
MIMEParser = require('./mimeparser'),
// customized copy of XRegExp to deal with multiple variables of the same
// name
XRegExp = require('./xregexp').XRegExp;
var parsers = require('./imap.parsers'),
@ -19,9 +20,10 @@ var CRLF = '\r\n',
BOXSELECTING: 3,
BOXSELECTED: 4
},
RE_LITHEADER = /(?:((?:BODY\[.*\])?|[^ ]+) )?\{(\d+)\}$/i,
RE_LITHEADER = /(?:((?:BODY\[.*\](?:<\d+>)?)?|[^ ]+) )?\{(\d+)\}$/i,
RE_UNRESP = /^\* (OK|PREAUTH|NO|BAD) (?:\[(.+)\] )?(.+)$/i,
RE_CMD = /^([^ ]+)(?: |$)/,
RE_ISHEADER = /HEADER/,
REX_UNRESPDATA = XRegExp('^\\* (?:(?:(?<type>NAMESPACE) (?<personal>(?:NIL|\\((?:\\(.+\\))+\\))) (?<other>(?:NIL|\\((?:\\(.+\\))+\\))) (?<shared>(?:NIL|\\((?:\\(.+\\))+\\))))|(?:(?<type>FLAGS) \\((?<flags>.*)\\))|(?:(?<type>LIST|LSUB|XLIST) \\((?<flags>.*)\\) (?<delimiter>".+"|NIL) (?<mailbox>.+))|(?:(?<type>(SEARCH|SORT))(?: (?<results>.+))?)|(?:(?<type>STATUS) (?<mailbox>.+) \\((?<attributes>.*)\\))|(?:(?<type>CAPABILITY) (?<capabilities>.+))|(?:(?<type>BYE) (?:\\[(?<code>.+)\\] )?(?<message>.+)))$', 'i'),
REX_UNRESPNUM = XRegExp('^\\* (?<num>\\d+) (?:(?<type>EXISTS)|(?<type>RECENT)|(?<type>EXPUNGE)|(?:(?<type>FETCH) \\((?<info>.*)\\)))$', 'i');
@ -31,18 +33,18 @@ var IDLE_NONE = 1,
IDLE_READY = 3,
IDLE_DONE = 4;
function ImapConnection (options) {
function ImapConnection(options) {
if (!(this instanceof ImapConnection))
return new ImapConnection(options);
EventEmitter.call(this);
this._options = {
username: '',
password: '',
host: 'localhost',
port: 143,
secure: false,
connTimeout: 10000, // connection timeout in msecs
username: options.username || options.user || '',
password: options.password || '',
host: options.host || 'localhost',
port: options.port || 143,
secure: options.secure || false,
connTimeout: options.connTimeout || 10000, // connection timeout in msecs
debug: false
};
@ -87,10 +89,9 @@ function ImapConnection (options) {
}
}
};
this._options = utils.extend(true, this._options, options);
if (typeof this._options.debug === 'function')
this.debug = this._options.debug;
if (typeof options.debug === 'function')
this.debug = options.debug;
else
this.debug = false;
@ -102,6 +103,7 @@ function ImapConnection (options) {
}
inherits(ImapConnection, EventEmitter);
module.exports = ImapConnection;
exports.ImapConnection = ImapConnection;
ImapConnection.prototype.connect = function(loginCb) {
@ -207,19 +209,13 @@ ImapConnection.prototype.connect = function(loginCb) {
if (b.length === 0 || b.p >= b.length) return;
self.debug&&self.debug('\n<== ' + inspect(b.toString('binary', b.p)) + '\n');
var r, m, litType, i, len, msg;
var r, m, litType, i, len, msg, fetches;
if (indata.expect > 0) {
r = read(b);
if (indata.streaming) {
if (requests[0]._useParser)
state.parser.execute(r);
else
requests[0]._msg.emit('data', r);
if (indata.expect === 0) {
requests[0].msg.emit('data', r);
if (indata.expect === 0)
indata.streaming = false;
if (requests[0]._useParser)
state.parser.finish();
}
} else {
if (indata.temp)
indata.temp += r.toString('binary');
@ -244,36 +240,20 @@ ImapConnection.prototype.connect = function(loginCb) {
indata.line = r;
if (m)
litType = m[1];
//assert((litType = m[1]) !== undefined);
indata.expect = (m ? parseInt(m[2], 10) : -1);
if (indata.expect > -1) {
if ((m = /\* (\d+) FETCH/i.exec(indata.line))
&& /^BODY\[/i.test(litType)) {
msg = new ImapMessage();
msg.seqno = parseInt(m[1], 10);
requests[0]._msg = msg;
requests[0]._fetcher.emit('message', msg);
indata.streaming = true;
requests[0].msg = msg;
requests[0].key = litType;
var fetches = requests[0].fetchers[litType];
for (var f = 0, lenf = fetches.length; f < lenf; ++f)
fetches[f].emit('message', msg);
indata.streaming = (!RE_ISHEADER.test(litType));
if (indata.streaming)
indata.literals.push(indata.expect);
if (requests[0]._useParser) {
requests[0]._msg.headers = {};
if (!state.parser) {
state.parser = new MIMEParser();
state.parser.on('header', function(name, val) {
name = name.toLowerCase();
if (requests[0]._headers
&& requests[0]._headers.indexOf(name) === -1)
return;
if (requests[0]._msg.headers[name] !== undefined)
requests[0]._msg.headers[name].push(val);
else
requests[0]._msg.headers[name] = [val];
});
state.parser.on('data', function(str) {
requests[0]._msg.emit('data', str);
});
}
}
} else if (indata.expect === 0)
indata.literals.push('');
// start reading of the literal or get the rest of the response
@ -292,13 +272,14 @@ ImapConnection.prototype.connect = function(loginCb) {
switch (m.type) {
case 'FETCH':
// m.info = message details
msg = (requests[0] && requests[0]._msg
? requests[0]._msg
var data, parsed, headers, f, lenf;
msg = (requests[0] && requests[0].msg
? requests[0].msg
: new ImapMessage());
parsers.parseFetch(m.info, indata.literals, msg);
if (typeof msg.body === 'number') {
// we streamed a body
// we streamed a body, e.g. {3}\r\nfoo
delete msg.body;
msg.emit('end');
} else {
@ -308,21 +289,44 @@ ImapConnection.prototype.connect = function(loginCb) {
self.emit('msgupdate', msg);
else {
if (typeof msg.body === 'string') {
// a body was given as a non-literal string
// a body was given as a non-literal string, e.g. "foo"
fetches = requests[0].fetchers[requests[0].key];
if (RE_ISHEADER.test(requests[0].key)) {
var parsed, data, headers;
for (f = 0, lenf = fetches.length; f < lenf; ++f) {
if (fetches[f]._parse) {
if (parsed === undefined)
parsed = parsers.parseHeaders(msg.body);
headers = parsed;
} else {
if (data === undefined)
data = new Buffer(msg.body, 'binary');
headers = data;
}
delete msg.body;
msg.emit('headers', headers);
msg.emit('end');
}
} else {
var data = new Buffer(msg.body, 'binary');
delete msg.body;
requests[0]._fetcher.emit('message', msg);
for (f = 0, lenf = fetches.length; f < lenf; ++f) {
msg.emit('data', data);
msg.emit('end');
}
}
} else {
// non-body fetch
if (Object.keys(msg).indexOf('body') > -1)
if ('body' in msg)
delete msg.body;
requests[0]._fetcher.emit('message', msg);
fetches = requests[0].fetchers[''];
for (f = 0, lenf = fetches.length; f < lenf; ++f) {
fetches[f].emit('message', msg);
msg.emit('end');
}
}
}
}
break;
case 'EXISTS':
// mailbox total message count
@ -777,27 +781,33 @@ ImapConnection.prototype.append = function(data, options, cb) {
options = {};
}
options = options || {};
if (!('mailbox' in options)) {
if (!options.mailbox) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox specified or currently selected');
else
options.mailbox = this._state.box.name;
}
var cmd = 'APPEND "' + utils.escape(options.mailbox) + '"';
if ('flags' in options) {
if (options.flags) {
if (!Array.isArray(options.flags))
options.flags = [options.flags];
cmd += " (\\" + options.flags.join(' \\') + ")";
}
if ('date' in options) {
if (options.date) {
if (!(options.date instanceof Date))
throw new Error('Expected null or Date object for date');
cmd += ' "' + options.date.getDate() + '-'
+ utils.MONTHS[options.date.getMonth()]
+ '-' + options.date.getFullYear();
cmd += ' ' + ('0' + options.date.getHours()).slice(-2) + ':'
+ ('0' + options.date.getMinutes()).slice(-2) + ':'
+ ('0' + options.date.getSeconds()).slice(-2);
cmd += ' "';
cmd += options.date.getDate();
cmd += '-';
cmd += utils.MONTHS[options.date.getMonth()];
cmd += '-';
cmd += options.date.getFullYear();
cmd += ' ';
cmd += ('0' + options.date.getHours()).slice(-2);
cmd += ':';
cmd += ('0' + options.date.getMinutes()).slice(-2);
cmd += ':';
cmd += ('0' + options.date.getSeconds()).slice(-2);
cmd += ((options.date.getTimezoneOffset() > 0) ? ' -' : ' +' );
cmd += ('0' + (-options.date.getTimezoneOffset() / 60)).slice(-2);
cmd += ('0' + (-options.date.getTimezoneOffset() % 60)).slice(-2);
@ -873,14 +883,11 @@ ImapConnection.prototype._sort = function(which, sorts, options, cb) {
+ utils.buildSearchQuery(options, this.capabilities), cb);
};
ImapConnection.prototype.fetch = function(uids, options) {
return this._fetch('UID ', uids, options);
ImapConnection.prototype.fetch = function(uids, options, what, cb) {
return this._fetch('UID ', uids, options, what);
};
ImapConnection.prototype._fetch = function(which, uids, options) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox is currently selected');
ImapConnection.prototype._fetch = function(which, uids, options, what, cb) {
if (uids === undefined || uids === null
|| (Array.isArray(uids) && uids.length === 0))
throw new Error('Nothing to fetch');
@ -889,60 +896,168 @@ ImapConnection.prototype._fetch = function(which, uids, options) {
uids = [uids];
utils.validateUIDList(uids);
var opts = {
markSeen: false,
request: {
struct: true,
headers: true,
body: false
}
}, toFetch, bodyRange, extensions, useParser, onlyHeaders, self = this;
var toFetch = '', prefix, extensions, self = this,
parse = true, headers, key, stream,
opts = { markSeen: false, size: false },
fetchers = {}, part;
if (typeof options !== 'object')
options = {};
utils.extend(true, opts, options);
if (Array.isArray(opts.request.body)) {
var rangeInfo;
if (opts.request.body.length !== 2)
throw new Error("Expected Array of length 2 for body byte range");
else if (typeof opts.request.body[1] !== 'string'
|| !(rangeInfo = /^([\d]+)\-([\d]+)$/.exec(opts.request.body[1]))
|| parseInt(rangeInfo[1], 10) >= parseInt(rangeInfo[2], 10))
throw new Error("Invalid body byte range format");
bodyRange = '<' + parseInt(rangeInfo[1], 10) + '.'
+ parseInt(rangeInfo[2], 10) + '>';
opts.request.body = opts.request.body[0];
}
if (opts.request.headers !== false
&& typeof opts.request.body === 'boolean') {
if (Array.isArray(opts.request.headers))
onlyHeaders = opts.request.headers.join(' ').toUpperCase();
if (opts.request.body === true) {
// fetches the whole entire message (including some/all headers)
toFetch = '';
} else if (onlyHeaders) {
// fetch specific headers only
toFetch = 'HEADER.FIELDS (' + onlyHeaders + ')';
if (typeof what === 'function') {
cb = what;
what = options;
} else {
// fetches (all) headers only
toFetch = 'HEADER';
}
useParser = true;
} else if (opts.request.body === true) {
// fetches the whole entire message text (minus the headers), including
// all message parts
toFetch = 'TEXT';
} else if (typeof opts.request.body === 'string') {
if (opts.request.body.toUpperCase() === 'FULL') {
// fetches the whole entire message (including the headers)
// NOTE: does NOT parse the headers!
toFetch = '';
} else if (/^([\d]+[\.]{0,1})*[\d]+$/.test(opts.request.body)) {
// specific message part identifier, e.g. '1', '2', '1.1', '1.2', etc
toFetch = opts.request.body;
if (options.markSeen)
opts.markSeen = true;
if (options.size)
opts.size = true;
}
prefix = (opts.markSeen ? ' BODY.PEEK[' : ' BODY[');
if (!Array.isArray(what))
what = [what];
for (var i = 0, wp, pprefix, len = what.length; i < len; ++i) {
wp = what[i];
if (wp.id !== undefined && !/^(?:[\d]+[\.]{0,1})*[\d]+$/.test(''+wp.id))
throw new Error('Invalid part id: ' + wp.id);
if (( (wp.headers
&& (!wp.headers.fields
|| (Array.isArray(wp.headers.fields)
&& wp.headers.fields.length === 0)
)
&& wp.headers.parse === false
)
||
(wp.headersNot
&& (!wp.headersNot.fields
|| (Array.isArray(wp.headersNot.fields)
&& wp.headersNot.fields.length === 0)
)
&& wp.headersNot.parse === false
)
)
&& wp.body === true) {
key = prefix.trim();
if (wp.id !== undefined)
key += wp.id;
key += ']';
if (!fetchers[key]) {
fetchers[key] = [new ImapFetch()];
toFetch += ' ';
toFetch += key;
}
if (typeof wp.cb === 'function')
wp.cb(fetchers[key][0]);
key = undefined;
} else if (wp.headers || wp.headersNot || wp.body) {
pprefix = prefix;
if (wp.id !== undefined) {
pprefix += wp.id;
pprefix += '.';
}
if (wp.headers) {
key = pprefix.trim();
if (wp.headers === true)
key += 'HEADER]';
else {
if (Array.isArray(wp.headers))
headers = wp.headers;
else if (typeof wp.headers === 'object') {
if (!Array.isArray(wp.headers.fields)
&& typeof wp.headers.fields !== 'string')
throw new Error('Invalid `fields` property');
if (Array.isArray(wp.headers.fields))
headers = wp.headers.fields;
else
headers = [wp.headers.fields];
if (wp.headers.parse === false)
parse = false;
} else
throw new Error('Invalid `headers` value: ' + wp.headers);
key += 'HEADER.FIELDS (';
key += headers.join(' ').toUpperCase();
key += ')]';
}
} else if (wp.headersNot) {
key = pprefix.trim();
if (wp.headersNot === true)
key += 'HEADER]';
else {
if (Array.isArray(wp.headersNot))
headers = wp.headersNot;
else if (typeof wp.headersNot === 'object') {
if (!Array.isArray(wp.headersNot.fields)
&& typeof wp.headersNot.fields !== 'string')
throw new Error('Invalid `fields` property');
if (Array.isArray(wp.headersNot.fields))
headers = wp.headersNot.fields;
else
headers = [wp.headersNot.fields];
if (wp.headersNot.parse === false)
parse = false;
} else
throw new Error("Invalid body partID format");
throw new Error('Invalid `headersNot` value: ' + wp.headersNot);
key += 'HEADER.FIELDS.NOT (';
key += headers.join(' ').toUpperCase();
key += ')]';
}
}
if (key) {
stream = new ImapFetch();
if (parse)
stream._parse = true;
if (!fetchers[key]) {
fetchers[key] = [stream];
toFetch += ' ';
toFetch += key;
} else
fetchers.push(stream);
if (typeof wp.cb === 'function')
wp.cb(stream);
key = undefined;
}
if (wp.body) {
key = pprefix;
if (wp.body === true) {
key += 'TEXT]';
part = key;
} else if (typeof wp.body.start === 'number'
&& wp.body.length === 'number') {
if (wp.body.start < 0)
throw new Error('Invalid `start` value: ' + wp.body.start);
else if (wp.body.length <= 0)
throw new Error('Invalid `length` value: ' + wp.body.length);
key += 'TEXT]<';
key += wp.body.start;
part = key;
part += '.';
part += wp.body.length;
key += '>';
part += '>';
} else
throw new Error('Invalid `body` value: ' + wp.body);
key = key.trim();
if (!stream)
stream = new ImapFetch();
if (!fetchers[key]) {
fetchers[key] = [stream];
toFetch += part;
} else
fetchers[key].push(stream);
if (!wp.headers && !wp.headersNot && typeof wp.cb === 'function')
wp.cb(stream);
stream = undefined;
part = undefined;
key = undefined;
}
} else {
// non-body fetches
stream = new ImapFetch();
if (fetchers[''])
fetchers[''].push(stream);
else
fetchers[''] = [stream];
if (typeof wp.cd === 'function')
wp.cb(stream);
}
}
// always fetch GMail-specific bits of information when on GMail
@ -956,38 +1071,33 @@ ImapConnection.prototype._fetch = function(which, uids, options) {
if (extensions)
cmd += extensions;
cmd += 'UID FLAGS INTERNALDATE';
if (opts.request.struct)
if (options.struct)
cmd += ' BODYSTRUCTURE';
if (opts.request.size)
if (options.size)
cmd += ' RFC822.SIZE';
if (toFetch !== undefined) {
cmd += ' BODY';
if (!opts.markSeen)
cmd += '.PEEK';
cmd += '[';
if (toFetch)
cmd += toFetch;
cmd += ']';
if (bodyRange)
cmd += bodyRange;
}
cmd += ')';
this._send(cmd, function(e) {
var fetcher = self._state.requests[0]._fetcher;
if (e && fetcher)
fetcher.emit('error', e);
else if (e && !fetcher)
self.emit('error', e);
else if (fetcher)
fetcher.emit('end');
this._send(cmd, function(err) {
var keys = Object.keys(fetchers), k, lenk = keys.length, f, lenf,
fetches;
if (err) {
for (k = 0; k < lenk; ++k) {
fetches = fetchers[keys[k]];
for (f = 0, lenf = fetches.length; f < lenf; ++f)
fetches[f].emit('error', err);
}
}
for (k = 0; k < lenk; ++k) {
fetches = fetchers[keys[k]];
for (f = 0, lenf = fetches.length; f < lenf; ++f)
fetches[f].emit('end');
}
cb(err);
});
var imapFetcher = new ImapFetch(),
req = this._state.requests[this._state.requests.length - 1];
req._fetcher = imapFetcher;
req._useParser = useParser;
if (Array.isArray(opts.request.headers))
req._headers = onlyHeaders.toLowerCase().split(' ');
return imapFetcher;
this._state.requests[this._state.requests.length - 1].fetchers = fetchers;
};
ImapConnection.prototype.addFlags = function(uids, flags, cb) {
@ -1151,8 +1261,8 @@ ImapConnection.prototype.__defineGetter__('seq', function() {
setLabels: function(seqnos, labels, cb) {
self._storeLabels('', seqnos, labels, '', cb);
},
fetch: function(seqnos, options) {
return self._fetch('', seqnos, options);
fetch: function(seqnos, options, what, cb) {
return self._fetch('', seqnos, options, what, cb);
},
search: function(options, cb) {
self._search('', options, cb);
@ -1347,8 +1457,17 @@ ImapConnection.prototype._send = function(cmdstr, cb, bypass) {
}
};
function ImapMessage() {}
function ImapMessage() {
this.seqno = undefined;
this.uid = undefined;
this.flags = undefined;
this.date = undefined;
this.structure = undefined;
this.size = undefined;
}
inherits(ImapMessage, EventEmitter);
function ImapFetch() {}
function ImapFetch() {
this._parse = false;
}
inherits(ImapFetch, EventEmitter);

@ -1,5 +1,9 @@
var utils = require('./imap.utilities');
var reCRLF = /\r\n/g,
reHdr = /^([^:]+):\s(.+)?$/,
reHdrFold = /^\s+(.+)$/;
exports.convStr = function(str, literals) {
if (str[0] === '"')
return str.substring(1, str.length-1);
@ -16,6 +20,32 @@ exports.convStr = function(str, literals) {
return str;
};
exports.parseHeaders = function(str) {
var lines = str.split(reCRLF),
headers = {}, m;
for (var i = 0, h, len = lines.length; i < len; ++i) {
if (lines[i].length === 0)
continue;
if (lines[i][0] === '\t' || lines[i][0] === ' ') {
// folded header content
m = reHdrFold.exec(lines[i]);
headers[h][headers[h].length - 1] += m[1];
} else {
m = reHdr.exec(lines[i]);
h = m[1].toLowerCase();
if (m[2]) {
if (headers[h] === undefined)
headers[h] = [m[2]];
else
headers[h].push(m[2]);
} else
headers[h] = [''];
}
}
return headers;
};
exports.parseNamespaces = function(str, literals) {
var result, vals;
if (str.length === 3 && str.toUpperCase() === 'NIL')
@ -50,6 +80,7 @@ exports.parseFetch = function(str, literals, fetchData) {
for (var i=0,len=result.length; i<len; i+=2) {
if (Array.isArray(result[i]))
result[i] = 'BODY';
else
result[i] = result[i].toUpperCase();
if (result[i] === 'UID')
fetchData.uid = parseInt(result[i+1], 10);

@ -36,95 +36,6 @@ exports.setSecure = function(tcpSocket) {
return cleartext;
};
/**
* Adopted from jquery's extend method. Under the terms of MIT License.
*
* http://code.jquery.com/jquery-1.4.2.js
*
* Modified by Brian White to use Array.isArray instead of the custom isArray
* method
*/
exports.extend = function() {
// copy reference to target object
var target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false,
options,
name,
src,
copy;
// Handle a deep copy situation
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
}
// Handle case when target is a string or something (possible in deep copy)
if (typeof target !== "object" && typeof target !== 'function')
target = {};
var isPlainObject = function(obj) {
// Must be an Object.
// Because of IE, we also have to check the presence of the constructor
// property.
// Make sure that DOM nodes and window objects don't pass through, as well
if (!obj || toString.call(obj) !== "[object Object]" || obj.nodeType
|| obj.setInterval)
return false;
var has_own_constructor = hasOwnProperty.call(obj, "constructor");
var has_is_prop_of_method = hasOwnProperty.call(obj.constructor.prototype,
"isPrototypeOf");
// Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_prop_of_method)
return false;
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var last_key;
for (var key in obj)
last_key = key;
return last_key === undefined || hasOwnProperty.call(obj, last_key);
};
for (; i < length; i++) {
// Only deal with non-null/undefined values
if ((options = arguments[i]) !== null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy)
continue;
// Recurse if we're merging object literal values or arrays
if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) {
var clone = src && (isPlainObject(src) || Array.isArray(src)
? src : (Array.isArray(copy) ? [] : {}));
// Never move original objects, clone them
target[name] = exports.extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined)
target[name] = copy;
}
}
}
// Return the modified object
return target;
};
exports.isNotEmpty = function(str) {
return str.trim().length > 0;
};

Loading…
Cancel
Save