diff --git a/README.md b/README.md index e5294a0..f1f4b0f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,16 @@ Description node-imap is an IMAP client module for [node.js](http://nodejs.org/). This module does not perform any magic such as auto-decoding of messages/attachments or parsing of email addresses (node-imap leaves all mail header values as-is). -If you are in need of this kind of extra functionality, check out andris9's [mimelib](https://github.com/andris9/mimelib) module. Also check out his [mailparser](http://github.com/andris9/mailparser) module, which comes in handy after you fetch() a raw email message with this module and wish to parse it manually. Requirements ============ -* [node.js](http://nodejs.org/) -- v0.6.0 or newer +* [node.js](http://nodejs.org/) -- v0.8.0 or newer + + * NOTE: node v0.8.x users are supported via the readable-stream module which + may not be up-to-date (compared to node v0.10 streams2 implementation) + * An IMAP server -- tested with gmail @@ -33,48 +36,57 @@ var imap = new Imap({ password: 'mygmailpassword', host: 'imap.gmail.com', port: 993, - secure: true + secure: true, + secureOptions: { rejectUnauthorized: false } }); -function show(obj) { - return inspect(obj, false, Infinity); -} +imap.once('ready', function() { -function die(err) { - console.log('Uh oh: ' + err); - process.exit(1); -} +}); + +imap.once('error', function(err) { + console.log(err); +}); + +imap.once('end', function() { + console.log('Connection ended'); +}); function openInbox(cb) { - imap.connect(function(err) { - if (err) die(err); - imap.openBox('INBOX', true, cb); - }); + imap.openBox('INBOX', true, cb); } -openInbox(function(err, mailbox) { - if (err) die(err); +openInbox(function(err, box) { + if (err) throw err; imap.search([ 'UNSEEN', ['SINCE', 'May 20, 2010'] ], function(err, results) { - if (err) die(err); - imap.fetch(results, - { headers: ['from', 'to', 'subject', 'date'], - cb: function(fetch) { - fetch.on('message', function(msg) { - console.log('Saw message no. ' + msg.seqno); - msg.on('headers', function(hdrs) { - console.log('Headers for no. ' + msg.seqno + ': ' + show(hdrs)); - }); - msg.on('end', function() { - console.log('Finished message no. ' + msg.seqno); - }); - }); - } - }, function(err) { - if (err) throw err; - console.log('Done fetching all messages!'); - imap.logout(); - } - ); + if (err) throw err; + var f = imap.fetch(results, { bodies: 'HEADER.FIELDS (FROM TO SUBJECT DATE)' }); + f.on('message', function(msg, seqno) { + console.log('Message #%d', seqno); + var prefix = '(#' + seqno + ') '; + msg.on('body', function(stream, info) { + var buffer = ''; + stream.on('data', function(chunk) { + buffer += chunk.toString('utf8'); + }); + stream.once('end', function() { + console.log(prefix + 'Parsed header: %s', inspect(Imap.parseHeader(buffer))); + }); + }); + msg.once('attributes', function(attrs) { + console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8)); + }); + msg.once('end', function() { + console.log(prefix + 'Finished'); + }); + }); + f.on('error', function(err) { + console.log('Fetch error: ' + err); + }); + f.on('end', function() { + console.log('Done fetching all messages!'); + imap.end(); + }); }); }); ``` @@ -84,36 +96,46 @@ openInbox(function(err, mailbox) { ```javascript // using the functions and variables already defined in the first example ... -openInbox(function(err, mailbox) { - if (err) die(err); - imap.seq.fetch(mailbox.messages.total + ':*', { struct: false }, - { headers: 'from', - body: true, - cb: function(fetch) { - fetch.on('message', function(msg) { - console.log('Saw message no. ' + msg.seqno); - var body = ''; - msg.on('headers', function(hdrs) { - console.log('Headers for no. ' + msg.seqno + ': ' + show(hdrs)); - }); - msg.on('data', function(chunk) { - body += chunk.toString('utf8'); - }); - msg.on('end', function() { - console.log('Finished message no. ' + msg.seqno); - console.log('UID: ' + msg.uid); - console.log('Flags: ' + msg.flags); - console.log('Date: ' + msg.date); - console.log('Body: ' + show(body)); - }); +openInbox(function(err, box) { + if (err) throw err; + imap.search([ 'UNSEEN', ['SINCE', 'May 20, 2010'] ], function(err, results) { + if (err) throw err; + var f = imap.fetch(results, { bodies: ['HEADER.FIELDS (FROM)','TEXT'] }); + f.on('message', function(msg, seqno) { + console.log('Message #%d', seqno); + var prefix = '(#' + seqno + ') '; + msg.on('body', function(stream, info) { + if (info.which === 'TEXT') + console.log(prefix + 'Body [%s] found, %d total bytes', inspect(info.which), info.size); + var buffer = '', count = 0; + stream.on('data', function(chunk) { + count += chunk.length; + buffer += chunk.toString('utf8'); + if (info.which === 'TEXT') + console.log(prefix + 'Body [%s] (%d/%d)', inspect(info.which), count, info.size); }); - } - }, function(err) { - if (err) throw err; + stream.once('end', function() { + if (info.which !== 'TEXT') + console.log(prefix + 'Parsed header: %s', inspect(Imap.parseHeader(buffer))); + else + console.log(prefix + 'Body [%s] Finished', inspect(info.which)); + }); + }); + msg.once('attributes', function(attrs) { + console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8)); + }); + msg.once('end', function() { + console.log(prefix + 'Finished'); + }); + }); + f.on('error', function(err) { + console.log('Fetch error: ' + err); + }); + f.on('end', function() { console.log('Done fetching all messages!'); - imap.logout(); - } - ); + imap.end(); + }); + }); }); ``` @@ -124,29 +146,32 @@ openInbox(function(err, mailbox) { var fs = require('fs'), fileStream; -openInbox(function(err, mailbox) { - if (err) die(err); +openInbox(function(err, box) { + if (err) throw err; imap.search([ 'UNSEEN', ['SINCE', 'May 20, 2010'] ], function(err, results) { - if (err) die(err); - imap.fetch(results, - { headers: { parse: false }, - body: true, - cb: function(fetch) { - fetch.on('message', function(msg) { - console.log('Got a message with sequence number ' + msg.seqno); - fileStream = fs.createWriteStream('msg-' + msg.seqno + '-body.txt'); - msg.on('data', function(chunk) { - fileStream.write(chunk); - }); - msg.on('end', function() { - fileStream.end(); - console.log('Finished message no. ' + msg.seqno); - }); - }); - } - }, function(err) { - } - ); + if (err) throw err; + var f = imap.fetch(results, { bodies: '' }); + f.on('message', function(msg, seqno) { + console.log('Message #%d', seqno); + var prefix = '(#' + seqno + ') '; + msg.on('body', function(stream, info) { + console.log(prefix + 'Body'); + stream.pipe(fs.createWriteStream('msg-' + seqno + '-body.txt')); + }); + msg.once('attributes', function(attrs) { + console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8)); + }); + msg.once('end', function() { + console.log(prefix + 'Finished'); + }); + }); + f.on('error', function(err) { + console.log('Fetch error: ' + err); + }); + f.on('end', function() { + console.log('Done fetching all messages!'); + imap.end(); + }); }); }); ``` @@ -155,15 +180,12 @@ openInbox(function(err, mailbox) { API === -require('imap') returns one object: **ImapConnection**. - - #### Data types * _Box_ is an object representing the currently open mailbox, and has the following properties: * **name** - < _string_ > - The name of this mailbox. - * **readOnly** - < _boolean_ > - True if this mailbox was opened in read-only mode. - * **uidvalidity** - < _integer_ > - A 32-bit number that can be used to determine if UIDs in this mailbox have changed since the last time this mailbox was opened. It is possible for this to change during a session, in which case a 'uidvalidity' event will be emitted on the ImapConnection instance. + * **readOnly** - < _boolean_ > - True if this mailbox was opened in read-only mode. **(Only available with openBox() calls)** + * **uidvalidity** - < _integer_ > - A 32-bit number that can be used to determine if UIDs in this mailbox have changed since the last time this mailbox was opened. * **uidnext** - < _integer_ > - The uid that will be assigned to the next message that arrives at this mailbox. * **permFlags** - < _array_ > - A list of flags that can be permanently added/removed to/from messages in this mailbox. * **messages** - < _object_ > Contains various message counts for this mailbox: @@ -171,19 +193,22 @@ require('imap') returns one object: **ImapConnection**. * **new** - < _integer_ > - Number of messages in this mailbox having the Recent flag (this IMAP session is the first to see these messages). * **unseen** - < _integer_ > - **(Only available with status() calls)** Number of messages in this mailbox not having the Seen flag (marked as not having been read). * _ImapMessage_ is an object representing an email message. It consists of: - * Properties: - * **seqno** - < _integer_ > - This message's sequence number. This number changes when messages with smaller sequence numbers are deleted for example (see the ImapConnection's 'deleted' event). This value is **always** available immediately. - * **uid** - < _integer_ > - A 32-bit ID that uniquely identifies this message within its mailbox. - * **flags** - < _array_ > - A list of flags currently set on this message. - * **date** - < _string_ > - The internal server date for the message (always represented in GMT?) - * **structure** - < _array_ > - The structure of the message, **if the structure was requested with fetch().** See below for an explanation of the format of this property. - * **size** - < _integer_ > - The RFC822 message size, **if the size was requesting with fetch().** * Events: - * **headers**(< _mixed_ >headers) - Emitted when headers are fetched. This is an _object_ unless 'parse' is set to false when requesting headers, in which case it will be a _Buffer_. Note: if you request a full raw message (all headers and entire body), only 'data' events will be emitted. - * **data**(< _Buffer_ >chunk) - Emitted for each message body chunk if a message body is being fetched. - * **end**() - Emitted when the fetch is complete for this message. -* _ImapFetch_ is an object that emits these events: - * **message**(< _ImapMessage_ >msg) - Emitted for each message resulting from a fetch request + * **body**(< _ReadableStream_ >stream, < _object_ >info) - Emitted for each requested body. Example `info` properties: + * **which** - < _string_ > - The specifier for this body (e.g. 'TEXT', 'HEADER.FIELDS (TO FROM SUBJECT)', etc). + * **size** - < _integer_ > - The size of this body in bytes. + * **attributes**(< _object_ >attrs) - Emitted when all message attributes have been collected. Example properties: + * **uid** - < _integer_ > - A 32-bit ID that uniquely identifies this message within its mailbox. + * **flags** - < _array_ > - A list of flags currently set on this message. + * **date** - < _string_ > - The internal server date for the message (always represented in GMT?) + * **struct** - < _array_ > - The message's body structure **(only set if requested with fetch())**. See below for an explanation of the format of this property. + * **size** - < _integer_ > - The RFC822 message size **(only set if requested with fetch())**. + * **end**() - Emitted when all attributes and bodies have been parsed. +* _ImapFetch_ is an object representing a fetch() request. It consists of: + * Events: + * **message**(< _ImapMessage_ >msg, < _integer_ >seqno) - Emitted for each message resulting from a fetch request. `seqno` is the message's sequence number. + * **error**(< _Error_ >err) - Emitted when an error occurred. + * **end**() - Emitted when all messages have been parsed. A message structure with multiple parts might look something like the following: @@ -281,40 +306,37 @@ Lastly, here are the system flags defined by RFC3501 that may be added/removed: * 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_. +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** first. +Additional custom flags may be provided by the server. If available, these will also be listed in the mailbox's **permFlags**. -ImapConnection Events ---------------------- +require('imap') returns one object: **Connection**. + + +Connection Events +----------------- -* **alert**(< _string_ >alertMsg) - Emitted when the server issues an alert (e.g. "the server is going down for maintenance"). +* **ready**() - Emitted when a connection to the server has been made and authentication was successful. + +* **alert**(< _string_ >message) - Emitted when the server issues an alert (e.g. "the server is going down for maintenance"). * **mail**(< _integer_ >numNewMsgs) - Emitted when new mail arrives in the currently open mailbox. * **deleted**(< _integer_ >seqno) - Emitted when a message is deleted from another IMAP connection's session. `seqno` is the sequence number (instead of the unique UID) of the message that was deleted. If you are caching sequence numbers, all sequence numbers higher than this value **MUST** be decremented by 1 in order to stay synchronized with the server and to keep correct continuity. -* **msgupdate**(< _ImapMessage_ >msg) - Emitted when a message's flags have changed, generally from another IMAP connection's session. With that in mind, the only available ImapMessage properties in this case will almost always only be 'seqno' and 'flags' (no 'data' or 'end' events will be emitted on the object). - -* **uidvalidity**(< _integer_ >uidvalidity) - Emitted when the UID validity value has changed for the currently open mailbox. Any UIDs previously stored for this mailbox are now invalidated. +* **error**(< _Error_ >err) - Emitted when an error occurs. The 'source' property will be set to indicate where the error originated from. * **close**(< _boolean_ >hadError) - Emitted when the connection has completely closed. * **end**() - Emitted when the connection has ended. -* **error**(< _Error_ >err) - Emitted when an exception/error occurs. - - -ImapConnection Properties -------------------------- - -* **connected** - _boolean_ - Are we connected? -* **authenticated** - _boolean_ - Are we authenticated? +Connection Properties +--------------------- -* **capabilities** - _array_ - Contains the IMAP capabilities of the server. +* **state** - _string_ - The current state of the connection (e.g. 'disconnected', 'connected', 'authenticated'). -* **delimiter** - _string_ - The (top-level) mailbox hierarchy delimiter. If the server does not support mailbox hierarchies and only a flat list, this value will be `false`. +* **delimiter** - _string_ - The (top-level) mailbox hierarchy delimiter. If the server does not support mailbox hierarchies and only a flat list, this value will be falsey. * **namespaces** - _object_ - Contains information about each namespace type (if supported by the server) with the following properties: @@ -341,12 +363,18 @@ ImapConnection Properties ``` -ImapConnection Functions ------------------------- +Connection Static Methods +------------------------- + +* **parseHeader**(< _string_ >rawHeader) - _object_ - Attempts to parse the raw header into an object keyed on header fields and the values are Arrays of values. + + +Connection Instance Methods +--------------------------- **Note:** Message UID ranges are not guaranteed to be contiguous. -* **(constructor)**([< _object_ >config]) - _ImapConnection_ - Creates and returns a new instance of _ImapConnection_ using the specified configuration object. Valid config properties are: +* **(constructor)**([< _object_ >config]) - _Connection_ - Creates and returns a new instance of _Connection_ using the specified configuration object. Valid config properties are: * **user** - < _string_ > - Username for plain-text authentication. @@ -362,13 +390,17 @@ ImapConnection Functions * **secure** - < _boolean_ > - Use SSL/TLS? **Default:** false + * **secureOptions** - < _object_ > - Options object to pass to tls.connect() **Default:** (none) + * **connTimeout** - < _integer_ > - Number of milliseconds to wait for a connection to be established. **Default:** 10000 + * **keepalive** - < _boolean_ > - Enable the keepalive mechnanism. **Default:** true + * **debug** - < _function_ > - If set, the function will be called with one argument, a string containing some debug info **Default:** -* **connect**(< _function_ >callback) - _(void)_ - Attempts to connect and log into the IMAP server. `callback` has 1 parameter: < _Error_ >err. +* **connect**() - _(void)_ - Attempts to connect and authenticate with the IMAP server. -* **logout**(< _function_ >callback) - _(void)_ - Logs out and closes the connection to the server. `callback` has 1 parameter: < _Error_ >err. +* **end**() - _(void)_ - Closes the connection to the server. * **openBox**(< _string_ >mailboxName[, < _boolean_ >openReadOnly=false], < _function_ >callback) - _(void)_ - Opens a specific mailbox that exists on the server. `mailboxName` should include any necessary prefix/path. `callback` has 2 parameters: < _Error_ >err, < _Box_ >mailbox. @@ -380,7 +412,7 @@ ImapConnection Functions * **renameBox**(< _string_ >oldMailboxName, < _string_ >newMailboxName, < _function_ >callback) - _(void)_ - Renames a specific mailbox that exists on the server. Both `oldMailboxName` and `newMailboxName` should include any necessary prefix/path. `callback` has 2 parameters: < _Error_ >err, < _Box_ >mailbox. **Note:** Renaming the 'INBOX' mailbox will instead cause all messages in 'INBOX' to be moved to the new mailbox. -* **status**(< _string_ >mailboxName, < _function_ >callback) - _(void)_ - Fetches information about a mailbox other than the one currently open. `callback` has 2 parameters: < _Error_ >err, < _Box_ >mailbox. **Note:** There is no guarantee that this will be a fast operation on the server. Also, do *not* call this on the currently open mailbox. +* **status**(< _string_ >mailboxName, < _function_ >callback) - _(void)_ - Fetches information about a mailbox other than the one currently open. `callback` has 2 parameters: < _Error_ >err, < _Box_ >mailbox. **Note:** There is no guarantee that this will be a fast operation on the server. Also, do **not** call this on the currently open mailbox. * **getBoxes**([< _string_ >nsPrefix,] < _function_ >callback) - _(void)_ - Obtains the full list of mailboxes. If `nsPrefix` is not specified, the main personal namespace is used. `callback` has 2 parameters: < _Error_ >err, < _object_ >boxes. `boxes` has the following format (with example values): @@ -404,37 +436,43 @@ ImapConnection Functions delimiter: '/', children: { 'All Mail': - { attribs: [], + { attribs: [ 'All' ], delimiter: '/', children: null, parent: [Circular] }, Drafts: - { attribs: [], + { attribs: [ 'Drafts' ], + delimiter: '/', + children: null, + parent: [Circular] + }, + Important: + { attribs: [ 'Important' ], delimiter: '/', children: null, parent: [Circular] }, 'Sent Mail': - { attribs: [], + { attribs: [ 'Sent' ], delimiter: '/', children: null, parent: [Circular] }, Spam: - { attribs: [], + { attribs: [ 'Junk' ], delimiter: '/', children: null, parent: [Circular] }, Starred: - { attribs: [], + { attribs: [ 'Flagged' ], delimiter: '/', children: null, parent: [Circular] }, Trash: - { attribs: [], + { attribs: [ 'Trash' ], delimiter: '/', children: null, parent: [Circular] @@ -551,7 +589,7 @@ ImapConnection Functions `callback` has 2 parameters: < _Error_ >err, < _array_ >UIDs. -* **fetch**(< _mixed_ >source, [< _object_ >options, ] < _mixed_ >request, < _function_ >callback) - _(void)_ - Fetches message(s) in the currently open mailbox. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. +* **fetch**(< _mixed_ >source, [< _object_ >options]) - _ImapFetch_ - Fetches message(s) in the currently open mailbox. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. Valid `options` properties are: @@ -561,28 +599,15 @@ ImapConnection Functions * **size** - < _boolean_ > - Fetch the RFC822 size. **Default:** false - `request` is an _object_ or an _array_ of _object_ with the following valid properties: - - * **id** - < _mixed_ > - _integer_ or _string_ referencing a message part to use when retrieving headers and/or a body. **Default:** (root part/entire message) - - * **headers** - < _mixed_ > - An _array_ of specific headers to retrieve, a _string_ containing a single header to retrieve, _boolean_ true to fetch all headers, or an _object_ of the form (**Default:** (no headers)): - - * **fields** - < _mixed_ > - An _array_ of specific headers to retrieve or _boolean_ true to fetch all headers. **Default:** (all headers) - - * **parse** - < _boolean_ > - Parse headers? **Default:** true - - * **headersNot** - < _mixed_ > - An _array_ of specific headers to exclude, a _string_ containing a single header to exclude, or an _object_ of the form (**Default:** (no headers)): - - * **fields** - < _mixed_ > - An _array_ of specific headers to exclude. **Default:** (all headers) - - * **parse** - < _boolean_ > - Parse headers? **Default:** true - - * **body** - < _boolean_ > - _boolean_ true to fetch the body - - * **cb** - < _function_ > - A callback that is passed an _ImapFetch_ object. - - `callback` has 1 parameter: < _Error_ >err. This is executed when all message retrievals are complete. - + * **bodies** - < _mixed_ > - A string or Array of strings containing body part section to fetch. **Default:** (none) Example sections: + + * 'HEADER' - The message header + * 'HEADER.FIELDS(TO FROM SUBJECT)' - Specific header fields only + * 'HEADER.FIELDS.NOT(TO FROM SUBJECT)' - Header fields only that do not match the fields given + * 'TEXT' - The message body + * '' - The entire message (header + body) + * 'MIME' - MIME-related header fields only (e.g. 'Content-Type') + * **copy**(< _mixed_ >source, < _string_ >mailboxName, < _function_ >callback) - _(void)_ - Copies message(s) in the currently open mailbox to another mailbox. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `callback` has 1 parameter: < _Error_ >err. * **move**(< _mixed_ >source, < _string_ >mailboxName, < _function_ >callback) - _(void)_ - Moves message(s) in the currently open mailbox to another mailbox. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `callback` has 1 parameter: < _Error_ >err. **Note:** The message(s) in the destination mailbox will have a new message UID. @@ -595,6 +620,8 @@ ImapConnection Functions * **delKeywords**(< _mixed_ >source, < _mixed_ >keywords, < _function_ >callback) - _(void)_ - Removes keyword(s) from message(s). `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `keywords` is either a single keyword or an _array_ of keywords. `callback` has 1 parameter: < _Error_ >err. +* **serverSupports**(< _string_ >capability) - _boolean_ - Checks if the server supports the specified capability. + Extensions Supported -------------------- @@ -613,21 +640,21 @@ Extensions Supported * X-GM-LABELS: string value which allows you to search for specific messages that have the given label applied - * fetch() will automatically retrieve the thread id, unique message id, and labels (named 'x-gm-thrid', 'x-gm-msgid', 'x-gm-labels' respectively) and they will be stored on the _ImapMessage_ object itself + * fetch() will automatically retrieve the thread id, unique message id, and labels (named 'x-gm-thrid', 'x-gm-msgid', 'x-gm-labels' respectively) - * Additional ImapConnection functions + * Additional Connection functions - * **setLabels**(< _mixed_ >source, < _mixed_ >labels, < _function_ >callback) - _(void)_ - Replaces labels(s) of message(s). `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `labels` is either a single label or an _array_ of labels. `callback` has 1 parameter: < _Error_ >err. + * **setLabels**(< _mixed_ >source, < _mixed_ >labels, < _function_ >callback) - _(void)_ - Replaces labels of message(s) with `labels`. `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `labels` is either a single label or an _array_ of labels. `callback` has 1 parameter: < _Error_ >err. - * **addLabels**(< _mixed_ >source, < _mixed_ >labels, < _function_ >callback) - _(void)_ - Adds labels(s) to message(s). `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `labels` is either a single label or an _array_ of labels. `callback` has 1 parameter: < _Error_ >err. + * **addLabels**(< _mixed_ >source, < _mixed_ >labels, < _function_ >callback) - _(void)_ - Adds `labels` to message(s). `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `labels` is either a single label or an _array_ of labels. `callback` has 1 parameter: < _Error_ >err. - * **delLabels**(< _mixed_ >source, < _mixed_ >labels, < _function_ >callback) - _(void)_ - Removes labels(s) from message(s). `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `labels` is either a single label or an _array_ of labels. `callback` has 1 parameter: < _Error_ >err. + * **delLabels**(< _mixed_ >source, < _mixed_ >labels, < _function_ >callback) - _(void)_ - Removes `labels` from message(s). `source` can be a message UID, a message UID range (e.g. '2504:2507' or '\*' or '2504:\*'), or an _array_ of message UIDs and/or message UID ranges. `labels` is either a single label or an _array_ of labels. `callback` has 1 parameter: < _Error_ >err. * **SORT** * Server capability: SORT - * Additional ImapConnection functions + * Additional Connection functions * **sort**(< _array_ >sortCriteria, < _array_ >searchCriteria, < _function_ >callback) - _(void)_ - Performs a sorted search(). A seqno-based counterpart also exists for this function. `callback` has 2 parameters: < _Error_ >err, < _array_ >UIDs. Valid `sortCriteria` are (reverse sorting of individual criteria is done by prefixing the criteria with '-'): diff --git a/lib/Connection.js b/lib/Connection.js new file mode 100644 index 0000000..f2d2546 --- /dev/null +++ b/lib/Connection.js @@ -0,0 +1,1321 @@ +var tls = require('tls'), + Socket = require('net').Socket, + EventEmitter = require('events').EventEmitter, + inherits = require('util').inherits, + inspect = require('util').inspect, + isDate = require('util').isDate, + utf7 = require('utf7').imap; + +var Parser = require('./Parser').Parser; + +var MAX_INT = 9007199254740992, + KEEPALIVE_INTERVAL = 10000, + MAX_IDLE_WAIT = 300000, // 5 minutes + MONTHS = ['Jan', 'Feb', 'Mar', + 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec'], + FETCH_ATTR_MAP = { + 'RFC822.SIZE': 'size', + 'BODY': 'struct', + 'BODYSTRUCTURE': 'struct', + 'UID': 'uid', + 'INTERNALDATE': 'date', + 'FLAGS': 'flags', + 'X-GM-THRID': 'x-gm-thrid', + 'X-GM-MSGID': 'x-gm-msgid', + 'X-GM-LABELS': 'x-gm-labels' + }, + CRLF = '\r\n', + RE_CMD = /^([^ ]+)(?: |$)/, + RE_UIDCMD_HASRESULTS = /^UID (?:FETCH|SEARCH|SORT)/, + RE_IDLENOOPRES = /^(IDLE|NOOP) /, + RE_OPENBOX = /^EXAMINE|SELECT$/, + RE_BODYPART = /^BODY\[/, + RE_INVALID_KW_CHARS = /[\(\)\{\\\"\]\%\*\x00-\x20\x7F]/, + RE_NUM_RANGE = /^(?:[\d]+|\*):(?:[\d]+|\*)$/, + RE_BACKSLASH = /\\/g, + RE_BACKSLASH_ESC = /\\\\/g, + RE_DBLQUOTE = /"/g, + RE_DBLQUOTE_ESC = /\\"/g, + RE_INTEGER = /^\d+$/; + +function Connection(config) { + if (!(this instanceof Connection)) + return new Connection(config); + + EventEmitter.call(this); + + config || (config = {}); + + this._config = { + host: config.host || 'localhost', + port: config.port || 143, + secure: (config.secure === true ? 'implicit' : config.secure), + secureOptions: config.secureOptions, + user: config.user, + password: config.password, + connTimeout: config.connTimeout || 10000, + keepalive: (typeof config.keepalive === 'boolean' + ? config.keepalive + : true) + }; + + this._sock = undefined; + this._tagcount = 0; + this._tmrConn = undefined; + this._queue = []; + this._box = undefined; + this._idle = {}; + this.delimiter = undefined; + this.namespaces = undefined; + this.state = 'disconnected'; + this.debug = config.debug; +} +inherits(Connection, EventEmitter); + +Connection.prototype.connect = function() { + var config = this._config, self = this, socket, tlsSocket, parser, tlsOptions; + + socket = new Socket(); + socket.setKeepAlive(true); + socket.setTimeout(0); + this._state = 'disconnected'; + + if (config.secure) { + tlsOptions = {}; + for (var k in config.secureOptions) + tlsOptions[k] = config.secureOptions[k]; + tlsOptions.socket = socket; + } + + if (config.secure === 'implicit') + this._sock = tlsSocket = tls.connect(tlsOptions, onconnect); + else { + socket.once('connect', onconnect); + this._sock = socket; + } + + function onconnect() { + clearTimeout(self._tmrConn); + self.state = 'connected'; + self.debug&&self.debug('[connection] Connected to host'); + } + + this._sock.once('error', function(err) { + clearTimeout(self._tmrConn); + clearTimeout(self._tmrKeepalive); + self.debug&&self.debug('[connection] Error: ' + err); + err.source = 'socket'; + self.emit('error', err); + }); + + socket.once('close', function(had_err) { + clearTimeout(self._tmrConn); + clearTimeout(self._tmrKeepalive); + self.debug&&self.debug('[connection] Closed'); + self.emit('close', had_err); + }); + + socket.once('end', function() { + clearTimeout(self._tmrConn); + clearTimeout(self._tmrKeepalive); + self.debug&&self.debug('[connection] Ended'); + self.emit('end'); + }); + + parser = new Parser(this._sock, this.debug); + + parser.on('untagged', function(info) { + self._resUntagged(info); + }); + parser.on('tagged', function(info) { + self._resTagged(info); + }); + parser.on('body', function(stream, info) { + var msg = self._curReq.fetchCache[info.seqno], toget; + + if (msg === undefined) { + msg = self._curReq.fetchCache[info.seqno] = { + msgEmitter: new EventEmitter(), + toget: self._curReq.fetching.slice(0), + attrs: {}, + ended: false + }; + + self._curReq.bodyEmitter.emit('message', msg.msgEmitter, info.seqno); + } + + toget = msg.toget; + + var idx = toget.indexOf('BODY[' + info.which + ']'); + if (idx > -1) { + toget.splice(idx, 1); + msg.msgEmitter.emit('body', stream, info); + } else + stream.resume(); // a body we didn't ask for? + }); + parser.on('continue', function(info) { + // only needed for IDLE and APPEND + var type = self._curReq.type; + if (type === 'IDLE') { + // now idling + self._idle.started = Date.now(); + } else if (/^AUTHENTICATE XOAUTH/.test(self._curReq.fullcmd)) { + self._curReq.oauthError = new Buffer(info.text, 'base64').toString('utf8'); + self._sock.write(CRLF); + } else if (type === 'APPEND') + self._sock.write(self._curReq.appendData); + }); + parser.on('other', function(line) { + var m; + if (m = RE_IDLENOOPRES.exec(line)) { + if (m[1] === 'IDLE') { + // no longer idling + self._idle.enabled = false; + self._idle.started = undefined; + } + + self._curReq = undefined; + + if (self._queue.length === 0 + && self._config.keepalive + && self.state === 'authenticated') { + self._idle.enabled = true; + self._doKeepaliveTimer(true); + } + + self._processQueue(); + } + }); + + this._tmrConn = setTimeout(function() { + var err = new Error('Connection timed out'); + err.source = 'timeout'; + self.emit('error', err); + socket.destroy(); + }, config.connTimeout); + + socket.connect(config.port, config.host); +}; + +Connection.prototype.serverSupports = function(cap) { + return (this._caps && this._caps.indexOf(cap) > -1); +}; + +Connection.prototype.end = function() { + this._sock.end(); +}; + +Connection.prototype.append = function(data, options, cb) { + if (typeof options === 'function') { + cb = options; + options = undefined; + } + options = options || {}; + if (!options.mailbox) { + if (!this._box) + throw new Error('No mailbox specified or currently selected'); + else + options.mailbox = this._box.name; + } + var cmd = 'APPEND "' + escape(utf7.encode(''+options.mailbox)) + '"'; + if (options.flags) { + if (!Array.isArray(options.flags)) + options.flags = [options.flags]; + if (options.flags.length > 0) + cmd += ' (\\' + options.flags.join(' \\') + ')'; + } + if (options.date) { + if (!isDate(options.date)) + throw new Error('`date` is not a Date object'); + cmd += ' "'; + cmd += options.date.getDate(); + cmd += '-'; + cmd += MONTHS[options.date.getMonth()]; + cmd += '-'; + cmd += options.date.getFullYear(); + cmd += ' '; + cmd += ('0' + options.date.getHours()).slice(-2); + cmd += ':'; + cmd += ('0' + options.date.getMinutes()).slice(-2); + cmd += ':'; + cmd += ('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 += '"'; + } + cmd += ' {'; + cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); + cmd += '}'; + + this._enqueue(cmd, cb); + this._queue[this._queue.length - 1].appendData = data; +}; + +Connection.prototype.getBoxes = function(namespace, cb) { + if (typeof namespace === 'function') { + cb = namespace; + namespace = ''; + } + + namespace = escape(utf7.encode(''+namespace)); + + this._enqueue('LIST "' + namespace + '" "*"', cb); +}; + +Connection.prototype.openBox = function(name, readOnly, cb) { + if (this.state !== 'authenticated') + throw new Error('Not authenticated'); + + if (cb === undefined) { + cb = readOnly; + readOnly = false; + } + + name = ''+name; + var encname = escape(utf7.encode(name)), + cmd = (readOnly ? 'EXAMINE' : 'SELECT'), + self = this; + + this._enqueue(cmd + ' "' + encname + '"', function(err) { + if (err) { + self._box = undefined; + cb(err); + } else { + self._box.name = name; + cb(err, self._box); + } + }); +}; + +// also deletes any messages in this box marked with \Deleted +Connection.prototype.closeBox = function(cb) { + var self = this; + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + + this._enqueue('CLOSE', function(err) { + if (!err) + self._box = undefined; + + cb(err); + }); +}; + +Connection.prototype.addBox = function(name, cb) { + this._enqueue('CREATE "' + escape(utf7.encode(''+name)) + '"', cb); +}; + +Connection.prototype.delBox = function(name, cb) { + this._enqueue('DELETE "' + escape(utf7.encode(''+name)) + '"', cb); +}; + +Connection.prototype.renameBox = function(oldname, newname, cb) { + var destname = newname; + if (this._box + && oldname === this._box.name + && oldname.toUpperCase() !== 'INBOX') + destname = ''+oldname; + + var encoldname = escape(utf7.encode(''+oldname)), + encnewname = escape(utf7.encode(''+newname)), + self = this; + + this._enqueue('RENAME "' + encoldname + '" "' + encnewname + '"', + function(err) { + if (err) + return cb(err); + self._box.name = destname; + cb(err, self._box); + } + ); +}; + +Connection.prototype.status = function(boxName, cb) { + if (this._box && this._box.name === boxName) + throw new Error('Cannot call status on currently selected mailbox'); + + boxName = escape(utf7.encode(''+boxName)); + + this._enqueue('STATUS "' + boxName + '" (MESSAGES RECENT UNSEEN UIDVALIDITY)', + cb); +}; + +Connection.prototype.removeDeleted = function(uids, cb) { + if (typeof uids === 'function') { + cb = uids; + uids = undefined; + } + + if (uids !== undefined) { + if (!Array.isArray(uids)) + uids = [uids]; + + validateUIDList(uids); + uids = uids.join(','); + + this._enqueue('UID EXPUNGE ' + uids, cb); + } else + this._enqueue('EXPUNGE', cb); +}; + +Connection.prototype.search = function(options, cb) { + this._search('UID ', options, cb); +}; + +Connection.prototype._search = function(which, options, cb) { + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + else if (!Array.isArray(options)) + throw new Error('Expected array for search options'); + + this._enqueue(which + 'SEARCH' + buildSearchQuery(options, this._caps), cb); +}; + +Connection.prototype.sort = function(sorts, options, cb) { + this._sort('UID ', sorts, options, cb); +}; + +Connection.prototype._sort = function(which, sorts, options, cb) { + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + else if (!Array.isArray(sorts) || !sorts.length) + throw new Error('Expected array with at least one sort criteria'); + else if (!Array.isArray(options)) + throw new Error('Expected array for search options'); + else if (!this.serverSupports('SORT')) + throw new Error('Sort is not supported on the server'); + + var criteria = sorts.map(function(c) { + if (typeof c !== 'string') + throw new Error('Unexpected sort criteria data type. ' + + 'Expected string. Got: ' + typeof criteria); + + var modifier = ''; + if (c[0] === '-') { + modifier = 'REVERSE '; + c = c.substring(1); + } + switch (c.toUpperCase()) { + case 'ARRIVAL': + case 'CC': + case 'DATE': + case 'FROM': + case 'SIZE': + case 'SUBJECT': + case 'TO': + break; + default: + throw new Error('Unexpected sort criteria: ' + c); + } + + return modifier + c; + }); + + this._enqueue(which + 'SORT (' + criteria.join(' ') + ') UTF-8' + + buildSearchQuery(options, this._caps), cb); +}; + +Connection.prototype.addFlags = function(uids, flags, cb) { + this._store('UID ', uids, flags, true, cb); +}; + +Connection.prototype.delFlags = function(uids, flags, cb) { + this._store('UID ', uids, flags, false, cb); +}; + +Connection.prototype.addKeywords = function(uids, flags, cb) { + this._addKeywords('UID ', uids, flags, cb); +}; + +Connection.prototype._addKeywords = function(which, uids, flags, cb) { + if (this._box && !this._box.newKeywords) + throw new Error('This mailbox does not allow new keywords to be added'); + this._store(which, uids, flags, true, cb); +}; + +Connection.prototype.delKeywords = function(uids, flags, cb) { + this._store('UID ', uids, flags, false, cb); +}; + +Connection.prototype._store = function(which, uids, flags, isAdding, cb) { + var isKeywords = (arguments.callee.caller === this._addKeywords + || arguments.callee.caller === this.delKeywords); + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + else if (uids === undefined) + throw new Error('No messages specified'); + + if (!Array.isArray(uids)) + uids = [uids]; + validateUIDList(uids); + + 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)) + flags = [flags]; + for (var i = 0, len = flags.length; i < len; ++i) { + if (!isKeywords) { + if (flags[i][0] === '\\') + flags[i] = flags[i].substr(1); + if (this._state.box.permFlags.indexOf(flags[i].toLowerCase()) === -1 + || flags[i] === '*') + throw new Error('The flag "' + flags[i] + + '" is not allowed by the server for this mailbox'); + flags[i] = '\\' + flags[i]; + } else { + // keyword contains any char except control characters (%x00-1F and %x7F) + // and: '(', ')', '{', ' ', '%', '*', '\', '"', ']' + if (RE_INVALID_KW_CHARS.test(flags[i])) { + throw new Error('The keyword "' + flags[i] + + '" contains invalid characters'); + } + } + } + + flags = flags.join(' '); + uids = uids.join(','); + + + this._enqueue(which + 'STORE ' + uids + ' ' + (isAdding ? '+' : '-') + + 'FLAGS.SILENT (' + flags + ')', cb); +}; + +Connection.prototype.setLabels = function(uids, labels, cb) { + this._storeLabels('UID ', uids, labels, '', cb); +}; + +Connection.prototype.addLabels = function(uids, labels, cb) { + this._storeLabels('UID ', uids, labels, '+', cb); +}; + +Connection.prototype.delLabels = function(uids, labels, cb) { + this._storeLabels('UID ', uids, labels, '-', cb); +}; + +Connection.prototype._storeLabels = function(which, uids, labels, mode, cb) { + if (!this.serverSupports('X-GM-EXT-1')) + throw new Error('Server must support X-GM-EXT-1 capability'); + else if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + else if (uids === undefined) + throw new Error('No messages specified'); + + if (!Array.isArray(uids)) + uids = [uids]; + validateUIDList(uids); + + if ((!Array.isArray(labels) && typeof labels !== 'string') + || (Array.isArray(labels) && labels.length === 0)) + throw new Error('labels argument must be a string or a non-empty Array'); + + if (!Array.isArray(labels)) + labels = [labels]; + labels = labels.map(function(v) { + return '"' + escape(utf7.encode(''+v)) + '"'; + }).join(' '); + + uids = uids.join(','); + + this._enqueue(which + 'STORE ' + uids + ' ' + mode + + 'X-GM-LABELS.SILENT (' + labels + ')', cb); +}; + +Connection.prototype.copy = function(uids, boxTo, cb) { + this._copy('UID ', uids, boxTo, cb); +}; + +Connection.prototype._copy = function(which, uids, boxTo, cb) { + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + + if (!Array.isArray(uids)) + uids = [uids]; + + validateUIDList(uids); + boxTo = escape(utf7.encode(''+boxTo)); + + this._enqueue(which + 'COPY ' + uids.join(',') + ' "' + + boxTo + '"', cb); +}; + +Connection.prototype.move = function(uids, boxTo, cb) { + this._move('UID ', uids, boxTo, cb); +}; + +Connection.prototype._move = function(which, uids, boxTo, cb) { + if (this._box === undefined) + throw new Error('No mailbox is currently selected'); + + if (this.serverSupports('MOVE')) { + if (!Array.isArray(uids)) + uids = [uids]; + + validateUIDList(uids); + uids = uids.join(','); + boxTo = escape(utf7.encode(''+boxTo)); + + this._enqueue(which + 'MOVE ' + uids + ' "' + boxTo + '"', cb); + } else if (this._box.permFlags.indexOf('deleted') === -1) { + throw new Error('Cannot move message: ' + + 'server does not allow deletion of messages'); + } else { + var deletedUIDs, task = 0, self = this; + this._copy(which, uids, boxTo, function ccb(err, info) { + if (err) + return cb(err, info); + + if (task === 0 && which && self.serverSupports('UIDPLUS')) { + // UIDPLUS gives us a 'UID EXPUNGE n' command to expunge a subset of + // messages with the \Deleted flag set. This allows us to skip some + // actions. + task = 2; + } + // Make sure we don't expunge any messages marked as Deleted except the + // one we are moving + if (task === 0) { + self.search(['DELETED'], function(e, result) { + ++task; + deletedUIDs = result; + ccb(e, info); + }); + } else if (task === 1) { + if (deletedUIDs.length) { + self.delFlags(deletedUIDs, 'Deleted', function(e) { + ++task; + ccb(e, info); + }); + } else { + ++task; + ccb(err, info); + } + } else if (task === 2) { + function cbMarkDel(e) { + ++task; + ccb(e, info); + } + if (which) + self.addFlags(uids, 'Deleted', cbMarkDel); + else + self.seq.addFlags(uids, 'Deleted', cbMarkDel); + } else if (task === 3) { + if (which && self.serverSupports('UIDPLUS')) + self.removeDeleted(uids, cb); + else { + self.removeDeleted(function(e) { + ++task; + ccb(e, info); + }); + } + } else if (task === 4) { + if (deletedUIDs.length) { + self.addFlags(deletedUIDs, 'Deleted', function(e) { + cb(e, info); + }); + } else + cb(err, info); + } + }); + } +}; + +Connection.prototype.fetch = function(uids, options) { + this._fetch('UID ', uids, options); +}; + +Connection.prototype._fetch = function(which, uids, options) { + if (uids === undefined + || uids === null + || (Array.isArray(uids) && uids.length === 0)) + throw new Error('Nothing to fetch'); + + if (!Array.isArray(uids)) + uids = [uids]; + validateUIDList(uids); + uids = uids.join(','); + + var cmd = which + 'FETCH ' + uids + ' (', fetching = []; + + // always fetch GMail-specific bits of information when on GMail + if (this.serverSupports('X-GM-EXT-1')) { + fetching.push('X-GM-THRID'); + fetching.push('X-GM-MSGID'); + fetching.push('X-GM-LABELS'); + } + + fetching.push('UID'); + fetching.push('FLAGS'); + fetching.push('INTERNALDATE'); + + if (options) { + if (options.struct) + fetching.push('BODYSTRUCTURE'); + if (options.size) + fetching.push('RFC822.SIZE'); + cmd += fetching.join(' '); + if (options.bodies !== undefined) { + var bodies = options.bodies, + prefix = (options.markSeen ? '' : '.PEEK'); + if (!Array.isArray(bodies)) + bodies = [bodies]; + for (var i = 0, len = bodies.length; i < len; ++i) { + fetching.push('BODY[' + bodies[i] + ']'); + cmd += ' BODY' + prefix + '[' + bodies[i] + ']'; + } + } + } else + cmd += fetching.join(' '); + + cmd += ')'; + + this._enqueue(cmd); + var req = this._queue[this._queue.length - 1]; + req.fetchCache = {}; + req.fetching = fetching; + return (req.bodyEmitter = new EventEmitter()); +}; + +// Namespace for seqno-based commands +Connection.prototype.__defineGetter__('seq', function() { + var self = this; + return { + move: function(seqnos, boxTo, cb) { + self._move('', seqnos, boxTo, cb); + }, + copy: function(seqnos, boxTo, cb) { + self._copy('', seqnos, boxTo, cb); + }, + delKeywords: function(seqnos, flags, cb) { + self._store('', seqnos, flags, false, cb); + }, + addKeywords: function(seqnos, flags, cb) { + self._addKeywords('', seqnos, flags, cb); + }, + delFlags: function(seqnos, flags, cb) { + self._store('', seqnos, flags, false, cb); + }, + addFlags: function(seqnos, flags, cb) { + self._store('', seqnos, flags, true, cb); + }, + delLabels: function(seqnos, labels, cb) { + self._storeLabels('', seqnos, labels, '-', cb); + }, + addLabels: function(seqnos, labels, cb) { + self._storeLabels('', seqnos, labels, '+', cb); + }, + setLabels: function(seqnos, labels, cb) { + self._storeLabels('', seqnos, labels, '', cb); + }, + fetch: function(seqnos, options, what, cb) { + return self._fetch('', seqnos, options, what, cb); + }, + search: function(options, cb) { + self._search('', options, cb); + }, + sort: function(sorts, options, cb) { + self._sort('', sorts, options, cb); + } + }; +}); + +Connection.prototype._resUntagged = function(info) { + var type = info.type; + + if (type === 'bye') + this._sock.end(); + else if (type === 'namespace') + this.namespaces = info.text; + else if (type === 'capability') + this._caps = info.text.map(function(v) { return v.toUpperCase(); }); + else if (type === 'preauth') + this.state = 'authenticated'; + else if (type === 'search' || type === 'sort') + this._curReq.cbargs.push(info.text); + else if (type === 'recent') { + if (!this._box && RE_OPENBOX.test(this._curReq.type)) + this._createCurrentBox(); + this._box.messages.new = info.num; + } + else if (type === 'flags') { + if (!this._box && RE_OPENBOX.test(this._curReq.type)) + this._createCurrentBox(); + this._box.flags = info.text; + } else if (type === 'bad' || type === 'no') { + if (this.state === 'connected' && !this._curReq) { + clearTimeout(this._tmrConn); + var err = new Error('Received negative welcome: ' + info.text); + err.level = 'protocol'; + this.emit('error', err); + this._sock.end(); + } + } else if (type === 'exists') { + if (!this._box && RE_OPENBOX.test(this._curReq.type)) + this._createCurrentBox(); + var prev = this._box.messages.total, + now = info.num; + this._box.messages.total = now; + if (now > prev && this.state === 'authenticated') { + this._box.messages.new = now - prev; + this.emit('mail', this._box.messages.total); + } + } else if (type === 'expunge') { + if (this._box.messages.total > 0) + --this._box.messages.total; + if (!this._curReq) + this.emit('deleted', info.num); + } else if (type === 'ok') { + if (this.state === 'connected' && !this._curReq) + this._login(); + else if (typeof info.textCode === 'string' + && info.textCode.toUpperCase() === 'ALERT') + this.emit('alert', info.text); + else if (this._curReq + && typeof info.textCode === 'object' + && (RE_OPENBOX.test(this._curReq.type))) { + // we're opening a mailbox + + if (!this._box) + this._createCurrentBox(); + + var key = info.textCode.key.toUpperCase(); + + if (key === 'UIDVALIDITY') + this._box.uidvalidity = info.textCode.val; + else if (key === 'UIDNEXT') + this._box.uidnext = info.textCode.val; + else if (key === 'PERMANENTFLAGS') { + var idx, permFlags, keywords; + this._box.permFlags = permFlags = info.textCode.val; + if ((idx = this._box.permFlags.indexOf('\\*')) > -1) { + this._box.newKeywords = true; + permFlags.splice(idx, 1); + } + this._box.keywords = keywords = permFlags.filter(function(f) { + return (f[0] !== '\\'); + }); + for (var i = 0, len = keywords.length; i < len; ++i) + permFlags.splice(permFlags.indexOf(keywords[i]), 1); + this._box.permFlags = permFlags.map(function(f) { + return f.substr(1).toLowerCase(); + }); + } + } + } else if (type === 'list') { + if (this.delimiter === undefined) + this.delimiter = info.text.delimiter; + else { + if (this._curReq.cbargs.length === 0) + this._curReq.cbargs.push({}); + + var box = { + attribs: info.text.flags.map(function(attr) { + return attr.substr(1); + }).filter(function(attr) { + return (attr.toUpperCase() !== 'HASNOCHILDREN'); + }), + delimiter: info.text.delimiter, + children: null, + parent: null + }, + name = info.text.name, + curChildren = this._curReq.cbargs[0]; + + if (box.delimiter) { + var path = name.split(box.delimiter), + parent = null; + name = path.pop(); + for (var i = 0, len = path.length; i < len; ++i) { + if (!curChildren[path[i]]) + curChildren[path[i]] = {}; + if (!curChildren[path[i]].children) + curChildren[path[i]].children = {}; + parent = curChildren[path[i]]; + curChildren = curChildren[path[i]].children; + } + box.parent = parent; + } + if (curChildren[name]) + box.children = curChildren[name].children; + curChildren[name] = box; + } + } else if (type === 'status') { + var box = { + name: info.text.name, + uidvalidity: 0, + messages: { + total: 0, + new: 0, + unseen: 0 + } + }, attrs = info.text.attrs; + + if (attrs) { + if (attrs.recent !== undefined) + box.messages.new = attrs.recent; + if (attrs.unseen !== undefined) + box.messages.unseen = attrs.unseen; + if (attrs.messages !== undefined) + box.messages.total = attrs.messages; + if (attrs.uidvalidity !== undefined) + box.uidvalidity = attrs.uidvalidity; + } + this._curReq.cbargs.push(box); + } else if (type === 'fetch') { + var msg = this._curReq.fetchCache[info.num], + keys = Object.keys(info.text), + keyslen = keys.length, + attrs, toget, msgEmitter, i, j; + + if (msg === undefined) { + // simple case -- no bodies were streamed + toget = this._curReq.fetching.slice(0); + if (toget.length === 0) + return; + + msgEmitter = new EventEmitter(); + attrs = {}; + + this._curReq.bodyEmitter.emit('message', msgEmitter, info.num); + } else { + toget = msg.toget; + msgEmitter = msg.msgEmitter; + attrs = msg.attrs; + } + + i = toget.length; + if (i === 0) { + if (!msg.ended) { + msg.ended = true; + msgEmitter.emit('end'); + } + return; + } + + if (keyslen > 0) { + while (--i >= 0) { + j = keyslen; + while (--j >= 0) { + if (keys[j].toUpperCase() === toget[i]) { + if (!RE_BODYPART.test(toget[i])) + attrs[FETCH_ATTR_MAP[toget[i]]] = info.text[keys[j]]; + toget.splice(i, 1); + break; + } + } + } + } + + if (toget.length === 0) { + msgEmitter.emit('attributes', attrs); + msgEmitter.emit('end'); + } else if (msg === undefined) { + this._curReq.fetchCache[info.num] = { + msgEmitter: msgEmitter, + toget: toget, + attrs: attrs, + ended: false + }; + } + } +}; + +Connection.prototype._resTagged = function(info) { + var req = this._curReq, err; + + this._curReq = undefined; + + if (info.type === 'no' || info.type === 'bad') { + var errtext; + if (/^AUTHENTICATE XOAUTH/.test(req.fullcmd) && req.oauthError) + errtext = req.oauthError; + else + errtext = info.text; + var err = new Error(errtext); + err.textCode = info.textCode; + err.source = 'protocol'; + } else if (this._box) { + if (req.type === 'EXAMINE' || req.type === 'SELECT') + this._box.readOnly = (info.textCode.toUpperCase() === 'READ-ONLY'); + + // According to RFC 3501, 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. + if (RE_UIDCMD_HASRESULTS.test(req.fullcmd) && req.cbargs.length === 0) + req.cbargs.push([]); + } + + if (req.bodyEmitter) { + if (err) + req.bodyEmitter.emit('error', err); + req.bodyEmitter.emit('end'); + } else { + req.cbargs.unshift(err); + req.cb && req.cb.apply(this, req.cbargs); + } + + if (this._queue.length === 0 + && this._config.keepalive + && this.state === 'authenticated') { + this._idle.enabled = true; + this._doKeepaliveTimer(true); + } + + this._processQueue(); +}; + +Connection.prototype._createCurrentBox = function() { + this._box = { + name: '', + flags: [], + readOnly: false, + uidvalidity: 0, + uidnext: 0, + permFlags: [], + keywords: [], + newKeywords: false, + messages: { + total: 0, + new: 0 + } + }; +}; + +Connection.prototype._doKeepaliveTimer = function(immediate) { + var self = this, + timerfn = function() { + if (self._idle.enabled) { + // unlike NOOP, IDLE is only a valid command after authenticating + if (!self.serverSupports('IDLE') || self.state !== 'authenticated') + self._enqueue('NOOP', true); + else { + if (self._idle.started === undefined) { + self._idle.started = 0; + self._enqueue('IDLE', true); + } else if (self._idle.started > 0) { + var timeDiff = Date.now() - self._idle.started; + if (timeDiff >= MAX_IDLE_WAIT) { + self._idle.enabled = false; + self.debug && self.debug('=> DONE'); + self._sock.write('DONE' + CRLF); + return; + } + } + self._doKeepaliveTimer(); + } + } + }; + + if (immediate) + timerfn(); + else + this._tmrKeepalive = setTimeout(timerfn, KEEPALIVE_INTERVAL); +}; + +Connection.prototype._login = function() { + var self = this, checkedNS = false; + + var reentry = function(err) { + if (err) { + self.emit('error', err); + return self._sock.destroy(); + } + + // 2. Get the list of available namespaces (RFC2342) + if (!checkedNS && self.serverSupports('NAMESPACE')) { + checkedNS = true; + return self._enqueue('NAMESPACE', reentry); + } + + // 3. Get the top-level mailbox hierarchy delimiter used by the server + self._enqueue('LIST "" ""', function() { + self.state = 'authenticated'; + self.emit('ready'); + }); + }; + + // 1. Get the supported capabilities + self._enqueue('CAPABILITY', function() { + // No need to attempt the login sequence if we're on a PREAUTH connection. + if (self.state === 'connected') { + var err, + checkCaps = function(error) { + if (error) { + error.source = 'authentication'; + return reentry(error); + } + + if (self._caps === undefined) { + // Fetch server capabilities if they were not automatically + // provided after authentication + return self._enqueue('CAPABILITY', reentry); + } else + reentry(); + }; + + if (self.serverSupports('LOGINDISABLED')) { + err = new Error('Logging in is disabled on this server'); + err.source = 'authentication'; + return reentry(err); + } + + if (self.serverSupports('AUTH=XOAUTH') && self._config.xoauth) { + self._caps = undefined; + self._enqueue('AUTHENTICATE XOAUTH ' + escape(self._config.xoauth), + checkCaps); + } else if (self.serverSupports('AUTH=XOAUTH2') && self._config.xoauth2) { + self._caps = undefined; + self._enqueue('AUTHENTICATE XOAUTH2 ' + escape(self._config.xoauth2), + checkCaps); + } else if (self._config.user && self._config.password) { + self._caps = undefined; + self._enqueue('LOGIN "' + escape(self._config.user) + '" "' + + escape(self._config.password) + '"', checkCaps); + } else { + err = new Error('No supported authentication method(s) available. ' + + 'Unable to login.'); + err.source = 'authentication'; + return reentry(err); + } + } else + reentry(); + }); +}; + +Connection.prototype._processQueue = function() { + if (this._curReq || !this._queue.length || !this._sock.writable) + return; + + this._curReq = this._queue.shift(); + + if (this._tagcount === MAX_INT) + this._tagcount = 0; + + var prefix; + + if (this._curReq.type === 'IDLE' || this._curReq.type === 'NOOP') + prefix = this._curReq.type; + else + prefix = 'A' + (this._tagcount++); + + var out = prefix + ' ' + this._curReq.fullcmd; + this.debug && this.debug('=> ' + inspect(out)); + this._sock.write(out + CRLF); +}; + +Connection.prototype._enqueue = function(fullcmd, promote, cb) { + if (typeof promote === 'function') { + cb = promote; + promote = false; + } + + var info = { + type: fullcmd.match(RE_CMD)[1], + fullcmd: fullcmd, + cb: cb, + cbargs: [] + }, self = this; + + if (promote) + this._queue.unshift(info); + else + this._queue.push(info); + + if (!this._curReq) { + // defer until next tick for requests like APPEND where access to the + // request object is needed immediately after enqueueing + process.nextTick(function() { self._processQueue(); }); + } else if (this._curReq.type === 'IDLE') { + this._idle.enabled = false; + this.debug && this.debug('=> DONE'); + this._sock.write('DONE' + CRLF); + } +}; + +module.exports = Connection; + +// utilities ------------------------------------------------------------------- + +function escape(str) { + return str.replace(RE_BACKSLASH, '\\\\').replace(RE_DBLQUOTE, '\\"'); +} +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 (RE_NUM_RANGE.test(uids[i])) + continue; + } + intval = parseInt(''+uids[i], 10); + if (isNaN(intval)) { + throw new Error('Message ID/number must be an integer, "*", or a range: ' + + uids[i]); + } else if (typeof uids[i] !== 'number') + uids[i] = intval; + } +} +function buildSearchQuery(options, extensions, isOrChild) { + var searchargs = ''; + for (var i = 0, len = options.length; i < len; ++i) { + var criteria = (isOrChild ? options : options[i]), + args = null, + modifier = (isOrChild ? '' : ' '); + if (typeof criteria === 'string') + criteria = criteria.toUpperCase(); + else if (Array.isArray(criteria)) { + if (criteria.length > 1) + args = criteria.slice(1); + if (criteria.length > 0) + criteria = criteria[0].toUpperCase(); + } else + 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'); + searchargs += ' OR ('; + searchargs += buildSearchQuery(args[0], extensions, true); + searchargs += ') ('; + searchargs += buildSearchQuery(args[1], extensions, true); + searchargs += ')'; + } else { + if (criteria[0] === '!') { + modifier += 'NOT '; + criteria = criteria.substr(1); + } + switch(criteria) { + // -- Standard criteria -- + case 'ALL': + 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 string'); + } + 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], 10); + 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; + case 'UID': + if (!args) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + validateUIDList(args); + searchargs += modifier + criteria + ' ' + args.join(','); + break; + // -- Extensions criteria -- + case 'X-GM-MSGID': // Gmail unique message ID + case 'X-GM-THRID': // Gmail thread ID + if (extensions.indexOf('X-GM-EXT-1') === -1) + throw new Error('IMAP extension not available: ' + criteria); + var val; + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + else { + val = ''+args[0]; + if (!(RE_INTEGER.test(args[0]))) + throw new Error('Invalid value'); + } + searchargs += modifier + criteria + ' ' + val; + break; + case 'X-GM-RAW': // Gmail search syntax + if (extensions.indexOf('X-GM-EXT-1') === -1) + throw new Error('IMAP extension not available: ' + criteria); + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + searchargs += modifier + criteria + ' "' + escape(''+args[0]) + + '"'; + break; + case 'X-GM-LABELS': // Gmail labels + if (extensions.indexOf('X-GM-EXT-1') === -1) + throw new Error('IMAP extension not available: ' + criteria); + if (!args || args.length !== 1) + throw new Error('Incorrect number of arguments for search option: ' + + criteria); + searchargs += modifier + criteria + ' ' + args[0]; + break; + default: + try { + // last hope it's a seqno set + // http://tools.ietf.org/html/rfc3501#section-6.4.4 + var seqnos = (args ? [criteria].concat(args) : [criteria]); + validateUIDList(seqnos); + searchargs += modifier + seqnos.join(','); + } catch(e) { + throw new Error('Unexpected search option: ' + criteria); + } + } + } + if (isOrChild) + break; + } + return searchargs; +} diff --git a/lib/Parser.js b/lib/Parser.js new file mode 100644 index 0000000..e5f521d --- /dev/null +++ b/lib/Parser.js @@ -0,0 +1,582 @@ +var EventEmitter = require('events').EventEmitter, + ReadableStream = require('stream').Readable || require('readable-stream'), + inherits = require('util').inherits, + inspect = require('util').inspect, + utf7 = require('utf7').imap; + +var CH_LF = 10, + LITPLACEHOLDER = String.fromCharCode(0), + EMPTY_READCB = function(n) {}, + RE_INTEGER = /^\d+$/, + RE_PRECEDING = /^(?:\*|A\d+|\+) /, + RE_BODYLITERAL = /BODY\[(.*)\] \{(\d+)\}$/i, + RE_SEQNO = /^\* (\d+)/, + RE_LISTCONTENT = /^\((.*)\)$/, + RE_LITERAL = /\{(\d+)\}$/, + RE_UNTAGGED = /^\* (?:(OK|NO|BAD|BYE|FLAGS|LIST|LSUB|SEARCH|STATUS|CAPABILITY|NAMESPACE|PREAUTH|SORT)|(\d+) (EXPUNGE|FETCH|RECENT|EXISTS))(?: (?:\[([^\]]+)\] )?(.+))?$/i, + RE_TAGGED = /^A(\d+) (OK|NO|BAD) (?:\[([^\]]+)\] )?(.+)$/i, + RE_CONTINUE = /^\+ (?:\[([^\]]+)\] )?(.+)$/i; + +function Parser(stream, debug) { + if (!(this instanceof Parser)) + return new Parser(stream, debug); + + EventEmitter.call(this); + + this._stream = stream; + this._body = undefined; + this._literallen = 0; + this._literals = []; + this._buffer = ''; + this.debug = debug; + + var self = this; + function cb() { + if (self._literallen > 0) + self._tryread(self._literallen); + else + self._tryread(); + } + this._stream.on('readable', cb); + process.nextTick(cb); +} +inherits(Parser, EventEmitter); + +Parser.prototype._tryread = function(n) { + var r = this._stream.read(n); + r && this._parse(r); +}; + +Parser.prototype._parse = function(data) { + var i = 0, datalen = data.length, idxlf; + + if (this._literallen > 0) { + if (this._body) { + var body = this._body; + if (datalen > this._literallen) { + var litlen = this._literallen; + + i = this._literallen; + this._literallen = 0; + this._body = undefined; + + body.push(data.slice(0, litlen)); + } else { + this._literallen -= datalen; + var r = body.push(data); + if (!r && this._literallen > 0) + return; + i = datalen; + } + if (this._literallen === 0) { + body._read = EMPTY_READCB; + body.push(null); + } + } else { + if (datalen > this._literallen) + this._literals.push(data.slice(0, this._literallen)); + else + this._literals.push(data); + i = this._literallen; + this._literallen = 0; + } + } + + while (i < datalen) { + idxlf = indexOfCh(data, datalen, i, CH_LF); + if (idxlf === -1) { + this._buffer += data.toString('utf8'); + break; + } else { + this._buffer += data.toString('utf8', i, idxlf); + this._buffer = this._buffer.trim(); + i = idxlf + 1; + + this.debug && this.debug('<= ' + inspect(this._buffer)); + + var clearBuffer = false; + + if (RE_PRECEDING.test(this._buffer)) { + var firstChar = this._buffer[0]; + if (firstChar === '*') + clearBuffer = this._resUntagged(); + else if (firstChar === 'A') + clearBuffer = this._resTagged(); + else if (firstChar === '+') + clearBuffer = this._resContinue(); + + if (this._literallen > 0 && i < datalen) { + // literal data included in this chunk -- put it back onto stream + this._stream.unshift(data.slice(i)); + i = datalen; + if (!this._body && this._literallen > 0) { + // check if unshifted contents satisfies non-body literal length + this._tryread(this._literallen); + } + } + } else { + this.emit('other', this._buffer); + clearBuffer = true; + } + + if (clearBuffer) + this._buffer = ''; + } + } + + if (this._literallen === 0 || this._body) + this._tryread(); +}; + +Parser.prototype._resTagged = function() { + var ret = false; + if (m = RE_LITERAL.exec(this._buffer)) { + // non-BODY literal -- buffer it + this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER); + this._literallen = parseInt(m[1], 10); + this._tryread(this._literallen); + } else { + var m = RE_TAGGED.exec(this._buffer), + tagnum = parseInt(m[1], 10), + type = m[2].toLowerCase(), + textCode = (m[3] ? parseExpr(m[3], this._literals) : m[3]), + text = m[4]; + + this._literals = []; + + this.emit('tagged', { + type: type, + tagnum: tagnum, + textCode: textCode, + text: text + }); + + ret = true; + } + + return ret; +}; + +Parser.prototype._resUntagged = function() { + var ret = false, self = this, m; + if (m = RE_BODYLITERAL.exec(this._buffer)) { + // BODY literal -- stream it + var which = m[1], size = parseInt(m[2], 10); + this._literallen = size; + this._body = new ReadableStream(); + this._body._read = function bodyread(n) { + self._tryread(); + }; + m = RE_SEQNO.exec(this._buffer); + this._buffer = this._buffer.replace(RE_BODYLITERAL, ''); + this.emit('body', this._body, { + seqno: parseInt(m[1], 10), + which: which, + size: size + }); + } else if (m = RE_LITERAL.exec(this._buffer)) { + // non-BODY literal -- buffer it + this._buffer = this._buffer.replace(RE_LITERAL, LITPLACEHOLDER); + this._literallen = parseInt(m[1], 10); + this._tryread(this._literallen); + } else if (m = RE_UNTAGGED.exec(this._buffer)) { + // normal single line response + + // m[1] or m[3] = response type + // if m[3] is set, m[2] = sequence number (for FETCH) or count + // m[4] = response text code (optional) + // m[5] = response text (optional) + + var type, num, textCode, val; + if (m[2] !== undefined) + num = parseInt(m[2], 10); + if (m[4] !== undefined) + textCode = parseTextCode(m[4], this._literals); + + type = (m[1] || m[3]).toLowerCase(); + + if (type === 'flags' + || type === 'search' + || type === 'capability' + || type === 'sort') { + if (m[5][0] === '(') + val = RE_LISTCONTENT.exec(m[5])[1].split(' '); + else + val = m[5].split(' '); + if (type === 'search' || type === 'sort') + val = val.map(function(v) { return parseInt(v, 10); }); + } else if (type === 'list' || type === 'lsub') + val = parseBoxList(m[5], this._literals); + else if (type === 'status') + val = parseStatus(m[5], this._literals); + else if (type === 'fetch') + val = parseFetch(m[5], this._literals); + else if (type === 'namespace') + val = parseNamespaces(m[5], this._literals); + else + val = m[5]; + + this._literals = []; + + this.emit('untagged', { + type: type, + num: num, + textCode: textCode, + text: val + }); + ret = true; + } else + ret = true; + return ret; +}; + +Parser.prototype._resContinue = function() { + var m = RE_CONTINUE.exec(this._buffer), textCode, text = m[2]; + + if (m[1] !== undefined) + textCode = parseTextCode(m[1], this._literals); + + this.emit('continue', { + textCode: textCode, + text: text + }); + + return true; +}; + +function indexOfCh(buffer, len, i, ch) { + var r = -1; + for (; i < len; ++i) { + if (buffer[i] === ch) { + r = i; + break; + } + } + return r; +} + +function parseTextCode(text, literals) { + var r = parseExpr(text, literals); + if (r.length === 1) + return r[0]; + else + return { key: r[0], val: r[1] }; +} + +function parseBoxList(text, literals) { + var r = parseExpr(text, literals); + return { + flags: r[0], + delimiter: r[1], + name: utf7.decode(''+r[2]) + }; +} + +function parseNamespaces(text, literals) { + var r = parseExpr(text, literals), i, len, j, len2, ns, nsobj, namespaces, n; + + for (n = 0; n < 3; ++n) { + if (r[n]) { + namespaces = []; + for (i = 0, len = r[n].length; i < len; ++i) { + ns = r[n][i]; + nsobj = { + prefix: ns[0], + delimiter: ns[1], + extensions: undefined + }; + if (ns.length > 2) + nsobj.extensions = {}; + for (j = 2, len2 = ns.length; j < len2; j += 2) + nsobj.extensions[ns[j]] = ns[j + 1]; + namespaces.push(nsobj); + } + r[n] = namespaces; + } + } + + return { + personal: r[0], + other: r[1], + shared: r[2] + }; +} + +function parseStatus(text, literals) { + var r = parseExpr(text, literals), attrs = {}; + // r[1] is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn] + for (var i = 0, len = r[1].length; i < len; i += 2) + attrs[r[1][i].toLowerCase()] = r[1][i + 1]; + return { + name: utf7.decode(''+r[0]), + attrs: attrs + }; +} + +function parseFetch(text, literals) { + var list = parseExpr(text, literals)[0], attrs = {}; + // list is [KEY1, VAL1, KEY2, VAL2, .... KEYn, VALn] + for (var i = 0, len = list.length, key, val; i < len; i += 2) { + key = list[i].toLowerCase(); + val = list[i + 1]; + if (key === 'envelope') + val = parseFetchEnvelope(val); + else if (key === 'internaldate') + val = new Date(val); + else if (key === 'body') + val = parseBodyStructure(val); + attrs[key] = val; + } + return attrs; +} + +function parseBodyStructure(cur, literals, prefix, partID) { + var ret = [], i, len; + if (prefix === undefined) { + var result = (Array.isArray(cur) ? cur : parseExpr(cur, literals)); + if (result.length) + ret = parseBodyStructure(result, literals, '', 1); + } else { + var part, partLen = cur.length, next; + if (Array.isArray(cur[0])) { // multipart + next = -1; + while (Array.isArray(cur[++next])) { + ret.push(parseBodyStructure(cur[next], + literals, + prefix + (prefix !== '' ? '.' : '') + + (partID++).toString(), 1)); + } + part = { type: cur[next++].toLowerCase() }; + if (partLen > next) { + if (Array.isArray(cur[next])) { + part.params = {}; + for (i = 0, len = cur[next].length; i < len; i += 2) + part.params[cur[next][i].toLowerCase()] = cur[next][i + 1]; + } else + part.params = cur[next]; + ++next; + } + } else { // single part + next = 7; + if (typeof cur[1] === 'string') { + part = { + // the path identifier for this part, useful for fetching specific + // parts of a message + partID: (prefix !== '' ? prefix : '1'), + + // required fields as per RFC 3501 -- null or otherwise + type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(), + params: null, id: cur[3], description: cur[4], encoding: cur[5], + size: cur[6] + }; + } else { + // type information for malformed multipart body + part = { type: cur[0].toLowerCase(), params: null }; + cur.splice(1, 0, null); + ++partLen; + next = 2; + } + if (Array.isArray(cur[2])) { + part.params = {}; + for (i = 0, len = cur[2].length; i < len; i += 2) + part.params[cur[2][i].toLowerCase()] = cur[2][i + 1]; + if (cur[1] === null) + ++next; + } + if (part.type === 'message' && part.subtype === 'rfc822') { + // envelope + if (partLen > next && Array.isArray(cur[next])) + part.envelope = parseFetchEnvelope(cur[next]); + else + part.envelope = null; + ++next; + + // body + if (partLen > next && Array.isArray(cur[next])) + part.body = parseBodyStructure(cur[next], literals, prefix, 1); + else + part.body = null; + ++next; + } + if ((part.type === 'text' + || (part.type === 'message' && part.subtype === 'rfc822')) + && partLen > next) + part.lines = cur[next++]; + if (typeof cur[1] === 'string' && partLen > next) + part.md5 = cur[next++]; + } + // add any extra fields that may or may not be omitted entirely + parseStructExtra(part, partLen, cur, next); + ret.unshift(part); + } + return ret; +} + +function parseStructExtra(part, partLen, cur, next) { + if (partLen > next) { + // disposition + // null or a special k/v list with these kinds of values: + // e.g.: ['Foo', null] + // ['Foo', ['Bar', 'Baz']] + // ['Foo', ['Bar', 'Baz', 'Bam', 'Pow']] + var disposition = { type: null, params: null }; + if (Array.isArray(cur[next])) { + disposition.type = cur[next][0]; + if (Array.isArray(cur[next][1])) { + disposition.params = {}; + for (var i = 0, len = cur[next][1].length, key; i < len; i += 2) { + key = cur[next][1][i].toLowerCase(); + disposition.params[key] = cur[next][1][i + 1]; + } + } + } else if (cur[next] !== null) + disposition.type = cur[next]; + + if (disposition.type === null) + part.disposition = null; + else + part.disposition = disposition; + + ++next; + } + if (partLen > next) { + // language can be a string or a list of one or more strings, so let's + // make this more consistent ... + if (cur[next] !== null) + part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]]); + else + part.language = null; + ++next; + } + if (partLen > next) + part.location = cur[next++]; + if (partLen > next) { + // extension stuff introduced by later RFCs + // this can really be any value: a string, number, or (un)nested list + // let's not parse it for now ... + part.extensions = cur[next]; + } +} + +function parseFetchEnvelope(list) { + return { + date: new Date(list[0]), + subject: list[1], + from: parseEnvelopeAddresses(list[2]), + sender: parseEnvelopeAddresses(list[3]), + replyTo: parseEnvelopeAddresses(list[4]), + to: parseEnvelopeAddresses(list[5]), + cc: parseEnvelopeAddresses(list[6]), + bcc: parseEnvelopeAddresses(list[7]), + inReplyTo: list[8], + messageId: list[9] + }; +} + +function parseEnvelopeAddresses(list) { + var addresses = null; + if (Array.isArray(list)) { + addresses = []; + var inGroup = false, curGroup; + for (var i = 0, len = list.length, addr; i < len; ++i) { + addr = list[i]; + if (addr[2] === null) { // end of group addresses + inGroup = false; + if (curGroup) { + addresses.push(curGroup); + curGroup = undefined; + } + } else if (addr[3] === null) { // start of group addresses + inGroup = true; + curGroup = { + group: addr[2], + addresses: [] + }; + } else { // regular user address + var info = { + name: addr[0], + mailbox: addr[2], + host: addr[3] + }; + if (inGroup) + curGroup.addresses.push(info); + else if (!inGroup) + addresses.push(info); + } + list[i] = addr; + } + if (inGroup) { + // no end of group found, assume implicit end + addresses.push(curGroup); + } + } + return addresses; +} + +function parseExpr(o, literals, result, start, useBrackets) { + start = start || 0; + var inQuote = false, lastPos = start - 1, isTop = false, val; + + if (useBrackets === undefined) + useBrackets = true; + if (!result) + result = []; + if (typeof o === 'string') { + o = { str: o }; + isTop = true; + } + for (var i = start, len = o.str.length; i < len; ++i) { + if (!inQuote) { + if (o.str[i] === '"') + inQuote = true; + else if (o.str[i] === ' ' || o.str[i] === ')' + || (useBrackets && o.str[i] === ']')) { + if (i - (lastPos + 1) > 0) { + val = convStr(o.str.substring(lastPos + 1, i), literals); + result.push(val); + } + if ((o.str[i] === ')' || (useBrackets && o.str[i] === ']')) && !isTop) + return i; + lastPos = i; + } else if ((o.str[i] === '(' || (useBrackets && o.str[i] === '['))) { + var innerResult = []; + i = parseExpr(o, literals, innerResult, i + 1, useBrackets); + lastPos = i; + result.push(innerResult); + } + } else if (o.str[i] === '"' && + (o.str[i - 1] && + (o.str[i - 1] !== '\\' + || (o.str[i - 2] && o.str[i - 2] === '\\') + ))) + inQuote = false; + if (i + 1 === len && len - (lastPos + 1) > 0) + result.push(convStr(o.str.substring(lastPos + 1), literals)); + } + return (isTop ? result : start); +} + +function convStr(str, literals) { + if (str[0] === '"') + return str.substring(1, str.length - 1); + else if (str === 'NIL') + return null; + else if (RE_INTEGER.test(str)) { + // some IMAP extensions utilize large (64-bit) integers, which JavaScript + // can't handle natively, so we'll just keep it as a string if it's too big + var val = parseInt(str, 10); + return (val.toString() === str ? val : str); + } else if (literals && literals.length && str === LITPLACEHOLDER) { + var l = literals.shift(); + if (Buffer.isBuffer(l)) + l = l.toString('utf8'); + return l; + } + + return str; +} + +exports.Parser = Parser; +exports.parseExpr = parseExpr; +exports.parseEnvelopeAddresses = parseEnvelopeAddresses; +exports.parseBodyStructure = parseBodyStructure; diff --git a/lib/imap.js b/lib/imap.js deleted file mode 100644 index b3829b6..0000000 --- a/lib/imap.js +++ /dev/null @@ -1,1674 +0,0 @@ -var assert = require('assert'), - tls = require('tls'), - isDate = require('util').isDate, - inspect = require('util').inspect, - inherits = require('util').inherits, - Socket = require('net').Socket, - EventEmitter = require('events').EventEmitter, - utf7 = require('utf7').imap, - // customized copy of XRegExp to deal with multiple variables of the same - // name - XRegExp = require('./xregexp').XRegExp; - -var parsers = require('./imap.parsers'), - utils = require('./imap.utilities'); - -// main constants -var CRLF = '\r\n', - STATES = { - NOCONNECT: 0, - NOAUTH: 1, - AUTH: 2, - BOXSELECTING: 3, - BOXSELECTED: 4 - }, - RE_LITHEADER = /(?:((?:BODY\[.*\](?:<\d+>)?)?|[^ ]+) )?\{(\d+)\}(?:$|\r\n)/i, - RE_UNRESP = /^\* (OK|PREAUTH|NO|BAD)(?:\r\n|(?: \[(.+?)\])?(?: (.+))?)(?:$|\r\n)/i, - RE_TAGGED_RESP = /^A\d+ (OK|NO|BAD) (?:\[(.+?)\] )?(.+)(?:$|\r\n)/i, - RE_TEXT_CODE = /([^ ]+)(?: (.*))?$/, - RE_RES_IDLE = /^IDLE /i, - RE_RES_NOOP = /^NOOP /i, - RE_CMD_FETCH = /^(?:UID )?FETCH/i, - RE_PARTID = /^(?:[\d]+[\.]{0,1})*[\d]+$/, - RE_ESCAPE = /\\\\/g, - RE_DBLQ = /"/g, - RE_CMD = /^([^ ]+)(?: |$)/, - RE_ISHEADER = /HEADER/, - REX_UNRESPDATA = XRegExp('^\\* (?:(?:(?NAMESPACE) (?(?:NIL|\\((?:\\(.+\\))+\\))) (?(?:NIL|\\((?:\\(.+\\))+\\))) (?(?:NIL|\\((?:\\(.+\\))+\\))))|(?:(?FLAGS) \\((?.*)\\))|(?:(?LIST|LSUB|XLIST) \\((?.*)\\) (?"[^"]+"|NIL) (?.+))|(?:(?(SEARCH|SORT))(?: (?.*))?)|(?:(?STATUS) (?.+) \\((?.*)\\))|(?:(?CAPABILITY) (?.+))|(?:(?BYE) (?:\\[(?.+)\\] )?(?.+)))[ \t]*(?:$|\r\n)', 'i'), - REX_UNRESPNUM = XRegExp('^\\* (?\\d+) (?:(?EXISTS)|(?RECENT)|(?EXPUNGE)|(?:(?FETCH) \\((?.*)\\)))[ \t]*(?:$|\r\n)', 'i'); - -// extension constants -var IDLE_NONE = 1, - IDLE_WAIT = 2, - IDLE_IDLING = 3, - IDLE_DONE = 4; - -function ImapConnection(options) { - if (!(this instanceof ImapConnection)) - return new ImapConnection(options); - EventEmitter.call(this); - - this._options = { - username: options.username || options.user || '', - password: options.password || '', - host: options.host || 'localhost', - port: options.port || 143, - secure: options.secure === true ? { // secure = true means default behavior - rejectUnauthorized: false // Force pre-node-0.9.2 behavior - } : (options.secure || false), - connTimeout: options.connTimeout || 10000, // connection timeout in msecs - xoauth: options.xoauth, - xoauth2: options.xoauth2 - }; - - this._state = { - status: STATES.NOCONNECT, - conn: null, - curId: 0, - requests: [], - numCapRecvs: 0, - isReady: false, - isIdle: true, - tmrKeepalive: null, - tmoKeepalive: 10000, - tmrConn: null, - indata: { - literals: [], - line: undefined, - line_s: { p: 0, ret: undefined }, - temp: undefined, - streaming: false, - expect: -1 - }, - box: { - uidnext: 0, - readOnly: false, - flags: [], - newKeywords: false, - uidvalidity: 0, - keywords: [], - permFlags: [], - name: null, - messages: { total: 0, new: 0 }, - _newName: undefined - }, - ext: { - // Capability-specific state info - idle: { - MAX_WAIT: 300000, // 5 mins in ms - state: IDLE_NONE, - timeStarted: undefined - } - } - }; - - if (typeof options.debug === 'function') - this.debug = options.debug; - else - this.debug = false; - - this.delimiter = undefined; - this.namespaces = { personal: [], other: [], shared: [] }; - this.capabilities = []; - this.connected = false; - this.authenticated = false; -} - -inherits(ImapConnection, EventEmitter); -module.exports = ImapConnection; -module.exports.ImapConnection = ImapConnection; - -ImapConnection.prototype.connect = function(loginCb) { - this._reset(); - - var self = this, - state = this._state, - requests = state.requests, - indata = state.indata; - - var socket = state.conn = new Socket(); - socket.setKeepAlive(true); - socket.setTimeout(0); - - if (this._options.secure) { - var tlsOptions = {}; - for (var k in this._options.secure) - tlsOptions[k] = this._options.secure[k]; - tlsOptions.socket = state.conn; - if (process.version.indexOf('v0.6.') > -1) - socket = tls.connect(null, tlsOptions, onconnect); - else - socket = tls.connect(tlsOptions, onconnect); - } else - state.conn.once('connect', onconnect); - - function onconnect() { - state.conn = socket; // re-assign for secure connections - self.connected = true; - self.authenticated = false; - self.debug&&self.debug('[connection] Connected to host.'); - state.status = STATES.NOAUTH; - } - - state.conn.on('end', function() { - self.connected = false; - self.authenticated = false; - self.debug&&self.debug('[connection] FIN packet received. Disconnecting...'); - clearTimeout(state.tmrConn); - self.emit('end'); - }); - - state.conn.on('close', function(had_error) { - self._reset(); - requests = state.requests; - self.connected = false; - self.authenticated = false; - self.debug&&self.debug('[connection] Connection closed.'); - self.emit('close', had_error); - }); - - socket.on('error', function(err) { - clearTimeout(state.tmrConn); - err.level = 'socket'; - if (state.status === STATES.NOCONNECT) - loginCb(err); - else - self.emit('error', err); - self.debug&&self.debug('[connection] Error occurred: ' + err); - }); - - socket.on('ready', function() { - var checkedNS = false; - var reentry = function(err) { - if (err) { - state.conn.destroy(); - return loginCb(err); - } - // Next, get the list of available namespaces if supported (RFC2342) - if (!checkedNS && self.serverSupports('NAMESPACE')) { - // Re-enter this function after we've obtained the available - // namespaces - checkedNS = true; - return self._send('NAMESPACE', reentry); - } - // Lastly, get the top-level mailbox hierarchy delimiter used by the - // server - self._send('LIST "" ""', loginCb); - }; - // First, get the supported (pre-auth or otherwise) capabilities: - self._send('CAPABILITY', function() { - // No need to attempt the login sequence if we're on a PREAUTH - // connection. - if (state.status !== STATES.AUTH) { - // First get pre-auth capabilities, including server-supported auth - // mechanisms - self._login(reentry); - } else - reentry(); - }); - }); - - function read(b) { - var blen = b.length, origPos = b.p; - if (indata.expect <= (blen - b.p)) { - var left = indata.expect; - indata.expect = 0; - b.p += left; - return b.slice(origPos, origPos + left); - } else { - indata.expect -= (blen - b.p); - b.p = blen; - return origPos > 0 ? b.slice(origPos) : b; - } - } - - function emitLitData(key, data) { - var fetches = requests[0].fetchers[key.replace(RE_DBLQ, '')]; - for (var i=0, len=fetches.length; i= b.length) return; - self.debug&&self.debug('\n<== ' + inspect(b.toString('binary', b.p)) + '\n'); - - var r, m, litType, i, len, msg, fetches, index; - - if (indata.expect > 0) { - r = read(b); - if (indata.streaming) { - emitLitData(requests[0].key, r); - if (indata.expect === 0) - indata.streaming = false; - } else { - if (indata.temp) - indata.temp += r.toString('binary'); - else - indata.temp = r.toString('binary'); - if (indata.expect === 0) { - indata.literals.push(indata.temp); - indata.temp = undefined; - } - } - if (b.p >= b.length) - return; - } - - if ((r = utils.line(b, indata.line_s)) === false) - return; - else { - m = RE_LITHEADER.exec(r); - if (indata.line) - indata.line += r; - else - indata.line = r; - if (m) - litType = m[1]; - indata.expect = (m ? parseInt(m[2], 10) : -1); - if (indata.expect > -1) { - if ((m = /\* (\d+) FETCH/i.exec(indata.line)) - && /^BODY\[/i.test(litType)) { - msg = new ImapMessage(); - msg.seqno = parseInt(m[1], 10); - fetches = requests[0].fetchers[litType]; - emitLitMsg(litType, msg); - - requests[0].key = litType; - indata.streaming = !RE_ISHEADER.test(litType); - if (indata.streaming) - indata.literals.push(indata.expect); - } else if (indata.expect === 0) - indata.literals.push(''); - // start reading of the literal or get the rest of the response - return ondata(b); - } - } - - indata.line = indata.line.trim(); - if (indata.line[0] === '*') { // Untagged server response - var isUnsolicited = (requests[0] && requests[0].cmd === 'NOOP') - || (state.isIdle && state.ext.idle.state !== IDLE_NONE) - || !requests.length; - if (m = XRegExp.exec(indata.line, REX_UNRESPNUM)) { - // m.type = response type (numeric-based) - m.type = m.type.toUpperCase(); - self.debug&&self.debug('[parsing incoming] saw untagged ' + m.type); - switch (m.type) { - case 'FETCH': - // m.info = message details - var data, parsed, headers, body, lenb, bodies, details, val; - - isUnsolicited = isUnsolicited - || (requests[0] - && !RE_CMD_FETCH.test(requests[0].cmdstr)); - - if (!isUnsolicited) - bodies = parsers.parseFetchBodies(m.info, indata.literals); - - details = new ImapMessage(); - parsers.parseFetch(m.info, indata.literals, details); - details.seqno = parseInt(m.num, 10); - - if (details['x-gm-labels'] !== undefined) { - var labels = details['x-gm-labels']; - for (i=0, len=labels.length; i prev) { - state.box.messages.new = now - prev; - self.emit('mail', state.box.messages.new); // new mail - } - break; - case 'RECENT': - // messages marked with the \Recent flag (i.e. new messages) - state.box.messages.new = parseInt(m.num, 10); - break; - case 'EXPUNGE': - // confirms permanent deletion of a single message - if (state.box.messages.total > 0) - --state.box.messages.total; - if (isUnsolicited) - self.emit('deleted', parseInt(m.num, 10)); - break; - } - } else if (m = XRegExp.exec(indata.line, REX_UNRESPDATA)) { - // m.type = response type (data) - m.type = m.type.toUpperCase(); - self.debug&&self.debug('[parsing incoming] saw untagged ' + m.type); - switch (m.type) { - case 'NAMESPACE': - // m.personal = personal namespaces (or null) - // m.other = personal namespaces (or null) - // m.shared = personal namespaces (or null) - self.namespaces.personal = - parsers.parseNamespaces(m.personal, indata.literals); - self.namespaces.other = - parsers.parseNamespaces(m.other, indata.literals); - self.namespaces.shared = - parsers.parseNamespaces(m.shared, indata.literals); - break; - case 'FLAGS': - // m.flags = list of 0+ flags - m.flags = (m.flags - ? m.flags.split(' ') - .map(function(f) { - return f.substr(1); - }) - : []); - if (state.status === STATES.BOXSELECTING) - state.box.flags = m.flags; - break; - case 'LIST': - case 'LSUB': - case 'XLIST': - // m.flags = list of 0+ flags - // m.delimiter = mailbox delimiter (string or null) - // m.mailbox = mailbox name (string) - m.flags = (m.flags ? m.flags.toUpperCase().split(' ') : []); - m.delimiter = parsers.convStr(m.delimiter, indata.literals); - m.mailbox = utf7.decode(''+parsers.convStr(m.mailbox, indata.literals)); - if (self.delimiter === undefined) - self.delimiter = parsers.convStr(m.delimiter, indata.literals); - else { - if (requests[0].cbargs.length === 0) - requests[0].cbargs.push({}); - var box = { - attribs: m.flags.map(function(attr) { - return attr.substr(1); - }), - delimiter: m.delimiter, - children: null, - parent: null - }, - name = m.mailbox, - curChildren = requests[0].cbargs[0]; - - if (box.delimiter) { - var path = name.split(box.delimiter), - parent = null; - name = path.pop(); - for (i=0,len=path.length; iv pairs) of mailbox attributes - m.mailbox = utf7.decode(''+parsers.convStr(m.mailbox, indata.literals)); - var ret = { - name: m.mailbox, - uidvalidity: 0, - messages: { - total: 0, - new: 0, - unseen: undefined - } - }; - if (m.attributes) { - m.attributes = parsers.parseExpr(m.attributes, indata.literals); - for (i=0,len=m.attributes.length; i -1) { - state.box.newKeywords = true; - permFlags.splice(idx, 1); - } - state.box.keywords = keywords = permFlags.filter(function(f) { - return (f[0] !== '\\'); - }); - for (i=0,len=keywords.length; i -1) - indata.line = indata.line.substr(index + 2); - else - indata.line = undefined; - state.ext.idle.state = IDLE_NONE; - state.ext.idle.timeStarted = undefined; - if (requests.length) { - state.isIdle = false; - self._send(); - } else - doKeepalive(); - } else if (RE_RES_NOOP.test(indata.line)) { - self.debug&&self.debug('[parsing incoming] saw NOOP'); - requests.shift(); // remove NOOP request - if ((index = indata.line.indexOf(CRLF)) > -1) - indata.line = indata.line.substr(index + 2); - else - indata.line = undefined; - if (!requests.length) - doKeepaliveTimer(); - else - self._send(); - } else { - // unknown response - self.debug&&self.debug('[parsing incoming] saw unexpected response: ' - + inspect(indata.line)); - assert(false); - } - } - - function doKeepalive() { - if (state.status >= STATES.AUTH) { - if (self.serverSupports('IDLE')) - self._send('IDLE'); - else - self._noop(); - } - } - - function doKeepaliveTimer() { - state.tmrKeepalive = setTimeout(function idleHandler() { - if (state.isIdle) { - if (state.ext.idle.state === IDLE_IDLING) { - var timeDiff = Date.now() - state.ext.idle.timeStarted; - if (timeDiff >= state.ext.idle.MAX_WAIT) { - state.ext.idle.state = IDLE_DONE; - self._send('DONE'); - } else - state.tmrKeepalive = setTimeout(idleHandler, state.tmoKeepalive); - } else if (!self.serverSupports('IDLE')) - doKeepalive(); - } - }, state.tmoKeepalive); - } - - state.conn.connect(this._options.port, this._options.host); - - state.tmrConn = setTimeout(function() { - state.conn.destroy(); - state.conn = undefined; - var err = new Error('Connection timed out'); - err.level = 'timeout'; - loginCb(err); - }, this._options.connTimeout); -}; - -ImapConnection.prototype.logout = function(cb) { - var self = this; - if (this._state.status >= STATES.NOAUTH) { - this._send('LOGOUT', function(err) { - self._state.conn.end(); - if (typeof cb === 'function') - cb(err); - }); - if (cb === true) - this._state.conn.removeAllListeners(); - } 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'); - if (this._state.status === STATES.BOXSELECTED) - this._resetBox(); - if (cb === undefined) { - cb = readOnly; - readOnly = false; - } - - name = ''+name; - this._state.box.name = name; - - this._send((readOnly ? 'EXAMINE' : 'SELECT') + ' "' - + utils.escape(utf7.encode(name)) + '"', cb); -}; - -// also deletes any messages in this box marked with \Deleted -ImapConnection.prototype.closeBox = function(cb) { - 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.status = function(boxName, cb) { - if (this._state.status === STATES.BOXSELECTED - && this._state.box.name === boxName) - throw new Error('Not allowed to call status on the currently selected mailbox'); - - var cmd = 'STATUS "'; - cmd += utils.escape(utf7.encode(''+boxName)); - cmd += '" (MESSAGES RECENT UNSEEN UIDVALIDITY)'; - - this._send(cmd, cb); -}; - -ImapConnection.prototype.removeDeleted = function(uids, cb) { - if (typeof uids === 'function') { - cb = uids; - uids = undefined; - } - if (uids !== undefined) { - if (!Array.isArray(uids)) - uids = [uids]; - - utils.validateUIDList(uids); - - this._send('UID EXPUNGE ' + uids.join(','), cb); - } else - this._send('EXPUNGE', cb); -}; - -ImapConnection.prototype.getBoxes = function(namespace, cb) { - if (typeof namespace === 'function') { - cb = namespace; - namespace = ''; - } - this._send((!this.serverSupports('XLIST') ? 'LIST' : 'XLIST') - + ' "' + utils.escape(utf7.encode(''+namespace)) + '" "*"', cb); -}; - -ImapConnection.prototype.addBox = function(name, cb) { - this._send('CREATE "' + utils.escape(utf7.encode(''+name)) + '"', cb); -}; - -ImapConnection.prototype.delBox = function(name, cb) { - this._send('DELETE "' + utils.escape(utf7.encode(''+name)) + '"', cb); -}; - -ImapConnection.prototype.renameBox = function(oldname, newname, cb) { - if (this._state.status === STATES.BOXSELECTED - && oldname === this._state.box.name && oldname !== 'INBOX') - this._state.box._newName = ''+oldname; - - var cmd = 'RENAME "'; - cmd += utils.escape(utf7.encode(''+oldname)); - cmd += '" "'; - cmd += utils.escape(utf7.encode(''+newname)); - cmd += '"'; - this._send(cmd, cb); -}; - -ImapConnection.prototype.append = function(data, options, cb) { - if (typeof options === 'function') { - cb = options; - options = undefined; - } - options = options || {}; - if (!options.mailbox) { - if (this._state.status !== STATES.BOXSELECTED) - throw new Error('No mailbox specified or currently selected'); - else - options.mailbox = this._state.box.name; - } - var cmd = 'APPEND "' + utils.escape(utf7.encode(''+options.mailbox)) + '"'; - if (options.flags) { - if (!Array.isArray(options.flags)) - options.flags = [options.flags]; - if (options.flags.length > 0) - cmd += " (\\" + options.flags.join(' \\') + ")"; - } - if (options.date) { - if (!isDate(options.date)) - throw new Error("`date` isn't a Date object"); - cmd += ' "'; - cmd += options.date.getDate(); - cmd += '-'; - cmd += utils.MONTHS[options.date.getMonth()]; - cmd += '-'; - cmd += options.date.getFullYear(); - cmd += ' '; - cmd += ('0' + options.date.getHours()).slice(-2); - cmd += ':'; - cmd += ('0' + options.date.getMinutes()).slice(-2); - cmd += ':'; - cmd += ('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 += '"'; - } - cmd += ' {'; - cmd += (Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data)); - cmd += '}'; - var self = this, step = 1; - this._send(cmd, function(err, info) { - if (err || step++ === 2) - return cb(err, info); - self._state.conn.write(data); - self._state.conn.write(CRLF); - self.debug&&self.debug('\n==> ' + inspect(data.toString()) + '\n'); - }); -}; - -ImapConnection.prototype.search = function(options, cb) { - this._search('UID ', options, cb); -}; - -ImapConnection.prototype._search = function(which, 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'); - this._send(which + 'SEARCH' - + utils.buildSearchQuery(options, this.capabilities), cb); -}; - -ImapConnection.prototype.sort = function(sorts, options, cb) { - this._sort('UID ', sorts, options, cb); -}; - -ImapConnection.prototype._sort = function(which, sorts, options, cb) { - if (this._state.status !== STATES.BOXSELECTED) - throw new Error('No mailbox is currently selected'); - if (!Array.isArray(sorts) || !sorts.length) - throw new Error('Expected array with at least one sort criteria'); - if (!Array.isArray(options)) - throw new Error('Expected array for search options'); - if (!this.serverSupports('SORT')) - return cb(new Error('Sorting is not supported on the server')); - - var criteria = sorts.map(function(criterion) { - if (typeof criterion !== 'string') - throw new Error('Unexpected sort criterion data type. ' - + 'Expected string. Got: ' + typeof criteria); - - var modifier = ''; - if (criterion[0] === '-') { - modifier = 'REVERSE '; - criterion = criterion.substring(1); - } - switch (criterion.toUpperCase()) { - case 'ARRIVAL': - case 'CC': - case 'DATE': - case 'FROM': - case 'SIZE': - case 'SUBJECT': - case 'TO': - break; - default: - throw new Error('Unexpected sort criteria: ' + criterion); - } - - return modifier + criterion; - }); - - this._send(which + 'SORT (' + criteria.join(' ') + ') UTF-8' - + utils.buildSearchQuery(options, this.capabilities), cb); -}; - -ImapConnection.prototype.fetch = function(uids, options, what, cb) { - return this._fetch('UID ', uids, options, what, cb); -}; - -ImapConnection.prototype._fetch = function(which, uids, options, what, cb) { - if (uids === undefined - || uids === null - || (Array.isArray(uids) && uids.length === 0)) - throw new Error('Nothing to fetch'); - - if (!Array.isArray(uids)) - uids = [uids]; - utils.validateUIDList(uids); - - var toFetch = '', prefix = ' BODY[', extensions, parse, headers, key, stream, - fetchers = {}; - - // argument detection! - if (cb === undefined) { - // fetch(uids, xxxx, yyyy) - if (what === undefined) { - // fetch(uids, xxxx) - if (options === undefined) { - // fetch(uids) - what = options = {}; - } else if (typeof options === 'function') { - // fetch(uids, callback) - cb = options; - what = options = {}; - } else if (options.struct !== undefined - || options.size !== undefined - || options.markSeen !== undefined) { - // fetch(uids, options) - what = {}; - } else { - // fetch(uids, what) - what = options; - options = {}; - } - } else if (typeof what === 'function') { - // fetch(uids, xxxx, callback) - cb = what; - if (options.struct !== undefined - || options.size !== undefined - || options.markSeen !== undefined) { - // fetch(uids, options, callback) - what = {}; - } else { - // fetch(uids, what, callback) - what = options; - options = {}; - } - } - } - - if (!Array.isArray(what)) - what = [what]; - - for (var i = 0, wp, pprefix, len = what.length; i < len; ++i) { - wp = what[i]; - parse = true; - if (wp.id !== undefined && !RE_PARTID.test(''+wp.id)) - throw new Error('Invalid part id: ' + wp.id); - if (( (typeof wp.headers === 'object' - && (!wp.headers.fields - || (Array.isArray(wp.headers.fields) - && wp.headers.fields.length === 0) - ) - && wp.headers.parse === false - ) - || - (typeof wp.headersNot === 'object' - && (!wp.headersNot.fields - || (Array.isArray(wp.headersNot.fields) - && wp.headersNot.fields.length === 0) - ) - && wp.headersNot.parse === false - ) - ) - && wp.body === true) { - key = prefix.trim(); - if (wp.id !== undefined) - key += wp.id; - key += ']'; - if (!fetchers[key]) { - fetchers[key] = [new ImapFetch()]; - toFetch += ' '; - toFetch += key; - } - if (typeof wp.cb === 'function') - wp.cb(fetchers[key][0]); - key = undefined; - } else if (wp.headers || wp.headersNot || wp.body) { - pprefix = prefix; - if (wp.id !== undefined) { - pprefix += wp.id; - pprefix += '.'; - } - if (wp.headers) { - key = pprefix.trim(); - if (wp.headers === true) - key += 'HEADER]'; - else { - if (Array.isArray(wp.headers)) - headers = wp.headers; - else if (typeof wp.headers === 'string') - headers = [wp.headers]; - else if (typeof wp.headers === 'object') { - if (wp.headers.fields === undefined) - wp.headers.fields = true; - if (!Array.isArray(wp.headers.fields) - && typeof wp.headers.fields !== 'string' - && wp.headers.fields !== true) - throw new Error('Invalid `fields` property'); - if (Array.isArray(wp.headers.fields)) - headers = wp.headers.fields; - else if (wp.headers.fields === true) - headers = true; - else - headers = [wp.headers.fields]; - if (wp.headers.parse === false) - parse = false; - } else - throw new Error('Invalid `headers` value: ' + wp.headers); - if (headers === true) - key += 'HEADER]'; - else { - key += 'HEADER.FIELDS ('; - key += headers.join(' ').toUpperCase(); - key += ')]'; - } - } - } else if (wp.headersNot) { - key = pprefix.trim(); - if (wp.headersNot === true) - key += 'HEADER]'; - else { - if (Array.isArray(wp.headersNot)) - headers = wp.headersNot; - else if (typeof wp.headersNot === 'string') - headers = [wp.headersNot]; - else if (typeof wp.headersNot === 'object') { - if (wp.headersNot.fields === undefined) - wp.headersNot.fields = true; - if (!Array.isArray(wp.headersNot.fields) - && typeof wp.headersNot.fields !== 'string' - && wp.headersNot.fields !== true) - throw new Error('Invalid `fields` property'); - if (Array.isArray(wp.headersNot.fields)) - headers = wp.headersNot.fields; - else if (wp.headersNot.fields) - headers = true; - else - headers = [wp.headersNot.fields]; - if (wp.headersNot.parse === false) - parse = false; - } else - throw new Error('Invalid `headersNot` value: ' + wp.headersNot); - if (headers === true) - key += 'HEADER]'; - else { - key += 'HEADER.FIELDS.NOT ('; - key += headers.join(' ').toUpperCase(); - key += ')]'; - } - } - } - if (key) { - stream = new ImapFetch(); - if (parse) - stream._parse = true; - if (!fetchers[key]) { - fetchers[key] = [stream]; - toFetch += ' '; - toFetch += key; - } else - fetchers[key].push(stream); - if (typeof wp.cb === 'function') - wp.cb(stream); - key = undefined; - } - if (wp.body) { - key = pprefix; - if (wp.body === true) - key += 'TEXT]'; - else - throw new Error('Invalid `body` value: ' + wp.body); - - key = key.trim(); - if (!stream) - stream = new ImapFetch(); - if (!fetchers[key]) { - fetchers[key] = [stream]; - toFetch += ' ' + key; - } else - fetchers[key].push(stream); - if (!wp.headers && !wp.headersNot && typeof wp.cb === 'function') - wp.cb(stream); - stream = undefined; - key = undefined; - } - } else { - // non-body fetches - stream = new ImapFetch(); - if (fetchers['']) - fetchers[''].push(stream); - else - fetchers[''] = [stream]; - if (typeof wp.cb === 'function') - wp.cb(stream); - } - } - - // always fetch GMail-specific bits of information when on GMail - if (this.serverSupports('X-GM-EXT-1')) - extensions = 'X-GM-THRID X-GM-MSGID X-GM-LABELS '; - - var cmd = which; - cmd += 'FETCH '; - cmd += uids.join(','); - cmd += ' ('; - if (extensions) - cmd += extensions; - cmd += 'UID FLAGS INTERNALDATE'; - if (options.struct) - cmd += ' BODYSTRUCTURE'; - if (options.size) - cmd += ' RFC822.SIZE'; - if (toFetch) { - if (!options.markSeen) - cmd += toFetch.replace(/BODY\[/g, 'BODY.PEEK['); - else - cmd += toFetch; - } - cmd += ')'; - - this._send(cmd, function(err) { - var keys = Object.keys(fetchers), k, lenk = keys.length, f, lenf, - fetches; - if (err) { - for (k = 0; k < lenk; ++k) { - fetches = fetchers[keys[k]]; - for (f = 0, lenf = fetches.length; f < lenf; ++f) - fetches[f].emit('error', err); - } - } - for (k = 0; k < lenk; ++k) { - fetches = fetchers[keys[k]]; - for (f = 0, lenf = fetches.length; f < lenf; ++f) - fetches[f].emit('end'); - } - cb&&cb(err); - }); - - this._state.requests[this._state.requests.length - 1].fetchers = fetchers; -}; - -ImapConnection.prototype.addFlags = function(uids, flags, cb) { - this._store('UID ', uids, flags, true, cb); -}; - -ImapConnection.prototype.delFlags = function(uids, flags, cb) { - this._store('UID ', uids, flags, false, cb); -}; - -ImapConnection.prototype.addKeywords = function(uids, flags, cb) { - return this._addKeywords('UID ', uids, flags, cb); -}; - -ImapConnection.prototype._addKeywords = function(which, uids, flags, cb) { - if (!this._state.box.newKeywords) - throw new Error('This mailbox does not allow new keywords to be added'); - this._store(which, uids, flags, true, cb); -}; - -ImapConnection.prototype.delKeywords = function(uids, flags, cb) { - this._store('UID ', uids, flags, false, cb); -}; - -ImapConnection.prototype.setLabels = function(uids, labels, cb) { - this._storeLabels('UID ', uids, labels, '', cb); -}; - -ImapConnection.prototype.addLabels = function(uids, labels, cb) { - this._storeLabels('UID ', uids, labels, '+', cb); -}; - -ImapConnection.prototype.delLabels = function(uids, labels, cb) { - this._storeLabels('UID ', uids, labels, '-', cb); -}; - -ImapConnection.prototype._storeLabels = function(which, uids, labels, mode, cb) { - if (!this.serverSupports('X-GM-EXT-1')) - throw new Error('Server must support X-GM-EXT-1 capability'); - if (this._state.status !== STATES.BOXSELECTED) - throw new Error('No mailbox is currently selected'); - if (uids === undefined) - throw new Error('The message ID(s) must be specified'); - - if (!Array.isArray(uids)) - uids = [uids]; - utils.validateUIDList(uids); - - if ((!Array.isArray(labels) && typeof labels !== 'string') - || (Array.isArray(labels) && labels.length === 0)) - throw new Error('labels argument must be a string or a non-empty Array'); - if (!Array.isArray(labels)) - labels = [labels]; - labels = labels.join(' '); - - this._send(which + 'STORE ' + uids.join(',') + ' ' + mode - + 'X-GM-LABELS.SILENT (' + labels + ')', cb); -}; - -ImapConnection.prototype.copy = function(uids, boxTo, cb) { - return this._copy('UID ', uids, boxTo, cb); -}; - -ImapConnection.prototype._copy = function(which, uids, boxTo, cb) { - if (this._state.status !== STATES.BOXSELECTED) - throw new Error('No mailbox is currently selected'); - - if (!Array.isArray(uids)) - uids = [uids]; - - utils.validateUIDList(uids); - - this._send(which + 'COPY ' + uids.join(',') + ' "' - + utils.escape(utf7.encode(''+boxTo)) + '"', cb); -}; - -ImapConnection.prototype.move = function(uids, boxTo, cb) { - return this._move('UID ', uids, boxTo, cb); -}; - -ImapConnection.prototype._move = function(which, uids, boxTo, cb) { - var self = this; - if (this._state.status !== STATES.BOXSELECTED) - throw new Error('No mailbox is currently selected'); - - if (this.serverSupports('MOVE')) { - if (!Array.isArray(uids)) - uids = [uids]; - - utils.validateUIDList(uids); - - this._send(which + 'MOVE ' + uids.join(',') + ' "' - + utils.escape(utf7.encode(''+boxTo)) + '"', cb); - } else if (this._state.box.permFlags.indexOf('deleted') === -1) { - throw new Error('Cannot move message: ' - + 'server does not allow deletion of messages'); - } else { - var deletedUIDs, task = 0; - this._copy(which, uids, boxTo, function ccb(err, info) { - if (err) - return cb(err, info); - - if (task === 0 && which && self.serverSupports('UIDPLUS')) { - // UIDPLUS gives us a 'UID EXPUNGE n' command to expunge a subset of - // messages with the \Deleted flag set. This allows us to skip some - // actions. - task = 2; - } - // Make sure we don't expunge any messages marked as Deleted except the - // one we are moving - if (task === 0) { - self.search(['DELETED'], function(e, result) { - ++task; - deletedUIDs = result; - ccb(e, info); - }); - } else if (task === 1) { - if (deletedUIDs.length) { - self.delFlags(deletedUIDs, 'Deleted', function(e) { - ++task; - ccb(e, info); - }); - } else { - ++task; - ccb(err, info); - } - } else if (task === 2) { - function cbMarkDel(e) { - ++task; - ccb(e, info); - } - if (which) - self.addFlags(uids, 'Deleted', cbMarkDel); - else - self.seq.addFlags(uids, 'Deleted', cbMarkDel); - } else if (task === 3) { - if (which && self.serverSupports('UIDPLUS')) - self.removeDeleted(uids, cb); - else { - self.removeDeleted(function(e) { - ++task; - ccb(e, info); - }); - } - } else if (task === 4) { - if (deletedUIDs.length) { - self.addFlags(deletedUIDs, 'Deleted', function(e) { - cb(e, info); - }); - } else - cb(err, info); - } - }); - } -}; - -// Namespace for seqno-based commands -ImapConnection.prototype.__defineGetter__('seq', function() { - var self = this; - return { - move: function(seqnos, boxTo, cb) { - return self._move('', seqnos, boxTo, cb); - }, - copy: function(seqnos, boxTo, cb) { - return self._copy('', seqnos, boxTo, cb); - }, - delKeywords: function(seqnos, flags, cb) { - self._store('', seqnos, flags, false, cb); - }, - addKeywords: function(seqnos, flags, cb) { - return self._addKeywords('', seqnos, flags, cb); - }, - delFlags: function(seqnos, flags, cb) { - self._store('', seqnos, flags, false, cb); - }, - addFlags: function(seqnos, flags, cb) { - self._store('', seqnos, flags, true, cb); - }, - delLabels: function(seqnos, labels, cb) { - self._storeLabels('', seqnos, labels, '-', cb); - }, - addLabels: function(seqnos, labels, cb) { - self._storeLabels('', seqnos, labels, '+', cb); - }, - setLabels: function(seqnos, labels, cb) { - self._storeLabels('', seqnos, labels, '', cb); - }, - fetch: function(seqnos, options, what, cb) { - return self._fetch('', seqnos, options, what, cb); - }, - search: function(options, cb) { - self._search('', options, cb); - }, - sort: function(sorts, options, cb) { - self._sort('', sorts, options, cb); - } - }; -}); - - -// Private/Internal Functions -ImapConnection.prototype.serverSupports = function(capability) { - return (this.capabilities.indexOf(capability) > -1); -}; - -ImapConnection.prototype._store = function(which, 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 (uids === undefined) - throw new Error('The message ID(s) must be specified'); - - if (!Array.isArray(uids)) - uids = [uids]; - utils.validateUIDList(uids); - - 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)) - flags = [flags]; - for (var i=0; i= STATES.AUTH) - this._send('NOOP'); -}; - -ImapConnection.prototype._send = function(cmdstr, cb) { - if (!this._state.conn.writable) - return; - - var reqs = this._state.requests, idle = this._state.ext.idle; - - if (cmdstr !== undefined) { - var info = { - cmd: cmdstr.match(RE_CMD)[1], - cmdstr: cmdstr, - callback: cb, - cbargs: [] - }; - if (cmdstr === 'IDLE' || cmdstr === 'DONE' || cmdstr === 'NOOP') - reqs.unshift(info); - else - reqs.push(info); - } - - if (idle.state !== IDLE_NONE && cmdstr !== 'DONE') { - if ((cmdstr !== undefined || reqs.length > 1) - && idle.state === IDLE_IDLING) { - idle.state = IDLE_DONE; - this._send('DONE'); - } - return; - } - - if ((cmdstr === undefined && reqs.length) || reqs.length === 1 - || cmdstr === 'DONE') { - var prefix = '', curReq = reqs[0]; - - cmdstr = curReq.cmdstr; - - clearTimeout(this._state.tmrKeepalive); - - if (cmdstr === 'IDLE') { - // we use a different prefix to differentiate and disregard the tagged - // response the server will send us when we issue DONE - prefix = 'IDLE '; - this._state.ext.idle.state = IDLE_WAIT; - } else if (cmdstr === 'NOOP') - prefix = 'NOOP '; - else if (cmdstr !== 'DONE') - prefix = 'A' + (++this._state.curId) + ' '; - - this._state.conn.write(prefix + cmdstr + CRLF); - this.debug&&this.debug('\n==> ' + prefix + cmdstr + '\n'); - - if (curReq.cmd === 'EXAMINE' || curReq.cmd === 'SELECT') - this._state.status = STATES.BOXSELECTING; - else if (cmdstr === 'DONE') - reqs.shift(); - } -}; - -function ImapMessage() { - this.seqno = undefined; - this.uid = undefined; - this.flags = undefined; - this.date = undefined; - this.structure = undefined; - this.size = undefined; -} -inherits(ImapMessage, EventEmitter); - -function ImapFetch() { - this._parse = false; -} -inherits(ImapFetch, EventEmitter); diff --git a/lib/imap.parsers.js b/lib/imap.parsers.js deleted file mode 100644 index f062abe..0000000 --- a/lib/imap.parsers.js +++ /dev/null @@ -1,348 +0,0 @@ -var utils = require('./imap.utilities'); - -var RE_CRLF = /\r\n/g, - RE_HDR = /^([^:]+):[ \t]?(.+)?$/; - -exports.convStr = function(str, literals) { - if (str[0] === '"') - return str.substring(1, str.length-1); - else if (str === 'NIL') - return null; - else if (/^\d+$/.test(str)) { - // some IMAP extensions utilize large (64-bit) integers, which JavaScript - // can't handle natively, so we'll just keep it as a string if it's too big - var val = parseInt(str, 10); - return (val.toString() === str ? val : str); - } else if (literals && literals.lp < literals.length && /^\{\d+\}$/.test(str)) - return literals[literals.lp++]; - else - return str; -}; - -exports.parseHeaders = function(str) { - var lines = str.split(RE_CRLF), - headers = {}, m; - - for (var i = 0, h, len = lines.length; i < len; ++i) { - if (lines[i].length === 0) - continue; - if (lines[i][0] === '\t' || lines[i][0] === ' ') { - // folded header content - // RFC2822 says to just remove the CRLF and not the whitespace following - // it, so we follow the RFC and include the leading whitespace ... - headers[h][headers[h].length - 1] += lines[i]; - } else { - m = RE_HDR.exec(lines[i]); - h = m[1].toLowerCase(); - if (m[2]) { - if (headers[h] === undefined) - headers[h] = [m[2]]; - else - headers[h].push(m[2]); - } else - headers[h] = ['']; - } - } - return headers; -}; - -exports.parseNamespaces = function(str, literals) { - var result, vals; - if (str.length === 3 && str.toUpperCase() === 'NIL') - vals = null; - else { - result = exports.parseExpr(str, literals); - vals = []; - for (var i = 0, len = result.length; i < len; ++i) { - var val = { - prefix: result[i][0], - delimiter: result[i][1] - }; - if (result[i].length > 2) { - // extension data - val.extensions = []; - for (var j = 2, len2 = result[i].length; j < len2; j += 2) { - val.extensions.push({ - name: result[i][j], - flags: result[i][j + 1] - }); - } - } - vals.push(val); - } - } - return vals; -}; - -exports.parseFetchBodies = function(str, literals) { - literals.lp = 0; - var result = exports.parseExpr(str, literals), - bodies; - for (var i = 0, len = result.length; i < len; i += 2) { - if (Array.isArray(result[i])) { - if (result[i].length === 0) - result[i].push(''); - else if (result[i].length > 1) { - // HEADER.FIELDS (foo) or HEADER.FIELDS (foo bar baz) - result[i][0] += ' ('; - if (Array.isArray(result[i][1])) - result[i][0] += result[i][1].join(' '); - else - result[i][0] += result[i].slice(1).join(' '); - result[i][0] += ')'; - } - if (bodies === undefined) - bodies = ['BODY[' + result[i][0] + ']', result[i + 1]]; - else { - bodies.push('BODY[' + result[i][0] + ']'); - bodies.push(result[i + 1]); - } - } - } - return bodies; -}; - -exports.parseFetch = function(str, literals, fetchData) { - literals.lp = 0; - var result = exports.parseExpr(str, literals, false, 0, false); - for (var i = 0, len = result.length; i < len; i += 2) { - result[i] = result[i].toUpperCase(); - if (/^BODY\[/.test(result[i])) - continue; - if (result[i] === 'UID') - fetchData.uid = parseInt(result[i + 1], 10); - else if (result[i] === 'INTERNALDATE') - fetchData.date = result[i + 1]; - else if (result[i] === 'FLAGS') - fetchData.flags = result[i + 1].filter(utils.isNotEmpty); - else if (result[i] === 'BODYSTRUCTURE') - fetchData.structure = exports.parseBodyStructure(result[i + 1], literals); - else if (result[i] === 'RFC822.SIZE') - fetchData.size = parseInt(result[i + 1], 10); - else if (typeof result[i] === 'string') // simple extensions - fetchData[result[i].toLowerCase()] = result[i + 1]; - } -}; - -exports.parseBodyStructure = function(cur, literals, prefix, partID) { - var ret = [], i, len; - if (prefix === undefined) { - var result = (Array.isArray(cur) ? cur : exports.parseExpr(cur, literals)); - if (result.length) - ret = exports.parseBodyStructure(result, literals, '', 1); - } else { - var part, partLen = cur.length, next; - if (Array.isArray(cur[0])) { // multipart - next = -1; - while (Array.isArray(cur[++next])) { - ret.push(exports.parseBodyStructure(cur[next], literals, prefix - + (prefix !== '' ? '.' : '') - + (partID++).toString(), 1)); - } - part = { type: cur[next++].toLowerCase() }; - if (partLen > next) { - if (Array.isArray(cur[next])) { - part.params = {}; - for (i = 0, len = cur[next].length; i < len; i += 2) - part.params[cur[next][i].toLowerCase()] = cur[next][i + 1]; - } else - part.params = cur[next]; - ++next; - } - } else { // single part - next = 7; - if (typeof cur[1] === 'string') { - part = { - // the path identifier for this part, useful for fetching specific - // parts of a message - partID: (prefix !== '' ? prefix : '1'), - - // required fields as per RFC 3501 -- null or otherwise - type: cur[0].toLowerCase(), subtype: cur[1].toLowerCase(), - params: null, id: cur[3], description: cur[4], encoding: cur[5], - size: cur[6] - }; - } else { - // type information for malformed multipart body - part = { type: cur[0].toLowerCase(), params: null }; - cur.splice(1, 0, null); - ++partLen; - next = 2; - } - if (Array.isArray(cur[2])) { - part.params = {}; - for (i = 0, len = cur[2].length; i < len; i += 2) - part.params[cur[2][i].toLowerCase()] = cur[2][i + 1]; - if (cur[1] === null) - ++next; - } - if (part.type === 'message' && part.subtype === 'rfc822') { - // envelope - if (partLen > next && Array.isArray(cur[next])) { - part.envelope = {}; - for (i = 0, len = cur[next].length; i < len; ++i) { - if (i === 0) - part.envelope.date = cur[next][i]; - else if (i === 1) - part.envelope.subject = cur[next][i]; - else if (i >= 2 && i <= 7) { - var val = cur[next][i]; - if (Array.isArray(val)) { - var addresses = [], inGroup = false, curGroup; - for (var j = 0, len2 = val.length; j < len2; ++j) { - if (val[j][3] === null) { // start group addresses - inGroup = true; - curGroup = { - group: val[j][2], - addresses: [] - }; - } else if (val[j][2] === null) { // end of group addresses - inGroup = false; - addresses.push(curGroup); - } else { // regular user address - var info = { - name: val[j][0], - mailbox: val[j][2], - host: val[j][3] - }; - if (inGroup) - curGroup.addresses.push(info); - else - addresses.push(info); - } - } - val = addresses; - } - if (i === 2) - part.envelope.from = val; - else if (i === 3) - part.envelope.sender = val; - else if (i === 4) - part.envelope['reply-to'] = val; - else if (i === 5) - part.envelope.to = val; - else if (i === 6) - part.envelope.cc = val; - else if (i === 7) - part.envelope.bcc = val; - } else if (i === 8) - // message ID being replied to - part.envelope['in-reply-to'] = cur[next][i]; - else if (i === 9) - part.envelope['message-id'] = cur[next][i]; - else - break; - } - } else - part.envelope = null; - ++next; - - // body - if (partLen > next && Array.isArray(cur[next])) - part.body = exports.parseBodyStructure(cur[next], literals, prefix, 1); - else - part.body = null; - ++next; - } - if ((part.type === 'text' - || (part.type === 'message' && part.subtype === 'rfc822')) - && partLen > next) - part.lines = cur[next++]; - if (typeof cur[1] === 'string' && partLen > next) - part.md5 = cur[next++]; - } - // add any extra fields that may or may not be omitted entirely - exports.parseStructExtra(part, partLen, cur, next); - ret.unshift(part); - } - return ret; -}; - -exports.parseStructExtra = function(part, partLen, cur, next) { - if (partLen > next) { - // disposition - // null or a special k/v list with these kinds of values: - // e.g.: ['Foo', null] - // ['Foo', ['Bar', 'Baz']] - // ['Foo', ['Bar', 'Baz', 'Bam', 'Pow']] - var disposition = { type: null, params: null }; - if (Array.isArray(cur[next])) { - disposition.type = cur[next][0]; - if (Array.isArray(cur[next][1])) { - disposition.params = {}; - for (var i = 0, len = cur[next][1].length, key; i < len; i += 2) { - key = cur[next][1][i].toLowerCase(); - disposition.params[key] = cur[next][1][i + 1]; - } - } - } else if (cur[next] !== null) - disposition.type = cur[next]; - - if (disposition.type === null) - part.disposition = null; - else - part.disposition = disposition; - - ++next; - } - if (partLen > next) { - // language can be a string or a list of one or more strings, so let's - // make this more consistent ... - if (cur[next] !== null) - part.language = (Array.isArray(cur[next]) ? cur[next] : [cur[next]]); - else - part.language = null; - ++next; - } - if (partLen > next) - part.location = cur[next++]; - if (partLen > next) { - // extension stuff introduced by later RFCs - // this can really be any value: a string, number, or (un)nested list - // let's not parse it for now ... - part.extensions = cur[next]; - } -}; - -exports.parseExpr = function(o, literals, result, start, useBrackets) { - start = start || 0; - var inQuote = false, lastPos = start - 1, isTop = false, val; - - if (useBrackets === undefined) - useBrackets = true; - if (!result) - result = []; - if (typeof o === 'string') { - o = { str: o }; - isTop = true; - } - for (var i = start, len = o.str.length; i < len; ++i) { - if (!inQuote) { - if (o.str[i] === '"') - inQuote = true; - else if (o.str[i] === ' ' || o.str[i] === ')' - || (useBrackets && o.str[i] === ']')) { - if (i - (lastPos + 1) > 0) { - val = exports.convStr(o.str.substring(lastPos + 1, i), literals); - result.push(val); - } - if ((o.str[i] === ')' || (useBrackets && o.str[i] === ']')) && !isTop) - return i; - lastPos = i; - } else if ((o.str[i] === '(' || (useBrackets && o.str[i] === '['))) { - var innerResult = []; - i = exports.parseExpr(o, literals, innerResult, i + 1, useBrackets); - lastPos = i; - result.push(innerResult); - } - } else if (o.str[i] === '"' && - (o.str[i - 1] && - (o.str[i - 1] !== '\\' - || (o.str[i - 2] && o.str[i - 2] === '\\') - ))) - inQuote = false; - if (i + 1 === len && len - (lastPos + 1) > 0) - result.push(exports.convStr(o.str.substring(lastPos + 1), literals)); - } - return (isTop ? result : start); -}; diff --git a/lib/imap.utilities.js b/lib/imap.utilities.js deleted file mode 100644 index 26a161e..0000000 --- a/lib/imap.utilities.js +++ /dev/null @@ -1,242 +0,0 @@ -exports.MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', - 'Oct', 'Nov', 'Dec']; - -exports.isNotEmpty = function(str) { - return str.trim().length > 0; -}; - -exports.escape = function(str) { - return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); -}; - -exports.unescape = function(str) { - return str.replace(/\\"/g, '"').replace(/\\\\/g, '\\'); -}; - -exports.buildSearchQuery = function(options, extensions, isOrChild) { - var searchargs = ''; - 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 or array. Got: ' + typeof criteria); - if (criteria === 'OR') { - if (args.length !== 2) - throw new Error('OR must have exactly two arguments'); - searchargs += ' OR ('; - searchargs += exports.buildSearchQuery(args[0], extensions, true); - searchargs += ') ('; - searchargs += exports.buildSearchQuery(args[1], extensions, true); - searchargs += ')'; - } else { - if (criteria[0] === '!') { - modifier += 'NOT '; - criteria = criteria.substr(1); - } - switch(criteria) { - // -- Standard criteria -- - case 'ALL': - 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 + ' "' + exports.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 string'); - } - searchargs += modifier + criteria + ' ' + args[0].getDate() + '-' - + exports.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], 10); - 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 + ' "' + exports.escape(''+args[0]) - + '" "' + exports.escape(''+args[1]) + '"'; - break; - case 'UID': - if (!args) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - exports.validateUIDList(args); - searchargs += modifier + criteria + ' ' + args.join(','); - break; - // -- Extensions criteria -- - case 'X-GM-MSGID': // Gmail unique message ID - case 'X-GM-THRID': // Gmail thread ID - if (extensions.indexOf('X-GM-EXT-1') === -1) - throw new Error('IMAP extension not available: ' + criteria); - var val; - if (!args || args.length !== 1) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - else { - val = ''+args[0]; - if (!(/^\d+$/.test(args[0]))) - throw new Error('Invalid value'); - } - searchargs += modifier + criteria + ' ' + val; - break; - case 'X-GM-RAW': // Gmail search syntax - if (extensions.indexOf('X-GM-EXT-1') === -1) - throw new Error('IMAP extension not available: ' + criteria); - if (!args || args.length !== 1) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - searchargs += modifier + criteria + ' "' + exports.escape(''+args[0]) - + '"'; - break; - case 'X-GM-LABELS': // Gmail labels - if (extensions.indexOf('X-GM-EXT-1') === -1) - throw new Error('IMAP extension not available: ' + criteria); - if (!args || args.length !== 1) - throw new Error('Incorrect number of arguments for search option: ' - + criteria); - searchargs += modifier + criteria + ' ' + args[0]; - break; - default: - try { - // last hope it's a seqno set - // http://tools.ietf.org/html/rfc3501#section-6.4.4 - var seqnos = (args ? [criteria].concat(args) : [criteria]); - exports.validateUIDList(seqnos); - searchargs += modifier + seqnos.join(','); - } catch(e) { - throw new Error('Unexpected search option: ' + criteria); - } - } - } - if (isOrChild) - break; - } - return searchargs; -}; - -exports.validateUIDList = function(uids) { - for (var i=0,len=uids.length,intval; i 1) - uids = ['*']; - break; - } else if (/^(?:[\d]+|\*):(?:[\d]+|\*)$/.test(uids[i])) - continue; - } - intval = parseInt(''+uids[i], 10); - if (isNaN(intval)) { - throw new Error('Message ID/number must be an integer, "*", or a range: ' - + uids[i]); - } else if (typeof uids[i] !== 'number') - uids[i] = intval; - } -}; - -var CHARR_CRLF = [13, 10]; -function line(b, s) { - var len = b.length, p = b.p, start = p, ret = false, retest = false; - while (p < len && !ret) { - if (b[p] === CHARR_CRLF[s.p]) { - if (++s.p === 2) - ret = true; - } else { - retest = (s.p > 0); - s.p = 0; - if (retest) - continue; - } - ++p; - } - if (ret === false) { - if (s.ret) - s.ret += b.toString('ascii', start); - else - s.ret = b.toString('ascii', start); - } else { - var iCR = p - 2; - if (iCR < 0) { - // the CR is at the end of s.ret - if (s.ret && s.ret.length > 1) - ret = s.ret.substr(0, s.ret.length - 1); - else - ret = ''; - } else { - // the entire CRLF is in b - if (iCR === 0) - ret = (s.ret ? s.ret : ''); - else { - if (s.ret) { - ret = s.ret; - ret += b.toString('ascii', start, iCR); - } else - ret = b.toString('ascii', start, iCR); - } - } - s.p = 0; - s.ret = undefined; - } - b.p = p; - return ret; -} - -exports.line = line; diff --git a/lib/xregexp.js b/lib/xregexp.js deleted file mode 100644 index 7150857..0000000 --- a/lib/xregexp.js +++ /dev/null @@ -1,3954 +0,0 @@ -/*! - * XRegExp All 3.0.0-pre - * - * Steven Levithan © 2012 MIT License - */ - -// Module systems magic dance -;(function(definition) { - // Don't turn on strict mode for this function, so it can assign to global - var self; - - // RequireJS - if (typeof define === 'function') { - define(definition); - // CommonJS - } else if (typeof exports === 'object') { - self = definition(); - // Use Node.js's `module.exports`. This supports both `require('xregexp')` and - // `require('xregexp').XRegExp` - (typeof module === 'object' ? (module.exports = self) : exports).XRegExp = self; - //