commit 0d593c4b0c514f27a7fe8e055fa74e4a95945144 Author: Brian Date: Sun Nov 14 00:46:44 2010 -0500 Initial commit. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df05ab1 --- /dev/null +++ b/README.md @@ -0,0 +1,258 @@ +Description +=========== + +node-imap is an IMAP module for [node.js](http://nodejs.org/) that provides an asynchronous interface for communicating with an IMAP mail server. + + +Requirements +============ + +* [node.js](http://nodejs.org/) -- tested with v0.2.4 +* An IMAP server -- tested with gmail + + +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: + + var ImapConnection = require('./imap').ImapConnection, sys = require('sys'), + imap = new ImapConnection({ + username: 'mygmailname@gmail.com', + password: 'mygmailpassword', + host: 'imap.gmail.com', + port: 993, + secure: true + }); + + function die(err) { + console.log('Uh oh: ' + err); + } + + var messages, cmds, next = 0, cb = function(err, box, result) { + if (err) + die(err); + else if (next < cmds.length) + cmds[next++](box, result); + }; + 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); } + ]; + cb(); + + +API +=== + +node-imap exposes one object: **ImapConnection**. + + +#### Data types + +* _Box_ is an Object representing the currently open mailbox, and has the following properties: + * **name** - A String containing the name of 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: + * **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). + * **body** - A String containing the text of the entire or a portion of the message, **if a body was requested when calling fetch().** + * **structure** - An Array containing the structure of the message, **if the structure was requested when calling fetch().** See below for an explanation of the format of this property. + +A message structure with multiple parts might look something like the following: + [ { type: 'mixed' + , params: { boundary: '000e0cd294e80dc84c0475bf339d' } + , disposition: null + , language: null + , location: null + } + , [ { type: 'alternative' + , params: { boundary: '000e0cd294e80dc83c0475bf339b' } + , disposition: null + , language: null + , location: null + } + , [ { partID: '1.1' + , type: + { name: 'text/plain' + , params: { charset: 'ISO-8859-1' } + } + , id: null + , description: null + , encoding: '7BIT' + , size: 935 + , lines: 46 + , md5: null + , disposition: null + , language: null + , location: null + } + ] + , [ { partID: '1.2' + , type: + { name: 'text/html' + , params: { charset: 'ISO-8859-1' } + } + , id: null + , description: null + , encoding: 'QUOTED-PRINTABLE' + , size: 1962 + , lines: 33 + , md5: null + , disposition: null + , language: null + , location: null + } + ] + ] + , [ { partID: '2' + , type: + { name: 'application/octet-stream' + , params: { name: 'somefile' } + } + , id: null + , description: null + , encoding: 'BASE64' + , size: 98 + , lines: null + , md5: null + , disposition: + { type: 'attachment' + , params: { filename: 'somefile' } + } + , language: null + , location: null + } + ] + ] +The above structure describes a message having both an attachment and two forms of the message body (plain text and HTML). +Each message part is identified by a partID which is used when you want to fetch the content of that part (**see fetch()**). + +The structure of a message with only one part will simply look something like this: + [ { partID: '1.1' + , type: + { name: 'text/plain' + , params: { charset: 'ISO-8859-1' } + } + , id: null + , description: null + , encoding: '7BIT' + , size: 935 + , lines: 46 + , md5: null + , disposition: null + , language: null + , location: null + } + ] +Therefore, an easy way to check for a multipart message is to check if the structure length is >1. + + +ImapConnection Events +--------------------- + +* **alert**(String) - Fires when the server issues an alert (e.g. "the server is going down for maintenance"). The supplied String is the text of the alert message. + +* **mail**(Integer) - Fires when new mail arrives in the currently open mailbox. The supplied Integer specifies the number of new messages. + +* **close**(Boolean) - Fires when the connection is completely closed (similar to net.Stream's close event). The specified Boolean indicates whether the connection was terminated due to a transmission error or not. + +* **end**() - Fires when the connection is ended (similar to net.Stream's end event). + +* **error**(Error) - Fires when an exception/error occurs (similar to net.Stream's error event). The given Error object represents the error raised. + + +ImapConnection Functions +------------------------ + +* **(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. + * **host** - A String representing the hostname or IP address of the IMAP server. **Default:** "localhost" + * **port** - An Integer representing the port of the IMAP server. **Default:** 143 + * **secure** - A Boolean indicating the connection should use SSL/TLS. **Default:** false + +* **connect**() - _(void)_ - Attempts to connect and log into the IMAP server. + +* **logout**(Function) - _(void)_ - Closes the connection to the server. The Function parameter is the callback. + +* **openBox**(String, Boolean, Function) - _(void)_ - Opens a specific mailbox that exists on the server. The String parameter is the name of the mailbox to open. The Boolean parameter specifies whether to open the mailbox in read-only mode or not. The Function parameter is the callback with two parameters: the error (null if none), and the _Box_ object of the newly opened mailbox. + +* **closeBox**(Function) - _(void)_ - Closes the currently open mailbox. **Any messages marked as \Deleted in the mailbox will be removed if the mailbox was NOT opened in read-only mode.** Also, logging out or opening another mailbox without closing the current one first will NOT cause deleted messages to be removed. The Function parameter is the callback with one parameter: the error (null if none). + +* **search**(Array, Function) - _(void)_ - Searches the currently open mailbox for messages using specific criteria. The Array parameter is a list of Arrays containing the criteria (and also value(s) for some types of criteria) to be used. Prefix the criteria name with an "!" to negate. For example, to search for unread messages since April 20, 2010: + [ + ['UNSEEN'], + ['SINCE', 'April 20, 2010'] + ] +Criterion that require dates can either be a String parseable by JavaScript's Date object or an instance of a Date. +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. + 'DRAFT' - Messages with the \Draft flag set. + 'FLAGGED' - Messages with the \Flagged flag set. + '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"). + '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. + 'UNFLAGGED' - Messages that do not have the \Flagged flag set. + 'UNSEEN' - Messages that do not have the \Seen flag set. +The following are valid criterion that require String values: + 'BCC' - Messages that contain the specified string in the BCC field. + 'CC' - Messages that contain the specified string in the CC field. + 'FROM' - Messages that contain the specified string in the FROM field. + 'SUBJECT' - Messages that contain the specified string in the SUBJECT field. + 'TO' - Messages that contain the specified string in the TO field. + 'BODY' - Messages that contain the specified string in the message body. + 'TEXT' - Messages that contain the specified string in the header OR the message body. + 'HEADER' - **Requires two String values with the first being the header name and the second being the value to search for.** If this second string is empty, all messages with the given header name will be returned. +The following are valid criterion that require a String parseable by JavaScript's Date object, or an instance of Date: + 'BEFORE' - Messages whose internal date (disregarding time and timezone) is earlier than the specified date. + 'ON' - Messages whose internal date (disregarding time and timezone) is within the specified date. + 'SINCE' - Messages whose internal date (disregarding time and timezone) is within or later than the specified date. + '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: + '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 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 criteria. + +* **fetch**(Integer, Object, Function) - _(void)_ - Fetches the message with the given message ID specified by the Integer parameter in the currently open mailbox. 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 + * **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) whose body you wish to fetch. **Default:** false +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. + + +TODO +---- + +A bunch of things not yet implemented in no particular order: + +* Connection timeout +* 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 additional IMAP commands/extensions: + * APPEND + * EXPUNGE + * STORE + * 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) ? \ No newline at end of file diff --git a/imap.js b/imap.js new file mode 100644 index 0000000..abf8734 --- /dev/null +++ b/imap.js @@ -0,0 +1,894 @@ +var sys = require('sys'), net = require('net'), EventEmitter = require('events').EventEmitter; +var empty = function() {}, CRLF = "\r\n", debug=empty/*sys.debug*/, STATES = { NOCONNECT: 0, NOAUTH: 1, AUTH: 2, BOXSELECTING: 3, BOXSELECTED: 4 }; + +function ImapConnection (options) { + this._options = { + username: '', + password: '', + host: 'localhost', + port: 143, + secure: false + }; + this._state = { + status: STATES.NOCONNECT, + conn: null, + curId: 0, + requests: [], + numCapRecvs: 0, + isReady: false, + isIdle: true, + delim: "/", + tmrKeepalive: null, + 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 }} + }; + this._capabilities = []; + + this._options = extend(true, this._options, options); +}; +sys.inherits(ImapConnection, EventEmitter); +exports.ImapConnection = ImapConnection; + +ImapConnection.prototype.connect = function(loginCb) { + var self = this; + var fnInit = function() { + // First get pre-auth capabilities, including server-supported auth mechanisms + self._send('CAPABILITY', function() { + // Next attempt to login + self._login(function(err) { + if (err) { + loginCb(err); + return; + } + // Lastly, get the mailbox hierarchy delimiter/separator used by the server + self._send('LIST "" ""', loginCb); + }); + }); + }; + this._reset(); + + this._state.conn = net.createConnection(this._options.port, this._options.host); + if (this._options.secure) { + this._state.conn.setSecure(); + this._state.conn.on('secure', function() { + debug('Secure connection made.'); + }); + } + this._state.conn.setKeepAlive(true); + this._state.conn.setEncoding('utf8'); + + this._state.conn.on('connect', function() { + debug('Connected to host.'); + self._state.conn.write(''); + self._state.status = STATES.NOAUTH; + }); + this._state.conn.on('ready', function() { + fnInit(); + }); + this._state.conn.on('data', function(data) { + var literalData; + debug('RECEIVED: ' + data); + + if (data.indexOf(CRLF) === -1) { + if (self._state.curData) + self._state.curData += data; + else + self._state.curData = data; + return; + } + if (self._state.curData) + data = self._state.curData + data; + 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))), + total = parseInt(result[1]); + self._state.fetchData._total = total; + } + if (self._state.fetchData._total > 0) { + if (data.length - (data.indexOf(CRLF)+2) <= self._state.fetchData._total) { + self._state.curData = data; + return; + } + literalData = data.substr(data.indexOf(CRLF)+2, total); + data = data.substr(0, data.indexOf(CRLF)) + data.substr(data.indexOf(CRLF) + 3 + total); + self._state.fetchData._total = 0; + } + + data = data.split(CRLF).filter(isNotEmpty); + + // Defer any extra server responses found in the incoming data + if (data.length > 1) { + data.slice(1).forEach(function(line) { + process.nextTick(function() { self._state.conn.emit('data', line + CRLF); }); + }); + } + data = data[0]; + data = data.explode(' ', 3); + + if (data[0] === '+') { // Continuation + // 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 + 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") { + self._state.conn.end(); + return; + } + if (!self._state.isReady) { + self._state.isReady = true; + self._state.conn.emit('ready'); + } + // Restrict the type of server responses when unauthenticated + if (data[1] !== 'CAPABILITY' || data[1] !== 'ALERT') + return; + } + switch (data[1]) { + case 'CAPABILITY': + if (self._state.numCapRecvs < 2) + self._state.numCapRecvs++; + self._capabilities = data[2].split(' ').map(up); + break; + case 'FLAGS': + if (self._state.status === STATES.BOXSELECTING) + self._state.box._flags = data[2].substr(1, data[2].length-2).split(' '); + case 'OK': + if ((result = /^\[ALERT\] (.*)$/i.exec(data[2])) !== null) + self.emit('alert', result[1]); + else if (self._state.status === STATES.BOXSELECTING) { + var result; + if ((result = /^\[UIDVALIDITY (\d+)\]$/i.exec(data[2])) !== null) + self._state.box._uidvalidity = 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) + self._state.box._permflags = result[1].split(' '); + } + break; + case 'SEARCH': + self._state.box._lastSearch = data[2].split(" "); + break; + case 'LIST': + var result; + if ((result = /^\(\\Noselect\) "(.+)" ""$/.exec(data[2])) !== null) + self._state.delim = result[1]; + break; + default: + if (/^\d+$/.test(data[1])) { + switch (data[2]) { + case 'EXISTS': // mailbox total message count + self._state.box.messages.total = parseInt(data[1]); + break; + case 'RECENT': // messages marked with the \Recent flag (i.e. new messages) + self._state.box.messages.new = parseInt(data[1]); + if (self._state.status !== STATES.BOXSELECTING) + self.emit('mail', self._state.box.messages.new); // new mail notification + 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]))) + 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); + if (literalData.length > 0) { + result = /BODY\[(.*)\] \{[\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 = {}; + for (var i=0,len=headers.length; i= STATES.AUTH; +}; + +ImapConnection.prototype.logout = function(cb) { + if (this._state.status >= STATES.NOAUTH) + this._send('LOGOUT', cb); + else + 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"); + else if (typeof name !== 'string') + name = "INBOX"; + if (this._state.status === STATES.BOXSELECTED) + this._resetBox(); + if (typeof readOnly !== 'boolean') + readOnly = false; + cb = arguments[arguments.length-1]; + this._state.status = STATES.BOXSELECTING; + this._state.box.name = name; + + this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' + escape(name) + '"', 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"); + this._send('CLOSE', function(err) { + if (!err) { + self._state.status = STATES.AUTH; + self._resetBox(); + } + cb(err); + }); +}; + +ImapConnection.prototype.search = function(options, cb) { + if (this._state.status !== STATES.BOXSELECTED) + 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']; + for (var i=0,len=options.length; i 1) + args = criteria.slice(1); + 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); + if (criteria[0] === "!") { + modifier += "NOT "; + criteria = criteria.substr(1); + } + switch(criteria) { + case 'ANSWERED': + case 'DELETED': + case 'DRAFT': + case 'FLAGGED': + case 'NEW': + case 'SEEN': + case 'RECENT': + case 'OLD': + case 'UNANSWERED': + case 'UNDELETED': + case 'UNDRAFT': + case 'UNFLAGGED': + case 'UNSEEN': + searchargs += modifier + criteria; + break; + case 'BCC': + case 'BODY': + case 'CC': + case 'FROM': + case 'SUBJECT': + case 'TEXT': + case 'TO': + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + criteria); + searchargs += modifier + criteria + " \"" + escape(''+args[0]) + "\""; + break; + case 'BEFORE': + case 'ON': + case 'SENTBEFORE': + case 'SENTON': + case 'SENTSINCE': + case 'SINCE': + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + criteria); + else if (!(args[0] instanceof Date)) { + 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(); + 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]; + break;*/ + case 'LARGER': + case 'SMALLER': + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + criteria); + var num = parseInt(args[0]); + if (isNaN(num)) + throw new Error('Search option argument must be a number'); + 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]) + "\""; + break; + default: + throw new Error('Unexpected search option: ' + criteria); + } + } + this._send('UID SEARCH' + searchargs, cb); +}; + +ImapConnection.prototype.fetch = function(uid, options, cb) { + 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'); + var defaults = { + markSeen: false, + request: { + struct: true, + headers: true, // \_______ at most one of these can be used for any given fetch request + body: false // / + } + }, toFetch; + cb = arguments[arguments.length-1]; + if (typeof options !== 'object') + options = {}; + options = extend(true, defaults, options); + + if (!Array.isArray(options.request.headers)) { + 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') + 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); +}; + +/****** Private Functions ******/ + +ImapConnection.prototype._login = function(cb) { + var self = this, + fnReturn = function(err) { + if (!err) { + self._state.status = STATES.AUTH; + if (self._state.numCapRecvs !== 2) { + self._send('CAPABILITY', cb); // fetch post-auth server capabilities if they were not automatically provided after login + return; + } + } + cb(err); + }; + if (this._state.status === STATES.NOAUTH) { + if (typeof this._capabilities.LOGINDISABLED !== 'undefined') { + cb(new Error('Logging in is disabled on this server')); + return; + } + //if (typeof this._capabilities['AUTH=PLAIN'] !== 'undefined') { + this._send('LOGIN "' + escape(this._options.username) + '" "' + escape(this._options.password) + '"', fnReturn); + /*} else { + cb(new Error('Unsupported authentication mechanism(s) detected. Unable to login.')); + return; + }*/ + } +}; +ImapConnection.prototype._reset = function() { + clearTimeout(this._state.tmrKeepalive); + this._state.status = STATES.NOCONNECT; + this._state.numCapRecvs = 0; + this._state.requests = []; + this._capabilities = []; + this._state.isIdle = true; + this._state.isReady = false; + this._state.delim = "/"; + this._resetBox(); + this._resetFetch(); +}; +ImapConnection.prototype._resetBox = function() { + this._state.box._uidnext = 0; + this._state.box._uidvalidity = 0; + this._state.box._flags = []; + this._state.box._permflags = []; + this._state.box._lastSearch = null; + this._state.box.name = null; + 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(); +}; +ImapConnection.prototype._noop = function() { + if (this._state.status >= STATES.AUTH) + this._send('NOOP', undefined, true); +}; +ImapConnection.prototype._send = function(cmdstr, cb, bypass) { + if (arguments.length > 0 && !bypass) + this._state.requests.push({ command: cmdstr, callback: cb }); + if ((arguments.length === 0 && this._state.requests.length > 0) || this._state.requests.length === 1 || bypass) { + clearTimeout(this._state.tmrKeepalive); + this._state.isIdle = false; + var cmd = (bypass ? cmdstr : this._state.requests[0].command); + this._state.conn.write('A' + ++this._state.curId + ' ' + cmd + CRLF); + debug('SENT: A' + this._state.curId + ' ' + cmd); + } +}; + +/****** Utility Functions ******/ + +function parseBodyStructure(str, prefix, partID) { + var retVal = []; + prefix = (prefix !== undefined ? prefix : ''); + partID = (partID !== undefined ? partID : 1); + if (str[0] === '(') { // multipart + var extensionData = { + type: null, // required + params: null, disposition: null, language: null, location: null // optional and may be omitted completely + }; + // Recursively parse each part + while (str[0] === '(') { + var inQuote = false, + countParen = 0, + lastIndex = -1; + for (var i=1,len=str.length; i 0) { + if (str[0] === '(') { + var isKey = true, key; + str = str.substr(1); + extensionData.params = {}; + while (str[0] !== ')') { + lastIndex = getLastIdxQuoted(str); + if (isKey) + key = str.substring(1, lastIndex).toLowerCase(); + else + extensionData.params[key] = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + isKey = !isKey; + } + str = str.substr(1).trim(); + } else + str = str.substr(4); + + // [disposition] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + extensionData.disposition = { type: null, params: null }; + str = str.substr(1); + lastIndex = getLastIdxQuoted(str); + extensionData.disposition.type = str.substring(1, lastIndex).toLowerCase(); + str = str.substr(lastIndex+1).trim(); + if (str[0] === '(') { + var isKey = true, key; + str = str.substr(1); + extensionData.disposition.params = {}; + while (str[0] !== ')') { + lastIndex = getLastIdxQuoted(str); + if (isKey) + key = str.substring(1, lastIndex).toLowerCase(); + else + extensionData.disposition.params[key] = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + isKey = !isKey; + } + str = str.substr(2).trim(); + } + } else + str = str.substr(4); + + // [language] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + extensionData.language = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + + // [location] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + extensionData.location = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + } + } + } + } + + retVal.unshift(extensionData); + } else { // single part + var part = { + partID: (prefix !== '' ? prefix : '1'), // the path identifier for this part, useful for fetching specific parts of a message + type: { name: null, params: null }, // content type and parameters (NIL or otherwise) + id: null, description: null, encoding: null, size: null, lines: null, // required -- NIL or otherwise + md5: null, disposition: null, language: null, location: null // optional extension data that may be omitted entirely + }, + lastIndex = getLastIdxQuoted(str), + contentTypeMain = str.substring(1, lastIndex), + contentTypeSub; + str = str.substr(lastIndex+1).trim(); + lastIndex = getLastIdxQuoted(str); + contentTypeSub = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + + // content type + part.type.name = contentTypeMain.toLowerCase() + "/" + contentTypeSub.toLowerCase(); + + // content type parameters + if (str[0] === '(') { + var isKey = true, key; + str = str.substr(1); + part.type.params = {}; + while (str[0] !== ')') { + lastIndex = getLastIdxQuoted(str); + if (isKey) + key = str.substring(1, lastIndex).toLowerCase(); + else + part.type.params[key] = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + isKey = !isKey; + } + str = str.substr(2); + } else + str = str.substr(4); + + // content id + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + part.id = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + + // content description + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + part.description = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + + // content encoding + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + part.encoding = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + + // size of content encoded in bytes + if (str.substr(0, 3) !== "NIL") { + lastIndex = 0; + while (str.charCodeAt(lastIndex) >= 48 && str.charCodeAt(lastIndex) <= 57) + lastIndex++; + part.size = parseInt(str.substring(0, lastIndex)); + str = str.substr(lastIndex).trim(); + } else + str = str.substr(4); + + // [# of lines] + 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++; + part.lines = parseInt(str.substring(0, lastIndex)); + str = str.substr(lastIndex).trim(); + } else + str = str.substr(4); + } + + // [md5 hash of content] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + part.md5 = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + + // [disposition] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + part.disposition = { type: null, params: null }; + str = str.substr(1); + lastIndex = getLastIdxQuoted(str); + part.disposition.type = str.substring(1, lastIndex).toLowerCase(); + str = str.substr(lastIndex+1).trim(); + if (str[0] === '(') { + var isKey = true, key; + str = str.substr(1); + part.disposition.params = {}; + while (str[0] !== ')') { + lastIndex = getLastIdxQuoted(str); + if (isKey) + key = str.substring(1, lastIndex).toLowerCase(); + else + part.disposition.params[key] = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + isKey = !isKey; + } + str = str.substr(2).trim(); + } + } else + str = str.substr(4); + + // [language] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + if (str[0] === '(') { + part.language = []; + str = str.substr(1); + while (str[0] !== ')') { + lastIndex = getLastIdxQuoted(str); + part.language.push(str.substring(1, lastIndex)); + str = str.substr(lastIndex+1).trim(); + } + } else { + lastIndex = getLastIdxQuoted(str); + part.language = [str.substring(1, lastIndex)]; + str = str.substr(lastIndex+1).trim(); + } + } else + str = str.substr(4); + + // [location] + if (str.length > 0) { + if (str.substr(0, 3) !== "NIL") { + lastIndex = getLastIdxQuoted(str); + part.location = str.substring(1, lastIndex); + str = str.substr(lastIndex+1).trim(); + } else + str = str.substr(4); + } + } + } + } + + retVal.push(part); + } + return retVal; +} + +String.prototype.explode = function(delimiter, limit) { + if (arguments.length < 2 || arguments[0] === undefined || arguments[1] === undefined || + !delimiter || delimiter === '' || typeof delimiter === 'function' || typeof delimiter === 'object') + return false; + + delimiter = (delimiter === true ? '1' : delimiter.toString()); + + if (!limit || limit === 0) + return this.split(delimiter); + else if (limit < 0) + return false; + else if (limit > 0) { + var splitted = this.split(delimiter); + var partA = splitted.splice(0, limit - 1); + var partB = splitted.join(delimiter); + partA.push(partB); + return partA; + } + + return false; +} + +function isNotEmpty(str) { + return str.trim().length > 0; +} + +function escape(str) { + return str.replace('\\', '\\\\').replace('"', '\"'); +} + +function unescape(str) { + return str.replace('\"', '"').replace('\\\\', '\\'); +} + +function up(str) { + return str.toUpperCase(); +} + +function getLastIdxQuoted(str) { + var index = -1, countQuote = 0; + for (var i=0,len=str.length; i 0 && str[i-1] === "\\") + continue; + countQuote++; + } + if (countQuote === 2) { + index = i; + break; + } + } + return index; +} + +/** + * Adopted from jquery's extend method. Under the terms of MIT License. + * + * http://code.jquery.com/jquery-1.4.2.js + * + * Modified by Brian White to use Array.isArray instead of the custom isArray method + */ +function extend() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !typeof target === 'function') { + target = {}; + } + + var isPlainObject = function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) + return false; + + var has_own_constructor = hasOwnProperty.call(obj, "constructor"); + var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf"); + // Not own constructor property must be Object + if ( obj.constructor && !has_own_constructor && !has_is_property_of_method) + return false; + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var last_key; + for ( key in obj ) + last_key = key; + + return typeof last_key === "undefined" || hasOwnProperty.call( obj, last_key ); + }; + + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) !== null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object literal values or arrays + if ( deep && copy && ( isPlainObject(copy) || Array.isArray(copy) ) ) { + var clone = src && ( isPlainObject(src) || Array.isArray(src) ) ? src : Array.isArray(copy) ? [] : {}; + + // Never move original objects, clone them + target[ name ] = extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( typeof copy !== "undefined" ) + target[ name ] = copy; + } + } + } + + // Return the modified object + return target; +}; \ No newline at end of file