├── .gitignore ├── .travis.yml ├── lib ├── imap-handler.js ├── imap-compiler.js ├── imap-formal-syntax.js ├── imap-compile-stream.js └── imap-parser.js ├── Gruntfile.js ├── package.json ├── .eslintrc.js ├── README.md └── test ├── imap-compiler-test.js ├── imap-compile-stream-test.js ├── imap-parser-test.js └── fixtures └── mimetorture.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.10" 5 | - 0.12 6 | - iojs 7 | - 4 8 | - 5 9 | before_install: 10 | - npm install -g grunt-cli 11 | notifications: 12 | email: 13 | - andris@kreata.ee 14 | -------------------------------------------------------------------------------- /lib/imap-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parser = require('./imap-parser'); 4 | var compiler = require('./imap-compiler'); 5 | var compileStream = require('./imap-compile-stream'); 6 | 7 | module.exports = { 8 | parser: parser, 9 | compiler: compiler, 10 | compileStream: compileStream 11 | }; 12 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | eslint: { 8 | all: ['lib/**/*.js', 'test/**/*.js', 'Gruntfile.js', '.eslintrc.js'] 9 | }, 10 | 11 | mochaTest: { 12 | all: { 13 | options: { 14 | reporter: 'spec' 15 | }, 16 | src: ['test/**/*-test.js'] 17 | } 18 | } 19 | }); 20 | 21 | // Load the plugin(s) 22 | grunt.loadNpmTasks('grunt-eslint'); 23 | grunt.loadNpmTasks('grunt-mocha-test'); 24 | 25 | // Tasks 26 | grunt.registerTask('default', ['eslint', 'mochaTest']); 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imap-handler-1", 3 | "main": "lib/imap-handler.js", 4 | "version": "1.1.0", 5 | "homepage": "https://github.com/andris9/imap-handler-1", 6 | "author": "Andris Reinman ", 7 | "description": "Parse and compile IMAP commands", 8 | "keywords": [ 9 | "IMAP", 10 | "parser" 11 | ], 12 | "license": "MIT", 13 | "scripts": { 14 | "test": "grunt" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/andris9/imap-handler-1.git" 19 | }, 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "chai": "~3.5.0", 23 | "grunt": "~0.4.5", 24 | "grunt-eslint": "^17.3.1", 25 | "grunt-mocha-test": "~0.12.7", 26 | "mocha": "~2.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | rules: { 5 | indent: [2, 4, { 6 | SwitchCase: 1 7 | }], 8 | quotes: [2, 'single'], 9 | 'linebreak-style': [2, 'unix'], 10 | semi: [2, 'always'], 11 | strict: [2, 'global'], 12 | eqeqeq: 2, 13 | 'dot-notation': 2, 14 | curly: 2, 15 | 'no-fallthrough': 2, 16 | 'quote-props': [2, 'as-needed'], 17 | 'no-unused-expressions': [2, { 18 | allowShortCircuit: true 19 | }], 20 | 'no-unused-vars': 2, 21 | 'no-undef': 2, 22 | 'handle-callback-err': 2, 23 | 'no-new': 2, 24 | 'new-cap': 2, 25 | 'no-eval': 2, 26 | 'no-invalid-this': 2, 27 | radix: [2, 'always'], 28 | 'no-use-before-define': [2, 'nofunc'], 29 | 'callback-return': [2, ['callback', 'cb', 'done']], 30 | 'comma-dangle': [2, 'never'], 31 | 'comma-style': [2, 'last'], 32 | 'no-regex-spaces': 2, 33 | 'no-empty': 2, 34 | 'no-duplicate-case': 2, 35 | 'no-empty-character-class': 2, 36 | 'no-redeclare': [2, { 37 | builtinGlobals: true 38 | }], 39 | 'block-scoped-var': 2, 40 | 'no-sequences': 2, 41 | 'no-throw-literal': 2, 42 | 'no-useless-concat': 2, 43 | 'no-void': 2, 44 | yoda: 2, 45 | 'no-bitwise': 2, 46 | 'no-lonely-if': 2, 47 | 'no-mixed-spaces-and-tabs': 2, 48 | 'no-console': 0 49 | }, 50 | env: { 51 | es6: false, 52 | node: true 53 | }, 54 | extends: 'eslint:recommended', 55 | fix: true 56 | }; 57 | -------------------------------------------------------------------------------- /lib/imap-compiler.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | 'use strict'; 4 | 5 | var imapFormalSyntax = require('./imap-formal-syntax'); 6 | 7 | /** 8 | * Compiles an input object into 9 | */ 10 | module.exports = function (response, asArray, isLogging) { 11 | var respParts = []; 12 | var resp = (response.tag || '') + (response.command ? ' ' + response.command : ''); 13 | var val; 14 | var lastType; 15 | var walk = function (node) { 16 | 17 | if (lastType === 'LITERAL' || (['(', '<', '['].indexOf(resp.substr(-1)) < 0 && resp.length)) { 18 | resp += ' '; 19 | } 20 | 21 | if (Array.isArray(node)) { 22 | lastType = 'LIST'; 23 | resp += '('; 24 | node.forEach(walk); 25 | resp += ')'; 26 | return; 27 | } 28 | 29 | if (!node && typeof node !== 'string' && typeof node !== 'number') { 30 | resp += 'NIL'; 31 | return; 32 | } 33 | 34 | if (typeof node === 'string') { 35 | if (isLogging && node.length > 20) { 36 | resp += '"(* ' + node.length + 'B string *)"'; 37 | } else { 38 | resp += JSON.stringify(node); 39 | } 40 | return; 41 | } 42 | 43 | if (typeof node === 'number') { 44 | resp += Math.round(node) || 0; // Only integers allowed 45 | return; 46 | } 47 | 48 | lastType = node.type; 49 | 50 | if (isLogging && node.sensitive) { 51 | resp += '"(* value hidden *)"'; 52 | return; 53 | } 54 | 55 | switch (node.type.toUpperCase()) { 56 | case 'LITERAL': 57 | if (isLogging) { 58 | resp += '"(* ' + node.value.length + 'B literal *)"'; 59 | } else { 60 | if (!node.value) { 61 | resp += '{0}\r\n'; 62 | } else { 63 | resp += '{' + node.value.length + '}\r\n'; 64 | } 65 | respParts.push(resp); 66 | resp = node.value || ''; 67 | } 68 | break; 69 | 70 | case 'STRING': 71 | if (isLogging && node.value.length > 20) { 72 | resp += '"(* ' + node.value.length + 'B string *)"'; 73 | } else { 74 | resp += JSON.stringify(node.value || ''); 75 | } 76 | break; 77 | case 'TEXT': 78 | case 'SEQUENCE': 79 | resp += node.value || ''; 80 | break; 81 | 82 | case 'NUMBER': 83 | resp += (node.value || 0); 84 | break; 85 | 86 | case 'ATOM': 87 | case 'SECTION': 88 | val = node.value || ''; 89 | 90 | if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) { // eslint-disable-line new-cap 91 | val = JSON.stringify(val); 92 | } 93 | 94 | resp += val; 95 | 96 | if (node.section) { 97 | resp += '['; 98 | node.section.forEach(walk); 99 | resp += ']'; 100 | } 101 | if (node.partial) { 102 | resp += '<' + node.partial.join('.') + '>'; 103 | } 104 | break; 105 | } 106 | 107 | }; 108 | 109 | [].concat(response.attributes || []).forEach(walk); 110 | 111 | if (resp.length) { 112 | respParts.push(resp); 113 | } 114 | 115 | return asArray ? respParts : respParts.join(''); 116 | }; 117 | -------------------------------------------------------------------------------- /lib/imap-formal-syntax.js: -------------------------------------------------------------------------------- 1 | /* eslint object-shorthand:0, new-cap: 0, no-useless-concat: 0 */ 2 | 3 | 'use strict'; 4 | 5 | // IMAP Formal Syntax 6 | // http://tools.ietf.org/html/rfc3501#section-9 7 | 8 | function expandRange(start, end) { 9 | var chars = []; 10 | for (var i = start; i <= end; i++) { 11 | chars.push(i); 12 | } 13 | return String.fromCharCode.apply(String,chars); 14 | } 15 | 16 | function excludeChars(source, exclude) { 17 | var sourceArr = Array.prototype.slice.call(source); 18 | for (var i = sourceArr.length - 1; i >= 0; i--) { 19 | if (exclude.indexOf(sourceArr[i]) >= 0) { 20 | sourceArr.splice(i, 1); 21 | } 22 | } 23 | return sourceArr.join(''); 24 | } 25 | 26 | module.exports = { 27 | 28 | CHAR: function () { 29 | var value = expandRange(0x01, 0x7F); 30 | this.CHAR = function () { 31 | return value; 32 | }; 33 | return value; 34 | }, 35 | 36 | CHAR8: function () { 37 | var value = expandRange(0x01, 0xFF); 38 | this.CHAR8 = function () { 39 | return value; 40 | }; 41 | return value; 42 | }, 43 | 44 | SP: function () { 45 | return ' '; 46 | }, 47 | 48 | CTL: function () { 49 | var value = expandRange(0x00, 0x1F) + '\x7F'; 50 | this.CTL = function () { 51 | return value; 52 | }; 53 | return value; 54 | }, 55 | 56 | DQUOTE: function () { 57 | return '"'; 58 | }, 59 | 60 | ALPHA: function () { 61 | var value = expandRange(0x41, 0x5A) + expandRange(0x61, 0x7A); 62 | this.ALPHA = function () { 63 | return value; 64 | }; 65 | return value; 66 | }, 67 | 68 | DIGIT: function () { 69 | var value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7A); 70 | this.DIGIT = function () { 71 | return value; 72 | }; 73 | return value; 74 | }, 75 | 76 | 'ATOM-CHAR': function () { 77 | var value = excludeChars(this.CHAR(), this['atom-specials']()); 78 | this['ATOM-CHAR'] = function () { 79 | return value; 80 | }; 81 | return value; 82 | }, 83 | 84 | 'ASTRING-CHAR': function () { 85 | var value = this['ATOM-CHAR']() + this['resp-specials'](); 86 | this['ASTRING-CHAR'] = function () { 87 | return value; 88 | }; 89 | return value; 90 | }, 91 | 92 | 'TEXT-CHAR': function () { 93 | var value = excludeChars(this.CHAR(), '\r\n'); 94 | this['TEXT-CHAR'] = function () { 95 | return value; 96 | }; 97 | return value; 98 | }, 99 | 100 | 'atom-specials': function () { 101 | var value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() + 102 | this['quoted-specials']() + this['resp-specials'](); 103 | this['atom-specials'] = function () { 104 | return value; 105 | }; 106 | return value; 107 | }, 108 | 109 | 'list-wildcards': function () { 110 | return '%' + '*'; 111 | }, 112 | 113 | 'quoted-specials': function () { 114 | var value = this.DQUOTE() + '\\'; 115 | this['quoted-specials'] = function () { 116 | return value; 117 | }; 118 | return value; 119 | }, 120 | 121 | 'resp-specials': function () { 122 | return ']'; 123 | }, 124 | 125 | tag: function () { 126 | var value = excludeChars(this['ASTRING-CHAR'](), '+'); 127 | this.tag = function () { 128 | return value; 129 | }; 130 | return value; 131 | }, 132 | 133 | command: function () { 134 | var value = this.ALPHA() + this.DIGIT(); 135 | this.command = function () { 136 | return value; 137 | }; 138 | return value; 139 | }, 140 | 141 | verify: function (str, allowedChars) { 142 | for (var i = 0, len = str.length; i < len; i++) { 143 | if (allowedChars.indexOf(str.charAt(i)) < 0) { 144 | return i; 145 | } 146 | } 147 | return -1; 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IMAP Handler 2 | 3 | Server specific fork of [emailjs-imap-handler](https://github.com/emailjs/emailjs-imap-handler). Mostly differs from the upstream in the behavior for compiling – instead of compiling a command into long string, a Stream object is returned that can be piped directly to socket. Goal is to pass around large messages as streams instead of keeping these in memory. 4 | 5 | This is more suitable for servers than clients as it is currently not possible to pause the output stream to wait for '+' tagged server response for literal values. 6 | 7 | [![Build Status](https://travis-ci.org/andris9/imap-handler-1.png?branch=master)](https://travis-ci.org/andris9/imap-handler-1) 8 | 9 | ## Install 10 | 11 | ### [npm](https://www.npmjs.org/): 12 | 13 | npm install imap-handler-1 14 | 15 | ## Usage 16 | 17 | ### Parse IMAP commands 18 | 19 | To parse a command you need to have the command as one complete string (including all literals) without the ending <CR><LF> 20 | 21 | imapHandler.parser(imapCommand); 22 | 23 | Where 24 | 25 | * **imapCommand** is an IMAP string without the final line break 26 | 27 | The function returns an object in the following form: 28 | 29 | ``` 30 | { 31 | tag: "TAG", 32 | command: "COMMAND", 33 | attributes: [ 34 | {type: "SEQUENCE", value: "sequence-set"}, 35 | {type: "ATOM", value: "atom", section:[section_elements], partial: [start, end]}, 36 | {type: "STRING", value: "string"}, 37 | {type: "LITERAL", value: "literal"}, 38 | [list_elements] 39 | ] 40 | } 41 | ``` 42 | 43 | Where 44 | 45 | * **tag** is a string containing the tag 46 | * **command** is the first element after tag 47 | * **attributes** (if present) is an array of next elements 48 | 49 | If section or partial values are not specified in the command, the values are also missing from the ATOM element 50 | 51 | **NB!** Sequence numbers are identified as ATOM values if the value contains only numbers. 52 | **NB!** NIL atoms are always identified as `null` values, even though in some cases it might be an ATOM with value `"NIL"` 53 | 54 | For example 55 | 56 | ```javascript 57 | var imapHandler = require("imap-handler-1"); 58 | 59 | imapHandler.parser("A1 FETCH *:4 (BODY[HEADER.FIELDS ({4}\r\nDate Subject)]<12.45> UID)"); 60 | ``` 61 | 62 | Results in the following value: 63 | 64 | ```json 65 | { 66 | "tag": "A1", 67 | "command": "FETCH", 68 | "attributes": [ 69 | [ 70 | { 71 | "type": "SEQUENCE", 72 | "value": "*:4" 73 | }, 74 | { 75 | "type": "ATOM", 76 | "value": "BODY", 77 | "section": [ 78 | { 79 | "type": "ATOM", 80 | "value": "HEADER.FIELDS" 81 | }, 82 | [ 83 | { 84 | "type": "LITERAL", 85 | "value": "Date" 86 | }, 87 | { 88 | "type": "ATOM", 89 | "value": "Subject" 90 | } 91 | ] 92 | ], 93 | "partial": [ 94 | 12, 95 | 45 96 | ] 97 | }, 98 | { 99 | "type": "ATOM", 100 | "value": "UID" 101 | } 102 | ] 103 | ] 104 | } 105 | ``` 106 | 107 | ### Compile command objects into IMAP commands 108 | 109 | You can "compile" parsed or self generated IMAP command objects to IMAP command strings with 110 | 111 | imapHandler.compileStream(commandObject, isLogging); 112 | 113 | Where 114 | 115 | * **commandObject** is an object parsed with `imapHandler.parser()` or self generated 116 | * **isLogging** if set to true, do not include literals and long strings, useful when logging stuff and do not want to include message bodies etc. Additionally nodes with `sensitive: true` options are also not displayed (useful with logging passwords) if `logging` is used. 117 | 118 | The function returns a Stream. 119 | 120 | The input object differs from the parsed object with the following aspects: 121 | 122 | * **string**, **number** and **null** (null values are all non-number and non-string falsy values) are allowed to use directly - `{type: "STRING", value: "hello"}` can be replaced with `"hello"` 123 | * Additional types are used: `SECTION` which is an alias for `ATOM` and `TEXT` which returns the input string as given with no modification (useful for server messages). 124 | * **LITERAL** can takes streams as values. You do need to know the expected length beforehand though `{type:'LITERAL', expectedLength: 1024, value: stream}`. If the provided length does not match actual stream output length, then the output is either truncated or padded with space symbols to match the expected length. 125 | 126 | ```javascript 127 | { 128 | type: 'LITERAL', 129 | value: stream, 130 | expectedLength: 100, // full stream length 131 | startFrom: 10, // optional start marker, do not emit bytes before it 132 | maxLength: 30 // optional length of the output stream 133 | } 134 | ``` 135 | 136 | For example 137 | 138 | ```javascript 139 | var command = { 140 | tag: "*", 141 | command: "OK", 142 | attributes: [ 143 | { 144 | type: "SECTION", 145 | section: [ 146 | {type: "ATOM", value: "ALERT"} 147 | ] 148 | }, 149 | {type:"TEXT", value: "NB! The server is shutting down"} 150 | ] 151 | }; 152 | 153 | imapHandler.compileStream(command).pipe(process.stdout); 154 | // * OK [ALERT] NB! The server is shutting down 155 | ``` 156 | 157 | ## License 158 | 159 | ``` 160 | Copyright (c) 2013-2016 Andris Reinman 161 | 162 | Permission is hereby granted, free of charge, to any person obtaining a copy 163 | of this software and associated documentation files (the "Software"), to deal 164 | in the Software without restriction, including without limitation the rights 165 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 166 | copies of the Software, and to permit persons to whom the Software is 167 | furnished to do so, subject to the following conditions: 168 | 169 | The above copyright notice and this permission notice shall be included in 170 | all copies or substantial portions of the Software. 171 | 172 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 173 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 174 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 175 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 176 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 177 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 178 | THE SOFTWARE. 179 | ``` 180 | -------------------------------------------------------------------------------- /lib/imap-compile-stream.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | 'use strict'; 4 | 5 | var imapFormalSyntax = require('./imap-formal-syntax'); 6 | var streams = require('stream'); 7 | var PassThrough = streams.PassThrough; 8 | var Transform = streams.Transform; 9 | var util = require('util'); 10 | 11 | // make sure that a stream piped to this transform stream 12 | // always emits a fixed amounts of bytes. Either by truncating 13 | // input or emitting padding characters 14 | function LengthLimiter(expectedLength, padding, startFrom) { 15 | this.expectedLength = expectedLength; 16 | this.padding = padding || ' '; 17 | this.byteCounter = 0; 18 | this.startFrom = startFrom || 0; 19 | this.finished = false; 20 | 21 | Transform.call(this); 22 | } 23 | util.inherits(LengthLimiter, Transform); 24 | 25 | LengthLimiter.prototype._transform = function (chunk, encoding, done) { 26 | if (encoding !== 'buffer') { 27 | chunk = new Buffer(chunk, encoding); 28 | } 29 | 30 | if (!chunk || !chunk.length || this.finished) { 31 | return done(); 32 | } 33 | 34 | if (chunk.length + this.byteCounter <= this.startFrom) { 35 | // ignore 36 | this.byteCounter += chunk.length; 37 | return done(); 38 | } 39 | 40 | if (this.byteCounter < this.startFrom) { 41 | // split the chunk and ignore the first part 42 | chunk = chunk.slice(this.startFrom - this.byteCounter); 43 | this.byteCounter += (this.startFrom - this.byteCounter); 44 | } 45 | 46 | if (chunk.length + this.byteCounter <= this.expectedLength) { 47 | this.byteCounter += chunk.length; 48 | if (this.byteCounter >= this.expectedLength) { 49 | this.finished = true; 50 | } 51 | this.push(chunk); 52 | return done(); 53 | } 54 | 55 | var buf = chunk.slice(0, this.expectedLength - this.byteCounter); 56 | this.finished = true; 57 | this.push(buf); 58 | done(); 59 | }; 60 | 61 | LengthLimiter.prototype._flush = function (done) { 62 | if (!this.finished) { 63 | var buf = new Buffer(repeat(this.padding, this.expectedLength - this.byteCounter)); 64 | this.push(buf); 65 | } 66 | done(); 67 | }; 68 | 69 | /** 70 | * Compiles an input object into 71 | */ 72 | module.exports = function (response, isLogging) { 73 | var output = new PassThrough(); 74 | 75 | var resp = (response.tag || '') + (response.command ? ' ' + response.command : ''); 76 | var val, lastType; 77 | 78 | var waiting = false; 79 | var queue = []; 80 | var ended = false; 81 | var emit = function (stream, expectedLength, startFrom, maxLength) { 82 | expectedLength = expectedLength || 0; 83 | startFrom = startFrom || 0; 84 | maxLength = maxLength || 0; 85 | 86 | if (resp.length) { 87 | queue.push(new Buffer(resp, 'binary')); 88 | resp = ''; 89 | } 90 | 91 | if (stream) { 92 | queue.push({ 93 | type: 'stream', 94 | stream: stream, 95 | expectedLength: expectedLength, 96 | startFrom: startFrom, 97 | maxLength: maxLength 98 | }); 99 | } 100 | 101 | if (waiting) { 102 | return; 103 | } 104 | 105 | if (!queue.length) { 106 | if (ended) { 107 | output.end(); 108 | } 109 | return; 110 | } 111 | 112 | var value = queue.shift(); 113 | 114 | if (value.type === 'stream') { 115 | if (!value.expectedLength) { 116 | return emit(); 117 | } 118 | waiting = true; 119 | var limiter = new LengthLimiter(value.maxLength ? Math.min(value.expectedLength, value.startFrom + value.maxLength) : value.expectedLength, ' ', value.startFrom); 120 | value.stream.pipe(limiter).pipe(output, { 121 | end: false 122 | }); 123 | 124 | // pass errors to output 125 | value.stream.on('error', function (err) { 126 | output.emit('error', err); 127 | }); 128 | 129 | limiter.on('end', function () { 130 | waiting = false; 131 | return emit(); 132 | }); 133 | } else if (value instanceof Buffer) { 134 | output.write(value); 135 | return emit(); 136 | } else { 137 | if (typeof value === 'number') { 138 | value = value.toString(); 139 | } else if (typeof value !== 'string') { 140 | value = (value || '').toString(); 141 | } 142 | output.write(new Buffer(value, 'binary')); 143 | return emit(); 144 | } 145 | }; 146 | 147 | var walk = function (node, callback) { 148 | var pos; 149 | var next; 150 | 151 | if (lastType === 'LITERAL' || (['(', '<', '['].indexOf(resp.substr(-1)) < 0 && resp.length)) { 152 | resp += ' '; 153 | } 154 | 155 | if (Array.isArray(node)) { 156 | lastType = 'LIST'; 157 | resp += '('; 158 | 159 | pos = 0; 160 | next = function () { 161 | if (pos >= node.length) { 162 | resp += ')'; 163 | return setImmediate(callback); 164 | } 165 | walk(node[pos++], next); 166 | }; 167 | 168 | return setImmediate(next); 169 | } 170 | 171 | if (!node && typeof node !== 'string' && typeof node !== 'number') { 172 | resp += 'NIL'; 173 | return setImmediate(callback); 174 | } 175 | 176 | if (typeof node === 'string') { 177 | if (isLogging && node.length > 20) { 178 | resp += '"(* ' + node.length + 'B string *)"'; 179 | } else { 180 | resp += JSON.stringify(node); 181 | } 182 | return setImmediate(callback); 183 | } 184 | 185 | if (typeof node === 'number') { 186 | resp += Math.round(node) || 0; // Only integers allowed 187 | return setImmediate(callback); 188 | } 189 | 190 | lastType = node.type; 191 | 192 | if (isLogging && node.sensitive) { 193 | resp += '"(* value hidden *)"'; 194 | return setImmediate(callback); 195 | } 196 | 197 | switch (node.type.toUpperCase()) { 198 | case 'LITERAL': 199 | var nval = node.value; 200 | if (typeof nval === 'number') { 201 | nval = nval.toString(); 202 | } 203 | 204 | var len; 205 | 206 | if (nval && typeof nval.pipe === 'function') { 207 | len = node.expectedLength || 0; 208 | if (node.startFrom) { 209 | len -= node.startFrom; 210 | } 211 | if (node.maxLength) { 212 | len = Math.min(len, node.maxLength); 213 | } 214 | } else { 215 | len = (nval || '').toString().length; 216 | } 217 | 218 | if (isLogging) { 219 | resp += '"(* ' + len + 'B literal *)"'; 220 | } else { 221 | resp += '{' + len + '}\r\n'; 222 | emit(); 223 | 224 | if (nval && typeof nval.pipe === 'function') { 225 | //value is a stream object 226 | emit(nval, node.expectedLength, node.startFrom, node.maxLength); 227 | } else { 228 | resp = nval || ''; 229 | } 230 | } 231 | break; 232 | 233 | case 'STRING': 234 | if (isLogging && node.value.length > 20) { 235 | resp += '"(* ' + node.value.length + 'B string *)"'; 236 | } else { 237 | resp += JSON.stringify(node.value || ''); 238 | } 239 | break; 240 | case 'TEXT': 241 | case 'SEQUENCE': 242 | resp += node.value || ''; 243 | break; 244 | 245 | case 'NUMBER': 246 | resp += (node.value || 0); 247 | break; 248 | 249 | case 'ATOM': 250 | case 'SECTION': 251 | val = node.value || ''; 252 | 253 | if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) { //eslint-disable-line new-cap 254 | val = JSON.stringify(val); 255 | } 256 | 257 | resp += val; 258 | 259 | var finalize = function () { 260 | if (node.partial) { 261 | resp += '<' + node.partial.join('.') + '>'; 262 | } 263 | setImmediate(callback); 264 | }; 265 | 266 | if (node.section) { 267 | resp += '['; 268 | 269 | pos = 0; 270 | next = function () { 271 | if (pos >= node.section.length) { 272 | resp += ']'; 273 | return setImmediate(finalize); 274 | } 275 | walk(node.section[pos++], next); 276 | }; 277 | 278 | return setImmediate(next); 279 | } 280 | 281 | return finalize(); 282 | } 283 | setImmediate(callback); 284 | }; 285 | 286 | var finalize = function () { 287 | ended = true; 288 | emit(); 289 | }; 290 | var pos = 0; 291 | var attribs = [].concat(response.attributes || []); 292 | var next = function () { 293 | if (pos >= attribs.length) { 294 | return setImmediate(finalize); 295 | } 296 | walk(attribs[pos++], next); 297 | }; 298 | setImmediate(next); 299 | 300 | return output; 301 | }; 302 | 303 | function repeat(str, count) { 304 | return new Array(count + 1).join(str); 305 | } 306 | 307 | // expose for testing 308 | module.exports.LengthLimiter = LengthLimiter; 309 | -------------------------------------------------------------------------------- /test/imap-compiler-test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions:0 */ 2 | /* globals beforeEach, describe, it */ 3 | 4 | 'use strict'; 5 | 6 | var chai = require('chai'); 7 | var imapHandler = require('../lib/imap-handler'); 8 | var expect = chai.expect; 9 | chai.config.includeStack = true; 10 | 11 | describe('IMAP Command Compiler', function () { 12 | describe('#compile', function () { 13 | it('should compile correctly', function () { 14 | var command = '* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))', 15 | parsed = imapHandler.parser(command, { 16 | allowUntagged: true 17 | }), 18 | compiled = imapHandler.compiler(parsed); 19 | 20 | expect(compiled).to.equal(command); 21 | }); 22 | }); 23 | 24 | describe('Types', function () { 25 | var parsed; 26 | 27 | beforeEach(function () { 28 | parsed = { 29 | tag: '*', 30 | command: 'CMD' 31 | }; 32 | }); 33 | 34 | describe('No attributes', function () { 35 | it('should compile correctly', function () { 36 | expect(imapHandler.compiler(parsed)).to.equal('* CMD'); 37 | }); 38 | }); 39 | 40 | describe('TEXT', function () { 41 | it('should compile correctly', function () { 42 | parsed.attributes = [{ 43 | type: 'TEXT', 44 | value: 'Tere tere!' 45 | }]; 46 | expect(imapHandler.compiler(parsed)).to.equal('* CMD Tere tere!'); 47 | }); 48 | }); 49 | 50 | describe('SECTION', function () { 51 | it('should compile correctly', function () { 52 | parsed.attributes = [{ 53 | type: 'SECTION', 54 | section: [{ 55 | type: 'ATOM', 56 | value: 'ALERT' 57 | }] 58 | }]; 59 | expect(imapHandler.compiler(parsed)).to.equal('* CMD [ALERT]'); 60 | }); 61 | }); 62 | 63 | describe('ATOM', function () { 64 | it('should compile correctly', function () { 65 | parsed.attributes = [{ 66 | type: 'ATOM', 67 | value: 'ALERT' 68 | }, { 69 | type: 'ATOM', 70 | value: '\\ALERT' 71 | }, { 72 | type: 'ATOM', 73 | value: 'NO ALERT' 74 | }]; 75 | expect(imapHandler.compiler(parsed)).to.equal('* CMD ALERT \\ALERT "NO ALERT"'); 76 | }); 77 | }); 78 | 79 | describe('SEQUENCE', function () { 80 | it('should compile correctly', function () { 81 | parsed.attributes = [{ 82 | type: 'SEQUENCE', 83 | value: '*:4,5,6' 84 | }]; 85 | expect(imapHandler.compiler(parsed)).to.equal('* CMD *:4,5,6'); 86 | }); 87 | }); 88 | 89 | describe('NIL', function () { 90 | it('should compile correctly', function () { 91 | parsed.attributes = [ 92 | null, 93 | null 94 | ]; 95 | 96 | expect(imapHandler.compiler(parsed)).to.equal('* CMD NIL NIL'); 97 | 98 | }); 99 | }); 100 | 101 | describe('TEXT', function () { 102 | it('should compile correctly', function () { 103 | parsed.attributes = [ 104 | // keep indentation 105 | { 106 | type: 'String', 107 | value: 'Tere tere!', 108 | sensitive: true 109 | }, 110 | 'Vana kere' 111 | ]; 112 | 113 | expect(imapHandler.compiler(parsed)).to.equal('* CMD "Tere tere!" "Vana kere"'); 114 | 115 | }); 116 | 117 | it('should keep short strings', function () { 118 | parsed.attributes = [ 119 | // keep indentation 120 | { 121 | type: 'String', 122 | value: 'Tere tere!' 123 | }, 124 | 'Vana kere' 125 | ]; 126 | 127 | expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "Tere tere!" "Vana kere"'); 128 | }); 129 | 130 | it('should hide strings', function () { 131 | parsed.attributes = [ 132 | // keep indentation 133 | { 134 | type: 'String', 135 | value: 'Tere tere!', 136 | sensitive: true 137 | }, 138 | 'Vana kere' 139 | ]; 140 | 141 | expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* value hidden *)" "Vana kere"'); 142 | }); 143 | 144 | it('should hide long strings', function () { 145 | parsed.attributes = [ 146 | // keep indentation 147 | { 148 | type: 'String', 149 | value: 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!' 150 | }, 151 | 'Vana kere' 152 | ]; 153 | 154 | expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* 54B string *)" "Vana kere"'); 155 | }); 156 | }); 157 | 158 | describe('No Command', function () { 159 | it('should compile correctly', function () { 160 | parsed = { 161 | tag: '*', 162 | attributes: [ 163 | 1, { 164 | type: 'ATOM', 165 | value: 'EXPUNGE' 166 | } 167 | ] 168 | }; 169 | 170 | expect(imapHandler.compiler(parsed)).to.equal('* 1 EXPUNGE'); 171 | }); 172 | }); 173 | describe('Literal', function () { 174 | it('shoud return as text', function () { 175 | var parsed = { 176 | tag: '*', 177 | command: 'CMD', 178 | attributes: [ 179 | // keep indentation 180 | { 181 | type: 'LITERAL', 182 | value: 'Tere tere!' 183 | }, 184 | 'Vana kere' 185 | ] 186 | }; 187 | 188 | expect(imapHandler.compiler(parsed)).to.equal('* CMD {10}\r\nTere tere! "Vana kere"'); 189 | }); 190 | 191 | it('should return as an array text 1', function () { 192 | var parsed = { 193 | tag: '*', 194 | command: 'CMD', 195 | attributes: [{ 196 | type: 'LITERAL', 197 | value: 'Tere tere!' 198 | }, { 199 | type: 'LITERAL', 200 | value: 'Vana kere' 201 | }] 202 | }; 203 | expect(imapHandler.compiler(parsed, true)).to.deep.equal(['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']); 204 | }); 205 | 206 | it('should return as an array text 2', function () { 207 | var parsed = { 208 | tag: '*', 209 | command: 'CMD', 210 | attributes: [ 211 | // keep indentation 212 | { 213 | type: 'LITERAL', 214 | value: 'Tere tere!' 215 | }, { 216 | type: 'LITERAL', 217 | value: 'Vana kere' 218 | }, 219 | 'zzz' 220 | ] 221 | }; 222 | expect(imapHandler.compiler(parsed, true)).to.deep.equal(['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere "zzz"']); 223 | }); 224 | 225 | it('should compile correctly without tag and command', function () { 226 | var parsed = { 227 | attributes: [{ 228 | type: 'LITERAL', 229 | value: 'Tere tere!' 230 | }, { 231 | type: 'LITERAL', 232 | value: 'Vana kere' 233 | }] 234 | }; 235 | expect(imapHandler.compiler(parsed, true)).to.deep.equal(['{10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']); 236 | }); 237 | 238 | it('shoud return byte length', function () { 239 | var parsed = { 240 | tag: '*', 241 | command: 'CMD', 242 | attributes: [ 243 | // keep indentation 244 | { 245 | type: 'LITERAL', 246 | value: 'Tere tere!' 247 | }, 248 | 'Vana kere' 249 | ] 250 | }; 251 | 252 | expect(imapHandler.compiler(parsed, false, true)).to.equal('* CMD "(* 10B literal *)" "Vana kere"'); 253 | }); 254 | }); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /test/imap-compile-stream-test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions:0 */ 2 | /* globals beforeEach, describe, it */ 3 | 4 | 'use strict'; 5 | 6 | var chai = require('chai'); 7 | var imapHandler = require('../lib/imap-handler'); 8 | var PassThrough = require('stream').PassThrough; 9 | var expect = chai.expect; 10 | chai.config.includeStack = true; 11 | 12 | describe('IMAP Command Compile Stream', function () { 13 | 14 | describe('#compile', function () { 15 | 16 | it('should compile correctly', function (done) { 17 | var command = '* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL) ("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL) ("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))', 18 | parsed = imapHandler.parser(command, { 19 | allowUntagged: true 20 | }); 21 | 22 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 23 | expect(err).to.not.exist; 24 | expect(compiled.toString('binary')).to.equal(command); 25 | done(); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('Types', function () { 31 | var parsed; 32 | 33 | beforeEach(function () { 34 | parsed = { 35 | tag: '*', 36 | command: 'CMD' 37 | }; 38 | }); 39 | 40 | describe('No attributes', function () { 41 | it('should compile correctly', function (done) { 42 | var command = '* CMD'; 43 | 44 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 45 | expect(err).to.not.exist; 46 | expect(compiled.toString('binary')).to.equal(command); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('TEXT', function () { 53 | it('should compile correctly', function (done) { 54 | parsed.attributes = [{ 55 | type: 'TEXT', 56 | value: 'Tere tere!' 57 | }]; 58 | var command = '* CMD Tere tere!'; 59 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 60 | expect(err).to.not.exist; 61 | expect(compiled.toString('binary')).to.equal(command); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('SECTION', function () { 68 | it('should compile correctly', function (done) { 69 | parsed.attributes = [{ 70 | type: 'SECTION', 71 | section: [{ 72 | type: 'ATOM', 73 | value: 'ALERT' 74 | }] 75 | }]; 76 | var command = '* CMD [ALERT]'; 77 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 78 | expect(err).to.not.exist; 79 | expect(compiled.toString('binary')).to.equal(command); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('ATOM', function () { 86 | it('should compile correctly', function (done) { 87 | parsed.attributes = [{ 88 | type: 'ATOM', 89 | value: 'ALERT' 90 | }, { 91 | type: 'ATOM', 92 | value: '\\ALERT' 93 | }, { 94 | type: 'ATOM', 95 | value: 'NO ALERT' 96 | }]; 97 | var command = '* CMD ALERT \\ALERT "NO ALERT"'; 98 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 99 | expect(err).to.not.exist; 100 | expect(compiled.toString('binary')).to.equal(command); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('SEQUENCE', function () { 107 | it('should compile correctly', function (done) { 108 | parsed.attributes = [{ 109 | type: 'SEQUENCE', 110 | value: '*:4,5,6' 111 | }]; 112 | var command = '* CMD *:4,5,6'; 113 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 114 | expect(err).to.not.exist; 115 | expect(compiled.toString('binary')).to.equal(command); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('NIL', function () { 122 | it('should compile correctly', function (done) { 123 | parsed.attributes = [ 124 | null, 125 | null 126 | ]; 127 | 128 | var command = '* CMD NIL NIL'; 129 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 130 | expect(err).to.not.exist; 131 | expect(compiled.toString('binary')).to.equal(command); 132 | done(); 133 | }); 134 | }); 135 | }); 136 | 137 | describe('TEXT', function () { 138 | it('should compile correctly', function (done) { 139 | parsed.attributes = [ 140 | // keep indentation 141 | { 142 | type: 'String', 143 | value: 'Tere tere!', 144 | sensitive: true 145 | }, 146 | 'Vana kere' 147 | ]; 148 | 149 | var command = '* CMD "Tere tere!" "Vana kere"'; 150 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 151 | expect(err).to.not.exist; 152 | expect(compiled.toString('binary')).to.equal(command); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('should keep short strings', function (done) { 158 | parsed.attributes = [ 159 | // keep indentation 160 | { 161 | type: 'String', 162 | value: 'Tere tere!' 163 | }, 164 | 'Vana kere' 165 | ]; 166 | 167 | var command = '* CMD "Tere tere!" "Vana kere"'; 168 | resolveStream(imapHandler.compileStream(parsed, true), function (err, compiled) { 169 | expect(err).to.not.exist; 170 | expect(compiled.toString('binary')).to.equal(command); 171 | done(); 172 | }); 173 | }); 174 | 175 | it('should hide strings', function (done) { 176 | parsed.attributes = [ 177 | // keep indentation 178 | { 179 | type: 'String', 180 | value: 'Tere tere!', 181 | sensitive: true 182 | }, 183 | 'Vana kere' 184 | ]; 185 | 186 | var command = '* CMD "(* value hidden *)" "Vana kere"'; 187 | resolveStream(imapHandler.compileStream(parsed, true), function (err, compiled) { 188 | expect(err).to.not.exist; 189 | expect(compiled.toString('binary')).to.equal(command); 190 | done(); 191 | }); 192 | }); 193 | 194 | it('should hide long strings', function (done) { 195 | parsed.attributes = [ 196 | // keep indentation 197 | { 198 | type: 'String', 199 | value: 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!' 200 | }, 201 | 'Vana kere' 202 | ]; 203 | 204 | var command = '* CMD "(* 54B string *)" "Vana kere"'; 205 | resolveStream(imapHandler.compileStream(parsed, true), function (err, compiled) { 206 | expect(err).to.not.exist; 207 | expect(compiled.toString('binary')).to.equal(command); 208 | done(); 209 | }); 210 | }); 211 | }); 212 | 213 | describe('No Command', function () { 214 | it('should compile correctly', function (done) { 215 | parsed = { 216 | tag: '*', 217 | attributes: [ 218 | 1, { 219 | type: 'ATOM', 220 | value: 'EXPUNGE' 221 | } 222 | ] 223 | }; 224 | 225 | var command = '* 1 EXPUNGE'; 226 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 227 | expect(err).to.not.exist; 228 | expect(compiled.toString('binary')).to.equal(command); 229 | done(); 230 | }); 231 | }); 232 | }); 233 | describe('Literal', function () { 234 | it('shoud return as text', function (done) { 235 | var parsed = { 236 | tag: '*', 237 | command: 'CMD', 238 | attributes: [ 239 | // keep indentation 240 | { 241 | type: 'LITERAL', 242 | value: 'Tere tere!' 243 | }, 244 | 'Vana kere' 245 | ] 246 | }; 247 | 248 | var command = '* CMD {10}\r\nTere tere! "Vana kere"'; 249 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 250 | expect(err).to.not.exist; 251 | expect(compiled.toString('binary')).to.equal(command); 252 | done(); 253 | }); 254 | }); 255 | 256 | it('should compile correctly without tag and command', function (done) { 257 | var parsed = { 258 | attributes: [{ 259 | type: 'LITERAL', 260 | value: 'Tere tere!' 261 | }, { 262 | type: 'LITERAL', 263 | value: 'Vana kere' 264 | }] 265 | }; 266 | var command = '{10}\r\nTere tere! {9}\r\nVana kere'; 267 | resolveStream(imapHandler.compileStream(parsed), function (err, compiled) { 268 | expect(err).to.not.exist; 269 | expect(compiled.toString('binary')).to.equal(command); 270 | done(); 271 | }); 272 | }); 273 | 274 | it('shoud return byte length', function (done) { 275 | var parsed = { 276 | tag: '*', 277 | command: 'CMD', 278 | attributes: [ 279 | // keep indentation 280 | { 281 | type: 'LITERAL', 282 | value: 'Tere tere!' 283 | }, 284 | 'Vana kere' 285 | ] 286 | }; 287 | 288 | var command = '* CMD "(* 10B literal *)" "Vana kere"'; 289 | resolveStream(imapHandler.compileStream(parsed, true), function (err, compiled) { 290 | expect(err).to.not.exist; 291 | expect(compiled.toString('binary')).to.equal(command); 292 | done(); 293 | }); 294 | }); 295 | 296 | it('shoud pipe literal streams', function (done) { 297 | var stream1 = new PassThrough(); 298 | var stream2 = new PassThrough(); 299 | var stream3 = new PassThrough(); 300 | var parsed = { 301 | tag: '*', 302 | command: 'CMD', 303 | attributes: [ 304 | // keep indentation 305 | { 306 | type: 'LITERAL', 307 | value: 'Tere tere!' 308 | }, { 309 | type: 'LITERAL', 310 | expectedLength: 5, 311 | value: stream1 312 | }, 313 | 'Vana kere', { 314 | type: 'LITERAL', 315 | expectedLength: 7, 316 | value: stream2 317 | }, { 318 | type: 'LITERAL', 319 | value: 'Kuidas laheb?' 320 | }, { 321 | type: 'LITERAL', 322 | expectedLength: 5, 323 | value: stream3 324 | } 325 | ] 326 | }; 327 | 328 | var command = '* CMD {10}\r\nTere tere! {5}\r\ntest1 "Vana kere" {7}\r\ntest2 {13}\r\nKuidas laheb? {5}\r\ntest3'; 329 | resolveStream(imapHandler.compileStream(parsed, false), function (err, compiled) { 330 | expect(err).to.not.exist; 331 | expect(compiled.toString('binary')).to.equal(command); 332 | done(); 333 | }); 334 | 335 | setTimeout(function () { 336 | stream2.end('test2'); 337 | setTimeout(function () { 338 | stream1.end('test1'); 339 | setTimeout(function () { 340 | stream3.end('test3'); 341 | }, 100); 342 | }, 100); 343 | }, 100); 344 | }); 345 | 346 | it('shoud pipe limited literal streams', function (done) { 347 | var stream1 = new PassThrough(); 348 | var stream2 = new PassThrough(); 349 | var stream3 = new PassThrough(); 350 | var parsed = { 351 | tag: '*', 352 | command: 'CMD', 353 | attributes: [ 354 | // keep indentation 355 | { 356 | type: 'LITERAL', 357 | value: 'Tere tere!' 358 | }, { 359 | type: 'LITERAL', 360 | expectedLength: 5, 361 | value: stream1, 362 | startFrom: 2, 363 | maxLength: 2 364 | }, 365 | 'Vana kere', { 366 | type: 'LITERAL', 367 | expectedLength: 7, 368 | value: stream2, 369 | startFrom: 2 370 | }, { 371 | type: 'LITERAL', 372 | value: 'Kuidas laheb?' 373 | }, { 374 | type: 'LITERAL', 375 | expectedLength: 7, 376 | value: stream3, 377 | startFrom: 2, 378 | maxLength: 2 379 | } 380 | ] 381 | }; 382 | 383 | var command = '* CMD {10}\r\nTere tere! {2}\r\nst "Vana kere" {5}\r\nst2 {13}\r\nKuidas laheb? {2}\r\nst'; 384 | resolveStream(imapHandler.compileStream(parsed, false), function (err, compiled) { 385 | expect(err).to.not.exist; 386 | expect(compiled.toString('binary')).to.equal(command); 387 | done(); 388 | }); 389 | 390 | setTimeout(function () { 391 | stream2.end('test2'); 392 | setTimeout(function () { 393 | stream1.end('test1'); 394 | setTimeout(function () { 395 | stream3.end('test3'); 396 | }, 100); 397 | }, 100); 398 | }, 100); 399 | }); 400 | 401 | it('shoud pipe errors for literal streams', function (done) { 402 | var stream1 = new PassThrough(); 403 | var parsed = { 404 | tag: '*', 405 | command: 'CMD', 406 | attributes: [ 407 | // keep indentation 408 | { 409 | type: 'LITERAL', 410 | value: 'Tere tere!' 411 | }, { 412 | type: 'LITERAL', 413 | expectedLength: 5, 414 | value: stream1 415 | } 416 | ] 417 | }; 418 | 419 | resolveStream(imapHandler.compileStream(parsed, false), function (err) { 420 | expect(err).to.exist; 421 | done(); 422 | }); 423 | 424 | setTimeout(function () { 425 | stream1.emit('error', new Error('Stream error')); 426 | }, 100); 427 | }); 428 | }); 429 | }); 430 | 431 | describe('#LengthLimiter', function () { 432 | this.timeout(10000); //eslint-disable-line no-invalid-this 433 | 434 | it('should emit exact length', function (done) { 435 | var len = 1024; 436 | var limiter = new imapHandler.compileStream.LengthLimiter(len); 437 | var expected = repeat('X', len); 438 | 439 | resolveStream(limiter, function (err, value) { 440 | value = value.toString(); 441 | expect(err).to.not.exist; 442 | expect(value).to.equal(expected); 443 | done(); 444 | }); 445 | 446 | var emitted = 0; 447 | var emitter = function () { 448 | var str = repeat('X', 128); 449 | emitted += str.length; 450 | limiter.write(new Buffer(str)); 451 | if (emitted >= len) { 452 | limiter.end(); 453 | } else { 454 | setTimeout(emitter, 100); 455 | } 456 | }; 457 | 458 | setTimeout(emitter, 100); 459 | }); 460 | 461 | it('should truncate output', function (done) { 462 | var len = 1024; 463 | var limiter = new imapHandler.compileStream.LengthLimiter(len - 100); 464 | var expected = repeat('X', len - 100); 465 | 466 | resolveStream(limiter, function (err, value) { 467 | value = value.toString(); 468 | expect(err).to.not.exist; 469 | expect(value).to.equal(expected); 470 | done(); 471 | }); 472 | 473 | var emitted = 0; 474 | var emitter = function () { 475 | var str = repeat('X', 128); 476 | emitted += str.length; 477 | limiter.write(new Buffer(str)); 478 | if (emitted >= len) { 479 | limiter.end(); 480 | } else { 481 | setTimeout(emitter, 100); 482 | } 483 | }; 484 | 485 | setTimeout(emitter, 100); 486 | }); 487 | 488 | it('should skip output', function (done) { 489 | var len = 1024; 490 | var limiter = new imapHandler.compileStream.LengthLimiter(len - 100, false, 30); 491 | var expected = repeat('X', len - 100 - 30); 492 | 493 | resolveStream(limiter, function (err, value) { 494 | value = value.toString(); 495 | expect(err).to.not.exist; 496 | expect(value).to.equal(expected); 497 | done(); 498 | }); 499 | 500 | var emitted = 0; 501 | var emitter = function () { 502 | var str = repeat('X', 128); 503 | emitted += str.length; 504 | limiter.write(new Buffer(str)); 505 | if (emitted >= len) { 506 | limiter.end(); 507 | } else { 508 | setTimeout(emitter, 100); 509 | } 510 | }; 511 | 512 | setTimeout(emitter, 100); 513 | }); 514 | 515 | it('should pad output', function (done) { 516 | var len = 1024; 517 | var limiter = new imapHandler.compileStream.LengthLimiter(len + 100); 518 | var expected = repeat('X', len) + repeat(' ', 100); 519 | 520 | resolveStream(limiter, function (err, value) { 521 | value = value.toString(); 522 | expect(err).to.not.exist; 523 | expect(value).to.equal(expected); 524 | done(); 525 | }); 526 | 527 | var emitted = 0; 528 | var emitter = function () { 529 | var str = repeat('X', 128); 530 | emitted += str.length; 531 | limiter.write(new Buffer(str)); 532 | if (emitted >= len) { 533 | limiter.end(); 534 | } else { 535 | setTimeout(emitter, 100); 536 | } 537 | }; 538 | 539 | setTimeout(emitter, 100); 540 | }); 541 | }); 542 | }); 543 | 544 | function resolveStream(stream, callback) { 545 | var chunks = []; 546 | var chunklen = 0; 547 | 548 | stream.on('readable', function () { 549 | var chunk; 550 | 551 | while ((chunk = stream.read()) !== null) { 552 | chunks.push(chunk); 553 | chunklen += chunk.length; 554 | } 555 | }); 556 | 557 | stream.on('error', function (err) { 558 | return callback(err); 559 | }); 560 | stream.on('end', function () { 561 | return callback(null, Buffer.concat(chunks, chunklen)); 562 | }); 563 | } 564 | 565 | function repeat(str, count) { 566 | return new Array(count + 1).join(str); 567 | } 568 | -------------------------------------------------------------------------------- /lib/imap-parser.js: -------------------------------------------------------------------------------- 1 | /* eslint new-cap: 0*/ 2 | 3 | 'use strict'; 4 | 5 | var imapFormalSyntax = require('./imap-formal-syntax'); 6 | 7 | function ParserInstance(input, options) { 8 | this.input = (input || '').toString(); 9 | this.options = options || {}; 10 | this.remainder = this.input; 11 | this.pos = 0; 12 | } 13 | 14 | ParserInstance.prototype.getTag = function () { 15 | if (!this.tag) { 16 | this.tag = this.getElement(imapFormalSyntax.tag() + '*+', true); 17 | } 18 | return this.tag; 19 | }; 20 | 21 | ParserInstance.prototype.getCommand = function () { 22 | var responseCode; 23 | 24 | if (!this.command) { 25 | this.command = this.getElement(imapFormalSyntax.command()); 26 | } 27 | 28 | switch ((this.command || '').toString().toUpperCase()) { 29 | case 'OK': 30 | case 'NO': 31 | case 'BAD': 32 | case 'PREAUTH': 33 | case 'BYE': 34 | responseCode = this.remainder.match(/^ \[(?:[^\]]*\])+/); 35 | if (responseCode) { 36 | this.humanReadable = this.remainder.substr(responseCode[0].length).trim(); 37 | this.remainder = responseCode[0]; 38 | } else { 39 | this.humanReadable = this.remainder.trim(); 40 | this.remainder = ''; 41 | } 42 | break; 43 | } 44 | 45 | return this.command; 46 | }; 47 | 48 | ParserInstance.prototype.getElement = function (syntax) { 49 | var match, element, errPos; 50 | if (this.remainder.match(/^\s/)) { 51 | throw new Error('Unexpected whitespace at position ' + this.pos); 52 | } 53 | 54 | if ((match = this.remainder.match(/^[^\s]+(?=\s|$)/))) { 55 | element = match[0]; 56 | 57 | if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) { 58 | throw new Error('Unexpected char at position ' + (this.pos + errPos)); 59 | } 60 | } else { 61 | throw new Error('Unexpected end of input at position ' + this.pos); 62 | } 63 | 64 | this.pos += match[0].length; 65 | this.remainder = this.remainder.substr(match[0].length); 66 | 67 | return element; 68 | }; 69 | 70 | ParserInstance.prototype.getSpace = function () { 71 | if (!this.remainder.length) { 72 | throw new Error('Unexpected end of input at position ' + this.pos); 73 | } 74 | 75 | if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) { 76 | throw new Error('Unexpected char at position ' + this.pos); 77 | } 78 | 79 | this.pos++; 80 | this.remainder = this.remainder.substr(1); 81 | }; 82 | 83 | ParserInstance.prototype.getAttributes = function () { 84 | if (!this.remainder.length) { 85 | throw new Error('Unexpected end of input at position ' + this.pos); 86 | } 87 | 88 | if (this.remainder.match(/^\s/)) { 89 | throw new Error('Unexpected whitespace at position ' + this.pos); 90 | } 91 | 92 | return new TokenParser(this, this.pos, this.remainder, this.options).getAttributes(); 93 | }; 94 | 95 | function TokenParser(parent, startPos, str, options) { 96 | this.str = (str || '').toString(); 97 | this.options = options || {}; 98 | this.parent = parent; 99 | 100 | this.tree = this.currentNode = this.createNode(); 101 | this.pos = startPos || 0; 102 | 103 | this.currentNode.type = 'TREE'; 104 | 105 | this.state = 'NORMAL'; 106 | 107 | this.processString(); 108 | } 109 | 110 | TokenParser.prototype.getAttributes = function () { 111 | var attributes = [], 112 | branch = attributes; 113 | 114 | var walk = function (node) { 115 | var curBranch = branch; 116 | var elm; 117 | var partial; 118 | 119 | if (!node.closed && node.type === 'SEQUENCE' && node.value === '*') { 120 | node.closed = true; 121 | node.type = 'ATOM'; 122 | } 123 | 124 | // If the node was never closed, throw it 125 | if (!node.closed) { 126 | throw new Error('Unexpected end of input at position ' + (this.pos + this.str.length - 1)); 127 | } 128 | 129 | var type = (node.type || '').toString().toUpperCase(); 130 | 131 | switch (type) { 132 | case 'LITERAL': 133 | case 'STRING': 134 | case 'SEQUENCE': 135 | elm = { 136 | type: node.type.toUpperCase(), 137 | value: node.value 138 | }; 139 | branch.push(elm); 140 | break; 141 | 142 | case 'ATOM': 143 | if (node.value.toUpperCase() === 'NIL') { 144 | branch.push(null); 145 | break; 146 | } 147 | elm = { 148 | type: node.type.toUpperCase(), 149 | value: node.value 150 | }; 151 | branch.push(elm); 152 | break; 153 | 154 | case 'SECTION': 155 | branch = branch[branch.length - 1].section = []; 156 | break; 157 | 158 | case 'LIST': 159 | elm = []; 160 | branch.push(elm); 161 | branch = elm; 162 | break; 163 | 164 | case 'PARTIAL': 165 | partial = node.value.split('.').map(Number); 166 | branch[branch.length - 1].partial = partial; 167 | break; 168 | } 169 | 170 | node.childNodes.forEach(function (childNode) { 171 | return walk(childNode); 172 | }); 173 | branch = curBranch; 174 | }.bind(this); 175 | 176 | walk(this.tree); 177 | 178 | return attributes; 179 | }; 180 | 181 | TokenParser.prototype.createNode = function (parentNode, startPos) { 182 | var node = { 183 | childNodes: [], 184 | type: false, 185 | value: '', 186 | closed: true 187 | }; 188 | 189 | if (parentNode) { 190 | node.parentNode = parentNode; 191 | } 192 | 193 | if (typeof startPos === 'number') { 194 | node.startPos = startPos; 195 | } 196 | 197 | if (parentNode) { 198 | parentNode.childNodes.push(node); 199 | } 200 | 201 | return node; 202 | }; 203 | 204 | TokenParser.prototype.processString = function () { 205 | var chr, i, len, 206 | checkSP = function () { 207 | // jump to the next non whitespace pos 208 | while (this.str.charAt(i + 1) === ' ') { 209 | i++; 210 | } 211 | }.bind(this); 212 | 213 | for (i = 0, len = this.str.length; i < len; i++) { 214 | 215 | chr = this.str.charAt(i); 216 | 217 | switch (this.state) { 218 | 219 | case 'NORMAL': 220 | 221 | switch (chr) { 222 | 223 | // DQUOTE starts a new string 224 | case '"': 225 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 226 | this.currentNode.type = 'string'; 227 | this.state = 'STRING'; 228 | this.currentNode.closed = false; 229 | break; 230 | 231 | // ( starts a new list 232 | case '(': 233 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 234 | this.currentNode.type = 'LIST'; 235 | this.currentNode.closed = false; 236 | break; 237 | 238 | // ) closes a list 239 | case ')': 240 | if (this.currentNode.type !== 'LIST') { 241 | throw new Error('Unexpected list terminator ) at position ' + (this.pos + i)); 242 | } 243 | 244 | this.currentNode.closed = true; 245 | this.currentNode.endPos = this.pos + i; 246 | this.currentNode = this.currentNode.parentNode; 247 | 248 | checkSP(); 249 | break; 250 | 251 | // ] closes section group 252 | case ']': 253 | if (this.currentNode.type !== 'SECTION') { 254 | throw new Error('Unexpected section terminator ] at position ' + (this.pos + i)); 255 | } 256 | this.currentNode.closed = true; 257 | this.currentNode.endPos = this.pos + i; 258 | this.currentNode = this.currentNode.parentNode; 259 | checkSP(); 260 | break; 261 | 262 | // < starts a new partial 263 | case '<': 264 | if (this.str.charAt(i - 1) !== ']') { 265 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 266 | this.currentNode.type = 'ATOM'; 267 | this.currentNode.value = chr; 268 | this.state = 'ATOM'; 269 | } else { 270 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 271 | this.currentNode.type = 'PARTIAL'; 272 | this.state = 'PARTIAL'; 273 | this.currentNode.closed = false; 274 | } 275 | break; 276 | 277 | // { starts a new literal 278 | case '{': 279 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 280 | this.currentNode.type = 'LITERAL'; 281 | this.state = 'LITERAL'; 282 | this.currentNode.closed = false; 283 | break; 284 | 285 | // ( starts a new sequence 286 | case '*': 287 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 288 | this.currentNode.type = 'SEQUENCE'; 289 | this.currentNode.value = chr; 290 | this.currentNode.closed = false; 291 | this.state = 'SEQUENCE'; 292 | break; 293 | 294 | // normally a space should never occur 295 | case ' ': 296 | // just ignore 297 | break; 298 | 299 | // [ starts section 300 | case '[': 301 | // If it is the *first* element after response command, then process as a response argument list 302 | if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].indexOf(this.parent.command.toUpperCase()) >= 0 && this.currentNode === this.tree) { 303 | this.currentNode.endPos = this.pos + i; 304 | 305 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 306 | this.currentNode.type = 'ATOM'; 307 | 308 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 309 | this.currentNode.type = 'SECTION'; 310 | this.currentNode.closed = false; 311 | this.state = 'NORMAL'; 312 | 313 | // RFC2221 defines a response code REFERRAL whose payload is an 314 | // RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but 315 | // fail quite badly at parsing. Since the imapurl is such a unique 316 | // (and crazy) term, we just specialize that case here. 317 | if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') { 318 | // create the REFERRAL atom 319 | this.currentNode = this.createNode(this.currentNode, this.pos + i + 1); 320 | this.currentNode.type = 'ATOM'; 321 | this.currentNode.endPos = this.pos + i + 8; 322 | this.currentNode.value = 'REFERRAL'; 323 | this.currentNode = this.currentNode.parentNode; 324 | 325 | // eat all the way through the ] to be the IMAPURL token. 326 | this.currentNode = this.createNode(this.currentNode, this.pos + i + 10); 327 | // just call this an ATOM, even though IMAPURL might be more correct 328 | this.currentNode.type = 'ATOM'; 329 | // jump i to the ']' 330 | i = this.str.indexOf(']', i + 10); 331 | this.currentNode.endPos = this.pos + i - 1; 332 | this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, 333 | this.currentNode.endPos - this.pos + 1); 334 | this.currentNode = this.currentNode.parentNode; 335 | 336 | // close out the SECTION 337 | this.currentNode.closed = true; 338 | this.currentNode = this.currentNode.parentNode; 339 | checkSP(); 340 | } 341 | 342 | break; 343 | } 344 | /* falls through */ 345 | default: 346 | // Any ATOM supported char starts a new Atom sequence, otherwise throw an error 347 | // Allow \ as the first char for atom to support system flags 348 | // Allow % to support LIST '' % 349 | if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%') { 350 | throw new Error('Unexpected char at position ' + (this.pos + i)); 351 | } 352 | 353 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 354 | this.currentNode.type = 'ATOM'; 355 | this.currentNode.value = chr; 356 | this.state = 'ATOM'; 357 | break; 358 | } 359 | break; 360 | 361 | case 'ATOM': 362 | 363 | // space finishes an atom 364 | if (chr === ' ') { 365 | this.currentNode.endPos = this.pos + i - 1; 366 | this.currentNode = this.currentNode.parentNode; 367 | this.state = 'NORMAL'; 368 | break; 369 | } 370 | 371 | // 372 | if ( 373 | this.currentNode.parentNode && 374 | ( 375 | (chr === ')' && this.currentNode.parentNode.type === 'LIST') || 376 | (chr === ']' && this.currentNode.parentNode.type === 'SECTION') 377 | ) 378 | ) { 379 | this.currentNode.endPos = this.pos + i - 1; 380 | this.currentNode = this.currentNode.parentNode; 381 | 382 | this.currentNode.closed = true; 383 | this.currentNode.endPos = this.pos + i; 384 | this.currentNode = this.currentNode.parentNode; 385 | this.state = 'NORMAL'; 386 | 387 | checkSP(); 388 | break; 389 | } 390 | 391 | if ((chr === ',' || chr === ':') && this.currentNode.value.match(/^\d+$/)) { 392 | this.currentNode.type = 'SEQUENCE'; 393 | this.currentNode.closed = true; 394 | this.state = 'SEQUENCE'; 395 | } 396 | 397 | // [ starts a section group for this element 398 | if (chr === '[') { 399 | // allowed only for selected elements 400 | if (['BODY', 'BODY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) < 0) { 401 | throw new Error('Unexpected section start char [ at position ' + this.pos); 402 | } 403 | this.currentNode.endPos = this.pos + i; 404 | this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i); 405 | this.currentNode.type = 'SECTION'; 406 | this.currentNode.closed = false; 407 | this.state = 'NORMAL'; 408 | break; 409 | } 410 | 411 | if (chr === '<') { 412 | throw new Error('Unexpected start of partial at position ' + this.pos); 413 | } 414 | 415 | // if the char is not ATOM compatible, throw. Allow \* as an exception 416 | if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== ']' && !(chr === '*' && this.currentNode.value === '\\')) { 417 | throw new Error('Unexpected char at position ' + (this.pos + i)); 418 | } else if (this.currentNode.value === '\\*') { 419 | throw new Error('Unexpected char at position ' + (this.pos + i)); 420 | } 421 | 422 | this.currentNode.value += chr; 423 | break; 424 | 425 | case 'STRING': 426 | 427 | // DQUOTE ends the string sequence 428 | if (chr === '"') { 429 | this.currentNode.endPos = this.pos + i; 430 | this.currentNode.closed = true; 431 | this.currentNode = this.currentNode.parentNode; 432 | this.state = 'NORMAL'; 433 | 434 | checkSP(); 435 | break; 436 | } 437 | 438 | // \ Escapes the following char 439 | if (chr === '\\') { 440 | i++; 441 | if (i >= len) { 442 | throw new Error('Unexpected end of input at position ' + (this.pos + i)); 443 | } 444 | chr = this.str.charAt(i); 445 | } 446 | 447 | /* // skip this check, otherwise the parser might explode on binary input 448 | if (imapFormalSyntax['TEXT-CHAR']().indexOf(chr) < 0) { 449 | throw new Error('Unexpected char at position ' + (this.pos + i)); 450 | } 451 | */ 452 | 453 | this.currentNode.value += chr; 454 | break; 455 | 456 | case 'PARTIAL': 457 | if (chr === '>') { 458 | if (this.currentNode.value.substr(-1) === '.') { 459 | throw new Error('Unexpected end of partial at position ' + this.pos); 460 | } 461 | this.currentNode.endPos = this.pos + i; 462 | this.currentNode.closed = true; 463 | this.currentNode = this.currentNode.parentNode; 464 | this.state = 'NORMAL'; 465 | checkSP(); 466 | break; 467 | } 468 | 469 | if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) { 470 | throw new Error('Unexpected partial separator . at position ' + this.pos); 471 | } 472 | 473 | if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') { 474 | throw new Error('Unexpected char at position ' + (this.pos + i)); 475 | } 476 | 477 | if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') { 478 | throw new Error('Invalid partial at position ' + (this.pos + i)); 479 | } 480 | 481 | this.currentNode.value += chr; 482 | break; 483 | 484 | case 'LITERAL': 485 | if (this.currentNode.started) { 486 | //if(imapFormalSyntax['CHAR8']().indexOf(chr) < 0){ 487 | if (chr === '\u0000') { 488 | throw new Error('Unexpected \\x00 at position ' + (this.pos + i)); 489 | } 490 | this.currentNode.value += chr; 491 | 492 | if (this.currentNode.value.length >= this.currentNode.literalLength) { 493 | this.currentNode.endPos = this.pos + i; 494 | this.currentNode.closed = true; 495 | this.currentNode = this.currentNode.parentNode; 496 | this.state = 'NORMAL'; 497 | checkSP(); 498 | } 499 | break; 500 | } 501 | 502 | if (chr === '+' && this.options.literalPlus) { 503 | this.currentNode.literalPlus = true; 504 | break; 505 | } 506 | 507 | if (chr === '}') { 508 | if (!('literalLength' in this.currentNode)) { 509 | throw new Error('Unexpected literal prefix end char } at position ' + (this.pos + i)); 510 | } 511 | if (this.str.charAt(i + 1) === '\n') { 512 | i++; 513 | } else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') { 514 | i += 2; 515 | } else { 516 | throw new Error('Unexpected char at position ' + (this.pos + i)); 517 | } 518 | this.currentNode.literalLength = Number(this.currentNode.literalLength); 519 | this.currentNode.started = true; 520 | 521 | if (!this.currentNode.literalLength) { 522 | // special case where literal content length is 0 523 | // close the node right away, do not wait for additional input 524 | this.currentNode.endPos = this.pos + i; 525 | this.currentNode.closed = true; 526 | this.currentNode = this.currentNode.parentNode; 527 | this.state = 'NORMAL'; 528 | checkSP(); 529 | } 530 | break; 531 | } 532 | if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) { 533 | throw new Error('Unexpected char at position ' + (this.pos + i)); 534 | } 535 | if (this.currentNode.literalLength === '0') { 536 | throw new Error('Invalid literal at position ' + (this.pos + i)); 537 | } 538 | this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr; 539 | break; 540 | 541 | case 'SEQUENCE': 542 | // space finishes the sequence set 543 | if (chr === ' ') { 544 | if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') { 545 | throw new Error('Unexpected whitespace at position ' + (this.pos + i)); 546 | } 547 | 548 | if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') { 549 | throw new Error('Unexpected whitespace at position ' + (this.pos + i)); 550 | } 551 | 552 | this.currentNode.closed = true; 553 | this.currentNode.endPos = this.pos + i - 1; 554 | this.currentNode = this.currentNode.parentNode; 555 | this.state = 'NORMAL'; 556 | break; 557 | } else if (this.currentNode.parentNode && 558 | chr === ']' && 559 | this.currentNode.parentNode.type === 'SECTION') { 560 | this.currentNode.endPos = this.pos + i - 1; 561 | this.currentNode = this.currentNode.parentNode; 562 | 563 | this.currentNode.closed = true; 564 | this.currentNode.endPos = this.pos + i; 565 | this.currentNode = this.currentNode.parentNode; 566 | this.state = 'NORMAL'; 567 | 568 | checkSP(); 569 | break; 570 | } 571 | 572 | if (chr === ':') { 573 | if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') { 574 | throw new Error('Unexpected range separator : at position ' + (this.pos + i)); 575 | } 576 | } else if (chr === '*') { 577 | if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) { 578 | throw new Error('Unexpected range wildcard at position ' + (this.pos + i)); 579 | } 580 | } else if (chr === ',') { 581 | if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') { 582 | throw new Error('Unexpected sequence separator , at position ' + (this.pos + i)); 583 | } 584 | if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') { 585 | throw new Error('Unexpected sequence separator , at position ' + (this.pos + i)); 586 | } 587 | } else if (!chr.match(/\d/)) { 588 | throw new Error('Unexpected char at position ' + (this.pos + i)); 589 | } 590 | 591 | if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') { 592 | throw new Error('Unexpected number at position ' + (this.pos + i)); 593 | } 594 | 595 | this.currentNode.value += chr; 596 | break; 597 | } 598 | } 599 | }; 600 | 601 | module.exports = function (command, options) { 602 | var parser, response = {}; 603 | 604 | options = options || {}; 605 | 606 | parser = new ParserInstance(command, options); 607 | 608 | response.tag = parser.getTag(); 609 | parser.getSpace(); 610 | response.command = parser.getCommand(); 611 | 612 | if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) { 613 | parser.getSpace(); 614 | response.command += ' ' + parser.getElement(imapFormalSyntax.command()); 615 | } 616 | 617 | if (parser.remainder.trim().length) { 618 | parser.getSpace(); 619 | response.attributes = parser.getAttributes(); 620 | } 621 | 622 | if (parser.humanReadable) { 623 | response.attributes = (response.attributes || []).concat({ 624 | type: 'TEXT', 625 | value: parser.humanReadable 626 | }); 627 | } 628 | 629 | return response; 630 | }; 631 | -------------------------------------------------------------------------------- /test/imap-parser-test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions:0 */ 2 | /* globals describe, it */ 3 | 4 | 'use strict'; 5 | 6 | var chai = require('chai'); 7 | var imapHandler = require('../lib/imap-handler'); 8 | var mimetorture = require('./fixtures/mimetorture'); 9 | 10 | var expect = chai.expect; 11 | chai.config.includeStack = true; 12 | 13 | describe('IMAP Command Parser', function () { 14 | describe('get tag', function () { 15 | it('should succeed', function () { 16 | expect(imapHandler.parser('TAG1 CMD').tag).to.equal('TAG1'); 17 | }); 18 | 19 | it('should fail for unexpected WS', function () { 20 | expect(function () { 21 | imapHandler.parser(' TAG CMD'); 22 | }).to.throw(Error); 23 | }); 24 | 25 | it('should * OK ', function () { 26 | expect(function () { 27 | imapHandler.parser(' TAG CMD'); 28 | }).to.throw(Error); 29 | }); 30 | 31 | it('should + OK ', function () { 32 | expect(imapHandler.parser('+ TAG CMD').tag).to.equal('+'); 33 | }); 34 | 35 | it('should allow untagged', function () { 36 | expect(function () { 37 | imapHandler.parser('* CMD'); 38 | }).to.not.throw(Error); 39 | }); 40 | 41 | it('should fail for empty tag', function () { 42 | expect(function () { 43 | imapHandler.parser(''); 44 | }).to.throw(Error); 45 | }); 46 | 47 | it('should fail for unexpected end', function () { 48 | expect(function () { 49 | imapHandler.parser('TAG1'); 50 | }).to.throw(Error); 51 | }); 52 | 53 | it('should fail for invalid char', function () { 54 | expect(function () { 55 | imapHandler.parser('TAG"1 CMD'); 56 | }).to.throw(Error); 57 | }); 58 | }); 59 | 60 | describe('get arguments', function () { 61 | it('should allow trailing whitespace and empty arguments', function () { 62 | expect(function () { 63 | imapHandler.parser('* SEARCH '); 64 | }).to.not.throw(Error); 65 | }); 66 | }); 67 | 68 | describe('get command', function () { 69 | it('should succeed', function () { 70 | expect(imapHandler.parser('TAG1 CMD').command).to.equal('CMD'); 71 | }); 72 | 73 | it('should work for multi word command', function () { 74 | expect(imapHandler.parser('TAG1 UID FETCH').command).to.equal('UID FETCH'); 75 | }); 76 | 77 | it('should fail for unexpected WS', function () { 78 | expect(function () { 79 | imapHandler.parser('TAG1 CMD'); 80 | }).to.throw(Error); 81 | }); 82 | 83 | it('should fail for empty command', function () { 84 | expect(function () { 85 | imapHandler.parser('TAG1 '); 86 | }).to.throw(Error); 87 | }); 88 | 89 | it('should fail for invalid char', function () { 90 | expect(function () { 91 | imapHandler.parser('TAG1 CM=D'); 92 | }).to.throw(Error); 93 | }); 94 | }); 95 | 96 | describe('get attribute', function () { 97 | it('should succeed', function () { 98 | expect(imapHandler.parser('TAG1 CMD FED').attributes).to.deep.equal([{ 99 | type: 'ATOM', 100 | value: 'FED' 101 | }]); 102 | }); 103 | 104 | it('should succeed for single whitespace between values', function () { 105 | expect(imapHandler.parser('TAG1 CMD FED TED').attributes).to.deep.equal([{ 106 | type: 'ATOM', 107 | value: 'FED' 108 | }, { 109 | type: 'ATOM', 110 | value: 'TED' 111 | }]); 112 | }); 113 | 114 | it('should succeed for ATOM', function () { 115 | expect(imapHandler.parser('TAG1 CMD ABCDE').attributes).to.deep.equal([{ 116 | type: 'ATOM', 117 | value: 'ABCDE' 118 | }]); 119 | 120 | expect(imapHandler.parser('TAG1 CMD ABCDE DEFGH').attributes).to.deep.equal([{ 121 | type: 'ATOM', 122 | value: 'ABCDE' 123 | }, { 124 | type: 'ATOM', 125 | value: 'DEFGH' 126 | }]); 127 | 128 | expect(imapHandler.parser('TAG1 CMD %').attributes).to.deep.equal([{ 129 | type: 'ATOM', 130 | value: '%' 131 | }]); 132 | 133 | expect(imapHandler.parser('TAG1 CMD \\*').attributes).to.deep.equal([{ 134 | type: 'ATOM', 135 | value: '\\*' 136 | }]); 137 | 138 | expect(imapHandler.parser('12.82 STATUS [Gmail].Trash (UIDNEXT UNSEEN HIGHESTMODSEQ)').attributes).to.deep.equal([ 139 | // keep indentation 140 | { 141 | type: 'ATOM', 142 | value: '[Gmail].Trash' 143 | }, 144 | [{ 145 | type: 'ATOM', 146 | value: 'UIDNEXT' 147 | }, { 148 | type: 'ATOM', 149 | value: 'UNSEEN' 150 | }, { 151 | type: 'ATOM', 152 | value: 'HIGHESTMODSEQ' 153 | }] 154 | ]); 155 | }); 156 | 157 | it('should not succeed for ATOM', function () { 158 | expect(function () { 159 | imapHandler.parser('TAG1 CMD \\*a'); 160 | }).to.throw(Error); 161 | }); 162 | }); 163 | 164 | describe('get string', function () { 165 | it('should succeed', function () { 166 | expect(imapHandler.parser('TAG1 CMD "ABCDE"').attributes).to.deep.equal([{ 167 | type: 'STRING', 168 | value: 'ABCDE' 169 | }]); 170 | 171 | expect(imapHandler.parser('TAG1 CMD "ABCDE" "DEFGH"').attributes).to.deep.equal([{ 172 | type: 'STRING', 173 | value: 'ABCDE' 174 | }, { 175 | type: 'STRING', 176 | value: 'DEFGH' 177 | }]); 178 | }); 179 | 180 | it('should not explode on invalid char', function () { 181 | expect(imapHandler.parser('* 1 FETCH (BODY[] "\xc2")').attributes).to.deep.equal([ 182 | // keep indentation 183 | { 184 | type: 'ATOM', 185 | value: 'FETCH' 186 | }, 187 | [{ 188 | type: 'ATOM', 189 | value: 'BODY', 190 | section: [] 191 | }, { 192 | type: 'STRING', 193 | value: '\xc2' 194 | }] 195 | ]); 196 | }); 197 | }); 198 | 199 | describe('get list', function () { 200 | it('should succeed', function () { 201 | expect(imapHandler.parser('TAG1 CMD (1234)').attributes).to.deep.equal([ 202 | [{ 203 | type: 'ATOM', 204 | value: '1234' 205 | }] 206 | ]); 207 | expect(imapHandler.parser('TAG1 CMD (1234 TERE)').attributes).to.deep.equal([ 208 | [{ 209 | type: 'ATOM', 210 | value: '1234' 211 | }, { 212 | type: 'ATOM', 213 | value: 'TERE' 214 | }] 215 | ]); 216 | expect(imapHandler.parser('TAG1 CMD (1234)(TERE)').attributes).to.deep.equal([ 217 | [{ 218 | type: 'ATOM', 219 | value: '1234' 220 | }], 221 | [{ 222 | type: 'ATOM', 223 | value: 'TERE' 224 | }] 225 | ]); 226 | expect(imapHandler.parser('TAG1 CMD ( 1234)').attributes).to.deep.equal([ 227 | [{ 228 | type: 'ATOM', 229 | value: '1234' 230 | }] 231 | ]); 232 | // Trailing whitespace in a BODYSTRUCTURE atom list has been 233 | // observed on yahoo.co.jp's 234 | expect(imapHandler.parser('TAG1 CMD (1234 )').attributes).to.deep.equal([ 235 | [{ 236 | type: 'ATOM', 237 | value: '1234' 238 | }] 239 | ]); 240 | expect(imapHandler.parser('TAG1 CMD (1234) ').attributes).to.deep.equal([ 241 | [{ 242 | type: 'ATOM', 243 | value: '1234' 244 | }] 245 | ]); 246 | }); 247 | }); 248 | 249 | describe('nested list', function () { 250 | it('should succeed', function () { 251 | expect(imapHandler.parser('TAG1 CMD (((TERE)) VANA)').attributes).to.deep.equal([ 252 | [ 253 | [ 254 | [{ 255 | type: 'ATOM', 256 | value: 'TERE' 257 | }] 258 | ], { 259 | type: 'ATOM', 260 | value: 'VANA' 261 | } 262 | ] 263 | ]); 264 | expect(imapHandler.parser('TAG1 CMD (( (TERE)) VANA)').attributes).to.deep.equal([ 265 | [ 266 | [ 267 | [{ 268 | type: 'ATOM', 269 | value: 'TERE' 270 | }] 271 | ], { 272 | type: 'ATOM', 273 | value: 'VANA' 274 | } 275 | ] 276 | ]); 277 | expect(imapHandler.parser('TAG1 CMD (((TERE) ) VANA)').attributes).to.deep.equal([ 278 | [ 279 | [ 280 | [{ 281 | type: 'ATOM', 282 | value: 'TERE' 283 | }] 284 | ], { 285 | type: 'ATOM', 286 | value: 'VANA' 287 | } 288 | ] 289 | ]); 290 | }); 291 | }); 292 | 293 | describe('get literal', function () { 294 | it('should succeed', function () { 295 | expect(imapHandler.parser('TAG1 CMD {4}\r\nabcd').attributes).to.deep.equal([{ 296 | type: 'LITERAL', 297 | value: 'abcd' 298 | }]); 299 | 300 | expect(imapHandler.parser('TAG1 CMD {4}\r\nabcd {4}\r\nkere').attributes).to.deep.equal([{ 301 | type: 'LITERAL', 302 | value: 'abcd' 303 | }, { 304 | type: 'LITERAL', 305 | value: 'kere' 306 | }]); 307 | 308 | expect(imapHandler.parser('TAG1 CMD ({4}\r\nabcd {4}\r\nkere)').attributes).to.deep.equal([ 309 | [{ 310 | type: 'LITERAL', 311 | value: 'abcd' 312 | }, { 313 | type: 'LITERAL', 314 | value: 'kere' 315 | }] 316 | ]); 317 | }); 318 | 319 | it('should fail', function () { 320 | expect(function () { 321 | imapHandler.parser('TAG1 CMD {4}\r\nabcd{4} \r\nkere'); 322 | }).to.throw(Error); 323 | }); 324 | 325 | it('should allow zero length literal in the end of a list', function () { 326 | expect(imapHandler.parser('TAG1 CMD ({0}\r\n)').attributes).to.deep.equal([ 327 | [{ 328 | type: 'LITERAL', 329 | value: '' 330 | }] 331 | ]); 332 | }); 333 | 334 | }); 335 | 336 | describe('ATOM Section', function () { 337 | it('should succeed', function () { 338 | expect(imapHandler.parser('TAG1 CMD BODY[]').attributes).to.deep.equal([{ 339 | type: 'ATOM', 340 | value: 'BODY', 341 | section: [] 342 | }]); 343 | expect(imapHandler.parser('TAG1 CMD BODY[(KERE)]').attributes).to.deep.equal([{ 344 | type: 'ATOM', 345 | value: 'BODY', 346 | section: [ 347 | [{ 348 | type: 'ATOM', 349 | value: 'KERE' 350 | }] 351 | ] 352 | }]); 353 | }); 354 | it('will not fail due to trailing whitespace', function () { 355 | // We intentionally have trailing whitespace in the section here 356 | // because we altered the parser to handle this when we made it 357 | // legal for lists and it makes sense to accordingly test it. 358 | // However, we have no recorded incidences of this happening in 359 | // reality (unlike for lists). 360 | expect(imapHandler.parser('TAG1 CMD BODY[HEADER.FIELDS (Subject From) ]').attributes).to.deep.equal([{ 361 | type: 'ATOM', 362 | value: 'BODY', 363 | section: [ 364 | // keep indentation 365 | { 366 | type: 'ATOM', 367 | value: 'HEADER.FIELDS' 368 | }, 369 | [{ 370 | type: 'ATOM', 371 | value: 'Subject' 372 | }, { 373 | type: 'ATOM', 374 | value: 'From' 375 | }] 376 | ] 377 | }]); 378 | }); 379 | it('should fail where default BODY and BODY.PEEK are allowed to have sections', function () {}); 380 | expect(function () { 381 | imapHandler.parser('TAG1 CMD KODY[]'); 382 | }).to.throw(Error); 383 | }); 384 | 385 | describe('Human readable', function () { 386 | it('should succeed', function () { 387 | expect(imapHandler.parser('* OK [CAPABILITY IDLE] Hello world!')).to.deep.equal({ 388 | command: 'OK', 389 | tag: '*', 390 | attributes: [{ 391 | section: [{ 392 | type: 'ATOM', 393 | value: 'CAPABILITY' 394 | }, { 395 | type: 'ATOM', 396 | value: 'IDLE' 397 | }], 398 | type: 'ATOM', 399 | value: '' 400 | }, { 401 | type: 'TEXT', 402 | value: 'Hello world!' 403 | }] 404 | }); 405 | 406 | expect(imapHandler.parser('* OK Hello world!')).to.deep.equal({ 407 | command: 'OK', 408 | tag: '*', 409 | attributes: [{ 410 | type: 'TEXT', 411 | value: 'Hello world!' 412 | }] 413 | }); 414 | 415 | expect(imapHandler.parser('* OK')).to.deep.equal({ 416 | command: 'OK', 417 | tag: '*' 418 | }); 419 | 420 | // USEATTR is from RFC6154; we are testing that just an ATOM 421 | // on its own will parse successfully here. (All of the 422 | // RFC5530 codes are also single atoms.) 423 | expect(imapHandler.parser('TAG1 OK [USEATTR] \\All not supported')).to.deep.equal({ 424 | tag: 'TAG1', 425 | command: 'OK', 426 | attributes: [{ 427 | type: 'ATOM', 428 | value: '', 429 | section: [{ 430 | type: 'ATOM', 431 | value: 'USEATTR' 432 | }] 433 | }, { 434 | type: 'TEXT', 435 | value: '\\All not supported' 436 | }] 437 | }); 438 | 439 | // RFC5267 defines the NOUPDATE error. Including for quote / 440 | // string coverage. 441 | expect(imapHandler.parser('* NO [NOUPDATE "B02"] Too many contexts')).to.deep.equal({ 442 | tag: '*', 443 | command: 'NO', 444 | attributes: [{ 445 | type: 'ATOM', 446 | value: '', 447 | section: [{ 448 | type: 'ATOM', 449 | value: 'NOUPDATE' 450 | }, { 451 | type: 'STRING', 452 | value: 'B02' 453 | }] 454 | }, { 455 | type: 'TEXT', 456 | value: 'Too many contexts' 457 | }] 458 | }); 459 | 460 | 461 | // RFC5464 defines the METADATA response code; adding this to 462 | // ensure the transition for when '2199' hits ']' is handled 463 | // safely. 464 | expect(imapHandler.parser('TAG1 OK [METADATA LONGENTRIES 2199] GETMETADATA complete')).to.deep.equal({ 465 | tag: 'TAG1', 466 | command: 'OK', 467 | attributes: [{ 468 | type: 'ATOM', 469 | value: '', 470 | section: [{ 471 | type: 'ATOM', 472 | value: 'METADATA' 473 | }, { 474 | type: 'ATOM', 475 | value: 'LONGENTRIES' 476 | }, { 477 | type: 'ATOM', 478 | value: '2199' 479 | }] 480 | }, { 481 | type: 'TEXT', 482 | value: 'GETMETADATA complete' 483 | }] 484 | }); 485 | 486 | // RFC4467 defines URLMECH. Included because of the example 487 | // third atom involves base64-encoding which is somewhat unusual 488 | expect(imapHandler.parser('TAG1 OK [URLMECH INTERNAL XSAMPLE=P34OKhO7VEkCbsiYY8rGEg==] done')).to.deep.equal({ 489 | tag: 'TAG1', 490 | command: 'OK', 491 | attributes: [{ 492 | type: 'ATOM', 493 | value: '', 494 | section: [{ 495 | type: 'ATOM', 496 | value: 'URLMECH' 497 | }, { 498 | type: 'ATOM', 499 | value: 'INTERNAL' 500 | }, { 501 | type: 'ATOM', 502 | value: 'XSAMPLE=P34OKhO7VEkCbsiYY8rGEg==' 503 | }] 504 | }, { 505 | type: 'TEXT', 506 | value: 'done' 507 | }] 508 | }); 509 | 510 | // RFC2221 defines REFERRAL where the argument is an imapurl 511 | // (defined by RFC2192 which is obsoleted by RFC5092) which 512 | // is significantly more complicated than the rest of the IMAP 513 | // grammar and which was based on the RFC2060 grammar where 514 | // resp_text_code included: 515 | // atom [SPACE 1*] 516 | // So this is just a test case of our explicit special-casing 517 | // of REFERRAL. 518 | expect(imapHandler.parser('TAG1 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/] Remote Server')).to.deep.equal({ 519 | tag: 'TAG1', 520 | command: 'NO', 521 | attributes: [{ 522 | type: 'ATOM', 523 | value: '', 524 | section: [{ 525 | type: 'ATOM', 526 | value: 'REFERRAL' 527 | }, { 528 | type: 'ATOM', 529 | value: 'IMAP://user;AUTH=*@SERVER2/' 530 | }] 531 | }, { 532 | type: 'TEXT', 533 | value: 'Remote Server' 534 | }] 535 | }); 536 | 537 | // PERMANENTFLAGS is from RFC3501. Its syntax is also very 538 | // similar to BADCHARSET, except BADCHARSET has astrings 539 | // inside the list. 540 | expect(imapHandler.parser('* OK [PERMANENTFLAGS (de:hacking $label kt-evalution [css3-page] \\*)] Flags permitted.')).to.deep.equal({ 541 | tag: '*', 542 | command: 'OK', 543 | attributes: [{ 544 | type: 'ATOM', 545 | value: '', 546 | section: [ 547 | // keep indentation 548 | { 549 | type: 'ATOM', 550 | value: 'PERMANENTFLAGS' 551 | }, 552 | [{ 553 | type: 'ATOM', 554 | value: 'de:hacking' 555 | }, { 556 | type: 'ATOM', 557 | value: '$label' 558 | }, { 559 | type: 'ATOM', 560 | value: 'kt-evalution' 561 | }, { 562 | type: 'ATOM', 563 | value: '[css3-page]' 564 | }, { 565 | type: 'ATOM', 566 | value: '\\*' 567 | }] 568 | ] 569 | }, { 570 | type: 'TEXT', 571 | value: 'Flags permitted.' 572 | }] 573 | }); 574 | 575 | // COPYUID is from RFC4315 and included the previously failing 576 | // parsing situation of a sequence terminated by ']' rather than 577 | // whitespace. 578 | expect(imapHandler.parser('TAG1 OK [COPYUID 4 1417051618:1417051620 1421730687:1421730689] COPY completed')).to.deep.equal({ 579 | tag: 'TAG1', 580 | command: 'OK', 581 | attributes: [{ 582 | type: 'ATOM', 583 | value: '', 584 | section: [{ 585 | type: 'ATOM', 586 | value: 'COPYUID' 587 | }, { 588 | type: 'ATOM', 589 | value: '4' 590 | }, { 591 | type: 'SEQUENCE', 592 | value: '1417051618:1417051620' 593 | }, { 594 | type: 'SEQUENCE', 595 | value: '1421730687:1421730689' 596 | }] 597 | }, { 598 | type: 'TEXT', 599 | value: 'COPY completed' 600 | }] 601 | }); 602 | 603 | // MODIFIED is from RFC4551 and is basically the same situation 604 | // as the COPYUID case, but in this case our example sequences 605 | // have commas in them. (Note that if there was no comma, the 606 | // '7,9' payload would end up an ATOM.) 607 | expect(imapHandler.parser('TAG1 OK [MODIFIED 7,9] Conditional STORE failed')).to.deep.equal({ 608 | tag: 'TAG1', 609 | command: 'OK', 610 | attributes: [{ 611 | type: 'ATOM', 612 | value: '', 613 | section: [{ 614 | type: 'ATOM', 615 | value: 'MODIFIED' 616 | }, { 617 | type: 'SEQUENCE', 618 | value: '7,9' 619 | }] 620 | }, { 621 | type: 'TEXT', 622 | value: 'Conditional STORE failed' 623 | }] 624 | }); 625 | 626 | }); 627 | }); 628 | 629 | describe('ATOM Partial', function () { 630 | it('should succeed', function () { 631 | expect(imapHandler.parser('TAG1 CMD BODY[]<0>').attributes).to.deep.equal([{ 632 | type: 'ATOM', 633 | value: 'BODY', 634 | section: [], 635 | partial: [0] 636 | }]); 637 | expect(imapHandler.parser('TAG1 CMD BODY[]<12.45>').attributes).to.deep.equal([{ 638 | type: 'ATOM', 639 | value: 'BODY', 640 | section: [], 641 | partial: [12, 45] 642 | }]); 643 | expect(imapHandler.parser('TAG1 CMD BODY[HEADER.FIELDS (Subject From)]<12.45>').attributes).to.deep.equal([{ 644 | type: 'ATOM', 645 | value: 'BODY', 646 | section: [ 647 | // keep indentation 648 | { 649 | type: 'ATOM', 650 | value: 'HEADER.FIELDS' 651 | }, 652 | [{ 653 | type: 'ATOM', 654 | value: 'Subject' 655 | }, { 656 | type: 'ATOM', 657 | value: 'From' 658 | }] 659 | ], 660 | partial: [12, 45] 661 | }]); 662 | }); 663 | 664 | it('should fail', function () { 665 | expect(function () { 666 | imapHandler.parser('TAG1 CMD KODY<0.123>'); 667 | }).to.throw(Error); 668 | 669 | expect(function () { 670 | imapHandler.parser('TAG1 CMD BODY[]<01>'); 671 | }).to.throw(Error); 672 | 673 | expect(function () { 674 | imapHandler.parser('TAG1 CMD BODY[]<0.01>'); 675 | }).to.throw(Error); 676 | 677 | expect(function () { 678 | imapHandler.parser('TAG1 CMD BODY[]<0.1.>'); 679 | }).to.throw(Error); 680 | }); 681 | }); 682 | 683 | describe('SEQUENCE', function () { 684 | it('should succeed', function () { 685 | expect(imapHandler.parser('TAG1 CMD *:4,5:7 TEST').attributes).to.deep.equal([{ 686 | type: 'SEQUENCE', 687 | value: '*:4,5:7' 688 | }, { 689 | type: 'ATOM', 690 | value: 'TEST' 691 | }]); 692 | 693 | expect(imapHandler.parser('TAG1 CMD 1:* TEST').attributes).to.deep.equal([{ 694 | type: 'SEQUENCE', 695 | value: '1:*' 696 | }, { 697 | type: 'ATOM', 698 | value: 'TEST' 699 | }]); 700 | 701 | expect(imapHandler.parser('TAG1 CMD *:4 TEST').attributes).to.deep.equal([{ 702 | type: 'SEQUENCE', 703 | value: '*:4' 704 | }, { 705 | type: 'ATOM', 706 | value: 'TEST' 707 | }]); 708 | }); 709 | 710 | it('should fail', function () { 711 | expect(function () { 712 | imapHandler.parser('TAG1 CMD *:4,5:'); 713 | }).to.throw(Error); 714 | 715 | expect(function () { 716 | imapHandler.parser('TAG1 CMD *:4,5:TEST TEST'); 717 | }).to.throw(Error); 718 | 719 | expect(function () { 720 | imapHandler.parser('TAG1 CMD *:4,5: TEST'); 721 | }).to.throw(Error); 722 | 723 | expect(function () { 724 | imapHandler.parser('TAG1 CMD *4,5 TEST'); 725 | }).to.throw(Error); 726 | 727 | expect(function () { 728 | imapHandler.parser('TAG1 CMD *,5 TEST'); 729 | }).to.throw(Error); 730 | 731 | expect(function () { 732 | imapHandler.parser('TAG1 CMD 5,* TEST'); 733 | }).to.throw(Error); 734 | 735 | expect(function () { 736 | imapHandler.parser('TAG1 CMD 5, TEST'); 737 | }).to.throw(Error); 738 | }); 739 | }); 740 | 741 | describe('Escaped quotes', function () { 742 | it('should succeed', function () { 743 | expect(imapHandler.parser('* 331 FETCH (ENVELOPE ("=?ISO-8859-1?Q?\\"G=FCnter__Hammerl\\"?="))').attributes).to.deep.equal([ 744 | // keep indentation 745 | { 746 | type: 'ATOM', 747 | value: 'FETCH' 748 | }, 749 | [ 750 | // keep indentation 751 | { 752 | type: 'ATOM', 753 | value: 'ENVELOPE' 754 | }, 755 | [{ 756 | type: 'STRING', 757 | value: '=?ISO-8859-1?Q?"G=FCnter__Hammerl"?=' 758 | }] 759 | ] 760 | ]); 761 | }); 762 | }); 763 | 764 | describe('MimeTorture', function () { 765 | it('should parse mimetorture input', function () { 766 | var parsed; 767 | expect(function () { 768 | parsed = imapHandler.parser(mimetorture.input); 769 | }).to.not.throw(Error); 770 | expect(parsed).to.deep.equal(mimetorture.output); 771 | }); 772 | }); 773 | }); 774 | -------------------------------------------------------------------------------- /test/fixtures/mimetorture.js: -------------------------------------------------------------------------------- 1 | /* eslint indent: 0 */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | // IMAP response value for Ryan Finnie's MIME Torture Test v1.0 7 | // Command used: a fetch 1 (ENVELOPE BODYSTRUCTURE FLAGS BODY[]) 8 | input: '* 1 FETCH (FLAGS (\\Seen $eee) ENVELOPE ("23 Oct 2003 23:28:34 -0700" "Ryan Finnie\'s MIME Torture Test v1.0" (("Andris Reinman" NIL "andris" "ekiri.ee")) (("Andris Reinman" NIL "andris" "ekiri.ee")) (("Andris Reinman" NIL "andris" "ekiri.ee")) ((NIL NIL "andmekala" "hot.ee")) NIL NIL NIL "<1066976914.4721.5.camel@localhost>") BODYSTRUCTURE (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 617 16 NIL NIL NIL NIL)("message" "rfc822" NIL NIL "I\'ll be whatever I wanna do. --Fry" "7bit" 582 ("23 Oct 2003 22:25:56 -0700" "plain jane message" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066973156.4264.42.camel@localhost>") ("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 311 9 NIL NIL NIL NIL) 18 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Would you kindly shut your noise-hole? --Bender" "7bit" 1460 ("23 Oct 2003 23:15:11 -0700" "messages inside messages inside..." (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066976111.4263.74.camel@localhost>") (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 193 3 NIL NIL NIL NIL)("message" "rfc822" NIL NIL "At the risk of sounding negative, no. --Leela" "7bit" 697 ("23 Oct 2003 23:09:05 -0700" "the original message" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066975745.4263.70.camel@localhost>") (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 78 3 NIL NIL NIL NIL)("application" "x-gzip" ("NAME" "foo.gz") NIL NIL "base64" 58 NIL ("attachment" ("filename" "foo.gz")) NIL NIL) "mixed" ("boundary" "=-XFYecI7w+0shpolXq8bb") NIL NIL NIL) 25 NIL ("inline" NIL) NIL NIL) "mixed" ("boundary" "=-9Brg7LoMERBrIDtMRose") NIL NIL NIL) 49 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Dirt doesn\'t need luck! --Professor" "7bit" 817 ("23 Oct 2003 22:40:49 -0700" "this message JUST contains an attachment" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066974048.4264.62.camel@localhost>") ("application" "x-gzip" ("NAME" "blah.gz") NIL "Attachment has identical content to above foo.gz" "base64" 396 NIL ("attachment" ("filename" "blah.gz")) NIL NIL) 17 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Hold still, I don\'t have good depth perception! --Leela" "7bit" 1045 ("23 Oct 2003 23:09:16 -0700" "Attachment filename vs. name" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066975756.4263.70.camel@localhost>") (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 377 6 NIL NIL NIL NIL)("application" "x-gzip" ("NAME" "blah2.gz") NIL "filename is blah1.gz, name is blah2.gz" "base64" 58 NIL ("attachment" ("filename" "blah1.gz")) NIL NIL) "mixed" ("boundary" "=-1066975756jd02") NIL NIL NIL) 29 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Hello little man. I WILL DESTROY YOU! --Moro" "7bit" 1149 ("23 Oct 2003 23:09:21 -0700" {24}\r\nNo filename? No problem! (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066975761.4263.70.camel@localhost>") (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 517 10 NIL NIL NIL NIL)("application" "x-gzip" NIL NIL "I\'m getting sick of witty things to say" "base64" 58 NIL ("attachment" NIL) NIL NIL) "mixed" ("boundary" "=-1066975756jd03") NIL NIL NIL) 33 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Friends! Help! A guinea pig tricked me! --Zoidberg" "7bit" 896 ("23 Oct 2003 22:40:45 -0700" "html and text, both inline" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066974044.4264.62.camel@localhost>") (("text" "html" ("CHARSET" "utf-8") NIL NIL "8bit" 327 11 NIL NIL NIL NIL)("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 61 2 NIL NIL NIL NIL) "mixed" ("boundary" "=-ZCKMfHzvHMyK1iBu4kff") NIL NIL NIL) 33 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Smeesh! --Amy" "7bit" 642 ("23 Oct 2003 22:41:29 -0700" "text and text, both inline" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066974089.4265.64.camel@localhost>") (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 62 2 NIL NIL NIL NIL)("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 68 2 NIL NIL NIL NIL) "mixed" ("boundary" "=-pNc4wtlOIxs8RcX7H/AK") NIL NIL NIL) 24 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "That\'s not a cigar. Uh... and it\'s not mine. --Hermes" "7bit" 1515 ("23 Oct 2003 22:39:17 -0700" {17}\r\nHTML and... HTML? (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066973957.4263.59.camel@localhost>") (("text" "html" ("CHARSET" "utf-8") NIL NIL "8bit" 824 22 NIL NIL NIL NIL)("text" "html" ("NAME" "htmlfile.html" "CHARSET" "UTF-8") NIL NIL "8bit" 118 6 NIL ("attachment" ("filename" "htmlfile.html")) NIL NIL) "mixed" ("boundary" "=-zxh/IezwzZITiphpcbJZ") NIL NIL NIL) 49 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL {71}\r\nThe spirit is willing, but the flesh is spongy, and\r\n bruised. --Zapp "7bit" 6643 ("23 Oct 2003 22:23:16 -0700" "smiley!" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066972996.4264.39.camel@localhost>") ((((("text" "plain" ("charset" "us-ascii") NIL NIL "quoted-printable" 1606 42 NIL NIL NIL NIL)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 2128 54 NIL NIL NIL NIL) "alternative" ("boundary" "=-dHujWM/Xizz57x/JOmDF") NIL NIL NIL)("image" "png" ("name" "smiley-3.png") "<1066971953.4232.15.camel@localhost>" NIL "base64" 1122 NIL ("attachment" ("filename" "smiley-3.png")) NIL NIL) "related" ("type" "multipart/alternative" "boundary" "=-GpwozF9CQ7NdF+fd+vMG") NIL NIL NIL)("image" "gif" ("name" "dot.gif") NIL NIL "base64" 96 NIL ("attachment" ("filename" "dot.gif")) NIL NIL) "mixed" ("boundary" "=-CgV5jm9HAY9VbUlAuneA") NIL NIL NIL)("application" "pgp-signature" ("name" "signature.asc") NIL "This is a digitally signed message part" "7bit" 196 NIL NIL NIL NIL) "signed" ("micalg" "pgp-sha1" "protocol" "application/pgp-signature" "boundary" "=-vH3FQO9a8icUn1ROCoAi") NIL NIL NIL) 177 NIL ("inline" NIL) NIL NIL)("message" "rfc822" NIL NIL "Kittens give Morbo gas. --Morbo" "7bit" 3088 ("23 Oct 2003 22:32:37 -0700" "the PROPER way to do alternative/related" (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) (("Ryan Finnie" NIL "rfinnie" "domain.dom")) ((NIL NIL "bob" "domain.dom")) NIL NIL NIL "<1066973557.4265.51.camel@localhost>") (("text" "plain" ("CHARSET" "US-ASCII") NIL NIL "8bit" 863 22 NIL NIL NIL NIL)(("text" "html" ("CHARSET" "utf-8") NIL NIL "8bit" 1258 22 NIL NIL NIL NIL)("image" "gif" NIL "<1066973340.4232.46.camel@localhost>" NIL "base64" 116 NIL NIL NIL NIL) "related" ("boundary" "=-bFkxH1S3HVGcxi+o/5jG") NIL NIL NIL) "alternative" ("type" "multipart/alternative" "boundary" "=-tyGlQ9JvB5uvPWzozI+y") NIL NIL NIL) 79 NIL ("inline" NIL) NIL NIL) "mixed" ("boundary" "=-qYxqvD9rbH0PNeExagh1") NIL NIL NIL) BODY[] {0}\r\n)', 9 | output: { 10 | tag: '*', 11 | command: '1', 12 | attributes: [{ 13 | type: 'ATOM', 14 | value: 'FETCH' 15 | }, 16 | [{ 17 | type: 'ATOM', 18 | value: 'FLAGS' 19 | }, 20 | [{ 21 | type: 'ATOM', 22 | value: '\\Seen' 23 | }, { 24 | type: 'ATOM', 25 | value: '$eee' 26 | }], { 27 | type: 'ATOM', 28 | value: 'ENVELOPE' 29 | }, 30 | [{ 31 | type: 'STRING', 32 | value: '23 Oct 2003 23:28:34 -0700' 33 | }, { 34 | type: 'STRING', 35 | value: 'Ryan Finnie\'s MIME Torture Test v1.0' 36 | }, 37 | [ 38 | [{ 39 | type: 'STRING', 40 | value: 'Andris Reinman' 41 | }, 42 | null, { 43 | type: 'STRING', 44 | value: 'andris' 45 | }, { 46 | type: 'STRING', 47 | value: 'ekiri.ee' 48 | } 49 | ] 50 | ], 51 | [ 52 | [{ 53 | type: 'STRING', 54 | value: 'Andris Reinman' 55 | }, 56 | null, { 57 | type: 'STRING', 58 | value: 'andris' 59 | }, { 60 | type: 'STRING', 61 | value: 'ekiri.ee' 62 | } 63 | ] 64 | ], 65 | [ 66 | [{ 67 | type: 'STRING', 68 | value: 'Andris Reinman' 69 | }, 70 | null, { 71 | type: 'STRING', 72 | value: 'andris' 73 | }, { 74 | type: 'STRING', 75 | value: 'ekiri.ee' 76 | } 77 | ] 78 | ], 79 | [ 80 | [null, 81 | null, { 82 | type: 'STRING', 83 | value: 'andmekala' 84 | }, { 85 | type: 'STRING', 86 | value: 'hot.ee' 87 | } 88 | ] 89 | ], 90 | null, 91 | null, 92 | null, { 93 | type: 'STRING', 94 | value: '<1066976914.4721.5.camel@localhost>' 95 | } 96 | ], { 97 | type: 'ATOM', 98 | value: 'BODYSTRUCTURE' 99 | }, 100 | [ 101 | [{ 102 | type: 'STRING', 103 | value: 'text' 104 | }, { 105 | type: 'STRING', 106 | value: 'plain' 107 | }, 108 | [{ 109 | type: 'STRING', 110 | value: 'CHARSET' 111 | }, { 112 | type: 'STRING', 113 | value: 'US-ASCII' 114 | }], 115 | null, 116 | null, { 117 | type: 'STRING', 118 | value: '8bit' 119 | }, { 120 | type: 'ATOM', 121 | value: '617' 122 | }, { 123 | type: 'ATOM', 124 | value: '16' 125 | }, 126 | null, 127 | null, 128 | null, 129 | null 130 | ], 131 | [{ 132 | type: 'STRING', 133 | value: 'message' 134 | }, { 135 | type: 'STRING', 136 | value: 'rfc822' 137 | }, 138 | null, 139 | null, { 140 | type: 'STRING', 141 | value: 'I\'ll be whatever I wanna do. --Fry' 142 | }, { 143 | type: 'STRING', 144 | value: '7bit' 145 | }, { 146 | type: 'ATOM', 147 | value: '582' 148 | }, 149 | [{ 150 | type: 'STRING', 151 | value: '23 Oct 2003 22:25:56 -0700' 152 | }, { 153 | type: 'STRING', 154 | value: 'plain jane message' 155 | }, 156 | [ 157 | [{ 158 | type: 'STRING', 159 | value: 'Ryan Finnie' 160 | }, 161 | null, { 162 | type: 'STRING', 163 | value: 'rfinnie' 164 | }, { 165 | type: 'STRING', 166 | value: 'domain.dom' 167 | } 168 | ] 169 | ], 170 | [ 171 | [{ 172 | type: 'STRING', 173 | value: 'Ryan Finnie' 174 | }, 175 | null, { 176 | type: 'STRING', 177 | value: 'rfinnie' 178 | }, { 179 | type: 'STRING', 180 | value: 'domain.dom' 181 | } 182 | ] 183 | ], 184 | [ 185 | [{ 186 | type: 'STRING', 187 | value: 'Ryan Finnie' 188 | }, 189 | null, { 190 | type: 'STRING', 191 | value: 'rfinnie' 192 | }, { 193 | type: 'STRING', 194 | value: 'domain.dom' 195 | } 196 | ] 197 | ], 198 | [ 199 | [null, 200 | null, { 201 | type: 'STRING', 202 | value: 'bob' 203 | }, { 204 | type: 'STRING', 205 | value: 'domain.dom' 206 | } 207 | ] 208 | ], 209 | null, 210 | null, 211 | null, { 212 | type: 'STRING', 213 | value: '<1066973156.4264.42.camel@localhost>' 214 | } 215 | ], 216 | [{ 217 | type: 'STRING', 218 | value: 'text' 219 | }, { 220 | type: 'STRING', 221 | value: 'plain' 222 | }, 223 | [{ 224 | type: 'STRING', 225 | value: 'CHARSET' 226 | }, { 227 | type: 'STRING', 228 | value: 'US-ASCII' 229 | }], 230 | null, 231 | null, { 232 | type: 'STRING', 233 | value: '8bit' 234 | }, { 235 | type: 'ATOM', 236 | value: '311' 237 | }, { 238 | type: 'ATOM', 239 | value: '9' 240 | }, 241 | null, 242 | null, 243 | null, 244 | null 245 | ], { 246 | type: 'ATOM', 247 | value: '18' 248 | }, 249 | null, [{ 250 | type: 'STRING', 251 | value: 'inline' 252 | }, 253 | null 254 | ], 255 | null, 256 | null 257 | ], 258 | [{ 259 | type: 'STRING', 260 | value: 'message' 261 | }, { 262 | type: 'STRING', 263 | value: 'rfc822' 264 | }, 265 | null, 266 | null, { 267 | type: 'STRING', 268 | value: 'Would you kindly shut your noise-hole? --Bender' 269 | }, { 270 | type: 'STRING', 271 | value: '7bit' 272 | }, { 273 | type: 'ATOM', 274 | value: '1460' 275 | }, 276 | [{ 277 | type: 'STRING', 278 | value: '23 Oct 2003 23:15:11 -0700' 279 | }, { 280 | type: 'STRING', 281 | value: 'messages inside messages inside...' 282 | }, 283 | [ 284 | [{ 285 | type: 'STRING', 286 | value: 'Ryan Finnie' 287 | }, 288 | null, { 289 | type: 'STRING', 290 | value: 'rfinnie' 291 | }, { 292 | type: 'STRING', 293 | value: 'domain.dom' 294 | } 295 | ] 296 | ], 297 | [ 298 | [{ 299 | type: 'STRING', 300 | value: 'Ryan Finnie' 301 | }, 302 | null, { 303 | type: 'STRING', 304 | value: 'rfinnie' 305 | }, { 306 | type: 'STRING', 307 | value: 'domain.dom' 308 | } 309 | ] 310 | ], 311 | [ 312 | [{ 313 | type: 'STRING', 314 | value: 'Ryan Finnie' 315 | }, 316 | null, { 317 | type: 'STRING', 318 | value: 'rfinnie' 319 | }, { 320 | type: 'STRING', 321 | value: 'domain.dom' 322 | } 323 | ] 324 | ], 325 | [ 326 | [null, 327 | null, { 328 | type: 'STRING', 329 | value: 'bob' 330 | }, { 331 | type: 'STRING', 332 | value: 'domain.dom' 333 | } 334 | ] 335 | ], 336 | null, 337 | null, 338 | null, { 339 | type: 'STRING', 340 | value: '<1066976111.4263.74.camel@localhost>' 341 | } 342 | ], 343 | [ 344 | [{ 345 | type: 'STRING', 346 | value: 'text' 347 | }, { 348 | type: 'STRING', 349 | value: 'plain' 350 | }, 351 | [{ 352 | type: 'STRING', 353 | value: 'CHARSET' 354 | }, { 355 | type: 'STRING', 356 | value: 'US-ASCII' 357 | }], 358 | null, 359 | null, { 360 | type: 'STRING', 361 | value: '8bit' 362 | }, { 363 | type: 'ATOM', 364 | value: '193' 365 | }, { 366 | type: 'ATOM', 367 | value: '3' 368 | }, 369 | null, 370 | null, 371 | null, 372 | null 373 | ], 374 | [{ 375 | type: 'STRING', 376 | value: 'message' 377 | }, { 378 | type: 'STRING', 379 | value: 'rfc822' 380 | }, 381 | null, 382 | null, { 383 | type: 'STRING', 384 | value: 'At the risk of sounding negative, no. --Leela' 385 | }, { 386 | type: 'STRING', 387 | value: '7bit' 388 | }, { 389 | type: 'ATOM', 390 | value: '697' 391 | }, 392 | [{ 393 | type: 'STRING', 394 | value: '23 Oct 2003 23:09:05 -0700' 395 | }, { 396 | type: 'STRING', 397 | value: 'the original message' 398 | }, 399 | [ 400 | [{ 401 | type: 'STRING', 402 | value: 'Ryan Finnie' 403 | }, 404 | null, { 405 | type: 'STRING', 406 | value: 'rfinnie' 407 | }, { 408 | type: 'STRING', 409 | value: 'domain.dom' 410 | } 411 | ] 412 | ], 413 | [ 414 | [{ 415 | type: 'STRING', 416 | value: 'Ryan Finnie' 417 | }, 418 | null, { 419 | type: 'STRING', 420 | value: 'rfinnie' 421 | }, { 422 | type: 'STRING', 423 | value: 'domain.dom' 424 | } 425 | ] 426 | ], 427 | [ 428 | [{ 429 | type: 'STRING', 430 | value: 'Ryan Finnie' 431 | }, 432 | null, { 433 | type: 'STRING', 434 | value: 'rfinnie' 435 | }, { 436 | type: 'STRING', 437 | value: 'domain.dom' 438 | } 439 | ] 440 | ], 441 | [ 442 | [null, 443 | null, { 444 | type: 'STRING', 445 | value: 'bob' 446 | }, { 447 | type: 'STRING', 448 | value: 'domain.dom' 449 | } 450 | ] 451 | ], 452 | null, 453 | null, 454 | null, { 455 | type: 'STRING', 456 | value: '<1066975745.4263.70.camel@localhost>' 457 | } 458 | ], 459 | [ 460 | [{ 461 | type: 'STRING', 462 | value: 'text' 463 | }, { 464 | type: 'STRING', 465 | value: 'plain' 466 | }, 467 | [{ 468 | type: 'STRING', 469 | value: 'CHARSET' 470 | }, { 471 | type: 'STRING', 472 | value: 'US-ASCII' 473 | }], 474 | null, 475 | null, { 476 | type: 'STRING', 477 | value: '8bit' 478 | }, { 479 | type: 'ATOM', 480 | value: '78' 481 | }, { 482 | type: 'ATOM', 483 | value: '3' 484 | }, 485 | null, 486 | null, 487 | null, 488 | null 489 | ], 490 | [{ 491 | type: 'STRING', 492 | value: 'application' 493 | }, { 494 | type: 'STRING', 495 | value: 'x-gzip' 496 | }, 497 | [{ 498 | type: 'STRING', 499 | value: 'NAME' 500 | }, { 501 | type: 'STRING', 502 | value: 'foo.gz' 503 | }], 504 | null, 505 | null, { 506 | type: 'STRING', 507 | value: 'base64' 508 | }, { 509 | type: 'ATOM', 510 | value: '58' 511 | }, 512 | null, [{ 513 | type: 'STRING', 514 | value: 'attachment' 515 | }, 516 | [{ 517 | type: 'STRING', 518 | value: 'filename' 519 | }, { 520 | type: 'STRING', 521 | value: 'foo.gz' 522 | }] 523 | ], 524 | null, 525 | null 526 | ], { 527 | type: 'STRING', 528 | value: 'mixed' 529 | }, 530 | [{ 531 | type: 'STRING', 532 | value: 'boundary' 533 | }, { 534 | type: 'STRING', 535 | value: '=-XFYecI7w+0shpolXq8bb' 536 | }], 537 | null, 538 | null, 539 | null 540 | ], { 541 | type: 'ATOM', 542 | value: '25' 543 | }, 544 | null, [{ 545 | type: 'STRING', 546 | value: 'inline' 547 | }, 548 | null 549 | ], 550 | null, 551 | null 552 | ], { 553 | type: 'STRING', 554 | value: 'mixed' 555 | }, 556 | [{ 557 | type: 'STRING', 558 | value: 'boundary' 559 | }, { 560 | type: 'STRING', 561 | value: '=-9Brg7LoMERBrIDtMRose' 562 | }], 563 | null, 564 | null, 565 | null 566 | ], { 567 | type: 'ATOM', 568 | value: '49' 569 | }, 570 | null, [{ 571 | type: 'STRING', 572 | value: 'inline' 573 | }, 574 | null 575 | ], 576 | null, 577 | null 578 | ], 579 | [{ 580 | type: 'STRING', 581 | value: 'message' 582 | }, { 583 | type: 'STRING', 584 | value: 'rfc822' 585 | }, 586 | null, 587 | null, { 588 | type: 'STRING', 589 | value: 'Dirt doesn\'t need luck! --Professor' 590 | }, { 591 | type: 'STRING', 592 | value: '7bit' 593 | }, { 594 | type: 'ATOM', 595 | value: '817' 596 | }, 597 | [{ 598 | type: 'STRING', 599 | value: '23 Oct 2003 22:40:49 -0700' 600 | }, { 601 | type: 'STRING', 602 | value: 'this message JUST contains an attachment' 603 | }, 604 | [ 605 | [{ 606 | type: 'STRING', 607 | value: 'Ryan Finnie' 608 | }, 609 | null, { 610 | type: 'STRING', 611 | value: 'rfinnie' 612 | }, { 613 | type: 'STRING', 614 | value: 'domain.dom' 615 | } 616 | ] 617 | ], 618 | [ 619 | [{ 620 | type: 'STRING', 621 | value: 'Ryan Finnie' 622 | }, 623 | null, { 624 | type: 'STRING', 625 | value: 'rfinnie' 626 | }, { 627 | type: 'STRING', 628 | value: 'domain.dom' 629 | } 630 | ] 631 | ], 632 | [ 633 | [{ 634 | type: 'STRING', 635 | value: 'Ryan Finnie' 636 | }, 637 | null, { 638 | type: 'STRING', 639 | value: 'rfinnie' 640 | }, { 641 | type: 'STRING', 642 | value: 'domain.dom' 643 | } 644 | ] 645 | ], 646 | [ 647 | [null, 648 | null, { 649 | type: 'STRING', 650 | value: 'bob' 651 | }, { 652 | type: 'STRING', 653 | value: 'domain.dom' 654 | } 655 | ] 656 | ], 657 | null, 658 | null, 659 | null, { 660 | type: 'STRING', 661 | value: '<1066974048.4264.62.camel@localhost>' 662 | } 663 | ], 664 | [{ 665 | type: 'STRING', 666 | value: 'application' 667 | }, { 668 | type: 'STRING', 669 | value: 'x-gzip' 670 | }, 671 | [{ 672 | type: 'STRING', 673 | value: 'NAME' 674 | }, { 675 | type: 'STRING', 676 | value: 'blah.gz' 677 | }], 678 | null, { 679 | type: 'STRING', 680 | value: 'Attachment has identical content to above foo.gz' 681 | }, { 682 | type: 'STRING', 683 | value: 'base64' 684 | }, { 685 | type: 'ATOM', 686 | value: '396' 687 | }, 688 | null, [{ 689 | type: 'STRING', 690 | value: 'attachment' 691 | }, 692 | [{ 693 | type: 'STRING', 694 | value: 'filename' 695 | }, { 696 | type: 'STRING', 697 | value: 'blah.gz' 698 | }] 699 | ], 700 | null, 701 | null 702 | ], { 703 | type: 'ATOM', 704 | value: '17' 705 | }, 706 | null, [{ 707 | type: 'STRING', 708 | value: 'inline' 709 | }, 710 | null 711 | ], 712 | null, 713 | null 714 | ], 715 | [{ 716 | type: 'STRING', 717 | value: 'message' 718 | }, { 719 | type: 'STRING', 720 | value: 'rfc822' 721 | }, 722 | null, 723 | null, { 724 | type: 'STRING', 725 | value: 'Hold still, I don\'t have good depth perception! --Leela' 726 | }, { 727 | type: 'STRING', 728 | value: '7bit' 729 | }, { 730 | type: 'ATOM', 731 | value: '1045' 732 | }, 733 | [{ 734 | type: 'STRING', 735 | value: '23 Oct 2003 23:09:16 -0700' 736 | }, { 737 | type: 'STRING', 738 | value: 'Attachment filename vs. name' 739 | }, 740 | [ 741 | [{ 742 | type: 'STRING', 743 | value: 'Ryan Finnie' 744 | }, 745 | null, { 746 | type: 'STRING', 747 | value: 'rfinnie' 748 | }, { 749 | type: 'STRING', 750 | value: 'domain.dom' 751 | } 752 | ] 753 | ], 754 | [ 755 | [{ 756 | type: 'STRING', 757 | value: 'Ryan Finnie' 758 | }, 759 | null, { 760 | type: 'STRING', 761 | value: 'rfinnie' 762 | }, { 763 | type: 'STRING', 764 | value: 'domain.dom' 765 | } 766 | ] 767 | ], 768 | [ 769 | [{ 770 | type: 'STRING', 771 | value: 'Ryan Finnie' 772 | }, 773 | null, { 774 | type: 'STRING', 775 | value: 'rfinnie' 776 | }, { 777 | type: 'STRING', 778 | value: 'domain.dom' 779 | } 780 | ] 781 | ], 782 | [ 783 | [null, 784 | null, { 785 | type: 'STRING', 786 | value: 'bob' 787 | }, { 788 | type: 'STRING', 789 | value: 'domain.dom' 790 | } 791 | ] 792 | ], 793 | null, 794 | null, 795 | null, { 796 | type: 'STRING', 797 | value: '<1066975756.4263.70.camel@localhost>' 798 | } 799 | ], 800 | [ 801 | [{ 802 | type: 'STRING', 803 | value: 'text' 804 | }, { 805 | type: 'STRING', 806 | value: 'plain' 807 | }, 808 | [{ 809 | type: 'STRING', 810 | value: 'CHARSET' 811 | }, { 812 | type: 'STRING', 813 | value: 'US-ASCII' 814 | }], 815 | null, 816 | null, { 817 | type: 'STRING', 818 | value: '8bit' 819 | }, { 820 | type: 'ATOM', 821 | value: '377' 822 | }, { 823 | type: 'ATOM', 824 | value: '6' 825 | }, 826 | null, 827 | null, 828 | null, 829 | null 830 | ], 831 | [{ 832 | type: 'STRING', 833 | value: 'application' 834 | }, { 835 | type: 'STRING', 836 | value: 'x-gzip' 837 | }, 838 | [{ 839 | type: 'STRING', 840 | value: 'NAME' 841 | }, { 842 | type: 'STRING', 843 | value: 'blah2.gz' 844 | }], 845 | null, { 846 | type: 'STRING', 847 | value: 'filename is blah1.gz, name is blah2.gz' 848 | }, { 849 | type: 'STRING', 850 | value: 'base64' 851 | }, { 852 | type: 'ATOM', 853 | value: '58' 854 | }, 855 | null, [{ 856 | type: 'STRING', 857 | value: 'attachment' 858 | }, 859 | [{ 860 | type: 'STRING', 861 | value: 'filename' 862 | }, { 863 | type: 'STRING', 864 | value: 'blah1.gz' 865 | }] 866 | ], 867 | null, 868 | null 869 | ], { 870 | type: 'STRING', 871 | value: 'mixed' 872 | }, 873 | [{ 874 | type: 'STRING', 875 | value: 'boundary' 876 | }, { 877 | type: 'STRING', 878 | value: '=-1066975756jd02' 879 | }], 880 | null, 881 | null, 882 | null 883 | ], { 884 | type: 'ATOM', 885 | value: '29' 886 | }, 887 | null, [{ 888 | type: 'STRING', 889 | value: 'inline' 890 | }, 891 | null 892 | ], 893 | null, 894 | null 895 | ], 896 | [{ 897 | type: 'STRING', 898 | value: 'message' 899 | }, { 900 | type: 'STRING', 901 | value: 'rfc822' 902 | }, 903 | null, 904 | null, { 905 | type: 'STRING', 906 | value: 'Hello little man. I WILL DESTROY YOU! --Moro' 907 | }, { 908 | type: 'STRING', 909 | value: '7bit' 910 | }, { 911 | type: 'ATOM', 912 | value: '1149' 913 | }, 914 | [{ 915 | type: 'STRING', 916 | value: '23 Oct 2003 23:09:21 -0700' 917 | }, { 918 | type: 'LITERAL', 919 | value: 'No filename? No problem!' 920 | }, 921 | [ 922 | [{ 923 | type: 'STRING', 924 | value: 'Ryan Finnie' 925 | }, 926 | null, { 927 | type: 'STRING', 928 | value: 'rfinnie' 929 | }, { 930 | type: 'STRING', 931 | value: 'domain.dom' 932 | } 933 | ] 934 | ], 935 | [ 936 | [{ 937 | type: 'STRING', 938 | value: 'Ryan Finnie' 939 | }, 940 | null, { 941 | type: 'STRING', 942 | value: 'rfinnie' 943 | }, { 944 | type: 'STRING', 945 | value: 'domain.dom' 946 | } 947 | ] 948 | ], 949 | [ 950 | [{ 951 | type: 'STRING', 952 | value: 'Ryan Finnie' 953 | }, 954 | null, { 955 | type: 'STRING', 956 | value: 'rfinnie' 957 | }, { 958 | type: 'STRING', 959 | value: 'domain.dom' 960 | } 961 | ] 962 | ], 963 | [ 964 | [null, 965 | null, { 966 | type: 'STRING', 967 | value: 'bob' 968 | }, { 969 | type: 'STRING', 970 | value: 'domain.dom' 971 | } 972 | ] 973 | ], 974 | null, 975 | null, 976 | null, { 977 | type: 'STRING', 978 | value: '<1066975761.4263.70.camel@localhost>' 979 | } 980 | ], 981 | [ 982 | [{ 983 | type: 'STRING', 984 | value: 'text' 985 | }, { 986 | type: 'STRING', 987 | value: 'plain' 988 | }, 989 | [{ 990 | type: 'STRING', 991 | value: 'CHARSET' 992 | }, { 993 | type: 'STRING', 994 | value: 'US-ASCII' 995 | }], 996 | null, 997 | null, { 998 | type: 'STRING', 999 | value: '8bit' 1000 | }, { 1001 | type: 'ATOM', 1002 | value: '517' 1003 | }, { 1004 | type: 'ATOM', 1005 | value: '10' 1006 | }, 1007 | null, 1008 | null, 1009 | null, 1010 | null 1011 | ], 1012 | [{ 1013 | type: 'STRING', 1014 | value: 'application' 1015 | }, { 1016 | type: 'STRING', 1017 | value: 'x-gzip' 1018 | }, 1019 | null, 1020 | null, { 1021 | type: 'STRING', 1022 | value: 'I\'m getting sick of witty things to say' 1023 | }, { 1024 | type: 'STRING', 1025 | value: 'base64' 1026 | }, { 1027 | type: 'ATOM', 1028 | value: '58' 1029 | }, 1030 | null, [{ 1031 | type: 'STRING', 1032 | value: 'attachment' 1033 | }, 1034 | null 1035 | ], 1036 | null, 1037 | null 1038 | ], { 1039 | type: 'STRING', 1040 | value: 'mixed' 1041 | }, 1042 | [{ 1043 | type: 'STRING', 1044 | value: 'boundary' 1045 | }, { 1046 | type: 'STRING', 1047 | value: '=-1066975756jd03' 1048 | }], 1049 | null, 1050 | null, 1051 | null 1052 | ], { 1053 | type: 'ATOM', 1054 | value: '33' 1055 | }, 1056 | null, [{ 1057 | type: 'STRING', 1058 | value: 'inline' 1059 | }, 1060 | null 1061 | ], 1062 | null, 1063 | null 1064 | ], 1065 | [{ 1066 | type: 'STRING', 1067 | value: 'message' 1068 | }, { 1069 | type: 'STRING', 1070 | value: 'rfc822' 1071 | }, 1072 | null, 1073 | null, { 1074 | type: 'STRING', 1075 | value: 'Friends! Help! A guinea pig tricked me! --Zoidberg' 1076 | }, { 1077 | type: 'STRING', 1078 | value: '7bit' 1079 | }, { 1080 | type: 'ATOM', 1081 | value: '896' 1082 | }, 1083 | [{ 1084 | type: 'STRING', 1085 | value: '23 Oct 2003 22:40:45 -0700' 1086 | }, { 1087 | type: 'STRING', 1088 | value: 'html and text, both inline' 1089 | }, 1090 | [ 1091 | [{ 1092 | type: 'STRING', 1093 | value: 'Ryan Finnie' 1094 | }, 1095 | null, { 1096 | type: 'STRING', 1097 | value: 'rfinnie' 1098 | }, { 1099 | type: 'STRING', 1100 | value: 'domain.dom' 1101 | } 1102 | ] 1103 | ], 1104 | [ 1105 | [{ 1106 | type: 'STRING', 1107 | value: 'Ryan Finnie' 1108 | }, 1109 | null, { 1110 | type: 'STRING', 1111 | value: 'rfinnie' 1112 | }, { 1113 | type: 'STRING', 1114 | value: 'domain.dom' 1115 | } 1116 | ] 1117 | ], 1118 | [ 1119 | [{ 1120 | type: 'STRING', 1121 | value: 'Ryan Finnie' 1122 | }, 1123 | null, { 1124 | type: 'STRING', 1125 | value: 'rfinnie' 1126 | }, { 1127 | type: 'STRING', 1128 | value: 'domain.dom' 1129 | } 1130 | ] 1131 | ], 1132 | [ 1133 | [null, 1134 | null, { 1135 | type: 'STRING', 1136 | value: 'bob' 1137 | }, { 1138 | type: 'STRING', 1139 | value: 'domain.dom' 1140 | } 1141 | ] 1142 | ], 1143 | null, 1144 | null, 1145 | null, { 1146 | type: 'STRING', 1147 | value: '<1066974044.4264.62.camel@localhost>' 1148 | } 1149 | ], 1150 | [ 1151 | [{ 1152 | type: 'STRING', 1153 | value: 'text' 1154 | }, { 1155 | type: 'STRING', 1156 | value: 'html' 1157 | }, 1158 | [{ 1159 | type: 'STRING', 1160 | value: 'CHARSET' 1161 | }, { 1162 | type: 'STRING', 1163 | value: 'utf-8' 1164 | }], 1165 | null, 1166 | null, { 1167 | type: 'STRING', 1168 | value: '8bit' 1169 | }, { 1170 | type: 'ATOM', 1171 | value: '327' 1172 | }, { 1173 | type: 'ATOM', 1174 | value: '11' 1175 | }, 1176 | null, 1177 | null, 1178 | null, 1179 | null 1180 | ], 1181 | [{ 1182 | type: 'STRING', 1183 | value: 'text' 1184 | }, { 1185 | type: 'STRING', 1186 | value: 'plain' 1187 | }, 1188 | [{ 1189 | type: 'STRING', 1190 | value: 'CHARSET' 1191 | }, { 1192 | type: 'STRING', 1193 | value: 'US-ASCII' 1194 | }], 1195 | null, 1196 | null, { 1197 | type: 'STRING', 1198 | value: '8bit' 1199 | }, { 1200 | type: 'ATOM', 1201 | value: '61' 1202 | }, { 1203 | type: 'ATOM', 1204 | value: '2' 1205 | }, 1206 | null, 1207 | null, 1208 | null, 1209 | null 1210 | ], { 1211 | type: 'STRING', 1212 | value: 'mixed' 1213 | }, 1214 | [{ 1215 | type: 'STRING', 1216 | value: 'boundary' 1217 | }, { 1218 | type: 'STRING', 1219 | value: '=-ZCKMfHzvHMyK1iBu4kff' 1220 | }], 1221 | null, 1222 | null, 1223 | null 1224 | ], { 1225 | type: 'ATOM', 1226 | value: '33' 1227 | }, 1228 | null, [{ 1229 | type: 'STRING', 1230 | value: 'inline' 1231 | }, 1232 | null 1233 | ], 1234 | null, 1235 | null 1236 | ], 1237 | [{ 1238 | type: 'STRING', 1239 | value: 'message' 1240 | }, { 1241 | type: 'STRING', 1242 | value: 'rfc822' 1243 | }, 1244 | null, 1245 | null, { 1246 | type: 'STRING', 1247 | value: 'Smeesh! --Amy' 1248 | }, { 1249 | type: 'STRING', 1250 | value: '7bit' 1251 | }, { 1252 | type: 'ATOM', 1253 | value: '642' 1254 | }, 1255 | [{ 1256 | type: 'STRING', 1257 | value: '23 Oct 2003 22:41:29 -0700' 1258 | }, { 1259 | type: 'STRING', 1260 | value: 'text and text, both inline' 1261 | }, 1262 | [ 1263 | [{ 1264 | type: 'STRING', 1265 | value: 'Ryan Finnie' 1266 | }, 1267 | null, { 1268 | type: 'STRING', 1269 | value: 'rfinnie' 1270 | }, { 1271 | type: 'STRING', 1272 | value: 'domain.dom' 1273 | } 1274 | ] 1275 | ], 1276 | [ 1277 | [{ 1278 | type: 'STRING', 1279 | value: 'Ryan Finnie' 1280 | }, 1281 | null, { 1282 | type: 'STRING', 1283 | value: 'rfinnie' 1284 | }, { 1285 | type: 'STRING', 1286 | value: 'domain.dom' 1287 | } 1288 | ] 1289 | ], 1290 | [ 1291 | [{ 1292 | type: 'STRING', 1293 | value: 'Ryan Finnie' 1294 | }, 1295 | null, { 1296 | type: 'STRING', 1297 | value: 'rfinnie' 1298 | }, { 1299 | type: 'STRING', 1300 | value: 'domain.dom' 1301 | } 1302 | ] 1303 | ], 1304 | [ 1305 | [null, 1306 | null, { 1307 | type: 'STRING', 1308 | value: 'bob' 1309 | }, { 1310 | type: 'STRING', 1311 | value: 'domain.dom' 1312 | } 1313 | ] 1314 | ], 1315 | null, 1316 | null, 1317 | null, { 1318 | type: 'STRING', 1319 | value: '<1066974089.4265.64.camel@localhost>' 1320 | } 1321 | ], 1322 | [ 1323 | [{ 1324 | type: 'STRING', 1325 | value: 'text' 1326 | }, { 1327 | type: 'STRING', 1328 | value: 'plain' 1329 | }, 1330 | [{ 1331 | type: 'STRING', 1332 | value: 'CHARSET' 1333 | }, { 1334 | type: 'STRING', 1335 | value: 'US-ASCII' 1336 | }], 1337 | null, 1338 | null, { 1339 | type: 'STRING', 1340 | value: '8bit' 1341 | }, { 1342 | type: 'ATOM', 1343 | value: '62' 1344 | }, { 1345 | type: 'ATOM', 1346 | value: '2' 1347 | }, 1348 | null, 1349 | null, 1350 | null, 1351 | null 1352 | ], 1353 | [{ 1354 | type: 'STRING', 1355 | value: 'text' 1356 | }, { 1357 | type: 'STRING', 1358 | value: 'plain' 1359 | }, 1360 | [{ 1361 | type: 'STRING', 1362 | value: 'CHARSET' 1363 | }, { 1364 | type: 'STRING', 1365 | value: 'US-ASCII' 1366 | }], 1367 | null, 1368 | null, { 1369 | type: 'STRING', 1370 | value: '8bit' 1371 | }, { 1372 | type: 'ATOM', 1373 | value: '68' 1374 | }, { 1375 | type: 'ATOM', 1376 | value: '2' 1377 | }, 1378 | null, 1379 | null, 1380 | null, 1381 | null 1382 | ], { 1383 | type: 'STRING', 1384 | value: 'mixed' 1385 | }, 1386 | [{ 1387 | type: 'STRING', 1388 | value: 'boundary' 1389 | }, { 1390 | type: 'STRING', 1391 | value: '=-pNc4wtlOIxs8RcX7H/AK' 1392 | }], 1393 | null, 1394 | null, 1395 | null 1396 | ], { 1397 | type: 'ATOM', 1398 | value: '24' 1399 | }, 1400 | null, [{ 1401 | type: 'STRING', 1402 | value: 'inline' 1403 | }, 1404 | null 1405 | ], 1406 | null, 1407 | null 1408 | ], 1409 | [{ 1410 | type: 'STRING', 1411 | value: 'message' 1412 | }, { 1413 | type: 'STRING', 1414 | value: 'rfc822' 1415 | }, 1416 | null, 1417 | null, { 1418 | type: 'STRING', 1419 | value: 'That\'s not a cigar. Uh... and it\'s not mine. --Hermes' 1420 | }, { 1421 | type: 'STRING', 1422 | value: '7bit' 1423 | }, { 1424 | type: 'ATOM', 1425 | value: '1515' 1426 | }, 1427 | [{ 1428 | type: 'STRING', 1429 | value: '23 Oct 2003 22:39:17 -0700' 1430 | }, { 1431 | type: 'LITERAL', 1432 | value: 'HTML and... HTML?' 1433 | }, 1434 | [ 1435 | [{ 1436 | type: 'STRING', 1437 | value: 'Ryan Finnie' 1438 | }, 1439 | null, { 1440 | type: 'STRING', 1441 | value: 'rfinnie' 1442 | }, { 1443 | type: 'STRING', 1444 | value: 'domain.dom' 1445 | } 1446 | ] 1447 | ], 1448 | [ 1449 | [{ 1450 | type: 'STRING', 1451 | value: 'Ryan Finnie' 1452 | }, 1453 | null, { 1454 | type: 'STRING', 1455 | value: 'rfinnie' 1456 | }, { 1457 | type: 'STRING', 1458 | value: 'domain.dom' 1459 | } 1460 | ] 1461 | ], 1462 | [ 1463 | [{ 1464 | type: 'STRING', 1465 | value: 'Ryan Finnie' 1466 | }, 1467 | null, { 1468 | type: 'STRING', 1469 | value: 'rfinnie' 1470 | }, { 1471 | type: 'STRING', 1472 | value: 'domain.dom' 1473 | } 1474 | ] 1475 | ], 1476 | [ 1477 | [null, 1478 | null, { 1479 | type: 'STRING', 1480 | value: 'bob' 1481 | }, { 1482 | type: 'STRING', 1483 | value: 'domain.dom' 1484 | } 1485 | ] 1486 | ], 1487 | null, 1488 | null, 1489 | null, { 1490 | type: 'STRING', 1491 | value: '<1066973957.4263.59.camel@localhost>' 1492 | } 1493 | ], 1494 | [ 1495 | [{ 1496 | type: 'STRING', 1497 | value: 'text' 1498 | }, { 1499 | type: 'STRING', 1500 | value: 'html' 1501 | }, 1502 | [{ 1503 | type: 'STRING', 1504 | value: 'CHARSET' 1505 | }, { 1506 | type: 'STRING', 1507 | value: 'utf-8' 1508 | }], 1509 | null, 1510 | null, { 1511 | type: 'STRING', 1512 | value: '8bit' 1513 | }, { 1514 | type: 'ATOM', 1515 | value: '824' 1516 | }, { 1517 | type: 'ATOM', 1518 | value: '22' 1519 | }, 1520 | null, 1521 | null, 1522 | null, 1523 | null 1524 | ], 1525 | [{ 1526 | type: 'STRING', 1527 | value: 'text' 1528 | }, { 1529 | type: 'STRING', 1530 | value: 'html' 1531 | }, 1532 | [{ 1533 | type: 'STRING', 1534 | value: 'NAME' 1535 | }, { 1536 | type: 'STRING', 1537 | value: 'htmlfile.html' 1538 | }, { 1539 | type: 'STRING', 1540 | value: 'CHARSET' 1541 | }, { 1542 | type: 'STRING', 1543 | value: 'UTF-8' 1544 | }], 1545 | null, 1546 | null, { 1547 | type: 'STRING', 1548 | value: '8bit' 1549 | }, { 1550 | type: 'ATOM', 1551 | value: '118' 1552 | }, { 1553 | type: 'ATOM', 1554 | value: '6' 1555 | }, 1556 | null, [{ 1557 | type: 'STRING', 1558 | value: 'attachment' 1559 | }, 1560 | [{ 1561 | type: 'STRING', 1562 | value: 'filename' 1563 | }, { 1564 | type: 'STRING', 1565 | value: 'htmlfile.html' 1566 | }] 1567 | ], 1568 | null, 1569 | null 1570 | ], { 1571 | type: 'STRING', 1572 | value: 'mixed' 1573 | }, 1574 | [{ 1575 | type: 'STRING', 1576 | value: 'boundary' 1577 | }, { 1578 | type: 'STRING', 1579 | value: '=-zxh/IezwzZITiphpcbJZ' 1580 | }], 1581 | null, 1582 | null, 1583 | null 1584 | ], { 1585 | type: 'ATOM', 1586 | value: '49' 1587 | }, 1588 | null, [{ 1589 | type: 'STRING', 1590 | value: 'inline' 1591 | }, 1592 | null 1593 | ], 1594 | null, 1595 | null 1596 | ], 1597 | [{ 1598 | type: 'STRING', 1599 | value: 'message' 1600 | }, { 1601 | type: 'STRING', 1602 | value: 'rfc822' 1603 | }, 1604 | null, 1605 | null, { 1606 | type: 'LITERAL', 1607 | value: 'The spirit is willing, but the flesh is spongy, and\r\n bruised. --Zap' 1608 | }, { 1609 | type: 'ATOM', 1610 | value: 'p' 1611 | }, { 1612 | type: 'STRING', 1613 | value: '7bit' 1614 | }, { 1615 | type: 'ATOM', 1616 | value: '6643' 1617 | }, 1618 | [{ 1619 | type: 'STRING', 1620 | value: '23 Oct 2003 22:23:16 -0700' 1621 | }, { 1622 | type: 'STRING', 1623 | value: 'smiley!' 1624 | }, 1625 | [ 1626 | [{ 1627 | type: 'STRING', 1628 | value: 'Ryan Finnie' 1629 | }, 1630 | null, { 1631 | type: 'STRING', 1632 | value: 'rfinnie' 1633 | }, { 1634 | type: 'STRING', 1635 | value: 'domain.dom' 1636 | } 1637 | ] 1638 | ], 1639 | [ 1640 | [{ 1641 | type: 'STRING', 1642 | value: 'Ryan Finnie' 1643 | }, 1644 | null, { 1645 | type: 'STRING', 1646 | value: 'rfinnie' 1647 | }, { 1648 | type: 'STRING', 1649 | value: 'domain.dom' 1650 | } 1651 | ] 1652 | ], 1653 | [ 1654 | [{ 1655 | type: 'STRING', 1656 | value: 'Ryan Finnie' 1657 | }, 1658 | null, { 1659 | type: 'STRING', 1660 | value: 'rfinnie' 1661 | }, { 1662 | type: 'STRING', 1663 | value: 'domain.dom' 1664 | } 1665 | ] 1666 | ], 1667 | [ 1668 | [null, 1669 | null, { 1670 | type: 'STRING', 1671 | value: 'bob' 1672 | }, { 1673 | type: 'STRING', 1674 | value: 'domain.dom' 1675 | } 1676 | ] 1677 | ], 1678 | null, 1679 | null, 1680 | null, { 1681 | type: 'STRING', 1682 | value: '<1066972996.4264.39.camel@localhost>' 1683 | } 1684 | ], 1685 | [ 1686 | [ 1687 | [ 1688 | [ 1689 | [{ 1690 | type: 'STRING', 1691 | value: 'text' 1692 | }, { 1693 | type: 'STRING', 1694 | value: 'plain' 1695 | }, 1696 | [{ 1697 | type: 'STRING', 1698 | value: 'charset' 1699 | }, { 1700 | type: 'STRING', 1701 | value: 'us-ascii' 1702 | }], 1703 | null, 1704 | null, { 1705 | type: 'STRING', 1706 | value: 'quoted-printable' 1707 | }, { 1708 | type: 'ATOM', 1709 | value: '1606' 1710 | }, { 1711 | type: 'ATOM', 1712 | value: '42' 1713 | }, 1714 | null, 1715 | null, 1716 | null, 1717 | null 1718 | ], 1719 | [{ 1720 | type: 'STRING', 1721 | value: 'text' 1722 | }, { 1723 | type: 'STRING', 1724 | value: 'html' 1725 | }, 1726 | [{ 1727 | type: 'STRING', 1728 | value: 'charset' 1729 | }, { 1730 | type: 'STRING', 1731 | value: 'utf-8' 1732 | }], 1733 | null, 1734 | null, { 1735 | type: 'STRING', 1736 | value: 'quoted-printable' 1737 | }, { 1738 | type: 'ATOM', 1739 | value: '2128' 1740 | }, { 1741 | type: 'ATOM', 1742 | value: '54' 1743 | }, 1744 | null, 1745 | null, 1746 | null, 1747 | null 1748 | ], { 1749 | type: 'STRING', 1750 | value: 'alternative' 1751 | }, 1752 | [{ 1753 | type: 'STRING', 1754 | value: 'boundary' 1755 | }, { 1756 | type: 'STRING', 1757 | value: '=-dHujWM/Xizz57x/JOmDF' 1758 | }], 1759 | null, 1760 | null, 1761 | null 1762 | ], 1763 | [{ 1764 | type: 'STRING', 1765 | value: 'image' 1766 | }, { 1767 | type: 'STRING', 1768 | value: 'png' 1769 | }, 1770 | [{ 1771 | type: 'STRING', 1772 | value: 'name' 1773 | }, { 1774 | type: 'STRING', 1775 | value: 'smiley-3.png' 1776 | }], { 1777 | type: 'STRING', 1778 | value: '<1066971953.4232.15.camel@localhost>' 1779 | }, 1780 | null, { 1781 | type: 'STRING', 1782 | value: 'base64' 1783 | }, { 1784 | type: 'ATOM', 1785 | value: '1122' 1786 | }, 1787 | null, [{ 1788 | type: 'STRING', 1789 | value: 'attachment' 1790 | }, 1791 | [{ 1792 | type: 'STRING', 1793 | value: 'filename' 1794 | }, { 1795 | type: 'STRING', 1796 | value: 'smiley-3.png' 1797 | }] 1798 | ], 1799 | null, 1800 | null 1801 | ], { 1802 | type: 'STRING', 1803 | value: 'related' 1804 | }, 1805 | [{ 1806 | type: 'STRING', 1807 | value: 'type' 1808 | }, { 1809 | type: 'STRING', 1810 | value: 'multipart/alternative' 1811 | }, { 1812 | type: 'STRING', 1813 | value: 'boundary' 1814 | }, { 1815 | type: 'STRING', 1816 | value: '=-GpwozF9CQ7NdF+fd+vMG' 1817 | }], 1818 | null, 1819 | null, 1820 | null 1821 | ], 1822 | [{ 1823 | type: 'STRING', 1824 | value: 'image' 1825 | }, { 1826 | type: 'STRING', 1827 | value: 'gif' 1828 | }, 1829 | [{ 1830 | type: 'STRING', 1831 | value: 'name' 1832 | }, { 1833 | type: 'STRING', 1834 | value: 'dot.gif' 1835 | }], 1836 | null, 1837 | null, { 1838 | type: 'STRING', 1839 | value: 'base64' 1840 | }, { 1841 | type: 'ATOM', 1842 | value: '96' 1843 | }, 1844 | null, [{ 1845 | type: 'STRING', 1846 | value: 'attachment' 1847 | }, 1848 | [{ 1849 | type: 'STRING', 1850 | value: 'filename' 1851 | }, { 1852 | type: 'STRING', 1853 | value: 'dot.gif' 1854 | }] 1855 | ], 1856 | null, 1857 | null 1858 | ], { 1859 | type: 'STRING', 1860 | value: 'mixed' 1861 | }, 1862 | [{ 1863 | type: 'STRING', 1864 | value: 'boundary' 1865 | }, { 1866 | type: 'STRING', 1867 | value: '=-CgV5jm9HAY9VbUlAuneA' 1868 | }], 1869 | null, 1870 | null, 1871 | null 1872 | ], 1873 | [{ 1874 | type: 'STRING', 1875 | value: 'application' 1876 | }, { 1877 | type: 'STRING', 1878 | value: 'pgp-signature' 1879 | }, 1880 | [{ 1881 | type: 'STRING', 1882 | value: 'name' 1883 | }, { 1884 | type: 'STRING', 1885 | value: 'signature.asc' 1886 | }], 1887 | null, { 1888 | type: 'STRING', 1889 | value: 'This is a digitally signed message part' 1890 | }, { 1891 | type: 'STRING', 1892 | value: '7bit' 1893 | }, { 1894 | type: 'ATOM', 1895 | value: '196' 1896 | }, 1897 | null, 1898 | null, 1899 | null, 1900 | null 1901 | ], { 1902 | type: 'STRING', 1903 | value: 'signed' 1904 | }, 1905 | [{ 1906 | type: 'STRING', 1907 | value: 'micalg' 1908 | }, { 1909 | type: 'STRING', 1910 | value: 'pgp-sha1' 1911 | }, { 1912 | type: 'STRING', 1913 | value: 'protocol' 1914 | }, { 1915 | type: 'STRING', 1916 | value: 'application/pgp-signature' 1917 | }, { 1918 | type: 'STRING', 1919 | value: 'boundary' 1920 | }, { 1921 | type: 'STRING', 1922 | value: '=-vH3FQO9a8icUn1ROCoAi' 1923 | }], 1924 | null, 1925 | null, 1926 | null 1927 | ], { 1928 | type: 'ATOM', 1929 | value: '177' 1930 | }, 1931 | null, [{ 1932 | type: 'STRING', 1933 | value: 'inline' 1934 | }, 1935 | null 1936 | ], 1937 | null, 1938 | null 1939 | ], 1940 | [{ 1941 | type: 'STRING', 1942 | value: 'message' 1943 | }, { 1944 | type: 'STRING', 1945 | value: 'rfc822' 1946 | }, 1947 | null, 1948 | null, { 1949 | type: 'STRING', 1950 | value: 'Kittens give Morbo gas. --Morbo' 1951 | }, { 1952 | type: 'STRING', 1953 | value: '7bit' 1954 | }, { 1955 | type: 'ATOM', 1956 | value: '3088' 1957 | }, 1958 | [{ 1959 | type: 'STRING', 1960 | value: '23 Oct 2003 22:32:37 -0700' 1961 | }, { 1962 | type: 'STRING', 1963 | value: 'the PROPER way to do alternative/related' 1964 | }, 1965 | [ 1966 | [{ 1967 | type: 'STRING', 1968 | value: 'Ryan Finnie' 1969 | }, 1970 | null, { 1971 | type: 'STRING', 1972 | value: 'rfinnie' 1973 | }, { 1974 | type: 'STRING', 1975 | value: 'domain.dom' 1976 | } 1977 | ] 1978 | ], 1979 | [ 1980 | [{ 1981 | type: 'STRING', 1982 | value: 'Ryan Finnie' 1983 | }, 1984 | null, { 1985 | type: 'STRING', 1986 | value: 'rfinnie' 1987 | }, { 1988 | type: 'STRING', 1989 | value: 'domain.dom' 1990 | } 1991 | ] 1992 | ], 1993 | [ 1994 | [{ 1995 | type: 'STRING', 1996 | value: 'Ryan Finnie' 1997 | }, 1998 | null, { 1999 | type: 'STRING', 2000 | value: 'rfinnie' 2001 | }, { 2002 | type: 'STRING', 2003 | value: 'domain.dom' 2004 | } 2005 | ] 2006 | ], 2007 | [ 2008 | [null, 2009 | null, { 2010 | type: 'STRING', 2011 | value: 'bob' 2012 | }, { 2013 | type: 'STRING', 2014 | value: 'domain.dom' 2015 | } 2016 | ] 2017 | ], 2018 | null, 2019 | null, 2020 | null, { 2021 | type: 'STRING', 2022 | value: '<1066973557.4265.51.camel@localhost>' 2023 | } 2024 | ], 2025 | [ 2026 | [{ 2027 | type: 'STRING', 2028 | value: 'text' 2029 | }, { 2030 | type: 'STRING', 2031 | value: 'plain' 2032 | }, 2033 | [{ 2034 | type: 'STRING', 2035 | value: 'CHARSET' 2036 | }, { 2037 | type: 'STRING', 2038 | value: 'US-ASCII' 2039 | }], 2040 | null, 2041 | null, { 2042 | type: 'STRING', 2043 | value: '8bit' 2044 | }, { 2045 | type: 'ATOM', 2046 | value: '863' 2047 | }, { 2048 | type: 'ATOM', 2049 | value: '22' 2050 | }, 2051 | null, 2052 | null, 2053 | null, 2054 | null 2055 | ], 2056 | [ 2057 | [{ 2058 | type: 'STRING', 2059 | value: 'text' 2060 | }, { 2061 | type: 'STRING', 2062 | value: 'html' 2063 | }, 2064 | [{ 2065 | type: 'STRING', 2066 | value: 'CHARSET' 2067 | }, { 2068 | type: 'STRING', 2069 | value: 'utf-8' 2070 | }], 2071 | null, 2072 | null, { 2073 | type: 'STRING', 2074 | value: '8bit' 2075 | }, { 2076 | type: 'ATOM', 2077 | value: '1258' 2078 | }, { 2079 | type: 'ATOM', 2080 | value: '22' 2081 | }, 2082 | null, 2083 | null, 2084 | null, 2085 | null 2086 | ], 2087 | [{ 2088 | type: 'STRING', 2089 | value: 'image' 2090 | }, { 2091 | type: 'STRING', 2092 | value: 'gif' 2093 | }, 2094 | null, { 2095 | type: 'STRING', 2096 | value: '<1066973340.4232.46.camel@localhost>' 2097 | }, 2098 | null, { 2099 | type: 'STRING', 2100 | value: 'base64' 2101 | }, { 2102 | type: 'ATOM', 2103 | value: '116' 2104 | }, 2105 | null, 2106 | null, 2107 | null, 2108 | null 2109 | ], { 2110 | type: 'STRING', 2111 | value: 'related' 2112 | }, 2113 | [{ 2114 | type: 'STRING', 2115 | value: 'boundary' 2116 | }, { 2117 | type: 'STRING', 2118 | value: '=-bFkxH1S3HVGcxi+o/5jG' 2119 | }], 2120 | null, 2121 | null, 2122 | null 2123 | ], { 2124 | type: 'STRING', 2125 | value: 'alternative' 2126 | }, 2127 | [{ 2128 | type: 'STRING', 2129 | value: 'type' 2130 | }, { 2131 | type: 'STRING', 2132 | value: 'multipart/alternative' 2133 | }, { 2134 | type: 'STRING', 2135 | value: 'boundary' 2136 | }, { 2137 | type: 'STRING', 2138 | value: '=-tyGlQ9JvB5uvPWzozI+y' 2139 | }], 2140 | null, 2141 | null, 2142 | null 2143 | ], { 2144 | type: 'ATOM', 2145 | value: '79' 2146 | }, 2147 | null, [{ 2148 | type: 'STRING', 2149 | value: 'inline' 2150 | }, 2151 | null 2152 | ], 2153 | null, 2154 | null 2155 | ], { 2156 | type: 'STRING', 2157 | value: 'mixed' 2158 | }, 2159 | [{ 2160 | type: 'STRING', 2161 | value: 'boundary' 2162 | }, { 2163 | type: 'STRING', 2164 | value: '=-qYxqvD9rbH0PNeExagh1' 2165 | }], 2166 | null, 2167 | null, 2168 | null 2169 | ], { 2170 | type: 'ATOM', 2171 | value: 'BODY', 2172 | section: [] 2173 | }, { 2174 | type: 'LITERAL', 2175 | value: '' 2176 | } 2177 | ] 2178 | ] 2179 | } 2180 | }; 2181 | --------------------------------------------------------------------------------