From 0bc8f47e97cdeb6c6beeb1f288419a7cdb928561 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 14 Nov 2010 23:34:53 -0500 Subject: [PATCH] Added the ability to specify a byte range when fetching a message's (raw or part) body. Fixed a bug that was causing flags to not be added or removed at all. Lastly, a list of available permanent flags for the current mailbox is now available under the permFlags property of the mailbox object. --- README.md | 20 +++++++++++++++----- imap.js | 36 +++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cabb67e..aa3ba61 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ 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. + * **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. @@ -153,6 +154,15 @@ The structure of a message with only one part will simply look something like th ] Therefore, an easy way to check for a multipart message is to check if the structure length is >1. +Lastly, here are the system flags defined by the IMAP spec (that may be added/removed to/from messages): +* \Seen - Message has been read +* \Answered - Message has been answered +* \Flagged - Message is "flagged" for urgent/special attention +* \Deleted - Message is "deleted" for removal +* \Draft - Message has not completed composition (marked as a draft). +It should be noted however that the IMAP server can limit which flags can be permanently modified for any given message. If in doubt, check the mailbox's **permFlags** Array first. +Additional custom flags may be provided by the server. If available, these will also be listed in the mailbox's **permFlags** Array. + ImapConnection Events --------------------- @@ -195,7 +205,7 @@ ImapConnection Functions * 'NEW' - Messages that have the \Recent flag set but not the \Seen flag. * 'SEEN' - Messages that have the \Seen flag set. * 'RECENT' - Messages that have the \Recent flag set. - * 'OLD' - Messages that do not have the \Recent flag set. This is functionally equivalent to "!RECENT" (as opposed to "!NEW"). + * 'OLD' - Messages that do not have the \Recent flag set. This is functionally equivalent to a criteria of "!RECENT" (as opposed to "!NEW"). * 'UNANSWERED' - Messages that do not have the \Answered flag set. * 'UNDELETED' - Messages that do not have the \Deleted flag set. * 'UNDRAFT' - Messages that do not have the \Draft flag set. @@ -226,13 +236,13 @@ ImapConnection Functions * **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 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 + * **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 * **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. +* **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. +* **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 @@ -244,7 +254,7 @@ A bunch of things not yet implemented in no particular order: * Support AUTH=CRAM-MD5/AUTH=CRAM_MD5 authentication * OR searching ability with () grouping * 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 IMAP keywords (with a workaround for gmail's lack of support for IMAP keywords) * Support additional IMAP commands/extensions: * APPEND (is this really useful?) * GETQUOTA (via QUOTA extension -- http://tools.ietf.org/html/rfc2087) diff --git a/imap.js b/imap.js index be37f89..03d6208 100644 --- a/imap.js +++ b/imap.js @@ -22,7 +22,7 @@ function ImapConnection (options) { tmoKeepalive: 10000, curData: '', fetchData: { flags: [], date: null, headers: null, body: null, structure: null, _total: 0 }, - box: { _uidnext: 0, _uidvalidity: 0, _flags: [], _permflags: [], name: null, messages: { total: 0, new: 0 }} + box: { _uidnext: 0, _uidvalidity: 0, _flags: [], permFlags: [], name: null, messages: { total: 0, new: 0 }} }; this._capabilities = []; @@ -68,7 +68,7 @@ ImapConnection.prototype.connect = function(loginCb) { fnInit(); }); this._state.conn.on('data', function(data) { - var literalData; + var literalData = ''; debug('RECEIVED: ' + data); if (data.indexOf(CRLF) === -1) { @@ -148,7 +148,7 @@ ImapConnection.prototype.connect = function(loginCb) { 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) - self._state.box._permflags = result[1].split(' '); + self._state.box.permFlags = result[1].split(' '); } break; case 'SEARCH': @@ -184,7 +184,7 @@ ImapConnection.prototype.connect = function(loginCb) { self._state.fetchData.date = result[2]; self._state.fetchData.flags = result[3].split(' ').filter(isNotEmpty); if (literalData.length > 0) { - result = /BODY\[(.*)\] \{[\d]+\}$/.exec(data[2]); + result = /BODY\[(.*)\](?:\<[\d]+\>)? \{[\d]+\}$/.exec(data[2]); if (result[1].indexOf('HEADER') === 0) { // either full or selective headers var headers = literalData.split(/\r\n(?=[\w])/), header; self._state.fetchData.headers = {}; @@ -406,26 +406,40 @@ ImapConnection.prototype.fetch = function(uid, options, cb) { headers: true, // \_______ at most one of these can be used for any given fetch request body: false // / } - }, toFetch; + }, toFetch, bodyRange = ''; cb = arguments[arguments.length-1]; if (typeof options !== 'object') options = {}; options = extend(true, defaults, options); if (!Array.isArray(options.request.headers)) { + if (Array.isArray(options.request.body)) { + var rangeInfo; + if (options.request.body.length !== 2) + throw new Error("Expected Array of length 2 for body property for byte range"); + else if (typeof options.request.body[1] !== 'string' + || !(rangeInfo = /^([\d]+)\-([\d]+)$/.exec(options.request.body[1])) + || parseInt(rangeInfo[1]) >= parseInt(rangeInfo[2])) + throw new Error("Invalid body byte range format"); + bodyRange = '<' + parseInt(rangeInfo[1]) + '.' + parseInt(rangeInfo[2]) + '>'; + options.request.body = options.request.body[0]; + } if (typeof options.request.headers === 'boolean' && options.request.headers === true) 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 - else if (typeof options.request.body === 'string') + else if (typeof options.request.body === 'string') { + if (!/^([\d]+[\.]{0,1})*[\d]+$/.test(options.request.body)) + throw new Error("Invalid body partID format"); 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 this._resetFetch(); this._send('UID FETCH ' + uid + ' (FLAGS INTERNALDATE' + (options.request.struct ? ' BODYSTRUCTURE' : '') - + (toFetch ? ' BODY' + (!options.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' : '') + ')', cb); + + (toFetch ? ' BODY' + (!options.markSeen ? '.PEEK' : '') + '[' + toFetch + ']' + bodyRange : '') + ')', cb); }; ImapConnection.prototype.removeDeleted = function(cb) { @@ -465,9 +479,13 @@ ImapConnection.prototype._storeFlag = function(uid, flags, isAdding, cb) { throw new Error('Flags argument must be a string or a non-empty Array'); if (!Array.isArray(flags)) flags = [flags]; + for (var i=0; i