From fb6fdbca826180e86717e73844844075f8c716af Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Thu, 2 Feb 2017 22:11:31 +0200 Subject: [PATCH] v2.0.0 --- .eslintrc | 3 + .gitignore | 3 +- .npmignore | 6 + .travis.yml | 6 +- Gruntfile.js | 22 + LICENSE | 314 +++++- README.md | 341 ++++--- bench/bench.sh | 6 + bench/parse.js | 61 ++ bench/passthrough.js | 38 + examples/nodemailer.eml | 616 ++++++++++++ examples/pipe.js | 69 ++ examples/simple.eml | 6 + examples/simple.js | 15 + index.js | 9 + lib/.npmignore | 4 + lib/datetime.js | 308 ------ lib/flowed-decoder.js | 48 + lib/mail-parser.js | 792 +++++++++++++++ lib/mailparser.js | 1513 ----------------------------- lib/simple-parser.js | 90 ++ lib/stream-hash.js | 28 + lib/streams.js | 213 ---- package.json | 55 +- test/{ => fixtures}/mixed.eml | 0 test/{ => fixtures}/nested.eml | 2 + test/fixtures/nodemailer.eml | 616 ++++++++++++ test/mail-parser-test.js | 1548 ++++++++++++++++++++++++++++++ test/mailparser.js | 1653 -------------------------------- test/simple-parser-test.js | 31 + 30 files changed, 4509 insertions(+), 3907 deletions(-) create mode 100644 .eslintrc create mode 100644 .npmignore create mode 100644 Gruntfile.js create mode 100755 bench/bench.sh create mode 100644 bench/parse.js create mode 100644 bench/passthrough.js create mode 100644 examples/nodemailer.eml create mode 100644 examples/pipe.js create mode 100644 examples/simple.eml create mode 100644 examples/simple.js create mode 100644 index.js create mode 100644 lib/.npmignore delete mode 100644 lib/datetime.js create mode 100644 lib/flowed-decoder.js create mode 100644 lib/mail-parser.js delete mode 100644 lib/mailparser.js create mode 100644 lib/simple-parser.js create mode 100644 lib/stream-hash.js delete mode 100644 lib/streams.js rename test/{ => fixtures}/mixed.eml (100%) rename test/{ => fixtures}/nested.eml (96%) create mode 100644 test/fixtures/nodemailer.eml create mode 100644 test/mail-parser-test.js delete mode 100644 test/mailparser.js create mode 100644 test/simple-parser-test.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..91926fc --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "nodemailer" +} diff --git a/.gitignore b/.gitignore index 8d4c2d1..812bd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .DS_Store -npm-debug.log \ No newline at end of file +npm-debug.log +.npmrc diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..897d479 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +bench +examples +test +.eslintrc +.travis.yml +Gruntfile.js diff --git a/.travis.yml b/.travis.yml index b34cf2c..e8b06f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js +sudo: false node_js: - - "0.10" - - 0.12 - - iojs + - 6 + - 7 notifications: email: recipients: diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..47abb19 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = function (grunt) { + + // Project configuration. + grunt.initConfig({ + eslint: { + all: ['lib/**/*.js', 'test/**/*.js', 'examples/**/*.js', 'Gruntfile.js'] + }, + + nodeunit: { + all: ['test/**/*-test.js'] + } + }); + + // Load the plugin(s) + grunt.loadNpmTasks('grunt-eslint'); + grunt.loadNpmTasks('grunt-contrib-nodeunit'); + + // Tasks + grunt.registerTask('default', ['eslint', 'nodeunit']); +}; diff --git a/LICENSE b/LICENSE index 74e3248..7391bc5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,16 +1,298 @@ -Copyright (c) 2012-2015 Andris Reinman - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +Copyright (c) 2012-2017 Andris Reinman + +European Union Public Licence + V. 1.1 + +EUPL (c) the European Community 2007 + + +This European Union Public Licence (the "EUPL") applies to the Work or Software +(as defined below) which is provided under the terms of this Licence. Any use +of the Work, other than as authorised under this Licence is prohibited (to the +extent such use is covered by a right of the copyright holder of the Work). + +The Original Work is provided under the terms of this Licence when the Licensor +(as defined below) has placed the following notice immediately following the +copyright notice for the Original Work: + +Licensed under the EUPL V.1.1 + +or has expressed by any other mean his willingness to license under the EUPL. + + +1. Definitions + +In this Licence, the following terms have the following meaning: + +* The Licence: this Licence. + +* The Original Work or the Software: the software distributed and/or +communicated by the Licensor under this Licence, available as Source Code +and also as Executable Code as the case may be. + +* Derivative Works: the works or software that could be created by the +Licensee, based upon the Original Work or modifications thereof. This +Licence does not define the extent of modification or dependence on the +Original Work required in order to classify a work as a Derivative Work; +this extent is determined by copyright law applicable in the country +mentioned in Article 15. + +* The Work: the Original Work and/or its Derivative Works. + +* The Source Code: the human-readable form of the Work which is the most +convenient for people to study and modify. + +* The Executable Code: any code which has generally been compiled and which is +meant to be interpreted by a computer as a program. + +* The Licensor: the natural or legal person that distributes and/or +communicates the Work under the Licence. + +* Contributor(s): any natural or legal person who modifies the Work under the +Licence, or otherwise contributes to the creation of a Derivative Work. + +* The Licensee or "You": any natural or legal person who makes any usage of +the Software under the terms of the Licence. + +* Distribution and/or Communication: any act of selling, giving, lending, +renting, distributing, communicating, transmitting, or otherwise making +available, on-line or off-line, copies of the Work or providing access to +its essential functionalities at the disposal of any other natural or legal +person. + + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a world-wide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +* use the Work in any circumstance and for all usage, +* reproduce the Work, +* modify the Original Work, and make Derivative Works based upon the Work, +* communicate to the public, including the right to make available or display +the Work or copies thereof to the public and perform publicly, as the case +may be, the Work, +* distribute the Work or copies thereof, +* lend and rent the Work or copies thereof, +* sub-license rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make +effective the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository +where the Source Code is easily and freely accessible for as long as the +Licensor continues to distribute and/or communicate the Work. + + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits +from any exception or limitation to the exclusive rights of the rights owners +in the Original Work or Software, of the exhaustion of those rights or of other +applicable limitations thereto. + + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +- Attribution right: the Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices +and a copy of the Licence with every copy of the Work he/she distributes +and/or communicates. The Licensee must cause any Derivative Work to carry +prominent notices stating that the Work has been modified and the date of +modification. + +- Copyleft clause: If the Licensee distributes and/or communicates copies of +the Original Works or Derivative Works based upon the Original Work, this +Distribution and/or Communication will be done under the terms of this +Licence or of a later version of this Licence unless the Original Work is +expressly distributed only under this version of the Licence. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or +conditions on the Work or Derivative Work that alter or restrict the terms +of the Licence. + +- Compatibility clause: If the Licensee Distributes and/or Communicates +Derivative Works or copies thereof based upon both the Original Work and +another work licensed under a Compatible Licence, this Distribution and/or +Communication can be done under the terms of this Compatible Licence. For +the sake of this clause, "Compatible Licence" refers to the licences listed +in the appendix attached to this Licence. Should the Licensee's obligations +under the Compatible Licence conflict with his/her obligations under this +Licence, the obligations of the Compatible Licence shall prevail. + +- Provision of Source Code: When distributing and/or communicating copies of +the Work, the Licensee will provide a machine-readable copy of the Source +Code or indicate a repository where this Source will be easily and freely +available for as long as the Licensee continues to distribute and/or +communicate the Work. Legal Protection: This Licence does not grant +permission to use the trade names, trademarks, service marks, or names of +the Licensor, except as required for reasonable and customary use in +describing the origin of the Work and reproducing the content of the +copyright notice. + + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +contributors. It is not a finished work and may therefore contain defects or +"bugs" inherent to this type of software development. + +For the above reason, the Work is provided under the Licence on an "as is" +basis and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of +defects or errors, accuracy, non-infringement of intellectual property rights +other than copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the +use of the Work, including without limitation, damages for loss of goodwill, +work stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such +damage. However, the Licensor will be liable under statutory product liability +laws as far such laws apply to the Work. + + +9. Additional agreements + +While distributing the Original Work or Derivative Works, You may choose to +conclude an additional agreement to offer, and charge a fee for, acceptance of +support, warranty, indemnity, or other liability obligations and/or services +consistent with this Licence. However, in accepting such obligations, You may +act only on your own behalf and on your sole responsibility, not on behalf of +the original Licensor or any other Contributor, and only if You agree to +indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against such Contributor by the fact You have +accepted any such warranty or additional liability. + + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon "I agree" +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this +Licence, such as the use of the Work, the creation by You of a Derivative Work +or the Distribution and/or Communication by You of the Work or copies thereof. + + +11. Information to the public + +In case of any Distribution and/or Communication of the Work by means of +electronic communication by You (for example, by offering to download the Work +from a remote location) the distribution channel or media (for example, a +website) must at least provide to the public the information requested by the +applicable law regarding the Licensor, the Licence and the way it may be +accessible, concluded, stored and reproduced by the Licensee. + + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work licensed hereunder. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed and/or reformed so as necessary to make +it valid and enforceable. + +The European Commission may publish other linguistic versions and/or new +versions of this Licence, so far this is required and reasonable, without +reducing the scope of the rights granted by the Licence. New versions of the +Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + + +14. Jurisdiction + +Any litigation resulting from the interpretation of this License, arising +between the European Commission, as a Licensor, and any Licensee, will be +subject to the jurisdiction of the Court of Justice of the European +Communities, as laid down in article 238 of the Treaty establishing the +European Community. + +Any litigation arising between Parties, other than the European Commission, and +resulting from the interpretation of this License, will be subject to the +exclusive jurisdiction of the competent court where the Licensor resides or +conducts its primary business. + + +15. Applicable Law + +This Licence shall be governed by the law of the European Union country where +the Licensor resides or has his registered office. + +This licence shall be governed by the Belgian law if: + +* a litigation arises between the European Commission, as a Licensor, and any +Licensee; +* the Licensor, other than the European Commission, has no residence or +registered office inside a European Union country. + + +Appendix + +"Compatible Licences" according to article 5 EUPL are: + +* GNU General Public License (GNU GPL) v. 2 +* Open Software License (OSL) v. 2.1, v. 3.0 +* Common Public License v. 1.0 +* Eclipse Public License v. 1.0 +* Cecill v. 2.0 diff --git a/README.md b/README.md index 37d3676..fb8fd68 100644 --- a/README.md +++ b/README.md @@ -1,232 +1,221 @@ -MailParser -========== +# MailParser -[![Build Status](https://api.travis-ci.org/andris9/mailparser.svg)](http://travis-ci.org/andris9/mailparser) -[![NPM version](https://badge.fury.io/js/mailparser.svg)](http://badge.fury.io/js/mailparser) +[![Build Status](https://api.travis-ci.org/andris9/mailparser.svg)](http://travis-ci.org/andris9/mailparser) [![NPM version](https://badge.fury.io/js/mailparser.svg)](http://badge.fury.io/js/mailparser) -**MailParser** is an asynchronous and non-blocking parser for -[node.js](http://nodejs.org) to parse mime encoded e-mail messages. -Handles even large attachments with ease - attachments can be parsed -in chunks and streamed if needed. +Advanced email parser for Node.js. Everything is handled as a stream which should make it able to parse even very large messages (100MB+) with relatively low overhead. -### Community version +The module exposes two separate modes, a lower level `MailParser` class and `simpleParser` function. The latter is simpler to use (hence the name) but is less resource efficient as it buffers attachment contents in memory. -This module is unmaintained. For an upgrade see [Mailparser2](https://gitlab.com/nodemailer/mailparser2). The upgraded version is more effective, has vastly better stream handling through Streams3 interface and is able to build up more accurate versions of HTML and Text contents. +For older version of MailParser see the docs [here](https://github.com/andris9/mailparser/blob/b8488c5b6a73920b58bcd2f62a5c3be115940fe4/README.md) -### Usage +> Starting from v2.0.0 MailParser is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html). In general, EUPLv1.1 is a _copyleft_ license compatible with GPLv2, so if you're OK using GPL then you should be OK using MailParser. Previous versions of MailParser are licensed under the MIT license. -**MailParser** parses raw source of e-mail messages into a structured object. +## Install -No need to worry about charsets or decoding *quoted-printable* or -*base64* data, **MailParser** does all of it for you. All the textual output -from **MailParser** (subject line, addressee names, message body) is always UTF-8. +``` +npm install mailparser +``` -For a 25MB e-mail it takes less than a second to parse if attachments are not streamed but buffered and about 3-4 seconds if they are streamed. Expect high RAM usage though if you do not stream the attachments. +## simpleParser -If you want to send e-mail instead of parsing it, check out my other module [Nodemailer](https://github.com/andris9/Nodemailer). +`simpleParser` is the easiest way to parse emails. You only need to provide a message source to get a parsed email structure in return. As an additional bonus all embedded images in HTML (eg. the images that point to attachments using cid: URIs) are replaced with base64 encoded data URIs, so the message can be displayed without any additional processing. Be aware though that this module does not do any security cleansing (eg. removing javascript and so on), this is left to your own application. -## ICONV NOTICE +```javascript +const simpleParser = require('mailparser').simpleParser; +simpleParser(source, (err, mail)=>{}) +``` -Since v0.4 `node-iconv` is not included by default as a dependency. If you need to support encodings not covered by `iconv-lite` you should add `iconv` as a dependency to your own project so `mailparser` could pick it up. +or as a Promise: -## Support mailparser development +```javascript +simpleParser(source).then(mail=>{}).catch(err=>{}) +``` -[![Donate to author](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DB26KWR2BQX5W) +Where -Installation ------------- +- **source** is either a stream, a Buffer or a string that needs to be parsed +- **err** is the possible error object +- **mail** is a structured email object - npm install mailparser +### mail object -Usage ------ +Parsed `mail` object has the following properties -Require MailParser module -```javascript -var MailParser = require("mailparser").MailParser; -``` -Create a new MailParser object -```javascript -var mailparser = new MailParser([options]); -``` -Options parameter is an object with the following properties: +- **headers** – a Map object with lowercase header keys +- **subject** is the subject line (also available from the header `mail.headers.get('subject')`) +- **from** is an address object for the From: header +- **to** is an address object for the To: header +- **cc** is an address object for the Cc: header +- **bcc** is an address object for the Bcc: header (usually not present) +- **date** is a Date object for the Date: header +- **messageId** is the Message-ID value string +- **inReplyTo** is the In-Reply-To value string +- **reply-to** is an address object for the Cc: header +- **references** is an array of referenced Message-ID values +- **html** is the HTML body of the message. If the message included embedded images as cid: urls then these are all replaced with base64 formatted data: URIs +- **text** is the plaintext body of the message +- **textAsHtml** is the plaintext body of the message formatted as HTML +- **attachments** is an array of attachments + +### address object + +Address objects have the following structure: + +- **value** an array with address details + + - **name** is the name part of the email/group + - **address** is the email address + - **group** is an array of grouped addresses - * **debug** - if set to true print all incoming lines to console - * **streamAttachments** - if set to true, stream attachments instead of including them - * **unescapeSMTP** - if set to true replace double dots in the beginning of the file - * **defaultCharset** - the default charset for *text/plain* and *text/html* content, if not set reverts to *Latin-1* - * **showAttachmentLinks** - if set to true, show inlined attachment links `filename` +- **text** is a formatted address string for plaintext context -MailParser object is a writable Stream - you can pipe directly -files to it or you can send chunks with `mailparser.write` +- **html** is a formatted address string for HTML context -When the headers have received, "headers" is emitted. The headers have not been pre-processed (except that mime words have been converted to UTF-8 text). +**Example** ```javascript -mailparser.on("headers", function(headers){ - console.log(headers.received); -}); +{ + value: [ + { + address: 'andris+123@kreata.ee', + name: 'Andris Reinman' + }, + { + address: 'andris.reinman@gmail.com', + name: '' + } + ], + html: 'Andris Reinman <andris+123@kreata.ee>, andris.reinman@gmail.com', + text: 'Andris Reinman , andris.reinman@gmail.com' +} ``` -When the parsing ends an `'end'` event is emitted which has an -object with parsed e-mail structure as a parameter. + +### headers Map + +`headers` is a Map with lowercase header keys. So if you want to check for the Subject: header then you can do it like this: ```javascript -mailparser.on("end", function(mail){ - mail; // object structure for parsed e-mail -}); +if (mail.headers.has('subject')) { + console.log(mail.headers.get('subject')); +} ``` -### Parsed mail object - - * **headers** - unprocessed headers in the form of - `{key: value}` - if there were multiple fields with the same key then the value is an array - * **from** - an array of parsed `From` addresses - `[{address:'sender@example.com',name:'Sender Name'}]` (should be only one though) - * **to** - an array of parsed `To` addresses - * **cc** - an array of parsed `Cc` addresses - * **bcc** - an array of parsed 'Bcc' addresses - * **subject** - the subject line - * **references** - an array of reference message id values (not set if no reference values present) - * **inReplyTo** - an array of In-Reply-To message id values (not set if no in-reply-to values present) - * **priority** - priority of the e-mail, always one of the following: *normal* (default), *high*, *low* - * **text** - text body - * **html** - html body - * **date** - date field as a `Date()` object. If date could not be resolved or is not found this field is not set. Check the original date string from `headers.date` - * **attachments** - an array of attachments - -### Decode a simple e-mail - -This example decodes an e-mail from a string -```javascript -var MailParser = require("mailparser").MailParser; -var mailparser = new MailParser(); - -var email = "From: 'Sender Name' \r\n"+ - "To: 'Receiver Name' \r\n"+ - "Subject: Hello world!\r\n"+ - "\r\n"+ - "How are you today?"; - -// setup an event listener when the parsing finishes -mailparser.on("end", function(mail_object){ - console.log("From:", mail_object.from); //[{address:'sender@example.com',name:'Sender Name'}] - console.log("Subject:", mail_object.subject); // Hello world! - console.log("Text body:", mail_object.text); // How are you today? -}); +The format of a header depends on the specific key. For most header keys the value is either a string (a single header) or an array of strings (multiple headers with the same key were found). -// send the email source to the parser -mailparser.write(email); -mailparser.end(); -``` -### Pipe file to MailParser +Special header keys are the following: -This example pipes a `readableStream` file to **MailParser** -```javascript -var MailParser = require("mailparser").MailParser; -var mailparser = new MailParser(); -var fs = require("fs"); +1. All address headers are converted into address objects -mailparser.on("end", function(mail_object){ - console.log("Subject:", mail_object.subject); -}); + - **from** + - **to** + - **cc** + - **bcc** + - **sender** + - **reply-to** + - **delivered-to** + - **return-path** -fs.createReadStream("email.eml").pipe(mailparser); -``` -### Attachments +2. All different priority headers are converted into **priority** with the following values: -By default any attachment found from the e-mail will be included fully in the -final mail structure object as Buffer objects. With large files this might not -be desirable so optionally it is possible to redirect the attachments to a Stream -and keep only the metadata about the file in the mail structure. + - **'high'** + - **'normal'** + - **'low'** -```javascript -mailparser.on("end", function(mail_object){ - mail_object.attachments.forEach(function(attachment){ - console.log(attachment.fileName); - }); -}); -``` -#### Default behavior +3. **references** is a string if only a single reference-id exists or an array if multiple ids exist -By default attachments will be included in the attachment objects as Buffers. -```javascript -attachments = [{ - contentType: 'image/png', - fileName: 'image.png', - contentDisposition: 'attachment', - contentId: '5.1321281380971@localhost', - transferEncoding: 'base64', - length: 126, - generatedFileName: 'image.png', - checksum: 'e4cef4c6e26037bcf8166905207ea09b', - content: -}]; -``` -The property `generatedFileName` is usually the same as `fileName` but if several -different attachments with the same name exist or there is no `fileName` set, an -unique name is generated. +4. **date** value is a Date object +5. The following headers are parsed into structured objects, where `value` property includes the main value as string and `params` property holds an object of additional arguments as key-value pairs + + - **content-type** + - **content-disposition** + - **dkim-signature** + +Some headers are also automaticaly mime-word decoded + +- all address headers (name parts and punycode encoded domains are converted to unicode) +- **subject** is converted to unicode + +### attachment object -Property `content` is always a Buffer object (or SlowBuffer on some occasions) +Attachment objects have the following structure: -#### Attachment streaming +- **filename** (if available) file name of the attachment +- **contentType** MIME type of the message +- **contentDisposition** content disposition type for the attachment, most probably "attachment" +- **checksum** a MD5 hash of the message content +- **size** message size in bytes +- **headers** a Map value that holds MIME headers for the attachment node +- **content** a Buffer that contains the attachment contents +- **contentId** the header value from 'Content-ID' (if present) +- **cid** contentId without < and > +- **related** if true then this attachment should not be offered for download (at least not in the main attachments list) + +## MailParser + +`MailParser` is a lower-level email parsing class. It is a transform stream that takes email source as bytestream for the input and emits data objects for attachments and text contents. -Attachment streaming can be used when providing an optional options parameter -to the `MailParser` constructor. ```javascript -var mp = new MailParser({ - streamAttachments: true -} +const MailParser = require('mailparser').MailParser; +let parser = new MailParser() ``` -This way there will be no `content` property on final attachment objects -(but the other fields will remain). -To catch the streams you should listen for `attachment` events on the MailParser -object. The parameter provided includes file information (`contentType`, -`fileName`, `contentId`) and a readable Stream object `stream`. -```javascript -var mp = new MailParser({ - streamAttachments: true -} +### Event 'headers' + +The parser emits 'headers' once message headers have been processed. The headers object is a Map. Different header keys have different kind of values, for example address headers have the address object/array as the value while subject value is string. + +Header keys in the Map are lowercase. -mp.on("attachment", function(attachment, mail){ - var output = fs.createWriteStream(attachment.generatedFileName); - attachment.stream.pipe(output); +```javascript +parser.on('headers', headers = { + console.log(headers.get('subject')); }); ``` -`generatedFileName` is unique for the parsed mail - if several attachments with -the same name exist, `generatedFileName` is updated accordingly. Also there -might not be `fileName` parameter at all, so it is better to rely on -`generatedFileName`. -#### Testing attachment integrity +### Event 'data' -Attachment objects include `length` property which is the length of the attachment -in bytes and `checksum` property which is a `md5` hash of the file. +Event 'data' or 'readable' emits message content objects. The type of the object can be determine by the `type` property. Currently there are two kind of data objects -### Running tests +- 'attachment' indicates that this object is an attachment +- 'text' indicates that this object includes the html and text parts of the message. This object is emitted once and it includes both values -Install **MailParser** with dev dependencies -```bash -npm install --dev mailparser -``` -And then run -```bash -npm test mailparser +### attachment object + +Attachment object is the same as in `simpleParser` except that `content` is not a buffer but a stream. Additionally there's a method `release()` that must be called once you have processed the attachment. The property `related` is set after message processing is ended, so at the `data` event this value is not yet available. + +```javascript +parser.on('data', data => { + if(data.type === 'attachment'){ + console.log(data.filename); + data.content.pipe(process.stdout); + data.on('end', ()=>data.release()); + } +}); ``` -There aren't many tests yet but basics should be covered. -## Issues +If you do not call `release()` then the message processing is paused. -**S/MIME** +### text object -Currently it is not possible to verify signed content as the incoming text is -split to lines when parsing and line ending characters are not preserved. One -can assume it is always \r\n but this might not be always the case. +Text object has the following keys: -**Seeking** +- **text** includes the plaintext version of the message. Is set if the message has at least one 'text/plain' node +- **html** includes the HTML version of the message. Is set if the message has at least one 'text/html' node +- **textAsHtml** includes the plaintext version of the message in HTML format. Is set if the message has at least one 'text/plain' node. -Due to the line based parsing it is also not possible to explicitly state -the beginning and ending bytes of the attachments for later source seeking. -Node.js doesn't support the concept of seeking very well anyway. +```javascript +parser.on('data', data => { + if(data.type === 'text'){ + console.log(data.html); + } +}); +``` + +## Issues + +Charset decoding is handled using [iconv-lite](https://github.com/ashtuchkin/iconv-lite) that is missing some charsets, especially some Japanese ones. If required then it would be possible to switch to native iconv bindings with [node-iconv](https://github.com/bnoordhuis/node-iconv) to handle these missing charsets but for now this option is not used for easier packaging. ## License -**MIT** +European Union Public License 1.1\. Commercial licenses available upon request. Contact [cales@nodemailer.com](mailto:sales@nodemailer.com) for details. + +© 2017 Kreata OÜ diff --git a/bench/bench.sh b/bench/bench.sh new file mode 100755 index 0000000..44d4a57 --- /dev/null +++ b/bench/bench.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +MESSAGES=20000 + +MESSAGES=$MESSAGES node passthrough.js +MESSAGES=$MESSAGES node parse.js diff --git a/bench/parse.js b/bench/parse.js new file mode 100644 index 0000000..e52aed6 --- /dev/null +++ b/bench/parse.js @@ -0,0 +1,61 @@ +'use strict'; + +const MailParser = require('../index.js').MailParser; +const randomMessage = require('random-message'); +const messages = Number(process.env.MESSAGES) || 10000; + +const Transform = require('stream').Transform; + +const messagesRoot = '/Users/andris/Projects/nodemailer/Gmail/Messages'; +let processed = 0; +let startTime = Date.now(); +let bytes = 0; + +class Counter extends Transform { + constructor() { + super(); + this.bytes = 0; + } + _transform(chunk, encoding, done) { + this.bytes += chunk.length; + done(null, chunk); + } + _flush(done) { + bytes += this.bytes; + done(); + } +} + +let processNext = () => { + if (++processed >= messages) { + let time = (Date.now() - startTime) / 1000; + let avg = Math.round(processed / time); + console.log('Done. %s messages [%s MB] processed in %s s. with average of %s messages/sec [%s MB/s]', processed, Math.round(bytes / (1024 * 1024)), time, avg, Math.round((bytes / (1024 * 1024)) / time)); // eslint-disable-line no-console + return; + } + + let parser = new MailParser(); + parser.on('data', data => { + if (data.type === 'attachment') { + data.content.on('data', () => false); + data.content.on('end', () => data.release()); + } + }); + + parser.on('end', () => { + parser = false; + + setImmediate(processNext); + }); + + parser.on('error', err => { + console.log(err); // eslint-disable-line no-console + }); + + //randomMessage.get(messagesRoot, (processed * 0x10000).toString(16)).pipe(require('fs').createWriteStream('test.eml')); + + randomMessage.get(messagesRoot, (processed * 0x10000).toString(16)).pipe(new Counter()).pipe(parser); +}; + +console.log('Streaming %s random messages through MailParser', messages); // eslint-disable-line no-console +processNext(); diff --git a/bench/passthrough.js b/bench/passthrough.js new file mode 100644 index 0000000..3029514 --- /dev/null +++ b/bench/passthrough.js @@ -0,0 +1,38 @@ +'use strict'; + +const PassThrough = require('stream').PassThrough; +const randomMessage = require('random-message'); +const messages = Number(process.env.MESSAGES) || 10000; + +const messagesRoot = '/Users/andris/Projects/nodemailer/Gmail/Messages'; +let processed = 0; +let startTime = Date.now(); +let bytes = 0; + +let processNext = () => { + if (++processed >= messages) { + let time = (Date.now() - startTime) / 1000; + let avg = Math.round(processed / time); + console.log('Done. %s messages [%s MB] processed in %s s. with average of %s messages/sec [%s MB/s]', processed, Math.round(bytes / (1024 * 1024)), time, avg, Math.round((bytes / (1024 * 1024)) / time)); // eslint-disable-line no-console + return; + } + + let stream = new PassThrough(); + + stream.on('readable', () => { + let chunk; + while ((chunk = stream.read()) !== null) { + bytes += chunk.length; + } + }); + + stream.on('end', () => { + stream = false; + setImmediate(processNext); + }); + + randomMessage.get(messagesRoot, (processed * 0x10000).toString(16)).pipe(stream); +}; + +console.log('Streaming %s random messages through a plain PassThrough', messages); // eslint-disable-line no-console +processNext(); diff --git a/examples/nodemailer.eml b/examples/nodemailer.eml new file mode 100644 index 0000000..6bf5094 --- /dev/null +++ b/examples/nodemailer.eml @@ -0,0 +1,616 @@ +Delivered-To: andris.reinman@gmail.com +Received: by 10.28.50.2 with SMTP id y2csp233403wmy; + Thu, 13 Oct 2016 04:39:49 -0700 (PDT) +X-Received: by 10.25.37.18 with SMTP id l18mr9511740lfl.88.1476358789184; + Thu, 13 Oct 2016 04:39:49 -0700 (PDT) +Return-Path: +Received: from srs1.zonevs.eu (srs1.zonevs.eu. [217.146.68.191]) + by mx.google.com with ESMTPS id l202si1012799lfg.293.2016.10.13.04.39.49 + for + (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); + Thu, 13 Oct 2016 04:39:49 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of srs0=63fc=v7=kreata.ee=andris@srs1.zonevs.eu designates 217.146.68.191 as permitted sender) client-ip=217.146.68.191; +Authentication-Results: mx.google.com; + dkim=pass header.i=@srs1.zonevs.eu; + spf=pass (google.com: best guess record for domain of srs0=63fc=v7=kreata.ee=andris@srs1.zonevs.eu designates 217.146.68.191 as permitted sender) smtp.mailfrom=SRS0=63fc=V7=kreata.ee=andris@srs1.zonevs.eu; + dmarc=fail (p=NONE dis=NONE) header.from=kreata.ee +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=srs1.zonevs.eu; + q=dns/txt; s=oct2016; bh=xKHKChGY0vTH8NsecmXwA0OqbinKOXeQbaC2UYp2BAM=; + h=from:subject:date:message-id:to:mime-version:content-type; + b=Ve18ogdCAG+7WZYkJPOewe1hKjhN4k9unz7bVHMXd6+1CQDRUkLCArQZJzSKxkM481nzXfjFn + bI8qOuQL8mRk/8fAjYhxLgnr/3SyVIOhCnXxjdQkRzgouZyl42hqD0gIaCxu9uodtQrp2pbKvyl + e+3sG+LhcdJmsPguOfILn14j+irinPSWrospC8PBIDTsUwO8DCyPqSlOADbW0B6TRUHWMf4XUX4 + W8TH61H1ZI3Xu3k0bvX7rsGHZjsy8dcshcnfYENLCLep8fsQMaB15EErc3RXycBX7CBd0iU1l50 + pYpUFd6bZehCF0ipTOgA7IJ7ZPafaH0YTU8wRntXOwbg== +Received: from host29.guest.zone.eu [217.146.66.6] + by srs1.zonevs.eu (ZoneMTA Forwarder) with ESMTP id 157bdd754f70005750.002 + for ; + Thu, 13 Oct 2016 11:39:48 +0000 +Content-Type: multipart/mixed; + boundary="----sinikael-?=_1-14763587882000.8241290969717285" +X-Laziness-Level: 1000 +From: Andris Kreata +To: Andris Reinman , andris.reinman@gmail.com +Subject: Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?= + (1476358788189) +Message-ID: <012d606e-3550-2d94-b566-6cd996de88e3@kreata.ee> +X-Mailer: nodemailer (2.6.0; +http://nodemailer.com/; + SMTP/2.7.2[client:2.12.0]) +Date: Thu, 13 Oct 2016 11:39:48 +0000 +MIME-Version: 1.0 +X-Zone-Spam-Resolution: no action +X-Zone-Spam-Status: No, score=0.408099, required=15, tests=[MIME_GOOD=-0.1, + R_MISSING_CHARSET=2.5, DMARC_POLICY_SOFTFAIL=0.1, MIME_UNKNOWN=0.1, + R_DKIM_NA=0, BAYES_HAM=-2.1919] +X-Original-Sender: andris@kreata.ee +X-Zone-Forwarded-For: andris@kreata.ee +X-Zone-Forwarded-To: andris.reinman@gmail.com + +------sinikael-?=_1-14763587882000.8241290969717285 +Content-Type: multipart/alternative; + boundary="----sinikael-?=_2-14763587882000.8241290969717285" + +------sinikael-?=_2-14763587882000.8241290969717285 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Hello to myself! +------sinikael-?=_2-14763587882000.8241290969717285 +Content-Type: text/watch-html +Content-Transfer-Encoding: 7bit + +Hello to myself +------sinikael-?=_2-14763587882000.8241290969717285 +Content-Type: multipart/related; type="text/html"; + boundary="----sinikael-?=_5-14763587882000.8241290969717285" + +------sinikael-?=_5-14763587882000.8241290969717285 +Content-Type: text/html +Content-Transfer-Encoding: quoted-printable + +

Hello to myself

Here's = +a nyan cat for you as an embedded attachment:

+------sinikael-?=_5-14763587882000.8241290969717285 +Content-Type: image/png; name=image.png +Content-ID: +Content-Disposition: attachment; filename=image.png +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE +QVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ +AAAAAElFTkSuQmCC +------sinikael-?=_5-14763587882000.8241290969717285 +Content-Type: image/gif; name="nyan cat =?UTF-8?Q?=E2=9C=94=2Egif?=" +Content-ID: +Content-Disposition: attachment; + filename*0*=utf-8''nyan%20cat%20%E2%9C%94.gif +Content-Transfer-Encoding: base64 + +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 +1Yyw8Iy+SAjcCE3euFPgyFXroI2Pd3XgdI5pl45nNWCwaFPuqE3w2DnXmAl0RRnRqHtC6F8C+GAx +I45Sp1ZOU1zrF1+7NHNQZJCzhpD1CFqdyAAMqUsOiQn7GDf9+FMKWZGclC/692rARjLyOEEdqVQf +KYtWKJBatWJJE3UZlZJpIl34UH0iOXswCQwbSTY0uSU2SX1j9QU9SJAxCZFp0n7QVRlBaXWr/6gu +GXkJPakkSjlc49ha8LNpINlbO9mLm1CVt9VZMhkLTfl+8JeLIteVXsmP52dJgCWWSKkJZYlokgSM +WKeWazl339d7YWkjJykKcxlk9yhzeJmX7eZziNcEW0eMn4WVveZrW5lcRmmYAtGWyccEi0mPthCY +lil2aCl0k0mZfNeZWgkFU4kJQblbT3mXoSmahUeaiQkArJULZllsdilGremaBwSbJDYFs7kGxFd8 +n9lthSmarFeX3kBcFImYdjec0RKV7HacjeWbyumY0kl+zslKxUmZ13l91MkRqUldq4mbJKk6ujkJ +3YmddVOdtEmXfBmZipebrpme05cp+zWYcf/ikghYnjr1jYzZV3v5Xe3FXtlZAheZAtAJImO5WLyJ +GfrlXuOJYTkJPNvZNwsqQgE6IQ9KoLcpodcFZL9mnv6pmSDGcxyKKvcZoaugn/jHn1DFjP/ZXRl6 +HRuaX/ipYB+aZZCGb+gYoww6o8tRo4NTjtiVoxQqn/oYl2wJpM8FXifKkkhwoCiQoAdyoZnFpIf3 +RUTKT4KpomDook93nruJpVm6QFvKf7XJdFQaYfRZprBzoyrglCG3pszVpm7qoHCKoGmap05VoXlp +p3eqJ3x6AnLaoVsIplonpoAaqKTipQDonq1nqF8aov2pm4vKqG0njSsKqd6ZKtVYkNvgHMH/ialo +VKBDxanqiSqfepShCma1SaoiZqqFqqrrhmM+amSoCqsjJqt7amO1SmS3OmWXqqt4wqu5mm2e+qvf +oKTfMKrECkaSSoDH+mQ8qhXBaiev+qylGq3016vDo6zYwKzY4KwIkWnMqT11dqxnmYyP2qXfWq1u +ca0zQa4HYa4bpzz2aqKAtmzeR6a3hqimdVqj5q90lq7DGhX52nGZyq/zdK4CCrDahVkD67CIkbAJ +d2AWm3ErJ5TWRbE06qeiRj3ZChIZq3JoVLI6t7FEoK4bywSrOqIlQa8GgbIqgbEG26DTya5shrM+ +RqkvqlgTe6+KQbMURrQLe5MNK7QP67Nh/wq0IsuyJHuzBAs4Rquy/eqxQQqyEBW0+uom2vamuJZs ++4q0kJSVvcm0iQqX8tp1I4t+3SCek/e2R8ux8GG24Adq4EoJpwkiMpuqXwC3m9dsf8ewZfuYtrmj +JemXVjpsbSuc5AC4TGq4kAmQdSu5aoakGvmXbAu1bvu31xa5Yju4ZFu5oXu3HpK3k7C3B9K36yq3 +/xq4X5uYzBa7roa2AYuN62hArIusrntsqFa6stsI8VGapwuvmbu4RzdCB6etYPFxpqpKluuWmCuV +mmsyN9dCzNu8MAellXJviUt410tC2fsVzsut0XcoL4tZ4btB4xsV5QufMuK9IsqI63tB7f+7FO/L +vYkiv5UKvsr7bverFPmrs/YWQahbp//LbwHsEwOsqf5nwMabb/XbQNPXcgrbsik7EQ1MuawJsfQV +MqqLngl8cZc7tY7btcSLwi8HdOYbcFp7HyE8pj7ycRU8txastCy0vQRMdtNbIzE8wQlUwxt7wyqs +I+b2vEJBp1FWvSY3wihXwlhba1F8uFi7wcLXwbYbsRnzw06Mc1CMw7Y1xfVZsxRhxcrHlT2soExs +c12MvV9cxKarsWMrxxJhxoQLlS9cHjEMCm06hBhMt6hEiPOrCXvsqiYcm5D3xzeTj4S8xhjTxz+o +yIhFjQdcyLlzyGf7e5IceB4MimdHokb/xLNhbHybbFmUHMGWYMnHI8qTe3uljEmn/L0PubbWy8pS +TMpzfMeB3MmniLutOm22TMWunMujS5iMB1KfnLswuwy7S6xnmi15Byia2My6+szYEs1zMs2Nu8DW +fC3YfCbazLkLzLv6i3dlt0WTSM2w2s3W8s24EM4HW6bs7MLHrHfpvM0BPM98ErdfYIh1KIbfVDT8 +7AX+7HgAHdCLDLuUV9AJddBSKi8D3QUM3dAOPaGTDLqiMdF7WNFGetFTXM8aHYgc7YXyp9DdENIo +XU9I7NFg7LcEndIw/TArjcWYPMojfdMVpM/jsrs43dOjotPiwtM+PdQZCtQ83J1EndRw/+zSDsxL +Qq3UUE3GntnC8PLUUZ3Ul1nMb4XPyHnVPZ3VgLzVudp8LDvTRyrGb8zBUSjKRt10ZknWYNnU5hDX +cj2pNc3Uau3Ub7192WrWrnTXkbrDp1pbbe1Be4196ge/rIbW0huZ2ueob3fYzJPY5bwLdJ3Xh8rW +g2rOY83XZU3VRXDZV7zWgE3Ogi3QB9gB/dcq/xenqd2Qr+1grU0msz2lsZ1LtY0Bqx0quU0vSijW +hgK9vb0hwy3cT1iUwd0nBfjAv92ixz2Sz42mk6Kdzd2u0T2Qyc2ltw1B1e3a3Y3b263by83a4e3b +2f2F6J3e6r3e7N3e7v3e8B3f8j3f9CZd3/Z93/id3/q93/zd3/793wAe4AI+4ARe4AZ+4Aie4Aq+ +4NqUAAA7 +------sinikael-?=_5-14763587882000.8241290969717285-- + +------sinikael-?=_2-14763587882000.8241290969717285-- + +------sinikael-?=_1-14763587882000.8241290969717285 +Content-Type: text/plain; name=notes.txt +Content-Disposition: attachment; filename=notes.txt +Content-Transfer-Encoding: 7bit + +Some notes about this e-mail +------sinikael-?=_1-14763587882000.8241290969717285-- diff --git a/examples/pipe.js b/examples/pipe.js new file mode 100644 index 0000000..c89a1f4 --- /dev/null +++ b/examples/pipe.js @@ -0,0 +1,69 @@ +/* eslint no-console:0 */ + +'use strict'; + +const util = require('util'); +const fs = require('fs'); +const MailParser = require('../lib/mail-parser.js'); + +let parser = new MailParser(); +let input = fs.createReadStream(__dirname + '/nodemailer.eml'); + +let attachments = []; + +input.pipe(parser); + +parser.on('headers', headers => { + console.log(util.inspect(headers, false, 22)); +}); + +parser.on('data', data => { + if (data.type === 'text') { + Object.keys(data).forEach(key => { + console.log(key); + console.log('----'); + console.log(data[key]); + }); + } + + if (data.type === 'attachment') { + attachments.push(data); + data.chunks = []; + data.chunklen = 0; + let size = 0; + Object.keys(data).forEach(key => { + if (typeof data[key] !== 'object' && typeof data[key] !== 'function') { + console.log('%s: %s', key, JSON.stringify(data[key])); + } + }); + data.content.on('readable', () => { + let chunk; + while ((chunk = data.content.read()) !== null) { + size += chunk.length; + data.chunks.push(chunk); + data.chunklen += chunk.length; + } + }); + + data.content.on('end', () => { + data.buf = Buffer.concat(data.chunks, data.chunklen); + console.log('%s: %s B', 'size', size); + // attachment needs to be released before next chunk of + // message data can be processed + data.release(); + }); + } +}); + +parser.on('end', () => { + console.log('READY'); + + parser.updateImageLinks((attachment, done) => done(false, 'data:' + attachment.contentType + ';base64,' + attachment.buf.toString('base64')), (err, html) => { + if (err) { + console.log(err); + } + if (html) { + console.log(html); + } + }); +}); diff --git a/examples/simple.eml b/examples/simple.eml new file mode 100644 index 0000000..1446140 --- /dev/null +++ b/examples/simple.eml @@ -0,0 +1,6 @@ +From: andris@kreata.ee +To: andris.reinman@gmail.com +Subject: test +Date: Sun, 8 Jan 2017 20:37:44 +0200 + +Hello world! diff --git a/examples/simple.js b/examples/simple.js new file mode 100644 index 0000000..80c2a7a --- /dev/null +++ b/examples/simple.js @@ -0,0 +1,15 @@ +/* eslint no-console:0 */ + +'use strict'; + +const util = require('util'); +const fs = require('fs'); +const simpleParser = require('../lib/simple-parser.js'); + +let input = fs.createReadStream(__dirname + '/simple.eml'); + +simpleParser(input).then(mail => { + console.log(util.inspect(mail, false, 22)); +}).catch(err => { + console.log(err); +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..be2a4b9 --- /dev/null +++ b/index.js @@ -0,0 +1,9 @@ +'use strict'; + +const MailParser = require('./lib/mail-parser'); +const SimpleParser = require('./lib/simple-parser'); + +module.exports = { + MailParser, + SimpleParser +}; diff --git a/lib/.npmignore b/lib/.npmignore new file mode 100644 index 0000000..cf55103 --- /dev/null +++ b/lib/.npmignore @@ -0,0 +1,4 @@ +bench +examples +test +Gruntfile.js diff --git a/lib/datetime.js b/lib/datetime.js deleted file mode 100644 index 99a5d0d..0000000 --- a/lib/datetime.js +++ /dev/null @@ -1,308 +0,0 @@ -"use strict"; - -/* - * More info at: http://phpjs.org - * - * This is version: 3.18 - * php.js is copyright 2010 Kevin van Zonneveld. - * - * Portions copyright Brett Zamir (http://brett-zamir.me), Kevin van Zonneveld - * (http://kevin.vanzonneveld.net), Onno Marsman, Theriault, Michael White - * (http://getsprink.com), Waldo Malqui Silva, Paulo Freitas, Jonas Raoni - * Soares Silva (http://www.jsfromhell.com), Jack, Philip Peterson, Ates Goral - * (http://magnetiq.com), Legaev Andrey, Ratheous, Alex, Martijn Wieringa, - * Nate, lmeyrick (https://sourceforge.net/projects/bcmath-js/), Philippe - * Baumann, Enrique Gonzalez, Webtoolkit.info (http://www.webtoolkit.info/), - * Ash Searle (http://hexmen.com/blog/), travc, Jani Hartikainen, Carlos R. L. - * Rodrigues (http://www.jsfromhell.com), Ole Vrijenhoek, WebDevHobo - * (http://webdevhobo.blogspot.com/), T.Wild, - * http://stackoverflow.com/questions/57803/how-to-convert-decimal-to-hex-in-javascript, - * pilus, GeekFG (http://geekfg.blogspot.com), Rafał Kukawski - * (http://blog.kukawski.pl), Johnny Mast (http://www.phpvrouwen.nl), Michael - * Grier, Erkekjetter, d3x, marrtins, Andrea Giammarchi - * (http://webreflection.blogspot.com), stag019, mdsjack - * (http://www.mdsjack.bo.it), Chris, Steven Levithan - * (http://blog.stevenlevithan.com), Arpad Ray (mailto:arpad@php.net), David, - * Joris, Tim de Koning (http://www.kingsquare.nl), Marc Palau, Michael White, - * Public Domain (http://www.json.org/json2.js), gettimeofday, felix, Aman - * Gupta, Pellentesque Malesuada, Thunder.m, Tyler Akins (http://rumkin.com), - * Karol Kowalski, Felix Geisendoerfer (http://www.debuggable.com/felix), - * Alfonso Jimenez (http://www.alfonsojimenez.com), Diplom@t - * (http://difane.com/), majak, Mirek Slugen, Mailfaker - * (http://www.weedem.fr/), Breaking Par Consulting Inc - * (http://www.breakingpar.com/bkp/home.nsf/0/87256B280015193F87256CFB006C45F7), - * Josh Fraser - * (http://onlineaspect.com/2007/06/08/auto-detect-a-time-zone-with-javascript/), - * Martin (http://www.erlenwiese.de/), Paul Smith, KELAN, Robin, saulius, AJ, - * Oleg Eremeev, Steve Hilder, gorthaur, Kankrelune - * (http://www.webfaktory.info/), Caio Ariede (http://caioariede.com), Lars - * Fischer, Sakimori, Imgen Tata (http://www.myipdf.com/), uestla, Artur - * Tchernychev, Wagner B. Soares, Christoph, nord_ua, class_exists, Der Simon - * (http://innerdom.sourceforge.net/), echo is bad, XoraX - * (http://www.xorax.info), Ozh, Alan C, Taras Bogach, Brad Touesnard, MeEtc - * (http://yass.meetcweb.com), Peter-Paul Koch - * (http://www.quirksmode.org/js/beat.html), T0bsn, Tim Wiel, Bryan Elliott, - * jpfle, JT, Thomas Beaucourt (http://www.webapp.fr), David Randall, Frank - * Forte, Eugene Bulkin (http://doubleaw.com/), noname, kenneth, Hyam Singer - * (http://www.impact-computing.com/), Marco, Raphael (Ao RUDLER), Ole - * Vrijenhoek (http://www.nervous.nl/), David James, Steve Clay, Jason Wong - * (http://carrot.org/), T. Wild, Paul, J A R, LH, strcasecmp, strcmp, JB, - * Daniel Esteban, strftime, madipta, Valentina De Rosa, Marc Jansen, - * Francesco, Stoyan Kyosev (http://www.svest.org/), metjay, Soren Hansen, - * 0m3r, Sanjoy Roy, Shingo, sankai, sowberry, hitwork, Rob, Norman "zEh" - * Fuchs, Subhasis Deb, josh, Yves Sucaet, Ulrich, Scott Baker, ejsanders, - * Nick Callen, Steven Levithan (stevenlevithan.com), Aidan Lister - * (http://aidanlister.com/), Philippe Jausions - * (http://pear.php.net/user/jausions), Zahlii, Denny Wardhana, Oskar Larsson - * Högfeldt (http://oskar-lh.name/), Brian Tafoya - * (http://www.premasolutions.com/), johnrembo, Gilbert, duncan, Thiago Mata - * (http://thiagomata.blog.com), Alexander Ermolaev - * (http://snippets.dzone.com/user/AlexanderErmolaev), Linuxworld, lmeyrick - * (https://sourceforge.net/projects/bcmath-js/this.), Jon Hohle, Pyerre, - * merabi, Saulo Vallory, HKM, ChaosNo1, djmix, Lincoln Ramsay, Adam Wallner - * (http://web2.bitbaro.hu/), paulo kuong, jmweb, Orlando, kilops, dptr1988, - * DxGx, Pedro Tainha (http://www.pedrotainha.com), Bayron Guevara, Le Torbi, - * James, Douglas Crockford (http://javascript.crockford.com), Devan - * Penner-Woelk, Jay Klehr, Kheang Hok Chin (http://www.distantia.ca/), Luke - * Smith (http://lucassmith.name), Rival, Amir Habibi - * (http://www.residence-mixte.com/), Blues (http://tech.bluesmoon.info/), Ben - * Bryan, booeyOH, Dreamer, Cagri Ekin, Diogo Resende, Howard Yeend, Pul, - * 3D-GRAF, jakes, Yannoo, Luke Godfrey, daniel airton wermann - * (http://wermann.com.br), Allan Jensen (http://www.winternet.no), Benjamin - * Lupton, davook, Atli Þór, Maximusya, Leslie Hoare, Bug?, setcookie, YUI - * Library: http://developer.yahoo.com/yui/docs/YAHOO.util.DateLocale.html, - * Blues at http://hacks.bluesmoon.info/strftime/strftime.js, Andreas, - * Michael, Christian Doebler, Gabriel Paderni, Marco van Oort, Philipp - * Lenssen, Arnout Kazemier (http://www.3rd-Eden.com), penutbutterjelly, Anton - * Ongson, DtTvB (http://dt.in.th/2008-09-16.string-length-in-bytes.html), - * meo, Greenseed, Yen-Wei Liu, mk.keck, William, rem, Jamie Beck - * (http://www.terabit.ca/), Russell Walker (http://www.nbill.co.uk/), - * Garagoth, Dino, Andrej Pavlovic, gabriel paderni, FGFEmperor, Scott Cariss, - * Slawomir Kaniecki, ReverseSyntax, Mateusz "loonquawl" Zalega, Francois, - * Kirk Strobeck, Billy, vlado houba, Jalal Berrami, date, Itsacon - * (http://www.itsacon.net/), Martin Pool, Pierre-Luc Paour, ger, john - * (http://www.jd-tech.net), mktime, Simon Willison - * (http://simonwillison.net), Nick Kolosov (http://sammy.ru), marc andreu, - * Arno, Nathan, Kristof Coomans (SCK-CEN Belgian Nucleair Research Centre), - * Fox, nobbler, stensi, Matteo, Riddler (http://www.frontierwebdev.com/), - * Tomasz Wesolowski, T.J. Leahy, rezna, Eric Nagel, Alexander M Beedie, baris - * ozdil, Greg Frazier, Bobby Drake, Ryan W Tenney (http://ryan.10e.us), Tod - * Gentille, Rafał Kukawski, FremyCompany, Manish, Cord, fearphage - * (http://http/my.opera.com/fearphage/), Victor, Brant Messenger - * (http://www.brantmessenger.com/), Matt Bradley, Luis Salazar - * (http://www.freaky-media.com/), Tim de Koning, taith, Rick Waldron, Mick@el - * - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL KEVIN VAN ZONNEVELD BE LIABLE FOR ANY CLAIM, DAMAGES - * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ -module.exports.strtotime = function(str, now) { - // http://kevin.vanzonneveld.net - // + original by: Caio Ariede (http://caioariede.com) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: David - // + improved by: Caio Ariede (http://caioariede.com) - // + improved by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Wagner B. Soares - // + bugfixed by: Artur Tchernychev - // % note 1: Examples all have a fixed timestamp to prevent tests to fail because of variable time(zones) - // * example 1: strtotime('+1 day', 1129633200); - // * returns 1: 1129719600 - // * example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200); - // * returns 2: 1130425202 - // * example 3: strtotime('last month', 1129633200); - // * returns 3: 1127041200 - // * example 4: strtotime('2009-05-04 08:30:00'); - // * returns 4: 1241418600 - - var i, match, s, strTmp = '', - parse = ''; - - strTmp = str; - strTmp = strTmp.replace(/\s{2,}|^\s|\s$/g, ' '); // unecessary spaces - strTmp = strTmp.replace(/[ \r\n]/g, ''); // unecessary chars - - if (strTmp == 'now') { - return (new Date()).getTime() / 1000; // Return seconds, not milli-seconds - } else if (!isNaN(parse = Date.parse(strTmp))) { - return (parse / 1000); - } else if (now) { - now = new Date(now * 1000); // Accept PHP-style seconds - } else { - now = new Date(); - } - - strTmp = strTmp.toLowerCase(); - - var __is = { - day: { - 'sun': 0, - 'mon': 1, - 'tue': 2, - 'wed': 3, - 'thu': 4, - 'fri': 5, - 'sat': 6 - }, - mon: { - 'jan': 0, - 'feb': 1, - 'mar': 2, - 'apr': 3, - 'may': 4, - 'jun': 5, - 'jul': 6, - 'aug': 7, - 'sep': 8, - 'oct': 9, - 'nov': 10, - 'dec': 11 - } - }; - - var process = function(m) { - var ago = (m[2] && m[2] == 'ago'); - var num = (num = m[0] == 'last' ? -1 : 1) * (ago ? -1 : 1); - - switch (m[0]) { - case 'last': - case 'next': - switch (m[1].substring(0, 3)) { - case 'yea': - now.setFullYear(now.getFullYear() + num); - break; - case 'mon': - now.setMonth(now.getMonth() + num); - break; - case 'wee': - now.setDate(now.getDate() + (num * 7)); - break; - case 'day': - now.setDate(now.getDate() + num); - break; - case 'hou': - now.setHours(now.getHours() + num); - break; - case 'min': - now.setMinutes(now.getMinutes() + num); - break; - case 'sec': - now.setSeconds(now.getSeconds() + num); - break; - default: - var day; - if (typeof(day = __is.day[m[1].substring(0, 3)]) != 'undefined') { - var diff = day - now.getDay(); - if (diff === 0) { - diff = 7 * num; - } else if (diff > 0) { - if (m[0] == 'last') { - diff -= 7; - } - } else { - if (m[0] == 'next') { - diff += 7; - } - } - now.setDate(now.getDate() + diff); - } - } - break; - - default: - if (/\d+/.test(m[0])) { - num *= parseInt(m[0], 10); - - switch (m[1].substring(0, 3)) { - case 'yea': - now.setFullYear(now.getFullYear() + num); - break; - case 'mon': - now.setMonth(now.getMonth() + num); - break; - case 'wee': - now.setDate(now.getDate() + (num * 7)); - break; - case 'day': - now.setDate(now.getDate() + num); - break; - case 'hou': - now.setHours(now.getHours() + num); - break; - case 'min': - now.setMinutes(now.getMinutes() + num); - break; - case 'sec': - now.setSeconds(now.getSeconds() + num); - break; - } - } else { - return false; - } - break; - } - return true; - }; - - match = strTmp.match(/^(\d{2,4}-\d{2}-\d{2})(?:\s(\d{1,2}:\d{2}(:\d{2})?)?(?:\.(\d+))?)?$/); - if (match) { - if (!match[2]) { - match[2] = '00:00:00'; - } else if (!match[3]) { - match[2] += ':00'; - } - - s = match[1].split(/-/g); - - for (i in __is.mon) { - if (__is.mon[i] == s[1] - 1) { - s[1] = i; - } - } - s[0] = parseInt(s[0], 10); - - s[0] = (s[0] >= 0 && s[0] <= 69) ? '20' + (s[0] < 10 ? '0' + s[0] : s[0] + '') : (s[0] >= 70 && s[0] <= 99) ? '19' + s[0] : s[0] + ''; - return parseInt(this.strtotime(s[2] + ' ' + s[1] + ' ' + s[0] + ' ' + match[2]) + (match[4] ? match[4] / 1000 : ''), 10); - } - - var regex = '([+-]?\\d+\\s' + - '(years?|months?|weeks?|days?|hours?|min|minutes?|sec|seconds?' + - '|sun\\.?|sunday|mon\\.?|monday|tue\\.?|tuesday|wed\\.?|wednesday' + - '|thu\\.?|thursday|fri\\.?|friday|sat\\.?|saturday)' + - '|(last|next)\\s' + - '(years?|months?|weeks?|days?|hours?|min|minutes?|sec|seconds?' + - '|sun\\.?|sunday|mon\\.?|monday|tue\\.?|tuesday|wed\\.?|wednesday' + - '|thu\\.?|thursday|fri\\.?|friday|sat\\.?|saturday))' + - '(\\sago)?'; - - match = strTmp.match(new RegExp(regex, 'gi')); // Brett: seems should be case insensitive per docs, so added 'i' - if (!match) { - return false; - } - - for (i = 0; i < match.length; i++) { - if (!process(match[i].split(' '))) { - return false; - } - } - - return (now.getTime() / 1000); -}; \ No newline at end of file diff --git a/lib/flowed-decoder.js b/lib/flowed-decoder.js new file mode 100644 index 0000000..c2dc7cc --- /dev/null +++ b/lib/flowed-decoder.js @@ -0,0 +1,48 @@ +'use strict'; + +// Helper class to rewrite nodes with specific mime type + +const Transform = require('stream').Transform; +const libmime = require('libmime'); + +/** + * Really bad "stream" transform to parse format=flowed content + * + * @constructor + * @param {String} delSp True if delsp option was used + */ +class FlowedDecoder extends Transform { + constructor(options) { + super(); + this.options = options || {}; + + this.chunks = []; + this.chunklen = 0; + } + + _transform(chunk, encoding, callback) { + if (!chunk || !chunk.length) { + return callback(); + } + + if (!encoding !== 'buffer') { + chunk = Buffer.from(chunk, encoding); + } + + this.chunks.push(chunk); + this.chunklen += chunk.length; + + callback(); + } + + _flush(callback) { + if (this.chunklen) { + let currentBody = Buffer.concat(this.chunks, this.chunklen); + let content = libmime.decodeFlowed(currentBody.toString('binary'), this.options.delSp); + this.push(Buffer.from(content, 'binary')); + } + return callback(); + } +} + +module.exports = FlowedDecoder; diff --git a/lib/mail-parser.js b/lib/mail-parser.js new file mode 100644 index 0000000..8b07f8e --- /dev/null +++ b/lib/mail-parser.js @@ -0,0 +1,792 @@ +'use strict'; + +const mailsplit = require('mailsplit'); +const libmime = require('libmime'); +const addressparser = require('addressparser'); +const Transform = require('stream').Transform; +const Splitter = mailsplit.Splitter; +const punycode = require('punycode'); +const FlowedDecoder = require('./flowed-decoder'); +const StreamHash = require('./stream-hash'); +const iconv = require('iconv-lite'); +const marked = require('marked'); +const htmlToText = require('html-to-text'); +const he = require('he'); + +class MailParser extends Transform { + constructor(config) { + let options = { + readableObjectMode: true, + writableObjectMode: false + }; + super(options); + + this.options = config || {}; + this.splitter = new Splitter(); + this.finished = false; + this.waitingEnd = false; + + this.headers = false; + + this.endReceived = false; + this.reading = false; + this.errored = false; + + this.tree = false; + this.curnode = false; + this.waitUntilAttachmentEnd = false; + this.attachmentCallback = false; + + this.hasHtml = false; + this.hasText = false; + + this.text = false; + this.html = false; + this.textAsHtml = false; + + this.attachmentList = []; + + this.splitter.on('readable', () => { + if (this.reading) { + return false; + } + this.readData(); + }); + + this.splitter.on('end', () => { + this.endReceived = true; + if (!this.reading) { + this.endStream(); + } + }); + + this.splitter.on('error', err => { + this.errored = true; + if (typeof this.waitingEnd === 'function') { + return this.waitingEnd(err); + } + this.emit('error', err); + }); + } + + readData() { + if (this.errored) { + return false; + } + this.reading = true; + let data = this.splitter.read(); + if (data === null) { + this.reading = false; + if (this.endReceived) { + this.endStream(); + } + return; + } + + this.processChunk(data, err => { + if (err) { + if (typeof this.waitingEnd === 'function') { + return this.waitingEnd(err); + } + return this.emit('error', err); + } + setImmediate(() => this.readData()); + }); + } + + endStream() { + this.finished = true; + if (typeof this.waitingEnd === 'function') { + this.waitingEnd(); + } + } + + _transform(chunk, encoding, done) { + if (!chunk || !chunk.length) { + return done(); + } + + if (this.splitter.write(chunk) === false) { + return this.splitter.once('drain', () => { + done(); + }); + } else { + return done(); + } + } + + _flush(done) { + setImmediate(() => this.splitter.end()); + if (this.finished) { + return this.cleanup(done); + } + this.waitingEnd = () => this.cleanup(done); + } + + cleanup(done) { + if (this.curnode && this.curnode.decoder) { + this.curnode.decoder.end(); + } + setImmediate(() => { + this.push(this.getTextContent()); + done(); + }); + } + + processHeaders(lines) { + let headers = new Map(); + (lines || []).forEach(line => { + let key = line.key; + let value = ((libmime.decodeHeader(line.line) || {}).value || '').toString().trim(); + value = Buffer.from(value, 'binary').toString(); + switch (key) { + case 'content-type': + case 'content-disposition': + case 'dkim-signature': + value = libmime.parseHeaderValue(value); + Object.keys(value && value.params || {}).forEach(key => { + try { + value.params[key] = libmime.decodeWords(value.params[key]); + } catch (E) { + // ignore, keep as is + } + }); + break; + case 'date': + value = new Date(value); + if (!value || value.toString() === 'Invalid Date' || !value.getTime()) { + // date parsing failed :S + value = new Date(); + } + break; + case 'subject': + try { + value = libmime.decodeWords(value); + } catch (E) { + // ignore, keep as is + } + break; + case 'references': + value = value.split(/\s+/).map(this.ensureMessageIDFormat); + break; + case 'message-id': + value = this.ensureMessageIDFormat(value); + break; + case 'in-reply-to': + value = this.ensureMessageIDFormat(value); + break; + case 'priority': + case 'x-priority': + case 'x-msmail-priority': + case 'importance': + key = 'priority'; + value = this.parsePriority(value); + break; + case 'from': + case 'to': + case 'cc': + case 'bcc': + case 'sender': + case 'reply-to': + case 'delivered-to': + case 'return-path': + value = addressparser(value); + this.decodeAddresses(value); + value = { + value, + html: this.getAddressesHTML(value), + text: this.getAddressesText(value) + }; + break; + } + + // handle list-* keys + if (key.substr(0, 5) === 'list-') { + value = this.parseListHeader(key.substr(5), value); + key = 'list'; + } + + if (value) { + if (!headers.has(key)) { + headers.set(key, [].concat(value || [])); + } else if (Array.isArray(value)) { + headers.set(key, headers.get(key).concat(value)); + } else { + headers.get(key).push(value); + } + } + }); + + // keep only the first value + let singleKeys = ['message-id', 'content-id', 'from', 'sender', 'in-reply-to', 'reply-to', 'subject', 'date', 'content-disposition', 'content-type', 'content-transfer-encoding', 'priority', 'mime-version', 'content-description', 'precedence', 'errors-to']; + + headers.forEach((value, key) => { + if (Array.isArray(value)) { + if (singleKeys.includes(key) && value.length) { + headers.set(key, value[value.length - 1]); + } else if (value.length === 1) { + headers.set(key, value[0]); + } + } + + if (key === 'list') { + // normalize List-* headers + let listValue = {}; + [].concat(value || []).forEach(val => { + Object.keys(val || {}).forEach(listKey => { + listValue[listKey] = val[listKey]; + }); + }); + headers.set(key, listValue); + } + }); + + return headers; + } + + parseListHeader(key, value) { + let addresses = addressparser(value); + let response = {}; + let data = addresses.map(address => { + if (/^https?:/i.test(address.name)) { + response.url = address.name; + } else if (address.name) { + response.name = address.name; + } + if (/^mailto:/.test(address.address)) { + response.mail = address.address.substr(7); + } else if (address.address && address.address.indexOf('@') < 0) { + response.id = address.address; + } else if (address.address) { + response.mail = address.address; + } + if (Object.keys(response).length) { + return response; + } + return false; + }).filter(address => address); + if (data.length) { + return { + [key]: response + }; + } + return false; + } + + parsePriority(value) { + value = value.toLowerCase().trim(); + if (!isNaN(parseInt(value, 10))) { // support "X-Priority: 1 (Highest)" + value = parseInt(value, 10) || 0; + if (value === 3) { + return 'normal'; + } else if (value > 3) { + return 'low'; + } else { + return 'high'; + } + } else { + switch (value) { + case 'non-urgent': + case 'low': + return 'low'; + case 'urgent': + case 'high': + return 'high'; + } + } + return 'normal'; + } + + ensureMessageIDFormat(value) { + if (!value.length) { + return false; + } + + if (value.charAt(0) !== '<') { + value = '<' + value; + } + + if (value.charAt(value.length - 1) !== '>') { + value += '>'; + } + + return value; + } + + decodeAddresses(addresses) { + addresses.forEach(address => { + address.name = (address.name || '').toString(); + if (address.name) { + try { + address.name = libmime.decodeWords(address.name); + } catch (E) { + //ignore, keep as is + } + } + if (/@xn\-\-/.test(address.address)) { + address.address = address.address.substr(0, address.address.lastIndexOf('@') + 1) + punycode.toUnicode(address.address.substr(address.address.lastIndexOf('@') + 1)); + } + if (address.group) { + this.decodeAddresses(address.group); + } + }); + } + + createNode(node) { + + let contentType = node.contentType; + let disposition = node.disposition; + let encoding = node.encoding; + let charset = node.charset; + + if (!contentType && node.root) { + contentType = 'text/plain'; + } + + let newNode = { + node, + headers: this.processHeaders(node.headers.getList()), + contentType, + children: [] + }; + + if (!/^multipart\//i.test(contentType)) { + + if (disposition && !['attachment', 'inline'].includes(disposition)) { + disposition = 'attachment'; + } + + if (!disposition && !['text/plain', 'text/html'].includes(contentType)) { + newNode.disposition = 'attachment'; + } else { + newNode.disposition = disposition || 'inline'; + } + + + newNode.encoding = ['quoted-printable', 'base64'].includes(encoding) ? encoding : 'binary'; + + if (charset) { + newNode.charset = charset; + } + + let decoder = node.getDecoder(); + if (/^text\//.test(contentType) && node.flowed) { + let flowDecoder = decoder; + decoder = new FlowedDecoder({ + delSp: node.delSp + }); + flowDecoder.on('error', err => { + decoder.emit('error', err); + }); + flowDecoder.pipe(decoder); + } + + newNode.decoder = decoder; + } + + if (node.root) { + this.headers = newNode.headers; + } + + // find location in tree + + if (!this.tree) { + newNode.root = true; + this.curnode = this.tree = newNode; + return newNode; + } + + // immediate child of root node + if (!this.curnode.parent) { + newNode.parent = this.curnode; + this.curnode.children.push(newNode); + this.curnode = newNode; + return newNode; + } + + // siblings + if (this.curnode.parent.node === node.parentNode) { + newNode.parent = this.curnode.parent; + this.curnode.parent.children.push(newNode); + this.curnode = newNode; + return newNode; + } + + // first child + if (this.curnode.node === node.parentNode) { + newNode.parent = this.curnode; + this.curnode.children.push(newNode); + this.curnode = newNode; + return newNode; + } + + // move up + let parentNode = this.curnode; + while ((parentNode = parentNode.parent)) { + if (parentNode.node === node.parentNode) { + newNode.parent = parentNode; + parentNode.children.push(newNode); + this.curnode = newNode; + return newNode; + } + } + + // should never happen, can't detect parent + this.curnode = newNode; + return newNode; + } + + getTextContent() { + let text = []; + let html = []; + let processNode = (alternative, level, node) => { + if (node.showMeta) { + let meta = ['From', 'Subject', 'Date', 'To', 'Cc', 'Bcc'].map(fkey => { + let key = fkey.toLowerCase(); + if (!node.headers.has(key)) { + return false; + } + let value = node.headers.get(key); + if (!value) { + return false; + } + return { + key: fkey, + value: Array.isArray(value) ? value[value.length - 1] : value + }; + }).filter(entry => entry); + if (this.hasHtml) { + html.push('' + meta.map(entry => { + + let value = entry.value; + switch (entry.key) { + case 'From': + case 'To': + case 'Cc': + case 'Bcc': + value = value.html; + break; + case 'Date': + value = this.options.formatDateString ? this.options.formatDateString(value) : value.toUTCString(); + break; + case 'Subject': + value = '' + he.encode(value) + ''; + break; + default: + value = he.encode(value); + } + + return ''; + }).join('\n') + '
' + he.encode(entry.key) + ':' + value + '
'); + } + if (this.hasText) { + text.push('\n' + meta.map(entry => { + let value = entry.value; + switch (entry.key) { + case 'From': + case 'To': + case 'Cc': + case 'Bcc': + value = value.text; + break; + case 'Date': + value = this.options.formatDateString ? this.options.formatDateString(value) : value.toUTCString(); + break; + } + return entry.key + ': ' + value; + }).join('\n') + '\n'); + } + } + if (node.textContent) { + if (node.contentType === 'text/plain') { + text.push(node.textContent); + if (!alternative && this.hasHtml) { + html.push(marked(node.textContent, { + breaks: true, + sanitize: true, + gfm: true, + tables: true, + smartypants: true + })); + } + } else if (node.contentType === 'text/html') { + html.push(node.textContent); + if (!alternative && this.hasText) { + text.push(htmlToText.fromString(node.textContent)); + } + } + } + alternative = alternative || node.contentType === 'multipart/alternative'; + node.children.forEach(subNode => { + processNode(alternative, level + 1, subNode); + }); + }; + + processNode(false, 0, this.tree); + + let response = { + type: 'text' + }; + if (html.length) { + this.html = response.html = html.join('
\n'); + } + if (text.length) { + this.text = response.text = text.join('\n'); + this.textAsHtml = response.textAsHtml = text.map(part => marked(part, { + breaks: true, + sanitize: true, + gfm: true, + tables: true, + smartypants: true + })).join('
\n'); + } + return response; + } + + processChunk(data, done) { + switch (data.type) { + case 'node': + { + let node = this.createNode(data); + if (node === this.tree) { + ['subject', 'references', 'date', 'to', 'from', 'to', 'cc', 'bcc', 'message-id', 'in-reply-to', 'reply-to'].forEach(key => { + if (node.headers.has(key)) { + this[key.replace(/\-([a-z])/g, (m, c) => c.toUpperCase())] = node.headers.get(key); + } + }); + this.emit('headers', node.headers); + } + + if (data.contentType === 'message/rfc822') { + break; + } + + if (data.parentNode && data.parentNode.contentType === 'message/rfc822') { + node.showMeta = true; + } + + if (node.disposition === 'attachment') { + + let contentType = node.contentType; + if (node.contentType === 'application/octet-stream' && data.filename) { + contentType = libmime.detectMimeType(data.filename) || 'application/octet-stream'; + } + + let attachment = { + type: 'attachment', + content: null, + contentType, + release: () => { + attachment.release = null; + if (this.waitUntilAttachmentEnd && typeof this.attachmentCallback === 'function') { + setImmediate(this.attachmentCallback); + } + this.attachmentCallback = false; + this.waitUntilAttachmentEnd = false; + } + }; + + let hasher = new StreamHash(attachment, 'md5'); + node.decoder.on('error', err => { + hasher.emit('error', err); + }); + node.decoder.pipe(hasher); + attachment.content = hasher; + + this.waitUntilAttachmentEnd = true; + if (data.disposition) { + attachment.contentDisposition = data.disposition; + } + if (data.filename) { + attachment.filename = data.filename; + } + if (node.headers.has('content-id')) { + attachment.contentId = [].concat(node.headers.get('content-id') || []).shift(); + attachment.cid = attachment.contentId.trim().replace(/^<|>$/g, '').trim(); + let parentNode = node; + while ((parentNode = parentNode.parent)) { + if (parentNode.contentType === 'multipart/related') { + attachment.related = true; + } + } + } + + attachment.headers = node.headers; + this.push(attachment); + this.attachmentList.push(attachment); + + } else if (node.disposition === 'inline') { + let chunks = []; + let chunklen = 0; + let contentStream = node.decoder; + + if (node.contentType === 'text/plain') { + this.hasText = true; + } else if (node.contentType === 'text/html') { + this.hasHtml = true; + } + + let charset = node.charset || 'windows-1257'; + //charset = charset || 'windows-1257'; + + if (!['ascii', 'usascii', 'utf8'].includes(charset.replace(/[^a-z0-9]+/g, '').trim().toLowerCase())) { + try { + let decodeStream = iconv.decodeStream(charset); + contentStream.on('error', err => { + decodeStream.emit('error', err); + }); + contentStream.pipe(decodeStream); + contentStream = decodeStream; + } catch (E) { + // do not decode charset + } + } + + contentStream.on('readable', () => { + let chunk; + while ((chunk = contentStream.read()) !== null) { + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk); + } + chunks.push(chunk); + chunklen += chunk.length; + } + }); + + contentStream.once('end', () => { + node.textContent = Buffer.concat(chunks, chunklen).toString().replace(/\r?\n/g, '\n'); + }); + + contentStream.once('error', err => { + this.emit('error', err); + }); + } + + break; + } + case 'data': + if (this.curnode && this.curnode.decoder) { + this.curnode.decoder.end(); + } + if (this.waitUntilAttachmentEnd) { + this.attachmentCallback = done; + return; + } + // multipart message structure + // this is not related to any specific 'node' block as it includes + // everything between the end of some node body and between the next header + //process.stdout.write(data.value); + break; + case 'body': + if (this.curnode && this.curnode.decoder) { + if (this.curnode.decoder.write(data.value) === false) { + return this.curnode.decoder.once('drain', done); + } + } + // Leaf element body. Includes the body for the last 'node' block. You might + // have several 'body' calls for a single 'node' block + //process.stdout.write(data.value); + break; + } + + setImmediate(done); + } + + getAddressesHTML(value) { + let formatSingleLevel = addresses => addresses.map(address => { + let str = ''; + if (address.name) { + str += '' + he.encode(address.name) + (address.group ? ': ' : '') + ''; + } + if (address.address) { + let link = '' + he.encode(address.address) + ''; + if (address.name) { + str += ' <' + link + '>'; + } else { + str += link; + } + } + if (address.group) { + str += formatSingleLevel(address.group) + ';'; + } + return str; + }).join(', '); + return formatSingleLevel([].concat(value || [])); + } + + getAddressesText(value) { + let formatSingleLevel = addresses => addresses.map(address => { + let str = ''; + if (address.name) { + str += address.name + (address.group ? ': ' : ''); + } + if (address.address) { + let link = address.address; + if (address.name) { + str += ' <' + link + '>'; + } else { + str += link; + } + } + if (address.group) { + str += formatSingleLevel(address.group) + ';'; + } + return str; + }).join(', '); + return formatSingleLevel([].concat(value || [])); + } + + updateImageLinks(replaceCallback, done) { + if (!this.html) { + return setImmediate(() => done(null, false)); + } + + let cids = new Map(); + let html = (this.html || '').toString(); + + html.replace(/\bcid:([^'"]{1,256})/g, (match, cid) => { + for (let i = 0, len = this.attachmentList.length; i < len; i++) { + if (this.attachmentList[i].cid === cid && /^image\/[\w]+$/i.test(this.attachmentList[i].contentType)) { + if (/^image\/[\w]+$/i.test(this.attachmentList[i].contentType)) { + cids.set(cid, { + attachment: this.attachmentList[i] + }); + } + break; + } + } + return match; + }); + + let cidList = []; + cids.forEach(entry => { + cidList.push(entry); + }); + + let pos = 0; + let processNext = () => { + if (pos >= cidList.length) { + html = html.replace(/\bcid:([^'"]{1,256})/g, (match, cid) => { + if (cids.has(cid) && cids.get(cid).url) { + return cids.get(cid).url; + } + return match; + }); + + return done(null, html); + } + let entry = cidList[pos++]; + replaceCallback(entry.attachment, (err, url) => { + if (err) { + return setImmediate(() => done(err)); + } + entry.url = url; + setImmediate(processNext); + }); + }; + + setImmediate(processNext); + } +} + +module.exports = MailParser; diff --git a/lib/mailparser.js b/lib/mailparser.js deleted file mode 100644 index 4851716..0000000 --- a/lib/mailparser.js +++ /dev/null @@ -1,1513 +0,0 @@ -"use strict"; - -/** - * @fileOverview This is the main file for the MailParser library to parse raw e-mail data - * @author Andris Reinman - * @version 0.2.23 - */ - -var Stream = require("stream").Stream, - utillib = require("util"), - mimelib = require("mimelib"), - datetime = require("./datetime"), - encodinglib = require("encoding"), - Streams = require("./streams"), - crypto = require("crypto"), - mime = require("mime"); - -// Expose to the world -module.exports.MailParser = MailParser; - -// MailParser is a FSM - it is always in one of the possible states -var STATES = { - header: 0x1, - body: 0x2, - finished: 0x3 -}; - -/** - *

Creates instance of MailParser which in turn extends Stream

- * - *

Options object has the following properties:

- * - *
    - *
  • debug - if set to true print all incoming lines to decodeq
  • - *
  • streamAttachments - if set to true, stream attachments instead of including them
  • - *
  • unescapeSMTP - if set to true replace double dots in the beginning of the file
  • - *
  • defaultCharset - the default charset for text/plain, text/html content, if not set reverts to Latin-1 - *
  • showAttachmentLinks
  • - if set to true, show inlined attachment links - *
- * - * @constructor - * @param {Object} [options] Optional options object - */ -function MailParser(options) { - - // Make MailParser a Stream object - Stream.call(this); - this.writable = true; - - /** - * Options object - * @public */ - this.options = options || {}; - - /** - * Indicates current state the parser is in - * @private */ - this._state = STATES.header; - - /** - * The remaining data from the previos chunk which is waiting to be processed - * @private */ - this._remainder = ""; - - /** - * The complete tree structure of the e-mail - * @public */ - this.mimeTree = this._createMimeNode(); - - /** - * Current node of the multipart mime tree that is being processed - * @private */ - this._currentNode = this.mimeTree; - - // default values for the root node - this._currentNode.priority = "normal"; - - /** - * An object of already used attachment filenames - * @private */ - this._fileNames = {}; - - /** - * An array of multipart nodes - * @private */ - this._multipartTree = []; - - - /** - * This is the final mail structure object that is returned to the client - * @public */ - this.mailData = {}; - - /** - * Line counter for debugging - * @private */ - this._lineCounter = 0; - - /** - * Did the last chunk end with \r - * @private */ - this._lineFeed = false; - - /** - * Is the "headers" event already emitted - * @private */ - this._headersSent = false; - - /** - * If the e-mail is in mbox format, unescape ">From " to "From " in body - * @private */ - this._isMbox = -1; -} -// inherit methods and properties of Stream -utillib.inherits(MailParser, Stream); - -/** - *

Writes a value to the MailParser stream

- * - * @param {Buffer|String} chunk The data to be written to the MailParser stream - * @param {String} [encoding] The encoding to be used when "chunk" is a string - * @returns {Boolean} Returns true - */ -MailParser.prototype.write = function(chunk, encoding) { - if (this._write(chunk, encoding)) { - if (typeof setImmediate == "function") { - setImmediate(this._process.bind(this)); - } else { - process.nextTick(this._process.bind(this)); - } - } - return true; -}; - -/** - *

Terminates the MailParser stream

- * - *

If "chunk" is set, writes it to the Stream before terminating.

- * - * @param {Buffer|String} chunk The data to be written to the MailParser stream - * @param {String} [encoding] The encoding to be used when "chunk" is a string - */ -MailParser.prototype.end = function(chunk, encoding) { - this._write(chunk, encoding); - - if (this.options.debug && this._remainder) { - console.log("REMAINDER: " + this._remainder); - } - - if (typeof setImmediate == "function") { - setImmediate(this._process.bind(this, true)); - } else { - process.nextTick(this._process.bind(this, true)); - } -}; - -/** - *

Normalizes CRLF's before writing to the Mailparser stream, does not call `_process`

- * - * @param {Buffer|String} chunk The data to be written to the MailParser stream - * @param {String} [encoding] The encoding to be used when "chunk" is a string - * @returns {Boolean} Returns true if writing the chunk was successful - */ -MailParser.prototype._write = function(chunk, encoding) { - if (typeof chunk == "string") { - chunk = new Buffer(chunk, encoding); - } - - chunk = chunk && chunk.toString("binary") || ""; - - // if the last chunk ended with \r and this one begins - // with \n, it's a split line ending. Since the last \r - // was already used, skip the \n - if (this._lineFeed && chunk.charAt(0) === "\n") { - chunk = chunk.substr(1); - } - this._lineFeed = chunk.substr(-1) === "\r"; - - if (chunk && chunk.length) { - this._remainder += chunk; - return true; - } - return false; -}; - - -/** - *

Processes the data written to the MailParser stream

- * - *

The data is split into lines and each line is processed individually. Last - * line in the batch is preserved as a remainder since it is probably not a - * complete line but just the beginning of it. The remainder is later prepended - * to the next batch of data.

- * - * @param {Boolean} [finalPart=false] if set to true indicates that this is the last part of the stream - */ -MailParser.prototype._process = function(finalPart) { - - finalPart = !!finalPart; - var lines = this._remainder.split(/\r?\n|\r/), - line, i, len; - - if (!finalPart) { - this._remainder = lines.pop(); - // force line to 1MB chunks if needed - if (this._remainder.length > 1048576) { - this._remainder = this._remainder.replace(/(.{1048576}(?!\r?\n|\r))/g, "$&\n"); - } - } - - for (i = 0, len = lines.length; i < len; i++) { - line = lines[i]; - - if (this.options.unescapeSMTP && line.substr(0, 2) == "..") { - line = line.substr(1); - } - - if (this._isMbox === true && line.match(/^\>+From /)) { - line = line.substr(1); - } - - if (this.options.debug) { - console.log("LINE " + (++this._lineCounter) + " (" + this._state + "): " + line); - } - - if (this._state == STATES.header) { - if (this._processStateHeader(line) === true) { - continue; - } - } - - if (this._state == STATES.body) { - if (this._processStateBody(line) === true) { - continue; - } - } - } - if (finalPart) { - if (this._state == STATES.header && this._remainder) { - this._processStateHeader(this._remainder); - if (!this._headersSent) { - this.emit("headers", this._currentNode.parsedHeaders); - this._headersSent = true; - } - } - if (this._currentNode.content || this._currentNode.stream) { - this._finalizeContents(); - } - this._state = STATES.finished; - if (typeof setImmediate == "function") { - setImmediate(this._processMimeTree.bind(this)); - } else { - process.nextTick(this._processMimeTree.bind(this)); - } - } - - -}; - -/** - *

Processes a line while in header state

- * - *

If header state ends and body starts, detect if the contents is an attachment - * and create a stream for it if needed

- * - * @param {String} line The contents of a line to be processed - * @returns {Boolean} If state changes to body retuns true - */ -MailParser.prototype._processStateHeader = function(line) { - var attachment, lastPos = this._currentNode.headers.length - 1, - textContent = false, - rootNode, - extension; - - // Check if the header ends and body starts - if (!line.length) { - if (lastPos >= 0) { - this._processHeaderLine(lastPos); - } - if (!this._headersSent) { - this.emit("headers", this._currentNode.parsedHeaders); - this._headersSent = true; - } - - this._state = STATES.body; - - // if there's unprocessed header data, do it now - if (lastPos >= 0) { - this._processHeaderLine(lastPos); - } - - // this is a very simple e-mail, no content type set - if (!this._currentNode.parentNode && !this._currentNode.meta.contentType) { - this._currentNode.meta.contentType = "text/plain"; - } - - textContent = ["text/plain", "text/html", "text/calendar"].indexOf(this._currentNode.meta.contentType || "") >= 0; - - // detect if this is an attachment or a text node (some agents use inline dispositions for text) - if (textContent && (!this._currentNode.meta.contentDisposition || this._currentNode.meta.contentDisposition == "inline")) { - this._currentNode.attachment = false; - } else if ((!textContent || ["attachment", "inline"].indexOf(this._currentNode.meta.contentDisposition) >= 0) && - !this._currentNode.meta.mimeMultipart) { - this._currentNode.attachment = true; - } - - // handle attachment start - if (this._currentNode.attachment) { - - this._currentNode.meta.generatedFileName = this._generateFileName(this._currentNode.meta.fileName, this._currentNode.meta.contentType); - - this._currentNode.meta.contentId = this._currentNode.meta.contentId || - crypto.createHash("md5").update(new Buffer(this._currentNode.meta.generatedFileName, 'utf-8')).digest("hex") + "@mailparser"; - - extension = this._currentNode.meta.generatedFileName.split(".").pop().toLowerCase(); - - // Update content-type if it's an application/octet-stream and file extension is available - if (this._currentNode.meta.contentType == "application/octet-stream" && mime.lookup(extension)) { - this._currentNode.meta.contentType = mime.lookup(extension); - } - - attachment = this._currentNode.meta; - if (this.options.streamAttachments) { - if (this._currentNode.meta.transferEncoding == "base64") { - this._currentNode.stream = new Streams.Base64Stream(); - } else if (this._currentNode.meta.transferEncoding == "quoted-printable") { - this._currentNode.stream = new Streams.QPStream("binary"); - } else if (this._currentNode.meta.transferEncoding == "uuencode") { - this._currentNode.stream = new Streams.UUEStream("binary"); - } else { - this._currentNode.stream = new Streams.BinaryStream(); - } - attachment.stream = this._currentNode.stream; - - rootNode = this._currentNode; - - while (rootNode.parentNode) { - rootNode = rootNode.parentNode; - } - - this.emit("attachment", attachment, rootNode); - } else { - this._currentNode.content = undefined; - } - } - - return true; - } - - // unfold header lines if needed - if (line.match(/^\s+/) && lastPos >= 0) { - this._currentNode.headers[lastPos] += " " + line.trim(); - } else { - this._currentNode.headers.push(line.trim()); - if (lastPos >= 0) { - // if a complete header line is received, process it - this._processHeaderLine(lastPos); - } - } - - return false; -}; - -/** - *

Processes a line while in body state

- * - * @param {String} line The contents of a line to be processed - * @returns {Boolean} If body ends return true - */ -MailParser.prototype._processStateBody = function(line) { - var i, len, node, - nodeReady = false; - - // Handle multipart boundaries - if (line.substr(0, 2) == "--") { - for (i = 0, len = this._multipartTree.length; i < len; i++) { - - // check if a new element block starts - if (line == "--" + this._multipartTree[i].boundary) { - - if (this._currentNode.content || this._currentNode.stream) { - this._finalizeContents(); - } - - node = this._createMimeNode(this._multipartTree[i].node); - this._multipartTree[i].node.childNodes.push(node); - this._currentNode = node; - this._state = STATES.header; - nodeReady = true; - break; - } else - // check if a multipart block ends - if (line == "--" + this._multipartTree[i].boundary + "--") { - - if (this._currentNode.content || this._currentNode.stream) { - this._finalizeContents(); - } - - if (this._multipartTree[i].node.parentNode) { - this._currentNode = this._multipartTree[i].node.parentNode; - } else { - this._currentNode = this._multipartTree[i].node; - } - this._state = STATES.body; - nodeReady = true; - break; - } - } - } - if (nodeReady) { - return true; - } - - // handle text or attachment line - if (["text/plain", "text/html", "text/calendar"].indexOf(this._currentNode.meta.contentType || "") >= 0 && - !this._currentNode.attachment) { - this._handleTextLine(line); - } else if (this._currentNode.attachment) { - this._handleAttachmentLine(line); - } - - return false; -}; - -/** - *

Processes a complete unfolded header line

- * - *

Processes a line from current node headers array and replaces its value. - * Input string is in the form of "X-Mailer: PHP" and its replacement would be - * an object {key: "x-mailer", value: "PHP"}

- * - *

Additionally node meta object will be filled also, for example with data from - * To: From: Cc: etc fields.

- * - * @param {Number} pos Which header element (from an header lines array) should be processed - */ -MailParser.prototype._processHeaderLine = function(pos) { - var key, value, parts, line; - - pos = pos || 0; - - if (!(line = this._currentNode.headers[pos]) || typeof line != "string") { - return; - } - - if (!this._headersSent && this._isMbox < 0) { - if ((this._isMbox = !!line.match(/^From /))) { - return; - } - } - - parts = line.split(":"); - - key = parts.shift().toLowerCase().trim(); - value = parts.join(":").trim(); - - switch (key) { - case "content-type": - this._parseContentType(value); - break; - case "mime-version": - this._currentNode.useMIME = true; - break; - case "date": - this._currentNode.meta.date = this._parseDateString(value); - break; - case "received": - case "x-received": - this._parseReceived(value); - break; - case "to": - if (this._currentNode.to && this._currentNode.to.length) { - this._currentNode.to = this._currentNode.to.concat(mimelib.parseAddresses(value)); - } else { - this._currentNode.to = mimelib.parseAddresses(value); - } - break; - case "from": - if (this._currentNode.from && this._currentNode.from.length) { - this._currentNode.from = this._currentNode.from.concat(mimelib.parseAddresses(value)); - } else { - this._currentNode.from = mimelib.parseAddresses(value); - } - break; - case "reply-to": - if (this._currentNode.replyTo && this._currentNode.replyTo.length) { - this._currentNode.replyTo = this._currentNode.replyTo.concat(mimelib.parseAddresses(value)); - } else { - this._currentNode.replyTo = mimelib.parseAddresses(value); - } - break; - case "cc": - if (this._currentNode.cc && this._currentNode.cc.length) { - this._currentNode.cc = this._currentNode.cc.concat(mimelib.parseAddresses(value)); - } else { - this._currentNode.cc = mimelib.parseAddresses(value); - } - break; - case "bcc": - if (this._currentNode.bcc && this._currentNode.bcc.length) { - this._currentNode.bcc = this._currentNode.bcc.concat(mimelib.parseAddresses(value)); - } else { - this._currentNode.bcc = mimelib.parseAddresses(value); - } - break; - case "x-priority": - case "x-msmail-priority": - case "importance": - value = this._parsePriority(value); - this._currentNode.priority = value; - break; - case "message-id": - this._currentNode.meta.messageId = this._trimQuotes(value); - this._currentNode.messageId = this._currentNode.meta.messageId; - break; - case "references": - this._parseReferences(value); - break; - case "in-reply-to": - this._parseInReplyTo(value); - break; - case "thread-index": - this._currentNode.meta.threadIndex = value; - break; - case "content-transfer-encoding": - this._currentNode.meta.transferEncoding = value.toLowerCase(); - break; - case "content-location": - this._currentNode.meta.location = value.toLowerCase(); - break; - case "subject": - this._currentNode.subject = this._encodeString(value); - break; - case "content-disposition": - this._parseContentDisposition(value); - break; - case "content-id": - this._currentNode.meta.contentId = this._trimQuotes(value); - break; - } - - if (this._currentNode.parsedHeaders[key]) { - if (!Array.isArray(this._currentNode.parsedHeaders[key])) { - this._currentNode.parsedHeaders[key] = [this._currentNode.parsedHeaders[key]]; - } - this._currentNode.parsedHeaders[key].push(this._replaceMimeWords(value)); - } else { - this._currentNode.parsedHeaders[key] = this._replaceMimeWords(value); - } - - this._currentNode.headers[pos] = { - key: key, - value: value - }; -}; - -/** - *

Creates an empty node element for the mime tree

- * - *

Created element includes parentNode property and a childNodes array. This is - * needed to later walk the whole mime tree

- * - * @param {Object} [parentNode] the parent object for the created node - * @returns {Object} node element for the mime tree - */ -MailParser.prototype._createMimeNode = function(parentNode) { - var node = { - parentNode: parentNode || this._currentNode || null, - headers: [], - parsedHeaders: {}, - meta: {}, - childNodes: [] - }; - - return node; -}; - -/** - *

Splits a header value into key-value pairs

- * - *

Splits on ; - the first value will be set as defaultValue property and will - * not be handled, others will be split on = to key-value pairs

- * - *

For example content-type: text/plain; charset=utf-8 will become:

- * - *
- * {
- *     defaultValue: "text/plain",
- *     charset: "utf-8"
- * }
- * 
- * - * @param {String} value A string to be splitted into key-value pairs - * @returns {Object} a key-value object, with defaultvalue property - */ -MailParser.prototype._parseHeaderLineWithParams = function(value) { - var key, parts, returnValue = {}; - - parts = value.match(/(?:[^;"]+|"[^"]*")+/g) || [value]; - returnValue.defaultValue = parts.shift().toLowerCase(); - - for (var i = 0, len = parts.length; i < len; i++) { - value = parts[i].split("="); - key = value.shift().trim().toLowerCase(); - value = value.join("=").trim(); - - // trim quotes - value = this._trimQuotes(value); - returnValue[key] = value; - } - - return returnValue; -}; - -/** - *

Parses date string - * - *

Receives possible date string in different formats and - * transforms it into a JS Date object

- * - * @param {String} value possible date string - * @returns {Date|Boolean} date object - */ -MailParser.prototype._parseDateString = function(value) { - var date; - - date = new Date(value); - if (Object.prototype.toString.call(date) != "[object Date]" || date.toString() == "Invalid Date") { - try { - date = datetime.strtotime(value); - } catch (E) { - return false; - } - if (date) { - date = new Date(date * 1000); - } else { - return false; - } - } - - return date; -}; - -/** - *

Parses Received and X-Received header field value

- * - *

Pulls received date from the received and x-received header fields and - * update current node meta object with this date as long as it's later as the - * existing date of the meta object

- * - *

Example: by 10.25.25.72 with SMTP id 69csp2404548lfz; Fri, 6 Feb 2015 15:15:32 -0800 (PST) - * will become: - *

- * - *
new Date('2015-02-06T23:15:32.000Z')
- * - * @param {String} value Received string - * @returns {Date|Boolean} parsed received date - */ -MailParser.prototype._parseReceived = function(value) { - var receivedDate, date, splitString; - if (!value) { - return false; - } - - splitString = value.split(';'); - value = splitString[splitString.length - 1]; - - date = this._parseDateString(value); - receivedDate = this._currentNode.meta.receivedDate; - - if (!date) { - if (!receivedDate) { - this._currentNode.meta.receivedDate = date; - } - return date; - } - - if (!receivedDate) { - this._currentNode.meta.receivedDate = date; - } else if (date > receivedDate) { - this._currentNode.meta.receivedDate = date; - } - - return date; -}; - -/** - *

Parses a Content-Type header field value

- * - *

Fetches additional properties from the content type (charset etc.) and fills - * current node meta object with this data

- * - * @param {String} value Content-Type string - * @returns {Object} parsed contenttype object - */ -MailParser.prototype._parseContentType = function(value) { - var fileName; - value = this._parseHeaderLineWithParams(value); - if (value) { - if (value.defaultValue) { - value.defaultValue = value.defaultValue.toLowerCase(); - this._currentNode.meta.contentType = value.defaultValue; - if (value.defaultValue.substr(0, "multipart/".length) == "multipart/") { - this._currentNode.meta.mimeMultipart = value.defaultValue.substr("multipart/".length); - } - } else { - this._currentNode.meta.contentType = "application/octet-stream"; - } - if (value.charset) { - value.charset = value.charset.toLowerCase(); - if (value.charset.substr(0, 4) == "win-") { - value.charset = "windows-" + value.charset.substr(4); - } else if (value.charset == "ks_c_5601-1987") { - value.charset = "cp949"; - } else if (value.charset.match(/^utf\d/)) { - value.charset = "utf-" + value.charset.substr(3); - } else if (value.charset.match(/^latin[\-_]?\d/)) { - value.charset = "iso-8859-" + value.charset.replace(/\D/g, ""); - } else if (value.charset.match(/^(us\-)?ascii$/)) { - value.charset = "utf-8"; - } else if (value.charset.match(/^ansi_x3\.4\-19/)) { - // ANSI_X3.4-1968 and ANSI_X3.4-1986 are aliases for ASCII. - // See http://en.wikipedia.org/wiki/ASCII#Aliases - value.charset = "utf-8"; - } - this._currentNode.meta.charset = value.charset; - } - if (value.format) { - this._currentNode.meta.textFormat = value.format.toLowerCase(); - } - if (value.delsp) { - this._currentNode.meta.textDelSp = value.delsp.toLowerCase(); - } - if (value.boundary) { - this._currentNode.meta.mimeBoundary = value.boundary; - } - - if (value.method) { - this._currentNode.meta.method = value.method; - } - - - if (!this._currentNode.meta.fileName && (fileName = this._detectFilename(value))) { - this._currentNode.meta.fileName = fileName; - } - - if (value.boundary) { - this._currentNode.meta.mimeBoundary = value.boundary; - this._multipartTree.push({ - boundary: value.boundary, - node: this._currentNode - }); - } - } - return value; -}; - -/** - *

Parses file name from a Content-Type or Content-Disposition field

- * - *

Supports RFC2231 for - * folded filenames

- * - * @param {Object} value Parsed Content-(Type|Disposition) object - * @return {String} filename - */ -MailParser.prototype._detectFilename = function(value) { - var fileName = "", - i = 0, - parts, encoding, name, part; - - if (value.name) { - return this._replaceMimeWords(value.name); - } - - if (value.filename) { - return this._replaceMimeWords(value.filename); - } - - // RFC2231 - if (value["name*"]) { - fileName = value["name*"]; - } else if (value["filename*"]) { - fileName = value["filename*"]; - } else if (value["name*0*"] || value["name*0"]) { - while ((part = (value["name*" + (i) + "*"] || value["name*" + (i)]))) { - fileName += part; - i++; - } - } else if (value["filename*0*"] || value["filename*0"]) { - while ((part = (value["filename*" + (i) + "*"] || value["filename*" + (i)]))) { - fileName += part; - i++; - } - } - - if (fileName) { - parts = fileName.split("'"); - encoding = (parts.length > 1) ? parts[0] : "us-ascii"; - name = parts.pop(); - if (name) { - return this._replaceMimeWords(this._replaceMimeWords("=?" + encoding + "?Q?" + name.replace(/%/g, "=") + "?=")); - } - } - return ""; -}; - -/** - *

Parses Content-Disposition header field value

- * - *

Fetches filename to current node meta object

- * - * @param {String} value A Content-Disposition header field - */ -MailParser.prototype._parseContentDisposition = function(value) { - var fileName; - - value = this._parseHeaderLineWithParams(value); - - if (value) { - if (value.defaultValue) { - this._currentNode.meta.contentDisposition = value.defaultValue.trim().toLowerCase(); - } - if ((fileName = this._detectFilename(value))) { - this._currentNode.meta.fileName = fileName; - } - } -}; - -/** - *

Parses "References" header

- * - * @param {String} value References header field - */ -MailParser.prototype._parseReferences = function(value) { - this._currentNode.references = (this._currentNode.references || []).concat( - (value || "").toString().trim().split(/\s+/).map(this._trimQuotes.bind(this)) - ); -}; - -/** - *

Parses "In-Reply-To" header

- * - * @param {String} value In-Reply-To header field - */ -MailParser.prototype._parseInReplyTo = function(value) { - this._currentNode.inReplyTo = (this._currentNode.inReplyTo || []).concat( - (value || "").toString().trim().split(/\s+/).map(this._trimQuotes.bind(this)) - ); -}; - -/** - *

Parses the priority of the e-mail

- * - * @param {String} value The priority value - * @returns {String} priority string low|normal|high - */ -MailParser.prototype._parsePriority = function(value) { - value = value.toLowerCase().trim(); - if (!isNaN(parseInt(value, 10))) { // support "X-Priority: 1 (Highest)" - value = parseInt(value, 10) || 0; - if (value == 3) { - return "normal"; - } else if (value > 3) { - return "low"; - } else { - return "high"; - } - } else { - switch (value) { - case "non-urgent": - case "low": - return "low"; - case "urgent": - case "high": - return "high"; - } - } - return "normal"; -}; - -/** - *

Processes a line in text/html or text/plain node

- * - *

Append the line to the content property

- * - * @param {String} line A line to be processed - */ -MailParser.prototype._handleTextLine = function(line) { - - if (["quoted-printable", "base64"].indexOf(this._currentNode.meta.transferEncoding) >= 0 || this._currentNode.meta.textFormat != "flowed") { - if (typeof this._currentNode.content != "string") { - this._currentNode.content = line; - } else { - this._currentNode.content += "\n" + line; - } - } else { - if (typeof this._currentNode.content != "string") { - this._currentNode.content = line; - } else if (this._currentNode.content.match(/[ ]$/)) { - if (this._currentNode.meta.textFormat == "flowed" && this._currentNode.content.match(/(^|\n)-- $/)) { - // handle special case for usenet signatures - this._currentNode.content += "\n" + line; - } else { - if (this._currentNode.meta.textDelSp == "yes") { - this._currentNode.content = this._currentNode.content.replace(/[ ]+$/, ""); - } - this._currentNode.content += line; - } - } else { - this._currentNode.content += "\n" + line; - } - } -}; - -/** - *

Processes a line in an attachment node

- * - *

If a stream is set up for the attachment write the line to the - * stream as a Buffer object, otherwise append it to the content property

- * - * @param {String} line A line to be processed - */ -MailParser.prototype._handleAttachmentLine = function(line) { - if (!this._currentNode.attachment) { - return; - } - if (this._currentNode.stream) { - if (!this._currentNode.streamStarted) { - this._currentNode.streamStarted = true; - this._currentNode.stream.write(new Buffer(line, "binary")); - } else { - this._currentNode.stream.write(new Buffer("\r\n" + line, "binary")); - } - } else if ("content" in this._currentNode) { - if (typeof this._currentNode.content != "string") { - this._currentNode.content = line; - } else { - this._currentNode.content += "\r\n" + line; - } - } -}; - -/** - *

Finalizes a node processing

- * - *

If the node is a text/plain or text/html, convert it to UTF-8 encoded string - * If it is an attachment, convert it to a Buffer or if an attachment stream is - * set up, close the stream

- */ -MailParser.prototype._finalizeContents = function() { - var streamInfo; - - if (this._currentNode.content) { - - if (!this._currentNode.attachment) { - - if (this._currentNode.meta.contentType == "text/html" && !this._currentNode.meta.charset) { - this._currentNode.meta.charset = this._detectHTMLCharset(this._currentNode.content) || this.options.defaultCharset || "iso-8859-1"; - } - - if (this._currentNode.meta.transferEncoding == "quoted-printable") { - this._currentNode.content = mimelib.decodeQuotedPrintable(this._currentNode.content, false, this._currentNode.meta.charset || this.options.defaultCharset || "iso-8859-1"); - if (this._currentNode.meta.textFormat == "flowed") { - if (this._currentNode.meta.textDelSp == "yes") { - this._currentNode.content = this._currentNode.content.replace(/(^|\n)-- \n/g, '$1-- \u0000').replace(/ \n/g, '').replace(/(^|\n)-- \u0000/g, '$1-- \n'); - } else { - this._currentNode.content = this._currentNode.content.replace(/(^|\n)-- \n/g, '$1-- \u0000').replace(/ \n/g, ' ').replace(/(^|\n)-- \u0000/g, '$1-- \n'); - } - } - } else if (this._currentNode.meta.transferEncoding == "base64") { - this._currentNode.content = mimelib.decodeBase64(this._currentNode.content, this._currentNode.meta.charset || this.options.defaultCharset || "iso-8859-1"); - } else { - this._currentNode.content = this._convertStringToUTF8(this._currentNode.content); - } - } else { - if (this._currentNode.meta.transferEncoding == "quoted-printable") { - this._currentNode.content = mimelib.decodeQuotedPrintable(this._currentNode.content, false, "binary"); - } else if (this._currentNode.meta.transferEncoding == "base64") { - - // WTF? if newlines are not removed, the resulting hash is *always* different - this._currentNode.content = new Buffer(this._currentNode.content.toString().replace(/\s+/g, ""), "base64"); - - } else if (this._currentNode.meta.transferEncoding == "uuencode") { - var uuestream = new Streams.UUEStream("binary"); - this._currentNode.content = uuestream.decode(new Buffer(this._currentNode.content, "binary")); - } else { - this._currentNode.content = new Buffer(this._currentNode.content, "binary"); - } - this._currentNode.checksum = crypto.createHash("md5"); - this._currentNode.checksum.update(this._currentNode.content); - this._currentNode.meta.checksum = this._currentNode.checksum.digest("hex"); - this._currentNode.meta.length = this._currentNode.content.length; - } - - } - - if (this._currentNode.stream) { - streamInfo = this._currentNode.stream.end() || {}; - if (streamInfo.checksum) { - this._currentNode.meta.checksum = streamInfo.checksum; - } - if (streamInfo.length) { - this._currentNode.meta.length = streamInfo.length; - } - } -}; - -/** - *

Processes the mime tree

- * - *

Finds text parts and attachments from the tree. If there's several text/plain - * or text/html parts, join these into one

- * - *

Emits "end" when finished

- */ -MailParser.prototype._processMimeTree = function() { - var returnValue = {}, - i, len; - - this.mailData = { - html: [], - text: [], - calendar: [], - attachments: [] - }; - - if (!this.mimeTree.meta.mimeMultipart) { - this._processMimeNode(this.mimeTree, 0); - } else { - this._walkMimeTree(this.mimeTree); - } - - if (this.mailData.html.length) { - for (i = 0, len = this.mailData.html.length; i < len; i++) { - if (!returnValue.html && this.mailData.html[i].content) { - returnValue.html = this.mailData.html[i].content; - } else if (this.mailData.html[i].content) { - returnValue.html = this._concatHTML(returnValue.html, this.mailData.html[i].content); - } - } - } - - if (this.mailData.text.length) { - var len = this.mailData.text.length; - // if we have both html and text, process text till the length of html assuming its alternative for html - if (this.mailData.html.length) { - len = Math.min(len, this.mailData.html.length); - } - for (i = 0, len; i < len; i++) { - if (!returnValue.text && this.mailData.text[i].content) { - returnValue.text = this.mailData.text[i].content; - } else if (this.mailData.text[i].content) { - returnValue.text += this.mailData.text[i].content; - } - } - // all remaining text contents if present assumed as additional content and concatenated with html as well as text - for (len = this.mailData.text.length; i < len; i++) { - if (this.mailData.text[i].content) { - // concatenate to both text and html so that text and html are always same content - // user should be able to chose any one of them - returnValue.text += this.mailData.text[i].content; - returnValue.html += this.mailData.text[i].content; - } - } - } - - if (this.mailData.calendar.length) { - returnValue.alternatives = []; - for (i = 0, len = this.mailData.calendar.length; i < len; i++) { - returnValue.alternatives.push(this.mailData.calendar[i].content); - } - } - - returnValue.headers = this.mimeTree.parsedHeaders; - - if (this.mimeTree.subject) { - returnValue.subject = this.mimeTree.subject; - } - - if (this.mimeTree.references) { - returnValue.references = this.mimeTree.references; - } - - if (this.mimeTree.messageId) { - returnValue.messageId = this.mimeTree.messageId; - } - - if (this.mimeTree.inReplyTo) { - returnValue.inReplyTo = this.mimeTree.inReplyTo; - } - - if (this.mimeTree.priority) { - returnValue.priority = this.mimeTree.priority; - } - - if (this.mimeTree.from) { - returnValue.from = this.mimeTree.from; - } - - if (this.mimeTree.replyTo) { - returnValue.replyTo = this.mimeTree.replyTo; - } - - if (this.mimeTree.to) { - returnValue.to = this.mimeTree.to; - } - - if (this.mimeTree.cc) { - returnValue.cc = this.mimeTree.cc; - } - - if (this.mimeTree.bcc) { - returnValue.bcc = this.mimeTree.bcc; - } - - if (this.mimeTree.meta.date) { - returnValue.date = this.mimeTree.meta.date; - } - - if (this.mimeTree.meta.receivedDate) { - returnValue.receivedDate = this.mimeTree.meta.receivedDate; - } - - if (this.mailData.attachments.length) { - returnValue.attachments = []; - for (i = 0, len = this.mailData.attachments.length; i < len; i++) { - returnValue.attachments.push(this.mailData.attachments[i].content); - } - } - - if (typeof setImmediate == "function") { - setImmediate(this.emit.bind(this, "end", returnValue)); - } else { - process.nextTick(this.emit.bind(this, "end", returnValue)); - } -}; - -/** - *

Walks the mime tree and runs processMimeNode on each node of the tree

- * - * @param {Object} node A mime tree node - * @param {Number} [level=0] current depth - */ -MailParser.prototype._walkMimeTree = function(node, level) { - level = level || 1; - for (var i = 0, len = node.childNodes.length; i < len; i++) { - this._processMimeNode(node.childNodes[i], level, node.meta.mimeMultipart); - this._walkMimeTree(node.childNodes[i], level + 1); - } -}; - -/** - *

Processes of a node in the mime tree

- * - *

Pushes the node into appropriate this.mailData array (text/html to this.mailData.html array etc)

- * - * @param {Object} node A mime tree node - * @param {Number} [level=0] current depth - * @param {String} mimeMultipart Type of multipart we are dealing with (if any) - */ -MailParser.prototype._processMimeNode = function(node, level, mimeMultipart) { - var i, len; - - level = level || 0; - - if (!node.attachment) { - switch (node.meta.contentType) { - case "text/html": - if (mimeMultipart == "mixed" && this.mailData.html.length) { - for (i = 0, len = this.mailData.html.length; i < len; i++) { - if (this.mailData.html[i].level == level) { - this._joinHTMLNodes(this.mailData.html[i], node.content); - return; - } - } - } - this.mailData.html.push({ - content: this._updateHTMLCharset(node.content || ""), - level: level - }); - return; - case "text/plain": - this.mailData.text.push({ - content: node.content || "", - level: level - }); - return; - case "text/calendar": - if (node.content) { - node.meta.content = node.content; - } - this.mailData.calendar.push({ - content: node.meta || {}, - level: level - }); - return; - } - } else { - node.meta = node.meta || {}; - if (node.content) { - node.meta.content = node.content; - } - this.mailData.attachments.push({ - content: node.meta || {}, - level: level - }); - - if (this.options.showAttachmentLinks && mimeMultipart == "mixed" && this.mailData.html.length) { - for (i = 0, len = this.mailData.html.length; i < len; i++) { - if (this.mailData.html[i].level == level) { - this._joinHTMLAttachment(this.mailData.html[i], node.meta); - return; - } - } - } - } -}; - -/** - *

Joins two HTML blocks by removing the header of the added element

- * - * @param {Object} htmlNode Original HTML contents node object - * @param {String} newHTML HTML text to add to the original object node - */ -MailParser.prototype._joinHTMLNodes = function(htmlNode, newHTML) { - var inserted = false; - - // process new HTML - newHTML = (newHTML || "").toString("utf-8").trim(); - - // remove doctype from the beginning - newHTML = newHTML.replace(/^\s*<\!doctype( [^>]*)?>/gi, ""); - - // remove and blocks - newHTML = newHTML.replace(/]*)?>(.*)<\/head( [^>]*)?>/gi, ""). - replace(/<\/?html( [^>]*)?>/gi, ""). - trim(); - - // keep only text between tags (if ]*)?>(.*)<\/body( [^>]*)?>/gi, function(match, body) { - newHTML = body.trim(); - }); - - htmlNode.content = (htmlNode.content || "").toString("utf-8").trim(); - - htmlNode.content = htmlNode.content.replace(/<\/body( [^>]*)?>/i, function(match) { - inserted = true; - return "
\n" + newHTML + match; - }); - - if (!inserted) { - htmlNode.content += "
\n" + newHTML; - } -}; - -/** - *

Adds filename placeholder to the HTML if needed

- * - * @param {Object} htmlNode Original HTML contents node object - * @param {String} attachment Attachment meta object - */ -MailParser.prototype._joinHTMLAttachment = function(htmlNode, attachment) { - var inserted = false, - fname = attachment.generatedFileName.replace(//g, ">").replace(/"/g, """), - newHTML; - - newHTML = "\n"; - - htmlNode.content = (htmlNode.content || "").toString("utf-8").trim(); - - htmlNode.content = htmlNode.content.replace(/<\/body\b[^>]*>/i, function(match) { - inserted = true; - return "
\n" + newHTML + match; - }); - - if (!inserted) { - htmlNode.content += "
\n" + newHTML; - } -}; - -/** - *

Joins two HTML blocks by removing the header of the added element

- * - * @param {Sting} htmlNode Original HTML contents - * @param {String} newHTML HTML text to add to the original object node - * @return {String} Joined HTML - */ -MailParser.prototype._concatHTML = function(firstNode, secondNode) { - var headerNode = "", - htmlHeader = ""; - - firstNode = (firstNode || "").toString("utf-8"); - secondNode = (secondNode || "").toString("utf-8"); - - if (!secondNode) { - return firstNode; - } - if (!firstNode) { - return secondNode; - } - - if (firstNode.substr(0, 1024).replace(/\r?\n/g, "\u0000").match(/^[\s\u0000]*(<\!doctype\b[^>]*?>)?[\s\u0000]*<(html|head)\b[^>]*?>/i)) { - headerNode = firstNode; - } else if (secondNode.substr(0, 1024).replace(/\r?\n/g, "\u0000").match(/^[\s\u0000]*(<\!doctype\b[^>]*?>)?[\s\u0000]*<(html|head)\b[^>]*?>/i)) { - headerNode = secondNode; - } - - if (headerNode) { - headerNode.replace(/\r?\n/g, "\u0000").replace(/^[\s\u0000]*(<\!doctype\b[^>]*?>)?[\s\u0000]*<(html|head)\b[^>]*>.*?<\/(head)\b[^>]*>(.*?]*>)?/i, function(h) { - var doctype = h.match(/^[\s\u0000]*(<\!doctype\b[^>]*?>)/i), - html = h.match(/]*?>/i), - head = h.match(/]*?>/i), - body = h.match(/]*?>/i); - - doctype = doctype && doctype[1] && doctype[1] + "\n" || ""; - html = html && html[0] || ""; - head = head && head[0] || ""; - body = body && body[0] || ""; - h = h.replace(/<[\!\/]?(doctype|html|head|body)\b[^>]*?>/ig, "\u0000").replace(/\u0000+/g, "\n").trim(); - - htmlHeader = doctype + html + "\n" + head + (h ? h + "\n" : "") + "\n" + body + "\n"; - }); - } - - firstNode = firstNode.replace(/\r?\n/g, "\u0000"). - replace(/[\s\u0000]*]*>.*?<\/(head|body)\b[^>]*>/gi, ""). - replace(/[\s\u0000]*<[\!\/]?(doctype|html|body)\b[^>]*>[\s\u0000]*/gi, ""). - replace(/\u0000/g, "\n"); - - secondNode = secondNode.replace(/\r?\n/g, "\u0000"). - replace(/[\s\u0000]*]*>.*?<\/(head|body)\b[^>]*>/gi, ""). - replace(/[\s\u0000]*<[\!\/]?(doctype|html|body)\b[^>]*>[\s\u0000]*/gi, ""). - replace(/\u0000/g, "\n"); - - return htmlHeader + firstNode + secondNode + (htmlHeader ? (firstNode || secondNode ? "\n" : "") + "\n" : ""); -}; - -/** - *

Converts a string from one charset to another

- * - * @param {Buffer|String} value A String to be converted - * @param {String} fromCharset source charset - * @param {String} [toCharset="UTF-8"] destination charset - * @returns {Buffer} Converted string as a Buffer (or SlowBuffer) - */ -MailParser.prototype._convertString = function(value, fromCharset, toCharset) { - toCharset = (toCharset || "utf-8").toUpperCase(); - fromCharset = (fromCharset || "utf-8").toUpperCase(); - - value = typeof value == "string" ? new Buffer(value, "binary") : value; - - if (toCharset == fromCharset) { - return value; - } - - value = encodinglib.convert(value, toCharset, fromCharset); - - return value; -}; - -/** - *

Converts a string to UTF-8

- * - * @param {String} value String to be encoded - * @returns {String} UTF-8 encoded string - */ -MailParser.prototype._convertStringToUTF8 = function(value) { - value = this._convertString(value, this._currentNode.meta.charset || this.options.defaultCharset || "iso-8859-1").toString("utf-8"); - return value; -}; - -/** - *

Encodes a header string to UTF-8

- * - * @param {String} value String to be encoded - * @returns {String} UTF-8 encoded string - */ -MailParser.prototype._encodeString = function(value) { - value = this._replaceMimeWords(this._convertStringToUTF8(value)); - return value; -}; - -/** - *

Replaces mime words in a string with UTF-8 encoded strings

- * - * @param {String} value String to be converted - * @returns {String} converted string - */ -MailParser.prototype._replaceMimeWords = function(value) { - return value. - replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, "$1"). // join mimeWords - replace(/\=\?[^?]+\?[QqBb]\?[^?]*\?=/g, (function(a) { - return mimelib.decodeMimeWord(a.replace(/\s/g, '')); - }).bind(this)); -}; - -/** - *

Removes enclosing quotes ("", '', <>) from a string

- * - * @param {String} value String to be converted - * @returns {String} converted string - */ -MailParser.prototype._trimQuotes = function(value) { - value = (value || "").trim(); - if ((value.charAt(0) == '"' && value.charAt(value.length - 1) == '"') || - (value.charAt(0) == "'" && value.charAt(value.length - 1) == "'") || - (value.charAt(0) == "<" && value.charAt(value.length - 1) == ">")) { - value = value.substr(1, value.length - 2); - } - return value; -}; - -/** - *

Generates a context unique filename for an attachment

- * - *

If a filename already exists, append a number to it

- * - *
    - *
  • file.txt
  • - *
  • file-1.txt
  • - *
  • file-2.txt
  • - *
- * - * @param {String} fileName source filename - * @param {String} contentType source content type - * @returns {String} generated filename - */ -MailParser.prototype._generateFileName = function(fileName, contentType) { - var ext, defaultExt = "", - fileRootName; - - if (contentType) { - defaultExt = mime.extension(contentType); - defaultExt = defaultExt ? "." + defaultExt : ""; - } - - fileName = fileName || "attachment" + defaultExt; - - // remove path if it is included in the filename - fileName = fileName.toString().split(/[\/\\]+/).pop().replace(/^\.+/, "") || "attachment"; - fileRootName = fileName.replace(/(?:\-\d+)+(\.[^.]*)$/, "$1") || "attachment"; - - if (fileRootName in this._fileNames) { - this._fileNames[fileRootName]++; - ext = fileName.substr((fileName.lastIndexOf(".") || 0) + 1); - if (ext == fileName) { - fileName += "-" + this._fileNames[fileRootName]; - } else { - fileName = fileName.substr(0, fileName.length - ext.length - 1) + "-" + this._fileNames[fileRootName] + "." + ext; - } - } else { - this._fileNames[fileRootName] = 0; - } - - return fileName; -}; - - -/** - *

Replaces character set to UTF-8 in HTML <meta> tags

- * - * @param {String} HTML html contents - * @returns {String} updated HTML - */ -MailParser.prototype._updateHTMLCharset = function(html) { - - html = html.replace(/\n/g, "\u0000"). - replace(/]*>/gi, function(meta) { - if (meta.match(/http\-equiv\s*=\s*"?content\-type/i)) { - return ''; - } - if (meta.match(/\scharset\s*=\s*['"]?[\w\-]+["'\s>\/]/i)) { - return ''; - } - return meta; - }). - replace(/\u0000/g, "\n"); - - return html; -}; - -/** - *

Detects the charset of an HTML file

- * - * @param {String} HTML html contents - * @returns {String} Charset for the HTML - */ -MailParser.prototype._detectHTMLCharset = function(html) { - var charset, input, meta; - - if (typeof html != "string") { - html = html.toString("ascii"); - } - - if ((meta = html.match(/]*?>/i))) { - input = meta[0]; - } - - if (input) { - charset = input.match(/charset\s?=\s?([a-zA-Z\-_:0-9]*);?/); - if (charset) { - charset = (charset[1] || "").trim().toLowerCase(); - } - } - - if (!charset && (meta = html.match(/ { + let promise; + if (!callback) { + promise = new Promise((resolve, reject) => { + callback = callbackPromise(resolve, reject); + }); + } + + let mail = { + attachments: [] + }; + + let parser = new MailParser(); + + parser.on('headers', headers => { + mail.headers = headers; + }); + + parser.on('data', data => { + if (data.type === 'text') { + Object.keys(data).forEach(key => { + if (['text', 'html', 'textAsHtml'].includes(key)) { + mail[key] = data[key]; + } + }); + } + + if (data.type === 'attachment') { + mail.attachments.push(data); + + let chunks = []; + let chunklen = 0; + data.content.on('readable', () => { + let chunk; + while ((chunk = data.content.read()) !== null) { + chunks.push(chunk); + chunklen += chunk.length; + } + }); + + data.content.on('end', () => { + data.content = Buffer.concat(chunks, chunklen); + data.release(); + }); + } + }); + + parser.on('end', () => { + ['subject', 'references', 'date', 'to', 'from', 'to', 'cc', 'bcc', 'message-id', 'in-reply-to', 'reply-to'].forEach(key => { + if (mail.headers.has(key)) { + mail[key.replace(/\-([a-z])/g, (m, c) => c.toUpperCase())] = mail.headers.get(key); + } + }); + + parser.updateImageLinks((attachment, done) => done(false, 'data:' + attachment.contentType + ';base64,' + attachment.content.toString('base64')), (err, html) => { + if (err) { + return callback(err); + } + mail.html = html; + + callback(null, mail); + }); + }); + + if (typeof input === 'string') { + parser.end(Buffer.from(input)); + } else if (Buffer.isBuffer(input)) { + parser.end(input); + } else { + input.pipe(parser); + } + + return promise; +}; + +function callbackPromise(resolve, reject) { + return function (...args) { + let err = args.shift(); + if (err) { + reject(err); + } else { + resolve(...args); + } + }; +} diff --git a/lib/stream-hash.js b/lib/stream-hash.js new file mode 100644 index 0000000..3f9b441 --- /dev/null +++ b/lib/stream-hash.js @@ -0,0 +1,28 @@ +'use strict'; + +const crypto = require('crypto'); +const Transform = require('stream').Transform; + +class StreamHash extends Transform { + constructor(attachment, algo) { + super(); + this.attachment = attachment; + this.algo = (algo || 'md5').toLowerCase(); + this.hash = crypto.createHash(algo); + this.byteCount = 0; + } + + _transform(chunk, encoding, done) { + this.hash.update(chunk); + this.byteCount += chunk.length; + done(null, chunk); + } + + _flush(done) { + this.attachment.checksum = this.hash.digest('hex'); + this.attachment.size = this.byteCount; + done(); + } +} + +module.exports = StreamHash; diff --git a/lib/streams.js b/lib/streams.js deleted file mode 100644 index 4ea5291..0000000 --- a/lib/streams.js +++ /dev/null @@ -1,213 +0,0 @@ -"use strict"; - -var Stream = require('stream').Stream, - utillib = require('util'), - mimelib = require("mimelib"), - encodinglib = require("encoding"), - crypto = require("crypto"), - uue = require('uue'); - -module.exports.Base64Stream = Base64Stream; -module.exports.QPStream = QPStream; -module.exports.BinaryStream = BinaryStream; -module.exports.UUEStream = UUEStream; - -function Base64Stream() { - Stream.call(this); - this.writable = true; - - this.checksum = crypto.createHash("md5"); - this.length = 0; - - this.current = ""; -} -utillib.inherits(Base64Stream, Stream); - -Base64Stream.prototype.write = function(data) { - this.handleInput(data); - return true; -}; - -Base64Stream.prototype.end = function(data) { - this.handleInput(data); - this.emit("end"); - return { - length: this.length, - checksum: this.checksum.digest("hex") - }; -}; - -Base64Stream.prototype.handleInput = function(data) { - if (!data || !data.length) { - return; - } - - data = (data || "").toString("utf-8"); - - var remainder = 0; - this.current += data.replace(/[^\w\+\/=]/g, ''); - var buffer = new Buffer(this.current.substr(0, this.current.length - this.current.length % 4), "base64"); - if (buffer.length) { - this.length += buffer.length; - this.checksum.update(buffer); - this.emit("data", buffer); - } - this.current = (remainder = this.current.length % 4) ? this.current.substr(-remainder) : ""; -}; - -function QPStream(charset) { - Stream.call(this); - this.writable = true; - - this.checksum = crypto.createHash("md5"); - this.length = 0; - - this.charset = charset || "UTF-8"; - this.current = undefined; -} -utillib.inherits(QPStream, Stream); - -QPStream.prototype.write = function(data) { - this.handleInput(data); - return true; -}; - -QPStream.prototype.end = function(data) { - this.handleInput(data); - this.flush(); - this.emit("end"); - return { - length: this.length, - checksum: this.checksum.digest("hex") - }; -}; - -QPStream.prototype.handleInput = function(data) { - if (!data || !data.length) { - return; - } - - data = (data || "").toString("utf-8"); - if (data.match(/^\r\n/)) { - data = data.substr(2); - } - - if (typeof this.current != "string") { - this.current = data; - } else { - this.current += "\r\n" + data; - } -}; - -QPStream.prototype.flush = function() { - var buffer = mimelib.decodeQuotedPrintable(this.current, false, this.charset); - - if (this.charset.toLowerCase() == "binary") { - // do nothing - } else if (this.charset.toLowerCase() != "utf-8") { - buffer = encodinglib.convert(buffer, "utf-8", this.charset); - } else { - buffer = new Buffer(buffer, "utf-8"); - } - - this.length += buffer.length; - this.checksum.update(buffer); - - this.emit("data", buffer); -}; - -function BinaryStream(charset) { - Stream.call(this); - this.writable = true; - - this.checksum = crypto.createHash("md5"); - this.length = 0; - - this.charset = charset || "UTF-8"; - this.current = ""; -} -utillib.inherits(BinaryStream, Stream); - -BinaryStream.prototype.write = function(data) { - if (data && data.length) { - this.length += data.length; - this.checksum.update(data); - this.emit("data", data); - } - return true; -}; - -BinaryStream.prototype.end = function(data) { - if (data && data.length) { - this.emit("data", data); - } - this.emit("end"); - return { - length: this.length, - checksum: this.checksum.digest("hex") - }; -}; - -// this is not a stream, it buffers data and decodes after end -function UUEStream(charset) { - Stream.call(this); - this.writable = true; - - this.checksum = crypto.createHash("md5"); - this.length = 0; - this.buf = []; - this.buflen = 0; - - this.charset = charset || "UTF-8"; - this.current = undefined; -} -utillib.inherits(UUEStream, Stream); - -UUEStream.prototype.write = function(data) { - this.buf.push(data); - this.buflen += data.length; - return true; -}; - -UUEStream.prototype.end = function(data) { - if (data) { - this.write(data); - } - - this.flush(); - - this.emit("end"); - return { - length: this.length, - checksum: this.checksum.digest("hex") - }; -}; - -UUEStream.prototype.flush = function() { - var buffer = this.decode(Buffer.concat(this.buf, this.buflen)); - - this.length += buffer.length; - this.checksum.update(buffer); - - this.emit("data", buffer); -}; - -UUEStream.prototype.decode = function(buffer) { - var filename; - - var re = /^begin [0-7]{3} (.*)/; - filename = buffer.slice(0, Math.min(buffer.length, 1024)).toString().match(re) || ''; - if (!filename) { - return new Buffer(0); - } - - buffer = uue.decodeFile(buffer.toString('ascii').replace(/\r\n/g, '\n'), filename[1]); - - if (this.charset.toLowerCase() == "binary") { - // do nothing - } else if (this.charset.toLowerCase() != "utf-8") { - buffer = encodinglib.convert(buffer, "utf-8", this.charset); - } - - return buffer; -}; diff --git a/package.json b/package.json index 1b578b2..187c093 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,40 @@ { "name": "mailparser", - "description": "Asynchronous and non-blocking parser for mime encoded e-mail messages", - "version": "0.6.2", - "author": "Andris Reinman", - "maintainers": [{ - "name": "andris", - "email": "andris@node.ee" - }], - "repository": { - "type": "git", - "url": "http://github.com/andris9/mailparser.git" - }, + "version": "2.0.0", + "description": "Parse e-mails", + "main": "index.js", "scripts": { - "test": "nodeunit test/" + "test": "grunt" }, - "main": "./lib/mailparser", - "license": "MIT", + "author": "Andris Reinman", + "license": "EUPL-1.1", "dependencies": { - "mimelib": "^0.3.0", - "encoding": "^0.1.12", - "mime": "^1.3.4", - "uue": "^3.1.0" + "addressparser": "1.0.1", + "he": "1.1.1", + "html-to-text": "3.1.0", + "iconv-lite": "0.4.15", + "libmime": "3.1.0", + "mailsplit": "3.1.2", + "marked": "0.3.6" }, "devDependencies": { - "nodeunit": "^0.10.2" + "eslint-config-nodemailer": "^1.0.0", + "grunt": "^1.0.1", + "grunt-cli": "^1.2.0", + "grunt-contrib-nodeunit": "^1.0.0", + "grunt-eslint": "^19.0.0", + "libbase64": "^0.1.0", + "libqp": "^1.1.0", + "random-message": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/andris9/mailparser.git" }, - "keywords": [ - "e-mail", - "mime", - "parser" - ] + "bugs": { + "url": "https://github.com/andris9/mailparser/issues" + } } diff --git a/test/mixed.eml b/test/fixtures/mixed.eml similarity index 100% rename from test/mixed.eml rename to test/fixtures/mixed.eml diff --git a/test/nested.eml b/test/fixtures/nested.eml similarity index 96% rename from test/nested.eml rename to test/fixtures/nested.eml index 52323a0..0b01f35 100644 --- a/test/nested.eml +++ b/test/fixtures/nested.eml @@ -37,6 +37,8 @@ Content-Type: text/html;

------=_NextPart_002_0D4A_01CE140D.19527DD0-- +------=_NextPart_001_0D49_01CE140D.19527DD0-- + ------=_NextPart_000_0D48_01CE140D.19527DD0 Content-Type: multipart/alternative; boundary="===============1276485360==" MIME-Version: 1.0 diff --git a/test/fixtures/nodemailer.eml b/test/fixtures/nodemailer.eml new file mode 100644 index 0000000..6bf5094 --- /dev/null +++ b/test/fixtures/nodemailer.eml @@ -0,0 +1,616 @@ +Delivered-To: andris.reinman@gmail.com +Received: by 10.28.50.2 with SMTP id y2csp233403wmy; + Thu, 13 Oct 2016 04:39:49 -0700 (PDT) +X-Received: by 10.25.37.18 with SMTP id l18mr9511740lfl.88.1476358789184; + Thu, 13 Oct 2016 04:39:49 -0700 (PDT) +Return-Path: +Received: from srs1.zonevs.eu (srs1.zonevs.eu. [217.146.68.191]) + by mx.google.com with ESMTPS id l202si1012799lfg.293.2016.10.13.04.39.49 + for + (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); + Thu, 13 Oct 2016 04:39:49 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of srs0=63fc=v7=kreata.ee=andris@srs1.zonevs.eu designates 217.146.68.191 as permitted sender) client-ip=217.146.68.191; +Authentication-Results: mx.google.com; + dkim=pass header.i=@srs1.zonevs.eu; + spf=pass (google.com: best guess record for domain of srs0=63fc=v7=kreata.ee=andris@srs1.zonevs.eu designates 217.146.68.191 as permitted sender) smtp.mailfrom=SRS0=63fc=V7=kreata.ee=andris@srs1.zonevs.eu; + dmarc=fail (p=NONE dis=NONE) header.from=kreata.ee +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=srs1.zonevs.eu; + q=dns/txt; s=oct2016; bh=xKHKChGY0vTH8NsecmXwA0OqbinKOXeQbaC2UYp2BAM=; + h=from:subject:date:message-id:to:mime-version:content-type; + b=Ve18ogdCAG+7WZYkJPOewe1hKjhN4k9unz7bVHMXd6+1CQDRUkLCArQZJzSKxkM481nzXfjFn + bI8qOuQL8mRk/8fAjYhxLgnr/3SyVIOhCnXxjdQkRzgouZyl42hqD0gIaCxu9uodtQrp2pbKvyl + e+3sG+LhcdJmsPguOfILn14j+irinPSWrospC8PBIDTsUwO8DCyPqSlOADbW0B6TRUHWMf4XUX4 + W8TH61H1ZI3Xu3k0bvX7rsGHZjsy8dcshcnfYENLCLep8fsQMaB15EErc3RXycBX7CBd0iU1l50 + pYpUFd6bZehCF0ipTOgA7IJ7ZPafaH0YTU8wRntXOwbg== +Received: from host29.guest.zone.eu [217.146.66.6] + by srs1.zonevs.eu (ZoneMTA Forwarder) with ESMTP id 157bdd754f70005750.002 + for ; + Thu, 13 Oct 2016 11:39:48 +0000 +Content-Type: multipart/mixed; + boundary="----sinikael-?=_1-14763587882000.8241290969717285" +X-Laziness-Level: 1000 +From: Andris Kreata +To: Andris Reinman , andris.reinman@gmail.com +Subject: Nodemailer is unicode friendly =?UTF-8?Q?=E2=9C=94?= + (1476358788189) +Message-ID: <012d606e-3550-2d94-b566-6cd996de88e3@kreata.ee> +X-Mailer: nodemailer (2.6.0; +http://nodemailer.com/; + SMTP/2.7.2[client:2.12.0]) +Date: Thu, 13 Oct 2016 11:39:48 +0000 +MIME-Version: 1.0 +X-Zone-Spam-Resolution: no action +X-Zone-Spam-Status: No, score=0.408099, required=15, tests=[MIME_GOOD=-0.1, + R_MISSING_CHARSET=2.5, DMARC_POLICY_SOFTFAIL=0.1, MIME_UNKNOWN=0.1, + R_DKIM_NA=0, BAYES_HAM=-2.1919] +X-Original-Sender: andris@kreata.ee +X-Zone-Forwarded-For: andris@kreata.ee +X-Zone-Forwarded-To: andris.reinman@gmail.com + +------sinikael-?=_1-14763587882000.8241290969717285 +Content-Type: multipart/alternative; + boundary="----sinikael-?=_2-14763587882000.8241290969717285" + +------sinikael-?=_2-14763587882000.8241290969717285 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Hello to myself! +------sinikael-?=_2-14763587882000.8241290969717285 +Content-Type: text/watch-html +Content-Transfer-Encoding: 7bit + +Hello to myself +------sinikael-?=_2-14763587882000.8241290969717285 +Content-Type: multipart/related; type="text/html"; + boundary="----sinikael-?=_5-14763587882000.8241290969717285" + +------sinikael-?=_5-14763587882000.8241290969717285 +Content-Type: text/html +Content-Transfer-Encoding: quoted-printable + +

Hello to myself

Here's = +a nyan cat for you as an embedded attachment:

+------sinikael-?=_5-14763587882000.8241290969717285 +Content-Type: image/png; name=image.png +Content-ID: +Content-Disposition: attachment; filename=image.png +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lE +QVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQ +AAAAAElFTkSuQmCC +------sinikael-?=_5-14763587882000.8241290969717285 +Content-Type: image/gif; name="nyan cat =?UTF-8?Q?=E2=9C=94=2Egif?=" +Content-ID: +Content-Disposition: attachment; + filename*0*=utf-8''nyan%20cat%20%E2%9C%94.gif +Content-Transfer-Encoding: base64 + +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 +1Yyw8Iy+SAjcCE3euFPgyFXroI2Pd3XgdI5pl45nNWCwaFPuqE3w2DnXmAl0RRnRqHtC6F8C+GAx +I45Sp1ZOU1zrF1+7NHNQZJCzhpD1CFqdyAAMqUsOiQn7GDf9+FMKWZGclC/692rARjLyOEEdqVQf +KYtWKJBatWJJE3UZlZJpIl34UH0iOXswCQwbSTY0uSU2SX1j9QU9SJAxCZFp0n7QVRlBaXWr/6gu +GXkJPakkSjlc49ha8LNpINlbO9mLm1CVt9VZMhkLTfl+8JeLIteVXsmP52dJgCWWSKkJZYlokgSM +WKeWazl339d7YWkjJykKcxlk9yhzeJmX7eZziNcEW0eMn4WVveZrW5lcRmmYAtGWyccEi0mPthCY +lil2aCl0k0mZfNeZWgkFU4kJQblbT3mXoSmahUeaiQkArJULZllsdilGremaBwSbJDYFs7kGxFd8 +n9lthSmarFeX3kBcFImYdjec0RKV7HacjeWbyumY0kl+zslKxUmZ13l91MkRqUldq4mbJKk6ujkJ +3YmddVOdtEmXfBmZipebrpme05cp+zWYcf/ikghYnjr1jYzZV3v5Xe3FXtlZAheZAtAJImO5WLyJ +GfrlXuOJYTkJPNvZNwsqQgE6IQ9KoLcpodcFZL9mnv6pmSDGcxyKKvcZoaugn/jHn1DFjP/ZXRl6 +HRuaX/ipYB+aZZCGb+gYoww6o8tRo4NTjtiVoxQqn/oYl2wJpM8FXifKkkhwoCiQoAdyoZnFpIf3 +RUTKT4KpomDook93nruJpVm6QFvKf7XJdFQaYfRZprBzoyrglCG3pszVpm7qoHCKoGmap05VoXlp +p3eqJ3x6AnLaoVsIplonpoAaqKTipQDonq1nqF8aov2pm4vKqG0njSsKqd6ZKtVYkNvgHMH/ialo +VKBDxanqiSqfepShCma1SaoiZqqFqqrrhmM+amSoCqsjJqt7amO1SmS3OmWXqqt4wqu5mm2e+qvf +oKTfMKrECkaSSoDH+mQ8qhXBaiev+qylGq3016vDo6zYwKzY4KwIkWnMqT11dqxnmYyP2qXfWq1u +ca0zQa4HYa4bpzz2aqKAtmzeR6a3hqimdVqj5q90lq7DGhX52nGZyq/zdK4CCrDahVkD67CIkbAJ +d2AWm3ErJ5TWRbE06qeiRj3ZChIZq3JoVLI6t7FEoK4bywSrOqIlQa8GgbIqgbEG26DTya5shrM+ +RqkvqlgTe6+KQbMURrQLe5MNK7QP67Nh/wq0IsuyJHuzBAs4Rquy/eqxQQqyEBW0+uom2vamuJZs ++4q0kJSVvcm0iQqX8tp1I4t+3SCek/e2R8ux8GG24Adq4EoJpwkiMpuqXwC3m9dsf8ewZfuYtrmj +JemXVjpsbSuc5AC4TGq4kAmQdSu5aoakGvmXbAu1bvu31xa5Yju4ZFu5oXu3HpK3k7C3B9K36yq3 +/xq4X5uYzBa7roa2AYuN62hArIusrntsqFa6stsI8VGapwuvmbu4RzdCB6etYPFxpqpKluuWmCuV +mmsyN9dCzNu8MAellXJviUt410tC2fsVzsut0XcoL4tZ4btB4xsV5QufMuK9IsqI63tB7f+7FO/L +vYkiv5UKvsr7bverFPmrs/YWQahbp//LbwHsEwOsqf5nwMabb/XbQNPXcgrbsik7EQ1MuawJsfQV +MqqLngl8cZc7tY7btcSLwi8HdOYbcFp7HyE8pj7ycRU8txastCy0vQRMdtNbIzE8wQlUwxt7wyqs +I+b2vEJBp1FWvSY3wihXwlhba1F8uFi7wcLXwbYbsRnzw06Mc1CMw7Y1xfVZsxRhxcrHlT2soExs +c12MvV9cxKarsWMrxxJhxoQLlS9cHjEMCm06hBhMt6hEiPOrCXvsqiYcm5D3xzeTj4S8xhjTxz+o +yIhFjQdcyLlzyGf7e5IceB4MimdHokb/xLNhbHybbFmUHMGWYMnHI8qTe3uljEmn/L0PubbWy8pS +TMpzfMeB3MmniLutOm22TMWunMujS5iMB1KfnLswuwy7S6xnmi15Byia2My6+szYEs1zMs2Nu8DW +fC3YfCbazLkLzLv6i3dlt0WTSM2w2s3W8s24EM4HW6bs7MLHrHfpvM0BPM98ErdfYIh1KIbfVDT8 +7AX+7HgAHdCLDLuUV9AJddBSKi8D3QUM3dAOPaGTDLqiMdF7WNFGetFTXM8aHYgc7YXyp9DdENIo +XU9I7NFg7LcEndIw/TArjcWYPMojfdMVpM/jsrs43dOjotPiwtM+PdQZCtQ83J1EndRw/+zSDsxL +Qq3UUE3GntnC8PLUUZ3Ul1nMb4XPyHnVPZ3VgLzVudp8LDvTRyrGb8zBUSjKRt10ZknWYNnU5hDX +cj2pNc3Uau3Ub7192WrWrnTXkbrDp1pbbe1Be4196ge/rIbW0huZ2ueob3fYzJPY5bwLdJ3Xh8rW +g2rOY83XZU3VRXDZV7zWgE3Ogi3QB9gB/dcq/xenqd2Qr+1grU0msz2lsZ1LtY0Bqx0quU0vSijW +hgK9vb0hwy3cT1iUwd0nBfjAv92ixz2Sz42mk6Kdzd2u0T2Qyc2ltw1B1e3a3Y3b263by83a4e3b +2f2F6J3e6r3e7N3e7v3e8B3f8j3f9CZd3/Z93/id3/q93/zd3/793wAe4AI+4ARe4AZ+4Aie4Aq+ +4NqUAAA7 +------sinikael-?=_5-14763587882000.8241290969717285-- + +------sinikael-?=_2-14763587882000.8241290969717285-- + +------sinikael-?=_1-14763587882000.8241290969717285 +Content-Type: text/plain; name=notes.txt +Content-Disposition: attachment; filename=notes.txt +Content-Transfer-Encoding: 7bit + +Some notes about this e-mail +------sinikael-?=_1-14763587882000.8241290969717285-- diff --git a/test/mail-parser-test.js b/test/mail-parser-test.js new file mode 100644 index 0000000..574b65f --- /dev/null +++ b/test/mail-parser-test.js @@ -0,0 +1,1548 @@ +'use strict'; + +const MailParser = require('..').MailParser; +const iconv = require('iconv-lite'); +const fs = require('fs'); + +exports['General tests'] = { + 'Many chunks': test => { + let encodedText = 'Content-Type: text/plain; charset=utf-8\r\n' + + '\r\n' + + 'ÕÄ\r\n' + + 'ÖÜ', // \r\nÕÄÖÜ + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + + mailparser.on('data', () => false); + + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄ\nÖÜ'); + test.done(); + }); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + mailparser.end(); + }, + + 'Many chunks - split line endings': test => { + let chunks = [ + 'Content-Type: text/plain; charset=utf-8\r', + '\nSubject: Hi Mom\r\n\r\n', + 'hello' + ]; + + test.expect(1); + let mailparser = new MailParser(); + + let writeNextChunk = function () { + let chunk = chunks.shift(); + if (chunk) { + mailparser.write(chunk, 'utf8'); + if (typeof setImmediate === 'function') { + setImmediate(writeNextChunk); + } else { + process.nextTick(writeNextChunk); + } + } else { + mailparser.end(); + } + }; + + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'hello'); + test.done(); + }); + + if (typeof setImmediate === 'function') { + setImmediate(writeNextChunk); + } else { + process.nextTick(writeNextChunk); + } + }, + + 'Headers only': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\n' + + 'Subject: ÕÄÖÜ', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.headers.get('subject'), 'ÕÄÖÜ'); + test.done(); + }); + }, + + 'Body only': test => { + let encodedText = '\r\n' + + '===', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, '==='); + test.done(); + }); + }, + + 'Different line endings': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\n' + + 'Subject: ÕÄÖÜ\n' + + '\n' + + '1234\r\n' + + 'ÕÄÖÜ\r\n' + + 'ÜÖÄÕ\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(2); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.headers.get('subject'), 'ÕÄÖÜ'); + test.equal(mailparser.text, '1234\nÕÄÖÜ\nÜÖÄÕ\n1234'); + test.done(); + }); + }, + + 'Headers event': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + 'X-Test: =?UTF-8?Q?=C3=95=C3=84?= =?UTF-8?Q?=C3=96=C3=9C?=\r\n' + + 'Subject: ABCDEF\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + 'Content-Disposition: attachment; filename="test.pdf"\r\n' + + '\r\n' + + 'AAECAwQFBg==\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(3); + let mailparser = new MailParser(); + + mailparser.on('headers', headers => { + test.equal(headers.get('subject'), 'ABCDEF'); + test.equal(headers.get('x-test'), '=?UTF-8?Q?=C3=95=C3=84?= =?UTF-8?Q?=C3=96=C3=9C?='); + }); + + mailparser.end(mail); + mailparser.on('data', data => { + if (data && data.release) { + data.content.on('data', () => false); + data.content.on('end', () => false); + data.release(); + } + }); + mailparser.on('end', () => { + test.ok(1, 'Parsing ended'); + test.done(); + }); + }, + + 'No priority': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\n' + + 'Subject: ÕÄÖÜ\n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.headers.has('priority'), false); + test.done(); + }); + }, + + 'MS Style priority': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\n' + + 'Subject: ÕÄÖÜ\n' + + 'X-Priority: 1 (Highest)\n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.headers.get('priority'), 'high'); + test.done(); + }); + }, + + 'Single reference': test => { + let encodedText = 'Content-type: text/plain\r\n' + + 'References: \n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.headers.get('references'), ''); + test.done(); + }); + }, + + 'Multiple reference values': test => { + let encodedText = 'Content-type: text/plain\r\n' + + 'References: \n' + + ' \n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.references, ['', '', '']); + test.done(); + }); + }, + + 'Multiple reference fields': test => { + let encodedText = 'Content-type: text/plain\r\n' + + 'References: \n' + + 'References: \n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.references, ['', '']); + test.done(); + }); + }, + + 'Single in-reply-to': test => { + let encodedText = 'Content-type: text/plain\r\n' + + 'in-reply-to: \n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.inReplyTo, ''); + test.done(); + }); + }, + + 'Multiple in-reply-to values': test => { + let encodedText = 'Content-type: text/plain\r\n' + + 'in-reply-to: \n' + + ' \n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.inReplyTo, ' '); + test.done(); + }); + }, + + 'Multiple in-reply-to fields': test => { + let encodedText = 'Content-type: text/plain\r\n' + + 'in-reply-to: \n' + + 'in-reply-to: \n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.inReplyTo, ''); + test.done(); + }); + }, + + 'Reply To address': test => { + let encodedText = 'Reply-TO: andris \r\n' + + 'Subject: ÕÄÖÜ\n' + + '\r\n' + + '1234', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.replyTo.value, [{ + name: 'andris', + address: 'andris@disposebox.com' + }]); + test.done(); + }); + } + +}; + +exports['Text encodings'] = { + + 'Plaintext encoding: Default': test => { + let encodedText = [13, 10, 213, 196, 214, 220], // \r\nÕÄÖÜ + mail = Buffer.from(encodedText); + + test.expect(1); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + + 'Plaintext encoding: Header defined': test => { + let encodedText = 'Content-Type: TEXT/PLAIN; CHARSET=UTF-8\r\n' + + '\r\n' + + 'ÕÄÖÜ', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + + 'HTML encoding: Header defined': test => { + let encodedText = 'Content-Type: text/html; charset=iso-UTF-8\r\n' + + '\r\n' + + 'ÕÄÖÜ', + mail = Buffer.from(encodedText, 'utf-8'); + + test.expect(1); + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.html, 'ÕÄÖÜ'); + test.done(); + }); + }, + + 'Mime Words': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\n' + + 'From: =?utf-8?q?_?= \r\n' + + 'To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= \r\n' + + 'Subject: =?iso-8859-1?Q?Avaldu?= =?iso-8859-1?Q?s_lepingu_?=\r\n =?iso-8859-1?Q?l=F5petamise?= =?iso-8859-1?Q?ks?=\r\n', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.subject, 'Avaldus lepingu lõpetamiseks'); + test.equal(mailparser.from.value[0].name, ' '); + test.equal(mailparser.to.value[0].name, 'Keld Jørn Simonsen'); + test.done(); + }); + } +}; + +exports['Binary attachment encodings'] = { + 'Quoted-Printable': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(Array.prototype.slice.apply(attachments[0].content && attachments[0].content || []).join(','), '0,1,2,3,253,254,255'); + test.done(); + }); + }, + Base64: test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + '\r\n' + + 'AAECA/3+/w==', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(Array.prototype.slice.apply(attachments[0].content && attachments[0].content || []).join(','), '0,1,2,3,253,254,255'); + test.done(); + }); + }, + '8bit': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + '\r\n' + + 'ÕÄÖÜ', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(Array.prototype.slice.apply(attachments[0].content && attachments[0].content || []).join(','), '195,149,195,132,195,150,195,156'); + test.done(); + }); + } +}; + +exports['Attachment Content-Id'] = { + Default: test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment; filename="=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?="\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.ok(!attachments[0].contentId); + test.done(); + }); + }, + + Defined: test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment; filename="=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?="\r\n' + + 'Content-Id: \r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].contentId, ''); + test.done(); + }); + } +}; + +exports['Attachment filename'] = { + + 'Content-Disposition filename': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment; filename="=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?="\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Content-Disposition filename*': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment; filename*="UTF-8\'\'%C3%95%C3%84%C3%96%C3%9C"\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Content-Disposition filename*X': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment;\r\n' + + ' filename*0=OA;\r\n' + + ' filename*1=U;\r\n' + + ' filename*2=.txt\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'OAU.txt'); + test.done(); + }); + }, + 'Content-Disposition filename*X*': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment;\r\n' + + ' filename*0*=UTF-8\'\'%C3%95%C3%84;\r\n' + + ' filename*1*=%C3%96%C3%9C\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Content-Disposition filename*X* mixed': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + 'Content-Disposition: attachment;\r\n' + + ' filename*0*=UTF-8\'\'%C3%95%C3%84;\r\n' + + ' filename*1*=%C3%96%C3%9C;\r\n' + + ' filename*2=.txt\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ.txt'); + test.done(); + }); + }, + + 'Content-Type name': test => { + let encodedText = 'Content-Type: application/octet-stream; name="=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?="\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Content-Type unknown; name': test => { + let encodedText = 'Content-Type: unknown; name="test"\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].filename, 'test'); + test.done(); + }); + }, + 'Content-Type name*': test => { + let encodedText = 'Content-Type: application/octet-stream;\r\n' + + ' name*=UTF-8\'\'%C3%95%C3%84%C3%96%C3%9C\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Content-Type name*X*': test => { + let encodedText = 'Content-Type: application/octet-stream;\r\n' + + ' name*0*=UTF-8\'\'%C3%95%C3%84;\r\n' + + ' name*1*=%C3%96%C3%9C\r\n' + + 'Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Multiple filenames - Same': test => { + let encodedText = 'Content-Type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream; name="test.txt"\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream; name="test.txt"\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'test.txt'); + test.equal(attachments && attachments[1] && attachments[1].content && attachments[1].filename, 'test.txt'); + test.done(); + }); + }, + 'Multiple filenames - Different': test => { + let encodedText = 'Content-Type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream; name="test.txt"\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(!attachments[0].filename, true); + test.equal(attachments[1].filename, 'test.txt'); + test.done(); + }); + }, + 'Filename with semicolon': test => { + let encodedText = 'Content-Type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Disposition: attachment; filename="hello;world;test.txt"\r\n' + + '\r\n' + + '=00=01=02=03=FD=FE=FF\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + mailparser.on('end', () => { + test.equal(attachments[0].content && attachments[0].filename, 'hello;world;test.txt'); + test.done(); + }); + } +}; + +exports['Plaintext format'] = { + Default: test => { + let encodedText = 'Content-Type: text/plain;\r\n\r\nFirst line \r\ncontinued', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'First line \ncontinued'); + test.done(); + }); + }, + Flowed: test => { + let encodedText = 'Content-Type: text/plain; format=flowed\r\n\r\nFirst line \r\ncontinued \r\nand so on', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'First line continued and so on'); + test.done(); + }); + }, + 'Flowed Signature': test => { + let encodedText = 'Content-Type: text/plain; format=flowed\r\n\r\nHow are you today?\r\n\r\n' + + '-- \r\n' + + 'Signature\r\n', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'How are you today?\n-- \nSignature\n'); + test.done(); + }); + }, + Fixed: test => { + let encodedText = 'Content-Type: text/plain; format=fixed\r\n\r\nFirst line \r\ncontinued \r\nand so on', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'First line \ncontinued \nand so on'); + test.done(); + }); + }, + DelSp: test => { + let encodedText = 'Content-Type: text/plain; format=flowed; delsp=yes\r\n\r\nFirst line \r\ncontinued \r\nand so on', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'First linecontinuedand so on'); + test.done(); + }); + } +}; + +exports['Transfer encoding'] = { + 'Quoted-Printable Default charset': test => { + let encodedText = 'Content-type: text/plain\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n=D5=C4=D6=DC', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Quoted-Printable UTF-8': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\n=C3=95=C3=84=C3=96=C3=9C', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Base64 Default charset': test => { + let encodedText = 'Content-type: text/plain\r\nContent-Transfer-Encoding: bAse64\r\n\r\n1cTW3A==', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Base64 UTF-8': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: bAse64\r\n\r\nw5XDhMOWw5w=', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Mime Words': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\nSubject: =?iso-8859-1?Q?Avaldu?= =?iso-8859-1?Q?s_lepingu_?=\r\n =?iso-8859-1?Q?l=F5petamise?= =?iso-8859-1?Q?ks?=\r\n', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.subject, 'Avaldus lepingu lõpetamiseks'); + test.done(); + }); + }, + '8bit Default charset': test => { + let encodedText = 'Content-type: text/plain\r\nContent-Transfer-Encoding: 8bit\r\n\r\nÕÄÖÜ', + textmap = encodedText.split('').map(chr => chr.charCodeAt(0)), + mail = Buffer.from(textmap); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + '8bit UTF-8': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\nÕÄÖÜ', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Invalid Quoted-Printable': test => { + let encodedText = 'Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\n==C3==95=C3=84=C3=96=C3=9C=', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, '=�=�ÄÖÜ'); + test.done(); + }); + }, + 'gb2312 mime words': test => { + let encodedText = 'From: =?gb2312?B?086yyZjl?= user@ldkf.com.tw\r\n\r\nBody', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.deepEqual(mailparser.from.value, [{ + address: 'user@ldkf.com.tw', + name: '游采樺' + }]); + test.done(); + }); + }, + 'Valid Date header': test => { + let encodedText = 'Date: Wed, 08 Jan 2014 09:52:26 -0800\r\n\r\n1cTW3A==', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.date.toISOString(), '2014-01-08T17:52:26.000Z'); + test.done(); + }); + }, + 'Invalid Date header': test => { + let encodedText = 'Date: zzzzz\r\n\r\n1cTW3A==', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.ok(!mail.date); + test.done(); + }); + }, + 'Missing Date header': test => { + let encodedText = 'Subject: test\r\n\r\n1cTW3A==', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.ok(!mail.date); + test.done(); + }); + } +}; + +exports['Multipart content'] = { + Simple: test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n\r\n--ABC\r\nContent-type: text/plain; charset=utf-8\r\n\r\nÕÄÖÜ\r\n--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + Nested: test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-type: multipart/related; boundary=DEF\r\n' + + '\r\n' + + '--DEF\r\n' + + 'Content-type: text/plain; charset=utf-8\r\n' + + '\r\n' + + 'ÕÄÖÜ\r\n' + + '--DEF--\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Inline text (Sparrow)': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: text/plain; charset="utf-8"\r\n' + + 'Content-Transfer-Encoding: 8bit\r\n' + + 'Content-Disposition: inline\r\n' + + '\r\n' + + 'ÕÄÖÜ\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ'); + test.done(); + }); + }, + 'Different Levels': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-type: text/html; charset=utf-8\r\n' + + '\r\n' + + 'ÕÄÖÜ2\r\n' + + '--ABC\r\n' + + 'Content-type: multipart/related; boundary=DEF\r\n' + + '\r\n' + + '--DEF\r\n' + + 'Content-type: text/plain; charset=utf-8\r\n' + + '\r\n' + + 'ÕÄÖÜ1\r\n' + + '--DEF--\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let mailparser = new MailParser(); + mailparser.end(mail); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, 'ÕÄÖÜ2\nÕÄÖÜ1'); + test.equal(mailparser.html, 'ÕÄÖÜ2
\n

ÕÄÖÜ1

\n'); + test.done(); + }); + } +}; + +exports['Attachment info'] = { + 'Included integrity': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: quoted-printable\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + '=00=01=02=03=04=05=06\r\n' + + '--ABC--', + expectedHash = '9aa461e1eca4086f9230aa49c90b0c61', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + mailparser.end(); + + mailparser.on('end', () => { + test.equal(attachments[0].checksum, expectedHash); + test.equal(attachments[0].size, 7); + test.done(); + }); + }, + 'Stream integrity base64': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + 'AAECAwQFBg==\r\n' + + '--ABC--', + expectedHash = '9aa461e1eca4086f9230aa49c90b0c61', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + test.expect(2); + + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + mailparser.end(); + mailparser.on('end', () => { + test.equal(attachments[0].checksum, expectedHash); + test.equal(attachments[0].size, 7); + test.done(); + }); + }, + 'Stream integrity - 8bit': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: 8bit\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + 'ÕÄ\r\n' + + 'ÖÜ\r\n' + + '--ABC--', + expectedHash = 'cad0f72629a7245dd3d2cbf41473e3ca', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + test.expect(2); + + mailparser.end(); + mailparser.on('end', () => { + test.equal(attachments[0].checksum, expectedHash); + test.equal(attachments[0].size, 10); + test.done(); + }); + }, + 'Stream integrity - binary, non utf-8': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: 8bit\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + 'ÕÄ\r\n' + + 'ÖÜ\r\n' + + 'ŽŠ\r\n' + + '--ABC--', + expectedHash = '34bca86f8cc340bbd11446ee16ee3cae', + mail = iconv.encode(encodedText, 'iso-8859-13'); + + let attachments = []; + let mailparser = new MailParser(); + + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + test.expect(2); + + mailparser.end(); + mailparser.on('end', () => { + test.equal(attachments[0].checksum, expectedHash); + test.equal(attachments[0].size, 10); + test.done(); + }); + }, + 'Stream integrity - qp, non utf-8': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream; charset=iso-8859-13\r\n' + + 'Content-Transfer-Encoding: quoted-printable\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + '=d5=c4\r\n' + + '=d6=dc\r\n' + + '=de=d0\r\n' + + '--ABC--', + expectedHash = '34bca86f8cc340bbd11446ee16ee3cae', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + test.expect(2); + + mailparser.end(); + mailparser.on('end', () => { + test.equal(attachments[0].checksum, expectedHash); + test.equal(attachments[0].size, 10); + test.done(); + }); + }, + 'Attachment in root node': test => { + let encodedText = 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: 8bit\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + 'ÕÄ\r\n' + + 'ÖÜ', + expectedHash = 'cad0f72629a7245dd3d2cbf41473e3ca', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser({ + streamAttachments: true + }); + + mailparser.on('data', data => { + if (data.type === 'attachment') { + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + test.expect(2); + + mailparser.end(); + mailparser.on('end', () => { + test.equal(attachments[0].checksum, expectedHash); + test.equal(attachments[0].size, 10); + test.done(); + }); + }, + 'Stream multiple attachments': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + 'AAECAwQFBg==\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + 'Content-Disposition: attachment\r\n' + + '\r\n' + + 'AAECAwQFBg==\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + 'Content-Disposition: attachment; filename="test.txt"\r\n' + + '\r\n' + + 'AAECAwQFBg==\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser({ + streamAttachments: true + }); + + test.expect(3); // should be 3 attachments + + mailparser.on('data', data => { + if (data.type === 'attachment') { + test.ok(data); + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + mailparser.end(mail); + mailparser.on('end', () => { + test.done(); + }); + }, + 'Detect Content-Type by filename': test => { + let encodedText = 'Content-type: multipart/mixed; boundary=ABC\r\n' + + '\r\n' + + '--ABC\r\n' + + 'Content-Type: application/octet-stream\r\n' + + 'Content-Transfer-Encoding: base64\r\n' + + 'Content-Disposition: attachment; filename="test.pdf"\r\n' + + '\r\n' + + 'AAECAwQFBg==\r\n' + + '--ABC--', + mail = Buffer.from(encodedText, 'utf-8'); + + let attachments = []; + let mailparser = new MailParser(); + + mailparser.on('data', data => { + if (data.type === 'attachment') { + test.ok(data); + let chunks = []; + data.content.on('data', chunk => chunks.push(chunk)); + data.content.on('end', () => { + data.content = Buffer.concat(chunks); + data.release(); + }); + attachments.push(data); + } + }); + + mailparser.write(mail); + mailparser.end(); + mailparser.on('end', () => { + test.equal(attachments[0].contentType, 'application/pdf'); + test.done(); + }); + } +}; + +exports['Advanced nested HTML'] = test => { + let mail = fs.readFileSync(__dirname + '/fixtures/nested.eml'); + + test.expect(2); + let mailparser = new MailParser(); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + mailparser.end(); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, '\nDear Sir,\n\nGood evening.\n\n\n\n\n\n\n\nThe footer\n'); + test.equal(mailparser.html, '

Dear Sir

\n

Good evening.

\n


\n

The footer

\n'); + test.done(); + }); +}; + +exports['Additional text'] = test => { + let mail = fs.readFileSync(__dirname + '/fixtures/mixed.eml'); + + test.expect(2); + let mailparser = new MailParser(); + + for (let i = 0, len = mail.length; i < len; i++) { + mailparser.write(Buffer.from([mail[i]])); + } + + mailparser.end(); + mailparser.on('data', () => false); + mailparser.on('end', () => { + test.equal(mailparser.text, '\nThis e-mail message has been scanned for Viruses and Content and cleared\n\nGood Morning;\n\n'); + test.equal(mailparser.html, '\n \n\n
\nThis e-mail message has been scanned for Viruses and Content and cleared\n
\n\n
\n

Good Morning;

\n'); + test.done(); + }); +}; diff --git a/test/mailparser.js b/test/mailparser.js deleted file mode 100644 index 99fac5f..0000000 --- a/test/mailparser.js +++ /dev/null @@ -1,1653 +0,0 @@ -"use strict"; - -var MailParser = require("../lib/mailparser").MailParser, - encodinglib = require("encoding"), - fs = require("fs"); - -exports["General tests"] = { - "Many chunks": function(test) { - var encodedText = "Content-Type: text/plain; charset=utf-8\r\n" + - "\r\n" + - "ÕÄ\r\n" + - "ÖÜ", // \r\nÕÄÖÜ - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄ\nÖÜ"); - test.done(); - }); - }, - - "Many chunks - split line endings": function(test) { - var chunks = [ - "Content-Type: text/plain; charset=utf-8\r", - "\nSubject: Hi Mom\r\n\r\n", - "hello" - ]; - - test.expect(1); - var mailparser = new MailParser(); - - var writeNextChunk = function() { - var chunk = chunks.shift(); - if (chunk !== undefined) { - mailparser.write(chunk, 'utf8'); - if (typeof setImmediate == "function") { - setImmediate(writeNextChunk); - } else { - process.nextTick(writeNextChunk); - } - } else { - mailparser.end(); - } - }; - - mailparser.on("end", function(mail) { - test.equal(mail.text, "hello"); - test.done(); - }); - - if (typeof setImmediate == "function") { - setImmediate(writeNextChunk); - } else { - process.nextTick(writeNextChunk); - } - }, - - "Headers only": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\n" + - "Subject: ÕÄÖÜ", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.subject, "ÕÄÖÜ"); - test.done(); - }); - }, - - "Body only": function(test) { - var encodedText = "\r\n" + - "===", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "==="); - test.done(); - }); - }, - - "Different line endings": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r" + - "Subject: ÕÄÖÜ\n" + - "\r" + - "1234\r\n" + - "ÕÄÖÜ\r\n" + - "ÜÖÄÕ\n" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(2); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.subject, "ÕÄÖÜ"); - test.equal(mail.text, "1234\nÕÄÖÜ\nÜÖÄÕ\n1234"); - test.done(); - }); - }, - - "Headers event": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "X-Test: =?UTF-8?Q?=C3=95=C3=84?= =?UTF-8?Q?=C3=96=C3=9C?=\r\n" + - "Subject: ABCDEF\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(3); - var mailparser = new MailParser(); - - mailparser.on("headers", function(headers) { - test.equal(headers.subject, "ABCDEF"); - test.equal(headers['x-test'], "ÕÄÖÜ"); - }); - - mailparser.end(mail); - mailparser.on("end", function() { - test.ok(1, "Parsing ended"); - test.done(); - }); - }, - - "No priority": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r" + - "Subject: ÕÄÖÜ\n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.priority, "normal"); - test.done(); - }); - }, - - "MS Style priority": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r" + - "Subject: ÕÄÖÜ\n" + - "X-Priority: 1 (Highest)\n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.priority, "high"); - test.done(); - }); - }, - - "Single reference": function(test) { - var encodedText = "Content-type: text/plain\r" + - "References: \n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.references, ["mail1"]); - test.done(); - }); - }, - - "Multiple reference values": function(test) { - var encodedText = "Content-type: text/plain\r" + - "References: \n" + - " \n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.references, ["mail1", "mail2", "mail3"]); - test.done(); - }); - }, - - "Multiple reference fields": function(test) { - var encodedText = "Content-type: text/plain\r" + - "References: \n" + - "References: \n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.references, ["mail1", "mail3"]); - test.done(); - }); - }, - - "Single in-reply-to": function(test) { - var encodedText = "Content-type: text/plain\r" + - "in-reply-to: \n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.inReplyTo, ["mail1"]); - test.done(); - }); - }, - - "Multiple in-reply-to values": function(test) { - var encodedText = "Content-type: text/plain\r" + - "in-reply-to: \n" + - " \n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.inReplyTo, ["mail1", "mail2", "mail3"]); - test.done(); - }); - }, - - "Multiple in-reply-to fields": function(test) { - var encodedText = "Content-type: text/plain\r" + - "in-reply-to: \n" + - "in-reply-to: \n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.inReplyTo, ["mail1", "mail3"]); - test.done(); - }); - }, - - "Reply To address": function(test) { - var encodedText = "Reply-TO: andris \r" + - "Subject: ÕÄÖÜ\n" + - "\r" + - "1234", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.replyTo, [{ - name: "andris", - address: "andris@disposebox.com" - }]); - test.done(); - }); - } - -}; - -exports["Text encodings"] = { - - "Plaintext encoding: Default": function(test) { - var encodedText = [13, 10, 213, 196, 214, 220], // \r\nÕÄÖÜ - mail = new Buffer(encodedText); - - test.expect(1); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - - "Plaintext encoding: Header defined": function(test) { - var encodedText = "Content-Type: TEXT/PLAIN; CHARSET=UTF-8\r\n" + - "\r\n" + - "ÕÄÖÜ", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - - "HTML encoding: From ": function(test) { - var encodedText = "Content-Type: text/html\r\n" + - "\r\n" + - "ÕÄÖÜ", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal((mail.html || "").substr(-4), "ÕÄÖÜ"); - test.done(); - }); - }, - - "HTML encoding: Header defined": function(test) { - var encodedText = "Content-Type: text/html; charset=iso-UTF-8\r\n" + - "\r\n" + - "ÕÄÖÜ", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.html, "ÕÄÖÜ"); - test.done(); - }); - }, - "Mime Words": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\n" + - "From: =?utf-8?q??= \r\n" + - "To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= \r\n" + - "Subject: =?iso-8859-1?Q?Avaldu?= =?iso-8859-1?Q?s_lepingu_?=\r\n =?iso-8859-1?Q?l=F5petamise?= =?iso-8859-1?Q?ks?=\r\n", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.subject, "Avaldus lepingu lõpetamiseks"); - test.equal(mail.from[0].name, ""); - test.equal(mail.to[0].name, "Keld Jørn Simonsen"); - test.done(); - }); - } -}; - -exports["Binary attachment encodings"] = { - "Quoted-Printable": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(Array.prototype.slice.apply(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].content || []).join(","), "0,1,2,3,253,254,255"); - test.done(); - }); - }, - "Base64": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "\r\n" + - "AAECA/3+/w==", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(Array.prototype.slice.apply(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].content || []).join(","), "0,1,2,3,253,254,255"); - test.done(); - }); - }, - "8bit": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "\r\n" + - "ÕÄÖÜ", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(Array.prototype.slice.apply(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].content || []).join(","), "195,149,195,132,195,150,195,156"); - test.done(); - }); - }, - "UUENCODE": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: uuencode\r\n" + - "\r\n" + - "begin 644 buffer.bin\r\n" + - "#0V%T\r\n" + - "`\r\n" + - "end", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments[0].content.toString(), "Cat"); - test.done(); - }); - } - -}; - -exports["Attachment Content-Id"] = { - "Default": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment; filename=\"=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?=\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].contentId, "7c7cf35ce5becf62faea56ed8d0ad6e4@mailparser"); - test.done(); - }); - }, - - "Defined": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment; filename=\"=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?=\"\r\n" + - "Content-Id: test@localhost\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].contentId, "test@localhost"); - test.done(); - }); - } -}; - -exports["Attachment filename"] = { - - "Content-Disposition filename": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment; filename=\"=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?=\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ"); - test.done(); - }); - }, - "Content-Disposition filename*": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment; filename*=\"UTF-8''%C3%95%C3%84%C3%96%C3%9C\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ"); - test.done(); - }); - }, - "Content-Disposition filename*X": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment;\r\n" + - " filename*0=OA;\r\n" + - " filename*1=U;\r\n" + - " filename*2=.txt\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "OAU.txt"); - test.done(); - }); - }, - "Content-Disposition filename*X*": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment;\r\n" + - " filename*0*=UTF-8''%C3%95%C3%84;\r\n" + - " filename*1*=%C3%96%C3%9C\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ"); - test.done(); - }); - }, - "Content-Disposition filename*X* mixed": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "Content-Disposition: attachment;\r\n" + - " filename*0*=UTF-8''%C3%95%C3%84;\r\n" + - " filename*1*=%C3%96%C3%9C;\r\n" + - " filename*2=.txt\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ.txt"); - test.done(); - }); - }, - - "Content-Type name": function(test) { - var encodedText = "Content-Type: application/octet-stream; name=\"=?UTF-8?Q?=C3=95=C3=84=C3=96=C3=9C?=\"\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ"); - test.done(); - }); - }, - "Content-Type ; name": function(test) { - var encodedText = "Content-Type: ; name=\"test\"\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - - mailparser.write(mail); - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].fileName, "test"); - test.done(); - }); - }, - "Content-Type name*": function(test) { - var encodedText = "Content-Type: application/octet-stream;\r\n" + - " name*=UTF-8''%C3%95%C3%84%C3%96%C3%9C\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ"); - test.done(); - }); - }, - "Content-Type name*X*": function(test) { - var encodedText = "Content-Type: application/octet-stream;\r\n" + - " name*0*=UTF-8''%C3%95%C3%84;\r\n" + - " name*1*=%C3%96%C3%9C\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].fileName, "ÕÄÖÜ"); - test.done(); - }); - }, - "Default name from Content-type": function(test) { - var encodedText = "Content-Type: application/pdf\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "attachment.pdf"); - test.done(); - }); - }, - "Default name": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: QUOTED-PRINTABLE\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "attachment.bin"); - test.done(); - }); - }, - "Multiple filenames - Same": function(test) { - var encodedText = "Content-Type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"test.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"test.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "test.txt"); - test.equal(mail.attachments && mail.attachments[1] && mail.attachments[1].content && mail.attachments[1].generatedFileName, "test-1.txt"); - test.done(); - }); - }, - "Multiple filenames - Different": function(test) { - var encodedText = "Content-Type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"test.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "attachment.bin"); - test.equal(mail.attachments && mail.attachments[1] && mail.attachments[1].content && mail.attachments[1].generatedFileName, "test.txt"); - test.done(); - }); - }, - "Multiple filenames - with number": function(test) { - var encodedText = "Content-Type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"somename.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"somename-1.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"somename.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; name=\"somename-1-1.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "somename.txt"); - test.equal(mail.attachments && mail.attachments[1] && mail.attachments[1].content && mail.attachments[1].generatedFileName, "somename-1-1.txt"); - test.equal(mail.attachments && mail.attachments[2] && mail.attachments[2].content && mail.attachments[2].generatedFileName, "somename-2.txt"); - test.equal(mail.attachments && mail.attachments[3] && mail.attachments[3].content && mail.attachments[3].generatedFileName, "somename-1-1-3.txt"); - test.done(); - }); - }, - "Generate filename from Content-Type": function(test) { - var encodedText = "Content-Type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/pdf\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "attachment.pdf"); - test.done(); - }); - }, - "Filename with semicolon": function(test) { - var encodedText = "Content-Type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Disposition: attachment; filename=\"hello;world;test.txt\"\r\n" + - "\r\n" + - "=00=01=02=03=FD=FE=FF\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "hello;world;test.txt"); - test.done(); - }); - }, - "UUE filename with special characters": function(test) { - var encodedText = "Content-Type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: uuencode\r\n" + - "Content-Disposition: attachment; filename=\"hello ~!@#%.txt\"\r\n" + - "\r\n" + - "begin 644 hello ~!@#%.txt\r\n" + - "#0V%T\r\n" + - "`\r\n" + - "end\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].content && mail.attachments[0].generatedFileName, "hello ~!@#%.txt"); - test.done(); - }); - } -}; - -exports["Plaintext format"] = { - "Default": function(test) { - var encodedText = "Content-Type: text/plain;\r\n\r\nFirst line \r\ncontinued", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "First line \ncontinued"); - test.done(); - }); - }, - "Flowed": function(test) { - var encodedText = "Content-Type: text/plain; format=flowed\r\n\r\nFirst line \r\ncontinued \r\nand so on", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "First line continued and so on"); - test.done(); - }); - }, - "Flowed Signature": function(test) { - var encodedText = "Content-Type: text/plain; format=flowed\r\n\r\nHow are you today?\r\n" + - "-- \r\n" + - "Signature\r\n", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "How are you today?\n-- \nSignature\n"); - test.done(); - }); - }, - "Fixed": function(test) { - var encodedText = "Content-Type: text/plain; format=fixed\r\n\r\nFirst line \r\ncontinued \r\nand so on", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "First line \ncontinued \nand so on"); - test.done(); - }); - }, - "DelSp": function(test) { - var encodedText = "Content-Type: text/plain; format=flowed; delsp=yes\r\n\r\nFirst line \r\ncontinued \r\nand so on", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "First linecontinuedand so on"); - test.done(); - }); - }, - "Quoted printable, Flowed": function(test) { - var encodedText = "Content-Type: text/plain; format=flowed\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\nFoo =\n\nBar =\n\nBaz", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "Foo Bar Baz"); - test.done(); - }); - }, - "Quoted printable, Flowed Signature": function(test) { - var encodedText = "Content-Type: text/plain; format=flowed\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\nHow are you today?\r\n" + - "-- \r\n" + - "Signature\r\n", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "How are you today?\n-- \nSignature\n"); - test.done(); - }); - }, - "Quoted printable, DelSp": function(test) { - var encodedText = "Content-Type: text/plain; format=flowed; delsp=yes\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\nFoo =\n\nBar =\n\nBaz", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "FooBarBaz"); - test.done(); - }); - } -}; - -exports["Transfer encoding"] = { - "Quoted-Printable Default charset": function(test) { - var encodedText = "Content-type: text/plain\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n=D5=C4=D6=DC", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Quoted-Printable UTF-8": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\n=C3=95=C3=84=C3=96=C3=9C", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Base64 Default charset": function(test) { - var encodedText = "Content-type: text/plain\r\nContent-Transfer-Encoding: bAse64\r\n\r\n1cTW3A==", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Base64 UTF-8": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: bAse64\r\n\r\nw5XDhMOWw5w=", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Mime Words": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\nSubject: =?iso-8859-1?Q?Avaldu?= =?iso-8859-1?Q?s_lepingu_?=\r\n =?iso-8859-1?Q?l=F5petamise?= =?iso-8859-1?Q?ks?=\r\n", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.subject, "Avaldus lepingu lõpetamiseks"); - test.done(); - }); - }, - "Mime Words with invalid linebreaks (Sparrow)": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\n" + - "Subject: abc=?utf-8?Q?=C3=B6=C\r\n" + - " 3=B5=C3=BC?=", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.subject, "abcöõü"); - test.done(); - }); - }, - "8bit Default charset": function(test) { - var encodedText = "Content-type: text/plain\r\nContent-Transfer-Encoding: 8bit\r\n\r\nÕÄÖÜ", - textmap = encodedText.split('').map(function(chr) { - return chr.charCodeAt(0); - }), - mail = new Buffer(textmap); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "8bit UTF-8": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\nÕÄÖÜ", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Invalid Quoted-Printable": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: QUOTED-PRINTABLE\r\n\r\n==C3==95=C3=84=C3=96=C3=9C=", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "=�=�ÄÖÜ"); - test.done(); - }); - }, - "Invalid BASE64": function(test) { - var encodedText = "Content-type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: base64\r\n\r\nw5XDhMOWw5", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(Array.prototype.map.call(mail.text, function(chr) { - return chr.charCodeAt(0); - }).join(","), "213,196,214,65533"); - test.done(); - }); - }, - "gb2312 mime words": function(test) { - var encodedText = "From: =?gb2312?B?086yyZjl?= user@ldkf.com.tw\r\n\r\nBody", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.deepEqual(mail.from, [{ - address: 'user@ldkf.com.tw', - name: '游采樺' - }]); - test.done(); - }); - }, - "Valid Date header": function(test) { - var encodedText = "Date: Wed, 08 Jan 2014 09:52:26 -0800\r\n\r\n1cTW3A==", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.date.toISOString(), "2014-01-08T17:52:26.000Z"); - test.equal(mail.headers.date, "Wed, 08 Jan 2014 09:52:26 -0800"); - test.done(); - }); - }, - "Invalid Date header": function(test) { - var encodedText = "Date: zzzzz\r\n\r\n1cTW3A==", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.ok(!mail.date); - test.equal(mail.headers.date, "zzzzz"); - test.done(); - }); - }, - "Missing Date header": function(test) { - var encodedText = "Subject: test\r\n\r\n1cTW3A==", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.ok(!mail.date); - test.equal(mail.headers.date, undefined); - test.done(); - }); - }, - "Received Headers": function(test) { - var encodedTest = "Received: by 10.25.25.72 with SMTP id 69csp2404548lfz;\r\n" + - " Fri, 6 Feb 2015 20:15:32 -0800 (PST)\r\n" + - "X-Received: by 10.194.200.68 with SMTP id jq4mr7518476wjc.128.1423264531879;\r\n" + - " Fri, 06 Feb 2015 15:15:31 -0800 (PST)\r\n" + - "Received: from mail.formilux.org (flx02.formilux.org. [195.154.117.161])\r\n" + - " by mx.google.com with ESMTP id wn4si6920692wjc.106.2015.02.06.15.15.31\r\n" + - " for ;\r\n" + - " Fri, 06 Feb 2015 15:15:31 -0800 (PST)\r\n" + - "Received: from flx02.formilux.org (flx02.formilux.org [127.0.0.1])\r\n" + - " by mail.formilux.org (Postfix) with SMTP id 9D262450C77\r\n" + - " for ; Sat, 7 Feb 2015 00:15:31 +0100 (CET)\r\n" + - "Date: Fri, 6 Feb 2015 16:13:51 -0700 (MST)\r\n" + - "\r\n" + - "1cTW3A==", - mail = new Buffer(encodedTest, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.ok(mail.date); - test.ok(mail.receivedDate); - test.equal(mail.date.toISOString(), "2015-02-06T23:13:51.000Z"); - test.equal(mail.receivedDate.toISOString(), "2015-02-07T04:15:32.000Z"); - test.done(); - }); - } -}; - -exports["Multipart content"] = { - "Simple": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n\r\n--ABC\r\nContent-type: text/plain; charset=utf-8\r\n\r\nÕÄÖÜ\r\n--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Nested": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-type: multipart/related; boundary=DEF\r\n" + - "\r\n" + - "--DEF\r\n" + - "Content-type: text/plain; charset=utf-8\r\n" + - "\r\n" + - "ÕÄÖÜ\r\n" + - "--DEF--\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Inline text (Sparrow)": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: text/plain; charset=\"utf-8\"\r\n" + - "Content-Transfer-Encoding: 8bit\r\n" + - "Content-Disposition: inline\r\n" + - "\r\n" + - "ÕÄÖÜ\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ"); - test.done(); - }); - }, - "Different Levels": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-type: text/html; charset=utf-8\r\n" + - "\r\n" + - "ÕÄÖÜ2\r\n" + - "--ABC\r\n" + - "Content-type: multipart/related; boundary=DEF\r\n" + - "\r\n" + - "--DEF\r\n" + - "Content-type: text/plain; charset=utf-8\r\n" + - "\r\n" + - "ÕÄÖÜ1\r\n" + - "--DEF--\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - mailparser.end(mail); - - mailparser.on("end", function(mail) { - test.equal(mail.text, "ÕÄÖÜ1"); - test.equal(mail.html, "ÕÄÖÜ2"); - test.done(); - }); - } -}; - -exports["Attachment info"] = { - "Included integrity": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: quoted-printable\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "=00=01=02=03=04=05=06\r\n" + - "--ABC--", - expectedHash = "9aa461e1eca4086f9230aa49c90b0c61", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 7); - test.done(); - }); - }, - "Stream integrity base64": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC--", - expectedHash = "9aa461e1eca4086f9230aa49c90b0c61", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(3); - - mailparser.on("attachment", function(attachment) { - test.ok(attachment.stream, "Stream detected"); - }); - - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 7); - test.done(); - }); - }, - "Stream integrity - 8bit": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: 8bit\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "ÕÄ\r\n" + - "ÖÜ\r\n" + - "--ABC--", - expectedHash = "cad0f72629a7245dd3d2cbf41473e3ca", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(4); - - mailparser.on("attachment", function(attachment, node) { - test.ok(attachment.stream, "Stream detected"); - test.ok(node); - }); - - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 10); - test.done(); - }); - }, - "Stream integrity - binary, non utf-8": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: 8bit\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "ÕÄ\r\n" + - "ÖÜ\r\n" + - "ŽŠ\r\n" + - "--ABC--", - expectedHash = "34bca86f8cc340bbd11446ee16ee3cae", - mail = encodinglib.convert(encodedText, "latin-13"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(3); - - mailparser.on("attachment", function(attachment) { - test.ok(attachment.stream, "Stream detected"); - }); - - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 10); - test.done(); - }); - }, - "Stream integrity - qp, non utf-8": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream; charset=iso-8859-13\r\n" + - "Content-Transfer-Encoding: quoted-printable\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "=d5=c4\r\n" + - "=d6=dc\r\n" + - "=de=d0\r\n" + - "--ABC--", - expectedHash = "34bca86f8cc340bbd11446ee16ee3cae", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(3); - - mailparser.on("attachment", function(attachment) { - test.ok(attachment.stream, "Stream detected"); - }); - - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 10); - test.done(); - }); - }, - "Stream integrity - uuencode": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: uuencode\r\n" + - "\r\n" + - "begin 644 buffer.bin\r\n" + - "#0V%T\r\n" + - "`\r\n" + - "end\r\n" + - "--ABC--", - expectedHash = "fa3ebd6742c360b2d9652b7f78d9bd7d", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(3); - - mailparser.on("attachment", function(attachment) { - test.ok(attachment.stream, "Stream detected"); - }); - - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 3); - test.done(); - }); - }, - "Attachment in root node": function(test) { - var encodedText = "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: 8bit\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "ÕÄ\r\n" + - "ÖÜ", - expectedHash = "cad0f72629a7245dd3d2cbf41473e3ca", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(4); - - mailparser.on("attachment", function(attachment, node) { - test.ok(attachment.stream, "Stream detected"); - test.ok(node); - }); - - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].checksum, expectedHash); - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].length, 10); - test.done(); - }); - }, - "Stream multiple attachments": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment; filename=\"test.txt\"\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - test.expect(3); // should be 3 attachments - mailparser.on("attachment", function(attachment) { - test.ok(attachment.stream, "Stream detected"); - }); - - mailparser.end(mail); - - mailparser.on("end", function() { - test.done(); - }); - }, - - "Pass mail node to headers event": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "Subject: ABCDEF\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser({ - streamAttachments: true - }); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - test.expect(2); - - mailparser.on("attachment", function(attachment, email) { - test.equal(email.subject, "ABCDEF"); - }); - - mailparser.end(); - - mailparser.on("end", function() { - test.ok(1, "Done"); - test.done(); - }); - }, - "Detect Content-Type by filename": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - var mailparser = new MailParser(); - - mailparser.write(mail); - mailparser.end(); - - mailparser.on("end", function(mail) { - test.equal(mail.attachments && mail.attachments[0] && mail.attachments[0].contentType, "application/pdf"); - test.done(); - }); - }, - - "Inline attachments": function(test) { - var encodedText = "Content-type: multipart/mixed; boundary=ABC\r\n" + - "X-Test: =?UTF-8?Q?=C3=95=C3=84?= =?UTF-8?Q?=C3=96=C3=9C?=\r\n" + - "Subject: ABCDEF\r\n" + - "\r\n" + - "--ABC\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "

test 1

\r\n" + - "--ABC\r\n" + - "Content-Type: application/octet-stream\r\n" + - "Content-Transfer-Encoding: base64\r\n" + - "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" + - "\r\n" + - "AAECAwQFBg==\r\n" + - "--ABC\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "

test 2

\r\n" + - "--ABC--", - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser({ - showAttachmentLinks: true - }); - - mailparser.end(mail); - mailparser.on("end", function(mail) { - test.equal(mail.html, '

test 1


\n\n
\n

test 2

'); - test.done(); - }); - } -}; - -exports["Advanced nested HTML"] = function(test) { - var mail = fs.readFileSync(__dirname + "/ali.eml"); - - test.expect(2); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function(mail) { - console.log(require('util').inspect(mail, false, 22)); - test.equal(mail.text, "\nDear Sir,\n\nGood evening.\n\n\n \n\n\n\nThe footer\n"); - test.equal(mail.html, "

Dear Sir

\n

Good evening.

\n

The footer

\n"); - test.done(); - }); -}; - -exports["Additional text"] = function(test) { - var mail = fs.readFileSync(__dirname + "/mixed.eml"); - - test.expect(2); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function(mail) { - test.equal(mail.text, "\nThis e-mail message has been scanned for Viruses and Content and cleared\nGood Morning;\n\n"); - test.equal(mail.html, "\n \n\n
\nThis e-mail message has been scanned for Viruses and Content and cleared\n
\n\nGood Morning;\n\n"); - test.done(); - }); -}; - -exports["MBOX format"] = { - "Not a mbox": function(test) { - var encodedText = "Content-Type: text/plain; charset=utf-8\r\n" + - "\r\n" + - "ÕÄ\r\n" + - "ÖÜ", // \r\nÕÄÖÜ - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function() { - test.equal(mailparser._isMbox, false); - test.done(); - }); - }, - - "Is a mbox": function(test) { - var encodedText = "From MAILER-DAEMON Fri Jul 8 12:08:34 2011\r\n" + - "Content-Type: text/plain; charset=utf-8\r\n" + - "\r\n" + - "ÕÄ\r\n" + - "ÖÜ", // \r\nÕÄÖÜ - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function() { - test.equal(mailparser._isMbox, true); - test.done(); - }); - }, - - "Don't unescape '>From '": function(test) { - var encodedText = "Content-Type: text/plain; charset=utf-8\r\n" + - "\r\n" + - ">From test\r\n" + - ">>From pest", // \r\nÕÄÖÜ - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function(mail) { - test.equal(mail.text, ">From test\n>>From pest"); - test.done(); - }); - }, - - "Unescape '>From '": function(test) { - var encodedText = "From MAILER-DAEMON Fri Jul 8 12:08:34 2011\r\n" + - "Content-Type: text/plain; charset=utf-8\r\n" + - "\r\n" + - ">From test\r\n" + - ">>From pest", // \r\nÕÄÖÜ - mail = new Buffer(encodedText, "utf-8"); - - test.expect(1); - var mailparser = new MailParser(); - - for (var i = 0, len = mail.length; i < len; i++) { - mailparser.write(new Buffer([mail[i]])); - } - - mailparser.end(); - mailparser.on("end", function(mail) { - test.equal(mail.text, "From test\n>From pest"); - test.done(); - }); - } -}; diff --git a/test/simple-parser-test.js b/test/simple-parser-test.js new file mode 100644 index 0000000..d354215 --- /dev/null +++ b/test/simple-parser-test.js @@ -0,0 +1,31 @@ +'use strict'; + +const simpleParser = require('..').SimpleParser; +const fs = require('fs'); + +module.exports['Parse message'] = test => { + simpleParser(fs.createReadStream(__dirname + '/fixtures/nodemailer.eml'), (err, mail) => { + test.ifError(err); + test.ok(mail); + test.equal(mail.attachments.length, 4); + test.equal(mail.attachments[2].checksum, '2822cbcf68de083b96ac3921d0e308a2'); + test.ok(mail.html.indexOf('data:image/png;base64,iVBORw0KGgoAAAANSU') >= 0); + test.equal(mail.subject, 'Nodemailer is unicode friendly ✔ (1476358788189)'); + test.deepEqual(mail.to, { + value: [ + // keep indent + { + address: 'andris+123@kreata.ee', + name: 'Andris Reinman' + }, + { + address: 'andris.reinman@gmail.com', + name: '' + } + ], + html: 'Andris Reinman <andris+123@kreata.ee>, andris.reinman@gmail.com', + text: 'Andris Reinman , andris.reinman@gmail.com' + }); + test.done(); + }); +};