From f8f923a6d0b7677aff24eea2f8186b4409977c13 Mon Sep 17 00:00:00 2001 From: Andrew Jessup Date: Sat, 28 Jan 2012 14:38:05 +1100 Subject: [PATCH 1/6] Addded basic support for APPEND command --- README.md | 2 ++ imap.js | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7beaf5e..60ea1ec 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,8 @@ ImapConnection Functions * **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. +* **append**(String, Array, Date, Function) - _(void)_ - Appends a message to selected mailbox with the specified flags and date. The first parameter is a string the message to be appended, which should be a RFC-822 compatible MIME document including any headers and suitably encoded message attachments. The second parameter is should be an array of strings that denote the flags to be applied to the appended message (eg. ['\Seen', '\Flagged']) or null or an empty array if no flags should be set. The third parameter is a Date object that denotes the date the IMAP server should consider the message as being received, or it can be null (in which case the server will determine the received date). The Function parameter is the callback with one parameter: the error (null if none). **Note:** This method only serves to manipulate a connected mailbox, not to actually send mail. Some IMAP servers such as GMail will modify an existing message rather than create a new one if the specified 'Message-ID' header conflicts with a message already stored in that 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, 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). diff --git a/imap.js b/imap.js index 2915677..1fba03e 100644 --- a/imap.js +++ b/imap.js @@ -617,7 +617,32 @@ ImapConnection.prototype._search = function(which, options, cb) { + buildSearchQuery(options, this.capabilities), cb); }; -ImapConnection.prototype.seq.fetch = function(seqnos, options) { +ImapConnection.prototype.append = function(mimedata, flags, date, cb) { + if (this._state.status !== STATES.BOXSELECTED) { + throw new Error('No mailbox is currently selected'); + } + cmd = 'APPEND '+this._state.box.name+' '; + if(flags && flags.length) + if (!Array.isArray(flags)) + throw new Error('Expected null or array for flags'); + cmd += "("+flags.join(' ')+") "; + if(date){ + var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + if (!(date instanceof Date)) + throw new Error('Expected null or Date object for date'); + cmd += '"'+date.getDate()+'-'+months[date.getMonth()]+'-'+date.getFullYear(); + cmd += ' '+('0'+date.getHours().toString()).slice(-2)+':'+('0'+date.getMinutes().toString()).slice(-2)+':'+('0'+date.getSeconds().toString()).slice(-2); + cmd += ((date.getTimezoneOffset() > 0) ? ' -' : ' +' ); + cmd += ('0'+(-date.getTimezoneOffset() / 60).toString()).slice(-2); + cmd += ('0'+(-date.getTimezoneOffset() % 60).toString()).slice(-2); + cmd += '" '; + } + cmd += '{'+mimedata.length+"}\r\n" + mimedata; + this._send(cmd, cb); +} + +ImapConnection.prototype.fetch_seq = function(seqnos, options) { return this._fetch('', seqnos, options); }; ImapConnection.prototype.fetch = function(uids, options) { From 0b319c31a060134b4022fa0d7123479b679c2379 Mon Sep 17 00:00:00 2001 From: Andrew Jessup Date: Mon, 30 Jan 2012 00:47:46 +1100 Subject: [PATCH 2/6] Updates .append() and ._send() to support Buffers, .append() to use a configuration object, and some code tidying --- README.md | 6 ++++- imap.js | 75 +++++++++++++++++++++++++++++++++---------------------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 60ea1ec..3332c62 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,11 @@ ImapConnection Functions * **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. -* **append**(String, Array, Date, Function) - _(void)_ - Appends a message to selected mailbox with the specified flags and date. The first parameter is a string the message to be appended, which should be a RFC-822 compatible MIME document including any headers and suitably encoded message attachments. The second parameter is should be an array of strings that denote the flags to be applied to the appended message (eg. ['\Seen', '\Flagged']) or null or an empty array if no flags should be set. The third parameter is a Date object that denotes the date the IMAP server should consider the message as being received, or it can be null (in which case the server will determine the received date). The Function parameter is the callback with one parameter: the error (null if none). **Note:** This method only serves to manipulate a connected mailbox, not to actually send mail. Some IMAP servers such as GMail will modify an existing message rather than create a new one if the specified 'Message-ID' header conflicts with a message already stored in that mailbox. +* **append**(Buffer/String, Object, Function) - _(void)_ - Appends a message to selected mailbox. The first parameter is either a string or Buffer containing a RFC-822 compatible MIME message. The second parameter is a configuration object. Valid options are: + * **mailbox** - (optional) The name of the mailbox to append the message to. If not specified, the currently connected mailbox is assumed. + * **flags** - (optional) Either a string or an Array of flags to append to the message, eg. `['Seen', 'Flagged']` + * **date** - (optional) A Date object that denotes when the message was received. +The Function parameter is the callback with one parameter: the error (null if none). * **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). diff --git a/imap.js b/imap.js index 9ab33d3..19b6773 100644 --- a/imap.js +++ b/imap.js @@ -9,6 +9,8 @@ var emptyFn = function() {}, CRLF = '\r\n', debug=emptyFn, BOXSELECTING: 3, BOXSELECTED: 4 }, BOX_ATTRIBS = ['NOINFERIORS', 'NOSELECT', 'MARKED', 'UNMARKED'], + MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec'], reFetch = /^\* (\d+) FETCH .+? \{(\d+)\}\r\n/; function ImapConnection (options) { @@ -613,30 +615,43 @@ ImapConnection.prototype._search = function(which, options, cb) { + buildSearchQuery(options, this.capabilities), cb); }; -ImapConnection.prototype.append = function(mimedata, flags, date, cb) { - if (this._state.status !== STATES.BOXSELECTED) { - throw new Error('No mailbox is currently selected'); +ImapConnection.prototype.append = function(data, options, cb) { + options = options || {}; + if (!('mailbox' in options)) { + if (this._state.status !== STATES.BOXSELECTED) + throw new Error('No mailbox specified or currently selected'); + else + options.mailbox = this._state.box.name + } + cmd = 'APPEND "'+escape(options.mailbox)+'"'; + if ('flags' in options) { + if (!Array.isArray(options.flags)) + options.flags = Array(options.flags); + cmd += " (\\"+options.flags.join(' \\')+")"; } - cmd = 'APPEND '+this._state.box.name+' '; - if(flags && flags.length) - if (!Array.isArray(flags)) - throw new Error('Expected null or array for flags'); - cmd += "("+flags.join(' ')+") "; - if(date){ - var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', - 'Oct', 'Nov', 'Dec']; - if (!(date instanceof Date)) + if ('date' in options) { + if (!(options.date instanceof Date)) throw new Error('Expected null or Date object for date'); - cmd += '"'+date.getDate()+'-'+months[date.getMonth()]+'-'+date.getFullYear(); - cmd += ' '+('0'+date.getHours().toString()).slice(-2)+':'+('0'+date.getMinutes().toString()).slice(-2)+':'+('0'+date.getSeconds().toString()).slice(-2); - cmd += ((date.getTimezoneOffset() > 0) ? ' -' : ' +' ); - cmd += ('0'+(-date.getTimezoneOffset() / 60).toString()).slice(-2); - cmd += ('0'+(-date.getTimezoneOffset() % 60).toString()).slice(-2); - cmd += '" '; + cmd += ' "'+options.date.getDate()+'-'+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 += ((options.date.getTimezoneOffset() > 0) ? ' -' : ' +' ); + cmd += ('0'+(-options.date.getTimezoneOffset() / 60)).slice(-2); + cmd += ('0'+(-options.date.getTimezoneOffset() % 60)).slice(-2); + cmd += '"'; + } + if (data instanceof Buffer) { + cmd += ' {'+data.length+"}\r\n" + cmdbuf = new Buffer(Buffer.byteLength(cmd)+data.length); + cmdbuf.write(cmd); + data.copy(cmdbuf, Buffer.byteLength(cmd)); + this._send(cmdbuf, cb); + } else { + cmd += ' {'+Buffer.byteLength(data.toString())+"}\r\n" + cmd += data.toString(); // Send the command+data as a string + this._send(cmd, cb); } - cmd += '{'+mimedata.length+"}\r\n" + mimedata; - this._send(cmd, cb); } + ImapConnection.prototype.fetch = function(uids, options) { return this._fetch('UID ', uids, options); }; @@ -961,12 +976,12 @@ ImapConnection.prototype._noop = function() { if (this._state.status >= STATES.AUTH) this._send('NOOP'); }; -ImapConnection.prototype._send = function(cmdstr, cb, bypass) { - if (typeof cmdstr !== 'undefined' && !bypass) - this._state.requests.push({ command: cmdstr, callback: cb, args: [] }); - if ((typeof cmdstr === 'undefined' && this._state.requests.length) || +ImapConnection.prototype._send = function(cmdobj, cb, bypass) { + if (typeof cmdobj !== 'undefined' && !bypass) + this._state.requests.push({ command: cmdobj, callback: cb, args: [] }); + if ((typeof cmdobj === 'undefined' && this._state.requests.length) || this._state.requests.length === 1 || bypass) { - var prefix = '', cmd = (bypass ? cmdstr : this._state.requests[0].command); + var prefix = '', cmd = (bypass ? cmdobj : this._state.requests[0].command); clearTimeout(this._state.tmrKeepalive); if (this._state.ext.idle.sentIdle && cmd !== 'DONE') { this._send('DONE', undefined, true); @@ -981,7 +996,9 @@ ImapConnection.prototype._send = function(cmdstr, cb, bypass) { } if (cmd !== 'IDLE' && cmd !== 'DONE') prefix = 'A' + ++this._state.curId + ' '; - this._state.conn.cleartext.write(prefix + cmd + CRLF); + this._state.conn.cleartext.write(prefix); + this._state.conn.cleartext.write(cmd); + this._state.conn.cleartext.write(CRLF); debug('\n<>: ' + prefix + cmd + '\n'); } }; @@ -994,9 +1011,7 @@ util.inherits(ImapFetch, EventEmitter); /****** Utility Functions ******/ function buildSearchQuery(options, extensions, isOrChild) { - var searchargs = '', - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', - 'Oct', 'Nov', 'Dec']; + var searchargs = ''; for (var i=0,len=options.length; i Date: Mon, 30 Jan 2012 10:26:26 +1100 Subject: [PATCH 3/6] Adds correct respect for contiunation responses following APPEND --- imap.js | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/imap.js b/imap.js index 19b6773..b005d93 100644 --- a/imap.js +++ b/imap.js @@ -414,7 +414,8 @@ ImapConnection.prototype.connect = function(loginCb) { } } } - } else if (data[0][0] === 'A') { // Tagged server response + } else if (data[0][0] === 'A' || + (data[0] === '+' && self._state.requests.length ) ) { // Tagged server response or continutation response var sendBox = false; clearTimeout(self._state.tmrKeepalive); @@ -427,7 +428,7 @@ ImapConnection.prototype.connect = function(loginCb) { self._resetBox(); } } - +debugger; if (self._state.requests[0].command.indexOf('RENAME') > -1) { self._state.box.name = self._state.box._newName; delete self._state.box._newName; @@ -438,7 +439,14 @@ ImapConnection.prototype.connect = function(loginCb) { var err = null; var args = self._state.requests[0].args, cmd = self._state.requests[0].command; - if (data[1] !== 'OK') { + if (data[0] === '+') { + if (cmd.indexOf('APPEND') !== 0) { + err = new Error('Unexpected continuation'); + err.type = 'continuation'; + err.request = cmd; + } else + return self._state.requests[0].callback(); + } else if (data[1] !== 'OK') { err = new Error('Error while executing request: ' + data[2]); err.type = data[1]; err.request = cmd; @@ -466,6 +474,7 @@ ImapConnection.prototype.connect = function(loginCb) { self.capabilities.indexOf('IDLE') > -1) { // According to RFC 2177, we should re-IDLE at least every 29 // minutes to avoid disconnection by the server +debugger; self._send('IDLE', undefined, true); } self._state.tmrKeepalive = setTimeout(function() { @@ -639,17 +648,17 @@ ImapConnection.prototype.append = function(data, options, cb) { cmd += ('0'+(-options.date.getTimezoneOffset() % 60)).slice(-2); cmd += '"'; } - if (data instanceof Buffer) { - cmd += ' {'+data.length+"}\r\n" - cmdbuf = new Buffer(Buffer.byteLength(cmd)+data.length); - cmdbuf.write(cmd); - data.copy(cmdbuf, Buffer.byteLength(cmd)); - this._send(cmdbuf, cb); - } else { - cmd += ' {'+Buffer.byteLength(data.toString())+"}\r\n" - cmd += data.toString(); // Send the command+data as a string - this._send(cmd, cb); - } + cmd += ' {'; + cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); + cmd += '}\r\n'; + var self = this, step = 1; + this._send(cmd, function(err) { + if (err || step++ === 2) + return cb(err); + self._state.conn.cleartext.write(data); + self._state.conn.cleartext.write(CRLF); + debug('\n<>: ' + util.inspect(data.toString()) + '\n'); + }); } ImapConnection.prototype.fetch = function(uids, options) { @@ -976,12 +985,12 @@ ImapConnection.prototype._noop = function() { if (this._state.status >= STATES.AUTH) this._send('NOOP'); }; -ImapConnection.prototype._send = function(cmdobj, cb, bypass) { - if (typeof cmdobj !== 'undefined' && !bypass) - this._state.requests.push({ command: cmdobj, callback: cb, args: [] }); - if ((typeof cmdobj === 'undefined' && this._state.requests.length) || +ImapConnection.prototype._send = function(cmdstr, cb, bypass) { + if (typeof cmdstr !== 'undefined' && !bypass) + this._state.requests.push({ command: cmdstr, callback: cb, args: [] }); + if ((typeof cmdstr === 'undefined' && this._state.requests.length) || this._state.requests.length === 1 || bypass) { - var prefix = '', cmd = (bypass ? cmdobj : this._state.requests[0].command); + var prefix = '', cmd = (bypass ? cmdstr : this._state.requests[0].command); clearTimeout(this._state.tmrKeepalive); if (this._state.ext.idle.sentIdle && cmd !== 'DONE') { this._send('DONE', undefined, true); From 681da750db0a968df8f6de4304d440dd8717f914 Mon Sep 17 00:00:00 2001 From: Andrew Jessup Date: Mon, 30 Jan 2012 11:07:36 +1100 Subject: [PATCH 4/6] Strips out spurious debugger breaks --- imap.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/imap.js b/imap.js index b005d93..e2c5b0b 100644 --- a/imap.js +++ b/imap.js @@ -428,7 +428,6 @@ ImapConnection.prototype.connect = function(loginCb) { self._resetBox(); } } -debugger; if (self._state.requests[0].command.indexOf('RENAME') > -1) { self._state.box.name = self._state.box._newName; delete self._state.box._newName; @@ -474,7 +473,6 @@ debugger; self.capabilities.indexOf('IDLE') > -1) { // According to RFC 2177, we should re-IDLE at least every 29 // minutes to avoid disconnection by the server -debugger; self._send('IDLE', undefined, true); } self._state.tmrKeepalive = setTimeout(function() { From 85ac886d0009006927b8bfabceae2263bf6e3037 Mon Sep 17 00:00:00 2001 From: Andrew Jessup Date: Mon, 30 Jan 2012 20:55:53 +1100 Subject: [PATCH 5/6] Ignores continuation commands due to IDLE --- imap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imap.js b/imap.js index e2c5b0b..c8ec92a 100644 --- a/imap.js +++ b/imap.js @@ -415,7 +415,7 @@ ImapConnection.prototype.connect = function(loginCb) { } } } else if (data[0][0] === 'A' || - (data[0] === '+' && self._state.requests.length ) ) { // Tagged server response or continutation response + (data[0] === '+' && self._state.requests.length && !self._state.isIdle)) { // Tagged server response or continutation response var sendBox = false; clearTimeout(self._state.tmrKeepalive); From 7981e3e0e5ca40c7d9aadec30293246ab557f603 Mon Sep 17 00:00:00 2001 From: Andrew Jessup Date: Mon, 30 Jan 2012 21:26:57 +1100 Subject: [PATCH 6/6] Removes spurious line break in APPEND --- imap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imap.js b/imap.js index c8ec92a..65dbb2a 100644 --- a/imap.js +++ b/imap.js @@ -648,7 +648,7 @@ ImapConnection.prototype.append = function(data, options, cb) { } cmd += ' {'; cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); - cmd += '}\r\n'; + cmd += '}'; var self = this, step = 1; this._send(cmd, function(err) { if (err || step++ === 2)