From 0c5ed3df537e2280863cf067f5a9de58a9f40418 Mon Sep 17 00:00:00 2001 From: Brian Date: Sun, 14 Nov 2010 14:00:55 -0500 Subject: [PATCH] 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. --- README.md | 10 ++-- imap.js | 150 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 106 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 7dd4c9b..cabb67e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/imap.js b/imap.js index abf8734..be37f89 100644 --- a/imap.js +++ b/imap.js @@ -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= 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 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 0 && str[i-1] === "\\") continue; countQuote++;