More refactoring, fixes, and additional features.

copy(), move(), fetch(), *Flags(), and *Keywords() methods now allow multiple message IDs.

move() now actually expunges the original message after copying, rather than merely setting the Deleted flag.

Removed command-specific items from the global state data and greatly simplified the populating of callback arguments.

Smarter literal data handling.

search() no longer goes kaput when no messages match the given criterion.

fetch() now always passes an Array to the callback.

Only pass the mailbox object to the callback for openBox() and renameBox().

Added UID criteria for search().

Fixed parsing of FETCH responses and added the message ID to the object generated by the FETCH parser.
fork
Brian White 14 years ago
parent 1519eb3043
commit 2a6d162025

@ -17,7 +17,7 @@ Requirements
Example
=======
This example fetches the 'date', 'from', 'to', 'subject' message headers and the message structure of the first message in the Inbox since May 20, 2010:
This example fetches the 'date', 'from', 'to', 'subject' message headers and the message structure of all unread messages in the Inbox since May 20, 2010:
var ImapConnection = require('./imap').ImapConnection, sys = require('sys'),
imap = new ImapConnection({
@ -30,20 +30,21 @@ This example fetches the 'date', 'from', 'to', 'subject' message headers and the
function die(err) {
console.log('Uh oh: ' + err);
process.exit(1);
}
var messages, cmds, next = 0, cb = function(err, box, result) {
var box, cmds, next = 0, cb = function(err) {
if (err)
die(err);
else if (next < cmds.length)
cmds[next++](box, result);
cmds[next++].apply(this, Array.prototype.slice.call(arguments).slice(1));
};
cmds = [
function() { imap.connect(cb); },
function() { imap.openBox('INBOX', false, cb); },
function() { imap.search([ ['SINCE', 'May 20, 2010'] ], cb); },
function(box, result) { imap.fetch(result[0], { request: { headers: ['from', 'to', 'subject', 'date'] } }, cb); },
function(box, result) { console.log(sys.inspect(result, false, 6)); imap.logout(cb); }
function(result) { box = result; imap.search([ 'UNSEEN', ['SINCE', 'May 20, 2010'] ], cb); },
function(results) { imap.fetch(results, { request: { headers: ['from', 'to', 'subject', 'date'] } }, cb); },
function(results) { console.log(sys.inspect(results, false, 6)); imap.logout(cb); }
];
cb();
@ -58,11 +59,13 @@ node-imap exposes one object: **ImapConnection**.
* _Box_ is an Object representing the currently open mailbox, and has the following properties:
* **name** - A String containing the name of this mailbox.
* **validity** - A String containing a number that indicates whether the message IDs in this mailbox have changed or not. In other words, as long as this value does not change on future openings of this mailbox, any cached message IDs for this mailbox are still valid.
* **permFlags** - An Array containing the flags that can be permanently added/removed to/from messages in this mailbox.
* **messages** - An Object containing properties about message counts for this mailbox.
* **total** - An Integer representing total number of messages in this mailbox.
* **new** - An Integer representing the number of new (unread) messages in this mailbox.
* _FetchResult_ is an Object representing the result of a message fetch, and has the following properties:
* **id** - An Integer that uniquely identifies this message (within its mailbox).
* **flags** - An Array containing the flags currently set on this message.
* **date** - A String containing the internal server date for the message (always represented in GMT?)
* **headers** - An Object containing the headers of the message, **if headers were requested when calling fetch().** Note: The value of each property in the object is an Array containing the value(s) for that particular header name (in case of duplicate headers).
@ -203,6 +206,8 @@ ImapConnection Properties
ImapConnection Functions
------------------------
**Note:** Message ID sets for message ID range arguments are not guaranteed to be contiguous.
* **(constructor)**([Object]) - _ImapConnection_ - Creates and returns a new instance of ImapConnection using the specified configuration object. Valid properties of the passed in object are:
* **username** - A String representing the username for authentication.
* **password** - A String representing the password for authentication.
@ -284,9 +289,9 @@ ImapConnection Functions
}
}
* **removeDeleted**(Function) - _(void)_ - Permanently removes all messages flagged as Deleted in the mailbox that is currently open. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **removeDeleted**(Function) - _(void)_ - Permanently removes (EXPUNGEs) all messages flagged as Deleted in the mailbox that is currently open. The Function parameter is the callback with one parameter: the error (null if none). **Note:** At least on Gmail, performing this operation with any currently open mailbox that is not the Spam or Trash mailbox will merely archive any messages marked as Deleted (by moving them to the 'All Mail' mailbox).
* **search**(Array, Function) - _(void)_ - Searches the currently open mailbox for messages using specific criterion. The Function parameter is the callback with three parameters: the error (null if none), the _Box_ object of the currently open mailbox, and an Array containing the message IDs matching the search criterion. The Array parameter is a list of Arrays containing the criterion (and any required arguments) to be used. Prefix the criteria name with an "!" to negate. For example, to search for unread messages since April 20, 2010 you could use: [ ['UNSEEN'], ['SINCE', 'April 20, 2010'] ]. To search for messages that are EITHER unread OR are dated April 20, 2010 or later, you could use: [ ['OR', ['UNSEEN'], ['SINCE', 'April 20, 2010'] ] ].
* **search**(Array, Function) - _(void)_ - Searches the currently open mailbox for messages using specific criterion. The Function parameter is the callback with two parameters: the error (null if none) and an Array containing the message IDs matching the search criterion. The Array parameter is a list of Arrays containing the criterion (and any required arguments) to be used. Prefix the criteria name with an "!" to negate. For example, to search for unread messages since April 20, 2010 you could use: [ 'UNSEEN', ['SINCE', 'April 20, 2010'] ]. To search for messages that are EITHER unread OR are dated April 20, 2010 or later, you could use: [ ['OR', 'UNSEEN', ['SINCE', 'April 20, 2010'] ] ].
* The following message flags are valid criterion and do not require values:
* 'ANSWERED' - Messages with the Answered flag set.
* 'DELETED' - Messages with the Deleted flag set.
@ -318,29 +323,31 @@ ImapConnection Functions
* 'SENTBEFORE' - Messages whose Date header (disregarding time and timezone) is earlier than the specified date.
* 'SENTON' - Messages whose Date header (disregarding time and timezone) is within the specified date.
* 'SENTSINCE' - Messages whose Date header (disregarding time and timezone) is within or later than the specified date.
* The following are valid criterion that require an Integer value:
* The following are valid criterion that require one Integer value:
* 'LARGER' - Messages with a size larger than the specified number of bytes.
* 'SMALLER' - Messages with a size smaller than the specified number of bytes.
* The following are valid criterion that require one or more Integer values:
* 'UID' - Messages with message IDs corresponding to the specified message ID set. Ranges are permitted (e.g. '2504:2507' or '*' or '2504:*').
* **Note:** By default, all criterion are ANDed together. You can use the special 'OR' on **two** criterion to find messages matching either search criteria (see example above).
* **fetch**(Integer, Object, Function) - _(void)_ - Fetches the message with the message ID specified by the Integer parameter in the currently open mailbox. The Function parameter is the callback with three parameters: the error (null if none), the _Box_ object of the currently open mailbox, and the _FetchResult_ containing the result of the fetch request. The Object parameter is a set of options used to determine how and what exactly to fetch. The valid options are:
* **markSeen** - A Boolean indicating whether to mark the message as read when fetching it. **Default:** false
* **fetch**(Integer/String/Array, Object, Function) - _(void)_ - Fetches the message(s) identified by the first parameter, in the currently open mailbox. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. The Function parameter is the callback with two parameters: the error (null if none) and an Array of _FetchResult_ Objects containing the results of the fetch request. An Object parameter is a set of options used to determine how and what exactly to fetch. The valid options are:
* **markSeen** - A Boolean indicating whether to mark the message(s) as read when fetching it. **Default:** false
* **request** - An Object indicating what to fetch (at least **headers** OR **body** must be set to false -- in other words, you can only fetch one aspect of the message at a time):
* **struct** - A Boolean indicating whether to fetch the structure of the message. **Default:** true
* **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/Array value. A Boolean value of true fetches the entire raw message body. A String value containing a valid partID (see _FetchResult_'s structure property) fetches the entire body/content of that particular part. An Array value of length 2 can be specified if you wish to request a byte range of the content, where the first item is a Boolean/String as previously described and the second item is a String indicating the byte range, for example, to fetch the first 500 bytes: '0-500'. **Default:** false
* **copy**(Integer, String, Function) - _(void)_ - Copies the message with the message ID specified by the Integer parameter in the currently open mailbox to the mailbox specified by the String parameter. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **copy**(Integer/String/Array, String, Function) - _(void)_ - Copies the message(s) with the message ID(s) identified by the first parameter, in the currently open mailbox, to the mailbox specified by the second parameter. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. The Function parameter is the callback with one parameter: the error (null if none).
* **move**(Integer, String, Function) - _(void)_ - Copies the message with the message ID specified by the Integer parameter in the currently open mailbox to the mailbox specified by the String parameter and marks the message in the current mailbox as Deleted. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **move**(Integer/String/Array, String, Function) - _(void)_ - Moves the message(s) with the message ID(s) identified by the first parameter, in the currently open mailbox, to the mailbox specified by the second parameter. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. The Function parameter is the callback with one parameter: the error (null if none). **Note:** The message in the destination mailbox will have a new message ID.
* **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.
* **addFlags**(Integer/String/Array, String/Array, Function) - _(void)_ - Adds the specified flag(s) to the message(s) identified by the first parameter. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. 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 one parameter: the error (null if none).
* **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.
* **delFlags**(Integer/String/Array, String/Array, Function) - _(void)_ - Removes the specified flag(s) from the message(s) identified by the first parameter. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. 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 one parameter: the error (null if none).
* **addKeywords**(Integer, String/Array, Function) - _(void)_ - Adds the specified keyword(s) to the message identified by the Integer parameter. The second parameter can either be a String containing a single keyword or can be an Array of keywords. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **addKeywords**(Integer/String/Array, String/Array, Function) - _(void)_ - Adds the specified keyword(s) to the message(s) identified by the first parameter. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. The second parameter can either be a String containing a single keyword or can be an Array of keywords. The Function parameter is the callback with one parameter: the error (null if none).
* **delKeywords**(Integer, String/Array, Function) - _(void)_ - Removes the specified keyword(s) from the message identified by the Integer parameter. The second parameter can either be a String containing a single keyword or can be an Array of keywords. The Function parameter is the callback with two parameters: the error (null if none), the _Box_ object of the currently open mailbox.
* **delKeywords**(Integer/String/Array, String/Array, Function) - _(void)_ - Removes the specified keyword(s) from the message(s) identified by the first parameter. The first parameter can either be an Integer for a single message ID, a String for a message ID range (e.g. '2504:2507' or '*' or '2504:*'), or an Array containing any number of the aforementioned Integers and/or Strings. The second parameter can either be a String containing a single keyword or can be an Array of keywords. The Function parameter is the callback with one parameter: the error (null if none).
TODO
@ -353,7 +360,6 @@ A bunch of things not yet implemented in no particular order:
* STATUS addition to LIST (via LISTA-STATUS extension -- http://tools.ietf.org/html/rfc5819)
* 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)
* SORT (via SORT extension -- http://tools.ietf.org/html/rfc5256)
* THREAD (via THREAD=ORDEREDSUBJECT and/or THREAD=REFERENCES extension(s) -- http://tools.ietf.org/html/rfc5256)
* ID (via ID extension -- http://tools.ietf.org/html/rfc2971) ?

@ -26,10 +26,9 @@ function ImapConnection (options) {
tmoKeepalive: 10000,
tmrConn: null,
curData: '',
curExpected: 0,
capabilities: [],
fetchData: { flags: [], date: null, headers: null, body: null, structure: null, _total: 0 },
box: { _uidnext: 0, _uidvalidity: 0, _flags: [], _lastSearch: null, _newKeywords: false, keywords: [], permFlags: [], name: null, messages: { total: 0, new: 0 }},
boxes: {}
box: { _uidnext: 0, _flags: [], _newKeywords: false, validity: 0, keywords: [], permFlags: [], name: null, messages: { total: 0, new: 0 }}
};
this._options = extend(true, this._options, options);
@ -104,18 +103,23 @@ ImapConnection.prototype.connect = function(loginCb) {
self._state.curData = undefined;
// Don't mess with incoming data if it's part of a literal
if (/\{(\d+)\}$/.test(data.substr(0, data.indexOf(CRLF)))) {
var result = /\{(\d+)\}$/.exec(data.substr(0, data.indexOf(CRLF)));
self._state.fetchData._total = parseInt(result[1]);
}
if (self._state.fetchData._total > 0) {
if (data.length - (data.indexOf(CRLF)+2) <= self._state.fetchData._total) {
var literalInfo;
if (self._state.curExpected === 0 && (literalInfo = /\{(\d+)\}$/.exec(data.substr(0, data.indexOf(CRLF)))))
self._state.curExpected = parseInt(literalInfo[1]);
if (self._state.curExpected > 0) {
if (data.length - (data.indexOf(CRLF)+2) <= self._state.curExpected) {
self._state.curData = data;
return;
}
literalData = data.substr(data.indexOf(CRLF) + 2, self._state.fetchData._total);
data = data.substr(0, data.indexOf(CRLF)) + data.substr(data.indexOf(CRLF) + 2 + self._state.fetchData._total);
self._state.fetchData._total = 0;
literalData = data.substr(data.indexOf(CRLF) + 2, self._state.curExpected);
data = data.substr(0, data.indexOf(CRLF)) + data.substr(data.indexOf(CRLF) + 2 + self._state.curExpected);
self._state.curExpected = 0;
if (data.substr(data.indexOf(CRLF)+2, 1) === '*') {
// found additional responses, so don't try splitting the proceeding response(s) for better performance in case they have literals too
var extra = data.substr(data.indexOf(CRLF)+2);
process.nextTick(function() { self._state.conn.emit('data', extra); });
data = data.substring(0, data.indexOf(CRLF));
}
}
data = data.split(CRLF).filter(isNotEmpty);
@ -162,7 +166,7 @@ ImapConnection.prototype.connect = function(loginCb) {
else if (self._state.status === STATES.BOXSELECTING) {
var result;
if ((result = /^\[UIDVALIDITY (\d+)\]$/i.exec(data[2])) !== null)
self._state.box._uidvalidity = result[1];
self._state.box.validity = result[1];
else if ((result = /^\[UIDNEXT (\d+)\]$/i.exec(data[2])) !== null)
self._state.box._uidnext = result[1];
else if ((result = /^\[PERMANENTFLAGS \((.*)\)\]$/i.exec(data[2])) !== null) {
@ -183,13 +187,19 @@ ImapConnection.prototype.connect = function(loginCb) {
parseNamespaces(data[2], self.namespaces);
break;
case 'SEARCH':
self._state.box._lastSearch = data[2].split(' ');
self._state.requests[0].args.push((typeof data[2] === 'undefined' || data[2].length === 0 ? [] : data[2].split(' ')));
break;
/*case 'STATUS':
var result = /UIDNEXT ([\d]+)\)$/.exec(data[2]);
self._state.requests[0].args.push(parseInt(result[1]));
break;*/
case 'LIST':
var result;
if (self.delim === null && (result = /^\(\\Noselect\) (.+?) ".*"$/.exec(data[2])) !== null)
self.delim = (result[1] === 'NIL' ? false : result[1].substring(1, result[1].length-1));
else if (self.delim !== null) {
if (self._state.requests[0].args.length === 0)
self._state.requests[0].args.push({});
result = /^\((.*)\) (.+?) "(.+)"$/.exec(data[2]);
var box = {
attribs: result[1].split(' ').map(function(attrib) {return attrib.substr(1).toUpperCase();})
@ -197,7 +207,7 @@ ImapConnection.prototype.connect = function(loginCb) {
delim: (result[2] === 'NIL' ? false : result[2].substring(1, result[2].length-1)),
children: null,
parent: null
}, name = result[3], curChildren = self._state.boxes;
}, name = result[3], curChildren = self._state.requests[0].args[0];
if (box.delim) {
var path = name.split(box.delim).filter(isNotEmpty), parent = null;
@ -230,51 +240,61 @@ ImapConnection.prototype.connect = function(loginCb) {
break;
default:
// Check for FETCH result
if (/^FETCH /i.test(data[2]))
parseFetch(data[2].substring(data[2].indexOf('(')+1, data[2].length-1), literalData, self._state.fetchData);
if (/^FETCH /i.test(data[2]) && self._state.requests[0].command.indexOf('UID FETCH') === 0) {
var idxResult;
if (self._state.requests[0].args.length === 0)
self._state.requests[0].args.push([]);
self._state.requests[0].args[0].push({ id: null, flags: [], date: null, headers: null, body: null, structure: null });
idxResult = self._state.requests[0].args[0].length-1;
parseFetch(data[2].substring(7, data[2].length-1), literalData, self._state.requests[0].args[0][idxResult]);
}
break;
}
}
}
} else if (data[0].indexOf('A') === 0) { // Tagged server response
var sendBox = false;
clearTimeout(self._state.tmrKeepalive);
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') {
sendBox = true;
self._state.status = STATES.BOXSELECTED;
else {
} else {
self._state.status = STATES.AUTH;
self._resetBox();
}
}
if (self._state.requests[0].command.indexOf('RENAME') > -1) {
self._state.box.name = self._state.box._newName;
delete self._state.box._newName;
sendBox = true;
}
if (typeof self._state.requests[0].callback === 'function') {
var err = null;
var args = self._state.requests[0].args, cmd = self._state.requests[0].command;
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);
err.request = cmd;
} else if (self._state.status === STATES.BOXSELECTED) {
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)
self._state.requests[0].callback(err, self._state.box, self._state.fetchData);
else if (self._state.requests[0].command.indexOf('LIST') === 0)
self._state.requests[0].callback(err, self._state.boxes);
else
self._state.requests[0].callback(err, self._state.box);
} else
self._state.requests[0].callback(err);
if (sendBox) // SELECT, EXAMINE, RENAME
args.unshift(self._state.box);
// According to RFC3501, UID commands do not give errors for non-existant user-supplied UIDs,
// so give the callback empty results if we unexpectedly received no untagged responses.
else if ((cmd.indexOf('UID FETCH') === 0 || cmd.indexOf('UID SEARCH') === 0) && args.length === 0)
args.unshift([]);
}
args.unshift(err);
self._state.requests[0].callback.apply({}, args);
}
self._state.requests.shift();
process.nextTick(function() { self._send(); });
self._state.isIdle = true;
self._resetFetch();
} else {
// unknown response
}
@ -372,7 +392,7 @@ ImapConnection.prototype.renameBox = function(oldname, newname, cb) {
else if (typeof newname !== 'string' || newname.length === 0)
throw new Error('New mailbox name must be a string describing the full path of a new mailbox to be renamed to');
if (this._state.status === STATES.BOXSELECTED && oldname === this._state.box.name && oldname !== 'INBOX')
this._state.box.name = oldname;
this._state.box._newName = oldname;
this._send('RENAME "' + escape(oldname) + '" "' + escape(newname) + '"', cb);
};
@ -385,13 +405,16 @@ ImapConnection.prototype.search = function(options, cb) {
this._send('UID SEARCH' + buildSearchQuery(options), cb);
};
ImapConnection.prototype.fetch = function(uid, options, cb) {
ImapConnection.prototype.fetch = function(uids, 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)))
throw new Error('Message ID must be a number');
if (!Array.isArray(uids))
uids = [uids];
try {
validateUIDList(uids);
} catch(e) {
throw e;
}
var defaults = {
markSeen: false,
request: {
@ -429,59 +452,91 @@ ImapConnection.prototype.fetch = function(uid, options, cb) {
} else
toFetch = 'HEADER.FIELDS (' + options.request.headers.join(' ').toUpperCase() + ')'; // fetch specific headers only
this._resetFetch();
this._send('UID FETCH ' + uid + ' (FLAGS INTERNALDATE'
this._send('UID FETCH ' + uids.join(',') + ' (FLAGS INTERNALDATE'
+ (options.request.struct ? ' BODYSTRUCTURE' : '')
+ (toFetch ? ' BODY' + (!options.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' + bodyRange : '') + ')', cb);
};
ImapConnection.prototype.addFlags = function(uid, flags, cb) {
ImapConnection.prototype.addFlags = function(uids, flags, cb) {
try {
this._store(uid, flags, true, cb);
this._store(uids, flags, true, cb);
} catch (err) {
throw err;
}
};
ImapConnection.prototype.delFlags = function(uid, flags, cb) {
ImapConnection.prototype.delFlags = function(uids, flags, cb) {
try {
this._store(uid, flags, false, cb);
this._store(uids, flags, false, cb);
} catch (err) {
throw err;
}
};
ImapConnection.prototype.addKeywords = function(uid, flags, cb) {
ImapConnection.prototype.addKeywords = function(uids, flags, cb) {
if (!self._state.box._newKeywords)
throw new Error('This mailbox does not allow new keywords to be added');
try {
this._store(uid, flags, true, cb);
this._store(uids, flags, true, cb);
} catch (err) {
throw err;
}
};
ImapConnection.prototype.delKeywords = function(uid, flags, cb) {
ImapConnection.prototype.delKeywords = function(uids, flags, cb) {
try {
this._store(uid, flags, false, cb);
this._store(uids, flags, false, cb);
} catch (err) {
throw err;
}
};
ImapConnection.prototype.copy = function(uid, boxTo, cb) {
this._send('UID COPY ' + uid + ' ' + boxTo, cb);
ImapConnection.prototype.copy = function(uids, boxTo, cb) {
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox is currently selected');
if (!Array.isArray(uids))
uids = [uids];
try {
validateUIDList(uids);
} catch(e) {
throw e;
}
this._send('UID COPY ' + uids.join(',') + ' "' + escape(boxTo) + '"', cb);
};
ImapConnection.prototype.move = function(uid, boxTo, cb) {
if (this._state.box.permFlags.indexOf('Deleted') === -1)
ImapConnection.prototype.move = function(uids, boxTo, cb) {
var self = this;
if (this._state.status !== STATES.BOXSELECTED)
throw new Error('No mailbox is currently selected');
if (self._state.box.permFlags.indexOf('Deleted') === -1)
cb(new Error('Cannot move message: server does not allow deletion of messages'));
else {
this.copy(uid, boxTo, function(err) {
self.copy(uids, boxTo, function(err, reentryCount, deletedUIDs, counter) {
if (err) {
cb(err);
return;
}
this.addFlags(uid, 'Deleted', cb);
var fnMe = arguments.callee;
counter = counter || 0;
// Make sure we don't expunge any messages marked as Deleted except the one we are moving
if (typeof reentryCount === 'undefined')
self.search(['DELETED'], function(e, result) { fnMe.call(this, e, 1, result); });
else if (reentryCount === 1) {
if (counter < deletedUIDs.length)
self.delFlags(deletedUIDs[counter], 'DELETED', function(e) { process.nextTick(function(){fnMe.call(this, e, reentryCount, deletedUIDs, counter+1);}); });
else
fnMe.call(this, err, reentryCount+1, deletedUIDs);
} else if (reentryCount === 2)
self.addFlags(uids, 'Deleted', function(e) { fnMe.call(this, e, reentryCount+1, deletedUIDs); });
else if (reentryCount === 3)
self.removeDeleted(function(e) { fnMe.call(this, e, reentryCount+1, deletedUIDs); });
else if (reentryCount === 4) {
if (counter < deletedUIDs.length)
self.addFlags(deletedUIDs[counter], 'DELETED', function(e) { process.nextTick(function(){fnMe.call(this, e, reentryCount, deletedUIDs, counter+1);}); });
else
cb();
}
});
}
};
@ -494,14 +549,19 @@ ImapConnection.prototype._fnTmrConn = function(loginCb) {
this._state.conn.destroy();
}
ImapConnection.prototype._store = function(uid, flags, isAdding, cb) {
ImapConnection.prototype._store = function(uids, flags, isAdding, cb) {
var isKeywords = (arguments.callee.caller === this.addKeywords || arguments.callee.caller === this.delKeywords);
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 (typeof uids === 'undefined')
throw new Error('The message ID(s) must be specified');
if (!Array.isArray(uids))
uids = [uids];
try {
validateUIDList(uids);
} catch(e) {
throw e;
}
if ((!Array.isArray(flags) && typeof flags !== 'string') || (Array.isArray(flags) && flags.length === 0))
throw new Error((isKeywords ? 'Keywords' : 'Flags') + ' argument must be a string or a non-empty Array');
if (!Array.isArray(flags))
@ -521,7 +581,7 @@ ImapConnection.prototype._store = function(uid, flags, isAdding, cb) {
flags = flags.join(' ');
cb = arguments[arguments.length-1];
this._send('UID STORE ' + uid + ' ' + (isAdding ? '+' : '-') + 'FLAGS.SILENT (' + flags + ')', cb);
this._send('UID STORE ' + uids.join(',') + ' ' + (isAdding ? '+' : '-') + 'FLAGS.SILENT (' + flags + ')', cb);
};
ImapConnection.prototype._login = function(cb) {
@ -556,19 +616,16 @@ ImapConnection.prototype._reset = function() {
this._state.numCapRecvs = 0;
this._state.requests = [];
this._state.capabilities = [];
this.namespaces = { personal: [], other: [], shared: [] };
this._state.isIdle = true;
this._state.isReady = false;
this.namespaces = { personal: [], other: [], shared: [] };
this.delim = null;
this._state.boxes = {};
this._resetBox();
this._resetFetch();
};
ImapConnection.prototype._resetBox = function() {
this._state.box._uidnext = 0;
this._state.box._uidvalidity = 0;
this._state.box.validity = 0;
this._state.box._flags = [];
this._state.box._lastSearch = null;
this._state.box._newKeywords = false;
this._state.box.permFlags = [];
this._state.box.keywords = [];
@ -576,14 +633,6 @@ ImapConnection.prototype._resetBox = function() {
this._state.box.messages.total = 0;
this._state.box.messages.new = 0;
};
ImapConnection.prototype._resetFetch = function() {
this._state.fetchData.flags = [];
this._state.fetchData.date = null;
this._state.fetchData.headers = null;
this._state.fetchData.body = null;
this._state.fetchData.structure = null;
this._state.fetchData._total = 0;
};
ImapConnection.prototype._idleCheck = function() {
if (this._state.isIdle)
this._noop();
@ -594,7 +643,7 @@ ImapConnection.prototype._noop = function() {
};
ImapConnection.prototype._send = function(cmdstr, cb, bypass) {
if (arguments.length > 0 && !bypass)
this._state.requests.push({ command: cmdstr, callback: cb });
this._state.requests.push({ command: cmdstr, callback: cb, args: [] });
if ((arguments.length === 0 && this._state.requests.length > 0) || this._state.requests.length === 1 || bypass) {
clearTimeout(this._state.tmrKeepalive);
this._state.isIdle = false;
@ -618,7 +667,7 @@ function buildSearchQuery(options, isOrChild) {
if (criteria.length > 0)
criteria = criteria[0].toUpperCase();
} else
throw new Error('Unexpected search option data type. Expected string, number, or array. Got: ' + typeof criteria);
throw new Error('Unexpected search option data type. Expected string or array. Got: ' + typeof criteria);
if (criteria === 'OR') {
if (args.length !== 2)
throw new Error('OR must have exactly two arguments');
@ -689,6 +738,17 @@ function buildSearchQuery(options, isOrChild) {
throw new Error('Incorrect number of arguments for search option: ' + criteria);
searchargs += modifier + criteria + ' "' + escape(''+args[0]) + '" "' + escape(''+args[1]) + '"';
break;
case 'UID':
if (!args)
throw new Error('Incorrect number of arguments for search option: ' + criteria);
args = args.slice(1);
try {
validateUIDList(args);
} catch(e) {
throw e;
}
searchargs += modifier + criteria + ' ' + args.join(',');
break;
default:
throw new Error('Unexpected search option: ' + criteria);
}
@ -699,6 +759,24 @@ function buildSearchQuery(options, isOrChild) {
return searchargs;
}
function validateUIDList(uids) {
for (var i=0,len=uids.length,intval; i<len; i++) {
if (typeof uids[i] === 'string') {
if (uids[i] === '*' || uids[i] === '*:*') {
if (len > 1)
uids = ['*'];
break;
} else if (/^(?:[\d]+|\*):(?:[\d]+|\*)$/.test(uids[i]))
continue;
}
intval = parseInt(''+uids[i]);
if (isNaN(intval))
throw new Error('Message ID must be an integer, "*", or a range: ' + uids[i]);
else if (typeof uids[i] !== 'number')
uids[i] = intval;
}
}
function parseNamespaces(str, namespaces) {
// str contains 3 parenthesized lists (or NIL) describing the personal, other users', and shared namespaces available
var idxNext, idxNextName, idxNextVal, strNamespace, strList, details, types = Object.keys(namespaces), curType = 0;
@ -782,7 +860,7 @@ function parseFetch(str, literalData, fetchData) {
// and {xxxx} is the byte count for the literalData describing the preceding item (almost always "BODY")
var key, idxNext;
while (str.length > 0) {
key = str.substring(0, str.indexOf(' '));
key = (str.substr(0, 5) === 'BODY[' ? str.substring(0, (str.indexOf('>') > -1 ? str.indexOf('>') : str.indexOf(']'))+1) : str.substring(0, str.indexOf(' ')));
str = str.substring(str.indexOf(' ')+1);
if (str.substr(0, 3) === 'NIL')
idxNext = 3;
@ -790,6 +868,7 @@ function parseFetch(str, literalData, fetchData) {
switch (key) {
case 'UID':
idxNext = str.indexOf(' ')+1;
fetchData.id = parseInt(str.substring(0, idxNext-1));
break;
case 'INTERNALDATE':
idxNext = str.indexOf('"', 1)+1;

Loading…
Cancel
Save