├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── dist ├── builder.js └── utils.js ├── package.json ├── scripts └── build.sh ├── src ├── builder-unit.js ├── builder.js ├── utils-unit.js └── utils.js └── testutils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | package-lock.json 5 | 6 | # VIM Swap Files 7 | [._]*.s[a-v][a-z] 8 | [._]*.sw[a-p] 9 | [._]s[a-v][a-z] 10 | [._]sw[a-p] 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - lts/* 5 | notifications: 6 | email: 7 | recipients: 8 | - felix.hammerl@gmail.com 9 | script: 10 | - npm test 11 | deploy: 12 | provider: npm 13 | email: felix.hammerl+emailjs-deployment-user@gmail.com 14 | api_key: 15 | secure: XJrE1GjoXwyqZUdDhUKlUUOIFEDvNJbImz219q5kZstIJC28pn858gT91/DFOkTgKySlTrzVn7Sa0g8Ogws6cBqTbb1B1CtkDm+wC5FYPrOPRdSHbcBKgNtiQsSeNdPxlEB9WU6DxzppPNVFJoAf9g3TfTdvxWlwPWJGgzeQbP8= 16 | on: 17 | tags: true 18 | all_branches: true 19 | condition: "$TRAVIS_TAG =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+" 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run ES6 Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 10 | "stopOnEntry": false, 11 | "args": [ 12 | "./src/*-unit.js", 13 | "--require", "babel-register", "testutils.js", 14 | "--reporter", "spec", 15 | "--no-timeouts" 16 | ], 17 | "runtimeArgs": [ 18 | "--nolazy" 19 | ], 20 | "sourceMaps": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emailjs-mime-builder 2 | 3 | ## DEPRECATION NOTICE 4 | 5 | This project is not actively being maintained. If you're sending emails on a node.js-esque platform, please use Andris Reinman's [nodemailer](https://github.com/nodemailer/nodemailer). It is actively supported, more widely used and maintained offers more possibilities for sending mails than this project. 6 | 7 | Background: This project was created because there was no option of using SMTP in a browser environment. This use case has been eliminated since Chrome Apps reached end of life and Firefox OS was scrapped. If you're on an electron-based platform, please use the capabilities that come with a full fledged node.js backend. 8 | 9 | If you still feel this project has merit and you would like to be a maintainer, please reach out to me. 10 | 11 | 12 | [![Greenkeeper badge](https://badges.greenkeeper.io/emailjs/emailjs-mime-builder.svg)](https://greenkeeper.io/) 13 | 14 | *emailjs-mime-builder* is a low level rfc2822 message composer. Define your own mime tree, no magic included. 15 | 16 | [![Greenkeeper badge](https://badges.greenkeeper.io/emailjs/emailjs-mime-builder.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.org/emailjs/emailjs-mime-builder.png?branch=master)](https://travis-ci.org/emailjs/emailjs-mime-builder) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) [![ES6+](https://camo.githubusercontent.com/567e52200713e0f0c05a5238d91e1d096292b338/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f65732d362b2d627269676874677265656e2e737667)](https://kangax.github.io/compat-table/es6/) 17 | 18 | ## Usage 19 | 20 | Install via 21 | 22 | ``` 23 | npm install --save emailjs-mime-builder 24 | ``` 25 | 26 | ... and then use via 27 | 28 | ```javascript 29 | import MimeBuilder from 'emailjs-mime-builder' 30 | ``` 31 | 32 | Create a new `MimeBuilder` object with 33 | 34 | ```javascript 35 | var builder = new MimeBuilder(contentType [, options]); 36 | ``` 37 | 38 | Where 39 | 40 | * **contentType** - define the content type for created node. Can be left blank for attachments (content type derived from `filename` option if available) 41 | * **options** - an optional options object 42 | * **filename** - *String* filename for an attachment node 43 | * **baseBoundary** - *String* shared part of the unique multipart boundary (generated randomly if not set) 44 | 45 | ## Methods 46 | 47 | The same methods apply to the root node created with `new MimeBuilder()` and to any child nodes. 48 | 49 | ### createChild 50 | 51 | Creates and appends a child node to the node object 52 | 53 | ```javascript 54 | node.createChild(contentType, options) 55 | ``` 56 | 57 | The same arguments apply as with `new MimeBuilder()`. Created node object is returned. 58 | 59 | **Example** 60 | 61 | ```javascript 62 | new MimeBuilder("multipart/mixed"). 63 | createChild("multipart/related"). 64 | createChild("text/plain"); 65 | ``` 66 | 67 | Generates the following mime tree: 68 | 69 | ``` 70 | multipart/mixed 71 | ↳ multipart/related 72 | ↳ text/plain 73 | ``` 74 | 75 | ### appendChild 76 | 77 | Appends an existing child node to the node object. Removes the node from an existing tree if needed. 78 | 79 | ```javascript 80 | node.appendChild(childNode) 81 | ``` 82 | 83 | Where 84 | 85 | * **childNode** - child node to be appended 86 | 87 | Method returns appended child node. 88 | 89 | **Example** 90 | 91 | ```javascript 92 | var childNode = new MimeBuilder("text/plain"), 93 | rootNode = new MimeBuilder("multipart/mixed"); 94 | rootnode.appendChild(childNode); 95 | ``` 96 | 97 | Generates the following mime tree: 98 | 99 | ``` 100 | multipart/mixed 101 | ↳ text/plain 102 | ``` 103 | 104 | ## replace 105 | 106 | Replaces current node with another node 107 | 108 | ```javascript 109 | node.replace(replacementNode) 110 | ``` 111 | 112 | Where 113 | 114 | * **replacementNode** - node to replace the current node with 115 | 116 | Method returns replacement node. 117 | 118 | **Example** 119 | 120 | ```javascript 121 | var rootNode = new MimeBuilder("multipart/mixed"), 122 | childNode = rootNode.createChild("text/plain"); 123 | childNode.replace(new MimeBuilder("text/html")); 124 | ``` 125 | 126 | Generates the following mime tree: 127 | 128 | ``` 129 | multipart/mixed 130 | ↳ text/html 131 | ``` 132 | 133 | ## remove 134 | 135 | Removes current node from the mime tree. Does not make a lot of sense for a root node. 136 | 137 | ```javascript 138 | node.remove(); 139 | ``` 140 | 141 | Method returns removed node. 142 | 143 | **Example** 144 | 145 | ```javascript 146 | 147 | var rootNode = new MimeBuilder("multipart/mixed"), 148 | childNode = rootNode.createChild("text/plain"); 149 | childNode.remove(); 150 | ``` 151 | 152 | Generates the following mime tree: 153 | 154 | ``` 155 | multipart/mixed 156 | ``` 157 | 158 | ## setHeader 159 | 160 | Sets a header value. If the value for selected key exists, it is overwritten. 161 | 162 | You can set multiple values as well by using `[{key:"", value:""}]` or 163 | `{key: "value"}` structures as the first argument. 164 | 165 | ```javascript 166 | node.setHeader(key, value); 167 | ``` 168 | 169 | Where 170 | 171 | * **key** - *String|Array|Object* Header key or a list of key value pairs 172 | * **value** - *String* Header value 173 | 174 | Method returns current node. 175 | 176 | **Example** 177 | 178 | ```javascript 179 | new MimeBuilder("text/plain"). 180 | setHeader("content-disposition", "inline"). 181 | setHeader({ 182 | "content-transfer-encoding": "7bit" 183 | }). 184 | setHeader([ 185 | {key: "message-id", value: "abcde"} 186 | ]); 187 | ``` 188 | 189 | Generates the following header: 190 | 191 | ``` 192 | Content-type: text/plain 193 | Content-Disposition: inline 194 | Content-Transfer-Encoding: 7bit 195 | Message-Id: 196 | ``` 197 | 198 | ## addHeader 199 | 200 | Adds a header value. If the value for selected key exists, the value is appended 201 | as a new field and old one is not touched. 202 | 203 | You can set multiple values as well by using `[{key:"", value:""}]` or 204 | `{key: "value"}` structures as the first argument. 205 | 206 | ```javascript 207 | node.addHeader(key, value); 208 | ``` 209 | 210 | Where 211 | 212 | * **key** - *String|Array|Object* Header key or a list of key value pairs 213 | * **value** - *String* Header value 214 | 215 | Method returns current node. 216 | 217 | **Example** 218 | 219 | ```javascript 220 | new MimeBuilder("text/plain"). 221 | addHeader("X-Spam", "1"). 222 | setHeader({ 223 | "x-spam": "2" 224 | }). 225 | setHeader([ 226 | {key: "x-spam", value: "3"} 227 | ]); 228 | ``` 229 | 230 | Generates the following header: 231 | 232 | ``` 233 | Content-type: text/plain 234 | X-Spam: 1 235 | X-Spam: 2 236 | X-Spam: 3 237 | ``` 238 | 239 | ## getHeader 240 | 241 | Retrieves the first mathcing value of a selected key 242 | 243 | ```javascript 244 | node.getHeader(key) 245 | ``` 246 | 247 | Where 248 | 249 | * **key** - *String* Key to search for 250 | 251 | **Example** 252 | 253 | ```javascript 254 | new MimeBuilder("text/plain").getHeader("content-type"); // text/plain 255 | ``` 256 | 257 | ## setContent 258 | 259 | Sets body content for current node. If the value is a string, charset is added automatically 260 | to Content-Type (if it is `text/*`). If the value is a Typed Array, you need to specify the charset yourself. 261 | 262 | ```javascript 263 | node.setContent(body) 264 | ``` 265 | 266 | Where 267 | 268 | * **body** - *String|Uint8Array* body content 269 | 270 | **Example** 271 | 272 | ```javascript 273 | new MimeBuilder("text/plain").setContent("Hello world!"); 274 | ``` 275 | 276 | ## build 277 | 278 | Builds the rfc2822 message from the current node. If this is a root node, mandatory header fields are set if missing (Date, Message-Id, MIME-Version) 279 | 280 | ```javascript 281 | node.build() 282 | ``` 283 | 284 | Method returns the rfc2822 message as a string 285 | 286 | **Example** 287 | 288 | ```javascript 289 | new MimeBuilder("text/plain").setContent("Hello world!").build(); 290 | ``` 291 | 292 | Returns the following string: 293 | 294 | ``` 295 | Content-type: text/plain 296 | Date: 297 | Message-Id: 298 | MIME-Version: 1.0 299 | 300 | Hello world! 301 | ``` 302 | 303 | ## getEnvelope 304 | 305 | Generates a SMTP envelope object. Makes sense only for root node. 306 | 307 | ```javascript 308 | var envelope = node.generateEnvelope() 309 | ``` 310 | 311 | Method returns the envelope in the form of `{from:'address', to: ['addresses']}` 312 | 313 | **Example** 314 | 315 | ```javascript 316 | new MimeBuilder(). 317 | addHeader({ 318 | from: "From ", 319 | to: "receiver1@example.com", 320 | cc: "receiver2@example.com" 321 | }). 322 | getEnvelope(); 323 | ``` 324 | 325 | Returns the following object: 326 | 327 | ```json 328 | { 329 | "from": "from@example.com", 330 | "to": ["receiver1@example.com", "receiver2@example.com"] 331 | } 332 | ``` 333 | 334 | ## Notes 335 | 336 | ### Addresses 337 | 338 | When setting address headers (`From`, `To`, `Cc`, `Bcc`) use of unicode is allowed. If needed 339 | the addresses are converted to punycode automatically. 340 | 341 | ### Attachments 342 | 343 | For attachments you should minimally set `filename` option and `Content-Disposition` header. If filename is specified, you can leave content type blank - if content type is not set, it is detected from the filename. 344 | 345 | ```javascript 346 | new MimeBuilder("multipart/mixed"). 347 | createChild(false, {filename: "image.png"}). 348 | setHeader("Content-Disposition", "attachment"); 349 | ``` 350 | 351 | Obviously you might want to add `Content-Id` header as well if you want to reference this attachment from the HTML content. 352 | 353 | ### MIME structure 354 | 355 | Most probably you only need to deal with the following multipart types when generating messages: 356 | 357 | * **multipart/alternative** - includes the same content in different forms (usually text/plain + text/html) 358 | * **multipart/related** - includes main node and related nodes (eg. text/html + referenced attachments) 359 | * **multipart/mixed** - includes other multipart nodes and attachments, or single content node and attachments 360 | 361 | **Examples** 362 | 363 | One content node and an attachment 364 | 365 | ``` 366 | multipart/mixed 367 | ↳ text/plain 368 | ↳ image/png 369 | ``` 370 | 371 | Content node with referenced attachment (eg. image with `Content-Type` referenced by `cid:` url in the HTML) 372 | 373 | ``` 374 | multipart/related 375 | ↳ text/html 376 | ↳ image/png 377 | ``` 378 | 379 | Plaintext and HTML alternatives 380 | 381 | ``` 382 | multipart/alternative 383 | ↳ text/html 384 | ↳ text/plain 385 | ``` 386 | 387 | One content node with referenced attachment and a regular attachment 388 | 389 | ``` 390 | multipart/mixed 391 | ↳ multipart/related 392 | ↳ text/plain 393 | ↳ image/png 394 | ↳ application/x-zip 395 | ``` 396 | 397 | Alternative content with referenced attachment for HTML and a regular attachment 398 | 399 | ``` 400 | multipart/mixed 401 | ↳ multipart/alternative 402 | ↳ text/plain 403 | ↳ multipart/related 404 | ↳ text/html 405 | ↳ image/png 406 | ↳ application/x-zip 407 | ``` 408 | 409 | ## Get your hands dirty 410 | 411 | ``` 412 | git clone git@github.com:whiteout-io/mailbuild.git 413 | cd mailbuild 414 | npm install && npm test 415 | grunt dev 416 | go to http://localhost:12345/example/ to run the example 417 | go to http://localhost:12345/test/ to run the tests in your browser of choice 418 | ``` 419 | 420 | ## License 421 | 422 | Copyright (c) 2013 Andris Reinman 423 | 424 | Permission is hereby granted, free of charge, to any person obtaining a copy 425 | of this software and associated documentation files (the "Software"), to deal 426 | in the Software without restriction, including without limitation the rights 427 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 428 | copies of the Software, and to permit persons to whom the Software is 429 | furnished to do so, subject to the following conditions: 430 | 431 | The above copyright notice and this permission notice shall be included in 432 | all copies or substantial portions of the Software. 433 | 434 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 435 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 436 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 437 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 438 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 439 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 440 | THE SOFTWARE. 441 | -------------------------------------------------------------------------------- /dist/builder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 10 | 11 | var _emailjsMimeCodec = require('emailjs-mime-codec'); 12 | 13 | var _emailjsMimeTypes = require('emailjs-mime-types'); 14 | 15 | var _utils = require('./utils'); 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 18 | 19 | /** 20 | * Creates a new mime tree node. Assumes 'multipart/*' as the content type 21 | * if it is a branch, anything else counts as leaf. If rootNode is missing from 22 | * the options, assumes this is the root. 23 | * 24 | * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename) 25 | * @param {Object} [options] optional options 26 | * @param {Object} [options.rootNode] root node for this tree 27 | * @param {Object} [options.parentNode] immediate parent for this node 28 | * @param {Object} [options.filename] filename for an attachment node 29 | * @param {String} [options.baseBoundary] shared part of the unique multipart boundary 30 | */ 31 | var MimeNode = function () { 32 | function MimeNode(contentType) { 33 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 34 | 35 | _classCallCheck(this, MimeNode); 36 | 37 | this.nodeCounter = 0; 38 | 39 | /** 40 | * shared part of the unique multipart boundary 41 | */ 42 | this.baseBoundary = options.baseBoundary || Date.now().toString() + Math.random(); 43 | 44 | /** 45 | * If date headers is missing and current node is the root, this value is used instead 46 | */ 47 | this.date = new Date(); 48 | 49 | /** 50 | * Root node for current mime tree 51 | */ 52 | this.rootNode = options.rootNode || this; 53 | 54 | /** 55 | * If filename is specified but contentType is not (probably an attachment) 56 | * detect the content type from filename extension 57 | */ 58 | if (options.filename) { 59 | /** 60 | * Filename for this node. Useful with attachments 61 | */ 62 | this.filename = options.filename; 63 | if (!contentType) { 64 | contentType = (0, _emailjsMimeTypes.detectMimeType)(this.filename.split('.').pop()); 65 | } 66 | } 67 | 68 | /** 69 | * Immediate parent for this node (or undefined if not set) 70 | */ 71 | this.parentNode = options.parentNode; 72 | 73 | /** 74 | * Used for generating unique boundaries (prepended to the shared base) 75 | */ 76 | this._nodeId = ++this.rootNode.nodeCounter; 77 | 78 | /** 79 | * An array for possible child nodes 80 | */ 81 | this._childNodes = []; 82 | 83 | /** 84 | * A list of header values for this node in the form of [{key:'', value:''}] 85 | */ 86 | this._headers = []; 87 | 88 | /** 89 | * If content type is set (or derived from the filename) add it to headers 90 | */ 91 | if (contentType) { 92 | this.setHeader('content-type', contentType); 93 | } 94 | 95 | /** 96 | * If true then BCC header is included in RFC2822 message. 97 | */ 98 | this.includeBccInHeader = options.includeBccInHeader || false; 99 | } 100 | 101 | /** 102 | * Creates and appends a child node. Arguments provided are passed to MimeNode constructor 103 | * 104 | * @param {String} [contentType] Optional content type 105 | * @param {Object} [options] Optional options object 106 | * @return {Object} Created node object 107 | */ 108 | 109 | 110 | _createClass(MimeNode, [{ 111 | key: 'createChild', 112 | value: function createChild(contentType) { 113 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 114 | 115 | var node = new MimeNode(contentType, options); 116 | this.appendChild(node); 117 | return node; 118 | } 119 | 120 | /** 121 | * Appends an existing node to the mime tree. Removes the node from an existing 122 | * tree if needed 123 | * 124 | * @param {Object} childNode node to be appended 125 | * @return {Object} Appended node object 126 | */ 127 | 128 | }, { 129 | key: 'appendChild', 130 | value: function appendChild(childNode) { 131 | if (childNode.rootNode !== this.rootNode) { 132 | childNode.rootNode = this.rootNode; 133 | childNode._nodeId = ++this.rootNode.nodeCounter; 134 | } 135 | 136 | childNode.parentNode = this; 137 | 138 | this._childNodes.push(childNode); 139 | return childNode; 140 | } 141 | 142 | /** 143 | * Replaces current node with another node 144 | * 145 | * @param {Object} node Replacement node 146 | * @return {Object} Replacement node 147 | */ 148 | 149 | }, { 150 | key: 'replace', 151 | value: function replace(node) { 152 | var _this = this; 153 | 154 | if (node === this) { 155 | return this; 156 | } 157 | 158 | this.parentNode._childNodes.forEach(function (childNode, i) { 159 | if (childNode === _this) { 160 | node.rootNode = _this.rootNode; 161 | node.parentNode = _this.parentNode; 162 | node._nodeId = _this._nodeId; 163 | 164 | _this.rootNode = _this; 165 | _this.parentNode = undefined; 166 | 167 | node.parentNode._childNodes[i] = node; 168 | } 169 | }); 170 | 171 | return node; 172 | } 173 | 174 | /** 175 | * Removes current node from the mime tree 176 | * 177 | * @return {Object} removed node 178 | */ 179 | 180 | }, { 181 | key: 'remove', 182 | value: function remove() { 183 | if (!this.parentNode) { 184 | return this; 185 | } 186 | 187 | for (var i = this.parentNode._childNodes.length - 1; i >= 0; i--) { 188 | if (this.parentNode._childNodes[i] === this) { 189 | this.parentNode._childNodes.splice(i, 1); 190 | this.parentNode = undefined; 191 | this.rootNode = this; 192 | return this; 193 | } 194 | } 195 | } 196 | 197 | /** 198 | * Sets a header value. If the value for selected key exists, it is overwritten. 199 | * You can set multiple values as well by using [{key:'', value:''}] or 200 | * {key: 'value'} as the first argument. 201 | * 202 | * @param {String|Array|Object} key Header key or a list of key value pairs 203 | * @param {String} value Header value 204 | * @return {Object} current node 205 | */ 206 | 207 | }, { 208 | key: 'setHeader', 209 | value: function setHeader(key, value) { 210 | var _this2 = this; 211 | 212 | var added = false; 213 | 214 | // Allow setting multiple headers at once 215 | if (!value && key && (typeof key === 'undefined' ? 'undefined' : _typeof(key)) === 'object') { 216 | if (key.key && key.value) { 217 | // allow {key:'content-type', value: 'text/plain'} 218 | this.setHeader(key.key, key.value); 219 | } else if (Array.isArray(key)) { 220 | // allow [{key:'content-type', value: 'text/plain'}] 221 | key.forEach(function (i) { 222 | return _this2.setHeader(i.key, i.value); 223 | }); 224 | } else { 225 | // allow {'content-type': 'text/plain'} 226 | Object.keys(key).forEach(function (i) { 227 | return _this2.setHeader(i, key[i]); 228 | }); 229 | } 230 | return this; 231 | } 232 | 233 | key = (0, _utils.normalizeHeaderKey)(key); 234 | 235 | var headerValue = { key: key, value: value 236 | 237 | // Check if the value exists and overwrite 238 | };for (var i = 0, len = this._headers.length; i < len; i++) { 239 | if (this._headers[i].key === key) { 240 | if (!added) { 241 | // replace the first match 242 | this._headers[i] = headerValue; 243 | added = true; 244 | } else { 245 | // remove following matches 246 | this._headers.splice(i, 1); 247 | i--; 248 | len--; 249 | } 250 | } 251 | } 252 | 253 | // match not found, append the value 254 | if (!added) { 255 | this._headers.push(headerValue); 256 | } 257 | 258 | return this; 259 | } 260 | 261 | /** 262 | * Adds a header value. If the value for selected key exists, the value is appended 263 | * as a new field and old one is not touched. 264 | * You can set multiple values as well by using [{key:'', value:''}] or 265 | * {key: 'value'} as the first argument. 266 | * 267 | * @param {String|Array|Object} key Header key or a list of key value pairs 268 | * @param {String} value Header value 269 | * @return {Object} current node 270 | */ 271 | 272 | }, { 273 | key: 'addHeader', 274 | value: function addHeader(key, value) { 275 | var _this3 = this; 276 | 277 | // Allow setting multiple headers at once 278 | if (!value && key && (typeof key === 'undefined' ? 'undefined' : _typeof(key)) === 'object') { 279 | if (key.key && key.value) { 280 | // allow {key:'content-type', value: 'text/plain'} 281 | this.addHeader(key.key, key.value); 282 | } else if (Array.isArray(key)) { 283 | // allow [{key:'content-type', value: 'text/plain'}] 284 | key.forEach(function (i) { 285 | return _this3.addHeader(i.key, i.value); 286 | }); 287 | } else { 288 | // allow {'content-type': 'text/plain'} 289 | Object.keys(key).forEach(function (i) { 290 | return _this3.addHeader(i, key[i]); 291 | }); 292 | } 293 | return this; 294 | } 295 | 296 | this._headers.push({ key: (0, _utils.normalizeHeaderKey)(key), value: value }); 297 | 298 | return this; 299 | } 300 | 301 | /** 302 | * Retrieves the first mathcing value of a selected key 303 | * 304 | * @param {String} key Key to search for 305 | * @retun {String} Value for the key 306 | */ 307 | 308 | }, { 309 | key: 'getHeader', 310 | value: function getHeader(key) { 311 | key = (0, _utils.normalizeHeaderKey)(key); 312 | for (var i = 0, len = this._headers.length; i < len; i++) { 313 | if (this._headers[i].key === key) { 314 | return this._headers[i].value; 315 | } 316 | } 317 | } 318 | 319 | /** 320 | * Sets body content for current node. If the value is a string, charset is added automatically 321 | * to Content-Type (if it is text/*). If the value is a Typed Array, you need to specify 322 | * the charset yourself 323 | * 324 | * @param (String|Uint8Array) content Body content 325 | * @return {Object} current node 326 | */ 327 | 328 | }, { 329 | key: 'setContent', 330 | value: function setContent(content) { 331 | this.content = content; 332 | return this; 333 | } 334 | 335 | /** 336 | * Builds the rfc2822 message from the current node. If this is a root node, 337 | * mandatory header fields are set if missing (Date, Message-Id, MIME-Version) 338 | * 339 | * @return {String} Compiled message 340 | */ 341 | 342 | }, { 343 | key: 'build', 344 | value: function build() { 345 | var _this4 = this; 346 | 347 | var lines = []; 348 | var contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim(); 349 | var transferEncoding = void 0; 350 | var flowed = void 0; 351 | 352 | if (this.content) { 353 | transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim(); 354 | if (!transferEncoding || ['base64', 'quoted-printable'].indexOf(transferEncoding) < 0) { 355 | if (/^text\//i.test(contentType)) { 356 | // If there are no special symbols, no need to modify the text 357 | if ((0, _utils.isPlainText)(this.content)) { 358 | // If there are lines longer than 76 symbols/bytes, make the text 'flowed' 359 | if (/^.{77,}/m.test(this.content)) { 360 | flowed = true; 361 | } 362 | transferEncoding = '7bit'; 363 | } else { 364 | transferEncoding = 'quoted-printable'; 365 | } 366 | } else if (!/^multipart\//i.test(contentType)) { 367 | transferEncoding = transferEncoding || 'base64'; 368 | } 369 | } 370 | 371 | if (transferEncoding) { 372 | this.setHeader('Content-Transfer-Encoding', transferEncoding); 373 | } 374 | } 375 | 376 | if (this.filename && !this.getHeader('Content-Disposition')) { 377 | this.setHeader('Content-Disposition', 'attachment'); 378 | } 379 | 380 | this._headers.forEach(function (header) { 381 | var key = header.key; 382 | var value = header.value; 383 | var structured = void 0; 384 | 385 | switch (header.key) { 386 | case 'Content-Disposition': 387 | structured = (0, _emailjsMimeCodec.parseHeaderValue)(value); 388 | if (_this4.filename) { 389 | structured.params.filename = _this4.filename; 390 | } 391 | value = (0, _utils.buildHeaderValue)(structured); 392 | break; 393 | case 'Content-Type': 394 | structured = (0, _emailjsMimeCodec.parseHeaderValue)(value); 395 | 396 | _this4._addBoundary(structured); 397 | 398 | if (flowed) { 399 | structured.params.format = 'flowed'; 400 | } 401 | if (String(structured.params.format).toLowerCase().trim() === 'flowed') { 402 | flowed = true; 403 | } 404 | 405 | if (structured.value.match(/^text\//) && typeof _this4.content === 'string' && /[\u0080-\uFFFF]/.test(_this4.content)) { 406 | structured.params.charset = 'utf-8'; 407 | } 408 | 409 | value = (0, _utils.buildHeaderValue)(structured); 410 | break; 411 | case 'Bcc': 412 | if (_this4.includeBccInHeader === false) { 413 | // skip BCC values 414 | return; 415 | } 416 | } 417 | 418 | // skip empty lines 419 | value = (0, _utils.encodeHeaderValue)(key, value); 420 | if (!(value || '').toString().trim()) { 421 | return; 422 | } 423 | 424 | lines.push((0, _emailjsMimeCodec.foldLines)(key + ': ' + value)); 425 | }); 426 | 427 | // Ensure mandatory header fields 428 | if (this.rootNode === this) { 429 | if (!this.getHeader('Date')) { 430 | lines.push('Date: ' + this.date.toUTCString().replace(/GMT/, '+0000')); 431 | } 432 | // You really should define your own Message-Id field 433 | if (!this.getHeader('Message-Id')) { 434 | lines.push('Message-Id: <' + 435 | // crux to generate random strings like this: 436 | // "1401391905590-58aa8c32-d32a065c-c1a2aad2" 437 | [0, 0, 0].reduce(function (prev) { 438 | return prev + '-' + Math.floor((1 + Math.random()) * 0x100000000).toString(16).substring(1); 439 | }, Date.now()) + '@' + 440 | // try to use the domain of the FROM address or fallback localhost 441 | (this.getEnvelope().from || 'localhost').split('@').pop() + '>'); 442 | } 443 | if (!this.getHeader('MIME-Version')) { 444 | lines.push('MIME-Version: 1.0'); 445 | } 446 | } 447 | lines.push(''); 448 | 449 | if (this.content) { 450 | switch (transferEncoding) { 451 | case 'quoted-printable': 452 | lines.push((0, _emailjsMimeCodec.quotedPrintableEncode)(this.content)); 453 | break; 454 | case 'base64': 455 | lines.push((0, _emailjsMimeCodec.base64Encode)(this.content, _typeof(this.content) === 'object' ? 'binary' : undefined)); 456 | break; 457 | default: 458 | if (flowed) { 459 | // space stuffing http://tools.ietf.org/html/rfc3676#section-4.2 460 | lines.push((0, _emailjsMimeCodec.foldLines)(this.content.replace(/\r?\n/g, '\r\n').replace(/^( |From|>)/igm, ' $1'), 76, true)); 461 | } else { 462 | lines.push(this.content.replace(/\r?\n/g, '\r\n')); 463 | } 464 | } 465 | if (this.multipart) { 466 | lines.push(''); 467 | } 468 | } 469 | 470 | if (this.multipart) { 471 | this._childNodes.forEach(function (node) { 472 | lines.push('--' + _this4.boundary); 473 | lines.push(node.build()); 474 | }); 475 | lines.push('--' + this.boundary + '--'); 476 | lines.push(''); 477 | } 478 | 479 | return lines.join('\r\n'); 480 | } 481 | 482 | /** 483 | * Generates and returns SMTP envelope with the sender address and a list of recipients addresses 484 | * 485 | * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']} 486 | */ 487 | 488 | }, { 489 | key: 'getEnvelope', 490 | value: function getEnvelope() { 491 | var envelope = { 492 | from: false, 493 | to: [] 494 | }; 495 | this._headers.forEach(function (header) { 496 | var list = []; 497 | if (header.key === 'From' || !envelope.from && ['Reply-To', 'Sender'].indexOf(header.key) >= 0) { 498 | (0, _utils.convertAddresses)((0, _utils.parseAddresses)(header.value), list); 499 | if (list.length && list[0]) { 500 | envelope.from = list[0]; 501 | } 502 | } else if (['To', 'Cc', 'Bcc'].indexOf(header.key) >= 0) { 503 | (0, _utils.convertAddresses)((0, _utils.parseAddresses)(header.value), envelope.to); 504 | } 505 | }); 506 | 507 | return envelope; 508 | } 509 | 510 | /** 511 | * Checks if the content type is multipart and defines boundary if needed. 512 | * Doesn't return anything, modifies object argument instead. 513 | * 514 | * @param {Object} structured Parsed header value for 'Content-Type' key 515 | */ 516 | 517 | }, { 518 | key: '_addBoundary', 519 | value: function _addBoundary(structured) { 520 | this.contentType = structured.value.trim().toLowerCase(); 521 | 522 | this.multipart = this.contentType.split('/').reduce(function (prev, value) { 523 | return prev === 'multipart' ? value : false; 524 | }); 525 | 526 | if (this.multipart) { 527 | this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || (0, _utils.generateBoundary)(this._nodeId, this.rootNode.baseBoundary); 528 | } else { 529 | this.boundary = false; 530 | } 531 | } 532 | }]); 533 | 534 | return MimeNode; 535 | }(); 536 | 537 | exports.default = MimeNode; 538 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9idWlsZGVyLmpzIl0sIm5hbWVzIjpbIk1pbWVOb2RlIiwiY29udGVudFR5cGUiLCJvcHRpb25zIiwibm9kZUNvdW50ZXIiLCJiYXNlQm91bmRhcnkiLCJEYXRlIiwibm93IiwidG9TdHJpbmciLCJNYXRoIiwicmFuZG9tIiwiZGF0ZSIsInJvb3ROb2RlIiwiZmlsZW5hbWUiLCJzcGxpdCIsInBvcCIsInBhcmVudE5vZGUiLCJfbm9kZUlkIiwiX2NoaWxkTm9kZXMiLCJfaGVhZGVycyIsInNldEhlYWRlciIsImluY2x1ZGVCY2NJbkhlYWRlciIsIm5vZGUiLCJhcHBlbmRDaGlsZCIsImNoaWxkTm9kZSIsInB1c2giLCJmb3JFYWNoIiwiaSIsInVuZGVmaW5lZCIsImxlbmd0aCIsInNwbGljZSIsImtleSIsInZhbHVlIiwiYWRkZWQiLCJBcnJheSIsImlzQXJyYXkiLCJPYmplY3QiLCJrZXlzIiwiaGVhZGVyVmFsdWUiLCJsZW4iLCJhZGRIZWFkZXIiLCJjb250ZW50IiwibGluZXMiLCJnZXRIZWFkZXIiLCJ0b0xvd2VyQ2FzZSIsInRyaW0iLCJ0cmFuc2ZlckVuY29kaW5nIiwiZmxvd2VkIiwiaW5kZXhPZiIsInRlc3QiLCJoZWFkZXIiLCJzdHJ1Y3R1cmVkIiwicGFyYW1zIiwiX2FkZEJvdW5kYXJ5IiwiZm9ybWF0IiwiU3RyaW5nIiwibWF0Y2giLCJjaGFyc2V0IiwidG9VVENTdHJpbmciLCJyZXBsYWNlIiwicmVkdWNlIiwicHJldiIsImZsb29yIiwic3Vic3RyaW5nIiwiZ2V0RW52ZWxvcGUiLCJmcm9tIiwibXVsdGlwYXJ0IiwiYm91bmRhcnkiLCJidWlsZCIsImpvaW4iLCJlbnZlbG9wZSIsInRvIiwibGlzdCJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7OztBQUFBOztBQU1BOztBQUNBOzs7O0FBVUE7Ozs7Ozs7Ozs7OztJQVlxQkEsUTtBQUNuQixvQkFBYUMsV0FBYixFQUF3QztBQUFBLFFBQWRDLE9BQWMsdUVBQUosRUFBSTs7QUFBQTs7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LCJmaWxlIjoiYnVpbGRlci5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIGJhc2U2NEVuY29kZSxcbiAgcXVvdGVkUHJpbnRhYmxlRW5jb2RlLFxuICBmb2xkTGluZXMsXG4gIHBhcnNlSGVhZGVyVmFsdWVcbn0gZnJvbSAnZW1haWxqcy1taW1lLWNvZGVjJ1xuaW1wb3J0IHsgZGV0ZWN0TWltZVR5cGUgfSBmcm9tICdlbWFpbGpzLW1pbWUtdHlwZXMnXG5pbXBvcnQge1xuICBjb252ZXJ0QWRkcmVzc2VzLFxuICBwYXJzZUFkZHJlc3NlcyxcbiAgZW5jb2RlSGVhZGVyVmFsdWUsXG4gIG5vcm1hbGl6ZUhlYWRlcktleSxcbiAgZ2VuZXJhdGVCb3VuZGFyeSxcbiAgaXNQbGFpblRleHQsXG4gIGJ1aWxkSGVhZGVyVmFsdWVcbn0gZnJvbSAnLi91dGlscydcblxuLyoqXG4gKiBDcmVhdGVzIGEgbmV3IG1pbWUgdHJlZSBub2RlLiBBc3N1bWVzICdtdWx0aXBhcnQvKicgYXMgdGhlIGNvbnRlbnQgdHlwZVxuICogaWYgaXQgaXMgYSBicmFuY2gsIGFueXRoaW5nIGVsc2UgY291bnRzIGFzIGxlYWYuIElmIHJvb3ROb2RlIGlzIG1pc3NpbmcgZnJvbVxuICogdGhlIG9wdGlvbnMsIGFzc3VtZXMgdGhpcyBpcyB0aGUgcm9vdC5cbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30gY29udGVudFR5cGUgRGVmaW5lIHRoZSBjb250ZW50IHR5cGUgZm9yIHRoZSBub2RlLiBDYW4gYmUgbGVmdCBibGFuayBmb3IgYXR0YWNobWVudHMgKGRlcml2ZWQgZnJvbSBmaWxlbmFtZSlcbiAqIEBwYXJhbSB7T2JqZWN0fSBbb3B0aW9uc10gb3B0aW9uYWwgb3B0aW9uc1xuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zLnJvb3ROb2RlXSByb290IG5vZGUgZm9yIHRoaXMgdHJlZVxuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zLnBhcmVudE5vZGVdIGltbWVkaWF0ZSBwYXJlbnQgZm9yIHRoaXMgbm9kZVxuICogQHBhcmFtIHtPYmplY3R9IFtvcHRpb25zLmZpbGVuYW1lXSBmaWxlbmFtZSBmb3IgYW4gYXR0YWNobWVudCBub2RlXG4gKiBAcGFyYW0ge1N0cmluZ30gW29wdGlvbnMuYmFzZUJvdW5kYXJ5XSBzaGFyZWQgcGFydCBvZiB0aGUgdW5pcXVlIG11bHRpcGFydCBib3VuZGFyeVxuICovXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNaW1lTm9kZSB7XG4gIGNvbnN0cnVjdG9yIChjb250ZW50VHlwZSwgb3B0aW9ucyA9IHt9KSB7XG4gICAgdGhpcy5ub2RlQ291bnRlciA9IDBcblxuICAgIC8qKlxuICAgICAqIHNoYXJlZCBwYXJ0IG9mIHRoZSB1bmlxdWUgbXVsdGlwYXJ0IGJvdW5kYXJ5XG4gICAgICovXG4gICAgdGhpcy5iYXNlQm91bmRhcnkgPSBvcHRpb25zLmJhc2VCb3VuZGFyeSB8fCBEYXRlLm5vdygpLnRvU3RyaW5nKCkgKyBNYXRoLnJhbmRvbSgpXG5cbiAgICAvKipcbiAgICAgKiBJZiBkYXRlIGhlYWRlcnMgaXMgbWlzc2luZyBhbmQgY3VycmVudCBub2RlIGlzIHRoZSByb290LCB0aGlzIHZhbHVlIGlzIHVzZWQgaW5zdGVhZFxuICAgICAqL1xuICAgIHRoaXMuZGF0ZSA9IG5ldyBEYXRlKClcblxuICAgIC8qKlxuICAgICAqIFJvb3Qgbm9kZSBmb3IgY3VycmVudCBtaW1lIHRyZWVcbiAgICAgKi9cbiAgICB0aGlzLnJvb3ROb2RlID0gb3B0aW9ucy5yb290Tm9kZSB8fCB0aGlzXG5cbiAgICAvKipcbiAgICAgKiBJZiBmaWxlbmFtZSBpcyBzcGVjaWZpZWQgYnV0IGNvbnRlbnRUeXBlIGlzIG5vdCAocHJvYmFibHkgYW4gYXR0YWNobWVudClcbiAgICAgKiBkZXRlY3QgdGhlIGNvbnRlbnQgdHlwZSBmcm9tIGZpbGVuYW1lIGV4dGVuc2lvblxuICAgICAqL1xuICAgIGlmIChvcHRpb25zLmZpbGVuYW1lKSB7XG4gICAgICAvKipcbiAgICAgICAqIEZpbGVuYW1lIGZvciB0aGlzIG5vZGUuIFVzZWZ1bCB3aXRoIGF0dGFjaG1lbnRzXG4gICAgICAgKi9cbiAgICAgIHRoaXMuZmlsZW5hbWUgPSBvcHRpb25zLmZpbGVuYW1lXG4gICAgICBpZiAoIWNvbnRlbnRUeXBlKSB7XG4gICAgICAgIGNvbnRlbnRUeXBlID0gZGV0ZWN0TWltZVR5cGUodGhpcy5maWxlbmFtZS5zcGxpdCgnLicpLnBvcCgpKVxuICAgICAgfVxuICAgIH1cblxuICAgIC8qKlxuICAgICAqIEltbWVkaWF0ZSBwYXJlbnQgZm9yIHRoaXMgbm9kZSAob3IgdW5kZWZpbmVkIGlmIG5vdCBzZXQpXG4gICAgICovXG4gICAgdGhpcy5wYXJlbnROb2RlID0gb3B0aW9ucy5wYXJlbnROb2RlXG5cbiAgICAvKipcbiAgICAgKiBVc2VkIGZvciBnZW5lcmF0aW5nIHVuaXF1ZSBib3VuZGFyaWVzIChwcmVwZW5kZWQgdG8gdGhlIHNoYXJlZCBiYXNlKVxuICAgICAqL1xuICAgIHRoaXMuX25vZGVJZCA9ICsrdGhpcy5yb290Tm9kZS5ub2RlQ291bnRlclxuXG4gICAgLyoqXG4gICAgICogQW4gYXJyYXkgZm9yIHBvc3NpYmxlIGNoaWxkIG5vZGVzXG4gICAgICovXG4gICAgdGhpcy5fY2hpbGROb2RlcyA9IFtdXG5cbiAgICAvKipcbiAgICAgKiBBIGxpc3Qgb2YgaGVhZGVyIHZhbHVlcyBmb3IgdGhpcyBub2RlIGluIHRoZSBmb3JtIG9mIFt7a2V5OicnLCB2YWx1ZTonJ31dXG4gICAgICovXG4gICAgdGhpcy5faGVhZGVycyA9IFtdXG5cbiAgICAvKipcbiAgICAgKiBJZiBjb250ZW50IHR5cGUgaXMgc2V0IChvciBkZXJpdmVkIGZyb20gdGhlIGZpbGVuYW1lKSBhZGQgaXQgdG8gaGVhZGVyc1xuICAgICAqL1xuICAgIGlmIChjb250ZW50VHlwZSkge1xuICAgICAgdGhpcy5zZXRIZWFkZXIoJ2NvbnRlbnQtdHlwZScsIGNvbnRlbnRUeXBlKVxuICAgIH1cblxuICAgIC8qKlxuICAgICAqIElmIHRydWUgdGhlbiBCQ0MgaGVhZGVyIGlzIGluY2x1ZGVkIGluIFJGQzI4MjIgbWVzc2FnZS5cbiAgICAgKi9cbiAgICB0aGlzLmluY2x1ZGVCY2NJbkhlYWRlciA9IG9wdGlvbnMuaW5jbHVkZUJjY0luSGVhZGVyIHx8IGZhbHNlXG4gIH1cblxuICAvKipcbiAgICogQ3JlYXRlcyBhbmQgYXBwZW5kcyBhIGNoaWxkIG5vZGUuIEFyZ3VtZW50cyBwcm92aWRlZCBhcmUgcGFzc2VkIHRvIE1pbWVOb2RlIGNvbnN0cnVjdG9yXG4gICAqXG4gICAqIEBwYXJhbSB7U3RyaW5nfSBbY29udGVudFR5cGVdIE9wdGlvbmFsIGNvbnRlbnQgdHlwZVxuICAgKiBAcGFyYW0ge09iamVjdH0gW29wdGlvbnNdIE9wdGlvbmFsIG9wdGlvbnMgb2JqZWN0XG4gICAqIEByZXR1cm4ge09iamVjdH0gQ3JlYXRlZCBub2RlIG9iamVjdFxuICAgKi9cbiAgY3JlYXRlQ2hpbGQgKGNvbnRlbnRUeXBlLCBvcHRpb25zID0ge30pIHtcbiAgICB2YXIgbm9kZSA9IG5ldyBNaW1lTm9kZShjb250ZW50VHlwZSwgb3B0aW9ucylcbiAgICB0aGlzLmFwcGVuZENoaWxkKG5vZGUpXG4gICAgcmV0dXJuIG5vZGVcbiAgfVxuXG4gIC8qKlxuICAgKiBBcHBlbmRzIGFuIGV4aXN0aW5nIG5vZGUgdG8gdGhlIG1pbWUgdHJlZS4gUmVtb3ZlcyB0aGUgbm9kZSBmcm9tIGFuIGV4aXN0aW5nXG4gICAqIHRyZWUgaWYgbmVlZGVkXG4gICAqXG4gICAqIEBwYXJhbSB7T2JqZWN0fSBjaGlsZE5vZGUgbm9kZSB0byBiZSBhcHBlbmRlZFxuICAgKiBAcmV0dXJuIHtPYmplY3R9IEFwcGVuZGVkIG5vZGUgb2JqZWN0XG4gICAqL1xuICBhcHBlbmRDaGlsZCAoY2hpbGROb2RlKSB7XG4gICAgaWYgKGNoaWxkTm9kZS5yb290Tm9kZSAhPT0gdGhpcy5yb290Tm9kZSkge1xuICAgICAgY2hpbGROb2RlLnJvb3ROb2RlID0gdGhpcy5yb290Tm9kZVxuICAgICAgY2hpbGROb2RlLl9ub2RlSWQgPSArK3RoaXMucm9vdE5vZGUubm9kZUNvdW50ZXJcbiAgICB9XG5cbiAgICBjaGlsZE5vZGUucGFyZW50Tm9kZSA9IHRoaXNcblxuICAgIHRoaXMuX2NoaWxkTm9kZXMucHVzaChjaGlsZE5vZGUpXG4gICAgcmV0dXJuIGNoaWxkTm9kZVxuICB9XG5cbiAgLyoqXG4gICAqIFJlcGxhY2VzIGN1cnJlbnQgbm9kZSB3aXRoIGFub3RoZXIgbm9kZVxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gbm9kZSBSZXBsYWNlbWVudCBub2RlXG4gICAqIEByZXR1cm4ge09iamVjdH0gUmVwbGFjZW1lbnQgbm9kZVxuICAgKi9cbiAgcmVwbGFjZSAobm9kZSkge1xuICAgIGlmIChub2RlID09PSB0aGlzKSB7XG4gICAgICByZXR1cm4gdGhpc1xuICAgIH1cblxuICAgIHRoaXMucGFyZW50Tm9kZS5fY2hpbGROb2Rlcy5mb3JFYWNoKChjaGlsZE5vZGUsIGkpID0+IHtcbiAgICAgIGlmIChjaGlsZE5vZGUgPT09IHRoaXMpIHtcbiAgICAgICAgbm9kZS5yb290Tm9kZSA9IHRoaXMucm9vdE5vZGVcbiAgICAgICAgbm9kZS5wYXJlbnROb2RlID0gdGhpcy5wYXJlbnROb2RlXG4gICAgICAgIG5vZGUuX25vZGVJZCA9IHRoaXMuX25vZGVJZFxuXG4gICAgICAgIHRoaXMucm9vdE5vZGUgPSB0aGlzXG4gICAgICAgIHRoaXMucGFyZW50Tm9kZSA9IHVuZGVmaW5lZFxuXG4gICAgICAgIG5vZGUucGFyZW50Tm9kZS5fY2hpbGROb2Rlc1tpXSA9IG5vZGVcbiAgICAgIH1cbiAgICB9KVxuXG4gICAgcmV0dXJuIG5vZGVcbiAgfVxuXG4gIC8qKlxuICAgKiBSZW1vdmVzIGN1cnJlbnQgbm9kZSBmcm9tIHRoZSBtaW1lIHRyZWVcbiAgICpcbiAgICogQHJldHVybiB7T2JqZWN0fSByZW1vdmVkIG5vZGVcbiAgICovXG4gIHJlbW92ZSAoKSB7XG4gICAgaWYgKCF0aGlzLnBhcmVudE5vZGUpIHtcbiAgICAgIHJldHVybiB0aGlzXG4gICAgfVxuXG4gICAgZm9yICh2YXIgaSA9IHRoaXMucGFyZW50Tm9kZS5fY2hpbGROb2Rlcy5sZW5ndGggLSAxOyBpID49IDA7IGktLSkge1xuICAgICAgaWYgKHRoaXMucGFyZW50Tm9kZS5fY2hpbGROb2Rlc1tpXSA9PT0gdGhpcykge1xuICAgICAgICB0aGlzLnBhcmVudE5vZGUuX2NoaWxkTm9kZXMuc3BsaWNlKGksIDEpXG4gICAgICAgIHRoaXMucGFyZW50Tm9kZSA9IHVuZGVmaW5lZFxuICAgICAgICB0aGlzLnJvb3ROb2RlID0gdGhpc1xuICAgICAgICByZXR1cm4gdGhpc1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBTZXRzIGEgaGVhZGVyIHZhbHVlLiBJZiB0aGUgdmFsdWUgZm9yIHNlbGVjdGVkIGtleSBleGlzdHMsIGl0IGlzIG92ZXJ3cml0dGVuLlxuICAgKiBZb3UgY2FuIHNldCBtdWx0aXBsZSB2YWx1ZXMgYXMgd2VsbCBieSB1c2luZyBbe2tleTonJywgdmFsdWU6Jyd9XSBvclxuICAgKiB7a2V5OiAndmFsdWUnfSBhcyB0aGUgZmlyc3QgYXJndW1lbnQuXG4gICAqXG4gICAqIEBwYXJhbSB7U3RyaW5nfEFycmF5fE9iamVjdH0ga2V5IEhlYWRlciBrZXkgb3IgYSBsaXN0IG9mIGtleSB2YWx1ZSBwYWlyc1xuICAgKiBAcGFyYW0ge1N0cmluZ30gdmFsdWUgSGVhZGVyIHZhbHVlXG4gICAqIEByZXR1cm4ge09iamVjdH0gY3VycmVudCBub2RlXG4gICAqL1xuICBzZXRIZWFkZXIgKGtleSwgdmFsdWUpIHtcbiAgICBsZXQgYWRkZWQgPSBmYWxzZVxuXG4gICAgLy8gQWxsb3cgc2V0dGluZyBtdWx0aXBsZSBoZWFkZXJzIGF0IG9uY2VcbiAgICBpZiAoIXZhbHVlICYmIGtleSAmJiB0eXBlb2Yga2V5ID09PSAnb2JqZWN0Jykge1xuICAgICAgaWYgKGtleS5rZXkgJiYga2V5LnZhbHVlKSB7XG4gICAgICAgIC8vIGFsbG93IHtrZXk6J2NvbnRlbnQtdHlwZScsIHZhbHVlOiAndGV4dC9wbGFpbid9XG4gICAgICAgIHRoaXMuc2V0SGVhZGVyKGtleS5rZXksIGtleS52YWx1ZSlcbiAgICAgIH0gZWxzZSBpZiAoQXJyYXkuaXNBcnJheShrZXkpKSB7XG4gICAgICAgIC8vIGFsbG93IFt7a2V5Oidjb250ZW50LXR5cGUnLCB2YWx1ZTogJ3RleHQvcGxhaW4nfV1cbiAgICAgICAga2V5LmZvckVhY2goaSA9PiB0aGlzLnNldEhlYWRlcihpLmtleSwgaS52YWx1ZSkpXG4gICAgICB9IGVsc2Uge1xuICAgICAgICAvLyBhbGxvdyB7J2NvbnRlbnQtdHlwZSc6ICd0ZXh0L3BsYWluJ31cbiAgICAgICAgT2JqZWN0LmtleXMoa2V5KS5mb3JFYWNoKGkgPT4gdGhpcy5zZXRIZWFkZXIoaSwga2V5W2ldKSlcbiAgICAgIH1cbiAgICAgIHJldHVybiB0aGlzXG4gICAgfVxuXG4gICAga2V5ID0gbm9ybWFsaXplSGVhZGVyS2V5KGtleSlcblxuICAgIGNvbnN0IGhlYWRlclZhbHVlID0geyBrZXksIHZhbHVlIH1cblxuICAgIC8vIENoZWNrIGlmIHRoZSB2YWx1ZSBleGlzdHMgYW5kIG92ZXJ3cml0ZVxuICAgIGZvciAodmFyIGkgPSAwLCBsZW4gPSB0aGlzLl9oZWFkZXJzLmxlbmd0aDsgaSA8IGxlbjsgaSsrKSB7XG4gICAgICBpZiAodGhpcy5faGVhZGVyc1tpXS5rZXkgPT09IGtleSkge1xuICAgICAgICBpZiAoIWFkZGVkKSB7XG4gICAgICAgICAgLy8gcmVwbGFjZSB0aGUgZmlyc3QgbWF0Y2hcbiAgICAgICAgICB0aGlzLl9oZWFkZXJzW2ldID0gaGVhZGVyVmFsdWVcbiAgICAgICAgICBhZGRlZCA9IHRydWVcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAvLyByZW1vdmUgZm9sbG93aW5nIG1hdGNoZXNcbiAgICAgICAgICB0aGlzLl9oZWFkZXJzLnNwbGljZShpLCAxKVxuICAgICAgICAgIGktLVxuICAgICAgICAgIGxlbi0tXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBtYXRjaCBub3QgZm91bmQsIGFwcGVuZCB0aGUgdmFsdWVcbiAgICBpZiAoIWFkZGVkKSB7XG4gICAgICB0aGlzLl9oZWFkZXJzLnB1c2goaGVhZGVyVmFsdWUpXG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXNcbiAgfVxuXG4gIC8qKlxuICAgKiBBZGRzIGEgaGVhZGVyIHZhbHVlLiBJZiB0aGUgdmFsdWUgZm9yIHNlbGVjdGVkIGtleSBleGlzdHMsIHRoZSB2YWx1ZSBpcyBhcHBlbmRlZFxuICAgKiBhcyBhIG5ldyBmaWVsZCBhbmQgb2xkIG9uZSBpcyBub3QgdG91Y2hlZC5cbiAgICogWW91IGNhbiBzZXQgbXVsdGlwbGUgdmFsdWVzIGFzIHdlbGwgYnkgdXNpbmcgW3trZXk6JycsIHZhbHVlOicnfV0gb3JcbiAgICoge2tleTogJ3ZhbHVlJ30gYXMgdGhlIGZpcnN0IGFyZ3VtZW50LlxuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ3xBcnJheXxPYmplY3R9IGtleSBIZWFkZXIga2V5IG9yIGEgbGlzdCBvZiBrZXkgdmFsdWUgcGFpcnNcbiAgICogQHBhcmFtIHtTdHJpbmd9IHZhbHVlIEhlYWRlciB2YWx1ZVxuICAgKiBAcmV0dXJuIHtPYmplY3R9IGN1cnJlbnQgbm9kZVxuICAgKi9cbiAgYWRkSGVhZGVyIChrZXksIHZhbHVlKSB7XG4gICAgLy8gQWxsb3cgc2V0dGluZyBtdWx0aXBsZSBoZWFkZXJzIGF0IG9uY2VcbiAgICBpZiAoIXZhbHVlICYmIGtleSAmJiB0eXBlb2Yga2V5ID09PSAnb2JqZWN0Jykge1xuICAgICAgaWYgKGtleS5rZXkgJiYga2V5LnZhbHVlKSB7XG4gICAgICAgIC8vIGFsbG93IHtrZXk6J2NvbnRlbnQtdHlwZScsIHZhbHVlOiAndGV4dC9wbGFpbid9XG4gICAgICAgIHRoaXMuYWRkSGVhZGVyKGtleS5rZXksIGtleS52YWx1ZSlcbiAgICAgIH0gZWxzZSBpZiAoQXJyYXkuaXNBcnJheShrZXkpKSB7XG4gICAgICAgIC8vIGFsbG93IFt7a2V5Oidjb250ZW50LXR5cGUnLCB2YWx1ZTogJ3RleHQvcGxhaW4nfV1cbiAgICAgICAga2V5LmZvckVhY2goaSA9PiB0aGlzLmFkZEhlYWRlcihpLmtleSwgaS52YWx1ZSkpXG4gICAgICB9IGVsc2Uge1xuICAgICAgICAvLyBhbGxvdyB7J2NvbnRlbnQtdHlwZSc6ICd0ZXh0L3BsYWluJ31cbiAgICAgICAgT2JqZWN0LmtleXMoa2V5KS5mb3JFYWNoKGkgPT4gdGhpcy5hZGRIZWFkZXIoaSwga2V5W2ldKSlcbiAgICAgIH1cbiAgICAgIHJldHVybiB0aGlzXG4gICAgfVxuXG4gICAgdGhpcy5faGVhZGVycy5wdXNoKHsga2V5OiBub3JtYWxpemVIZWFkZXJLZXkoa2V5KSwgdmFsdWUgfSlcblxuICAgIHJldHVybiB0aGlzXG4gIH1cblxuICAvKipcbiAgICogUmV0cmlldmVzIHRoZSBmaXJzdCBtYXRoY2luZyB2YWx1ZSBvZiBhIHNlbGVjdGVkIGtleVxuICAgKlxuICAgKiBAcGFyYW0ge1N0cmluZ30ga2V5IEtleSB0byBzZWFyY2ggZm9yXG4gICAqIEByZXR1biB7U3RyaW5nfSBWYWx1ZSBmb3IgdGhlIGtleVxuICAgKi9cbiAgZ2V0SGVhZGVyIChrZXkpIHtcbiAgICBrZXkgPSBub3JtYWxpemVIZWFkZXJLZXkoa2V5KVxuICAgIGZvciAobGV0IGkgPSAwLCBsZW4gPSB0aGlzLl9oZWFkZXJzLmxlbmd0aDsgaSA8IGxlbjsgaSsrKSB7XG4gICAgICBpZiAodGhpcy5faGVhZGVyc1tpXS5rZXkgPT09IGtleSkge1xuICAgICAgICByZXR1cm4gdGhpcy5faGVhZGVyc1tpXS52YWx1ZVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBTZXRzIGJvZHkgY29udGVudCBmb3IgY3VycmVudCBub2RlLiBJZiB0aGUgdmFsdWUgaXMgYSBzdHJpbmcsIGNoYXJzZXQgaXMgYWRkZWQgYXV0b21hdGljYWxseVxuICAgKiB0byBDb250ZW50LVR5cGUgKGlmIGl0IGlzIHRleHQvKikuIElmIHRoZSB2YWx1ZSBpcyBhIFR5cGVkIEFycmF5LCB5b3UgbmVlZCB0byBzcGVjaWZ5XG4gICAqIHRoZSBjaGFyc2V0IHlvdXJzZWxmXG4gICAqXG4gICAqIEBwYXJhbSAoU3RyaW5nfFVpbnQ4QXJyYXkpIGNvbnRlbnQgQm9keSBjb250ZW50XG4gICAqIEByZXR1cm4ge09iamVjdH0gY3VycmVudCBub2RlXG4gICAqL1xuICBzZXRDb250ZW50IChjb250ZW50KSB7XG4gICAgdGhpcy5jb250ZW50ID0gY29udGVudFxuICAgIHJldHVybiB0aGlzXG4gIH1cblxuICAvKipcbiAgICogQnVpbGRzIHRoZSByZmMyODIyIG1lc3NhZ2UgZnJvbSB0aGUgY3VycmVudCBub2RlLiBJZiB0aGlzIGlzIGEgcm9vdCBub2RlLFxuICAgKiBtYW5kYXRvcnkgaGVhZGVyIGZpZWxkcyBhcmUgc2V0IGlmIG1pc3NpbmcgKERhdGUsIE1lc3NhZ2UtSWQsIE1JTUUtVmVyc2lvbilcbiAgICpcbiAgICogQHJldHVybiB7U3RyaW5nfSBDb21waWxlZCBtZXNzYWdlXG4gICAqL1xuICBidWlsZCAoKSB7XG4gICAgY29uc3QgbGluZXMgPSBbXVxuICAgIGNvbnN0IGNvbnRlbnRUeXBlID0gKHRoaXMuZ2V0SGVhZGVyKCdDb250ZW50LVR5cGUnKSB8fCAnJykudG9TdHJpbmcoKS50b0xvd2VyQ2FzZSgpLnRyaW0oKVxuICAgIGxldCB0cmFuc2ZlckVuY29kaW5nXG4gICAgbGV0IGZsb3dlZFxuXG4gICAgaWYgKHRoaXMuY29udGVudCkge1xuICAgICAgdHJhbnNmZXJFbmNvZGluZyA9ICh0aGlzLmdldEhlYWRlcignQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZycpIHx8ICcnKS50b1N0cmluZygpLnRvTG93ZXJDYXNlKCkudHJpbSgpXG4gICAgICBpZiAoIXRyYW5zZmVyRW5jb2RpbmcgfHwgWydiYXNlNjQnLCAncXVvdGVkLXByaW50YWJsZSddLmluZGV4T2YodHJhbnNmZXJFbmNvZGluZykgPCAwKSB7XG4gICAgICAgIGlmICgvXnRleHRcXC8vaS50ZXN0KGNvbnRlbnRUeXBlKSkge1xuICAgICAgICAgIC8vIElmIHRoZXJlIGFyZSBubyBzcGVjaWFsIHN5bWJvbHMsIG5vIG5lZWQgdG8gbW9kaWZ5IHRoZSB0ZXh0XG4gICAgICAgICAgaWYgKGlzUGxhaW5UZXh0KHRoaXMuY29udGVudCkpIHtcbiAgICAgICAgICAgIC8vIElmIHRoZXJlIGFyZSBsaW5lcyBsb25nZXIgdGhhbiA3NiBzeW1ib2xzL2J5dGVzLCBtYWtlIHRoZSB0ZXh0ICdmbG93ZWQnXG4gICAgICAgICAgICBpZiAoL14uezc3LH0vbS50ZXN0KHRoaXMuY29udGVudCkpIHtcbiAgICAgICAgICAgICAgZmxvd2VkID0gdHJ1ZVxuICAgICAgICAgICAgfVxuICAgICAgICAgICAgdHJhbnNmZXJFbmNvZGluZyA9ICc3Yml0J1xuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICB0cmFuc2ZlckVuY29kaW5nID0gJ3F1b3RlZC1wcmludGFibGUnXG4gICAgICAgICAgfVxuICAgICAgICB9IGVsc2UgaWYgKCEvXm11bHRpcGFydFxcLy9pLnRlc3QoY29udGVudFR5cGUpKSB7XG4gICAgICAgICAgdHJhbnNmZXJFbmNvZGluZyA9IHRyYW5zZmVyRW5jb2RpbmcgfHwgJ2Jhc2U2NCdcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBpZiAodHJhbnNmZXJFbmNvZGluZykge1xuICAgICAgICB0aGlzLnNldEhlYWRlcignQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZycsIHRyYW5zZmVyRW5jb2RpbmcpXG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKHRoaXMuZmlsZW5hbWUgJiYgIXRoaXMuZ2V0SGVhZGVyKCdDb250ZW50LURpc3Bvc2l0aW9uJykpIHtcbiAgICAgIHRoaXMuc2V0SGVhZGVyKCdDb250ZW50LURpc3Bvc2l0aW9uJywgJ2F0dGFjaG1lbnQnKVxuICAgIH1cblxuICAgIHRoaXMuX2hlYWRlcnMuZm9yRWFjaChoZWFkZXIgPT4ge1xuICAgICAgY29uc3Qga2V5ID0gaGVhZGVyLmtleVxuICAgICAgbGV0IHZhbHVlID0gaGVhZGVyLnZhbHVlXG4gICAgICBsZXQgc3RydWN0dXJlZFxuXG4gICAgICBzd2l0Y2ggKGhlYWRlci5rZXkpIHtcbiAgICAgICAgY2FzZSAnQ29udGVudC1EaXNwb3NpdGlvbic6XG4gICAgICAgICAgc3RydWN0dXJlZCA9IHBhcnNlSGVhZGVyVmFsdWUodmFsdWUpXG4gICAgICAgICAgaWYgKHRoaXMuZmlsZW5hbWUpIHtcbiAgICAgICAgICAgIHN0cnVjdHVyZWQucGFyYW1zLmZpbGVuYW1lID0gdGhpcy5maWxlbmFtZVxuICAgICAgICAgIH1cbiAgICAgICAgICB2YWx1ZSA9IGJ1aWxkSGVhZGVyVmFsdWUoc3RydWN0dXJlZClcbiAgICAgICAgICBicmVha1xuICAgICAgICBjYXNlICdDb250ZW50LVR5cGUnOlxuICAgICAgICAgIHN0cnVjdHVyZWQgPSBwYXJzZUhlYWRlclZhbHVlKHZhbHVlKVxuXG4gICAgICAgICAgdGhpcy5fYWRkQm91bmRhcnkoc3RydWN0dXJlZClcblxuICAgICAgICAgIGlmIChmbG93ZWQpIHtcbiAgICAgICAgICAgIHN0cnVjdHVyZWQucGFyYW1zLmZvcm1hdCA9ICdmbG93ZWQnXG4gICAgICAgICAgfVxuICAgICAgICAgIGlmIChTdHJpbmcoc3RydWN0dXJlZC5wYXJhbXMuZm9ybWF0KS50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PT0gJ2Zsb3dlZCcpIHtcbiAgICAgICAgICAgIGZsb3dlZCA9IHRydWVcbiAgICAgICAgICB9XG5cbiAgICAgICAgICBpZiAoc3RydWN0dXJlZC52YWx1ZS5tYXRjaCgvXnRleHRcXC8vKSAmJiB0eXBlb2YgdGhpcy5jb250ZW50ID09PSAnc3RyaW5nJyAmJiAvW1xcdTAwODAtXFx1RkZGRl0vLnRlc3QodGhpcy5jb250ZW50KSkge1xuICAgICAgICAgICAgc3RydWN0dXJlZC5wYXJhbXMuY2hhcnNldCA9ICd1dGYtOCdcbiAgICAgICAgICB9XG5cbiAgICAgICAgICB2YWx1ZSA9IGJ1aWxkSGVhZGVyVmFsdWUoc3RydWN0dXJlZClcbiAgICAgICAgICBicmVha1xuICAgICAgICBjYXNlICdCY2MnOlxuICAgICAgICAgIGlmICh0aGlzLmluY2x1ZGVCY2NJbkhlYWRlciA9PT0gZmFsc2UpIHtcbiAgICAgICAgICAgIC8vIHNraXAgQkNDIHZhbHVlc1xuICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICAvLyBza2lwIGVtcHR5IGxpbmVzXG4gICAgICB2YWx1ZSA9IGVuY29kZUhlYWRlclZhbHVlKGtleSwgdmFsdWUpXG4gICAgICBpZiAoISh2YWx1ZSB8fCAnJykudG9TdHJpbmcoKS50cmltKCkpIHtcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIGxpbmVzLnB1c2goZm9sZExpbmVzKGtleSArICc6ICcgKyB2YWx1ZSkpXG4gICAgfSlcblxuICAgIC8vIEVuc3VyZSBtYW5kYXRvcnkgaGVhZGVyIGZpZWxkc1xuICAgIGlmICh0aGlzLnJvb3ROb2RlID09PSB0aGlzKSB7XG4gICAgICBpZiAoIXRoaXMuZ2V0SGVhZGVyKCdEYXRlJykpIHtcbiAgICAgICAgbGluZXMucHVzaCgnRGF0ZTogJyArIHRoaXMuZGF0ZS50b1VUQ1N0cmluZygpLnJlcGxhY2UoL0dNVC8sICcrMDAwMCcpKVxuICAgICAgfVxuICAgICAgLy8gWW91IHJlYWxseSBzaG91bGQgZGVmaW5lIHlvdXIgb3duIE1lc3NhZ2UtSWQgZmllbGRcbiAgICAgIGlmICghdGhpcy5nZXRIZWFkZXIoJ01lc3NhZ2UtSWQnKSkge1xuICAgICAgICBsaW5lcy5wdXNoKCdNZXNzYWdlLUlkOiA8JyArXG4gICAgICAgICAgLy8gY3J1eCB0byBnZW5lcmF0ZSByYW5kb20gc3RyaW5ncyBsaWtlIHRoaXM6XG4gICAgICAgICAgLy8gXCIxNDAxMzkxOTA1NTkwLTU4YWE4YzMyLWQzMmEwNjVjLWMxYTJhYWQyXCJcbiAgICAgICAgICBbMCwgMCwgMF0ucmVkdWNlKGZ1bmN0aW9uIChwcmV2KSB7XG4gICAgICAgICAgICByZXR1cm4gcHJldiArICctJyArIE1hdGguZmxvb3IoKDEgKyBNYXRoLnJhbmRvbSgpKSAqIDB4MTAwMDAwMDAwKVxuICAgICAgICAgICAgICAudG9TdHJpbmcoMTYpXG4gICAgICAgICAgICAgIC5zdWJzdHJpbmcoMSlcbiAgICAgICAgICB9LCBEYXRlLm5vdygpKSArXG4gICAgICAgICAgJ0AnICtcbiAgICAgICAgICAvLyB0cnkgdG8gdXNlIHRoZSBkb21haW4gb2YgdGhlIEZST00gYWRkcmVzcyBvciBmYWxsYmFjayBsb2NhbGhvc3RcbiAgICAgICAgICAodGhpcy5nZXRFbnZlbG9wZSgpLmZyb20gfHwgJ2xvY2FsaG9zdCcpLnNwbGl0KCdAJykucG9wKCkgK1xuICAgICAgICAgICc+JylcbiAgICAgIH1cbiAgICAgIGlmICghdGhpcy5nZXRIZWFkZXIoJ01JTUUtVmVyc2lvbicpKSB7XG4gICAgICAgIGxpbmVzLnB1c2goJ01JTUUtVmVyc2lvbjogMS4wJylcbiAgICAgIH1cbiAgICB9XG4gICAgbGluZXMucHVzaCgnJylcblxuICAgIGlmICh0aGlzLmNvbnRlbnQpIHtcbiAgICAgIHN3aXRjaCAodHJhbnNmZXJFbmNvZGluZykge1xuICAgICAgICBjYXNlICdxdW90ZWQtcHJpbnRhYmxlJzpcbiAgICAgICAgICBsaW5lcy5wdXNoKHF1b3RlZFByaW50YWJsZUVuY29kZSh0aGlzLmNvbnRlbnQpKVxuICAgICAgICAgIGJyZWFrXG4gICAgICAgIGNhc2UgJ2Jhc2U2NCc6XG4gICAgICAgICAgbGluZXMucHVzaChiYXNlNjRFbmNvZGUodGhpcy5jb250ZW50LCB0eXBlb2YgdGhpcy5jb250ZW50ID09PSAnb2JqZWN0JyA/ICdiaW5hcnknIDogdW5kZWZpbmVkKSlcbiAgICAgICAgICBicmVha1xuICAgICAgICBkZWZhdWx0OlxuICAgICAgICAgIGlmIChmbG93ZWQpIHtcbiAgICAgICAgICAgIC8vIHNwYWNlIHN0dWZmaW5nIGh0dHA6Ly90b29scy5pZXRmLm9yZy9odG1sL3JmYzM2NzYjc2VjdGlvbi00LjJcbiAgICAgICAgICAgIGxpbmVzLnB1c2goZm9sZExpbmVzKHRoaXMuY29udGVudC5yZXBsYWNlKC9cXHI/XFxuL2csICdcXHJcXG4nKS5yZXBsYWNlKC9eKCB8RnJvbXw+KS9pZ20sICcgJDEnKSwgNzYsIHRydWUpKVxuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICBsaW5lcy5wdXNoKHRoaXMuY29udGVudC5yZXBsYWNlKC9cXHI/XFxuL2csICdcXHJcXG4nKSlcbiAgICAgICAgICB9XG4gICAgICB9XG4gICAgICBpZiAodGhpcy5tdWx0aXBhcnQpIHtcbiAgICAgICAgbGluZXMucHVzaCgnJylcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAodGhpcy5tdWx0aXBhcnQpIHtcbiAgICAgIHRoaXMuX2NoaWxkTm9kZXMuZm9yRWFjaChub2RlID0+IHtcbiAgICAgICAgbGluZXMucHVzaCgnLS0nICsgdGhpcy5ib3VuZGFyeSlcbiAgICAgICAgbGluZXMucHVzaChub2RlLmJ1aWxkKCkpXG4gICAgICB9KVxuICAgICAgbGluZXMucHVzaCgnLS0nICsgdGhpcy5ib3VuZGFyeSArICctLScpXG4gICAgICBsaW5lcy5wdXNoKCcnKVxuICAgIH1cblxuICAgIHJldHVybiBsaW5lcy5qb2luKCdcXHJcXG4nKVxuICB9XG5cbiAgLyoqXG4gICAqIEdlbmVyYXRlcyBhbmQgcmV0dXJucyBTTVRQIGVudmVsb3BlIHdpdGggdGhlIHNlbmRlciBhZGRyZXNzIGFuZCBhIGxpc3Qgb2YgcmVjaXBpZW50cyBhZGRyZXNzZXNcbiAgICpcbiAgICogQHJldHVybiB7T2JqZWN0fSBTTVRQIGVudmVsb3BlIGluIHRoZSBmb3JtIG9mIHtmcm9tOiAnZnJvbUBleGFtcGxlLmNvbScsIHRvOiBbJ3RvQGV4YW1wbGUuY29tJ119XG4gICAqL1xuICBnZXRFbnZlbG9wZSAoKSB7XG4gICAgdmFyIGVudmVsb3BlID0ge1xuICAgICAgZnJvbTogZmFsc2UsXG4gICAgICB0bzogW11cbiAgICB9XG4gICAgdGhpcy5faGVhZGVycy5mb3JFYWNoKGhlYWRlciA9PiB7XG4gICAgICB2YXIgbGlzdCA9IFtdXG4gICAgICBpZiAoaGVhZGVyLmtleSA9PT0gJ0Zyb20nIHx8ICghZW52ZWxvcGUuZnJvbSAmJiBbJ1JlcGx5LVRvJywgJ1NlbmRlciddLmluZGV4T2YoaGVhZGVyLmtleSkgPj0gMCkpIHtcbiAgICAgICAgY29udmVydEFkZHJlc3NlcyhwYXJzZUFkZHJlc3NlcyhoZWFkZXIudmFsdWUpLCBsaXN0KVxuICAgICAgICBpZiAobGlzdC5sZW5ndGggJiYgbGlzdFswXSkge1xuICAgICAgICAgIGVudmVsb3BlLmZyb20gPSBsaXN0WzBdXG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSBpZiAoWydUbycsICdDYycsICdCY2MnXS5pbmRleE9mKGhlYWRlci5rZXkpID49IDApIHtcbiAgICAgICAgY29udmVydEFkZHJlc3NlcyhwYXJzZUFkZHJlc3NlcyhoZWFkZXIudmFsdWUpLCBlbnZlbG9wZS50bylcbiAgICAgIH1cbiAgICB9KVxuXG4gICAgcmV0dXJuIGVudmVsb3BlXG4gIH1cblxuICAvKipcbiAgICogQ2hlY2tzIGlmIHRoZSBjb250ZW50IHR5cGUgaXMgbXVsdGlwYXJ0IGFuZCBkZWZpbmVzIGJvdW5kYXJ5IGlmIG5lZWRlZC5cbiAgICogRG9lc24ndCByZXR1cm4gYW55dGhpbmcsIG1vZGlmaWVzIG9iamVjdCBhcmd1bWVudCBpbnN0ZWFkLlxuICAgKlxuICAgKiBAcGFyYW0ge09iamVjdH0gc3RydWN0dXJlZCBQYXJzZWQgaGVhZGVyIHZhbHVlIGZvciAnQ29udGVudC1UeXBlJyBrZXlcbiAgICovXG4gIF9hZGRCb3VuZGFyeSAoc3RydWN0dXJlZCkge1xuICAgIHRoaXMuY29udGVudFR5cGUgPSBzdHJ1Y3R1cmVkLnZhbHVlLnRyaW0oKS50b0xvd2VyQ2FzZSgpXG5cbiAgICB0aGlzLm11bHRpcGFydCA9IHRoaXMuY29udGVudFR5cGUuc3BsaXQoJy8nKS5yZWR1Y2UoZnVuY3Rpb24gKHByZXYsIHZhbHVlKSB7XG4gICAgICByZXR1cm4gcHJldiA9PT0gJ211bHRpcGFydCcgPyB2YWx1ZSA6IGZhbHNlXG4gICAgfSlcblxuICAgIGlmICh0aGlzLm11bHRpcGFydCkge1xuICAgICAgdGhpcy5ib3VuZGFyeSA9IHN0cnVjdHVyZWQucGFyYW1zLmJvdW5kYXJ5ID0gc3RydWN0dXJlZC5wYXJhbXMuYm91bmRhcnkgfHwgdGhpcy5ib3VuZGFyeSB8fCBnZW5lcmF0ZUJvdW5kYXJ5KHRoaXMuX25vZGVJZCwgdGhpcy5yb290Tm9kZS5iYXNlQm91bmRhcnkpXG4gICAgfSBlbHNlIHtcbiAgICAgIHRoaXMuYm91bmRhcnkgPSBmYWxzZVxuICAgIH1cbiAgfVxufVxuIl19 -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.isPlainText = isPlainText; 7 | exports.convertAddresses = convertAddresses; 8 | exports.parseAddresses = parseAddresses; 9 | exports.encodeHeaderValue = encodeHeaderValue; 10 | exports.normalizeHeaderKey = normalizeHeaderKey; 11 | exports.generateBoundary = generateBoundary; 12 | exports.escapeHeaderArgument = escapeHeaderArgument; 13 | exports.buildHeaderValue = buildHeaderValue; 14 | 15 | var _ramda = require('ramda'); 16 | 17 | var _emailjsAddressparser = require('emailjs-addressparser'); 18 | 19 | var _emailjsAddressparser2 = _interopRequireDefault(_emailjsAddressparser); 20 | 21 | var _emailjsMimeCodec = require('emailjs-mime-codec'); 22 | 23 | var _punycode = require('punycode'); 24 | 25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 26 | 27 | /** 28 | * If needed, mime encodes the name part 29 | * 30 | * @param {String} name Name part of an address 31 | * @returns {String} Mime word encoded string if needed 32 | */ 33 | /* eslint-disable node/no-deprecated-api */ 34 | /* eslint-disable no-control-regex */ 35 | 36 | function encodeAddressName(name) { 37 | if (!/^[\w ']*$/.test(name)) { 38 | if (/^[\x20-\x7e]*$/.test(name)) { 39 | return '"' + name.replace(/([\\"])/g, '\\$1') + '"'; 40 | } else { 41 | return (0, _emailjsMimeCodec.mimeWordEncode)(name, 'Q'); 42 | } 43 | } 44 | return name; 45 | } 46 | 47 | /** 48 | * Checks if a value is plaintext string (uses only printable 7bit chars) 49 | * 50 | * @param {String} value String to be tested 51 | * @returns {Boolean} true if it is a plaintext string 52 | */ 53 | function isPlainText(value) { 54 | return !(typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)); 55 | } 56 | 57 | /** 58 | * Rebuilds address object using punycode and other adjustments 59 | * 60 | * @param {Array} addresses An array of address objects 61 | * @param {Array} [uniqueList] An array to be populated with addresses 62 | * @return {String} address string 63 | */ 64 | function convertAddresses() { 65 | var addresses = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 66 | var uniqueList = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 67 | 68 | var values = [];[].concat(addresses).forEach(function (address) { 69 | if (address.address) { 70 | address.address = address.address.replace(/^.*?(?=@)/, function (user) { 71 | return (0, _emailjsMimeCodec.mimeWordsEncode)(user, 'Q'); 72 | }).replace(/@.+$/, function (domain) { 73 | return '@' + (0, _punycode.toASCII)(domain.substr(1)); 74 | }); 75 | 76 | if (!address.name) { 77 | values.push(address.address); 78 | } else if (address.name) { 79 | values.push(encodeAddressName(address.name) + ' <' + address.address + '>'); 80 | } 81 | 82 | if (uniqueList.indexOf(address.address) < 0) { 83 | uniqueList.push(address.address); 84 | } 85 | } else if (address.group) { 86 | values.push(encodeAddressName(address.name) + ':' + (address.group.length ? convertAddresses(address.group, uniqueList) : '').trim() + ';'); 87 | } 88 | }); 89 | 90 | return values.join(', '); 91 | } 92 | 93 | /** 94 | * Parses addresses. Takes in a single address or an array or an 95 | * array of address arrays (eg. To: [[first group], [second group],...]) 96 | * 97 | * @param {Mixed} addresses Addresses to be parsed 98 | * @return {Array} An array of address objects 99 | */ 100 | function parseAddresses() { 101 | var addresses = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 102 | 103 | return (0, _ramda.flatten)([].concat(addresses).map(function (address) { 104 | if (address && address.address) { 105 | address = convertAddresses(address); 106 | } 107 | return (0, _emailjsAddressparser2.default)(address); 108 | })); 109 | } 110 | 111 | /** 112 | * Encodes a header value for use in the generated rfc2822 email. 113 | * 114 | * @param {String} key Header key 115 | * @param {String} value Header value 116 | */ 117 | function encodeHeaderValue(key) { 118 | var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 119 | 120 | key = normalizeHeaderKey(key); 121 | 122 | switch (key) { 123 | case 'From': 124 | case 'Sender': 125 | case 'To': 126 | case 'Cc': 127 | case 'Bcc': 128 | case 'Reply-To': 129 | return convertAddresses(parseAddresses(value)); 130 | 131 | case 'Message-Id': 132 | case 'In-Reply-To': 133 | case 'Content-Id': 134 | value = value.replace(/\r?\n|\r/g, ' '); 135 | 136 | if (value.charAt(0) !== '<') { 137 | value = '<' + value; 138 | } 139 | 140 | if (value.charAt(value.length - 1) !== '>') { 141 | value = value + '>'; 142 | } 143 | return value; 144 | 145 | case 'References': 146 | value = [].concat.apply([], [].concat(value).map(function () { 147 | var elm = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 148 | return elm.replace(/\r?\n|\r/g, ' ').trim().replace(/<[^>]*>/g, function (str) { 149 | return str.replace(/\s/g, ''); 150 | }).split(/\s+/); 151 | })).map(function (elm) { 152 | if (elm.charAt(0) !== '<') { 153 | elm = '<' + elm; 154 | } 155 | if (elm.charAt(elm.length - 1) !== '>') { 156 | elm = elm + '>'; 157 | } 158 | return elm; 159 | }); 160 | 161 | return value.join(' ').trim(); 162 | 163 | default: 164 | return (0, _emailjsMimeCodec.mimeWordsEncode)((value || '').toString().replace(/\r?\n|\r/g, ' '), 'B'); 165 | } 166 | } 167 | 168 | /** 169 | * Normalizes a header key, uses Camel-Case form, except for uppercase MIME- 170 | * 171 | * @param {String} key Key to be normalized 172 | * @return {String} key in Camel-Case form 173 | */ 174 | function normalizeHeaderKey() { 175 | var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 176 | 177 | return key.replace(/\r?\n|\r/g, ' ') // no newlines in keys 178 | .trim().toLowerCase().replace(/^MIME\b|^[a-z]|-[a-z]/ig, function (c) { 179 | return c.toUpperCase(); 180 | }); // use uppercase words, except MIME 181 | } 182 | 183 | /** 184 | * Generates a multipart boundary value 185 | * 186 | * @return {String} boundary value 187 | */ 188 | function generateBoundary(nodeId, baseBoundary) { 189 | return '----sinikael-?=_' + nodeId + '-' + baseBoundary; 190 | } 191 | 192 | /** 193 | * Escapes a header argument value (eg. boundary value for content type), 194 | * adds surrounding quotes if needed 195 | * 196 | * @param {String} value Header argument value 197 | * @return {String} escaped and quoted (if needed) argument value 198 | */ 199 | function escapeHeaderArgument(value) { 200 | if (value.match(/[\s'"\\;/=]|^-/g)) { 201 | return '"' + value.replace(/(["\\])/g, '\\$1') + '"'; 202 | } else { 203 | return value; 204 | } 205 | } 206 | 207 | /** 208 | * Joins parsed header value together as 'value; param1=value1; param2=value2' 209 | * 210 | * @param {Object} structured Parsed header value 211 | * @return {String} joined header value 212 | */ 213 | function buildHeaderValue(structured) { 214 | var paramsArray = []; 215 | 216 | Object.keys(structured.params || {}).forEach(function (param) { 217 | // filename might include unicode characters so it is a special case 218 | if (param === 'filename') { 219 | (0, _emailjsMimeCodec.continuationEncode)(param, structured.params[param], 50).forEach(function (encodedParam) { 220 | // continuation encoded strings are always escaped, so no need to use enclosing quotes 221 | // in fact using quotes might end up with invalid filenames in some clients 222 | paramsArray.push(encodedParam.key + '=' + encodedParam.value); 223 | }); 224 | } else { 225 | paramsArray.push(param + '=' + escapeHeaderArgument(structured.params[param])); 226 | } 227 | }); 228 | 229 | return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : ''); 230 | } 231 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlscy5qcyJdLCJuYW1lcyI6WyJpc1BsYWluVGV4dCIsImNvbnZlcnRBZGRyZXNzZXMiLCJwYXJzZUFkZHJlc3NlcyIsImVuY29kZUhlYWRlclZhbHVlIiwibm9ybWFsaXplSGVhZGVyS2V5IiwiZ2VuZXJhdGVCb3VuZGFyeSIsImVzY2FwZUhlYWRlckFyZ3VtZW50IiwiYnVpbGRIZWFkZXJWYWx1ZSIsImVuY29kZUFkZHJlc3NOYW1lIiwibmFtZSIsInRlc3QiLCJyZXBsYWNlIiwidmFsdWUiLCJhZGRyZXNzZXMiLCJ1bmlxdWVMaXN0IiwidmFsdWVzIiwiY29uY2F0IiwiZm9yRWFjaCIsImFkZHJlc3MiLCJ1c2VyIiwiZG9tYWluIiwic3Vic3RyIiwicHVzaCIsImluZGV4T2YiLCJncm91cCIsImxlbmd0aCIsInRyaW0iLCJqb2luIiwibWFwIiwia2V5IiwiY2hhckF0IiwiYXBwbHkiLCJlbG0iLCJzdHIiLCJzcGxpdCIsInRvU3RyaW5nIiwidG9Mb3dlckNhc2UiLCJjIiwidG9VcHBlckNhc2UiLCJub2RlSWQiLCJiYXNlQm91bmRhcnkiLCJtYXRjaCIsInN0cnVjdHVyZWQiLCJwYXJhbXNBcnJheSIsIk9iamVjdCIsImtleXMiLCJwYXJhbXMiLCJwYXJhbSIsImVuY29kZWRQYXJhbSJdLCJtYXBwaW5ncyI6Ijs7Ozs7UUFtQ2dCQSxXLEdBQUFBLFc7UUFXQUMsZ0IsR0FBQUEsZ0I7UUFpQ0FDLGMsR0FBQUEsYztRQWVBQyxpQixHQUFBQSxpQjtRQXVEQUMsa0IsR0FBQUEsa0I7UUFXQUMsZ0IsR0FBQUEsZ0I7UUFXQUMsb0IsR0FBQUEsb0I7UUFjQUMsZ0IsR0FBQUEsZ0I7O0FBdExoQjs7QUFDQTs7OztBQUNBOztBQUtBOzs7O0FBRUE7Ozs7OztBQVpBO0FBQ0E7O0FBaUJBLFNBQVNDLGlCQUFULENBQTRCQyxJQUE1QixFQUFrQztBQUNoQyxNQUFJLENBQUMsWUFBWUMsSUFBWixDQUFpQkQsSUFBakIsQ0FBTCxFQUE2QjtBQUMzQixR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O0FBQ0EsUUFBSThCLFVBQVUsVUFBZCxFQUEwQjtBQUN4QixnREFBbUJBLEtBQW5CLEVBQTBCTCxXQUFXSSxNQUFYLENBQWtCQyxLQUFsQixDQUExQixFQUFvRCxFQUFwRCxFQUF3RDlCLE9BQXhELENBQWdFLFVBQVUrQixZQUFWLEVBQXdCO0FBQ3RGO0FBQ0E7QUFDQUwsb0JBQVlyQixJQUFaLENBQWlCMEIsYUFBYW5CLEdBQWIsR0FBbUIsR0FBbkIsR0FBeUJtQixhQUFhcEMsS0FBdkQ7QUFDRCxPQUpEO0FBS0QsS0FORCxNQU1PO0FBQ0wrQixrQkFBWXJCLElBQVosQ0FBaUJ5QixRQUFRLEdBQVIsR0FBY3pDLHFCQUFxQm9DLFdBQVdJLE1BQVgsQ0FBa0JDLEtBQWxCLENBQXJCLENBQS9CO0FBQ0Q7QUFDRixHQVhEOztBQWFBLFNBQU9MLFdBQVc5QixLQUFYLElBQW9CK0IsWUFBWWxCLE1BQVosR0FBcUIsT0FBT2tCLFlBQVloQixJQUFaLENBQWlCLElBQWpCLENBQTVCLEdBQXFELEVBQXpFLENBQVA7QUFDRCIsImZpbGUiOiJ1dGlscy5qcyIsInNvdXJjZXNDb250ZW50IjpbIi8qIGVzbGludC1kaXNhYmxlIG5vZGUvbm8tZGVwcmVjYXRlZC1hcGkgKi9cbi8qIGVzbGludC1kaXNhYmxlIG5vLWNvbnRyb2wtcmVnZXggKi9cblxuaW1wb3J0IHsgZmxhdHRlbiB9IGZyb20gJ3JhbWRhJ1xuaW1wb3J0IHBhcnNlQWRkcmVzcyBmcm9tICdlbWFpbGpzLWFkZHJlc3NwYXJzZXInXG5pbXBvcnQge1xuICBtaW1lV29yZHNFbmNvZGUsXG4gIG1pbWVXb3JkRW5jb2RlLFxuICBjb250aW51YXRpb25FbmNvZGVcbn0gZnJvbSAnZW1haWxqcy1taW1lLWNvZGVjJ1xuaW1wb3J0IHsgdG9BU0NJSSB9IGZyb20gJ3B1bnljb2RlJ1xuXG4vKipcbiAqIElmIG5lZWRlZCwgbWltZSBlbmNvZGVzIHRoZSBuYW1lIHBhcnRcbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30gbmFtZSBOYW1lIHBhcnQgb2YgYW4gYWRkcmVzc1xuICogQHJldHVybnMge1N0cmluZ30gTWltZSB3b3JkIGVuY29kZWQgc3RyaW5nIGlmIG5lZWRlZFxuICovXG5mdW5jdGlvbiBlbmNvZGVBZGRyZXNzTmFtZSAobmFtZSkge1xuICBpZiAoIS9eW1xcdyAnXSokLy50ZXN0KG5hbWUpKSB7XG4gICAgaWYgKC9eW1xceDIwLVxceDdlXSokLy50ZXN0KG5hbWUpKSB7XG4gICAgICByZXR1cm4gJ1wiJyArIG5hbWUucmVwbGFjZSgvKFtcXFxcXCJdKS9nLCAnXFxcXCQxJykgKyAnXCInXG4gICAgfSBlbHNlIHtcbiAgICAgIHJldHVybiBtaW1lV29yZEVuY29kZShuYW1lLCAnUScpXG4gICAgfVxuICB9XG4gIHJldHVybiBuYW1lXG59XG5cbi8qKlxuICogQ2hlY2tzIGlmIGEgdmFsdWUgaXMgcGxhaW50ZXh0IHN0cmluZyAodXNlcyBvbmx5IHByaW50YWJsZSA3Yml0IGNoYXJzKVxuICpcbiAqIEBwYXJhbSB7U3RyaW5nfSB2YWx1ZSBTdHJpbmcgdG8gYmUgdGVzdGVkXG4gKiBAcmV0dXJucyB7Qm9vbGVhbn0gdHJ1ZSBpZiBpdCBpcyBhIHBsYWludGV4dCBzdHJpbmdcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGlzUGxhaW5UZXh0ICh2YWx1ZSkge1xuICByZXR1cm4gISh0eXBlb2YgdmFsdWUgIT09ICdzdHJpbmcnIHx8IC9bXFx4MDAtXFx4MDhcXHgwYlxceDBjXFx4MGUtXFx4MWZcXHUwMDgwLVxcdUZGRkZdLy50ZXN0KHZhbHVlKSlcbn1cblxuLyoqXG4gKiBSZWJ1aWxkcyBhZGRyZXNzIG9iamVjdCB1c2luZyBwdW55Y29kZSBhbmQgb3RoZXIgYWRqdXN0bWVudHNcbiAqXG4gKiBAcGFyYW0ge0FycmF5fSBhZGRyZXNzZXMgQW4gYXJyYXkgb2YgYWRkcmVzcyBvYmplY3RzXG4gKiBAcGFyYW0ge0FycmF5fSBbdW5pcXVlTGlzdF0gQW4gYXJyYXkgdG8gYmUgcG9wdWxhdGVkIHdpdGggYWRkcmVzc2VzXG4gKiBAcmV0dXJuIHtTdHJpbmd9IGFkZHJlc3Mgc3RyaW5nXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjb252ZXJ0QWRkcmVzc2VzIChhZGRyZXNzZXMgPSBbXSwgdW5pcXVlTGlzdCA9IFtdKSB7XG4gIHZhciB2YWx1ZXMgPSBbXVxuXG4gIDtbXS5jb25jYXQoYWRkcmVzc2VzKS5mb3JFYWNoKGFkZHJlc3MgPT4ge1xuICAgIGlmIChhZGRyZXNzLmFkZHJlc3MpIHtcbiAgICAgIGFkZHJlc3MuYWRkcmVzcyA9IGFkZHJlc3MuYWRkcmVzc1xuICAgICAgICAucmVwbGFjZSgvXi4qPyg/PUApLywgdXNlciA9PiBtaW1lV29yZHNFbmNvZGUodXNlciwgJ1EnKSlcbiAgICAgICAgLnJlcGxhY2UoL0AuKyQvLCBkb21haW4gPT4gJ0AnICsgdG9BU0NJSShkb21haW4uc3Vic3RyKDEpKSlcblxuICAgICAgaWYgKCFhZGRyZXNzLm5hbWUpIHtcbiAgICAgICAgdmFsdWVzLnB1c2goYWRkcmVzcy5hZGRyZXNzKVxuICAgICAgfSBlbHNlIGlmIChhZGRyZXNzLm5hbWUpIHtcbiAgICAgICAgdmFsdWVzLnB1c2goZW5jb2RlQWRkcmVzc05hbWUoYWRkcmVzcy5uYW1lKSArICcgPCcgKyBhZGRyZXNzLmFkZHJlc3MgKyAnPicpXG4gICAgICB9XG5cbiAgICAgIGlmICh1bmlxdWVMaXN0LmluZGV4T2YoYWRkcmVzcy5hZGRyZXNzKSA8IDApIHtcbiAgICAgICAgdW5pcXVlTGlzdC5wdXNoKGFkZHJlc3MuYWRkcmVzcylcbiAgICAgIH1cbiAgICB9IGVsc2UgaWYgKGFkZHJlc3MuZ3JvdXApIHtcbiAgICAgIHZhbHVlcy5wdXNoKGVuY29kZUFkZHJlc3NOYW1lKGFkZHJlc3MubmFtZSkgKyAnOicgKyAoYWRkcmVzcy5ncm91cC5sZW5ndGggPyBjb252ZXJ0QWRkcmVzc2VzKGFkZHJlc3MuZ3JvdXAsIHVuaXF1ZUxpc3QpIDogJycpLnRyaW0oKSArICc7JylcbiAgICB9XG4gIH0pXG5cbiAgcmV0dXJuIHZhbHVlcy5qb2luKCcsICcpXG59XG5cbi8qKlxuICogUGFyc2VzIGFkZHJlc3Nlcy4gVGFrZXMgaW4gYSBzaW5nbGUgYWRkcmVzcyBvciBhbiBhcnJheSBvciBhblxuICogYXJyYXkgb2YgYWRkcmVzcyBhcnJheXMgKGVnLiBUbzogW1tmaXJzdCBncm91cF0sIFtzZWNvbmQgZ3JvdXBdLC4uLl0pXG4gKlxuICogQHBhcmFtIHtNaXhlZH0gYWRkcmVzc2VzIEFkZHJlc3NlcyB0byBiZSBwYXJzZWRcbiAqIEByZXR1cm4ge0FycmF5fSBBbiBhcnJheSBvZiBhZGRyZXNzIG9iamVjdHNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHBhcnNlQWRkcmVzc2VzIChhZGRyZXNzZXMgPSBbXSkge1xuICByZXR1cm4gZmxhdHRlbihbXS5jb25jYXQoYWRkcmVzc2VzKS5tYXAoKGFkZHJlc3MpID0+IHtcbiAgICBpZiAoYWRkcmVzcyAmJiBhZGRyZXNzLmFkZHJlc3MpIHtcbiAgICAgIGFkZHJlc3MgPSBjb252ZXJ0QWRkcmVzc2VzKGFkZHJlc3MpXG4gICAgfVxuICAgIHJldHVybiBwYXJzZUFkZHJlc3MoYWRkcmVzcylcbiAgfSkpXG59XG5cbi8qKlxuICogRW5jb2RlcyBhIGhlYWRlciB2YWx1ZSBmb3IgdXNlIGluIHRoZSBnZW5lcmF0ZWQgcmZjMjgyMiBlbWFpbC5cbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30ga2V5IEhlYWRlciBrZXlcbiAqIEBwYXJhbSB7U3RyaW5nfSB2YWx1ZSBIZWFkZXIgdmFsdWVcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGVuY29kZUhlYWRlclZhbHVlIChrZXksIHZhbHVlID0gJycpIHtcbiAga2V5ID0gbm9ybWFsaXplSGVhZGVyS2V5KGtleSlcblxuICBzd2l0Y2ggKGtleSkge1xuICAgIGNhc2UgJ0Zyb20nOlxuICAgIGNhc2UgJ1NlbmRlcic6XG4gICAgY2FzZSAnVG8nOlxuICAgIGNhc2UgJ0NjJzpcbiAgICBjYXNlICdCY2MnOlxuICAgIGNhc2UgJ1JlcGx5LVRvJzpcbiAgICAgIHJldHVybiBjb252ZXJ0QWRkcmVzc2VzKHBhcnNlQWRkcmVzc2VzKHZhbHVlKSlcblxuICAgIGNhc2UgJ01lc3NhZ2UtSWQnOlxuICAgIGNhc2UgJ0luLVJlcGx5LVRvJzpcbiAgICBjYXNlICdDb250ZW50LUlkJzpcbiAgICAgIHZhbHVlID0gdmFsdWUucmVwbGFjZSgvXFxyP1xcbnxcXHIvZywgJyAnKVxuXG4gICAgICBpZiAodmFsdWUuY2hhckF0KDApICE9PSAnPCcpIHtcbiAgICAgICAgdmFsdWUgPSAnPCcgKyB2YWx1ZVxuICAgICAgfVxuXG4gICAgICBpZiAodmFsdWUuY2hhckF0KHZhbHVlLmxlbmd0aCAtIDEpICE9PSAnPicpIHtcbiAgICAgICAgdmFsdWUgPSB2YWx1ZSArICc+J1xuICAgICAgfVxuICAgICAgcmV0dXJuIHZhbHVlXG5cbiAgICBjYXNlICdSZWZlcmVuY2VzJzpcbiAgICAgIHZhbHVlID0gW10uY29uY2F0LmFwcGx5KFtdLCBbXS5jb25jYXQodmFsdWUpLm1hcCgoZWxtID0gJycpID0+IGVsbVxuICAgICAgICAucmVwbGFjZSgvXFxyP1xcbnxcXHIvZywgJyAnKVxuICAgICAgICAudHJpbSgpXG4gICAgICAgIC5yZXBsYWNlKC88W14+XSo+L2csIHN0ciA9PiBzdHIucmVwbGFjZSgvXFxzL2csICcnKSlcbiAgICAgICAgLnNwbGl0KC9cXHMrLylcbiAgICAgICkpLm1hcChmdW5jdGlvbiAoZWxtKSB7XG4gICAgICAgIGlmIChlbG0uY2hhckF0KDApICE9PSAnPCcpIHtcbiAgICAgICAgICBlbG0gPSAnPCcgKyBlbG1cbiAgICAgICAgfVxuICAgICAgICBpZiAoZWxtLmNoYXJBdChlbG0ubGVuZ3RoIC0gMSkgIT09ICc+Jykge1xuICAgICAgICAgIGVsbSA9IGVsbSArICc+J1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBlbG1cbiAgICAgIH0pXG5cbiAgICAgIHJldHVybiB2YWx1ZS5qb2luKCcgJykudHJpbSgpXG5cbiAgICBkZWZhdWx0OlxuICAgICAgcmV0dXJuIG1pbWVXb3Jkc0VuY29kZSgodmFsdWUgfHwgJycpLnRvU3RyaW5nKCkucmVwbGFjZSgvXFxyP1xcbnxcXHIvZywgJyAnKSwgJ0InKVxuICB9XG59XG5cbi8qKlxuICogTm9ybWFsaXplcyBhIGhlYWRlciBrZXksIHVzZXMgQ2FtZWwtQ2FzZSBmb3JtLCBleGNlcHQgZm9yIHVwcGVyY2FzZSBNSU1FLVxuICpcbiAqIEBwYXJhbSB7U3RyaW5nfSBrZXkgS2V5IHRvIGJlIG5vcm1hbGl6ZWRcbiAqIEByZXR1cm4ge1N0cmluZ30ga2V5IGluIENhbWVsLUNhc2UgZm9ybVxuICovXG5leHBvcnQgZnVuY3Rpb24gbm9ybWFsaXplSGVhZGVyS2V5IChrZXkgPSAnJykge1xuICByZXR1cm4ga2V5LnJlcGxhY2UoL1xccj9cXG58XFxyL2csICcgJykgLy8gbm8gbmV3bGluZXMgaW4ga2V5c1xuICAgIC50cmltKCkudG9Mb3dlckNhc2UoKVxuICAgIC5yZXBsYWNlKC9eTUlNRVxcYnxeW2Etel18LVthLXpdL2lnLCBjID0+IGMudG9VcHBlckNhc2UoKSkgLy8gdXNlIHVwcGVyY2FzZSB3b3JkcywgZXhjZXB0IE1JTUVcbn1cblxuLyoqXG4gKiBHZW5lcmF0ZXMgYSBtdWx0aXBhcnQgYm91bmRhcnkgdmFsdWVcbiAqXG4gKiBAcmV0dXJuIHtTdHJpbmd9IGJvdW5kYXJ5IHZhbHVlXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZW5lcmF0ZUJvdW5kYXJ5IChub2RlSWQsIGJhc2VCb3VuZGFyeSkge1xuICByZXR1cm4gJy0tLS1zaW5pa2FlbC0/PV8nICsgbm9kZUlkICsgJy0nICsgYmFzZUJvdW5kYXJ5XG59XG5cbi8qKlxuICogRXNjYXBlcyBhIGhlYWRlciBhcmd1bWVudCB2YWx1ZSAoZWcuIGJvdW5kYXJ5IHZhbHVlIGZvciBjb250ZW50IHR5cGUpLFxuICogYWRkcyBzdXJyb3VuZGluZyBxdW90ZXMgaWYgbmVlZGVkXG4gKlxuICogQHBhcmFtIHtTdHJpbmd9IHZhbHVlIEhlYWRlciBhcmd1bWVudCB2YWx1ZVxuICogQHJldHVybiB7U3RyaW5nfSBlc2NhcGVkIGFuZCBxdW90ZWQgKGlmIG5lZWRlZCkgYXJndW1lbnQgdmFsdWVcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGVzY2FwZUhlYWRlckFyZ3VtZW50ICh2YWx1ZSkge1xuICBpZiAodmFsdWUubWF0Y2goL1tcXHMnXCJcXFxcOy89XXxeLS9nKSkge1xuICAgIHJldHVybiAnXCInICsgdmFsdWUucmVwbGFjZSgvKFtcIlxcXFxdKS9nLCAnXFxcXCQxJykgKyAnXCInXG4gIH0gZWxzZSB7XG4gICAgcmV0dXJuIHZhbHVlXG4gIH1cbn1cblxuLyoqXG4gKiBKb2lucyBwYXJzZWQgaGVhZGVyIHZhbHVlIHRvZ2V0aGVyIGFzICd2YWx1ZTsgcGFyYW0xPXZhbHVlMTsgcGFyYW0yPXZhbHVlMidcbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gc3RydWN0dXJlZCBQYXJzZWQgaGVhZGVyIHZhbHVlXG4gKiBAcmV0dXJuIHtTdHJpbmd9IGpvaW5lZCBoZWFkZXIgdmFsdWVcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkSGVhZGVyVmFsdWUgKHN0cnVjdHVyZWQpIHtcbiAgdmFyIHBhcmFtc0FycmF5ID0gW11cblxuICBPYmplY3Qua2V5cyhzdHJ1Y3R1cmVkLnBhcmFtcyB8fCB7fSkuZm9yRWFjaChwYXJhbSA9PiB7XG4gICAgLy8gZmlsZW5hbWUgbWlnaHQgaW5jbHVkZSB1bmljb2RlIGNoYXJhY3RlcnMgc28gaXQgaXMgYSBzcGVjaWFsIGNhc2VcbiAgICBpZiAocGFyYW0gPT09ICdmaWxlbmFtZScpIHtcbiAgICAgIGNvbnRpbnVhdGlvbkVuY29kZShwYXJhbSwgc3RydWN0dXJlZC5wYXJhbXNbcGFyYW1dLCA1MCkuZm9yRWFjaChmdW5jdGlvbiAoZW5jb2RlZFBhcmFtKSB7XG4gICAgICAgIC8vIGNvbnRpbnVhdGlvbiBlbmNvZGVkIHN0cmluZ3MgYXJlIGFsd2F5cyBlc2NhcGVkLCBzbyBubyBuZWVkIHRvIHVzZSBlbmNsb3NpbmcgcXVvdGVzXG4gICAgICAgIC8vIGluIGZhY3QgdXNpbmcgcXVvdGVzIG1pZ2h0IGVuZCB1cCB3aXRoIGludmFsaWQgZmlsZW5hbWVzIGluIHNvbWUgY2xpZW50c1xuICAgICAgICBwYXJhbXNBcnJheS5wdXNoKGVuY29kZWRQYXJhbS5rZXkgKyAnPScgKyBlbmNvZGVkUGFyYW0udmFsdWUpXG4gICAgICB9KVxuICAgIH0gZWxzZSB7XG4gICAgICBwYXJhbXNBcnJheS5wdXNoKHBhcmFtICsgJz0nICsgZXNjYXBlSGVhZGVyQXJndW1lbnQoc3RydWN0dXJlZC5wYXJhbXNbcGFyYW1dKSlcbiAgICB9XG4gIH0pXG5cbiAgcmV0dXJuIHN0cnVjdHVyZWQudmFsdWUgKyAocGFyYW1zQXJyYXkubGVuZ3RoID8gJzsgJyArIHBhcmFtc0FycmF5LmpvaW4oJzsgJykgOiAnJylcbn1cbiJdfQ== -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emailjs-mime-builder", 3 | "version": "2.0.5", 4 | "homepage": "https://github.com/emailjs/emailjs-mime-builder", 5 | "description": "emailjs-mime-builder is a low level rfc2822 message composer. Define your own mime tree, no magic included.", 6 | "author": "Andris Reinman ", 7 | "keywords": [ 8 | "RFC2822", 9 | "mime" 10 | ], 11 | "license": "MIT", 12 | "scripts": { 13 | "build": "./scripts/build.sh", 14 | "lint": "$(npm bin)/standard", 15 | "preversion": "npm run build", 16 | "test": "npm run lint && npm run unit", 17 | "unit": "$(npm bin)/mocha './src/*-unit.js' --reporter spec --require babel-register testutils.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/emailjs/emailjs-mime-builder.git" 22 | }, 23 | "main": "dist/builder", 24 | "dependencies": { 25 | "emailjs-addressparser": "^2.0.2", 26 | "emailjs-mime-codec": "^2.0.8", 27 | "emailjs-mime-types": "^2.1.0", 28 | "punycode": "2.1.1", 29 | "ramda": "^0.26.1" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-preset-env": "^1.7.0", 34 | "babel-register": "^6.26.0", 35 | "chai": "^4.2.0", 36 | "mocha": "^6.1.4", 37 | "pre-commit": "^1.2.2", 38 | "standard": "^12.0.1" 39 | }, 40 | "standard": { 41 | "globals": [ 42 | "describe", 43 | "it", 44 | "before", 45 | "beforeEach", 46 | "afterEach", 47 | "after", 48 | "expect" 49 | ], 50 | "ignore": [ 51 | "dist" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf $PWD/dist 4 | babel src --out-dir dist --ignore '**/*-unit.js' --source-maps inline 5 | git reset 6 | git add $PWD/dist 7 | git commit -m 'Updating dist files' -n 8 | -------------------------------------------------------------------------------- /src/builder-unit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import Mimebuilder from './builder' 4 | 5 | describe('Mimebuilder', function () { 6 | it('should create Mimebuilder object', function () { 7 | expect(new Mimebuilder()).to.exist 8 | }) 9 | 10 | describe('createChild', function () { 11 | it('should create child', function () { 12 | const mb = new Mimebuilder('multipart/mixed') 13 | 14 | const child = mb.createChild('multipart/mixed') 15 | expect(child.parentNode).to.equal(mb) 16 | expect(child.rootNode).to.equal(mb) 17 | 18 | const subchild1 = child.createChild('text/html') 19 | expect(subchild1.parentNode).to.equal(child) 20 | expect(subchild1.rootNode).to.equal(mb) 21 | 22 | const subchild2 = child.createChild('text/html') 23 | expect(subchild2.parentNode).to.equal(child) 24 | expect(subchild2.rootNode).to.equal(mb) 25 | }) 26 | }) 27 | 28 | describe('appendChild', function () { 29 | it('should append child node', function () { 30 | const mb = new Mimebuilder('multipart/mixed') 31 | 32 | const child = new Mimebuilder('text/plain') 33 | mb.appendChild(child) 34 | expect(child.parentNode).to.equal(mb) 35 | expect(child.rootNode).to.equal(mb) 36 | expect(mb._childNodes.length).to.equal(1) 37 | expect(mb._childNodes[0]).to.equal(child) 38 | }) 39 | }) 40 | 41 | describe('replace', function () { 42 | it('should replace node', function () { 43 | const mb = new Mimebuilder() 44 | const child = mb.createChild('text/plain') 45 | const replacement = new Mimebuilder('image/png') 46 | 47 | child.replace(replacement) 48 | 49 | expect(mb._childNodes.length).to.equal(1) 50 | expect(mb._childNodes[0]).to.equal(replacement) 51 | }) 52 | }) 53 | 54 | describe('remove', function () { 55 | it('should remove node', function () { 56 | const mb = new Mimebuilder() 57 | const child = mb.createChild('text/plain') 58 | 59 | child.remove() 60 | expect(mb._childNodes.length).to.equal(0) 61 | expect(child.parenNode).to.not.exist 62 | }) 63 | }) 64 | 65 | describe('setHeader', function () { 66 | it('should set header', function () { 67 | const mb = new Mimebuilder() 68 | 69 | mb.setHeader('key', 'value') 70 | mb.setHeader('key', 'value1') 71 | expect(mb.getHeader('Key')).to.equal('value1') 72 | 73 | mb.setHeader([{ 74 | key: 'key', 75 | value: 'value2' 76 | }, { 77 | key: 'key2', 78 | value: 'value3' 79 | }]) 80 | 81 | expect(mb._headers).to.deep.equal([{ 82 | key: 'Key', 83 | value: 'value2' 84 | }, { 85 | key: 'Key2', 86 | value: 'value3' 87 | }]) 88 | 89 | mb.setHeader({ 90 | key: 'value4', 91 | key2: 'value5' 92 | }) 93 | 94 | expect(mb._headers).to.deep.equal([{ 95 | key: 'Key', 96 | value: 'value4' 97 | }, { 98 | key: 'Key2', 99 | value: 'value5' 100 | }]) 101 | }) 102 | }) 103 | 104 | describe('addHeader', function () { 105 | it('should add header', function () { 106 | const mb = new Mimebuilder() 107 | 108 | mb.addHeader('key', 'value1') 109 | mb.addHeader('key', 'value2') 110 | 111 | mb.addHeader([{ 112 | key: 'key', 113 | value: 'value2' 114 | }, { 115 | key: 'key2', 116 | value: 'value3' 117 | }]) 118 | 119 | mb.addHeader({ 120 | key: 'value4', 121 | key2: 'value5' 122 | }) 123 | 124 | expect(mb._headers).to.deep.equal([{ 125 | key: 'Key', 126 | value: 'value1' 127 | }, { 128 | key: 'Key', 129 | value: 'value2' 130 | }, { 131 | key: 'Key', 132 | value: 'value2' 133 | }, { 134 | key: 'Key2', 135 | value: 'value3' 136 | }, { 137 | key: 'Key', 138 | value: 'value4' 139 | }, { 140 | key: 'Key2', 141 | value: 'value5' 142 | }]) 143 | }) 144 | }) 145 | 146 | describe('getHeader', function () { 147 | it('should return first matching header value', function () { 148 | const mb = new Mimebuilder() 149 | mb._headers = [{ 150 | key: 'Key', 151 | value: 'value4' 152 | }, { 153 | key: 'Key2', 154 | value: 'value5' 155 | }] 156 | 157 | expect(mb.getHeader('KEY')).to.equal('value4') 158 | }) 159 | }) 160 | 161 | describe('setContent', function () { 162 | it('should set the contents for a node', function () { 163 | const mb = new Mimebuilder() 164 | mb.setContent('abc') 165 | expect(mb.content).to.equal('abc') 166 | }) 167 | }) 168 | 169 | describe('build', function () { 170 | it('should build root node', function () { 171 | const mb = new Mimebuilder('text/plain') 172 | .setHeader({ 173 | date: '12345', 174 | 'message-id': '67890' 175 | }) 176 | .setContent('Hello world!') 177 | 178 | const expected = 'Content-Type: text/plain\r\n' + 179 | 'Date: 12345\r\n' + 180 | 'Message-Id: <67890>\r\n' + 181 | 'Content-Transfer-Encoding: 7bit\r\n' + 182 | 'MIME-Version: 1.0\r\n' + 183 | '\r\n' + 184 | 'Hello world!' 185 | 186 | expect(mb.build()).to.equal(expected) 187 | }) 188 | 189 | it('should build child node', function () { 190 | const mb = new Mimebuilder('multipart/mixed') 191 | const childNode = mb.createChild('text/plain') 192 | .setContent('Hello world!') 193 | 194 | const expected = 'Content-Type: text/plain\r\n' + 195 | 'Content-Transfer-Encoding: 7bit\r\n' + 196 | '\r\n' + 197 | 'Hello world!' 198 | 199 | expect(childNode.build()).to.equal(expected) 200 | }) 201 | 202 | it('should build multipart node', function () { 203 | const mb = new Mimebuilder('multipart/mixed', { 204 | baseBoundary: 'test' 205 | }) 206 | .setHeader({ 207 | date: '12345', 208 | 'message-id': '67890' 209 | }) 210 | 211 | const expected = 'Content-Type: multipart/mixed; boundary="----sinikael-?=_1-test"\r\n' + 212 | 'Date: 12345\r\n' + 213 | 'Message-Id: <67890>\r\n' + 214 | 'MIME-Version: 1.0\r\n' + 215 | '\r\n' + 216 | '------sinikael-?=_1-test\r\n' + 217 | 'Content-Type: text/plain\r\n' + 218 | 'Content-Transfer-Encoding: 7bit\r\n' + 219 | '\r\n' + 220 | 'Hello world!\r\n' + 221 | '------sinikael-?=_1-test--\r\n' 222 | 223 | mb.createChild('text/plain').setContent('Hello world!') 224 | 225 | expect(mb.build()).to.equal(expected) 226 | }) 227 | 228 | it('should build root with generated headers', function () { 229 | const msg = new Mimebuilder('text/plain').build() 230 | 231 | expect(/^Date:\s/m.test(msg)).to.be.true 232 | expect(/^Message-Id:\s abc\nabc') 358 | .build() 359 | 360 | expect(/^Content-Type: text\/plain; format=flowed$/m.test(msg)).to.be.true 361 | expect(/^Content-Transfer-Encoding: 7bit$/m.test(msg)).to.be.true 362 | 363 | msg = msg.split('\r\n\r\n') 364 | msg.shift() 365 | msg = msg.join('\r\n\r\n') 366 | 367 | expect(msg).to.equal('tere\r\n From\r\n Hello\r\n > abc\r\nabc') 368 | }) 369 | 370 | it('should use auto charset in unicode text', function () { 371 | const msg = new Mimebuilder('text/plain') 372 | .setContent('jõgeva') 373 | .build() 374 | 375 | expect(/\r\n\r\nj=C3=B5geva$/.test(msg)).to.be.true 376 | expect(/^Content-Type: text\/plain; charset=utf-8$/m.test(msg)).to.be.true 377 | expect(/^Content-Transfer-Encoding: quoted-printable$/m.test(msg)).to.be.true 378 | }) 379 | 380 | it('should fetch ascii filename', function () { 381 | const msg = new Mimebuilder('text/plain', { 382 | filename: 'jogeva.txt' 383 | }) 384 | .setContent('jogeva') 385 | .build() 386 | 387 | expect(/\r\n\r\njogeva$/.test(msg)).to.be.true 388 | expect(/^Content-Type: text\/plain$/m.test(msg)).to.be.true 389 | expect(/^Content-Transfer-Encoding: 7bit$/m.test(msg)).to.be.true 390 | expect(/^Content-Disposition: attachment; filename=jogeva.txt$/m.test(msg)).to.be.true 391 | }) 392 | 393 | it('should set unicode filename', function () { 394 | const msg = new Mimebuilder('text/plain', { 395 | filename: 'jõgeva.txt' 396 | }) 397 | .setContent('jõgeva') 398 | .build() 399 | 400 | expect(/\r\n\r\nj=C3=B5geva$/.test(msg)).to.be.true 401 | expect(/^Content-Type: text\/plain; charset=utf-8$/m.test(msg)).to.be.true 402 | expect(/^Content-Transfer-Encoding: quoted-printable$/m.test(msg)).to.be.true 403 | expect(/^Content-Disposition: attachment; filename\*0\*=utf-8''j%C3%B5geva.txt$/m.test(msg)).to.be.true 404 | }) 405 | 406 | it('should set filename with a smiley without throwing URIError: URI malformed', function () { 407 | expect(() => new Mimebuilder('text/plain', { 408 | filename: 'Emoji-😊.png' 409 | }).build()).to.not.throw() 410 | }) 411 | 412 | it('should detect content type from filename', function () { 413 | const msg = new Mimebuilder(false, { 414 | filename: 'jogeva.zip' 415 | }) 416 | .setContent('jogeva') 417 | .build() 418 | 419 | expect(/^Content-Type: application\/zip$/m.test(msg)).to.be.true 420 | }) 421 | 422 | it('should convert address objects', function () { 423 | const msg = new Mimebuilder(false) 424 | .setHeader({ 425 | from: [{ 426 | name: 'the safewithme testuser', 427 | address: 'safewithme.testuser@jõgeva.com' 428 | }], 429 | cc: [{ 430 | name: 'the safewithme testuser', 431 | address: 'safewithme.testuser@jõgeva.com' 432 | }] 433 | }) 434 | 435 | expect(/^From: the safewithme testuser $/m.test(msg.build())).to.be.true 436 | expect(/^Cc: the safewithme testuser $/m.test(msg.build())).to.be.true 437 | 438 | expect(msg.getEnvelope()).to.deep.equal({ 439 | from: 'safewithme.testuser@xn--jgeva-dua.com', 440 | to: [ 441 | 'safewithme.testuser@xn--jgeva-dua.com' 442 | ] 443 | }) 444 | }) 445 | 446 | it('should skip empty header', function () { 447 | const mb = new Mimebuilder('text/plain') 448 | .setHeader({ 449 | a: 'b', 450 | cc: '', 451 | dd: [], 452 | o: false, 453 | date: 'zzz', 454 | 'message-id': '67890' 455 | }) 456 | .setContent('Hello world!') 457 | const expected = 'Content-Type: text/plain\r\n' + 458 | 'A: b\r\n' + 459 | 'Date: zzz\r\n' + 460 | 'Message-Id: <67890>\r\n' + 461 | 'Content-Transfer-Encoding: 7bit\r\n' + 462 | 'MIME-Version: 1.0\r\n' + 463 | '\r\n' + 464 | 'Hello world!' 465 | 466 | expect(mb.build()).to.equal(expected) 467 | }) 468 | 469 | it('should set default transfer encoding for application content', function () { 470 | const mb = new Mimebuilder('application/x-my-stuff') 471 | .setHeader({ 472 | date: '12345', 473 | 'message-id': '67890' 474 | }) 475 | .setContent('Hello world!') 476 | 477 | const expected = 'Content-Type: application/x-my-stuff\r\n' + 478 | 'Date: 12345\r\n' + 479 | 'Message-Id: <67890>\r\n' + 480 | 'Content-Transfer-Encoding: base64\r\n' + 481 | 'MIME-Version: 1.0\r\n' + 482 | '\r\n' + 483 | 'SGVsbG8gd29ybGQh' 484 | 485 | expect(mb.build()).to.equal(expected) 486 | }) 487 | 488 | it('should not set transfer encoding for multipart content', function () { 489 | const mb = new Mimebuilder('multipart/global') 490 | .setHeader({ 491 | date: '12345', 492 | 'message-id': '67890' 493 | }) 494 | .setContent('Hello world!') 495 | const expected = 'Content-Type: multipart/global; boundary=abc\r\n' + 496 | 'Date: 12345\r\n' + 497 | 'Message-Id: <67890>\r\n' + 498 | 'MIME-Version: 1.0\r\n' + 499 | '\r\n' + 500 | 'Hello world!\r\n' + 501 | '\r\n' + 502 | '--abc--' + 503 | '\r\n' 504 | 505 | mb.boundary = 'abc' 506 | 507 | expect(mb.build()).to.equal(expected) 508 | }) 509 | 510 | it('should use from domain for message-id', function () { 511 | const mb = new Mimebuilder('text/plain') 512 | .setHeader({ 513 | from: 'test@example.com' 514 | }) 515 | 516 | expect(/^Message-Id: <\d+(-[a-f0-9]{8}){3}@example\.com>$/m.test(mb.build())).to.be.true 517 | }) 518 | 519 | it('should fallback to localhost for message-id', function () { 520 | const mb = new Mimebuilder('text/plain') 521 | 522 | expect(/^Message-Id: <\d+(-[a-f0-9]{8}){3}@localhost>$/m.test(mb.build())).to.be.true 523 | }) 524 | }) 525 | 526 | describe('getEnvelope', function () { 527 | it('should get envelope', function () { 528 | expect(new Mimebuilder().addHeader({ 529 | from: 'From ', 530 | sender: 'Sender ', 531 | to: 'receiver1@example.com' 532 | }).addHeader({ 533 | to: 'receiver2@example.com', 534 | cc: 'receiver1@example.com, receiver3@example.com', 535 | bcc: 'receiver4@example.com, Rec5 ' 536 | }).getEnvelope()).to.deep.equal({ 537 | from: 'from@example.com', 538 | to: ['receiver1@example.com', 'receiver2@example.com', 'receiver3@example.com', 'receiver4@example.com', 'receiver5@example.com'] 539 | }) 540 | 541 | expect(new Mimebuilder().addHeader({ 542 | sender: 'Sender ', 543 | to: 'receiver1@example.com' 544 | }).addHeader({ 545 | to: 'receiver2@example.com', 546 | cc: 'receiver1@example.com, receiver3@example.com', 547 | bcc: 'receiver4@example.com, Rec5 ' 548 | }).getEnvelope()).to.deep.equal({ 549 | from: 'sender@example.com', 550 | to: ['receiver1@example.com', 'receiver2@example.com', 'receiver3@example.com', 'receiver4@example.com', 'receiver5@example.com'] 551 | }) 552 | }) 553 | }) 554 | 555 | describe('_addBoundary', function () { 556 | it('should do nothing on non multipart', function () { 557 | const mb = new Mimebuilder() 558 | expect(mb.boundary).to.not.exist 559 | mb._addBoundary({ 560 | value: 'text/plain' 561 | }) 562 | expect(mb.boundary).to.be.false 563 | expect(mb.multipart).to.be.false 564 | }) 565 | 566 | it('should use provided boundary', function () { 567 | const mb = new Mimebuilder() 568 | expect(mb.boundary).to.not.exist 569 | mb._addBoundary({ 570 | value: 'multipart/mixed', 571 | params: { 572 | boundary: 'abc' 573 | } 574 | }) 575 | expect(mb.boundary).to.equal('abc') 576 | expect(mb.multipart).to.equal('mixed') 577 | }) 578 | 579 | it('should generate boundary', function () { 580 | const mb = new Mimebuilder() 581 | 582 | expect(mb.boundary).to.not.exist 583 | mb._addBoundary({ 584 | value: 'multipart/mixed', 585 | params: {} 586 | }) 587 | expect(mb.boundary).to.exist 588 | expect(mb.multipart).to.equal('mixed') 589 | }) 590 | }) 591 | }) 592 | -------------------------------------------------------------------------------- /src/builder.js: -------------------------------------------------------------------------------- 1 | import { 2 | base64Encode, 3 | quotedPrintableEncode, 4 | foldLines, 5 | parseHeaderValue 6 | } from 'emailjs-mime-codec' 7 | import { detectMimeType } from 'emailjs-mime-types' 8 | import { 9 | convertAddresses, 10 | parseAddresses, 11 | encodeHeaderValue, 12 | normalizeHeaderKey, 13 | generateBoundary, 14 | isPlainText, 15 | buildHeaderValue 16 | } from './utils' 17 | 18 | /** 19 | * Creates a new mime tree node. Assumes 'multipart/*' as the content type 20 | * if it is a branch, anything else counts as leaf. If rootNode is missing from 21 | * the options, assumes this is the root. 22 | * 23 | * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename) 24 | * @param {Object} [options] optional options 25 | * @param {Object} [options.rootNode] root node for this tree 26 | * @param {Object} [options.parentNode] immediate parent for this node 27 | * @param {Object} [options.filename] filename for an attachment node 28 | * @param {String} [options.baseBoundary] shared part of the unique multipart boundary 29 | */ 30 | export default class MimeNode { 31 | constructor (contentType, options = {}) { 32 | this.nodeCounter = 0 33 | 34 | /** 35 | * shared part of the unique multipart boundary 36 | */ 37 | this.baseBoundary = options.baseBoundary || Date.now().toString() + Math.random() 38 | 39 | /** 40 | * If date headers is missing and current node is the root, this value is used instead 41 | */ 42 | this.date = new Date() 43 | 44 | /** 45 | * Root node for current mime tree 46 | */ 47 | this.rootNode = options.rootNode || this 48 | 49 | /** 50 | * If filename is specified but contentType is not (probably an attachment) 51 | * detect the content type from filename extension 52 | */ 53 | if (options.filename) { 54 | /** 55 | * Filename for this node. Useful with attachments 56 | */ 57 | this.filename = options.filename 58 | if (!contentType) { 59 | contentType = detectMimeType(this.filename.split('.').pop()) 60 | } 61 | } 62 | 63 | /** 64 | * Immediate parent for this node (or undefined if not set) 65 | */ 66 | this.parentNode = options.parentNode 67 | 68 | /** 69 | * Used for generating unique boundaries (prepended to the shared base) 70 | */ 71 | this._nodeId = ++this.rootNode.nodeCounter 72 | 73 | /** 74 | * An array for possible child nodes 75 | */ 76 | this._childNodes = [] 77 | 78 | /** 79 | * A list of header values for this node in the form of [{key:'', value:''}] 80 | */ 81 | this._headers = [] 82 | 83 | /** 84 | * If content type is set (or derived from the filename) add it to headers 85 | */ 86 | if (contentType) { 87 | this.setHeader('content-type', contentType) 88 | } 89 | 90 | /** 91 | * If true then BCC header is included in RFC2822 message. 92 | */ 93 | this.includeBccInHeader = options.includeBccInHeader || false 94 | } 95 | 96 | /** 97 | * Creates and appends a child node. Arguments provided are passed to MimeNode constructor 98 | * 99 | * @param {String} [contentType] Optional content type 100 | * @param {Object} [options] Optional options object 101 | * @return {Object} Created node object 102 | */ 103 | createChild (contentType, options = {}) { 104 | var node = new MimeNode(contentType, options) 105 | this.appendChild(node) 106 | return node 107 | } 108 | 109 | /** 110 | * Appends an existing node to the mime tree. Removes the node from an existing 111 | * tree if needed 112 | * 113 | * @param {Object} childNode node to be appended 114 | * @return {Object} Appended node object 115 | */ 116 | appendChild (childNode) { 117 | if (childNode.rootNode !== this.rootNode) { 118 | childNode.rootNode = this.rootNode 119 | childNode._nodeId = ++this.rootNode.nodeCounter 120 | } 121 | 122 | childNode.parentNode = this 123 | 124 | this._childNodes.push(childNode) 125 | return childNode 126 | } 127 | 128 | /** 129 | * Replaces current node with another node 130 | * 131 | * @param {Object} node Replacement node 132 | * @return {Object} Replacement node 133 | */ 134 | replace (node) { 135 | if (node === this) { 136 | return this 137 | } 138 | 139 | this.parentNode._childNodes.forEach((childNode, i) => { 140 | if (childNode === this) { 141 | node.rootNode = this.rootNode 142 | node.parentNode = this.parentNode 143 | node._nodeId = this._nodeId 144 | 145 | this.rootNode = this 146 | this.parentNode = undefined 147 | 148 | node.parentNode._childNodes[i] = node 149 | } 150 | }) 151 | 152 | return node 153 | } 154 | 155 | /** 156 | * Removes current node from the mime tree 157 | * 158 | * @return {Object} removed node 159 | */ 160 | remove () { 161 | if (!this.parentNode) { 162 | return this 163 | } 164 | 165 | for (var i = this.parentNode._childNodes.length - 1; i >= 0; i--) { 166 | if (this.parentNode._childNodes[i] === this) { 167 | this.parentNode._childNodes.splice(i, 1) 168 | this.parentNode = undefined 169 | this.rootNode = this 170 | return this 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Sets a header value. If the value for selected key exists, it is overwritten. 177 | * You can set multiple values as well by using [{key:'', value:''}] or 178 | * {key: 'value'} as the first argument. 179 | * 180 | * @param {String|Array|Object} key Header key or a list of key value pairs 181 | * @param {String} value Header value 182 | * @return {Object} current node 183 | */ 184 | setHeader (key, value) { 185 | let added = false 186 | 187 | // Allow setting multiple headers at once 188 | if (!value && key && typeof key === 'object') { 189 | if (key.key && key.value) { 190 | // allow {key:'content-type', value: 'text/plain'} 191 | this.setHeader(key.key, key.value) 192 | } else if (Array.isArray(key)) { 193 | // allow [{key:'content-type', value: 'text/plain'}] 194 | key.forEach(i => this.setHeader(i.key, i.value)) 195 | } else { 196 | // allow {'content-type': 'text/plain'} 197 | Object.keys(key).forEach(i => this.setHeader(i, key[i])) 198 | } 199 | return this 200 | } 201 | 202 | key = normalizeHeaderKey(key) 203 | 204 | const headerValue = { key, value } 205 | 206 | // Check if the value exists and overwrite 207 | for (var i = 0, len = this._headers.length; i < len; i++) { 208 | if (this._headers[i].key === key) { 209 | if (!added) { 210 | // replace the first match 211 | this._headers[i] = headerValue 212 | added = true 213 | } else { 214 | // remove following matches 215 | this._headers.splice(i, 1) 216 | i-- 217 | len-- 218 | } 219 | } 220 | } 221 | 222 | // match not found, append the value 223 | if (!added) { 224 | this._headers.push(headerValue) 225 | } 226 | 227 | return this 228 | } 229 | 230 | /** 231 | * Adds a header value. If the value for selected key exists, the value is appended 232 | * as a new field and old one is not touched. 233 | * You can set multiple values as well by using [{key:'', value:''}] or 234 | * {key: 'value'} as the first argument. 235 | * 236 | * @param {String|Array|Object} key Header key or a list of key value pairs 237 | * @param {String} value Header value 238 | * @return {Object} current node 239 | */ 240 | addHeader (key, value) { 241 | // Allow setting multiple headers at once 242 | if (!value && key && typeof key === 'object') { 243 | if (key.key && key.value) { 244 | // allow {key:'content-type', value: 'text/plain'} 245 | this.addHeader(key.key, key.value) 246 | } else if (Array.isArray(key)) { 247 | // allow [{key:'content-type', value: 'text/plain'}] 248 | key.forEach(i => this.addHeader(i.key, i.value)) 249 | } else { 250 | // allow {'content-type': 'text/plain'} 251 | Object.keys(key).forEach(i => this.addHeader(i, key[i])) 252 | } 253 | return this 254 | } 255 | 256 | this._headers.push({ key: normalizeHeaderKey(key), value }) 257 | 258 | return this 259 | } 260 | 261 | /** 262 | * Retrieves the first mathcing value of a selected key 263 | * 264 | * @param {String} key Key to search for 265 | * @retun {String} Value for the key 266 | */ 267 | getHeader (key) { 268 | key = normalizeHeaderKey(key) 269 | for (let i = 0, len = this._headers.length; i < len; i++) { 270 | if (this._headers[i].key === key) { 271 | return this._headers[i].value 272 | } 273 | } 274 | } 275 | 276 | /** 277 | * Sets body content for current node. If the value is a string, charset is added automatically 278 | * to Content-Type (if it is text/*). If the value is a Typed Array, you need to specify 279 | * the charset yourself 280 | * 281 | * @param (String|Uint8Array) content Body content 282 | * @return {Object} current node 283 | */ 284 | setContent (content) { 285 | this.content = content 286 | return this 287 | } 288 | 289 | /** 290 | * Builds the rfc2822 message from the current node. If this is a root node, 291 | * mandatory header fields are set if missing (Date, Message-Id, MIME-Version) 292 | * 293 | * @return {String} Compiled message 294 | */ 295 | build () { 296 | const lines = [] 297 | const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim() 298 | let transferEncoding 299 | let flowed 300 | 301 | if (this.content) { 302 | transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim() 303 | if (!transferEncoding || ['base64', 'quoted-printable'].indexOf(transferEncoding) < 0) { 304 | if (/^text\//i.test(contentType)) { 305 | // If there are no special symbols, no need to modify the text 306 | if (isPlainText(this.content)) { 307 | // If there are lines longer than 76 symbols/bytes, make the text 'flowed' 308 | if (/^.{77,}/m.test(this.content)) { 309 | flowed = true 310 | } 311 | transferEncoding = '7bit' 312 | } else { 313 | transferEncoding = 'quoted-printable' 314 | } 315 | } else if (!/^multipart\//i.test(contentType)) { 316 | transferEncoding = transferEncoding || 'base64' 317 | } 318 | } 319 | 320 | if (transferEncoding) { 321 | this.setHeader('Content-Transfer-Encoding', transferEncoding) 322 | } 323 | } 324 | 325 | if (this.filename && !this.getHeader('Content-Disposition')) { 326 | this.setHeader('Content-Disposition', 'attachment') 327 | } 328 | 329 | this._headers.forEach(header => { 330 | const key = header.key 331 | let value = header.value 332 | let structured 333 | 334 | switch (header.key) { 335 | case 'Content-Disposition': 336 | structured = parseHeaderValue(value) 337 | if (this.filename) { 338 | structured.params.filename = this.filename 339 | } 340 | value = buildHeaderValue(structured) 341 | break 342 | case 'Content-Type': 343 | structured = parseHeaderValue(value) 344 | 345 | this._addBoundary(structured) 346 | 347 | if (flowed) { 348 | structured.params.format = 'flowed' 349 | } 350 | if (String(structured.params.format).toLowerCase().trim() === 'flowed') { 351 | flowed = true 352 | } 353 | 354 | if (structured.value.match(/^text\//) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) { 355 | structured.params.charset = 'utf-8' 356 | } 357 | 358 | value = buildHeaderValue(structured) 359 | break 360 | case 'Bcc': 361 | if (this.includeBccInHeader === false) { 362 | // skip BCC values 363 | return 364 | } 365 | } 366 | 367 | // skip empty lines 368 | value = encodeHeaderValue(key, value) 369 | if (!(value || '').toString().trim()) { 370 | return 371 | } 372 | 373 | lines.push(foldLines(key + ': ' + value)) 374 | }) 375 | 376 | // Ensure mandatory header fields 377 | if (this.rootNode === this) { 378 | if (!this.getHeader('Date')) { 379 | lines.push('Date: ' + this.date.toUTCString().replace(/GMT/, '+0000')) 380 | } 381 | // You really should define your own Message-Id field 382 | if (!this.getHeader('Message-Id')) { 383 | lines.push('Message-Id: <' + 384 | // crux to generate random strings like this: 385 | // "1401391905590-58aa8c32-d32a065c-c1a2aad2" 386 | [0, 0, 0].reduce(function (prev) { 387 | return prev + '-' + Math.floor((1 + Math.random()) * 0x100000000) 388 | .toString(16) 389 | .substring(1) 390 | }, Date.now()) + 391 | '@' + 392 | // try to use the domain of the FROM address or fallback localhost 393 | (this.getEnvelope().from || 'localhost').split('@').pop() + 394 | '>') 395 | } 396 | if (!this.getHeader('MIME-Version')) { 397 | lines.push('MIME-Version: 1.0') 398 | } 399 | } 400 | lines.push('') 401 | 402 | if (this.content) { 403 | switch (transferEncoding) { 404 | case 'quoted-printable': 405 | lines.push(quotedPrintableEncode(this.content)) 406 | break 407 | case 'base64': 408 | lines.push(base64Encode(this.content, typeof this.content === 'object' ? 'binary' : undefined)) 409 | break 410 | default: 411 | if (flowed) { 412 | // space stuffing http://tools.ietf.org/html/rfc3676#section-4.2 413 | lines.push(foldLines(this.content.replace(/\r?\n/g, '\r\n').replace(/^( |From|>)/igm, ' $1'), 76, true)) 414 | } else { 415 | lines.push(this.content.replace(/\r?\n/g, '\r\n')) 416 | } 417 | } 418 | if (this.multipart) { 419 | lines.push('') 420 | } 421 | } 422 | 423 | if (this.multipart) { 424 | this._childNodes.forEach(node => { 425 | lines.push('--' + this.boundary) 426 | lines.push(node.build()) 427 | }) 428 | lines.push('--' + this.boundary + '--') 429 | lines.push('') 430 | } 431 | 432 | return lines.join('\r\n') 433 | } 434 | 435 | /** 436 | * Generates and returns SMTP envelope with the sender address and a list of recipients addresses 437 | * 438 | * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']} 439 | */ 440 | getEnvelope () { 441 | var envelope = { 442 | from: false, 443 | to: [] 444 | } 445 | this._headers.forEach(header => { 446 | var list = [] 447 | if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].indexOf(header.key) >= 0)) { 448 | convertAddresses(parseAddresses(header.value), list) 449 | if (list.length && list[0]) { 450 | envelope.from = list[0] 451 | } 452 | } else if (['To', 'Cc', 'Bcc'].indexOf(header.key) >= 0) { 453 | convertAddresses(parseAddresses(header.value), envelope.to) 454 | } 455 | }) 456 | 457 | return envelope 458 | } 459 | 460 | /** 461 | * Checks if the content type is multipart and defines boundary if needed. 462 | * Doesn't return anything, modifies object argument instead. 463 | * 464 | * @param {Object} structured Parsed header value for 'Content-Type' key 465 | */ 466 | _addBoundary (structured) { 467 | this.contentType = structured.value.trim().toLowerCase() 468 | 469 | this.multipart = this.contentType.split('/').reduce(function (prev, value) { 470 | return prev === 'multipart' ? value : false 471 | }) 472 | 473 | if (this.multipart) { 474 | this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || generateBoundary(this._nodeId, this.rootNode.baseBoundary) 475 | } else { 476 | this.boundary = false 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /src/utils-unit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import { 4 | convertAddresses, 5 | generateBoundary, 6 | parseAddresses, 7 | normalizeHeaderKey, 8 | escapeHeaderArgument, 9 | encodeHeaderValue, 10 | buildHeaderValue, 11 | isPlainText 12 | } from './utils' 13 | 14 | describe('#convertAddresses', function () { 15 | it('should convert address object to a string', function () { 16 | expect(convertAddresses([{ 17 | name: 'Jõgeva Ants', 18 | address: 'ants@jõgeva.ee' 19 | }, { 20 | name: 'Composers', 21 | group: [{ 22 | address: 'sebu@example.com', 23 | name: 'Bach, Sebastian' 24 | }, { 25 | address: 'mozart@example.com', 26 | name: 'Mozzie' 27 | }] 28 | }])).to.equal('=?UTF-8?Q?J=C3=B5geva_Ants?= , Composers:"Bach, Sebastian" , Mozzie ;') 29 | }) 30 | 31 | it('should keep ascii name as is', function () { 32 | expect(convertAddresses([{ 33 | name: 'O\'Vigala Sass', 34 | address: 'a@b.c' 35 | }])).to.equal('O\'Vigala Sass ') 36 | }) 37 | 38 | it('should include name in quotes for special symbols', function () { 39 | expect(convertAddresses([{ 40 | name: 'Sass, Vigala', 41 | address: 'a@b.c' 42 | }])).to.equal('"Sass, Vigala" ') 43 | }) 44 | 45 | it('should escape quotes', function () { 46 | expect(convertAddresses([{ 47 | name: '"Vigala Sass"', 48 | address: 'a@b.c' 49 | }])).to.equal('"\\"Vigala Sass\\"" ') 50 | }) 51 | 52 | it('should mime encode unicode names', function () { 53 | expect(convertAddresses([{ 54 | name: '"Jõgeva Sass"', 55 | address: 'a@b.c' 56 | }])).to.equal('=?UTF-8?Q?=22J=C3=B5geva_Sass=22?= ') 57 | }) 58 | }) 59 | 60 | describe('isPlainText', function () { 61 | it('should return true', function () { 62 | expect(isPlainText('az09\t\r\n~!?')).to.be.true 63 | }) 64 | 65 | it('should return false on low bits', function () { 66 | expect(isPlainText('az09\n\x08!?')).to.be.false 67 | }) 68 | 69 | it('should return false on high bits', function () { 70 | expect(isPlainText('az09\nõ!?')).to.be.false 71 | }) 72 | }) 73 | 74 | describe('generateBoundary ', function () { 75 | it('should genereate boundary string', function () { 76 | const nodeId = 'abc' 77 | const rootBoundary = 'def' 78 | expect(generateBoundary(nodeId, rootBoundary)).to.equal('----sinikael-?=_abc-def') 79 | }) 80 | }) 81 | 82 | describe('parseAddresses', function () { 83 | it('should normalize header key', function () { 84 | expect(parseAddresses('test address@example.com')).to.deep.equal([{ 85 | address: 'address@example.com', 86 | name: 'test' 87 | }]) 88 | 89 | expect(parseAddresses(['test address@example.com'])).to.deep.equal([{ 90 | address: 'address@example.com', 91 | name: 'test' 92 | }]) 93 | 94 | expect(parseAddresses([ 95 | ['test address@example.com'] 96 | ])).to.deep.equal([{ 97 | address: 'address@example.com', 98 | name: 'test' 99 | }]) 100 | 101 | expect(parseAddresses([{ 102 | address: 'address@example.com', 103 | name: 'test' 104 | }])).to.deep.equal([{ 105 | address: 'address@example.com', 106 | name: 'test' 107 | }]) 108 | }) 109 | }) 110 | 111 | describe('normalizeHeaderKey', function () { 112 | it('should normalize header key', function () { 113 | expect(normalizeHeaderKey('key')).to.equal('Key') 114 | expect(normalizeHeaderKey('mime-vERSION')).to.equal('MIME-Version') 115 | expect(normalizeHeaderKey('-a-long-name')).to.equal('-A-Long-Name') 116 | }) 117 | }) 118 | 119 | describe('escapeHeaderArgument', function () { 120 | it('should return original value if possible', function () { 121 | expect(escapeHeaderArgument('abc')).to.equal('abc') 122 | }) 123 | 124 | it('should use quotes', function () { 125 | expect(escapeHeaderArgument('abc "tere"')).to.equal('"abc \\"tere\\""') 126 | }) 127 | }) 128 | 129 | describe('encodeHeaderValue', function () { 130 | it('should do noting if possible', function () { 131 | expect(encodeHeaderValue('x-my', 'test value')).to.equal('test value') 132 | }) 133 | 134 | it('should encode non ascii characters', function () { 135 | expect(encodeHeaderValue('x-my', 'test jõgeva value')).to.equal('test =?UTF-8?B?asO1Z2V2YQ==?= value') 136 | }) 137 | 138 | it('should format references', function () { 139 | expect(encodeHeaderValue('references', 'abc def')).to.equal(' ') 140 | expect(encodeHeaderValue('references', ['abc', 'def'])).to.equal(' ') 141 | }) 142 | 143 | it('should format message-id', function () { 144 | expect(encodeHeaderValue('message-id', 'abc')).to.equal('') 145 | }) 146 | 147 | it('should format addresses', function () { 148 | expect(encodeHeaderValue('from', { 149 | name: 'the safewithme testuser', 150 | address: 'safewithme.testuser@jõgeva.com' 151 | })).to.equal('the safewithme testuser ') 152 | }) 153 | }) 154 | 155 | describe('buildHeaderValue', function () { 156 | it('should build header value', function () { 157 | expect(buildHeaderValue({ 158 | value: 'test' 159 | })).to.equal('test') 160 | }) 161 | it('should build header value with params', function () { 162 | expect(buildHeaderValue({ 163 | value: 'test', 164 | params: { 165 | a: 'b' 166 | } 167 | })).to.equal('test; a=b') 168 | }) 169 | it('should build header value with empty params', function () { 170 | expect(buildHeaderValue({ 171 | value: 'test', 172 | params: { 173 | a: ';' 174 | } 175 | })).to.equal('test; a=";"') 176 | }) 177 | it('should build header value with quotes in params', function () { 178 | expect(buildHeaderValue({ 179 | value: 'test', 180 | params: { 181 | a: ';"' 182 | } 183 | })).to.equal('test; a=";\\""') 184 | }) 185 | it('should build header value with multiple params', function () { 186 | expect(buildHeaderValue({ 187 | value: 'test', 188 | params: { 189 | a: 'b', 190 | c: 'd' 191 | } 192 | })).to.equal('test; a=b; c=d') 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-deprecated-api */ 2 | /* eslint-disable no-control-regex */ 3 | 4 | import { flatten } from 'ramda' 5 | import parseAddress from 'emailjs-addressparser' 6 | import { 7 | mimeWordsEncode, 8 | mimeWordEncode, 9 | continuationEncode 10 | } from 'emailjs-mime-codec' 11 | import { toASCII } from 'punycode' 12 | 13 | /** 14 | * If needed, mime encodes the name part 15 | * 16 | * @param {String} name Name part of an address 17 | * @returns {String} Mime word encoded string if needed 18 | */ 19 | function encodeAddressName (name) { 20 | if (!/^[\w ']*$/.test(name)) { 21 | if (/^[\x20-\x7e]*$/.test(name)) { 22 | return '"' + name.replace(/([\\"])/g, '\\$1') + '"' 23 | } else { 24 | return mimeWordEncode(name, 'Q') 25 | } 26 | } 27 | return name 28 | } 29 | 30 | /** 31 | * Checks if a value is plaintext string (uses only printable 7bit chars) 32 | * 33 | * @param {String} value String to be tested 34 | * @returns {Boolean} true if it is a plaintext string 35 | */ 36 | export function isPlainText (value) { 37 | return !(typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)) 38 | } 39 | 40 | /** 41 | * Rebuilds address object using punycode and other adjustments 42 | * 43 | * @param {Array} addresses An array of address objects 44 | * @param {Array} [uniqueList] An array to be populated with addresses 45 | * @return {String} address string 46 | */ 47 | export function convertAddresses (addresses = [], uniqueList = []) { 48 | var values = [] 49 | 50 | ;[].concat(addresses).forEach(address => { 51 | if (address.address) { 52 | address.address = address.address 53 | .replace(/^.*?(?=@)/, user => mimeWordsEncode(user, 'Q')) 54 | .replace(/@.+$/, domain => '@' + toASCII(domain.substr(1))) 55 | 56 | if (!address.name) { 57 | values.push(address.address) 58 | } else if (address.name) { 59 | values.push(encodeAddressName(address.name) + ' <' + address.address + '>') 60 | } 61 | 62 | if (uniqueList.indexOf(address.address) < 0) { 63 | uniqueList.push(address.address) 64 | } 65 | } else if (address.group) { 66 | values.push(encodeAddressName(address.name) + ':' + (address.group.length ? convertAddresses(address.group, uniqueList) : '').trim() + ';') 67 | } 68 | }) 69 | 70 | return values.join(', ') 71 | } 72 | 73 | /** 74 | * Parses addresses. Takes in a single address or an array or an 75 | * array of address arrays (eg. To: [[first group], [second group],...]) 76 | * 77 | * @param {Mixed} addresses Addresses to be parsed 78 | * @return {Array} An array of address objects 79 | */ 80 | export function parseAddresses (addresses = []) { 81 | return flatten([].concat(addresses).map((address) => { 82 | if (address && address.address) { 83 | address = convertAddresses(address) 84 | } 85 | return parseAddress(address) 86 | })) 87 | } 88 | 89 | /** 90 | * Encodes a header value for use in the generated rfc2822 email. 91 | * 92 | * @param {String} key Header key 93 | * @param {String} value Header value 94 | */ 95 | export function encodeHeaderValue (key, value = '') { 96 | key = normalizeHeaderKey(key) 97 | 98 | switch (key) { 99 | case 'From': 100 | case 'Sender': 101 | case 'To': 102 | case 'Cc': 103 | case 'Bcc': 104 | case 'Reply-To': 105 | return convertAddresses(parseAddresses(value)) 106 | 107 | case 'Message-Id': 108 | case 'In-Reply-To': 109 | case 'Content-Id': 110 | value = value.replace(/\r?\n|\r/g, ' ') 111 | 112 | if (value.charAt(0) !== '<') { 113 | value = '<' + value 114 | } 115 | 116 | if (value.charAt(value.length - 1) !== '>') { 117 | value = value + '>' 118 | } 119 | return value 120 | 121 | case 'References': 122 | value = [].concat.apply([], [].concat(value).map((elm = '') => elm 123 | .replace(/\r?\n|\r/g, ' ') 124 | .trim() 125 | .replace(/<[^>]*>/g, str => str.replace(/\s/g, '')) 126 | .split(/\s+/) 127 | )).map(function (elm) { 128 | if (elm.charAt(0) !== '<') { 129 | elm = '<' + elm 130 | } 131 | if (elm.charAt(elm.length - 1) !== '>') { 132 | elm = elm + '>' 133 | } 134 | return elm 135 | }) 136 | 137 | return value.join(' ').trim() 138 | 139 | default: 140 | return mimeWordsEncode((value || '').toString().replace(/\r?\n|\r/g, ' '), 'B') 141 | } 142 | } 143 | 144 | /** 145 | * Normalizes a header key, uses Camel-Case form, except for uppercase MIME- 146 | * 147 | * @param {String} key Key to be normalized 148 | * @return {String} key in Camel-Case form 149 | */ 150 | export function normalizeHeaderKey (key = '') { 151 | return key.replace(/\r?\n|\r/g, ' ') // no newlines in keys 152 | .trim().toLowerCase() 153 | .replace(/^MIME\b|^[a-z]|-[a-z]/ig, c => c.toUpperCase()) // use uppercase words, except MIME 154 | } 155 | 156 | /** 157 | * Generates a multipart boundary value 158 | * 159 | * @return {String} boundary value 160 | */ 161 | export function generateBoundary (nodeId, baseBoundary) { 162 | return '----sinikael-?=_' + nodeId + '-' + baseBoundary 163 | } 164 | 165 | /** 166 | * Escapes a header argument value (eg. boundary value for content type), 167 | * adds surrounding quotes if needed 168 | * 169 | * @param {String} value Header argument value 170 | * @return {String} escaped and quoted (if needed) argument value 171 | */ 172 | export function escapeHeaderArgument (value) { 173 | if (value.match(/[\s'"\\;/=]|^-/g)) { 174 | return '"' + value.replace(/(["\\])/g, '\\$1') + '"' 175 | } else { 176 | return value 177 | } 178 | } 179 | 180 | /** 181 | * Joins parsed header value together as 'value; param1=value1; param2=value2' 182 | * 183 | * @param {Object} structured Parsed header value 184 | * @return {String} joined header value 185 | */ 186 | export function buildHeaderValue (structured) { 187 | var paramsArray = [] 188 | 189 | Object.keys(structured.params || {}).forEach(param => { 190 | // filename might include unicode characters so it is a special case 191 | if (param === 'filename') { 192 | continuationEncode(param, structured.params[param], 50).forEach(function (encodedParam) { 193 | // continuation encoded strings are always escaped, so no need to use enclosing quotes 194 | // in fact using quotes might end up with invalid filenames in some clients 195 | paramsArray.push(encodedParam.key + '=' + encodedParam.value) 196 | }) 197 | } else { 198 | paramsArray.push(param + '=' + escapeHeaderArgument(structured.params[param])) 199 | } 200 | }) 201 | 202 | return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '') 203 | } 204 | -------------------------------------------------------------------------------- /testutils.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | global.expect = expect 4 | --------------------------------------------------------------------------------