Added support for IMAP STORE and EXPUNGE commands, fixed parsing of dispositions with NIL parameters in BODYSTRUCTUREs, and fixed the regex for capturing the BODYSTRUCTURE sent by the server.

fork
Brian 14 years ago
parent c45ab05455
commit 0c5ed3df53

@ -228,6 +228,12 @@ ImapConnection Functions
* **headers** - A Boolean/Array value. A value of true fetches all message headers. An Array containing specific message headers to retrieve can also be specified. **Default:** true
* **body** - A Boolean/String value. A value of true fetches the entire raw message body. A String value containing a valid partID (see _FetchResult_'s structure property) fetches the body/content of that particular part. **Default:** false
* **removeDeleted**(Function) - _(void)_ - Permanently removes all messages flagged as \Deleted. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **addFlags**(Integer, String/Array, Function) - _(void)_ - Adds the specified flag(s) to the message identified by the Integer parameter. The second parameter can either be a string containing a single flag or can be an Array of flags. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **delFlags**(Integer, String/Array, Function) - _(void)_ - Removes the specified flag(s) from the message identified by the Integer parameter. The second parameter can either be a string containing a single flag or can be an Array of flags. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
TODO
----
@ -240,9 +246,7 @@ A bunch of things not yet implemented in no particular order:
* HEADER.FIELDS.NOT capability during FETCH using "!" prefix
* Allow FETCHing of byte ranges of body TEXTs instead of always the entire body (useful for previews of large messages, etc)
* Support additional IMAP commands/extensions:
* APPEND
* EXPUNGE
* STORE
* APPEND (is this really useful?)
* GETQUOTA (via QUOTA extension -- http://tools.ietf.org/html/rfc2087)
* UNSELECT (via UNSELECT extension -- http://tools.ietf.org/html/rfc3691)
* LIST (and XLIST via XLIST extension -- http://groups.google.com/group/Gmail-Help-POP-and-IMAP-en/browse_thread/thread/a154105c54f020fb)

@ -17,7 +17,7 @@ function ImapConnection (options) {
numCapRecvs: 0,
isReady: false,
isIdle: true,
delim: "/",
delim: '/',
tmrKeepalive: null,
tmoKeepalive: 10000,
curData: '',
@ -113,11 +113,11 @@ ImapConnection.prototype.connect = function(loginCb) {
// Should never happen ....
} else if (data[0] === '*') { // Untagged server response
if (self._state.status === STATES.NOAUTH) {
if (data[1] === "PREAUTH") { // no need to login, the server pre-authenticated us
if (data[1] === 'PREAUTH') { // no need to login, the server pre-authenticated us
self._state.status = STATES.AUTH;
if (self._state.numCapRecvs === 0)
self._state.numCapRecvs = 1;
} else if (data[1] === "NO" || data[1] === "BAD" || data[1] === "BYE") {
} else if (data[1] === 'NO' || data[1] === 'BAD' || data[1] === 'BYE') {
self._state.conn.end();
return;
}
@ -152,7 +152,7 @@ ImapConnection.prototype.connect = function(loginCb) {
}
break;
case 'SEARCH':
self._state.box._lastSearch = data[2].split(" ");
self._state.box._lastSearch = data[2].split(' ');
break;
case 'LIST':
var result;
@ -170,25 +170,29 @@ ImapConnection.prototype.connect = function(loginCb) {
if (self._state.status !== STATES.BOXSELECTING)
self.emit('mail', self._state.box.messages.new); // new mail notification
break;
case 'EXPUNGE': // confirms permanent deletion of a single message
if (self._state.box.messages.total > 0)
self._state.box.messages.total--;
break;
default:
// Check for FETCH result
if (/^FETCH /i.test(data[2])) {
var regex = "\\(UID ([\\d]+) INTERNALDATE \"(.*?)\" FLAGS \\((.*?)\\)", result;
if ((result = new RegExp(regex + " BODYSTRUCTURE \\((.*)\\)").exec(data[2])))
if ((result = new RegExp(regex + " BODYSTRUCTURE \\((.*\\))(?=\\)|[\\s])").exec(data[2])))
self._state.fetchData.structure = parseBodyStructure(result[4]);
result = new RegExp(regex).exec(data[2]);
self._state.fetchData.date = result[2];
self._state.fetchData.flags = result[3].split(" ").filter(isNotEmpty);
self._state.fetchData.flags = result[3].split(' ').filter(isNotEmpty);
if (literalData.length > 0) {
result = /BODY\[(.*)\] \{[\d]+\}$/.exec(data[2]);
if (result[1].indexOf("HEADER") === 0) { // either full or selective headers
if (result[1].indexOf('HEADER') === 0) { // either full or selective headers
var headers = literalData.split(/\r\n(?=[\w])/), header;
self._state.fetchData.headers = {};
for (var i=0,len=headers.length; i<len; i++) {
header = headers[i].substr(0, headers[i].indexOf(": ")).toLowerCase();
header = headers[i].substr(0, headers[i].indexOf(': ')).toLowerCase();
if (!self._state.fetchData.headers[header])
self._state.fetchData.headers[header] = [];
self._state.fetchData.headers[header].push(headers[i].substr(headers[i].indexOf(": ")+2).replace(/\r\n/g, "").trim());
self._state.fetchData.headers[header].push(headers[i].substr(headers[i].indexOf(': ')+2).replace(/\r\n/g, '').trim());
}
} else // full message or part body
self._state.fetchData.body = literalData;
@ -204,7 +208,7 @@ ImapConnection.prototype.connect = function(loginCb) {
self._state.tmrKeepalive = setTimeout(self._idleCheck.bind(self), self._state.tmoKeepalive);
if (self._state.status === STATES.BOXSELECTING) {
if (data[1] === "OK")
if (data[1] === 'OK')
self._state.status = STATES.BOXSELECTED;
else {
self._state.status = STATES.AUTH;
@ -214,17 +218,17 @@ ImapConnection.prototype.connect = function(loginCb) {
if (typeof self._state.requests[0].callback === 'function') {
var err = null;
if (data[1] !== "OK") {
err = new Error("Error while executing request: " + data[2]);
if (data[1] !== 'OK') {
err = new Error('Error while executing request: ' + data[2]);
err.type = data[1];
err.request = self._state.requests[0].command;
self._state.requests[0].callback(err);
} else if (self._state.status === STATES.BOXSELECTED) {
if (data[2].indexOf("SEARCH") === 0) {
if (data[2].indexOf('SEARCH') === 0) {
var result = self._state.box._lastSearch;
self._state.box._lastSearch = null;
self._state.requests[0].callback(err, self._state.box, result);
} else if (self._state.requests[0].command.indexOf("UID FETCH") === 0)
} else if (self._state.requests[0].command.indexOf('UID FETCH') === 0)
self._state.requests[0].callback(err, self._state.box, self._state.fetchData);
else
self._state.requests[0].callback(err, self._state.box);
@ -267,14 +271,14 @@ ImapConnection.prototype.logout = function(cb) {
if (this._state.status >= STATES.NOAUTH)
this._send('LOGOUT', cb);
else
throw new Error("Not connected");
throw new Error('Not connected');
};
ImapConnection.prototype.openBox = function(name, readOnly, cb) {
if (this._state.status < STATES.AUTH)
throw new Error("Not connected or authenticated");
throw new Error('Not connected or authenticated');
else if (typeof name !== 'string')
name = "INBOX";
name = 'INBOX';
if (this._state.status === STATES.BOXSELECTED)
this._resetBox();
if (typeof readOnly !== 'boolean')
@ -289,7 +293,7 @@ ImapConnection.prototype.openBox = function(name, readOnly, cb) {
ImapConnection.prototype.closeBox = function(cb) { // also deletes any messages in this box marked with \Deleted
var self = this;
if (this._state.status !== STATES.BOXSELECTED)
throw new Error("No mailbox is currently selected");
throw new Error('No mailbox is currently selected');
this._send('CLOSE', function(err) {
if (!err) {
self._state.status = STATES.AUTH;
@ -301,12 +305,12 @@ ImapConnection.prototype.closeBox = function(cb) { // also deletes any messages
ImapConnection.prototype.search = function(options, cb) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error("No mailbox is currently selected");
throw new Error('No mailbox is currently selected');
if (!Array.isArray(options))
throw new Error("Expected array for search options");
var searchargs = "", months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
throw new Error('Expected array for search options');
var searchargs = '', months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
for (var i=0,len=options.length; i<len; i++) {
var criteria = options[i], args = null, modifier = " ";
var criteria = options[i], args = null, modifier = ' ';
if (typeof criteria === 'string')
criteria = criteria.toUpperCase();
else if (Array.isArray(criteria)) {
@ -316,8 +320,8 @@ ImapConnection.prototype.search = function(options, cb) {
criteria = criteria[0].toUpperCase();
} else
throw new Error('Unexpected search option data type. Expected string, number, or array. Got: ' + typeof criteria);
if (criteria[0] === "!") {
modifier += "NOT ";
if (criteria[0] === '!') {
modifier += 'NOT ';
criteria = criteria.substr(1);
}
switch(criteria) {
@ -345,7 +349,7 @@ ImapConnection.prototype.search = function(options, cb) {
case 'TO':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + " \"" + escape(''+args[0]) + "\"";
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '"';
break;
case 'BEFORE':
case 'ON':
@ -359,13 +363,13 @@ ImapConnection.prototype.search = function(options, cb) {
if ((args[0] = new Date(args[0])).toString() === 'Invalid Date')
throw new Error('Search option argument must be a Date object or a parseable date');
}
searchargs += modifier + criteria + " " + args[0].getDate() + "-" + months[args[0].getMonth()] + "-" + args[0].getFullYear();
searchargs += modifier + criteria + ' ' + args[0].getDate() + '-' + months[args[0].getMonth()] + '-' + args[0].getFullYear();
break;
/*case 'KEYWORD':
case 'UNKEYWORD':
if (!args || args.length !== 1)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + " " + args[0];
searchargs += modifier + criteria + ' ' + args[0];
break;*/
case 'LARGER':
case 'SMALLER':
@ -374,12 +378,12 @@ ImapConnection.prototype.search = function(options, cb) {
var num = parseInt(args[0]);
if (isNaN(num))
throw new Error('Search option argument must be a number');
searchargs += modifier + criteria + " " + args[0];
searchargs += modifier + criteria + ' ' + args[0];
break;
case 'HEADER':
if (!args || args.length !== 2)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + " \"" + escape(''+args[0]) + "\" \"" + escape(''+args[1]) + "\"";
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '" "' + escape(''+args[1]) + '"';
break;
default:
throw new Error('Unexpected search option: ' + criteria);
@ -389,6 +393,8 @@ ImapConnection.prototype.search = function(options, cb) {
};
ImapConnection.prototype.fetch = function(uid, options, cb) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox is currently selected');
if (arguments.length < 1)
throw new Error('The message ID must be specified');
if (isNaN(parseInt(''+uid)))
@ -408,13 +414,13 @@ ImapConnection.prototype.fetch = function(uid, options, cb) {
if (!Array.isArray(options.request.headers)) {
if (typeof options.request.headers === 'boolean' && options.request.headers === true)
toFetch = "HEADER"; // fetches headers only
toFetch = 'HEADER'; // fetches headers only
else if (typeof options.request.body === 'boolean' && options.request.body === true)
toFetch = "TEXT"; // fetches the whole entire message text (minus the headers), including all message parts
toFetch = 'TEXT'; // fetches the whole entire message text (minus the headers), including all message parts
else if (typeof options.request.body === 'string')
toFetch = options.request.body; // specific message part identifier, e.g. "1", "2", "1.1", "1.2", etc
toFetch = options.request.body; // specific message part identifier, e.g. '1', '2', '1.1', '1.2', etc
} else
toFetch = "HEADER.FIELDS (" + options.request.headers.join(" ").toUpperCase() + ")"; // fetch specific headers only
toFetch = 'HEADER.FIELDS (' + options.request.headers.join(' ').toUpperCase() + ')'; // fetch specific headers only
this._resetFetch();
this._send('UID FETCH ' + uid + ' (FLAGS INTERNALDATE'
@ -422,8 +428,48 @@ ImapConnection.prototype.fetch = function(uid, options, cb) {
+ (toFetch ? ' BODY' + (!options.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' : '') + ')', cb);
};
ImapConnection.prototype.removeDeleted = function(cb) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox is currently selected');
cb = arguments[arguments.length-1];
this._send('EXPUNGE', cb);
};
ImapConnection.prototype.addFlags = function(uid, flags, cb) {
try {
this._storeFlag(uid, flags, true, cb);
} catch (err) {
throw err;
}
};
ImapConnection.prototype.delFlags = function(uid, flags, cb) {
try {
this._storeFlag(uid, flags, false, cb);
} catch (err) {
throw err;
}
};
/****** Private Functions ******/
ImapConnection.prototype._storeFlag = function(uid, flags, isAdding, cb) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox is currently selected');
if (typeof uid === 'undefined')
throw new Error('The message ID must be specified');
if (isNaN(parseInt(''+uid)))
throw new Error('Message ID must be a number');
if ((!Array.isArray(flags) && typeof flags !== 'string') || (Array.isArray(flags) && flags.length === 0))
throw new Error('Flags argument must be a string or a non-empty Array');
if (!Array.isArray(flags))
flags = [flags];
cb = arguments[arguments.length-1];
this._send('STORE ' + (isAdding ? '+' : '-') + 'FLAGS.SILENT (' + flags.join(' ') + ')', cb);
};
ImapConnection.prototype._login = function(cb) {
var self = this,
fnReturn = function(err) {
@ -457,7 +503,7 @@ ImapConnection.prototype._reset = function() {
this._capabilities = [];
this._state.isIdle = true;
this._state.isReady = false;
this._state.delim = "/";
this._state.delim = '/';
this._resetBox();
this._resetFetch();
};
@ -560,7 +606,7 @@ function parseBodyStructure(str, prefix, partID) {
// [disposition]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
extensionData.disposition = { type: null, params: null };
str = str.substr(1);
lastIndex = getLastIdxQuoted(str);
@ -580,13 +626,14 @@ function parseBodyStructure(str, prefix, partID) {
isKey = !isKey;
}
str = str.substr(2).trim();
}
} else
str = str.substr(4).trim();
} else
str = str.substr(4);
// [language]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
extensionData.language = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -595,7 +642,7 @@ function parseBodyStructure(str, prefix, partID) {
// [location]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
extensionData.location = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -623,7 +670,7 @@ function parseBodyStructure(str, prefix, partID) {
str = str.substr(lastIndex+1).trim();
// content type
part.type.name = contentTypeMain.toLowerCase() + "/" + contentTypeSub.toLowerCase();
part.type.name = contentTypeMain.toLowerCase() + '/' + contentTypeSub.toLowerCase();
// content type parameters
if (str[0] === '(') {
@ -644,7 +691,7 @@ function parseBodyStructure(str, prefix, partID) {
str = str.substr(4);
// content id
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
part.id = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -652,7 +699,7 @@ function parseBodyStructure(str, prefix, partID) {
str = str.substr(4);
// content description
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
part.description = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -660,7 +707,7 @@ function parseBodyStructure(str, prefix, partID) {
str = str.substr(4);
// content encoding
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
part.encoding = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -668,7 +715,7 @@ function parseBodyStructure(str, prefix, partID) {
str = str.substr(4);
// size of content encoded in bytes
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = 0;
while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57)
lastIndex++;
@ -678,8 +725,8 @@ function parseBodyStructure(str, prefix, partID) {
str = str.substr(4);
// [# of lines]
if (part.type.name.indexOf("text/") === 0) {
if (str.substr(0, 3) !== "NIL") {
if (part.type.name.indexOf('text/') === 0) {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = 0;
while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57)
lastIndex++;
@ -691,7 +738,7 @@ function parseBodyStructure(str, prefix, partID) {
// [md5 hash of content]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
part.md5 = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -700,7 +747,7 @@ function parseBodyStructure(str, prefix, partID) {
// [disposition]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
part.disposition = { type: null, params: null };
str = str.substr(1);
lastIndex = getLastIdxQuoted(str);
@ -720,13 +767,14 @@ function parseBodyStructure(str, prefix, partID) {
isKey = !isKey;
}
str = str.substr(2).trim();
}
} else
str = str.substr(4).trim();
} else
str = str.substr(4);
// [language]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
if (str[0] === '(') {
part.language = [];
str = str.substr(1);
@ -745,7 +793,7 @@ function parseBodyStructure(str, prefix, partID) {
// [location]
if (str.length > 0) {
if (str.substr(0, 3) !== "NIL") {
if (str.substr(0, 3) !== 'NIL') {
lastIndex = getLastIdxQuoted(str);
part.location = str.substring(1, lastIndex);
str = str.substr(lastIndex+1).trim();
@ -802,7 +850,7 @@ function up(str) {
function getLastIdxQuoted(str) {
var index = -1, countQuote = 0;
for (var i=0,len=str.length; i<len; i++) {
if (str[i] === "\"") {
if (str[i] === '"') {
if (i > 0 && str[i-1] === "\\")
continue;
countQuote++;

Loading…
Cancel
Save