├── .gitignore ├── test ├── messages │ ├── msg1 │ ├── msg4 │ ├── msg2 │ └── msg3 ├── headers │ ├── header2.json │ └── header1.json ├── tools.js ├── test_message.js └── test_parser.js ├── docs ├── index.md ├── mimemessage.md └── Entity.md ├── .prettierrc ├── lib ├── mimemessage.js ├── factory.js ├── grammar.js ├── encoding.js ├── Entity.js └── parse.js ├── .jscsrc ├── banner.txt ├── circle.yml ├── rollup.config.js ├── .jshintrc ├── LICENSE ├── .babelrc ├── .eslintrc ├── package.json ├── README.md └── dist └── mimemessage.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /NO_GIT/ 3 | .idea 4 | -------------------------------------------------------------------------------- /test/messages/msg1: -------------------------------------------------------------------------------- 1 | Content-Type: text/plain; charset=utf-8 2 | 3 | Hi! 4 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | * [mimemessage Module API](mimemessage.md) 4 | * [mimemessage.Entity Class API](Entity.md) 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "proseWrap": "never" 7 | } 8 | -------------------------------------------------------------------------------- /lib/mimemessage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: require('./factory'), 3 | parse: require('./parse'), 4 | Entity: require('./Entity') 5 | }; 6 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "crockford", 3 | "validateIndentation": "\t", 4 | "disallowKeywords": ["with"], 5 | "disallowDanglingUnderscores": null, 6 | "requireVarDeclFirst": null 7 | } 8 | -------------------------------------------------------------------------------- /banner.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * <%= pkg.name %> v<%= pkg.version %> 3 | * <%= pkg.description %> 4 | * Copyright 2015<%= currentYear > 2015 ? '-' + currentYear : '' %> <%= pkg.author %> 5 | * License <%= pkg.license %> 6 | */ 7 | 8 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.9.0-stretch-browsers 6 | steps: 7 | - checkout 8 | - run: npm i 9 | - run: npm run lint 10 | - run: npm test 11 | -------------------------------------------------------------------------------- /test/headers/header2.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": [ 3 | "image/png; name=\"logo.png\"", 4 | "image/png" 5 | ], 6 | "contentTransfer": [ 7 | "base64", 8 | "base64" 9 | ], 10 | "contentId": "", 11 | "contentDisposition": "inline; filename=\"logo.png\"" 12 | } 13 | -------------------------------------------------------------------------------- /test/headers/header1.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "image/jpeg; name=\"IMG_83201.jpeg\"", 3 | "contentTransferEncoding": "base64", 4 | "contentId": "<16698c9f23ddfaf1da1>", 5 | "contentDescription": "IMG_83201.jpeg", 6 | "contentLocation": "IMG_83201.jpeg", 7 | "contentDisposition": "inline; filename=\"IMG_83201.jpeg\"; size=91134" 8 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import pkg from './package.json'; 4 | 5 | 6 | export default [ 7 | { 8 | input: 'lib/mimemessage.js', 9 | output: { 10 | file: pkg.main, 11 | format: 'cjs' 12 | }, 13 | name: 'mimemessage', 14 | plugins: [ 15 | commonjs(), 16 | babel() 17 | ] 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /test/tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | /** 7 | * Constants. 8 | */ 9 | const MESSAGES_FOLDER = 'messages'; 10 | 11 | 12 | module.exports.readFile = function (filename) { 13 | const filepath = path.join(__dirname, MESSAGES_FOLDER, filename); 14 | 15 | // NOTE: Return this in case files are not CRLF line ended. 16 | // return content.replace(/\n/g, '\r\n'); 17 | return fs.readFileSync(filepath, 'utf8'); 18 | }; 19 | -------------------------------------------------------------------------------- /test/messages/msg4: -------------------------------------------------------------------------------- 1 | Content-Type: multipart/mixed;boundary="----=_Part_68_509885327.1447152748066" 2 | 3 | ------=_Part_68_509885327.1447152748066 4 | 5 | OLA 6 | 7 | KE ASE 8 | ------=_Part_68_509885327.1447152748066 9 | Content-Type: message/cpim 10 | 11 | From: 12 | To: 13 | NS: imdn 14 | imdn.Message-ID: dcf2ebb0-859f-11e5-b577-e1a44228c85f 15 | DateTime: 2015-11-07T22:35+0000 16 | 17 | Content-Type: text/plain 18 | 19 | JEJE 20 | ------=_Part_68_509885327.1447152748066-- 21 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "forin": true, 6 | "freeze": true, 7 | "latedef": "function", 8 | "noarg": true, 9 | "nonbsp": true, 10 | "nonew": true, 11 | "plusplus": false, 12 | "undef": true, 13 | "unused": true, 14 | "strict": false, 15 | "maxparams": 6, 16 | "maxdepth": 4, 17 | "maxstatements": false, 18 | "maxlen": 200, 19 | "browser": true, 20 | "browserify": true, 21 | "devel": false, 22 | "jquery": false, 23 | "mocha": true, 24 | "node": true, 25 | "shelljs": false, 26 | "worker": false 27 | } 28 | -------------------------------------------------------------------------------- /test/messages/msg2: -------------------------------------------------------------------------------- 1 | From: Nathaniel Borenstein 2 | To: Ned Freed 3 | Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST) 4 | Subject: Sample message 5 | MIME-Version: 1.0 6 | Content-type: multipart/mixed; boundary="simple boundary" 7 | 8 | This is a preamble to be ignored. 9 | 10 | --simple boundary 11 | 12 | Body NOT ending with a linebreak. 13 | --simple boundary 14 | Content-type: text/plain; charset=us-ascii 15 | 16 | Body ending with a linebreak. 17 | 18 | --simple boundary-- 19 | 20 | This is a epilogue to be ignored. 21 | -------------------------------------------------------------------------------- /test/messages/msg3: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | Message-ID: 1234 3 | Subject: Some Book 4 | From: =?utf-8?q?I=c3=b1aki?= Baz Castillo 5 | To: Alice 6 | Content-Type: multipart/mixed; boundary="AAAA" 7 | 8 | --AAAA 9 | 10 | body_AAAA_1 11 | --AAAA 12 | Content-Type: multipart/alternative; boundary=BBBB 13 | 14 | --BBBB 15 | content-type: text/plain 16 | 17 | body_BBBB_1 18 | 19 | --BBBB 20 | Content-Type: text/html 21 | x-foo: bar 22 | 23 |

body_BBBB_1

24 | --BBBB-- 25 | --AAAA 26 | Content-Type: text/plain; 27 | charset = utf-8; 28 | bar=yes 29 | CONTENT-Transfer-Encoding: quoted-printable 30 | 31 | body_AAAA_3 32 | 33 | --AAAA 34 | Content-Type: application/epub+zip; name="Some Book.epub" 35 | Content-Disposition: attachment;filename="Some Book.epub" 36 | Content-Transfer-Encoding: BASE64 37 | X-Attachment-Id: f_icxs58pn0 38 | 39 | UEsDBBQAAAAAAAKfVkVvYassFAAAABQAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi9lcHVi 40 | --AAAA-- 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 eFace2Face, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "ie 11" 9 | ] 10 | }, 11 | "modules": false 12 | } 13 | ] 14 | ], 15 | "plugins": [ 16 | "@babel/plugin-transform-regenerator", 17 | "@babel/plugin-syntax-dynamic-import", 18 | "@babel/plugin-syntax-import-meta", 19 | "@babel/plugin-proposal-class-properties", 20 | "@babel/plugin-proposal-json-strings", 21 | [ 22 | "@babel/plugin-proposal-decorators", 23 | { 24 | "legacy": true 25 | } 26 | ], 27 | "@babel/plugin-proposal-function-sent", 28 | "@babel/plugin-proposal-export-namespace-from", 29 | "@babel/plugin-proposal-numeric-separator", 30 | "@babel/plugin-proposal-throw-expressions", 31 | "@babel/plugin-proposal-export-default-from", 32 | "@babel/plugin-proposal-logical-assignment-operators", 33 | "@babel/plugin-proposal-optional-chaining", 34 | [ 35 | "@babel/plugin-proposal-pipeline-operator", 36 | { 37 | "proposal": "minimal" 38 | } 39 | ], 40 | "@babel/plugin-proposal-nullish-coalescing-operator", 41 | "@babel/plugin-proposal-do-expressions", 42 | "@babel/plugin-proposal-function-bind" 43 | ] 44 | } -------------------------------------------------------------------------------- /docs/mimemessage.md: -------------------------------------------------------------------------------- 1 | # mimemessage Module API 2 | 3 | The top-level module exported by the library is a JavaScript object with the entries described below. 4 | 5 | 6 | ### `mimemessage.factory(data)` 7 | 8 | Returns an instance of [Entity](Entity.md). 9 | 10 | If given, `data` object may contain the following fields: 11 | 12 | * `contentType` (String): Same data passed to [entity.contentType(value)](Entity.md#messagecontenttypevalue). 13 | * `contentTransferEncoding` (String): Same data passed to [entity.contentTransferEncoding(value)](Entity.md#messagecontenttransferencodingvalue). 14 | * `body` (String or Array of [Entity](Entity.md)): The body of the MIME message or entity, or an array of entities if this is a multipart MIME message. 15 | 16 | ```javascript 17 | var message = mimemessage.factory({ 18 | contentType: 'text/plain', 19 | body: 'HELLO' 20 | }); 21 | ``` 22 | 23 | *Note:* Further modifications can be done to the entity returned by the `factory()` call by means of the [Entity](Entity.md) API. 24 | 25 | 26 | ### `mimemessage.parse(raw)` 27 | 28 | Parses the given raw MIME message. If valid an instance of [Entity](Entity.md) is returned, `false` otherwise. 29 | 30 | * `raw` (String): A raw MIME message. 31 | 32 | ```javascript 33 | myWebSocket.onmessage = function (event) { 34 | var 35 | raw = event.data, 36 | msg = mimemessage.parse(raw); 37 | 38 | if (msg) { 39 | console.log('MIME message received: %s', msg); 40 | } else { 41 | console.error('invalid MIME message received: "%s"', raw); 42 | } 43 | }); 44 | ``` 45 | 46 | 47 | ### `mimemessage.Entity` 48 | 49 | The [Entity](Entity.md) class. Useful to check `instanceof mimemessage.Entity`. 50 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "commonjs": true 7 | }, 8 | "globals": { 9 | "openpgp": true, 10 | "asmCrypto": true 11 | }, 12 | "rules": { 13 | 14 | "object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": true }], 15 | "arrow-parens": ["error", "always"], 16 | "comma-dangle": ["error", "never"], 17 | "no-shadow": ["off", { "hoist": "never", "builtinGlobals": true }], 18 | "array-bracket-spacing": ["off", "never"], 19 | "object-property-newline": "off", 20 | "no-sequences": "off", 21 | "no-param-reassign": ["error", { "props": false }], 22 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 23 | "padded-blocks": ["off", "always"], 24 | "arrow-body-style": ["off", "as-needed"], 25 | "no-use-before-define": ["error", { "functions": false, "classes": true }], 26 | "new-cap": ["error", { "properties": true, "capIsNewExceptionPattern": "^Awesomplete.", "newIsCapExceptions": ["vCard"] }], 27 | "no-mixed-operators": ["error", {"groups": [["&", "|", "^", "~", "<<", ">>", ">>>"], ["&&", "||"]]}], 28 | "no-return-assign": "off", 29 | "max-len": ["error", { "ignoreComments": true, "code": 120, "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true }], 30 | "consistent-return": "off", 31 | "default-case": "off", 32 | "no-plusplus": "off", 33 | "no-bitwise": "off", 34 | "no-debugger": "off", 35 | "prefer-template": "off", 36 | "class-methods-use-this": "off", 37 | "func-names": ["off", "never"], 38 | "prefer-destructuring": "off", 39 | "function-paren-newline": "off", 40 | "prefer-promise-reject-errors": "off", 41 | "no-console": "off", 42 | "object-curly-newline": "off", 43 | "space-before-function-paren": "off", 44 | "global-require": "off", 45 | "indent": "off", 46 | "import/no-unresolved": [2, {"commonjs": true, "amd": true}], 47 | "import/named": 2, 48 | "import/namespace": 2, 49 | "import/default": 2, 50 | "import/export": 2, 51 | "operator-linebreak": "off", 52 | "implicit-arrow-linebreak": "off", 53 | "no-restricted-globals": ["error", "event"] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose the factory function. 3 | */ 4 | module.exports = factory; 5 | 6 | /** 7 | * Dependencies. 8 | */ 9 | const debug = require('debug')('mimemessage:factory'); 10 | const debugerror = require('debug')('mimemessage:ERROR:factory'); 11 | const Entity = require('./Entity'); 12 | 13 | debugerror.log = console.warn.bind(console); 14 | 15 | function buildEntity(data) { 16 | const entity = new Entity(); 17 | 18 | // Add Content-Type. 19 | if (data.contentType) { 20 | entity.contentType(data.contentType); 21 | } 22 | 23 | // Add Content-Disposition. 24 | if (data.contentDisposition) { 25 | entity.contentDisposition(data.contentDisposition); 26 | } 27 | 28 | // Add Content-Transfer-Encoding. 29 | if (data.contentTransferEncoding) { 30 | entity.contentTransferEncoding(data.contentTransferEncoding); 31 | } 32 | 33 | // Add body. 34 | if (data.body) { 35 | entity.body = data.body; 36 | } 37 | 38 | return entity; 39 | } 40 | 41 | const formatKey = (key) => { 42 | if (key === 'contentTransfer') { 43 | return 'contentTransferEncoding'; 44 | } 45 | return key; 46 | }; 47 | 48 | function factory(data = {}) { 49 | debug('factory() | [data:%o]', data); 50 | 51 | const stringifyKey = ['contentType', 'contentDisposition', 'contentTransferEncoding']; 52 | 53 | /* 54 | Some keys can be an array, as headers are strings we parse them 55 | then we keep only the longest string. 56 | ex: 57 | "contentType": [ 58 | "image/png; name=\"logo.png\"", 59 | "image/png" 60 | ], 61 | Output: 62 | "contentType": "image/png; name=\"logo.png\"" 63 | 64 | Some key are also non-standard ex: contentTransfer instead of contentTransferEncoding, we format the key too. 65 | */ 66 | const config = Object.keys(data).reduce((acc, item) => { 67 | const key = formatKey(item); 68 | if (stringifyKey.includes(key) && Array.isArray(data[item])) { 69 | acc[key] = data[item][0]; // BE convention is to do the first one 70 | return acc; 71 | } 72 | acc[key] = data[key]; 73 | return acc; 74 | }, Object.create(null)); 75 | 76 | return buildEntity(config); 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@protontech/mimemessage", 3 | "version": "1.1.4", 4 | "description": "MIME messages for JavaScript (RFC 2045 & 2046)", 5 | "author": "Iñaki Baz Castillo", 6 | "license": "MIT", 7 | "keywords": [ 8 | "mime" 9 | ], 10 | "main": "dist/mimemessage.js", 11 | "scripts": { 12 | "test": "mocha --ui tdd", 13 | "lint": "eslint $(find lib -type f -name '*.js') --quiet", 14 | "pretty": "prettier -c --write $(find lib -type f -name '*.js')", 15 | "build": "rollup -c", 16 | "postversion": "git push --tags" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged" 21 | } 22 | }, 23 | "lint-staged": { 24 | "*.js,!dist/*.js": [ 25 | "prettier -c -write", 26 | "git add" 27 | ] 28 | }, 29 | "homepage": "https://github.com/ProtonMail/mimemessage.js", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/ProtonMail/mimemessage.js.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/ProtonMail/mimemessage.js/issues" 36 | }, 37 | "dependencies": { 38 | "debug": "^2.2.0", 39 | "rfc2047": "^2.0.1" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.0.0", 43 | "@babel/plugin-external-helpers": "^7.0.0", 44 | "@babel/plugin-proposal-class-properties": "^7.0.0", 45 | "@babel/plugin-proposal-decorators": "^7.0.0", 46 | "@babel/plugin-proposal-do-expressions": "^7.0.0", 47 | "@babel/plugin-proposal-export-default-from": "^7.0.0", 48 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 49 | "@babel/plugin-proposal-function-bind": "^7.0.0", 50 | "@babel/plugin-proposal-function-sent": "^7.0.0", 51 | "@babel/plugin-proposal-json-strings": "^7.0.0", 52 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", 53 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", 54 | "@babel/plugin-proposal-numeric-separator": "^7.0.0", 55 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 56 | "@babel/plugin-proposal-optional-chaining": "^7.0.0", 57 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0", 58 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 59 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 60 | "@babel/plugin-syntax-import-meta": "^7.0.0", 61 | "@babel/plugin-transform-regenerator": "^7.0.0", 62 | "@babel/plugin-transform-runtime": "^7.0.0", 63 | "@babel/polyfill": "^7.0.0", 64 | "@babel/preset-env": "^7.0.0", 65 | "babel-loader": "^8.0.4", 66 | "babel-plugin-istanbul": "^4.1.4", 67 | "eslint": "^5.7.0", 68 | "eslint-config-airbnb-base": "^13.1.0", 69 | "eslint-plugin-import": "^2.14.0", 70 | "expect.js": "^0.3.1", 71 | "mocha": "^5.2.0", 72 | "husky": "^1.1.2", 73 | "lint-staged": "^7.3.0", 74 | "prettier": "^1.14.3", 75 | "rollup": "^0.66.6", 76 | "rollup-plugin-babel": "^4.0.3", 77 | "rollup-plugin-commonjs": "^9.2.0" 78 | }, 79 | "engines": { 80 | "node": ">=0.10.32" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/grammar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exported object. 3 | */ 4 | const grammar = {}; 5 | module.exports = grammar; 6 | /** 7 | * Constants. 8 | */ 9 | const REGEXP_CONTENT_TYPE = /^([^\t /]+)\/([^\t ;]+)(.*)$/; 10 | const REGEXP_CONTENT_TRANSFER_ENCODING = /^([a-zA-Z0-9\-_]+)$/; 11 | const REGEXP_PARAM_KEY = /;[ \t|]*([^\t =]+)[ \t]*=[ \t]*/g; 12 | const REGEXP_PARAM_VALUES = /[ \t]*([^"\t =]+|"([^"]*)")[ \t]*$/; 13 | 14 | grammar.headerRules = { 15 | 'Content-Type': { 16 | reg(value) { 17 | const match = value.match(REGEXP_CONTENT_TYPE); 18 | let params = {}; 19 | 20 | if (!match) { 21 | return undefined; 22 | } 23 | 24 | if (match[3]) { 25 | params = parseParams(match[3]); 26 | if (!params) { 27 | return undefined; 28 | } 29 | } 30 | 31 | return { 32 | fulltype: match[1].toLowerCase() + '/' + match[2].toLowerCase(), 33 | type: match[1].toLowerCase(), 34 | subtype: match[2].toLowerCase(), 35 | params 36 | }; 37 | } 38 | }, 39 | 40 | 'Content-Disposition': { 41 | reg(value) { 42 | return { 43 | fulltype: value, 44 | params: parseParams(value) 45 | }; 46 | } 47 | }, 48 | 49 | 'Content-Transfer-Encoding': { 50 | reg(value) { 51 | const match = value.match(REGEXP_CONTENT_TRANSFER_ENCODING); 52 | 53 | if (!match) { 54 | return undefined; 55 | } 56 | 57 | return { 58 | value: match[1].toLowerCase() 59 | }; 60 | } 61 | } 62 | }; 63 | 64 | grammar.unknownHeaderRule = { 65 | reg: /(.*)/, 66 | names: ['value'] 67 | }; 68 | 69 | grammar.headerize = function(string) { 70 | const exceptions = { 71 | 'Mime-Version': 'MIME-Version', 72 | 'Content-Id': 'Content-ID' 73 | }; 74 | const name = string 75 | .toLowerCase() 76 | .replace(/_/g, '-') 77 | .split('-'); 78 | const parts = name.length; 79 | 80 | let hname = ''; 81 | let part; 82 | for (part = 0; part < parts; part++) { 83 | if (part !== 0) { 84 | hname += '-'; 85 | } 86 | hname += name[part].charAt(0).toUpperCase() + name[part].substring(1); 87 | } 88 | 89 | if (exceptions[hname]) { 90 | hname = exceptions[hname]; 91 | } 92 | 93 | return hname; 94 | }; 95 | 96 | // Set sensible defaults to avoid polluting the grammar with boring details. 97 | 98 | Object.keys(grammar.headerRules).forEach((name) => { 99 | const rule = grammar.headerRules[name]; 100 | 101 | if (!rule.reg) { 102 | rule.reg = /(.*)/; 103 | } 104 | }); 105 | 106 | /** 107 | * Private API. 108 | */ 109 | function parseParams(rawParams) { 110 | if (rawParams === '' || rawParams === undefined || rawParams === null) { 111 | return {}; 112 | } 113 | const splittedParams = rawParams.split(REGEXP_PARAM_KEY); 114 | 115 | return splittedParams.slice(1).reduce((acc, key, i, list) => { 116 | if (!(i % 2)) { 117 | const values = (list[i + 1] || '').match(REGEXP_PARAM_VALUES) || []; 118 | acc[key.toLowerCase()] = values[2] || values[1]; 119 | } 120 | return acc; 121 | }, Object.create(null)); 122 | } 123 | -------------------------------------------------------------------------------- /lib/encoding.js: -------------------------------------------------------------------------------- 1 | const RFC2045_LIMIT = 76; 2 | 3 | const wrapline = (line, escape = '', limit = RFC2045_LIMIT) => { 4 | const lineCount = Math.ceil(line.length / limit); 5 | const result = Array.from({ length: lineCount }, (_, i) => line.substring(limit * i, limit * (i + 1))); 6 | return result.join(escape + '\r\n'); 7 | }; 8 | // the newlines in mime messages are \r\n. This function expects \n as incoming lines and produces \r\n newlines. 9 | const wraplines = (lines, escape = '', limit = RFC2045_LIMIT) => 10 | lines 11 | .split('\n') 12 | .map((line) => wrapline(line, escape, limit)) 13 | .join('\r\n'); 14 | 15 | // Don't escape newlines, tabs, everything between space and ~ save the = sign. 16 | const MATCH_ESCAPE_CHARS = /[^\t\n\r\x20-\x3C\x3E-\x7E]/g; 17 | const encodeQPSequence = (char) => 18 | '=' + 19 | ( 20 | '00' + 21 | char 22 | .charCodeAt(0) 23 | .toString(16) 24 | .toUpperCase() 25 | ).substr(-2); 26 | const encodeQPSequences = (input) => input.replace(MATCH_ESCAPE_CHARS, encodeQPSequence); 27 | const normalLinebreaks = (input) => input.replace(/(\r\n|\n|\r)/g, '\n'); 28 | // restore wrapping in escape sequences ==\r\n0D, =0\r\nD -> =0D=\r\n 29 | const restoreQPSequences = (input) => 30 | input.replace(/(?=.{0,2}=\r\n)(=(=\r\n)?[0-9A-F](=\r\n)?[0-9A-F])/g, (seq) => seq.replace(/=\r\n/, '') + '=\r\n'); 31 | const wrapQPLines = (input) => restoreQPSequences(wraplines(input, '=', RFC2045_LIMIT - 2)); 32 | const encodeQPTrailingSpace = (input) => input.replace(/ $/gm, ' =\r\n\r\n'); 33 | 34 | const encodeUTF8 = (value) => { 35 | return unescape(encodeURIComponent(value)); 36 | }; 37 | const decodeUTF8 = (value) => { 38 | try { 39 | return decodeURIComponent(escape(value)); 40 | } catch (e) { 41 | return value; 42 | } 43 | }; 44 | 45 | const base64encode = typeof btoa === 'undefined' ? (str) => Buffer.from(str, 'binary').toString('base64') : btoa; 46 | const base64decode = typeof atob === 'undefined' ? (str) => Buffer.from(str, 'base64').toString('binary') : atob; 47 | 48 | const encodeBase64 = (value) => wraplines(base64encode(value)); 49 | const decodeBase64 = (value) => decodeUTF8(base64decode(value)); 50 | 51 | /** 52 | * Quoted-Printable, or QP encoding, is an encoding using printable ASCII characters 53 | * (alphanumeric and the equals sign =) to transmit 8-bit data over a 7-bit data path) 54 | * Any 8-bit byte value may be encoded with 3 characters: an = followed by two hexadecimal digits (0–9 or A–F) 55 | * representing the byte's numeric value. For example, an ASCII form feed character (decimal value 12) can be 56 | * represented by "=0C", and an ASCII equal sign (decimal value 61) must be represented by =3D. 57 | * All characters except printable ASCII characters or end of line characters (but also =) 58 | * must be encoded in this fashion. 59 | * 60 | * All printable ASCII characters (decimal values between 33 and 126) may be represented by themselves, except = 61 | * (decimal 61). 62 | * 63 | * @param binarydata 64 | * @return 7-bit encoding of the input using QP encoding 65 | */ 66 | const encodeQP = (binarydata) => 67 | encodeQPTrailingSpace(wrapQPLines(normalLinebreaks(encodeQPSequences(binarydata)))); 68 | 69 | const removeSoftBreaks = (value) => value.replace(/=(\r\n|\n|\r)|/g, ''); 70 | 71 | const decodeQuotedPrintables = (value) => 72 | value.replace(/=([0-9A-F][0-9A-F])/gm, (match, contents) => { 73 | return String.fromCharCode(parseInt(contents, 16)); 74 | }); 75 | 76 | const decodeQP = (value) => { 77 | return decodeQuotedPrintables(removeSoftBreaks(value)); 78 | }; 79 | 80 | module.exports = { 81 | encodeBase64, 82 | decodeBase64, 83 | encodeQP, 84 | decodeQP, 85 | encodeUTF8, 86 | decodeUTF8 87 | }; 88 | -------------------------------------------------------------------------------- /docs/Entity.md: -------------------------------------------------------------------------------- 1 | # Entity Class API 2 | 3 | A `Entity` instance represents a [MIME entity](https://tools.ietf.org/html/rfc2045) (which can be the top-level message or a MIME sub-entity in a [multipart message](https://tools.ietf.org/html/rfc2046)). An entity has both headers and a body, which can also be a multipart body containing N MIME sub-entities. 4 | 5 | 6 | ## Constructor 7 | 8 | ```javascript 9 | var entity = new mimemessage.Entity(); 10 | ``` 11 | mi 12 | 13 | ## Properties 14 | 15 | 16 | ### `entity.body` 17 | 18 | A getter that returns the body of this MIME message or entity. The body can be an array of MIME entities if this is a multipart message/entity. 19 | 20 | Returns `undefined` if there is no body. 21 | 22 | 23 | ### `entity.body = body` 24 | 25 | Sets the MIME body of the message to the given `body` (string or array of [Entity](Entity.md)). 26 | 27 | If `body` is `null` the body is removed. 28 | 29 | *NOTE:* In case of a multipart message, further sub-entities can be safely added to the body later by using `entity.body.push(subEntity1);`. 30 | 31 | 32 | ## Methods 33 | 34 | 35 | ### `entity.contentType()` 36 | 37 | Returns the *Content-Type* header as an object with these fields: 38 | 39 | * `type` (String): Type. 40 | * `subtype` (String): Subtype. 41 | * `fulltype` (String): MIME type in "type/subtype" format (no parameters). 42 | * `params` (Object): Param/value pairs. 43 | * `value` (String): The full string value. 44 | 45 | Returns `undefined` if there is no *Content-Type* header. 46 | 47 | ```javascript 48 | entity.contentType(); 49 | 50 | // => {type: 'text', subtype: 'plain', fulltype: 'text/plain', params: {charset: 'utf-16'}, value: 'text/plain;charset:utf-16'} 51 | ``` 52 | 53 | 54 | ### `entity.contentType(value)` 55 | 56 | Sets the MIME *Content-Type* header with the given string. 57 | 58 | If `value` is `null` the header is removed. 59 | 60 | ```javascript 61 | entity.contentType('text/html;charset=utf-8'); 62 | entity.contentType('text/plain ; charset = utf-16'); 63 | ``` 64 | 65 | 66 | ### `entity.contentTransferEncoding()` 67 | 68 | Returns the *Content-Transfer-Encoding* string value (lowcase), or `undefined` if there is no *Content-Transfer-Encoding* header. 69 | 70 | ```javascript 71 | entity.contentTransferEncoding(); 72 | 73 | // => '8bit' 74 | ``` 75 | 76 | 77 | ### `entity.contentTransferEncoding(value)` 78 | 79 | Sets the MIME *Content-Transfer-Encoding* header with the given string. 80 | 81 | If `value` is `null` the header is removed. 82 | 83 | ```javascript 84 | entity.contentTransferEncoding('base64'); 85 | ``` 86 | 87 | 88 | ### `entity.header()` 89 | 90 | Returns the MIME header value (string) matching the given header `name` (string). 91 | 92 | Returns `undefined` if there is such a header. 93 | 94 | ```javascript 95 | entity.header('Content-ID'); 96 | 97 | // => "" 98 | ``` 99 | 100 | 101 | ### `entity.header(name, value)` 102 | 103 | Sets the MIME header with the given header `name` (string) and header `value` (string). 104 | 105 | If `value` is `null` the header is removed. 106 | 107 | ```javascript 108 | entity.header('Content-ID', '<1234@foo.com>'); 109 | ``` 110 | 111 | 112 | ### `entity.toString(options)` 113 | 114 | Serializes the MIME message/entity into a single string. 115 | 116 | ```javascript 117 | myWebSocket.send(entity.toString()); 118 | ``` 119 | 120 | If given, `options` object may contain the following fields: 121 | 122 | * `noHeaders` (Boolean): Don't print MIME headers of the top-level MIME message/entity. 123 | 124 | 125 | ### `entity.isMultiPart()` 126 | 127 | Returns `true` if the current message/entity has a multipart "Content-Type" header ("multipart/mixed", "multipart/alternative"...). If so, the `entity.body` must be treated as an array. 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mimemessage.js 2 | 3 | MIME messages for JavaScript (RFC [2045](https://tools.ietf.org/html/rfc2045) & [2046](https://tools.ietf.org/html/rfc2046)). 4 | 5 | Suitable for parsing and generating MIME messages, allowing access to headers and body, including multipart messages such as: 6 | 7 | ``` 8 | From: Nathaniel Borenstein 9 | To: Ned Freed 10 | Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST) 11 | Subject: Sample message 12 | MIME-Version: 1.0 13 | Content-type: multipart/mixed; boundary="simple boundary" 14 | 15 | --simple boundary 16 | 17 | This is implicitly typed plain US-ASCII text. 18 | It does NOT end with a linebreak. 19 | --simple boundary 20 | Content-type: text/plain; charset=us-ascii 21 | 22 | This is explicitly typed plain US-ASCII text. 23 | It DOES end with a linebreak. 24 | 25 | --simple boundary-- 26 | ``` 27 | 28 | *NOTE:* This library is not intended for mail parsing (there are tons of MIME related Node libraries for that). In fact, it does not deal with encodings different that UTF-8. The purpose of this library is to be used in pure browser/Node environments in which MIME messages are useful to transmit data. 29 | 30 | 31 | ## Installation 32 | 33 | ### **npm**: 34 | 35 | ```bash 36 | $ npm install mimemessage --save 37 | ``` 38 | 39 | And then: 40 | 41 | ```javascript 42 | var mimemessage = require('mimemessage'); 43 | ``` 44 | 45 | 46 | ## Browserified library 47 | 48 | The browserified version of the library at `dist/mimemessage.js` exposes the global `window.mimemessage` module. 49 | 50 | ```html 51 | 52 | ``` 53 | 54 | 55 | ## Usage Example 56 | 57 | Let's build a complex multipart MIME message with the following content: 58 | 59 | * An HTML body. 60 | * An alternate plain text for those non HTML capable clients. 61 | * An attached PNG image named "mypicture.png" encoded in Base64. 62 | 63 | ```javascript 64 | var mimemessage = require('mimemessage'); 65 | var msg, alternateEntity, htmlEntity, plainEntity, pngEntity; 66 | 67 | // Build the top-level multipart MIME message. 68 | msg = mimemessage.factory({ 69 | contentType: 'multipart/mixed', 70 | body: [] 71 | }); 72 | msg.header('Message-ID', '<1234qwerty>'); 73 | 74 | // Build the multipart/alternate MIME entity containing both the HTML and plain text entities. 75 | alternateEntity = mimemessage.factory({ 76 | contentType: 'multipart/alternate', 77 | body: [] 78 | }); 79 | 80 | // Build the HTML MIME entity. 81 | htmlEntity = mimemessage.factory({ 82 | contentType: 'text/html;charset=utf-8', 83 | body: '

This is the HTML version of the message.

' 84 | }); 85 | 86 | // Build the plain text MIME entity. 87 | plainEntity = mimemessage.factory({ 88 | body: 'This is the plain text version of the message.' 89 | }); 90 | 91 | // Build the PNG MIME entity. 92 | pngEntity = mimemessage.factory({ 93 | contentType: 'image/png', 94 | contentTransferEncoding: 'base64', 95 | body: 'fVkVvYassFAAAABQAAAAIAAAAbWltZXR5cG==' 96 | }); 97 | pngEntity.header('Content-Disposition', 'attachment ;filename="mypicture.png"'); 98 | 99 | // Add both the HTML and plain text entities to the multipart/alternate entity. 100 | alternateEntity.body.push(htmlEntity); 101 | alternateEntity.body.push(plainEntity); 102 | 103 | // Add the multipart/alternate entity to the top-level MIME message. 104 | msg.body.push(alternateEntity); 105 | 106 | // Add the PNG entity to the top-level MIME message. 107 | msg.body.push(pngEntity); 108 | ``` 109 | 110 | By calling `msg.toString()` it produces the following MIME formatted string: 111 | 112 | ``` 113 | Content-Type: multipart/mixed;boundary=92ckNGfS 114 | Message-Id: <1234qwerty> 115 | 116 | --92ckNGfS 117 | Content-Type: multipart/alternate;boundary=EVGuDPPT 118 | 119 | --EVGuDPPT 120 | Content-Type: text/html;charset=utf-8 121 | 122 |

This is the HTML version of the message.

123 | --EVGuDPPT 124 | Content-Type: text/plain;charset=utf-8 125 | 126 | This is the plain text version of the message. 127 | --EVGuDPPT-- 128 | --92ckNGfS 129 | Content-Type: image/png 130 | Content-Transfer-Encoding: base64 131 | Content-Disposition: attachment ;filename="mypicture.png" 132 | 133 | fVkVvYassFAAAABQAAAAIAAAAbWltZXR5cG== 134 | --92ckNGfS-- 135 | ``` 136 | 137 | 138 | ## Documentation 139 | 140 | You can read the full [API documentation](docs/index.md) in the *docs* folder. 141 | 142 | ## Tests 143 | 144 | Run once, 145 | ```sh 146 | $ npm test 147 | ``` 148 | 149 | Watch mode: 150 | ```sh 151 | $ npm run test -- -w 152 | ``` 153 | 154 | ### Debugging 155 | 156 | The library includes the Node [debug](https://github.com/visionmedia/debug) module. In order to enable debugging: 157 | 158 | In Node set the `DEBUG=mimemessage*` environment variable before running the application, or set it at the top of the script: 159 | 160 | ```javascript 161 | process.env.DEBUG = 'mimemessage*'; 162 | ``` 163 | 164 | In the browser run `mimemessage.debug.enable('mimemessage*');` and reload the page. Note that the debugging settings are stored into the browser LocalStorage. To disable it run `mimemessage.debug.disable('mimemessage*');`. 165 | 166 | You can also set the envia the console, ex: 167 | ```sh 168 | DEBUG=mimemessage:* npm run test -- -w 169 | ``` 170 | 171 | 172 | ## Author 173 | 174 | Iñaki Baz Castillo 175 | Kay Lukas 176 | ProtonMail team 177 | 178 | 179 | ## License 180 | 181 | [MIT](./LICENSE) :) 182 | -------------------------------------------------------------------------------- /test/test_message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | const expect = require('expect.js'); 5 | const mimemessage = require('../'); 6 | const factory = require('../lib/factory'); 7 | 8 | /** 9 | * Local variables. 10 | */ 11 | 12 | 13 | describe('Message', () => { 14 | 15 | let msg = factory({ 16 | contentType: 'Text/Plain', 17 | contentTransferEncoding: 'BASE64', 18 | body: 'HELLO œæ€!' 19 | }); 20 | 21 | it('must create a MIME message via mimemessage.factory()', () => { 22 | expect(msg.contentType().type).to.be('text'); 23 | expect(msg.contentType().subtype).to.be('plain'); 24 | expect(msg.contentType().fulltype).to.be('text/plain'); 25 | expect(msg.contentTransferEncoding()).to.be('base64'); 26 | expect(msg.isMultiPart()).not.to.be.ok(); 27 | expect(msg.body).to.be('HELLO œæ€!'); 28 | }); 29 | 30 | it('must extend the MIME message via Message API', () => { 31 | 32 | msg.contentTransferEncoding('8BIT'); 33 | expect(msg.contentTransferEncoding()).to.be('8bit'); 34 | 35 | msg.body = []; 36 | expect(msg.isMultiPart()).to.be.ok(); 37 | expect(msg.contentType().type).to.be('multipart'); 38 | 39 | const part1 = factory({ 40 | body: 'PART1' 41 | }); 42 | 43 | msg.body.push(part1); 44 | expect(msg.body[0].contentType().fulltype).to.be('text/plain'); 45 | 46 | const part2 = factory({ 47 | contentType: 'multipart/alternative', 48 | body: [] 49 | }); 50 | 51 | msg.body.push(part2); 52 | expect(msg.body[1].contentType().fulltype).to.be('multipart/alternative'); 53 | 54 | part2.body.push(factory({ 55 | body: 'SUBPART1' 56 | })); 57 | part2.body.push(factory({ 58 | body: 'SUBPART2' 59 | })); 60 | }); 61 | 62 | it('must parse header custom with ;', () => { 63 | const contentType = 'image/png; filename="Fleshing out a sketch of a bird for a friend! - ;.png"; name="Fleshing out a sketch of a bird for a friend! - ;.png"'; 64 | const name = 'Fleshing out a sketch of a bird for a friend! - ;.png'; 65 | const msg = factory({ 66 | contentType 67 | }); 68 | expect(msg.contentType().params).to.eql({ 69 | name, filename: name 70 | }); 71 | }); 72 | 73 | 74 | it('must parse header with unicode', () => { 75 | const name = '🗳🧙️📩❤️💡😒🗳🗃😍💡😂.png'; 76 | const contentType = `image/png; filename="${name}"; name="${name}"`; 77 | const msg = factory({ 78 | contentType 79 | }); 80 | 81 | expect(/[^\u0000-\u00ff]/.test(msg.toString())).to.be(false); 82 | expect(/[^\u0000-\u00ff]/.test(msg.toString({ unicode: true }))).to.be(true); 83 | }); 84 | 85 | it('must encode content', () => { 86 | const entity = factory({ 87 | contentType: 'text/plain; filename=tada; name=tada', 88 | contentTransferEncoding: 'base64', 89 | body: '0'.repeat(200) 90 | }); 91 | 92 | expect(entity.toString()).to.equal(`Content-Type: text/plain; filename=tada; name=tada 93 | Content-Transfer-Encoding: base64 94 | 95 | MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw 96 | MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw 97 | MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw 98 | MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=`.replace(/\n/g, '\r\n')); 99 | 100 | const entityunicode = factory({ 101 | contentType: 'text/plain; filename=売伝済屯講天表禁衣佐後山; name=正見打実労叫投樫媛由峰図読時要位; charset=utf-8', 102 | contentTransferEncoding: 'quoted-printable', 103 | body: '🗳🧙️📩❤️💡😒正見打実労叫投樫媛由峰図読時要位🗳😍💡😂'.repeat(200) 104 | }); 105 | 106 | const parsed = mimemessage.parse(entityunicode.toString()); 107 | expect(parsed.contentType().params.name).to.equal('正見打実労叫投樫媛由峰図読時要位'); 108 | expect(parsed.contentType().params.filename).to.equal('売伝済屯講天表禁衣佐後山'); 109 | expect(parsed.body).to.equal('🗳🧙️📩❤️💡😒正見打実労叫投樫媛由峰図読時要位🗳😍💡😂'.repeat(200)); 110 | }); 111 | 112 | it('must encode utf8 content', () => { 113 | const entity = factory({ 114 | contentType: 'text/plain; filename=tada; name=tada; charset=utf-8', 115 | contentTransferEncoding: 'quoted-printable', 116 | body: 'ó'.repeat(200) 117 | }); 118 | 119 | expect(entity.toString()).to.equal(`Content-Type: text/plain; filename=tada; name=tada; charset=utf-8 120 | Content-Transfer-Encoding: quoted-printable 121 | 122 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3= 123 | =B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 124 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 125 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3= 126 | =B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 127 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 128 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3= 129 | =B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 130 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 131 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3= 132 | =B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 133 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 134 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3= 135 | =B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 136 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3= 137 | =C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3=B3=C3= 138 | =B3=C3=B3=C3=B3`.replace(/\n/g, '\r\n')); 139 | }); 140 | 141 | }); 142 | -------------------------------------------------------------------------------- /lib/Entity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose the Entity class. 3 | */ 4 | module.exports = Entity; 5 | 6 | /** 7 | * Dependencies. 8 | */ 9 | const rfc2047 = require('rfc2047'); 10 | const debug = require('debug')('mimemessage:Entity'); 11 | const debugerror = require('debug')('mimemessage:ERROR:Entity'); 12 | const grammar = require('./grammar'); 13 | const parseHeaderValue = require('./parse').parseHeaderValue; 14 | const encoding = require('./encoding'); 15 | 16 | debugerror.log = console.warn.bind(console); 17 | 18 | function Entity() { 19 | debug('new()'); 20 | 21 | this.headers = {}; 22 | this.internalBody = null; 23 | } 24 | 25 | Entity.prototype.contentType = function(value) { 26 | // Get. 27 | if (!value && value !== null) { 28 | return this.headers['Content-Type']; 29 | // Set. 30 | } 31 | if (value) { 32 | this.headers['Content-Type'] = parseHeaderValue(grammar.headerRules['Content-Type'], value); 33 | // Delete. 34 | } else { 35 | delete this.headers['Content-Type']; 36 | } 37 | }; 38 | 39 | Entity.prototype.contentDisposition = function(value) { 40 | // Get. 41 | if (!value && value !== null) { 42 | return this.headers['Content-Disposition']; 43 | // Set. 44 | } 45 | if (value) { 46 | this.headers['Content-Disposition'] = parseHeaderValue(grammar.headerRules['Content-Disposition'], value); 47 | // Delete. 48 | } else { 49 | delete this.headers['Content-Disposition']; 50 | } 51 | }; 52 | 53 | Entity.prototype.contentTransferEncoding = function(value) { 54 | const contentTransferEncoding = this.headers['Content-Transfer-Encoding']; 55 | 56 | // Get. 57 | if (!value && value !== null) { 58 | return contentTransferEncoding ? contentTransferEncoding.value : undefined; 59 | // Set. 60 | } 61 | if (value) { 62 | this.headers['Content-Transfer-Encoding'] = parseHeaderValue( 63 | grammar.headerRules['Content-Transfer-Encoding'], 64 | value 65 | ); 66 | // Delete. 67 | } else { 68 | delete this.headers['Content-Transfer-Encoding']; 69 | } 70 | }; 71 | 72 | Entity.prototype.header = function(name, value) { 73 | const headername = grammar.headerize(name); 74 | 75 | // Get. 76 | if (!value && value !== null) { 77 | if (this.headers[headername]) { 78 | return this.headers[headername].value; 79 | } 80 | // Set. 81 | } else if (value) { 82 | this.headers[headername] = { 83 | value 84 | }; 85 | // Delete. 86 | } else { 87 | delete this.headers[headername]; 88 | } 89 | }; 90 | 91 | Object.defineProperty(Entity.prototype, 'body', { 92 | get() { 93 | return this.internalBody; 94 | }, 95 | set(body) { 96 | if (body) { 97 | setBody.call(this, body); 98 | } else { 99 | delete this.internalBody; 100 | } 101 | } 102 | }); 103 | 104 | Entity.prototype.isMultiPart = function() { 105 | const contentType = this.headers['Content-Type']; 106 | 107 | return contentType && contentType.type === 'multipart'; 108 | }; 109 | 110 | Entity.prototype.toString = function(options = { noHeaders: false, unicode: false }) { 111 | let raw = ''; 112 | const contentType = this.headers['Content-Type']; 113 | 114 | const encode = options.unicode ? (x) => x : rfc2047.encode; 115 | 116 | if (!options.noHeaders) { 117 | // MIME headers. 118 | 119 | const headers = Object.keys(this.headers).map( 120 | (name) => { 121 | const val = this.headers[name].value; 122 | const list = val.split(';').map((val) => val.split('=').map(encode).join('=')); 123 | return name + ': ' + list.join(';') + '\r\n'; 124 | } 125 | ); 126 | raw = headers.join('') + '\r\n'; 127 | } 128 | 129 | // Body. 130 | if (Array.isArray(this.internalBody)) { 131 | const boundary = contentType.params.boundary; 132 | 133 | let i; 134 | const len = this.internalBody.length; 135 | for (i = 0; i < len; i++) { 136 | if (i > 0) { 137 | raw += '\r\n'; 138 | } 139 | raw += '--' + boundary + '\r\n' + this.internalBody[i].toString(options); 140 | } 141 | raw += '\r\n--' + boundary + '--'; 142 | } else if (typeof this.internalBody === 'string') { 143 | const { value } = this.headers['Content-Transfer-Encoding'] || {}; 144 | const { params: { charset = '' } = {} } = this.contentType() || {}; 145 | const transform = []; 146 | 147 | if (charset.replace(/-/g, '').toLowerCase() === 'utf8') { 148 | transform.push(encoding.encodeUTF8); 149 | } 150 | 151 | if (value === 'base64') { 152 | transform.push(encoding.encodeBase64); 153 | } else if (value === 'quoted-printable') { 154 | transform.push(encoding.encodeQP); 155 | } 156 | 157 | raw += transform.reduce((body, cb) => cb(body), this.internalBody); 158 | } else if (typeof this.internalBody === 'object') { 159 | raw += JSON.stringify(this.internalBody); 160 | } 161 | 162 | return raw; 163 | }; 164 | 165 | const random16bitHex = () => 166 | Math.floor(Math.random() * (2 << 15)) 167 | .toString(16) 168 | .padStart(4, 0); 169 | const random128bitHex = () => 170 | new Array(8) 171 | .fill(null) 172 | .map(random16bitHex) 173 | .join(''); 174 | 175 | const generateBoundary = () => `---------------------${random128bitHex()}`; 176 | 177 | /** 178 | * Private API. 179 | */ 180 | 181 | function setBody(body) { 182 | const contentType = this.headers['Content-Type']; 183 | 184 | this.internalBody = body; 185 | 186 | // Multipart internalBody. 187 | if (Array.isArray(body)) { 188 | if (!contentType || contentType.type !== 'multipart') { 189 | this.contentType('multipart/mixed;boundary=' + generateBoundary()); 190 | } else if (!contentType.params.boundary) { 191 | this.contentType(contentType.fulltype + ';boundary=' + generateBoundary()); 192 | } 193 | // Single internalBody. 194 | } else if (!contentType || contentType.type === 'multipart') { 195 | this.contentType('text/plain;charset=utf-8'); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose the parse function and some util funtions within it. 3 | */ 4 | module.exports = parse; 5 | parse.parseHeaderValue = parseHeaderValue; 6 | 7 | /** 8 | * Dependencies. 9 | */ 10 | const rfc2047 = require('rfc2047'); 11 | const debug = require('debug')('mimemessage:parse'); 12 | const debugerror = require('debug')('mimemessage:ERROR:parse'); 13 | const encoding = require('./encoding'); 14 | const grammar = require('./grammar'); 15 | const Entity = require('./Entity'); 16 | /** 17 | * Constants. 18 | */ 19 | const REGEXP_VALID_MIME_HEADER = /^([a-zA-Z0-9!#$%&'+,\-^_`|~]+)[ \t]*:[ \t]*(.+)$/; 20 | 21 | debugerror.log = console.warn.bind(console); 22 | 23 | function parse(rawMessage) { 24 | debug('parse()'); 25 | 26 | if (typeof rawMessage !== 'string') { 27 | throw new TypeError('given data must be a string'); 28 | } 29 | 30 | const entity = new Entity(); 31 | 32 | if (!parseEntity(entity, rawMessage, true)) { 33 | debugerror('invalid MIME message'); 34 | return false; 35 | } 36 | 37 | return entity; 38 | } 39 | 40 | function parseEntity(entity, rawEntity, topLevel) { 41 | debug('parseEntity()'); 42 | 43 | let headersEnd = -1; 44 | let rawHeaders; 45 | let rawBody; 46 | let match; 47 | let partStart; 48 | const parts = []; 49 | 50 | // Just look for headers if first line is not empty. 51 | if (/^[^\r\n]/.test(rawEntity)) { 52 | headersEnd = rawEntity.indexOf('\r\n\r\n'); 53 | } 54 | 55 | if (headersEnd !== -1) { 56 | rawHeaders = rawEntity.slice(0, headersEnd); 57 | rawBody = rawEntity.slice(headersEnd + 4); 58 | } else if (topLevel) { 59 | debugerror('parseEntity() | wrong MIME headers in top level entity'); 60 | return false; 61 | } else if (/^\r\n/.test(rawEntity)) { 62 | rawBody = rawEntity.slice(2); 63 | } else { 64 | debugerror('parseEntity() | wrong sub-entity'); 65 | return false; 66 | } 67 | 68 | if (rawHeaders && !parseEntityHeaders(entity, rawHeaders)) { 69 | return false; 70 | } 71 | 72 | const contentType = entity.contentType(); 73 | 74 | // Multipart internalBody. 75 | if (contentType && contentType.type === 'multipart') { 76 | const boundary = contentType.params.boundary; 77 | if (!boundary) { 78 | debugerror('parseEntity() | "multipart" Content-Type must have "boundary" parameter'); 79 | return false; 80 | } 81 | 82 | // Build the complete boundary regexps. 83 | const boundaryRegExp = new RegExp('(\\r\\n)?--' + boundary + '[\\t ]*\\r\\n', 'g'); 84 | const boundaryEndRegExp = new RegExp('\\r\\n--' + boundary + '--[\\t ]*'); 85 | 86 | while (true) { 87 | match = boundaryRegExp.exec(rawBody); 88 | 89 | if (match) { 90 | if (partStart !== undefined) { 91 | parts.push(rawBody.slice(partStart, match.index)); 92 | } 93 | 94 | partStart = boundaryRegExp.lastIndex; 95 | } else { 96 | if (partStart === undefined) { 97 | debugerror('parseEntity() | no bodies found in a "multipart" sub-entity'); 98 | return false; 99 | } 100 | 101 | boundaryEndRegExp.lastIndex = partStart; 102 | match = boundaryEndRegExp.exec(rawBody); 103 | 104 | if (!match) { 105 | debugerror('parseEntity() | no ending boundary in a "multipart" sub-entity'); 106 | return false; 107 | } 108 | 109 | parts.push(rawBody.slice(partStart, match.index)); 110 | break; 111 | } 112 | } 113 | 114 | entity.internalBody = []; 115 | 116 | const len = parts.length; 117 | for (let i = 0; i < len; i++) { 118 | const subEntity = new Entity(); 119 | entity.internalBody.push(subEntity); 120 | 121 | if (!parseEntity(subEntity, parts[i])) { 122 | debugerror('invalid MIME sub-entity'); 123 | return false; 124 | } 125 | } 126 | // Non multipart internalBody. 127 | } else { 128 | const transferencoding = entity.header('Content-Transfer-Encoding'); 129 | const { params: { charset = '' } = {} } = contentType || {}; 130 | const transform = []; 131 | 132 | if (transferencoding === 'base64') { 133 | transform.push(encoding.decodeBase64); 134 | } else if (transferencoding === 'quoted-printable') { 135 | transform.push(encoding.decodeQP); 136 | } 137 | 138 | if (charset.replace(/-/g, '').toLowerCase() === 'utf8') { 139 | transform.push(encoding.decodeUTF8); 140 | } 141 | 142 | entity.internalBody = transform.reduce((body, cb) => cb(body), rawBody); 143 | } 144 | 145 | return true; 146 | } 147 | 148 | function parseEntityHeaders(entity, rawHeaders) { 149 | const lines = rawHeaders.split('\r\n'); 150 | const len = lines.length; 151 | 152 | for (let i = 0; i < len; i++) { 153 | let line = lines[i]; 154 | 155 | while (/^[ \t]/.test(lines[i + 1])) { 156 | line = line + ' ' + lines[i + 1].trim(); 157 | i++; 158 | } 159 | 160 | if (!parseHeader(entity, line)) { 161 | debugerror('parseEntityHeaders() | invalid MIME header: "%s"', line); 162 | return false; 163 | } 164 | } 165 | 166 | return true; 167 | } 168 | 169 | function parseHeader(entity, rawHeader) { 170 | const match = rawHeader.match(REGEXP_VALID_MIME_HEADER); 171 | 172 | if (!match) { 173 | debugerror('invalid MIME header "%s"', rawHeader); 174 | return false; 175 | } 176 | 177 | const name = grammar.headerize(match[1]); 178 | const value = match[2]; 179 | 180 | const rule = grammar.headerRules[name] || grammar.unknownHeaderRule; 181 | 182 | let data; 183 | try { 184 | data = parseHeaderValue(rule, value); 185 | } catch (error) { 186 | debugerror('wrong MIME header: "%s"', rawHeader); 187 | return false; 188 | } 189 | 190 | entity.headers[name] = data; 191 | return true; 192 | } 193 | 194 | function parseHeaderValue(rule, value) { 195 | let parsedValue; 196 | let data = {}; 197 | 198 | const decodedvalue = value.split(';').map(rfc2047.decode).join(';'); 199 | 200 | if (typeof rule.reg !== 'function') { 201 | parsedValue = decodedvalue.match(rule.reg); 202 | if (!parsedValue) { 203 | throw new Error('parseHeaderValue() failed for ' + value); 204 | } 205 | const len = rule.names.length; 206 | for (let i = 0; i < len; i++) { 207 | if (parsedValue[i + 1] !== undefined) { 208 | data[rule.names[i]] = parsedValue[i + 1]; 209 | } 210 | } 211 | } else { 212 | data = rule.reg(decodedvalue); 213 | if (!data) { 214 | throw new Error('parseHeaderValue() failed for ' + value); 215 | } 216 | } 217 | 218 | if (!data.value) { 219 | data.value = decodedvalue; 220 | } 221 | 222 | 223 | return data; 224 | } 225 | -------------------------------------------------------------------------------- /test/test_parser.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const mimemessage = require('../'); 3 | const factory = require('../lib/factory'); 4 | const parse = require('../lib/parse'); 5 | const tools = require('./tools'); 6 | 7 | describe('Parser', () => { 8 | it('must parse msg1', () => { 9 | const raw = tools.readFile('msg1'); 10 | const msg = parse(raw); 11 | 12 | expect(msg).to.be.ok(); 13 | expect(msg.isMultiPart()).not.to.be.ok(); 14 | expect(msg.contentType().type).to.eql('text'); 15 | expect(msg.contentType().subtype).to.eql('plain'); 16 | expect(msg.contentType().fulltype).to.eql('text/plain'); 17 | expect(msg.contentType().params).to.eql({ 18 | charset: 'utf-8' 19 | }); 20 | expect(msg.body).to.be('Hi!\r\n'); 21 | }); 22 | 23 | it('must parse multipart msg2', () => { 24 | const raw = tools.readFile('msg2'); 25 | const msg = parse(raw); 26 | 27 | expect(msg).to.be.ok(); 28 | expect(msg.isMultiPart()).to.be.ok(); 29 | expect(msg.contentType().type).to.eql('multipart'); 30 | expect(msg.contentType().subtype).to.eql('mixed'); 31 | expect(msg.contentType().fulltype).to.eql('multipart/mixed'); 32 | expect(msg.contentType().params).to.eql({ 33 | boundary: 'simple boundary' 34 | }); 35 | 36 | const part1 = msg.body[0]; 37 | expect(part1).to.be.ok(); 38 | expect(part1.body).to.be('Body NOT ending with a linebreak.'); 39 | 40 | const part2 = msg.body[1]; 41 | expect(part2).to.be.ok(); 42 | expect(part2.contentType().type).to.eql('text'); 43 | expect(part2.contentType().subtype).to.eql('plain'); 44 | expect(part2.contentType().fulltype).to.eql('text/plain'); 45 | expect(part2.contentType().params).to.eql({ 46 | charset: 'us-ascii' 47 | }); 48 | expect(part2.body).to.be('Body ending with a linebreak.\r\n'); 49 | }); 50 | 51 | it('must parse recursive multipart msg3', () => { 52 | const raw = tools.readFile('msg3'); 53 | const msg = parse(raw); 54 | expect(msg).to.be.ok(); 55 | 56 | expect(msg.contentType().type).to.eql('multipart'); 57 | expect(msg.contentType().subtype).to.eql('mixed'); 58 | expect(msg.contentType().fulltype).to.eql('multipart/mixed'); 59 | expect(msg.contentType().params).to.eql({ 60 | boundary: 'AAAA' 61 | }); 62 | expect(msg.header('from')).to.eql('Iñaki Baz Castillo '); 63 | const partAAAA1 = msg.body[0]; 64 | 65 | expect(partAAAA1).to.be.ok(); 66 | expect(partAAAA1.body).to.be('body_AAAA_1'); 67 | const partAAAA2 = msg.body[1]; 68 | 69 | expect(partAAAA2).to.be.ok(); 70 | expect(partAAAA2.contentType().type).to.eql('multipart'); 71 | expect(partAAAA2.contentType().subtype).to.eql('alternative'); 72 | expect(partAAAA2.contentType().fulltype).to.eql('multipart/alternative'); 73 | expect(partAAAA2.contentType().params).to.eql({ 74 | boundary: 'BBBB' 75 | }); 76 | const partBBBB1 = partAAAA2.body[0]; 77 | 78 | expect(partBBBB1).to.be.ok(); 79 | expect(partBBBB1.contentType().type).to.eql('text'); 80 | expect(partBBBB1.contentType().subtype).to.eql('plain'); 81 | expect(partBBBB1.contentType().fulltype).to.eql('text/plain'); 82 | expect(partBBBB1.contentType().params).to.eql({}); 83 | expect(partBBBB1.body).to.be('body_BBBB_1\r\n'); 84 | const partBBBB2 = partAAAA2.body[1]; 85 | 86 | expect(partBBBB2).to.be.ok(); 87 | expect(partBBBB2.contentType().type).to.eql('text'); 88 | expect(partBBBB2.contentType().subtype).to.eql('html'); 89 | expect(partBBBB2.contentType().fulltype).to.eql('text/html'); 90 | expect(partBBBB2.contentType().params).to.eql({}); 91 | expect(partBBBB2.header('X-foo')).to.eql('bar'); 92 | expect(partBBBB2.body).to.be('

body_BBBB_1

'); 93 | const partAAAA3 = msg.body[2]; 94 | 95 | expect(partAAAA3).to.be.ok(); 96 | expect(partAAAA3.contentType().type).to.eql('text'); 97 | expect(partAAAA3.contentType().subtype).to.eql('plain'); 98 | expect(partAAAA3.contentType().fulltype).to.eql('text/plain'); 99 | expect(partAAAA3.contentType().params).to.eql({ 100 | charset: 'utf-8', 101 | bar: 'yes' 102 | }); 103 | expect(partAAAA3.contentTransferEncoding()).to.eql('quoted-printable'); 104 | expect(partAAAA3.body).to.be('body_AAAA_3\r\n'); 105 | const partAAAA4 = msg.body[3]; 106 | 107 | expect(partAAAA4).to.be.ok(); 108 | expect(partAAAA4.contentType().type).to.eql('application'); 109 | expect(partAAAA4.contentType().subtype).to.eql('epub+zip'); 110 | expect(partAAAA4.contentType().fulltype).to.eql('application/epub+zip'); 111 | expect(partAAAA4.contentType().params).to.eql({ 112 | name: 'Some Book.epub' 113 | }); 114 | expect(partAAAA4.header('content-disposition')).to.eql('attachment;filename="Some Book.epub"'); 115 | expect(partAAAA4.contentTransferEncoding()).to.eql('base64'); 116 | expect(partAAAA4.header('x-attachment-id')).to.eql('f_icxs58pn0'); 117 | expect(partAAAA4.body).to.be( 118 | Buffer.from( 119 | 'UEsDBBQAAAAAAAKfVkVvYassFAAAABQAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi9lcHVi==', 120 | 'base64' 121 | ).toString('binary') 122 | ); 123 | const normalizedRawPrinted = raw 124 | .toLowerCase() 125 | .replace(/[\t ]+/g, ' ') 126 | .replace(/\r\n[\t ]+/g, ' ') 127 | .trim(); 128 | 129 | const normalizedParsedPrinted = msg 130 | .toString() 131 | .toLowerCase() 132 | .replace(/[\t ]+/g, ' ') 133 | .trim(); 134 | 135 | expect(normalizedParsedPrinted).to.be(normalizedRawPrinted); 136 | }); 137 | 138 | it('must parse multipart msg4', () => { 139 | const raw = tools.readFile('msg4'); 140 | const msg = parse(raw); 141 | 142 | expect(msg).to.be.ok(); 143 | expect(msg.isMultiPart()).to.be.ok(); 144 | expect(msg.contentType().type).to.eql('multipart'); 145 | expect(msg.contentType().subtype).to.eql('mixed'); 146 | expect(msg.contentType().fulltype).to.eql('multipart/mixed'); 147 | expect(msg.contentType().params).to.eql({ 148 | boundary: '----=_Part_68_509885327.1447152748066' 149 | }); 150 | 151 | const part1 = msg.body[0]; 152 | expect(part1).to.be.ok(); 153 | 154 | const part2 = msg.body[1]; 155 | expect(part2).to.be.ok(); 156 | expect(part2.contentType().type).to.eql('message'); 157 | expect(part2.contentType().subtype).to.eql('cpim'); 158 | }); 159 | }); 160 | 161 | describe('Parse headers', () => { 162 | 163 | describe('Type as a string', () => { 164 | const headers = require('./headers/header1.json'); 165 | const formatted = factory(headers); 166 | 167 | it('must parse headers', () => { 168 | expect(formatted).to.be.ok(); 169 | }); 170 | 171 | it('must parse contentType', () => { 172 | const contentType = formatted.contentType(); 173 | expect(contentType.type).to.eql('image'); 174 | expect(contentType.subtype).to.eql('jpeg'); 175 | expect(contentType.fulltype).to.eql('image/jpeg'); 176 | expect(contentType.params).to.eql({ 177 | name: 'IMG_83201.jpeg' 178 | }); 179 | }); 180 | 181 | it('must parse contentDisposition', () => { 182 | const contentDisposition = formatted.contentDisposition(); 183 | expect(contentDisposition.fulltype).to.eql('inline; filename="IMG_83201.jpeg"; size=91134'); 184 | expect(contentDisposition.params).to.eql({ 185 | filename: 'IMG_83201.jpeg', 186 | size: '91134' 187 | }); 188 | }); 189 | 190 | it('must parse contentTransferEncoding', () => { 191 | const contentTransferEncoding = formatted.contentTransferEncoding(); 192 | expect(contentTransferEncoding).to.eql('base64'); 193 | }); 194 | }); 195 | 196 | describe('Type headers array + non standard', () => { 197 | const headers = require('./headers/header2.json'); 198 | const formatted = factory(headers); 199 | 200 | it('must parse headers', () => { 201 | expect(formatted).to.be.ok(); 202 | }); 203 | 204 | it('must parse contentType', () => { 205 | const contentType = formatted.contentType(); 206 | expect(contentType.type).to.eql('image'); 207 | expect(contentType.subtype).to.eql('png'); 208 | expect(contentType.fulltype).to.eql('image/png'); 209 | 210 | expect(contentType.params).to.eql({ name: 'logo.png' }); 211 | }); 212 | 213 | it('must parse contentDisposition', () => { 214 | 215 | const contentDisposition = formatted.contentDisposition(); 216 | expect(contentDisposition.fulltype).to.eql('inline; filename="logo.png"'); 217 | expect(contentDisposition.params).to.eql({ 218 | filename: 'logo.png' 219 | }); 220 | }); 221 | 222 | it('must parse contentTransferEncoding', () => { 223 | const contentTransferEncoding = formatted.contentTransferEncoding(); 224 | expect(contentTransferEncoding).to.eql('base64'); 225 | }); 226 | }); 227 | 228 | 229 | }); 230 | -------------------------------------------------------------------------------- /dist/mimemessage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 6 | 7 | var rfc2047 = _interopDefault(require('rfc2047')); 8 | var debug = _interopDefault(require('debug')); 9 | 10 | function _typeof(obj) { 11 | if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { 12 | _typeof = function (obj) { 13 | return typeof obj; 14 | }; 15 | } else { 16 | _typeof = function (obj) { 17 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 18 | }; 19 | } 20 | 21 | return _typeof(obj); 22 | } 23 | 24 | /** 25 | * Exported object. 26 | */ 27 | var grammar = {}; 28 | var grammar_1 = grammar; 29 | /** 30 | * Constants. 31 | */ 32 | 33 | var REGEXP_CONTENT_TYPE = /^([^\t /]+)\/([^\t ;]+)(.*)$/; 34 | var REGEXP_CONTENT_TRANSFER_ENCODING = /^([a-zA-Z0-9\-_]+)$/; 35 | var REGEXP_PARAM_KEY = /;[ \t|]*([^\t =]+)[ \t]*=[ \t]*/g; 36 | var REGEXP_PARAM_VALUES = /[ \t]*([^"\t =]+|"([^"]*)")[ \t]*$/; 37 | grammar.headerRules = { 38 | 'Content-Type': { 39 | reg: function reg(value) { 40 | var match = value.match(REGEXP_CONTENT_TYPE); 41 | var params = {}; 42 | 43 | if (!match) { 44 | return undefined; 45 | } 46 | 47 | if (match[3]) { 48 | params = parseParams(match[3]); 49 | 50 | if (!params) { 51 | return undefined; 52 | } 53 | } 54 | 55 | return { 56 | fulltype: match[1].toLowerCase() + '/' + match[2].toLowerCase(), 57 | type: match[1].toLowerCase(), 58 | subtype: match[2].toLowerCase(), 59 | params: params 60 | }; 61 | } 62 | }, 63 | 'Content-Disposition': { 64 | reg: function reg(value) { 65 | return { 66 | fulltype: value, 67 | params: parseParams(value) 68 | }; 69 | } 70 | }, 71 | 'Content-Transfer-Encoding': { 72 | reg: function reg(value) { 73 | var match = value.match(REGEXP_CONTENT_TRANSFER_ENCODING); 74 | 75 | if (!match) { 76 | return undefined; 77 | } 78 | 79 | return { 80 | value: match[1].toLowerCase() 81 | }; 82 | } 83 | } 84 | }; 85 | grammar.unknownHeaderRule = { 86 | reg: /(.*)/, 87 | names: ['value'] 88 | }; 89 | 90 | grammar.headerize = function (string) { 91 | var exceptions = { 92 | 'Mime-Version': 'MIME-Version', 93 | 'Content-Id': 'Content-ID' 94 | }; 95 | var name = string.toLowerCase().replace(/_/g, '-').split('-'); 96 | var parts = name.length; 97 | var hname = ''; 98 | var part; 99 | 100 | for (part = 0; part < parts; part++) { 101 | if (part !== 0) { 102 | hname += '-'; 103 | } 104 | 105 | hname += name[part].charAt(0).toUpperCase() + name[part].substring(1); 106 | } 107 | 108 | if (exceptions[hname]) { 109 | hname = exceptions[hname]; 110 | } 111 | 112 | return hname; 113 | }; // Set sensible defaults to avoid polluting the grammar with boring details. 114 | 115 | 116 | Object.keys(grammar.headerRules).forEach(function (name) { 117 | var rule = grammar.headerRules[name]; 118 | 119 | if (!rule.reg) { 120 | rule.reg = /(.*)/; 121 | } 122 | }); 123 | /** 124 | * Private API. 125 | */ 126 | 127 | function parseParams(rawParams) { 128 | if (rawParams === '' || rawParams === undefined || rawParams === null) { 129 | return {}; 130 | } 131 | 132 | var splittedParams = rawParams.split(REGEXP_PARAM_KEY); 133 | return splittedParams.slice(1).reduce(function (acc, key, i, list) { 134 | if (!(i % 2)) { 135 | var values = (list[i + 1] || '').match(REGEXP_PARAM_VALUES) || []; 136 | acc[key.toLowerCase()] = values[2] || values[1]; 137 | } 138 | 139 | return acc; 140 | }, Object.create(null)); 141 | } 142 | 143 | var RFC2045_LIMIT = 76; 144 | 145 | var wrapline = function wrapline(line) { 146 | var escape = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 147 | var limit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : RFC2045_LIMIT; 148 | var lineCount = Math.ceil(line.length / limit); 149 | var result = Array.from({ 150 | length: lineCount 151 | }, function (_, i) { 152 | return line.substring(limit * i, limit * (i + 1)); 153 | }); 154 | return result.join(escape + '\r\n'); 155 | }; // the newlines in mime messages are \r\n. This function expects \n as incoming lines and produces \r\n newlines. 156 | 157 | 158 | var wraplines = function wraplines(lines) { 159 | var escape = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 160 | var limit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : RFC2045_LIMIT; 161 | return lines.split('\n').map(function (line) { 162 | return wrapline(line, escape, limit); 163 | }).join('\r\n'); 164 | }; // Don't escape newlines, tabs, everything between space and ~ save the = sign. 165 | 166 | 167 | var MATCH_ESCAPE_CHARS = /[^\t\n\r\x20-\x3C\x3E-\x7E]/g; 168 | 169 | var encodeQPSequence = function encodeQPSequence(char) { 170 | return '=' + ('00' + char.charCodeAt(0).toString(16).toUpperCase()).substr(-2); 171 | }; 172 | 173 | var encodeQPSequences = function encodeQPSequences(input) { 174 | return input.replace(MATCH_ESCAPE_CHARS, encodeQPSequence); 175 | }; 176 | 177 | var normalLinebreaks = function normalLinebreaks(input) { 178 | return input.replace(/(\r\n|\n|\r)/g, '\n'); 179 | }; // restore wrapping in escape sequences ==\r\n0D, =0\r\nD -> =0D=\r\n 180 | 181 | 182 | var restoreQPSequences = function restoreQPSequences(input) { 183 | return input.replace(/(?=.{0,2}=\r\n)(=(=\r\n)?[0-9A-F](=\r\n)?[0-9A-F])/g, function (seq) { 184 | return seq.replace(/=\r\n/, '') + '=\r\n'; 185 | }); 186 | }; 187 | 188 | var wrapQPLines = function wrapQPLines(input) { 189 | return restoreQPSequences(wraplines(input, '=', RFC2045_LIMIT - 2)); 190 | }; 191 | 192 | var encodeQPTrailingSpace = function encodeQPTrailingSpace(input) { 193 | return input.replace(/ $/gm, ' =\r\n\r\n'); 194 | }; 195 | 196 | var encodeUTF8 = function encodeUTF8(value) { 197 | return unescape(encodeURIComponent(value)); 198 | }; 199 | 200 | var decodeUTF8 = function decodeUTF8(value) { 201 | try { 202 | return decodeURIComponent(escape(value)); 203 | } catch (e) { 204 | return value; 205 | } 206 | }; 207 | 208 | var base64encode = typeof btoa === 'undefined' ? function (str) { 209 | return Buffer.from(str, 'binary').toString('base64'); 210 | } : btoa; 211 | var base64decode = typeof atob === 'undefined' ? function (str) { 212 | return Buffer.from(str, 'base64').toString('binary'); 213 | } : atob; 214 | 215 | var encodeBase64 = function encodeBase64(value) { 216 | return wraplines(base64encode(value)); 217 | }; 218 | 219 | var decodeBase64 = function decodeBase64(value) { 220 | return decodeUTF8(base64decode(value)); 221 | }; 222 | /** 223 | * Quoted-Printable, or QP encoding, is an encoding using printable ASCII characters 224 | * (alphanumeric and the equals sign =) to transmit 8-bit data over a 7-bit data path) 225 | * Any 8-bit byte value may be encoded with 3 characters: an = followed by two hexadecimal digits (0–9 or A–F) 226 | * representing the byte's numeric value. For example, an ASCII form feed character (decimal value 12) can be 227 | * represented by "=0C", and an ASCII equal sign (decimal value 61) must be represented by =3D. 228 | * All characters except printable ASCII characters or end of line characters (but also =) 229 | * must be encoded in this fashion. 230 | * 231 | * All printable ASCII characters (decimal values between 33 and 126) may be represented by themselves, except = 232 | * (decimal 61). 233 | * 234 | * @param binarydata 235 | * @return 7-bit encoding of the input using QP encoding 236 | */ 237 | 238 | 239 | var encodeQP = function encodeQP(binarydata) { 240 | return encodeQPTrailingSpace(wrapQPLines(normalLinebreaks(encodeQPSequences(binarydata)))); 241 | }; 242 | 243 | var removeSoftBreaks = function removeSoftBreaks(value) { 244 | return value.replace(/=(\r\n|\n|\r)|/g, ''); 245 | }; 246 | 247 | var decodeQuotedPrintables = function decodeQuotedPrintables(value) { 248 | return value.replace(/=([0-9A-F][0-9A-F])/gm, function (match, contents) { 249 | return String.fromCharCode(parseInt(contents, 16)); 250 | }); 251 | }; 252 | 253 | var decodeQP = function decodeQP(value) { 254 | return decodeQuotedPrintables(removeSoftBreaks(value)); 255 | }; 256 | 257 | var encoding = { 258 | encodeBase64: encodeBase64, 259 | decodeBase64: decodeBase64, 260 | encodeQP: encodeQP, 261 | decodeQP: decodeQP, 262 | encodeUTF8: encodeUTF8, 263 | decodeUTF8: decodeUTF8 264 | }; 265 | 266 | /** 267 | * Expose the parse function and some util funtions within it. 268 | */ 269 | 270 | var parse_1 = parse; 271 | parse.parseHeaderValue = parseHeaderValue; 272 | /** 273 | * Dependencies. 274 | */ 275 | 276 | var debug$1 = debug('mimemessage:parse'); 277 | var debugerror = debug('mimemessage:ERROR:parse'); 278 | /** 279 | * Constants. 280 | */ 281 | 282 | var REGEXP_VALID_MIME_HEADER = /^([a-zA-Z0-9!#$%&'+,\-^_`|~]+)[ \t]*:[ \t]*(.+)$/; 283 | debugerror.log = console.warn.bind(console); 284 | 285 | function parse(rawMessage) { 286 | debug$1('parse()'); 287 | 288 | if (typeof rawMessage !== 'string') { 289 | throw new TypeError('given data must be a string'); 290 | } 291 | 292 | var entity = new Entity_1(); 293 | 294 | if (!parseEntity(entity, rawMessage, true)) { 295 | debugerror('invalid MIME message'); 296 | return false; 297 | } 298 | 299 | return entity; 300 | } 301 | 302 | function parseEntity(entity, rawEntity, topLevel) { 303 | debug$1('parseEntity()'); 304 | var headersEnd = -1; 305 | var rawHeaders; 306 | var rawBody; 307 | var match; 308 | var partStart; 309 | var parts = []; // Just look for headers if first line is not empty. 310 | 311 | if (/^[^\r\n]/.test(rawEntity)) { 312 | headersEnd = rawEntity.indexOf('\r\n\r\n'); 313 | } 314 | 315 | if (headersEnd !== -1) { 316 | rawHeaders = rawEntity.slice(0, headersEnd); 317 | rawBody = rawEntity.slice(headersEnd + 4); 318 | } else if (topLevel) { 319 | debugerror('parseEntity() | wrong MIME headers in top level entity'); 320 | return false; 321 | } else if (/^\r\n/.test(rawEntity)) { 322 | rawBody = rawEntity.slice(2); 323 | } else { 324 | debugerror('parseEntity() | wrong sub-entity'); 325 | return false; 326 | } 327 | 328 | if (rawHeaders && !parseEntityHeaders(entity, rawHeaders)) { 329 | return false; 330 | } 331 | 332 | var contentType = entity.contentType(); // Multipart internalBody. 333 | 334 | if (contentType && contentType.type === 'multipart') { 335 | var boundary = contentType.params.boundary; 336 | 337 | if (!boundary) { 338 | debugerror('parseEntity() | "multipart" Content-Type must have "boundary" parameter'); 339 | return false; 340 | } // Build the complete boundary regexps. 341 | 342 | 343 | var boundaryRegExp = new RegExp('(\\r\\n)?--' + boundary + '[\\t ]*\\r\\n', 'g'); 344 | var boundaryEndRegExp = new RegExp('\\r\\n--' + boundary + '--[\\t ]*'); 345 | 346 | while (true) { 347 | match = boundaryRegExp.exec(rawBody); 348 | 349 | if (match) { 350 | if (partStart !== undefined) { 351 | parts.push(rawBody.slice(partStart, match.index)); 352 | } 353 | 354 | partStart = boundaryRegExp.lastIndex; 355 | } else { 356 | if (partStart === undefined) { 357 | debugerror('parseEntity() | no bodies found in a "multipart" sub-entity'); 358 | return false; 359 | } 360 | 361 | boundaryEndRegExp.lastIndex = partStart; 362 | match = boundaryEndRegExp.exec(rawBody); 363 | 364 | if (!match) { 365 | debugerror('parseEntity() | no ending boundary in a "multipart" sub-entity'); 366 | return false; 367 | } 368 | 369 | parts.push(rawBody.slice(partStart, match.index)); 370 | break; 371 | } 372 | } 373 | 374 | entity.internalBody = []; 375 | var len = parts.length; 376 | 377 | for (var i = 0; i < len; i++) { 378 | var subEntity = new Entity_1(); 379 | entity.internalBody.push(subEntity); 380 | 381 | if (!parseEntity(subEntity, parts[i])) { 382 | debugerror('invalid MIME sub-entity'); 383 | return false; 384 | } 385 | } // Non multipart internalBody. 386 | 387 | } else { 388 | var transferencoding = entity.header('Content-Transfer-Encoding'); 389 | 390 | var _ref = contentType || {}, 391 | _ref$params = _ref.params; 392 | 393 | _ref$params = _ref$params === void 0 ? {} : _ref$params; 394 | var _ref$params$charset = _ref$params.charset, 395 | charset = _ref$params$charset === void 0 ? '' : _ref$params$charset; 396 | var transform = []; 397 | 398 | if (transferencoding === 'base64') { 399 | transform.push(encoding.decodeBase64); 400 | } else if (transferencoding === 'quoted-printable') { 401 | transform.push(encoding.decodeQP); 402 | } 403 | 404 | if (charset.replace(/-/g, '').toLowerCase() === 'utf8') { 405 | transform.push(encoding.decodeUTF8); 406 | } 407 | 408 | entity.internalBody = transform.reduce(function (body, cb) { 409 | return cb(body); 410 | }, rawBody); 411 | } 412 | 413 | return true; 414 | } 415 | 416 | function parseEntityHeaders(entity, rawHeaders) { 417 | var lines = rawHeaders.split('\r\n'); 418 | var len = lines.length; 419 | 420 | for (var i = 0; i < len; i++) { 421 | var line = lines[i]; 422 | 423 | while (/^[ \t]/.test(lines[i + 1])) { 424 | line = line + ' ' + lines[i + 1].trim(); 425 | i++; 426 | } 427 | 428 | if (!parseHeader(entity, line)) { 429 | debugerror('parseEntityHeaders() | invalid MIME header: "%s"', line); 430 | return false; 431 | } 432 | } 433 | 434 | return true; 435 | } 436 | 437 | function parseHeader(entity, rawHeader) { 438 | var match = rawHeader.match(REGEXP_VALID_MIME_HEADER); 439 | 440 | if (!match) { 441 | debugerror('invalid MIME header "%s"', rawHeader); 442 | return false; 443 | } 444 | 445 | var name = grammar_1.headerize(match[1]); 446 | var value = match[2]; 447 | var rule = grammar_1.headerRules[name] || grammar_1.unknownHeaderRule; 448 | var data; 449 | 450 | try { 451 | data = parseHeaderValue(rule, value); 452 | } catch (error) { 453 | debugerror('wrong MIME header: "%s"', rawHeader); 454 | return false; 455 | } 456 | 457 | entity.headers[name] = data; 458 | return true; 459 | } 460 | 461 | function parseHeaderValue(rule, value) { 462 | var parsedValue; 463 | var data = {}; 464 | var decodedvalue = value.split(';').map(rfc2047.decode).join(';'); 465 | 466 | if (typeof rule.reg !== 'function') { 467 | parsedValue = decodedvalue.match(rule.reg); 468 | 469 | if (!parsedValue) { 470 | throw new Error('parseHeaderValue() failed for ' + value); 471 | } 472 | 473 | var len = rule.names.length; 474 | 475 | for (var i = 0; i < len; i++) { 476 | if (parsedValue[i + 1] !== undefined) { 477 | data[rule.names[i]] = parsedValue[i + 1]; 478 | } 479 | } 480 | } else { 481 | data = rule.reg(decodedvalue); 482 | 483 | if (!data) { 484 | throw new Error('parseHeaderValue() failed for ' + value); 485 | } 486 | } 487 | 488 | if (!data.value) { 489 | data.value = decodedvalue; 490 | } 491 | 492 | return data; 493 | } 494 | 495 | /** 496 | * Expose the Entity class. 497 | */ 498 | 499 | var Entity_1 = Entity; 500 | /** 501 | * Dependencies. 502 | */ 503 | 504 | var debug$2 = debug('mimemessage:Entity'); 505 | var debugerror$1 = debug('mimemessage:ERROR:Entity'); 506 | var parseHeaderValue$1 = parse_1.parseHeaderValue; 507 | debugerror$1.log = console.warn.bind(console); 508 | 509 | function Entity() { 510 | debug$2('new()'); 511 | this.headers = {}; 512 | this.internalBody = null; 513 | } 514 | 515 | Entity.prototype.contentType = function (value) { 516 | // Get. 517 | if (!value && value !== null) { 518 | return this.headers['Content-Type']; // Set. 519 | } 520 | 521 | if (value) { 522 | this.headers['Content-Type'] = parseHeaderValue$1(grammar_1.headerRules['Content-Type'], value); // Delete. 523 | } else { 524 | delete this.headers['Content-Type']; 525 | } 526 | }; 527 | 528 | Entity.prototype.contentDisposition = function (value) { 529 | // Get. 530 | if (!value && value !== null) { 531 | return this.headers['Content-Disposition']; // Set. 532 | } 533 | 534 | if (value) { 535 | this.headers['Content-Disposition'] = parseHeaderValue$1(grammar_1.headerRules['Content-Disposition'], value); // Delete. 536 | } else { 537 | delete this.headers['Content-Disposition']; 538 | } 539 | }; 540 | 541 | Entity.prototype.contentTransferEncoding = function (value) { 542 | var contentTransferEncoding = this.headers['Content-Transfer-Encoding']; // Get. 543 | 544 | if (!value && value !== null) { 545 | return contentTransferEncoding ? contentTransferEncoding.value : undefined; // Set. 546 | } 547 | 548 | if (value) { 549 | this.headers['Content-Transfer-Encoding'] = parseHeaderValue$1(grammar_1.headerRules['Content-Transfer-Encoding'], value); // Delete. 550 | } else { 551 | delete this.headers['Content-Transfer-Encoding']; 552 | } 553 | }; 554 | 555 | Entity.prototype.header = function (name, value) { 556 | var headername = grammar_1.headerize(name); // Get. 557 | 558 | if (!value && value !== null) { 559 | if (this.headers[headername]) { 560 | return this.headers[headername].value; 561 | } // Set. 562 | 563 | } else if (value) { 564 | this.headers[headername] = { 565 | value: value 566 | }; // Delete. 567 | } else { 568 | delete this.headers[headername]; 569 | } 570 | }; 571 | 572 | Object.defineProperty(Entity.prototype, 'body', { 573 | get: function get() { 574 | return this.internalBody; 575 | }, 576 | set: function set(body) { 577 | if (body) { 578 | setBody.call(this, body); 579 | } else { 580 | delete this.internalBody; 581 | } 582 | } 583 | }); 584 | 585 | Entity.prototype.isMultiPart = function () { 586 | var contentType = this.headers['Content-Type']; 587 | return contentType && contentType.type === 'multipart'; 588 | }; 589 | 590 | Entity.prototype.toString = function () { 591 | var _this = this; 592 | 593 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { 594 | noHeaders: false, 595 | unicode: false 596 | }; 597 | var raw = ''; 598 | var contentType = this.headers['Content-Type']; 599 | var encode = options.unicode ? function (x) { 600 | return x; 601 | } : rfc2047.encode; 602 | 603 | if (!options.noHeaders) { 604 | // MIME headers. 605 | var headers = Object.keys(this.headers).map(function (name) { 606 | var val = _this.headers[name].value; 607 | var list = val.split(';').map(function (val) { 608 | return val.split('=').map(encode).join('='); 609 | }); 610 | return name + ': ' + list.join(';') + '\r\n'; 611 | }); 612 | raw = headers.join('') + '\r\n'; 613 | } // Body. 614 | 615 | 616 | if (Array.isArray(this.internalBody)) { 617 | var boundary = contentType.params.boundary; 618 | var i; 619 | var len = this.internalBody.length; 620 | 621 | for (i = 0; i < len; i++) { 622 | if (i > 0) { 623 | raw += '\r\n'; 624 | } 625 | 626 | raw += '--' + boundary + '\r\n' + this.internalBody[i].toString(options); 627 | } 628 | 629 | raw += '\r\n--' + boundary + '--'; 630 | } else if (typeof this.internalBody === 'string') { 631 | var _ref = this.headers['Content-Transfer-Encoding'] || {}, 632 | value = _ref.value; 633 | 634 | var _ref2 = this.contentType() || {}, 635 | _ref2$params = _ref2.params; 636 | 637 | _ref2$params = _ref2$params === void 0 ? {} : _ref2$params; 638 | var _ref2$params$charset = _ref2$params.charset, 639 | charset = _ref2$params$charset === void 0 ? '' : _ref2$params$charset; 640 | var transform = []; 641 | 642 | if (charset.replace(/-/g, '').toLowerCase() === 'utf8') { 643 | transform.push(encoding.encodeUTF8); 644 | } 645 | 646 | if (value === 'base64') { 647 | transform.push(encoding.encodeBase64); 648 | } else if (value === 'quoted-printable') { 649 | transform.push(encoding.encodeQP); 650 | } 651 | 652 | raw += transform.reduce(function (body, cb) { 653 | return cb(body); 654 | }, this.internalBody); 655 | } else if (_typeof(this.internalBody) === 'object') { 656 | raw += JSON.stringify(this.internalBody); 657 | } 658 | 659 | return raw; 660 | }; 661 | 662 | var random16bitHex = function random16bitHex() { 663 | return Math.floor(Math.random() * (2 << 15)).toString(16).padStart(4, 0); 664 | }; 665 | 666 | var random128bitHex = function random128bitHex() { 667 | return new Array(8).fill(null).map(random16bitHex).join(''); 668 | }; 669 | 670 | var generateBoundary = function generateBoundary() { 671 | return "---------------------".concat(random128bitHex()); 672 | }; 673 | /** 674 | * Private API. 675 | */ 676 | 677 | 678 | function setBody(body) { 679 | var contentType = this.headers['Content-Type']; 680 | this.internalBody = body; // Multipart internalBody. 681 | 682 | if (Array.isArray(body)) { 683 | if (!contentType || contentType.type !== 'multipart') { 684 | this.contentType('multipart/mixed;boundary=' + generateBoundary()); 685 | } else if (!contentType.params.boundary) { 686 | this.contentType(contentType.fulltype + ';boundary=' + generateBoundary()); 687 | } // Single internalBody. 688 | 689 | } else if (!contentType || contentType.type === 'multipart') { 690 | this.contentType('text/plain;charset=utf-8'); 691 | } 692 | } 693 | 694 | /** 695 | * Expose the factory function. 696 | */ 697 | 698 | var factory_1 = factory; 699 | /** 700 | * Dependencies. 701 | */ 702 | 703 | var debug$3 = debug('mimemessage:factory'); 704 | var debugerror$2 = debug('mimemessage:ERROR:factory'); 705 | debugerror$2.log = console.warn.bind(console); 706 | 707 | function buildEntity(data) { 708 | var entity = new Entity_1(); // Add Content-Type. 709 | 710 | if (data.contentType) { 711 | entity.contentType(data.contentType); 712 | } // Add Content-Disposition. 713 | 714 | 715 | if (data.contentDisposition) { 716 | entity.contentDisposition(data.contentDisposition); 717 | } // Add Content-Transfer-Encoding. 718 | 719 | 720 | if (data.contentTransferEncoding) { 721 | entity.contentTransferEncoding(data.contentTransferEncoding); 722 | } // Add body. 723 | 724 | 725 | if (data.body) { 726 | entity.body = data.body; 727 | } 728 | 729 | return entity; 730 | } 731 | 732 | var formatKey = function formatKey(key) { 733 | if (key === 'contentTransfer') { 734 | return 'contentTransferEncoding'; 735 | } 736 | 737 | return key; 738 | }; 739 | 740 | function factory() { 741 | var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 742 | debug$3('factory() | [data:%o]', data); 743 | var stringifyKey = ['contentType', 'contentDisposition', 'contentTransferEncoding']; 744 | /* 745 | Some keys can be an array, as headers are strings we parse them 746 | then we keep only the longest string. 747 | ex: 748 | "contentType": [ 749 | "image/png; name=\"logo.png\"", 750 | "image/png" 751 | ], 752 | Output: 753 | "contentType": "image/png; name=\"logo.png\"" 754 | Some key are also non-standard ex: contentTransfer instead of contentTransferEncoding, we format the key too. 755 | */ 756 | 757 | var config = Object.keys(data).reduce(function (acc, item) { 758 | var key = formatKey(item); 759 | 760 | if (stringifyKey.includes(key) && Array.isArray(data[item])) { 761 | acc[key] = data[item][0]; // BE convention is to do the first one 762 | 763 | return acc; 764 | } 765 | 766 | acc[key] = data[key]; 767 | return acc; 768 | }, Object.create(null)); 769 | return buildEntity(config); 770 | } 771 | 772 | var mimemessage = { 773 | factory: factory_1, 774 | parse: parse_1, 775 | Entity: Entity_1 776 | }; 777 | var mimemessage_1 = mimemessage.factory; 778 | var mimemessage_2 = mimemessage.parse; 779 | var mimemessage_3 = mimemessage.Entity; 780 | 781 | exports.default = mimemessage; 782 | exports.factory = mimemessage_1; 783 | exports.parse = mimemessage_2; 784 | exports.Entity = mimemessage_3; 785 | --------------------------------------------------------------------------------