module rewrite

fork
mscdex 11 years ago
parent ed7203022c
commit 5eb8334553

@ -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:** <no debug output>
* **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 '-'):

File diff suppressed because it is too large Load Diff

@ -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;

File diff suppressed because it is too large Load Diff

@ -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);
};

@ -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<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 += 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<len; i++) {
if (typeof uids[i] === 'string') {
if (uids[i] === '*' || uids[i] === '*:*') {
if (len > 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;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,72 @@
var parseBodyStructure = require('../lib/Parser').parseBodyStructure;
var assert = require('assert'),
inspect = require('util').inspect;
[
{ source: '("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 1152 23)'
+ '("TEXT" "PLAIN" ("CHARSET" "US-ASCII" "NAME" "cc.diff")'
+ ' "<960723163407.20117h@cac.washington.edu>" "Compiler diff"'
+ ' "BASE64" 4554 73)'
+ '"MIXED"',
expected: [ { type: 'mixed' },
[ { partID: '1',
type: 'text',
subtype: 'plain',
params: { charset: 'US-ASCII' },
id: null,
description: null,
encoding: '7BIT',
size: 1152,
lines: 23
}
],
[ { partID: '2',
type: 'text',
subtype: 'plain',
params: { charset: 'US-ASCII', name: 'cc.diff' },
id: '<960723163407.20117h@cac.washington.edu>',
description: 'Compiler diff',
encoding: 'BASE64',
size: 4554,
lines: 73
}
]
],
what: 'RFC3501 example #1'
},
{ source: '"TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92',
expected: [ { partID: '1',
type: 'text',
subtype: 'plain',
params: { charset: 'US-ASCII' },
id: null,
description: null,
encoding: '7BIT',
size: 3028,
lines: 92
}
],
what: 'RFC3501 example #2'
},
].forEach(function(v) {
var result;
try {
result = parseBodyStructure(v.source);
} catch (e) {
console.log(makeMsg(v.what, 'JS Exception: ' + e.stack));
return;
}
assert.deepEqual(result,
v.expected,
makeMsg(v.what,
'Result mismatch:'
+ '\nParsed: ' + inspect(result, false, 10)
+ '\nExpected: ' + inspect(v.expected, false, 10)
)
);
});
function makeMsg(what, msg) {
return '[' + what + ']: ' + msg;
}

@ -0,0 +1,109 @@
var parseExpr = require('../lib/Parser').parseExpr,
parseEnvelopeAddresses = require('../lib/Parser').parseEnvelopeAddresses;
var assert = require('assert'),
inspect = require('util').inspect;
[
{ source: '("Terry Gray" NIL "gray" "cac.washington.edu")',
expected: [ { name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
],
what: 'RFC3501 example #1'
},
{ source: '(NIL NIL "imap" "cac.washington.edu")',
expected: [ { name: null,
mailbox: 'imap',
host: 'cac.washington.edu'
}
],
what: 'RFC3501 example #2'
},
{ source: '(NIL NIL "imap" NIL)'
+ '(NIL NIL NIL NIL)',
expected: [ { group: 'imap',
addresses: []
}
],
what: 'Zero-length group'
},
{ source: '(NIL NIL "imap" NIL)'
+ '("Terry Gray" NIL "gray" "cac.washington.edu")'
+ '(NIL NIL NIL NIL)',
expected: [ { group: 'imap',
addresses: [
{ name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
]
}
],
what: 'One-length group'
},
{ source: '(NIL NIL "imap" NIL)'
+ '("Terry Gray" NIL "gray" "cac.washington.edu")'
+ '(NIL NIL NIL NIL)'
+ '(NIL NIL "imap" "cac.washington.edu")',
expected: [ { group: 'imap',
addresses: [
{ name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
]
},
{ name: null,
mailbox: 'imap',
host: 'cac.washington.edu'
}
],
what: 'One-length group and address'
},
{ source: '(NIL NIL "imap" NIL)'
+ '("Terry Gray" NIL "gray" "cac.washington.edu")',
expected: [ { group: 'imap',
addresses: [
{ name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
]
}
],
what: 'Implicit group end'
},
{ source: '("Terry Gray" NIL "gray" "cac.washington.edu")'
+ '(NIL NIL NIL NIL)',
expected: [ { name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
],
what: 'Group end without start'
},
].forEach(function(v) {
var result;
try {
result = parseEnvelopeAddresses(parseExpr(v.source));
} catch (e) {
console.log(makeMsg(v.what, 'JS Exception: ' + e.stack));
return;
}
assert.deepEqual(result,
v.expected,
makeMsg(v.what,
'Result mismatch:'
+ '\nParsed: ' + inspect(result, false, 10)
+ '\nExpected: ' + inspect(v.expected, false, 10)
)
);
});
function makeMsg(what, msg) {
return '[' + what + ']: ' + msg;
}

@ -0,0 +1,57 @@
var parseExpr = require('../lib/Parser').parseExpr;
var assert = require('assert'),
inspect = require('util').inspect;
[
{ source: '',
expected: [],
what: 'Empty value'
},
{ source: 'FLAGS NIL RFC822.SIZE 44827',
expected: ['FLAGS', null, 'RFC822.SIZE', 44827],
what: 'Simple, two key-value pairs with nil'
},
{ source: 'FLAGS (\\Seen) RFC822.SIZE 44827',
expected: ['FLAGS', ['\\Seen'], 'RFC822.SIZE', 44827],
what: 'Simple, two key-value pairs with list'
},
{ source: 'RFC822.SIZE 9007199254740993',
expected: ['RFC822.SIZE', '9007199254740993'],
what: 'Integer exceeding JavaScript max int size'
},
{ source: 'FLAGS (\\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700"',
expected: ['FLAGS', ['\\Seen'], 'INTERNALDATE', '17-Jul-1996 02:44:25 -0700'],
what: 'Quoted string'
},
{ source: '("Foo")("Bar") ("Baz")',
expected: [['Foo'], ['Bar'], ['Baz']],
what: 'Lists with varying spacing'
},
{ source: '""',
expected: [''],
what: 'Empty quoted string'
},
].forEach(function(v) {
var result;
try {
result = parseExpr(v.source);
} catch (e) {
console.log(makeMsg(v.what, 'JS Exception: ' + e.stack));
return;
}
assert.deepEqual(result,
v.expected,
makeMsg(v.what,
'Result mismatch:'
+ '\nParsed: ' + inspect(result, false, 10)
+ '\nExpected: ' + inspect(v.expected, false, 10)
)
);
});
function makeMsg(what, msg) {
return '[' + what + ']: ' + msg;
}

@ -1,236 +0,0 @@
var assert = require('assert');
var parseFetch = require('../lib/imap.parsers').parseFetch;
var tests = {
simple: [
['', {}],
['FLAGS (\\Seen) RFC822.SIZE 44827',
{ flags: [ '\\Seen' ], 'rfc822.size': 44827 }],
['FLAGS (\\Seen) UID 4827313',
{ flags: [ '\\Seen' ], id: 4827313 }],
['FLAGS (\\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700"\
RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)"\
"IMAP4rev1 WG mtg summary and minutes"\
(("Terry Gray" NIL "gray" "cac.washington.edu"))\
(("Terry Gray" NIL "gray" "cac.washington.edu"))\
(("Terry Gray" NIL "gray" "cac.washington.edu"))\
((NIL NIL "imap" "cac.washington.edu"))\
((NIL NIL "minutes" "CNRI.Reston.VA.US")\
("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL\
"<B27397-0100000@cac.washington.edu>")\
BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028\
92)',
{ flags: [ '\\Seen' ],
date: '17-Jul-1996 02:44:25 -0700',
'rfc822.size': 4286,
envelope: [
'Wed, 17 Jul 1996 02:23:25 -0700 (PDT)',
'IMAP4rev1 WG mtg summary and minutes',
[ [ 'Terry Gray', null, 'gray', 'cac.washington.edu' ] ],
[ [ 'Terry Gray', null, 'gray', 'cac.washington.edu' ] ],
[ [ 'Terry Gray', null, 'gray', 'cac.washington.edu' ] ],
[ [ null, null, 'imap', 'cac.washington.edu' ] ],
[ [ null, null, 'minutes', 'CNRI.Reston.VA.US' ],
[ 'John Klensin', null, 'KLENSIN', 'MIT.EDU' ] ],
null,
null,
'<B27397-0100000@cac.washington.edu>'
],
body: [
'TEXT',
'PLAIN',
[ 'CHARSET', 'US-ASCII' ],
null,
null,
'7BIT',
3028,
92
]
}
],
['BODYSTRUCTURE (("text" "plain" ("charset" "us-ascii") NIL NIL "quoted-printable" 370 19 NIL NIL NIL NIL)("application" "pdf" ("x-mac-hide-extension" "yes" "x-unix-mode" "0644" "name" "filename.pdf") NIL NIL "base64" 2067794 NIL ("inline" ("filename" "filename.pdf")) NIL NIL) "mixed" ("boundary" "Apple-Mail=_801866EE-56A0-4F30-8DB3-2496094971D1") NIL NIL NIL)',
{ structure:
[ { type: 'mixed',
params: { boundary: 'Apple-Mail=_801866EE-56A0-4F30-8DB3-2496094971D1' },
disposition: null,
language: null,
location: null
},
[ { partID: '1',
type: 'text',
subtype: 'plain',
params: { charset: 'us-ascii' },
id: null,
description: null,
encoding: 'quoted-printable',
size: 370,
lines: 19,
md5: null,
disposition: null,
language: null,
location: null
} ],
[ { partID: '2',
type: 'application',
subtype: 'pdf',
params:
{ 'x-mac-hide-extension': 'yes',
'x-unix-mode': '0644',
name: 'filename.pdf' },
id: null,
description: null,
encoding: 'base64',
size: 2067794,
md5: null,
disposition: { type: 'inline', params: { filename: 'filename.pdf' } },
language: null,
location: null
} ]
]
}
],
['BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 266 16 NIL ("INLINE" NIL) NIL)("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "7BIT" 7343 65 NIL ("INLINE" NIL) NIL) "ALTERNATIVE" ("BOUNDARY" "--4c81da7e1fb95-MultiPart-Mime-Boundary") NIL NIL)',
{ structure:
[ { type: 'alternative',
params: { boundary: '--4c81da7e1fb95-MultiPart-Mime-Boundary' },
disposition: null,
language: null
},
[ { partID: '1',
type: 'text',
subtype: 'plain',
params: { charset: 'utf-8' },
id: null,
description: null,
encoding: '7BIT',
size: 266,
lines: 16,
md5: null,
disposition: { type: 'INLINE', params: null },
language: null
} ],
[ { partID: '2',
type: 'text',
subtype: 'html',
params: { charset: 'utf-8' },
id: null,
description: null,
encoding: '7BIT',
size: 7343,
lines: 65,
md5: null,
disposition: { type: 'INLINE', params: null },
language: null
} ]
]
}
],
['BODYSTRUCTURE (("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "7BIT" 935 46 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 1962 33 NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "000e0cd294e80dc83c0475bf339b") NIL NIL)',
{ structure:
[ { type: 'alternative',
params: { boundary: '000e0cd294e80dc83c0475bf339b' },
disposition: null,
language: null
},
[ { partID: '1',
type: 'text',
subtype: 'plain',
params: { charset: 'ISO-8859-1' },
id: null,
description: null,
encoding: '7BIT',
size: 935,
lines: 46,
md5: null,
disposition: null,
language: null
} ],
[ { partID: '2',
type: 'text',
subtype: 'html',
params: { charset: 'ISO-8859-1' },
id: null,
description: null,
encoding: 'QUOTED-PRINTABLE',
size: 1962,
lines: 33,
md5: null,
disposition: null,
language: null
} ]
]
}
],
['BODYSTRUCTURE ((("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "7BIT" 935 46 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 1962 33 NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "000e0cd294e80dc83c0475bf339b") NIL NIL)("APPLICATION" "OCTET-STREAM" ("NAME" "license") NIL NIL "BASE64" 98 NIL ("ATTACHMENT" ("FILENAME" "license")) NIL) "MIXED" ("BOUNDARY" "000e0cd294e80dc84c0475bf339d") NIL NIL)',
{ structure:
[ { type: 'mixed',
params: { boundary: '000e0cd294e80dc84c0475bf339d' },
disposition: null,
language: null
},
[ { type: 'alternative',
params: { boundary: '000e0cd294e80dc83c0475bf339b' },
disposition: null,
language: null
},
[ { partID: '1.1',
type: 'text',
subtype: 'plain',
params: { charset: 'ISO-8859-1' },
id: null,
description: null,
encoding: '7BIT',
size: 935,
lines: 46,
md5: null,
disposition: null,
language: null
} ],
[ { partID: '1.2',
type: 'text',
subtype: 'html',
params: { charset: 'ISO-8859-1' },
id: null,
description: null,
encoding: 'QUOTED-PRINTABLE',
size: 1962,
lines: 33,
md5: null,
disposition: null,
language: null
} ]
],
[ { partID: '2',
type: 'application',
subtype: 'octet-stream',
params: { name: 'license' },
id: null,
description: null,
encoding: 'BASE64',
size: 98,
md5: null,
disposition: { type: 'ATTACHMENT', params: { filename: 'license' } },
language: null
} ]
]
}
]
]
};
var result;
for (var i=0,len=tests.simple.length; i<len; ++i) {
result = {};
parseFetch(tests.simple[i][0], result);
assert.deepEqual(tests.simple[i][1], result);
}

@ -1,54 +0,0 @@
var assert = require('assert');
var parseNamespaces = require('../lib/imap.parsers').parseNamespaces;
var tests = {
simple: [
['', {}],
['(("" "/")) NIL NIL',
{ personal: [ { prefix: '', delim: '/' } ] }],
['(("INBOX." ".")) NIL NIL',
{ personal: [ { prefix: 'INBOX.', delim: '.' } ] }],
['NIL NIL (("" "."))',
{ shared: [ { prefix: '', delim: '.' } ] }],
['(("" "/")) NIL (("Public Folders/" "/"))',
{ personal: [ { prefix: '', delim: '/' } ],
shared: [ { prefix: 'Public Folders/', delim: '/' } ] }
],
['(("" "/")) (("~" "/")) (("#shared/" "/")("#public/" "/")("#ftp/" "/")("#news." "."))',
{ personal: [ { prefix: '', delim: '/' } ],
other: [ { prefix: '~', delim: '/' } ],
shared:
[ { prefix: '#shared/', delim: '/' },
{ prefix: '#public/', delim: '/' },
{ prefix: '#ftp/', delim: '/' },
{ prefix: '#news.', delim: '.' }
]
}
],
['(("" "/")("#mh/" "/" "X-PARAM" ("FLAG1" "FLAG2"))) NIL NIL',
{ personal:
[ { prefix: '', delim: '/' },
{ prefix: '#mh/',
delim: '/',
extensions: [ { name: 'X-PARAM', flags: [ 'FLAG1', 'FLAG2' ] } ]
}
]
}
]
]
};
var result;
for (var i=0,len=tests.simple.length; i<len; ++i) {
result = {};
parseNamespaces(tests.simple[i][0], result);
assert.deepEqual(tests.simple[i][1], result);
}

@ -0,0 +1,422 @@
var Parser = require('../lib/Parser').Parser;
var assert = require('assert'),
crypto = require('crypto'),
inspect = require('util').inspect;
var CR = '\r', LF = '\n', CRLF = CR + LF;
[
{ source: ['A1 OK LOGIN completed', CRLF],
expected: [ { type: 'ok',
tagnum: 1,
textCode: undefined,
text: 'LOGIN completed'
}
],
what: 'Tagged OK'
},
{ source: ['IDLE OK IDLE terminated', CRLF],
expected: [ 'IDLE OK IDLE terminated' ],
what: 'Unknown line'
},
{ source: ['+ idling', CRLF],
expected: [ { textCode: undefined,
text: 'idling'
}
],
what: 'Continuation'
},
{ source: ['+ [ALERT] idling', CRLF],
expected: [ { textCode: 'ALERT',
text: 'idling'
}
],
what: 'Continuation with text code'
},
{ source: ['* NAMESPACE ',
'(("" "/")) ',
'(("~" "/")) ',
'(("#shared/" "/")("#public/" "/")("#ftp/" "/")("#news." "."))',
CRLF],
expected: [ { type: 'namespace',
num: undefined,
textCode: undefined,
text: {
personal: [
{ prefix: '',
delimiter: '/',
extensions: undefined
}
],
other: [
{ prefix: '~',
delimiter: '/',
extensions: undefined
}
],
shared: [
{ prefix: '#shared/',
delimiter: '/',
extensions: undefined
},
{ prefix: '#public/',
delimiter: '/',
extensions: undefined
},
{ prefix: '#ftp/',
delimiter: '/',
extensions: undefined
},
{ prefix: '#news.',
delimiter: '.',
extensions: undefined
}
]
}
}
],
what: 'Multiple namespaces'
},
{ source: ['* NAMESPACE ',
'(("" "/" "X-PARAM" ("FLAG1" "FLAG2"))) ',
'NIL ',
'NIL',
CRLF],
expected: [ { type: 'namespace',
num: undefined,
textCode: undefined,
text: {
personal: [
{ prefix: '',
delimiter: '/',
extensions: {
'X-PARAM': [ 'FLAG1', 'FLAG2' ]
}
}
],
other: null,
shared: null
}
}
],
what: 'Multiple namespaces'
},
{ source: ['* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)', CRLF],
expected: [ { type: 'flags',
num: undefined,
textCode: undefined,
text: [
'\\Answered',
'\\Flagged',
'\\Deleted',
'\\Seen',
'\\Draft'
]
}
],
what: 'Flags'
},
{ source: ['* SEARCH 2 3 6', CRLF],
expected: [ { type: 'search',
num: undefined,
textCode: undefined,
text: [ 2, 3, 6 ]
}
],
what: 'Search'
},
{ source: ['* LIST (\\Noselect) "/" ~/Mail/foo', CRLF],
expected: [ { type: 'list',
num: undefined,
textCode: undefined,
text: {
flags: [ '\\Noselect' ],
delimiter: '/',
name: '~/Mail/foo'
}
}
],
what: 'List'
},
{ source: ['* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)', CRLF],
expected: [ { type: 'status',
num: undefined,
textCode: undefined,
text: {
name: 'blurdybloop',
attrs: { messages: 231, uidnext: 44292 }
}
}
],
what: 'Status'
},
{ source: ['* OK [UNSEEN 17] Message 17 is the first unseen message', CRLF],
expected: [ { type: 'ok',
num: undefined,
textCode: {
key: 'UNSEEN',
val: 17
},
text: 'Message 17 is the first unseen message'
}
],
what: 'Untagged OK (with text code, with text)'
},
{ source: ['* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited', CRLF],
expected: [ { type: 'ok',
num: undefined,
textCode: {
key: 'PERMANENTFLAGS',
val: [ '\\Deleted', '\\Seen', '\\*' ]
},
text: 'Limited'
}
],
what: 'Untagged OK (with text code, with text)'
},
{ source: ['* OK [UNSEEN 17]', CRLF],
expected: [ { type: 'ok',
num: undefined,
textCode: undefined,
text: '[UNSEEN 17]'
}
],
what: 'Untagged OK (no text code, with text) (RFC violation)'
},
{ source: ['* OK IMAP4rev1 Service Ready', CRLF],
expected: [ { type: 'ok',
num: undefined,
textCode: undefined,
text: 'IMAP4rev1 Service Ready'
}
],
what: 'Untagged OK (no text code, with text)'
},
{ source: ['* OK', CRLF], // I have seen servers that send stuff like this ..
expected: [ { type: 'ok',
num: undefined,
textCode: undefined,
text: undefined
}
],
what: 'Untagged OK (no text code, no text) (RFC violation)'
},
{ source: ['* 18 EXISTS', CRLF],
expected: [ { type: 'exists',
num: 18,
textCode: undefined,
text: undefined
}
],
what: 'Untagged EXISTS'
},
{ source: ['* 2 RECENT', CRLF],
expected: [ { type: 'recent',
num: 2,
textCode: undefined,
text: undefined
}
],
what: 'Untagged RECENT'
},
{ source: ['* 12 FETCH (BODY[HEADER] {342}', CRLF,
'Date: Wed, 17 Jul 1996 02:23:25 -0700 (PDT)', CRLF,
'From: Terry Gray <gray@cac.washington.edu>', CRLF,
'Subject: IMAP4rev1 WG mtg summary and minutes', CRLF,
'To: imap@cac.washington.edu', CRLF,
'cc: minutes@CNRI.Reston.VA.US, John Klensin <KLENSIN@MIT.EDU>', CRLF,
'Message-Id: <B27397-0100000@cac.washington.edu>', CRLF,
'MIME-Version: 1.0', CRLF,
'Content-Type: TEXT/PLAIN; CHARSET=US-ASCII', CRLF, CRLF,
')', CRLF],
expected: [ { seqno: 12,
which: 'HEADER',
size: 342
},
{ type: 'fetch',
num: 12,
textCode: undefined,
text: {}
}
],
bodySHA1s: ['1f96faf50f6410f99237791f9e3b89454bf93fa7'],
what: 'Untagged FETCH (body)'
},
{ source: ['* 12 FETCH (INTERNALDATE {26}', CRLF,
'17-Jul-1996 02:44:25 -0700)' + CRLF],
expected: [ { type: 'fetch',
num: 12,
textCode: undefined,
text: {
internaldate: new Date('17-Jul-1996 02:44:25 -0700')
}
}
],
what: 'Untagged FETCH with non-body literal'
},
{ source: ['* 12 FETCH (INTERNALDATE {2',
'6}' + CRLF + '17-Jul-1996 02:44:25 -0700)' + CRLF],
expected: [ { type: 'fetch',
num: 12,
textCode: undefined,
text: {
internaldate: new Date('17-Jul-1996 02:44:25 -0700')
}
}
],
what: 'Untagged FETCH with non-body literal (length split)'
},
{ source: ['* 12 FETCH (INTERNALDATE {26}', CRLF,
'17-Jul-1996 02:44:25 -0700)' + CR,
LF],
expected: [ { type: 'fetch',
num: 12,
textCode: undefined,
text: {
internaldate: new Date('17-Jul-1996 02:44:25 -0700')
}
}
],
what: 'Untagged FETCH with non-body literal (split CRLF)'
},
{ source: ['* 12 FETCH (FLAGS (\\Seen)',
' INTERNALDATE "17-Jul-1996 02:44:25 -0700"',
' RFC822.SIZE 4286',
' ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)"',
' "IMAP4rev1 WG mtg summary and minutes"',
' (("Terry Gray" NIL "gray" "cac.washington.edu"))',
' (("Terry Gray" NIL "gray" "cac.washington.edu"))',
' (("Terry Gray" NIL "gray" "cac.washington.edu"))',
' ((NIL NIL "imap" "cac.washington.edu"))',
' ((NIL NIL "minutes" "CNRI.Reston.VA.US")',
'("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL',
' "<B27397-0100000@cac.washington.edu>")',
' BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028',
' 92))',
CRLF],
expected: [ { type: 'fetch',
num: 12,
textCode: undefined,
text: {
flags: [ '\\Seen' ],
internaldate: new Date('17-Jul-1996 02:44:25 -0700'),
'rfc822.size': 4286,
envelope: {
date: new Date('Wed, 17 Jul 1996 02:23:25 -0700 (PDT)'),
subject: 'IMAP4rev1 WG mtg summary and minutes',
from: [
{ name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
],
sender: [
{ name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
],
replyTo: [
{ name: 'Terry Gray',
mailbox: 'gray',
host: 'cac.washington.edu'
}
],
to: [
{ name: null,
mailbox: 'imap',
host: 'cac.washington.edu'
}
],
cc: [
{ name: null,
mailbox: 'minutes',
host: 'CNRI.Reston.VA.US'
},
{ name: 'John Klensin',
mailbox: 'KLENSIN',
host: 'MIT.EDU'
}
],
bcc: null,
inReplyTo: null,
messageId: '<B27397-0100000@cac.washington.edu>'
},
body: [
{ partID: '1',
type: 'text',
subtype: 'plain',
params: { charset: 'US-ASCII' },
id: null,
description: null,
encoding: '7BIT',
size: 3028,
lines: 92
}
]
}
}
],
what: 'Untagged FETCH (flags, date, size, envelope, body[structure])'
},
].forEach(function(v) {
var ss = new require('stream').Readable(), p, result = [];
ss._read = function(){};
p = new Parser(ss);
p.on('tagged', function(info) {
result.push(info);
});
p.on('untagged', function(info) {
result.push(info);
});
p.on('continue', function(info) {
result.push(info);
});
p.on('other', function(line) {
result.push(line);
});
p.on('body', function(stream, info) {
result.push(info);
if (Array.isArray(v.bodySHA1s)) {
var hash = crypto.createHash('sha1');
stream.on('data', function(d) {
hash.update(d);
});
stream.on('end', function() {
var calculated = hash.digest('hex'),
expected = v.bodySHA1s.shift();
assert.equal(calculated,
expected,
makeMsg(v.what,
'Body SHA1 mismatch:'
+ '\nCalculated: ' + calculated
+ '\nExpected: ' + expected
)
);
});
}
});
try {
v.source.forEach(function(chunk) {
ss.push(chunk);
});
} catch (e) {
console.log(makeMsg(v.what, 'JS Exception: ' + e.stack));
return;
}
assert.deepEqual(result,
v.expected,
makeMsg(v.what,
'Result mismatch:'
+ '\nParsed: ' + inspect(result, false, 10)
+ '\nExpected: ' + inspect(v.expected, false, 10)
)
);
});
function makeMsg(what, msg) {
return '[' + what + ']: ' + msg;
}

@ -0,0 +1,4 @@
require('fs').readdirSync(__dirname).forEach(function(f) {
if (f.substr(0, 5) === 'test-')
require('./' + f);
});
Loading…
Cancel
Save