├── .github └── pull_request_template.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── plugin.js └── replaceBase64Images.js └── test └── replaceBase64ImagesSpec.js /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > **Note** - Since this is a public repository, make sure that we're not publishing private data in the code, commit comments, or this PR. 4 | 5 | > **Note for reviewers** - Please add a 2nd reviewer if the PR affects more than 15 files or 100 lines (not counting 6 | `package-lock.json`), if it incurs significant risk, or if it is going through a 2nd review+fix cycle. 7 | 8 | ## 📚 Context/Description Behind The Change 9 | 18 | 19 | ## 🚨 Potential Risks & What To Monitor After Deployment 20 | 28 | 29 | ## 🧑‍🔬 How Has This Been Tested? 30 | 36 | 37 | ## 🚚 Release Plan 38 | 44 | 45 | 46 | 47 | 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | * 2.1.1 Fixed issue where other types of quotes not supported (thanks @thorn0) 4 | 5 | * 2.1.0 Adds support for rewriting CSS images, eg `
` 6 | 7 | * 2.0.0 **Breaking change**: the constructor now needs to be called as a function. Adds `cidPrefix` option. 8 | 9 | * 1.0.1 Support image types other than `png` 10 | 11 | * 1.0.0 Initial release 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mixmax, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodemailer plugin for handling inline Base64 images as attachments 2 | 3 | This plugin will convert base64-encoded images in your [nodemailer](https://github.com/nodemailer/nodemailer) email to be inline ("CID-referenced") attachments within the email. Inline attachments are useful because them embed the image inside the actual email, so it's viewable even if the user is checking their email without an internet connection. But if you're OK with requiring that the user be online to see the image, then consider hosting your images from AWS Cloudfront using [nodemailer-base64-to-s3](https://github.com/crocodilejs/nodemailer-base64-to-s3). 4 | 5 | Base64 images are generally a [bad idea](https://sendgrid.com/blog/embedding-images-emails-facts/) because they aren't supported in most email clients. This Nodemailer plugin will take base64 images in your email html in the form: 6 | 7 | 8 | 9 | and replace it with a CID-referenced attachment that works in all email clients. 10 | 11 | ## Install 12 | 13 | ``` 14 | npm install nodemailer-plugin-inline-base64 15 | ``` 16 | or 17 | ``` 18 | npm install nodemailer-plugin-inline-base64 --save 19 | ``` 20 | 21 | ## Usage 22 | 23 | #### 1. Load the `nodemailer-plugin-inline-base64` plugin: 24 | 25 | ```javascript 26 | var inlineBase64 = require('nodemailer-plugin-inline-base64'); 27 | ``` 28 | 29 | #### 2. Attach it as a 'compile' handler for a nodemailer transport object 30 | 31 | ```javascript 32 | nodemailerTransport.use('compile', inlineBase64(options)) 33 | ``` 34 | Options allow to set CID prefix1 ```{cidPrefix: 'somePrefix_'}```, 35 | then all inline images will have prefix in cid, i.e.: `cid:somePrefix_5fe3b631c651bdb1`. If you don't need this, 36 | you can use inlineBase64 plugin without options. 37 | 38 | 39 | 40 | ## Example 41 | 42 | ```javascript 43 | var nodemailer = require('nodemailer'); 44 | var inlineBase64 = require('nodemailer-plugin-inline-base64'); 45 | transporter.use('compile', inlineBase64({cidPrefix: 'somePrefix_'})); 46 | transporter.sendMail({ 47 | from: 'me@example.com', 48 | to: 'hello@mixmax.com', 49 | html: '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAACCAYAAACE7KJkAAAAI0lEQVRYR+3DMQ0AAAgDsKlFzZxgEhOcbdIEAIBf7Y6qqn8P0MMQZPno7TMAAAAASUVORK5CYII=">' 50 | }); 51 | ``` 52 | 53 | ## References 54 | 1 It might be useful for reply email processing, example with [MailParser](https://github.com/andris9/mailparser) 55 | 56 | ```javascript 57 | mp.on("attachment", function(attachment, mail){ 58 | if (!attachment.contentId.includes('somePrefix')) { // process only images attached by user in reply 59 | // ... 60 | } 61 | }); 62 | ``` 63 | 64 | ## License 65 | 66 | **MIT** 67 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemailer-plugin-inline-base64", 3 | "version": "2.1.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "2.1.1", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "jasmine-node": "^1.14.5" 12 | } 13 | }, 14 | "node_modules/coffee-script": { 15 | "version": "1.12.7", 16 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 17 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 18 | "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", 19 | "dev": true, 20 | "bin": { 21 | "cake": "bin/cake", 22 | "coffee": "bin/coffee" 23 | }, 24 | "engines": { 25 | "node": ">=0.8.0" 26 | } 27 | }, 28 | "node_modules/fileset": { 29 | "version": "0.1.8", 30 | "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", 31 | "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", 32 | "dev": true, 33 | "dependencies": { 34 | "glob": "3.x", 35 | "minimatch": "0.x" 36 | } 37 | }, 38 | "node_modules/gaze": { 39 | "version": "0.3.4", 40 | "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.3.4.tgz", 41 | "integrity": "sha1-X5S92gr+U7xxCWm81vKCVI1gwnk=", 42 | "dev": true, 43 | "dependencies": { 44 | "fileset": "~0.1.5", 45 | "minimatch": "~0.2.9" 46 | }, 47 | "engines": { 48 | "node": ">= 0.6.0" 49 | } 50 | }, 51 | "node_modules/glob": { 52 | "version": "3.2.11", 53 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 54 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 55 | "dev": true, 56 | "dependencies": { 57 | "inherits": "2", 58 | "minimatch": "0.3" 59 | }, 60 | "engines": { 61 | "node": "*" 62 | } 63 | }, 64 | "node_modules/glob/node_modules/minimatch": { 65 | "version": "0.3.0", 66 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 67 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 68 | "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", 69 | "dev": true, 70 | "dependencies": { 71 | "lru-cache": "2", 72 | "sigmund": "~1.0.0" 73 | }, 74 | "engines": { 75 | "node": "*" 76 | } 77 | }, 78 | "node_modules/growl": { 79 | "version": "1.7.0", 80 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", 81 | "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", 82 | "dev": true 83 | }, 84 | "node_modules/inherits": { 85 | "version": "2.0.3", 86 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 87 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 88 | "dev": true 89 | }, 90 | "node_modules/jasmine-growl-reporter": { 91 | "version": "0.0.3", 92 | "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.0.3.tgz", 93 | "integrity": "sha1-uHrlUeNZ0orVIXdl6u9sB7dj9sg=", 94 | "dev": true, 95 | "dependencies": { 96 | "growl": "~1.7.0" 97 | } 98 | }, 99 | "node_modules/jasmine-node": { 100 | "version": "1.14.5", 101 | "resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-1.14.5.tgz", 102 | "integrity": "sha1-GOg5e4VpJO53ADZmw3MbWupQw50=", 103 | "deprecated": "jasmine-node 1.x & 2.x are deprecated, with known vulnerability in jasmine-growl-reporter pre-2.0.0", 104 | "dev": true, 105 | "dependencies": { 106 | "coffee-script": ">=1.0.1", 107 | "gaze": "~0.3.2", 108 | "jasmine-growl-reporter": "~0.0.2", 109 | "jasmine-reporters": "~1.0.0", 110 | "mkdirp": "~0.3.5", 111 | "requirejs": ">=0.27.1", 112 | "underscore": ">= 1.3.1", 113 | "walkdir": ">= 0.0.1" 114 | }, 115 | "bin": { 116 | "jasmine-node": "bin/jasmine-node" 117 | } 118 | }, 119 | "node_modules/jasmine-reporters": { 120 | "version": "1.0.2", 121 | "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-1.0.2.tgz", 122 | "integrity": "sha1-q2E+1Zd9x0h+hbPBL2qOqNsq3jE=", 123 | "dev": true, 124 | "dependencies": { 125 | "mkdirp": "~0.3.5" 126 | } 127 | }, 128 | "node_modules/lru-cache": { 129 | "version": "2.7.3", 130 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 131 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 132 | "dev": true 133 | }, 134 | "node_modules/minimatch": { 135 | "version": "0.2.14", 136 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 137 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 138 | "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", 139 | "dev": true, 140 | "dependencies": { 141 | "lru-cache": "2", 142 | "sigmund": "~1.0.0" 143 | }, 144 | "engines": { 145 | "node": "*" 146 | } 147 | }, 148 | "node_modules/mkdirp": { 149 | "version": "0.3.5", 150 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 151 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 152 | "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", 153 | "dev": true 154 | }, 155 | "node_modules/requirejs": { 156 | "version": "2.3.5", 157 | "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.5.tgz", 158 | "integrity": "sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw==", 159 | "dev": true, 160 | "bin": { 161 | "r_js": "bin/r.js", 162 | "r.js": "bin/r.js" 163 | }, 164 | "engines": { 165 | "node": ">=0.4.0" 166 | } 167 | }, 168 | "node_modules/sigmund": { 169 | "version": "1.0.1", 170 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 171 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 172 | "dev": true 173 | }, 174 | "node_modules/underscore": { 175 | "version": "1.9.0", 176 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.0.tgz", 177 | "integrity": "sha512-4IV1DSSxC1QK48j9ONFK1MoIAKKkbE8i7u55w2R6IqBqbT7A/iG7aZBCR2Bi8piF0Uz+i/MG1aeqLwl/5vqF+A==", 178 | "dev": true 179 | }, 180 | "node_modules/walkdir": { 181 | "version": "0.0.12", 182 | "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", 183 | "integrity": "sha512-HFhaD4mMWPzFSqhpyDG48KDdrjfn409YQuVW7ckZYhW4sE87mYtWifdB/+73RA7+p4s4K18n5Jfx1kHthE1gBw==", 184 | "dev": true, 185 | "engines": { 186 | "node": ">=0.6.0" 187 | } 188 | } 189 | }, 190 | "dependencies": { 191 | "coffee-script": { 192 | "version": "1.12.7", 193 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 194 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 195 | "dev": true 196 | }, 197 | "fileset": { 198 | "version": "0.1.8", 199 | "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", 200 | "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", 201 | "dev": true, 202 | "requires": { 203 | "glob": "3.x", 204 | "minimatch": "0.x" 205 | } 206 | }, 207 | "gaze": { 208 | "version": "0.3.4", 209 | "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.3.4.tgz", 210 | "integrity": "sha1-X5S92gr+U7xxCWm81vKCVI1gwnk=", 211 | "dev": true, 212 | "requires": { 213 | "fileset": "~0.1.5", 214 | "minimatch": "~0.2.9" 215 | } 216 | }, 217 | "glob": { 218 | "version": "3.2.11", 219 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 220 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 221 | "dev": true, 222 | "requires": { 223 | "inherits": "2", 224 | "minimatch": "0.3" 225 | }, 226 | "dependencies": { 227 | "minimatch": { 228 | "version": "0.3.0", 229 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 230 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 231 | "dev": true, 232 | "requires": { 233 | "lru-cache": "2", 234 | "sigmund": "~1.0.0" 235 | } 236 | } 237 | } 238 | }, 239 | "growl": { 240 | "version": "1.7.0", 241 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", 242 | "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", 243 | "dev": true 244 | }, 245 | "inherits": { 246 | "version": "2.0.3", 247 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 248 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 249 | "dev": true 250 | }, 251 | "jasmine-growl-reporter": { 252 | "version": "0.0.3", 253 | "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.0.3.tgz", 254 | "integrity": "sha1-uHrlUeNZ0orVIXdl6u9sB7dj9sg=", 255 | "dev": true, 256 | "requires": { 257 | "growl": "~1.7.0" 258 | } 259 | }, 260 | "jasmine-node": { 261 | "version": "1.14.5", 262 | "resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-1.14.5.tgz", 263 | "integrity": "sha1-GOg5e4VpJO53ADZmw3MbWupQw50=", 264 | "dev": true, 265 | "requires": { 266 | "coffee-script": ">=1.0.1", 267 | "gaze": "~0.3.2", 268 | "jasmine-growl-reporter": "~0.0.2", 269 | "jasmine-reporters": "~1.0.0", 270 | "mkdirp": "~0.3.5", 271 | "requirejs": ">=0.27.1", 272 | "underscore": ">= 1.3.1", 273 | "walkdir": ">= 0.0.1" 274 | } 275 | }, 276 | "jasmine-reporters": { 277 | "version": "1.0.2", 278 | "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-1.0.2.tgz", 279 | "integrity": "sha1-q2E+1Zd9x0h+hbPBL2qOqNsq3jE=", 280 | "dev": true, 281 | "requires": { 282 | "mkdirp": "~0.3.5" 283 | } 284 | }, 285 | "lru-cache": { 286 | "version": "2.7.3", 287 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 288 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 289 | "dev": true 290 | }, 291 | "minimatch": { 292 | "version": "0.2.14", 293 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 294 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 295 | "dev": true, 296 | "requires": { 297 | "lru-cache": "2", 298 | "sigmund": "~1.0.0" 299 | } 300 | }, 301 | "mkdirp": { 302 | "version": "0.3.5", 303 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 304 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 305 | "dev": true 306 | }, 307 | "requirejs": { 308 | "version": "2.3.5", 309 | "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.5.tgz", 310 | "integrity": "sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw==", 311 | "dev": true 312 | }, 313 | "sigmund": { 314 | "version": "1.0.1", 315 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 316 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 317 | "dev": true 318 | }, 319 | "underscore": { 320 | "version": "1.9.0", 321 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.0.tgz", 322 | "integrity": "sha512-4IV1DSSxC1QK48j9ONFK1MoIAKKkbE8i7u55w2R6IqBqbT7A/iG7aZBCR2Bi8piF0Uz+i/MG1aeqLwl/5vqF+A==", 323 | "dev": true 324 | }, 325 | "walkdir": { 326 | "version": "0.0.12", 327 | "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", 328 | "integrity": "sha512-HFhaD4mMWPzFSqhpyDG48KDdrjfn409YQuVW7ckZYhW4sE87mYtWifdB/+73RA7+p4s4K18n5Jfx1kHthE1gBw==", 329 | "dev": true 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemailer-plugin-inline-base64", 3 | "version": "2.1.1", 4 | "description": "Nodemailer plugin that will inline base64 images", 5 | "main": "src/plugin.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "jasmine-node": "^1.14.5" 12 | }, 13 | "scripts": { 14 | "test": "jasmine-node test/" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/mixmaxhq/nodemailer-plugin-inline-base64.git" 19 | }, 20 | "keywords": [ 21 | "nodemailer", 22 | "plugin", 23 | "base64", 24 | "attachment" 25 | ], 26 | "author": "Brad Vogel (https://mixmax.com/)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/mixmaxhq/nodemailer-plugin-inline-base64/issues" 30 | }, 31 | "homepage": "https://github.com/mixmaxhq/nodemailer-plugin-inline-base64" 32 | } 33 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>mixmaxhq/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | var replaceBase64Images = require('./replaceBase64Images'); 2 | var crypto = require('crypto'); 3 | 4 | var plugin = function(options) { 5 | options = options || {}; 6 | return function(mail, done) { 7 | if (!mail || !mail.data || !mail.data.html) { 8 | return done(); 9 | } 10 | 11 | mail.resolveContent(mail.data, 'html', function(err, html) { 12 | if (err) return done(err); 13 | 14 | var attachments = {}; 15 | 16 | html = replaceBase64Images(html, function(mimeType, base64) { 17 | if (attachments[base64]) return attachments[base64].cid; 18 | 19 | var randomCid = (options.cidPrefix || '') + crypto.randomBytes(8).toString('hex'); 20 | 21 | attachments[base64] = { 22 | contentType: mimeType, 23 | cid: randomCid, 24 | content: base64, 25 | encoding: 'base64', 26 | contentDisposition: 'inline' 27 | }; 28 | return randomCid; 29 | }); 30 | 31 | mail.data.html = html; 32 | 33 | if (!mail.data.attachments) mail.data.attachments = []; 34 | 35 | Object.keys(attachments).forEach(function(cid) { 36 | mail.data.attachments.push(attachments[cid]); 37 | }); 38 | 39 | done(); 40 | }); 41 | } 42 | }; 43 | 44 | module.exports = plugin; 45 | -------------------------------------------------------------------------------- /src/replaceBase64Images.js: -------------------------------------------------------------------------------- 1 | var replaceBase64Images = function(html, getCid) { 2 | //Replace html base64 images 3 | var result = html.replace(/()/g, function(g, start, mimeType, base64, end) { 4 | return start + 'cid:' + getCid(mimeType, base64) + end; 5 | }); 6 | 7 | //Replace css base64 images 8 | return result.replace( 9 | /(url\(\s*('|"|"|"|'|[49];|&#[xX]2[27];)?)data:(image\/(?:png|jpe?g|gif));base64,([\s\S]*?)(\2\s*\))/g, 10 | function(g, start, quot, mimeType, base64, end) { 11 | return start + 'cid:' + getCid(mimeType, base64) + end; 12 | } 13 | ); 14 | }; 15 | 16 | module.exports = replaceBase64Images; 17 | -------------------------------------------------------------------------------- /test/replaceBase64ImagesSpec.js: -------------------------------------------------------------------------------- 1 | var replaceBase64Images = require('../src/replaceBase64Images'); 2 | 3 | describe('replaceBase64Images', function() { 4 | var replacer = function(mimeType, contents) { 5 | // Make up a fake string to very parameters were passed correctly. 6 | return mimeType + '-' + contents.split('').reverse().join(''); 7 | }; 8 | 9 | describe('HTML tags', function() { 10 | 11 | it('should replace a single source (double quote)', function() { 12 | var html = ''; 13 | var expected = ''; 14 | expect(replaceBase64Images(html, replacer)).toBe(expected); 15 | }); 16 | 17 | it('should replace a single source (single quote)', function() { 18 | var html = ''; 19 | var expected = ''; 20 | expect(replaceBase64Images(html, replacer)).toBe(expected); 21 | }); 22 | 23 | it('should handle gifs', function() { 24 | var html = ''; 25 | var expected = ''; 26 | expect(replaceBase64Images(html, replacer)).toBe(expected); 27 | }); 28 | 29 | it('should handle jpegs', function() { 30 | var html = ''; 31 | var expected = ''; 32 | expect(replaceBase64Images(html, replacer)).toBe(expected); 33 | }); 34 | 35 | it('should handle attributes on the HTML tag', function() { 36 | var html = ''; 37 | var expected = ''; 38 | expect(replaceBase64Images(html, replacer)).toBe(expected); 39 | }); 40 | 41 | it('should replace multiple sources', function() { 42 | var html = '
'; 43 | var expected = '
'; 44 | expect(replaceBase64Images(html, replacer)).toBe(expected); 45 | }); 46 | 47 | it('should ignore other mimetypes', function() { 48 | var html = ''; 49 | var expected = ''; 50 | expect(replaceBase64Images(html, replacer)).toBe(expected); 51 | }); 52 | 53 | it('should not touch regular img tags', function() { 54 | var html = ''; 55 | expect(replaceBase64Images(html, replacer)).toBe(html); 56 | }); 57 | 58 | it('should not replace base64 outside of an image tag', function() { 59 | var html = 'data:image/png;base64,abc'; 60 | expect(replaceBase64Images(html, replacer)).toBe(html); 61 | }); 62 | 63 | }); 64 | 65 | describe('Inline CSS `url`', function() { 66 | 67 | it('should replace a single source (no quote)', function() { 68 | var html = '
'; 69 | var expected = '
'; 70 | expect(replaceBase64Images(html, replacer)).toBe(expected); 71 | }); 72 | 73 | it('should replace a single source (double quote)', function() { 74 | var html = '
'; 75 | var expected = '
'; 76 | expect(replaceBase64Images(html, replacer)).toBe(expected); 77 | }); 78 | 79 | it('should replace a single source (single quote)', function() { 80 | var html = '
'; 81 | var expected = '
'; 82 | expect(replaceBase64Images(html, replacer)).toBe(expected); 83 | }); 84 | 85 | '" " " " "'.split(' ').forEach(function(q) { 86 | it('should replace a single source (double quote escaped as ' + q + ')', function() { 87 | var html = '
'; 88 | var expected = '
'; 89 | expect(replaceBase64Images(html, replacer)).toBe(expected); 90 | }); 91 | }); 92 | 93 | '' ' ' ''.split(' ').forEach(function(q) { 94 | it('should replace a single source (single quote escaped as ' + q + ')', function() { 95 | var html = '
'; 96 | var expected = '
'; 97 | expect(replaceBase64Images(html, replacer)).toBe(expected); 98 | }); 99 | }); 100 | 101 | it('should handle gifs', function() { 102 | var html = '
'; 103 | var expected = '
'; 104 | expect(replaceBase64Images(html, replacer)).toBe(expected); 105 | }); 106 | 107 | it('should handle jpegs', function() { 108 | var html = '
'; 109 | var expected = '
'; 110 | expect(replaceBase64Images(html, replacer)).toBe(expected); 111 | }); 112 | 113 | it('should replace multiple sources', function() { 114 | var html = '

'; 115 | var expected = '

'; 116 | expect(replaceBase64Images(html, replacer)).toBe(expected); 117 | }); 118 | 119 | it('should ignore other mimetypes', function() { 120 | var html = '
'; 121 | var expected = '
'; 122 | expect(replaceBase64Images(html, replacer)).toBe(expected); 123 | }); 124 | 125 | it('should not replace base64 outside of a css `url()`', function() { 126 | var html = 'data:image/png;base64,abc'; 127 | expect(replaceBase64Images(html, replacer)).toBe(html); 128 | }); 129 | 130 | it('should allow spaces like `url( "data:..." )`', function() { 131 | var html = '
'; 132 | var expected = '
'; 133 | expect(replaceBase64Images(html, replacer)).toBe(expected); 134 | }); 135 | 136 | }); 137 | 138 | }); 139 | --------------------------------------------------------------------------------