├── .gitignore ├── LICENSE ├── README.md ├── THEME.md ├── examples ├── receipt.js ├── reset.js └── welcome.js ├── index.d.ts ├── index.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── screenshots ├── cerberus │ ├── receipt.png │ ├── reset.png │ └── welcome.png ├── default │ ├── receipt.png │ ├── reset.png │ └── welcome.png ├── neopolitan │ ├── receipt.png │ ├── reset.png │ └── welcome.png └── salted │ ├── receipt.png │ ├── reset.png │ └── welcome.png └── themes ├── cerberus ├── index.html └── index.txt ├── default ├── index.html └── index.txt ├── neopolitan ├── index.html └── index.txt └── salted ├── index.html └── index.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules 3 | 4 | # Test files 5 | tests/ 6 | test.js 7 | preview.html 8 | preview.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mailgen 2 | [![npm version](https://badge.fury.io/js/mailgen.svg)](https://www.npmjs.com/package/mailgen) 3 | 4 | A Node.js package that generates clean, responsive HTML e-mails for sending transactional mail. 5 | 6 | > Programmatically create beautiful e-mails using plain old JavaScript. 7 | 8 | ## Demo 9 | 10 | 11 | 12 | > These e-mails were generated using the built-in `salted` theme. 13 | 14 | ## Usage 15 | 16 | First, install the package using npm: 17 | 18 | ```shell 19 | npm install mailgen --save 20 | ``` 21 | 22 | Then, start using the package by importing and configuring it: 23 | 24 | ```js 25 | var Mailgen = require('mailgen'); 26 | 27 | // Configure mailgen by setting a theme and your product info 28 | var mailGenerator = new Mailgen({ 29 | theme: 'default', 30 | product: { 31 | // Appears in header & footer of e-mails 32 | name: 'Mailgen', 33 | link: 'https://mailgen.js/' 34 | // Optional product logo 35 | // logo: 'https://mailgen.js/img/logo.png' 36 | } 37 | }); 38 | ``` 39 | 40 | Next, generate an e-mail using the following code: 41 | 42 | ```js 43 | var email = { 44 | body: { 45 | name: 'John Appleseed', 46 | intro: 'Welcome to Mailgen! We\'re very excited to have you on board.', 47 | action: { 48 | instructions: 'To get started with Mailgen, please click here:', 49 | button: { 50 | color: '#22BC66', // Optional action button color 51 | text: 'Confirm your account', 52 | link: 'https://mailgen.js/confirm?s=d9729feb74992cc3482b350163a1a010' 53 | } 54 | }, 55 | outro: 'Need help, or have questions? Just reply to this email, we\'d love to help.' 56 | } 57 | }; 58 | 59 | // Generate an HTML email with the provided contents 60 | var emailBody = mailGenerator.generate(email); 61 | 62 | // Generate the plaintext version of the e-mail (for clients that do not support HTML) 63 | var emailText = mailGenerator.generatePlaintext(email); 64 | 65 | // Optionally, preview the generated HTML e-mail by writing it to a local file 66 | require('fs').writeFileSync('preview.html', emailBody, 'utf8'); 67 | 68 | // `emailBody` now contains the HTML body, 69 | // and `emailText` contains the textual version. 70 | // 71 | // It's up to you to send the e-mail. 72 | // Check out nodemailer to accomplish this: 73 | // https://nodemailer.com/ 74 | ``` 75 | 76 | This code would output the following HTML template: 77 | 78 | 79 | 80 | ## More Examples 81 | 82 | * [Receipt](examples/receipt.js) 83 | * [Password Reset](examples/reset.js) 84 | 85 | ## Plaintext E-mails 86 | 87 | To generate a [plaintext version of the e-mail](https://litmus.com/blog/best-practices-for-plain-text-emails-a-look-at-why-theyre-important), simply call `generatePlaintext()`: 88 | 89 | ```js 90 | // Generate plaintext email using mailgen 91 | var emailText = mailGenerator.generatePlaintext(email); 92 | ``` 93 | 94 | ## Supported Themes 95 | 96 | The following open-source themes are bundled with this package: 97 | 98 | * `default` by [Postmark Transactional Email Templates](https://github.com/wildbit/postmark-templates) 99 | 100 | 101 | 102 | * `neopolitan` by [Send With Us](https://github.com/sendwithus/templates/tree/master/templates/neopolitan) 103 | 104 | 105 | 106 | * `salted` by [Jason Rodriguez](https://github.com/rodriguezcommaj/salted) 107 | 108 | 109 | 110 | * `cerberus` by [Ted Goas](http://tedgoas.github.io/Cerberus/) 111 | 112 | 113 | 114 | We thank the contributing authors for creating these themes. 115 | 116 | ## Custom Themes 117 | 118 | If you want to supply your own custom theme or add a new built-in theme, check out [THEME.md](THEME.md) for instructions. 119 | 120 | ## RTL Support 121 | 122 | To change the default text direction (left-to-right), simply override it as follows: 123 | 124 | ```js 125 | var mailGenerator = new Mailgen({ 126 | theme: 'salted', 127 | // Custom text direction 128 | textDirection: 'rtl', 129 | }); 130 | ``` 131 | 132 | ## Custom Logo Height 133 | 134 | To change the default product logo height, set it as follows: 135 | 136 | ```js 137 | var mailGenerator = new Mailgen({ 138 | product: { 139 | // Custom product logo URL 140 | logo: 'https://mailgen.js/img/logo.png', 141 | // Custom logo height 142 | logoHeight: '30px' 143 | } 144 | }); 145 | ``` 146 | 147 | ## Language Customizations 148 | 149 | To customize the e-mail greeting (Hi) or signature (Yours truly), supply custom strings within the e-mail `body`: 150 | 151 | ```js 152 | var email = { 153 | body: { 154 | greeting: 'Dear', 155 | signature: 'Sincerely' 156 | } 157 | }; 158 | ``` 159 | 160 | To not include the signature or greeting at all, set the signature or greeting fields to `false`: 161 | 162 | ```js 163 | var email = { 164 | body: { 165 | signature: false, 166 | greeting: false // This will override and disable name & title options 167 | } 168 | }; 169 | ``` 170 | 171 | To use a custom title string rather than a greeting/name introduction, provide it instead of `name`: 172 | 173 | ```js 174 | var email = { 175 | body: { 176 | // Title will override `name` 177 | title: 'Welcome to Mailgen!' 178 | } 179 | }; 180 | ``` 181 | 182 | To customize the `copyright`, override it when initializing `Mailgen` within your `product` as follows: 183 | 184 | ```js 185 | // Configure mailgen 186 | var mailGenerator = new Mailgen({ 187 | theme: 'salted', 188 | product: { 189 | name: 'Mailgen', 190 | link: 'https://mailgen.js/', 191 | // Custom copyright notice 192 | copyright: `Copyright © ${new Date().getFullYear()} Mailgen. All rights reserved.`, 193 | } 194 | }); 195 | ``` 196 | 197 | ## Multiline Support 198 | 199 | To inject multiple lines of text for the `intro` or `outro`, simply supply an array of strings: 200 | 201 | ```js 202 | var email = { 203 | body: { 204 | intro: ['Welcome to Mailgen!', 'We\'re very excited to have you on board.'], 205 | outro: ['Need help, or have questions?', 'Just reply to this email, we\'d love to help.'], 206 | } 207 | }; 208 | ``` 209 | 210 | ## Elements 211 | 212 | Mailgen supports injecting custom elements such as dictionaries, tables and action buttons into e-mails. 213 | 214 | ### Action 215 | 216 | To inject an action button in to the e-mail, supply the `action` object as follows: 217 | 218 | ```js 219 | var email = { 220 | body: { 221 | action: { 222 | instructions: 'To get started with Mailgen, please click here:', 223 | button: { 224 | color: '#48cfad', // Optional action button color 225 | text: 'Confirm your account', 226 | link: 'https://mailgen.js/confirm?s=d9729feb74992cc3482b350163a1a010' 227 | } 228 | } 229 | } 230 | }; 231 | ``` 232 | 233 | To inject multiple action buttons in to the e-mail, supply the `action` object as follows: 234 | 235 | ```js 236 | var email = { 237 | body: { 238 | action: [ 239 | { 240 | instructions: 'To get started with Mailgen, please click here:', 241 | button: { 242 | color: '#22BC66', 243 | text: 'Confirm your account', 244 | link: 'https://mailgen.js/confirm?s=d9729feb74992cc3482b350163a1a010' 245 | } 246 | }, 247 | { 248 | instructions: 'To read our frequently asked questions, please click here:', 249 | button: { 250 | text: 'Read our FAQ', 251 | link: 'https://mailgen.js/faq' 252 | } 253 | } 254 | ] 255 | } 256 | }; 257 | ``` 258 | 259 | You can enable a fallback link and instructions for action buttons in case e-mail clients don't render them properly. This can be achieved by setting `button.fallback` to `true`, or by specifying custom fallback text as follows: 260 | ```js 261 | var email = { 262 | body: { 263 | action: [ 264 | { 265 | instructions: 'To get started with Mailgen, please click here:', 266 | button: { 267 | color: '#22BC66', 268 | text: 'Confirm your account', 269 | link: 'https://mailgen.js/confirm?s=d9729feb74992cc3482b350163a1a010', 270 | fallback: true 271 | } 272 | }, 273 | { 274 | instructions: 'To read our frequently asked questions, please click here:', 275 | button: { 276 | text: 'Read our FAQ', 277 | link: 'https://mailgen.js/faq', 278 | fallback: { 279 | text: 'This is my custom text for fallback' 280 | } 281 | } 282 | } 283 | ] 284 | } 285 | }; 286 | ``` 287 | 288 | ### Table 289 | 290 | To inject a table into the e-mail, supply the `table` object as follows: 291 | 292 | ```js 293 | var email = { 294 | body: { 295 | table: { 296 | data: [ 297 | { 298 | item: 'Node.js', 299 | description: 'Event-driven I/O server-side JavaScript environment based on V8.', 300 | price: '$10.99' 301 | }, 302 | { 303 | item: 'Mailgen', 304 | description: 'Programmatically create beautiful e-mails using plain old JavaScript.', 305 | price: '$1.99' 306 | } 307 | ], 308 | columns: { 309 | // Optionally, customize the column widths 310 | customWidth: { 311 | item: '20%', 312 | price: '15%' 313 | }, 314 | // Optionally, change column text alignment 315 | customAlignment: { 316 | price: 'right' 317 | } 318 | } 319 | } 320 | } 321 | }; 322 | ``` 323 | 324 | To inject multiple tables into the e-mail, supply the `table` property with an array of objects as follows: 325 | 326 | ```js 327 | var email = { 328 | body: { 329 | table: [ 330 | { 331 | // Optionally, add a title to each table. 332 | title: 'Order 1', 333 | data: [ 334 | { 335 | item: 'Item 1', 336 | description: 'Item 1 description', 337 | price: '$1.99' 338 | }, 339 | { 340 | item: 'Item 2', 341 | description: 'Item 2 description', 342 | price: '$2.99' 343 | } 344 | ], 345 | columns: { 346 | // Optionally, customize the column widths 347 | customWidth: { 348 | item: '20%', 349 | price: '15%' 350 | }, 351 | // Optionally, change column text alignment 352 | customAlignment: { 353 | price: 'right' 354 | } 355 | } 356 | }, 357 | { 358 | // Optionally, add a title to each table. 359 | title: 'Order 2', 360 | data: [ 361 | { 362 | item: 'Item 1', 363 | description: 'Item 1 description', 364 | price: '$2.99' 365 | }, 366 | { 367 | item: 'Item 2', 368 | description: 'Item 2 description', 369 | price: '$1.99' 370 | } 371 | ], 372 | columns: { 373 | // Optionally, customize the column widths 374 | customWidth: { 375 | item: '20%', 376 | price: '15%' 377 | }, 378 | // Optionally, change column text alignment 379 | customAlignment: { 380 | price: 'right' 381 | } 382 | } 383 | } 384 | ] 385 | } 386 | }; 387 | ``` 388 | 389 | > Note: Tables are currently not supported in plaintext versions of e-mails. 390 | 391 | ### Dictionary 392 | 393 | To inject key-value pairs of data into the e-mail, supply the `dictionary` object as follows: 394 | 395 | ```js 396 | var email = { 397 | body: { 398 | dictionary: { 399 | date: 'June 11th, 2016', 400 | address: '123 Park Avenue, Miami, Florida' 401 | } 402 | } 403 | }; 404 | ``` 405 | 406 | ## Go-To Actions 407 | 408 | You can make use of Gmail's [Go-To Actions](https://developers.google.com/gmail/markup/reference/go-to-action) within your e-mails by suppling the `goToAction` object as follows: 409 | 410 | ```js 411 | var email = { 412 | body: { 413 | // Optionally configure a Go-To Action button 414 | goToAction: { 415 | text: 'Go to Dashboard', 416 | link: 'https://mailgen.com/confirm?s=d9729feb74992cc3482b350163a1a010', 417 | description: 'Check the status of your order in your dashboard' 418 | } 419 | } 420 | }; 421 | ``` 422 | 423 | > Note that you need to [get your sender address whitelisted](https://developers.google.com/gmail/markup/registering-with-google) before your Go-To Actions will show up in Gmail. 424 | 425 | ## Troubleshooting 426 | 427 | 1. After sending multiple e-mails to the same Gmail / Inbox address, they become grouped and truncated since they contain similar text, breaking the responsive e-mail layout. 428 | 429 | > Simply sending the `X-Entity-Ref-ID` header with your e-mails will prevent grouping / truncation. 430 | 431 | ## Contributing 432 | 433 | Thanks so much for wanting to help! We really appreciate it. 434 | 435 | * Have an idea for a new feature? 436 | * Want to add a new built-in theme? 437 | 438 | Excellent! You've come to the right place. 439 | 440 | 1. If you find a bug or wish to suggest a new feature, please create an issue first 441 | 2. Make sure your code & comment conventions are in-line with the project's style 442 | 3. Make your commits and PRs as tiny as possible - one feature or bugfix at a time 443 | 4. Write detailed commit messages, in-line with the project's commit naming conventions 444 | 445 | > Check out [THEME.md](THEME.md) if you want to add a new built-in theme to Mailgen. 446 | 447 | ## License 448 | 449 | Apache 2.0 450 | -------------------------------------------------------------------------------- /THEME.md: -------------------------------------------------------------------------------- 1 | # Theming Instructions 2 | 3 | This file contains instructions on adding themes to Mailgen: 4 | 5 | * [Using a Custom Theme](#using-a-custom-theme) 6 | * [Creating a Built-In Theme](#creating-a-built-in-theme) 7 | 8 | > We use [ejs](http://ejs.co/) under the hood to inject the e-mail body into themes. 9 | 10 | ## Using a Custom Theme 11 | 12 | If you want to supply your own **custom theme** for Mailgen to use (but don't want it included with Mailgen): 13 | 14 | 1. Create an `.html` file within your project directory 15 | 2. Paste your e-mail template into the file 16 | 3. Scroll down to the [injection snippets](#injection-snippets) and copy and paste each code snippet into the relevant area of your template markup 17 | 4. Optionally create a `.txt` file for the plaintext version of your theme (base off of [`themes/default/index.txt`](themes/default/index.txt)) 18 | 19 | When you've got your custom theme file(s) ready, apply them as follows: 20 | 21 | ```js 22 | var path = require('path'); 23 | var Mailgen = require('mailgen'); 24 | 25 | // Configure mailgen by providing the path to your custom theme 26 | var mailGenerator = new Mailgen({ 27 | theme: { 28 | // Build an absolute path to the theme file within your project 29 | path: path.resolve('assets/mailgen/theme.html'), 30 | // Also (optionally) provide the path to a plaintext version of the theme (if you wish to use `generatePlaintext()`) 31 | plaintextPath: path.resolve('assets/mailgen/theme.txt') 32 | }, 33 | // Configure your product as usual (see examples above) 34 | product: {} 35 | }); 36 | ``` 37 | 38 | # Creating a Built-In Theme 39 | 40 | If you want to create a new **built-in** Mailgen theme: 41 | 42 | 1. Fork the repository to your GitHub account and clone it to your computer 43 | 2. Create a new directory inside `themes` with the desired theme name 44 | 3. Create an `index.html` file and an `index.txt` file within your theme directory 45 | 4. Copy the contents of [`themes/default/index.txt`](themes/default/index.txt) to your `index.txt` file and make any necessary changes 46 | 5. Paste your custom template HTML into the `index.html` file you created 47 | 6. Scroll down to the [injection snippets](#injection-snippets) and copy and paste each code snippet into the relevant area of your template markup 48 | 7. Test the theme by running the `examples/*.js` scripts (insert your theme name in each script) and observing the template output in `preview.html` 49 | 8. Take a screenshot of your theme portraying each example and place it in `screenshots/{theme}/{example}.png` 50 | 9. Add the theme name, credit, and screenshots to the `README.md` file's [Supported Themes](README.md#supported-themes) section (copy one of the existing themes' markup and modify it accordingly) 51 | 7. Submit a pull request with your changes and we'll let you know if anything's missing! 52 | 53 | Thanks again for your contribution! 54 | 55 | # Injection Snippets 56 | 57 | ## Product Branding Injection 58 | 59 | The following will inject either the product logo or name into the template. 60 | 61 | ```html 62 | 63 | <% if (locals.product.logo) { %> 64 | <% if (locals.product.logoHeight) { %> 65 | 66 | <% } else { %> 67 | 68 | <% } %> 69 | <% } else { %> 70 | <%- product.name %> 71 | <% } %> 72 | 73 | ``` 74 | 75 | It's a good idea to add the following CSS declaration to set `max-height: 50px` for the logo: 76 | 77 | ```css 78 | .email-logo { 79 | max-height: 50px; 80 | } 81 | ``` 82 | 83 | ## Title Injection 84 | 85 | The following will inject the e-mail title (Hi John Appleseed,) or a custom title provided by the user: 86 | 87 | ```html 88 | <%- title %> 89 | ``` 90 | 91 | ## Intro Injection 92 | 93 | The following will inject the intro text (string or array) into the e-mail: 94 | 95 | ```html 96 | <% if (locals.intro) { %> 97 | <% intro.forEach(function (introItem) { -%> 98 |

<%- introItem %>

99 | <% }) -%> 100 | <% } %> 101 | ``` 102 | 103 | ## Dictionary Injection 104 | 105 | The following will inject a `
` of key-value pairs into the e-mail: 106 | 107 | ```html 108 | 109 | <% if (locals.dictionary) { %> 110 |
111 | <% for (item in dictionary) { -%> 112 |
<%- item.charAt(0).toUpperCase() + item.slice(1) %>:
113 |
<%- dictionary[item] %>
114 | <% } -%> 115 |
116 | <% } %> 117 | ``` 118 | 119 | It's a good idea to add this to the top of the template to improve the styling of the dictionary: 120 | 121 | ```css 122 | /* Dictionary */ 123 | .dictionary { 124 | width: 100%; 125 | overflow: hidden; 126 | margin: 0 auto; 127 | padding: 0; 128 | } 129 | .dictionary dt { 130 | clear: both; 131 | color: #000; 132 | font-weight: bold; 133 | margin-right: 4px; 134 | } 135 | .dictionary dd { 136 | margin: 0 0 10px 0; 137 | } 138 | ``` 139 | 140 | ## Table Injection 141 | 142 | The following will inject the table into the e-mail: 143 | 144 | ```html 145 | 146 | <% if (locals.table) { %> 147 | 148 | 149 | <% for (var column in table.data[0]) {%> 150 | 160 | <% } %> 161 | 162 | <% for (var i in table.data) {%> 163 | 164 | <% for (var column in table.data[i]) {%> 165 | 172 | <% } %> 173 | 174 | <% } %> 175 |
152 | width="<%= table.columns.customWidth[column] %>" 153 | <% } %> 154 | <% if(locals.table.columns && locals.table.columns.customAlignment && locals.table.columns.customAlignment[column]) { %> 155 | style="text-align:<%= table.columns.customAlignment[column] %>" 156 | <% } %> 157 | > 158 |

<%- column.charAt(0).toUpperCase() + column.slice(1) %>

159 |
167 | style="text-align:<%= table.columns.customAlignment[column] %>" 168 | <% } %> 169 | > 170 | <%- table.data[i][column] %> 171 |
176 | <% } %> 177 | ``` 178 | 179 | It's a good idea to add this to the top of the template to improve the styling of the table: 180 | 181 | ```css 182 | /* Table */ 183 | .data-wrapper { 184 | width: 100%; 185 | margin: 0; 186 | padding: 35px 0; 187 | } 188 | .data-table { 189 | width: 100%; 190 | margin: 0; 191 | } 192 | .data-table th { 193 | text-align: left; 194 | padding: 0px 5px; 195 | padding-bottom: 8px; 196 | border-bottom: 1px solid #DEDEDE; 197 | } 198 | .data-table th p { 199 | margin: 0; 200 | font-size: 12px; 201 | } 202 | .data-table td { 203 | text-align: left; 204 | padding: 10px 5px; 205 | font-size: 15px; 206 | line-height: 18px; 207 | } 208 | ``` 209 | 210 | ## Action Injection 211 | 212 | The following will inject the action link (or button) into the e-mail: 213 | 214 | ```html 215 | 216 | <% if (locals.action) { %> 217 | <% action.forEach(function (actionItem) { -%> 218 |

<%- actionItem.instructions %>

219 | 220 | <%- actionItem.button.text %> 221 | 222 | <% }) -%> 223 | <% } %> 224 | ``` 225 | 226 | It's a good idea to add this to the top of the template to specify a fallback color for the action buttons in case it wasn't provided by the user: 227 | 228 | ```html 229 | <% 230 | if (locals.action) { 231 | // Make it possible to override action button color (specify fallback color if no color specified) 232 | locals.action.forEach(function(actionItem) { 233 | if (!actionItem.button.color) { 234 | actionItem.button.color = '#48CFAD'; 235 | } 236 | }); 237 | } 238 | %> 239 | ``` 240 | 241 | ## Outro Injection 242 | 243 | The following will inject the outro text (string or array) into the e-mail: 244 | 245 | ```html 246 | <% if (locals.outro) { %> 247 | <% outro.forEach(function (outroItem) { -%> 248 |

<%- outroItem %>

249 | <% }) -%> 250 | <% } %> 251 | ``` 252 | 253 | ## Signature Injection 254 | 255 | The following will inject the signature phrase (e.g. Yours truly) along with the product name into the e-mail: 256 | 257 | ```html 258 | <%- signature %>, 259 |
260 | <%- product.name %> 261 | ``` 262 | 263 | ## Copyright Injection 264 | 265 | The following will inject the copyright notice into the e-mail: 266 | 267 | ```html 268 | <%- product.copyright %> 269 | ``` 270 | 271 | ## Go-To Action Injection 272 | 273 | In order to support Gmail's [Go-To Actions](https://developers.google.com/gmail/markup/reference/go-to-action), add the following anywhere within the template: 274 | 275 | ```html 276 | 277 | <% if (locals.goToAction) { %> 278 | 291 | <% } %> 292 | ``` 293 | 294 | ## Text Direction Injection 295 | 296 | In order to support generating RTL e-mails, inject the `textDirection` variable into the `` tag: 297 | 298 | ```html 299 | 300 | ``` 301 | -------------------------------------------------------------------------------- /examples/receipt.js: -------------------------------------------------------------------------------- 1 | var Mailgen = require('../'); 2 | 3 | // Configure mailgen by setting a theme and your product info 4 | var mailGenerator = new Mailgen({ 5 | theme: 'default', 6 | product: { 7 | // Appears in header & footer of e-mails 8 | name: 'Mailgen', 9 | link: 'https://mailgen.js/' 10 | // Optional logo 11 | // logo: 'https://mailgen.js/img/logo.png' 12 | } 13 | }); 14 | 15 | // Prepare email contents 16 | var email = { 17 | body: { 18 | name: 'John Appleseed', 19 | intro: 'Your order has been processed successfully.', 20 | table: { 21 | data: [ 22 | { 23 | item: 'Node.js', 24 | description: 'Event-driven I/O server-side JavaScript environment based on V8.', 25 | price: '$10.99' 26 | }, 27 | { 28 | item: 'Mailgen', 29 | description: 'Programmatically create beautiful e-mails using plain old JavaScript.', 30 | price: '$1.99' 31 | } 32 | ], 33 | columns: { 34 | // Optionally, customize the column widths 35 | customWidth: { 36 | item: '20%', 37 | price: '15%' 38 | }, 39 | // Optionally, change column text alignment 40 | customAlignment: { 41 | price: 'right' 42 | } 43 | } 44 | }, 45 | action: { 46 | instructions: 'You can check the status of your order and more in your dashboard:', 47 | button: { 48 | color: '#3869D4', 49 | text: 'Go to Dashboard', 50 | link: 'https://mailgen.js/confirm?s=d9729feb74992cc3482b350163a1a010' 51 | } 52 | }, 53 | outro: 'We thank you for your purchase.' 54 | } 55 | }; 56 | 57 | // Generate an HTML email with the provided contents 58 | var emailBody = mailGenerator.generate(email); 59 | 60 | // Generate the plaintext version of the e-mail (for clients that do not support HTML) 61 | var emailText = mailGenerator.generatePlaintext(email); 62 | 63 | // Optionally, preview the generated HTML e-mail by writing it to a local file 64 | require('fs').writeFileSync('preview.html', emailBody, 'utf8'); 65 | require('fs').writeFileSync('preview.txt', emailText, 'utf8'); 66 | 67 | // `emailBody` now contains the HTML body, 68 | // and `emailText` contains the textual version. 69 | // 70 | // It's up to you to send the e-mail. 71 | // Check out nodemailer to accomplish this: 72 | // https://nodemailer.com/ 73 | 74 | // Send the e-mail with your favorite mailer 75 | // transporter.sendMail({ 76 | // from: 'no-reply@mailgen.js', 77 | // to: 'target@email.com', 78 | // subject: 'Mailgen', 79 | // html: emailBody, 80 | // text: emailText, 81 | // }, function (err) { 82 | // if (err) return console.log(err); 83 | // console.log('Message sent successfully.'); 84 | // }); 85 | -------------------------------------------------------------------------------- /examples/reset.js: -------------------------------------------------------------------------------- 1 | var Mailgen = require('../'); 2 | 3 | // Configure mailgen by setting a theme and your product info 4 | var mailGenerator = new Mailgen({ 5 | theme: 'default', 6 | product: { 7 | // Appears in header & footer of e-mails 8 | name: 'Mailgen', 9 | link: 'https://mailgen.js/' 10 | // Optional logo 11 | // logo: 'https://mailgen.js/img/logo.png' 12 | } 13 | }); 14 | 15 | // Prepare email contents 16 | var email = { 17 | body: { 18 | name: 'John Appleseed', 19 | intro: 'You have received this email because a password reset request for your account was received.', 20 | action: { 21 | instructions: 'Click the button below to reset your password:', 22 | button: { 23 | color: '#DC4D2F', 24 | text: 'Reset your password', 25 | link: 'https://mailgen.js/reset?s=b350163a1a010d9729feb74992c1a010' 26 | } 27 | }, 28 | outro: 'If you did not request a password reset, no further action is required on your part.' 29 | } 30 | }; 31 | 32 | // Generate an HTML email with the provided contents 33 | var emailBody = mailGenerator.generate(email); 34 | 35 | // Generate the plaintext version of the e-mail (for clients that do not support HTML) 36 | var emailText = mailGenerator.generatePlaintext(email); 37 | 38 | // Optionally, preview the generated HTML e-mail by writing it to a local file 39 | require('fs').writeFileSync('preview.html', emailBody, 'utf8'); 40 | require('fs').writeFileSync('preview.txt', emailText, 'utf8'); 41 | 42 | // `emailBody` now contains the HTML body, 43 | // and `emailText` contains the textual version. 44 | // 45 | // It's up to you to send the e-mail. 46 | // Check out nodemailer to accomplish this: 47 | // https://nodemailer.com/ 48 | 49 | // Send the e-mail with your favorite mailer 50 | // transporter.sendMail({ 51 | // from: 'no-reply@mailgen.js', 52 | // to: 'target@email.com', 53 | // subject: 'Mailgen', 54 | // html: emailBody, 55 | // text: emailText, 56 | // }, function (err) { 57 | // if (err) return console.log(err); 58 | // console.log('Message sent successfully.'); 59 | // }); 60 | -------------------------------------------------------------------------------- /examples/welcome.js: -------------------------------------------------------------------------------- 1 | var Mailgen = require('../'); 2 | 3 | // Configure mailgen by setting a theme and your product info 4 | var mailGenerator = new Mailgen({ 5 | theme: 'default', 6 | product: { 7 | // Appears in header & footer of e-mails 8 | name: 'Mailgen', 9 | link: 'https://mailgen.js/' 10 | // Optional logo 11 | // logo: 'https://mailgen.js/img/logo.png' 12 | } 13 | }); 14 | 15 | // Prepare email contents 16 | var email = { 17 | body: { 18 | name: 'John Appleseed', 19 | intro: 'Welcome to Mailgen! We\'re very excited to have you on board.', 20 | action: { 21 | instructions: 'To get started with Mailgen, please click here:', 22 | button: { 23 | color: '#22BC66', 24 | text: 'Confirm your account', 25 | link: 'https://mailgen.js/confirm?s=d9729feb74992cc3482b350163a1a010' 26 | } 27 | }, 28 | outro: 'Need help, or have questions? Just reply to this email, we\'d love to help.' 29 | } 30 | }; 31 | 32 | // Generate an HTML email with the provided contents 33 | var emailBody = mailGenerator.generate(email); 34 | 35 | // Generate the plaintext version of the e-mail (for clients that do not support HTML) 36 | var emailText = mailGenerator.generatePlaintext(email); 37 | 38 | // Optionally, preview the generated HTML e-mail by writing it to a local file 39 | require('fs').writeFileSync('preview.html', emailBody, 'utf8'); 40 | require('fs').writeFileSync('preview.txt', emailText, 'utf8'); 41 | 42 | // `emailBody` now contains the HTML body, 43 | // and `emailText` contains the textual version. 44 | // 45 | // It's up to you to send the e-mail. 46 | // Check out nodemailer to accomplish this: 47 | // https://nodemailer.com/ 48 | 49 | // Send the e-mail with your favorite mailer 50 | // transporter.sendMail({ 51 | // from: 'no-reply@mailgen.js', 52 | // to: 'target@email.com', 53 | // subject: 'Mailgen', 54 | // html: emailBody, 55 | // text: emailText, 56 | // }, function (err) { 57 | // if (err) return console.log(err); 58 | // console.log('Message sent successfully.'); 59 | // }); 60 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for mailgen 2.0 2 | // Definitions by: Kiet Thanh Vo , Jordan Farrer , Grzegorz Kawka-Osik 3 | import Option = Mailgen.Option; 4 | import Content = Mailgen.Content; 5 | /** 6 | * Created by kiettv on 7/24/16. 7 | */ 8 | declare class Mailgen { 9 | constructor(opts: Option); 10 | 11 | cacheThemes(): void; 12 | 13 | generate(params: Content): any; 14 | 15 | generatePlaintext(params: Content): any; 16 | 17 | parseParams(params: any): any; 18 | } 19 | 20 | declare namespace Mailgen { 21 | interface Option { 22 | theme: string | CustomTheme; 23 | product: Product; 24 | /** 25 | * To change the default text direction 26 | * @default ltr 27 | */ 28 | textDirection?: 'ltr' | 'rtl' | string; 29 | } 30 | 31 | interface CustomTheme { 32 | path: string; 33 | plaintextPath?: string; 34 | } 35 | 36 | interface Product { 37 | name: string; 38 | link: string; 39 | logo?: string; 40 | logoHeight?: string; 41 | copyright?: string; 42 | } 43 | 44 | interface Content { 45 | body: ContentBody; 46 | } 47 | 48 | interface ContentBody { 49 | name?: string; 50 | greeting?: string | boolean; 51 | signature?: string | boolean; 52 | title?: string; 53 | intro?: string | string[]; 54 | action?: Action | Action[]; 55 | table?: Table | Table[]; 56 | dictionary?: any; 57 | goToAction?: GoToAction; 58 | outro?: string | string[]; 59 | } 60 | 61 | interface Table { 62 | title?: string, 63 | data: any[]; 64 | columns?: ColumnOptions; 65 | } 66 | 67 | interface ColumnOptions { 68 | customWidth?: Record; 69 | customAlignment?: Record; 70 | } 71 | 72 | interface GoToAction { 73 | text: string; 74 | link: string; 75 | description: string; 76 | } 77 | 78 | interface Action { 79 | instructions: string; 80 | button: Button; 81 | } 82 | 83 | interface Button { 84 | color?: string; 85 | fallback?: boolean | Fallback; 86 | text: string; 87 | link: string; 88 | } 89 | 90 | interface Fallback { 91 | text?: string; 92 | } 93 | } 94 | 95 | export = Mailgen; 96 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var he = require('he'); 2 | var fs = require('fs'); 3 | var ejs = require('ejs'); 4 | var juice = require('juice'); 5 | 6 | // Package constructor 7 | function Mailgen(options) { 8 | // Set options as instance members 9 | this.theme = options.theme; 10 | this.product = options.product; 11 | this.themeName = (typeof this.theme === 'string' && this.theme) ? this.theme : 'default'; 12 | 13 | // No product? 14 | if (!this.product || typeof this.product !== 'object') { 15 | throw new Error('Please provide the `product` object.'); 16 | } 17 | 18 | // No product name or link? 19 | if (!this.product.name || !this.product.link) { 20 | throw new Error('Please provide the product name and link.'); 21 | } 22 | 23 | // Support for custom text direction (fallback to LTR) 24 | this.textDirection = options.textDirection || 'ltr'; 25 | 26 | // Support for custom copyright (fallback to sensible default) 27 | this.product.copyright = this.product.copyright || '© ' + new Date().getFullYear() + ' ' + this.product.name + '. All rights reserved.'; 28 | 29 | // Cache theme files for later to avoid spamming fs.readFileSync() 30 | this.cacheThemes(); 31 | } 32 | 33 | function convertToArray(arr) { 34 | return Array.isArray(arr) ? arr : [arr].filter(Boolean) 35 | } 36 | 37 | function setupButtonFallbackText(btn) { 38 | // No fallback or false passed in? 39 | if (!btn.fallback) { 40 | return; 41 | } 42 | 43 | // Default fallback text requested? 44 | if (btn.fallback === true) { 45 | btn.fallback = { 46 | text: `If you're having trouble clicking the "${btn.text}" button, please copy and paste the following link into your web browser's address bar:` 47 | }; 48 | } 49 | } 50 | 51 | Mailgen.prototype.cacheThemes = function () { 52 | // Build path to theme file (make it possible to pass in a custom theme path, fallback to mailgen-bundled theme) 53 | var themePath = (typeof this.theme === 'object' && this.theme.path) ? this.theme.path : __dirname + '/themes/' + this.themeName + '/index.html'; 54 | 55 | // Bad theme path? 56 | if (!fs.existsSync(themePath)) { 57 | throw new Error('You have specified an invalid theme.'); 58 | } 59 | 60 | // Load theme (sync) and cache it for later 61 | this.cachedTheme = fs.readFileSync(themePath, 'utf8'); 62 | 63 | // Build path to plaintext theme file (make it possible to pass in a custom plaintext theme path, fallback to mailgen-bundled theme) 64 | var plaintextPath = (typeof this.theme === 'object' && this.theme.plaintextPath) ? this.theme.plaintextPath : __dirname + '/themes/' + this.themeName + '/index.txt'; 65 | 66 | // Bad plaintext theme path specified? 67 | if (!fs.existsSync(plaintextPath) && (typeof this.theme === 'object' && this.theme.plaintextPath)) { 68 | throw new Error('You have specified an invalid plaintext theme.'); 69 | } 70 | 71 | // Keep this for referencing in ejs.render() 72 | this.themePath = themePath; 73 | 74 | // Plaintext path exists? 75 | if (fs.existsSync(plaintextPath)) { 76 | // Load plaintext theme (sync) and cache it for later 77 | this.cachedPlaintextTheme = fs.readFileSync(plaintextPath, 'utf8'); 78 | } 79 | }; 80 | 81 | // HTML e-mail generator 82 | Mailgen.prototype.generate = function (params) { 83 | // Parse email params and get back an object with data to inject 84 | var ejsParams = this.parseParams(params); 85 | 86 | // Render the theme with ejs, injecting the data accordingly 87 | var output = ejs.render(this.cachedTheme, ejsParams, { filename: this.themePath }); 88 | 89 | // Inline CSS 90 | output = juice(output); 91 | 92 | // All done! 93 | return output; 94 | }; 95 | 96 | // Plaintext text e-mail generator 97 | Mailgen.prototype.generatePlaintext = function (params) { 98 | // Plaintext theme not cached? 99 | if (!this.cachedPlaintextTheme) { 100 | throw new Error('An error was encountered while loading the plaintext theme.'); 101 | } 102 | 103 | // Parse email params and get back an object with data to inject 104 | var ejsParams = this.parseParams(params); 105 | 106 | // Render the plaintext theme with ejs, injecting the data accordingly 107 | var output = ejs.render(this.cachedPlaintextTheme, ejsParams); 108 | 109 | // Definition of the
tag as a regex pattern 110 | var breakTag = /(?:\)/g; 111 | var breakTagPattern = new RegExp(breakTag); 112 | 113 | // Check the plaintext for html break tag, maintains backwards compatiblity 114 | if (breakTagPattern.test(this.cachedPlaintextTheme)) { 115 | // Strip all linebreaks from the rendered plaintext 116 | output = output.replace(/(?:\r\n|\r|\n)/g, ''); 117 | 118 | // Replace html break tags with linebreaks 119 | output = output.replace(breakTag, '\n'); 120 | 121 | // Remove plaintext theme indentation (tabs or spaces in the beginning of each line) 122 | output = output.replace(/^(?: |\t)*/gm, ""); 123 | } 124 | 125 | // Strip all HTML tags from plaintext output 126 | output = output.replace(/<.+?>/g, ''); 127 | 128 | // Decode HTML entities such as © 129 | output = he.decode(output); 130 | 131 | // All done! 132 | return output; 133 | }; 134 | 135 | // Validates, parses and returns injectable ejs parameters 136 | Mailgen.prototype.parseParams = function (params) { 137 | // Basic params validation 138 | if (!params || typeof params !== 'object') { 139 | throw new Error('Please provide parameters for generating transactional e-mails.'); 140 | } 141 | 142 | // Get body params to inject into theme 143 | var body = params.body; 144 | 145 | // Basic body validation 146 | if (!body || typeof body !== 'object') { 147 | throw new Error('Please provide the `body` parameter as an object.'); 148 | } 149 | 150 | // Pass text direction to template 151 | body.textDirection = this.textDirection; 152 | 153 | // Only set greeting if greeting is not false (allow any greeting (name & title) to be optional) 154 | // Setting greeting to false will override title and name options 155 | if (body.greeting !== false) { 156 | // Support for custom greeting/signature (fallback to sensible defaults) 157 | body.greeting = body.greeting || 'Hi'; 158 | 159 | // Use `greeting` or `name` for title if not set 160 | if (!body.title) { 161 | // Use name if provided, otherwise, default to greeting only 162 | body.title = (body.name ? body.greeting + ' ' + body.name : body.greeting) + ','; 163 | } 164 | } 165 | 166 | // Only set signature if signature is not false 167 | if (body.signature !== false) { 168 | body.signature = body.signature || 'Yours truly'; 169 | } 170 | 171 | // Convert intro, outro, and action to arrays if a string or object is used instead 172 | body.intro = convertToArray(body.intro); 173 | body.outro = convertToArray(body.outro); 174 | body.action = convertToArray(body.action); 175 | 176 | // Enable multiple buttons per action 177 | for (var action of body.action) { 178 | action.button = convertToArray(action.button); 179 | 180 | // Set up default button fallback text 181 | for (var button of action.button) { 182 | setupButtonFallbackText(button); 183 | } 184 | } 185 | 186 | body.table = convertToArray(body.table); 187 | 188 | // Prepare data to be passed to ejs engine 189 | var ejsParams = { 190 | product: this.product 191 | }; 192 | 193 | // Pass email body elements to ejs 194 | for (var k in body) { 195 | ejsParams[k] = body[k]; 196 | } 197 | 198 | return ejsParams; 199 | }; 200 | 201 | // Expose the Mailgen class 202 | module.exports = Mailgen; 203 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs" 4 | } 5 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailgen", 3 | "version": "2.0.29", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-colors": { 8 | "version": "4.1.1", 9 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 10 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" 11 | }, 12 | "ansi-styles": { 13 | "version": "4.3.0", 14 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 15 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 16 | "requires": { 17 | "color-convert": "^2.0.1" 18 | } 19 | }, 20 | "async": { 21 | "version": "3.2.5", 22 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", 23 | "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" 24 | }, 25 | "balanced-match": { 26 | "version": "1.0.2", 27 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 28 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 29 | }, 30 | "boolbase": { 31 | "version": "1.0.0", 32 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 33 | "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" 34 | }, 35 | "brace-expansion": { 36 | "version": "2.0.1", 37 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 38 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 39 | "requires": { 40 | "balanced-match": "^1.0.0" 41 | } 42 | }, 43 | "chalk": { 44 | "version": "4.1.2", 45 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 46 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 47 | "requires": { 48 | "ansi-styles": "^4.1.0", 49 | "supports-color": "^7.1.0" 50 | } 51 | }, 52 | "cheerio": { 53 | "version": "1.0.0-rc.10", 54 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", 55 | "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", 56 | "requires": { 57 | "cheerio-select": "^1.5.0", 58 | "dom-serializer": "^1.3.2", 59 | "domhandler": "^4.2.0", 60 | "htmlparser2": "^6.1.0", 61 | "parse5": "^6.0.1", 62 | "parse5-htmlparser2-tree-adapter": "^6.0.1", 63 | "tslib": "^2.2.0" 64 | } 65 | }, 66 | "cheerio-select": { 67 | "version": "1.5.0", 68 | "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", 69 | "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", 70 | "requires": { 71 | "css-select": "^4.1.3", 72 | "css-what": "^5.0.1", 73 | "domelementtype": "^2.2.0", 74 | "domhandler": "^4.2.0", 75 | "domutils": "^2.7.0" 76 | } 77 | }, 78 | "color-convert": { 79 | "version": "2.0.1", 80 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 81 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 82 | "requires": { 83 | "color-name": "~1.1.4" 84 | } 85 | }, 86 | "color-name": { 87 | "version": "1.1.4", 88 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 89 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 90 | }, 91 | "commander": { 92 | "version": "6.2.1", 93 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", 94 | "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" 95 | }, 96 | "concat-map": { 97 | "version": "0.0.1", 98 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 99 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 100 | }, 101 | "css-select": { 102 | "version": "4.1.3", 103 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", 104 | "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", 105 | "requires": { 106 | "boolbase": "^1.0.0", 107 | "css-what": "^5.0.0", 108 | "domhandler": "^4.2.0", 109 | "domutils": "^2.6.0", 110 | "nth-check": "^2.0.0" 111 | } 112 | }, 113 | "css-what": { 114 | "version": "5.0.1", 115 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", 116 | "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==" 117 | }, 118 | "dom-serializer": { 119 | "version": "1.3.2", 120 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", 121 | "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", 122 | "requires": { 123 | "domelementtype": "^2.0.1", 124 | "domhandler": "^4.2.0", 125 | "entities": "^2.0.0" 126 | } 127 | }, 128 | "domelementtype": { 129 | "version": "2.2.0", 130 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", 131 | "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" 132 | }, 133 | "domhandler": { 134 | "version": "4.2.0", 135 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", 136 | "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", 137 | "requires": { 138 | "domelementtype": "^2.2.0" 139 | } 140 | }, 141 | "domutils": { 142 | "version": "2.7.0", 143 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", 144 | "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", 145 | "requires": { 146 | "dom-serializer": "^1.0.1", 147 | "domelementtype": "^2.2.0", 148 | "domhandler": "^4.2.0" 149 | } 150 | }, 151 | "ejs": { 152 | "version": "3.1.10", 153 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", 154 | "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", 155 | "requires": { 156 | "jake": "^10.8.5" 157 | } 158 | }, 159 | "entities": { 160 | "version": "2.2.0", 161 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 162 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" 163 | }, 164 | "escape-goat": { 165 | "version": "3.0.0", 166 | "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", 167 | "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==" 168 | }, 169 | "filelist": { 170 | "version": "1.0.4", 171 | "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", 172 | "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", 173 | "requires": { 174 | "minimatch": "^5.0.1" 175 | }, 176 | "dependencies": { 177 | "minimatch": { 178 | "version": "5.1.6", 179 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 180 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 181 | "requires": { 182 | "brace-expansion": "^2.0.1" 183 | } 184 | } 185 | } 186 | }, 187 | "has-flag": { 188 | "version": "4.0.0", 189 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 190 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 191 | }, 192 | "he": { 193 | "version": "1.2.0", 194 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 195 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" 196 | }, 197 | "htmlparser2": { 198 | "version": "6.1.0", 199 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", 200 | "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", 201 | "requires": { 202 | "domelementtype": "^2.0.1", 203 | "domhandler": "^4.0.0", 204 | "domutils": "^2.5.2", 205 | "entities": "^2.0.0" 206 | } 207 | }, 208 | "jake": { 209 | "version": "10.8.7", 210 | "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", 211 | "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", 212 | "requires": { 213 | "async": "^3.2.3", 214 | "chalk": "^4.0.2", 215 | "filelist": "^1.0.4", 216 | "minimatch": "^3.1.2" 217 | } 218 | }, 219 | "juice": { 220 | "version": "8.0.0", 221 | "resolved": "https://registry.npmjs.org/juice/-/juice-8.0.0.tgz", 222 | "integrity": "sha512-LRCfXBOqI1wt+zYR/5xwDnf+ZyiJiDt44DGZaBSAVwZWyWv3BliaiGTLS6KCvadv3uw6XGiPPFcTfY7CdF7Z/Q==", 223 | "requires": { 224 | "cheerio": "^1.0.0-rc.3", 225 | "commander": "^6.1.0", 226 | "mensch": "^0.3.4", 227 | "slick": "^1.12.2", 228 | "web-resource-inliner": "^5.0.0" 229 | } 230 | }, 231 | "mensch": { 232 | "version": "0.3.4", 233 | "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", 234 | "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" 235 | }, 236 | "mime": { 237 | "version": "2.5.2", 238 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", 239 | "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" 240 | }, 241 | "minimatch": { 242 | "version": "3.1.2", 243 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 244 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 245 | "requires": { 246 | "brace-expansion": "^1.1.7" 247 | }, 248 | "dependencies": { 249 | "brace-expansion": { 250 | "version": "1.1.11", 251 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 252 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 253 | "requires": { 254 | "balanced-match": "^1.0.0", 255 | "concat-map": "0.0.1" 256 | } 257 | } 258 | } 259 | }, 260 | "node-fetch": { 261 | "version": "2.6.7", 262 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 263 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 264 | "requires": { 265 | "whatwg-url": "^5.0.0" 266 | } 267 | }, 268 | "nth-check": { 269 | "version": "2.0.1", 270 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", 271 | "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", 272 | "requires": { 273 | "boolbase": "^1.0.0" 274 | } 275 | }, 276 | "parse5": { 277 | "version": "6.0.1", 278 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", 279 | "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" 280 | }, 281 | "parse5-htmlparser2-tree-adapter": { 282 | "version": "6.0.1", 283 | "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", 284 | "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", 285 | "requires": { 286 | "parse5": "^6.0.1" 287 | } 288 | }, 289 | "slick": { 290 | "version": "1.12.2", 291 | "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", 292 | "integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc=" 293 | }, 294 | "supports-color": { 295 | "version": "7.2.0", 296 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 297 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 298 | "requires": { 299 | "has-flag": "^4.0.0" 300 | } 301 | }, 302 | "tr46": { 303 | "version": "0.0.3", 304 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 305 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 306 | }, 307 | "tslib": { 308 | "version": "2.3.0", 309 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", 310 | "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" 311 | }, 312 | "valid-data-url": { 313 | "version": "3.0.1", 314 | "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", 315 | "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==" 316 | }, 317 | "web-resource-inliner": { 318 | "version": "5.0.0", 319 | "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-5.0.0.tgz", 320 | "integrity": "sha512-AIihwH+ZmdHfkJm7BjSXiEClVt4zUFqX4YlFAzjL13wLtDuUneSaFvDBTbdYRecs35SiU7iNKbMnN+++wVfb6A==", 321 | "requires": { 322 | "ansi-colors": "^4.1.1", 323 | "escape-goat": "^3.0.0", 324 | "htmlparser2": "^4.0.0", 325 | "mime": "^2.4.6", 326 | "node-fetch": "^2.6.0", 327 | "valid-data-url": "^3.0.0" 328 | }, 329 | "dependencies": { 330 | "domhandler": { 331 | "version": "3.3.0", 332 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", 333 | "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", 334 | "requires": { 335 | "domelementtype": "^2.0.1" 336 | } 337 | }, 338 | "htmlparser2": { 339 | "version": "4.1.0", 340 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", 341 | "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", 342 | "requires": { 343 | "domelementtype": "^2.0.1", 344 | "domhandler": "^3.0.0", 345 | "domutils": "^2.0.0", 346 | "entities": "^2.0.0" 347 | } 348 | } 349 | } 350 | }, 351 | "webidl-conversions": { 352 | "version": "3.0.1", 353 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 354 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 355 | }, 356 | "whatwg-url": { 357 | "version": "5.0.0", 358 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 359 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 360 | "requires": { 361 | "tr46": "~0.0.3", 362 | "webidl-conversions": "^3.0.0" 363 | } 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailgen", 3 | "version": "2.0.29", 4 | "description": "Generates clean, responsive HTML e-mails for sending transactional mail.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/eladnava/mailgen.git" 13 | }, 14 | "author": "Elad Nava", 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/eladnava/mailgen/issues" 18 | }, 19 | "homepage": "https://github.com/eladnava/mailgen#readme", 20 | "dependencies": { 21 | "ejs": "^3.1.6", 22 | "he": "^1.2.0", 23 | "juice": "^8.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /screenshots/cerberus/receipt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/cerberus/receipt.png -------------------------------------------------------------------------------- /screenshots/cerberus/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/cerberus/reset.png -------------------------------------------------------------------------------- /screenshots/cerberus/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/cerberus/welcome.png -------------------------------------------------------------------------------- /screenshots/default/receipt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/default/receipt.png -------------------------------------------------------------------------------- /screenshots/default/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/default/reset.png -------------------------------------------------------------------------------- /screenshots/default/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/default/welcome.png -------------------------------------------------------------------------------- /screenshots/neopolitan/receipt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/neopolitan/receipt.png -------------------------------------------------------------------------------- /screenshots/neopolitan/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/neopolitan/reset.png -------------------------------------------------------------------------------- /screenshots/neopolitan/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/neopolitan/welcome.png -------------------------------------------------------------------------------- /screenshots/salted/receipt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/salted/receipt.png -------------------------------------------------------------------------------- /screenshots/salted/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/salted/reset.png -------------------------------------------------------------------------------- /screenshots/salted/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eladnava/mailgen/7ea09226ccef64432d5f3cd2a4e0c6775b06c01c/screenshots/salted/welcome.png -------------------------------------------------------------------------------- /themes/cerberus/index.html: -------------------------------------------------------------------------------- 1 | <% 2 | if (locals.action) { 3 | // Make it possible to override action button color (specify fallback color if no color specified) 4 | locals.action.forEach(function(actionItem) { 5 | actionItem.button.forEach(function(button) { 6 | // Make it possible to override action button color (specify fallback color if no color specified) 7 | if (!button.color) { 8 | button.color = '#222222'; 9 | } 10 | 11 | deprecatedColorToHex(button); 12 | }); 13 | }); 14 | 15 | // Convert deprecated red/green/blue action button color to hex since we switched to a hex-based button color 16 | function deprecatedColorToHex(button) { 17 | if (button.color === 'red') { 18 | button.color = '#DC4D2F'; 19 | } 20 | else if (button.color === 'green') { 21 | button.color = '#22BC66'; 22 | } 23 | else if (button.color === 'blue') { 24 | button.color = '#3869D4'; 25 | } 26 | } 27 | } 28 | %> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 197 | 198 | 199 | 213 | 214 | 215 | 216 |
217 | 218 | 223 |
224 | 229 | 230 | 231 | 232 | 233 | 246 | 247 |
234 | 235 | <% if (locals.product.logo) { %> 236 | <% if (locals.product.logoHeight) { %> 237 | 238 | <% } else { %> 239 | 240 | <% } %> 241 | <% } else { %> 242 | <%- product.name %> 243 | <% } %> 244 | 245 |
248 | 249 | 250 | 251 | 252 | 253 | 254 | <% if (locals.title) { %> 255 | 256 | 261 | 262 | <% } %> 263 | 264 | 265 | 266 | 267 | 329 | 330 | <% if (locals.action.length !== 0) { %> 331 | 332 | 374 | 375 | <% } %> 376 | 377 | 378 | 379 | 380 | 402 | 403 | 404 |
257 |

258 | <%- title %> 259 |

260 |
268 | 269 | 270 | 326 | 327 |
271 | <% if (locals.intro) { %> 272 | <% intro.forEach(function (introItem) { -%> 273 |

<%- introItem %>

274 | <% }) -%> 275 | <% } %> 276 | 277 | 278 | <% if (locals.dictionary) { %> 279 |
280 |
281 | <% for (item in dictionary) { -%> 282 |
<%- item.charAt(0).toUpperCase() + item.slice(1) %>:
283 |
<%- dictionary[item] %>
284 | <% } -%> 285 |
286 |
287 | <% } %> 288 | 289 | 290 | <% if (locals.table) { %> 291 | <% table.forEach(function (tableItem, i) { -%> 292 |
293 |

<%- tableItem.title %>

294 | 295 | 296 | <% for (var column in tableItem.data[0]) {%> 297 | 307 | <% } %> 308 | 309 | <% for (var i in tableItem.data) {%> 310 | 311 | <% for (var column in tableItem.data[i]) {%> 312 | 319 | <% } %> 320 | 321 | <% } %> 322 |
299 | width="<%= tableItem.columns.customWidth[column] %>" 300 | <% } %> 301 | <% if(tableItem.columns && tableItem.columns.customAlignment && tableItem.columns.customAlignment[column]) { %> 302 | style="text-align:<%= tableItem.columns.customAlignment[column] %>" 303 | <% } %> 304 | > 305 |

<%- column.charAt(0).toUpperCase() + column.slice(1) %>

306 |
314 | style="text-align:<%= tableItem.columns.customAlignment[column] %>" 315 | <% } %> 316 | > 317 | <%- tableItem.data[i][column] %> 318 |
323 | <% }) %> 324 | <% } %> 325 |
328 |
333 | 334 | 335 | 371 | 372 |
336 |
337 | 338 | <% action.forEach(function (actionItem) { -%> 339 |

<%- actionItem.instructions %>

340 |
341 | 342 | 354 | 355 | 356 | 357 | <% actionItem.button.forEach(function (actionButton) { -%> 358 | 363 | <% }) -%> 364 | 365 |
359 | 360 |     <%- actionButton.text %>     361 | 362 |
366 | 367 |
368 | 369 | <% }) -%> 370 |
373 |
381 | 382 | 383 | 390 | 391 | <% if (signature) { %> 392 | 393 | 398 | 399 | <% } %> 400 |
384 | <% if (locals.outro) { %> 385 | <% outro.forEach(function (outroItem) { -%> 386 |

<%- outroItem %>

387 | <% }) -%> 388 | <% } %> 389 |
394 | <%- signature %>, 395 |
396 | <%- product.name %> 397 |
401 |
405 | 406 | 407 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %> 408 | 409 | 410 | 423 | 424 |
411 | 412 | <% action.forEach(function (actionItem) { -%> 413 | <% actionItem.button.forEach(function (actionButton) { -%> 414 | <% if (actionButton.fallback != null) { %> 415 |

416 | <%- actionButton.fallback.text %>
417 | <%- actionButton.link %> 418 |

419 | <% } %> 420 | <% }) -%> 421 | <% }) -%> 422 |
425 | <% } %> 426 | 427 | 428 | 429 | 430 | 433 | 434 |
435 | 436 | 437 | 442 |
443 |
444 | 445 | 446 | <% if (locals.goToAction) { %> 447 | 460 | <% } %> 461 | 462 | 463 | 464 | -------------------------------------------------------------------------------- /themes/cerberus/index.txt: -------------------------------------------------------------------------------- 1 | <% if (locals.title) { %> 2 | <%- title %> 3 | <% } %> 4 | 5 |
6 |
7 | 8 | <% if (locals.intro) { %> 9 | <% intro.forEach(function (introItem) { -%> 10 | <%- introItem %> 11 |
12 | <% }) -%> 13 | 14 |
15 | <% } %> 16 | 17 | <% if (locals.dictionary) { %> 18 | <% for (item in dictionary) { -%> 19 | <%- item.charAt(0).toUpperCase() + item.slice(1) %>: <%- dictionary[item] %> 20 |
21 | <% } -%> 22 | 23 |
24 | <% } %> 25 | 26 | <% if (locals.action) { %> 27 | <% action.forEach(function (actionItem) { -%> 28 | <%- actionItem.instructions %> 29 |
30 | <% actionItem.button.forEach(function (actionButton) { -%> 31 | <%- actionButton.link %> 32 |
33 | <% }) -%> 34 | 35 |
36 | <% }) -%> 37 | <% } %> 38 | 39 | <% if (locals.outro) { %> 40 | <% outro.forEach(function (outroItem) { -%> 41 | <%- outroItem %> 42 |
43 | <% }) -%> 44 | 45 |
46 | <% } %> 47 | 48 | <% if (signature) { %> 49 | <%- signature %>, 50 |
51 | <%- product.name %> 52 | <% } %> 53 | 54 |
55 |
56 | <%- product.copyright %> 57 | -------------------------------------------------------------------------------- /themes/default/index.html: -------------------------------------------------------------------------------- 1 | <% 2 | if (locals.action) { 3 | // Make it possible to override action button color (specify fallback color if no color specified) 4 | locals.action.forEach(function(actionItem) { 5 | actionItem.button.forEach(function(button) { 6 | // Make it possible to override action button color (specify fallback color if no color specified) 7 | if (!button.color) { 8 | button.color = 'blue'; 9 | } 10 | 11 | deprecatedColorToHex(button); 12 | }); 13 | }); 14 | 15 | // Convert deprecated red/green/blue action button color to hex since we switched to a hex-based button color 16 | function deprecatedColorToHex(button) { 17 | if (button.color === 'red') { 18 | button.color = '#DC4D2F'; 19 | } 20 | else if (button.color === 'green') { 21 | button.color = '#22BC66'; 22 | } 23 | else if (button.color === 'blue') { 24 | button.color = '#3869D4'; 25 | } 26 | } 27 | } 28 | %> 29 | 30 | 31 | 32 | 33 | 34 | 239 | 240 | 241 | 242 | 243 | 435 | 436 | 437 | 438 | 439 | -------------------------------------------------------------------------------- /themes/default/index.txt: -------------------------------------------------------------------------------- 1 | <% if (locals.title) { %> 2 | <%- title %> 3 | <% } %> 4 | 5 |
6 |
7 | 8 | <% if (locals.intro) { %> 9 | <% intro.forEach(function (introItem) { -%> 10 | <%- introItem %> 11 |
12 | <% }) -%> 13 | 14 |
15 | <% } %> 16 | 17 | <% if (locals.dictionary) { %> 18 | <% for (item in dictionary) { -%> 19 | <%- item.charAt(0).toUpperCase() + item.slice(1) %>: <%- dictionary[item] %> 20 |
21 | <% } -%> 22 | 23 |
24 | <% } %> 25 | 26 | <% if (locals.action) { %> 27 | <% action.forEach(function (actionItem) { -%> 28 | <%- actionItem.instructions %> 29 |
30 | <% actionItem.button.forEach(function (actionButton) { -%> 31 | <%- actionButton.link %> 32 |
33 | <% }) -%> 34 | 35 |
36 | <% }) -%> 37 | <% } %> 38 | 39 | <% if (locals.outro) { %> 40 | <% outro.forEach(function (outroItem) { -%> 41 | <%- outroItem %> 42 |
43 | <% }) -%> 44 | 45 |
46 | <% } %> 47 | 48 | <% if (signature) { %> 49 | <%- signature %>, 50 |
51 | <%- product.name %> 52 | <% } %> 53 | 54 |
55 |
56 | <%- product.copyright %> 57 | -------------------------------------------------------------------------------- /themes/neopolitan/index.html: -------------------------------------------------------------------------------- 1 | <% 2 | if (locals.action) { 3 | // Make it possible to override action button color (specify fallback color if no color specified) 4 | locals.action.forEach(function(actionItem) { 5 | actionItem.button.forEach(function(button) { 6 | // Make it possible to override action button color (specify fallback color if no color specified) 7 | if (!button.color) { 8 | button.color = '#414141'; 9 | } 10 | 11 | deprecatedColorToHex(button); 12 | }); 13 | }); 14 | 15 | // Convert deprecated red/green/blue action button color to hex since we switched to a hex-based button color 16 | function deprecatedColorToHex(button) { 17 | if (button.color === 'red') { 18 | button.color = '#DC4D2F'; 19 | } 20 | else if (button.color === 'green') { 21 | button.color = '#22BC66'; 22 | } 23 | else if (button.color === 'blue') { 24 | button.color = '#3869D4'; 25 | } 26 | } 27 | } 28 | %> 29 | 30 | 31 | 32 | 33 | 34 | Neopolitan Welcome Email 35 | 36 | 37 | 38 | 139 | 147 | 148 | 156 | 157 | 158 | 159 | <% if (locals.goToAction) { %> 160 | 173 | <% } %> 174 | 175 | 176 | 381 | 382 |
177 |
178 | 179 | 180 | 377 | 378 |
181 | 182 | 183 | 196 | 197 |
184 | 185 | <% if (locals.product.logo) { %> 186 | <% if (product.logoHeight) { %> 187 | 188 | <% } else { %> 189 | 190 | <% } %> 191 | <% } else { %> 192 | <%- product.name %> 193 | <% } %> 194 | 195 |
198 | 199 | 200 | <% if (locals.title) { %> 201 | 202 | 207 | 208 | <% } %> 209 | 210 | 234 | 235 | 236 | 283 | 284 |
203 |
204 | <%- title %> 205 |
206 |
211 |
212 | 213 | 214 | 231 | 232 |
215 | <% if (locals.intro) { %> 216 | <% intro.forEach(function (introItem) { -%> 217 |

<%- introItem %>

218 | <% }) -%> 219 | <% } %> 220 | 221 | 222 | <% if (locals.dictionary) { %> 223 |
224 | <% for (item in dictionary) { -%> 225 |
<%- item.charAt(0).toUpperCase() + item.slice(1) %>:
226 |
<%- dictionary[item] %>
227 | <% } -%> 228 |
229 | <% } %> 230 |
233 |
237 | 238 | <% if (locals.table) { %> 239 | <% table.forEach(function (tableItem, i) { -%> 240 |
241 |
242 |

<%- tableItem.title %>

243 | 244 | 245 | 276 | 277 |
246 | 247 | 248 | <% for (var column in tableItem.data[0]) {%> 249 | 259 | <% } %> 260 | 261 | <% for (var i in tableItem.data) {%> 262 | 263 | <% for (var column in tableItem.data[i]) {%> 264 | 271 | <% } %> 272 | 273 | <% } %> 274 |
251 | width="<%= tableItem.columns.customWidth[column] %>" 252 | <% } %> 253 | <% if(tableItem.columns && tableItem.columns.customAlignment && tableItem.columns.customAlignment[column]) { %> 254 | style="text-align:<%= tableItem.columns.customAlignment[column] %>" 255 | <% } %> 256 | > 257 |

<%- column.charAt(0).toUpperCase() + column.slice(1) %>

258 |
266 | style="text-align:<%= tableItem.columns.customAlignment[column] %>" 267 | <% } %> 268 | > 269 | <%- tableItem.data[i][column] %> 270 |
275 |
278 | <% }) %> 279 | <% } %> 280 |
281 |
282 |
285 | 286 | 287 | 353 | 354 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %> 355 | 356 | 368 | 369 | <% } %> 370 | 371 | 374 | 375 |
288 |
289 | 290 | 291 | 310 | 311 | 312 | 349 | 350 |
292 |
293 | 294 | <% if (locals.action.length !== 0) { %> 295 | <% action.forEach(function (actionItem) { -%> 296 | 297 |
298 | 299 | 300 | 306 | 307 |
301 |
302 | <%- actionItem.instructions %> 303 |
304 |
305 |
308 |
309 |
313 | <% actionItem.button.forEach(function (actionButton) { -%> 314 | 319 | 320 | <%- actionButton.text %> 321 | 322 | 326 | 327 | <% }) -%> 328 | <% }) -%> 329 | <% } %> 330 |
331 |
332 | 333 | <% if (locals.outro) { %> 334 | <% outro.forEach(function (outroItem) { -%> 335 | <%- outroItem %> 336 |
337 |
338 | <% }) -%> 339 | <% } %> 340 | 341 | <% if (signature) { %> 342 | <%- signature %>, 343 |
344 | <%- product.name %> 345 | <% } %> 346 | 347 |

348 |
351 |
352 |
357 | <% action.forEach(function (actionItem) { -%> 358 | <% actionItem.button.forEach(function (actionButton) { -%> 359 | <% if (actionButton.fallback != null) { %> 360 |

361 | <%- actionButton.fallback.text %>
362 | <%- actionButton.link %> 363 |

364 | <% } %> 365 | <% }) -%> 366 | <% }) -%> 367 |
372 | <%- product.copyright %> 373 |
376 |
379 |
380 |
383 | 384 | 385 | -------------------------------------------------------------------------------- /themes/neopolitan/index.txt: -------------------------------------------------------------------------------- 1 | <% if (locals.title) { %> 2 | <%- title %> 3 | <% } %> 4 | 5 |
6 |
7 | 8 | <% if (locals.intro) { %> 9 | <% intro.forEach(function (introItem) { -%> 10 | <%- introItem %> 11 |
12 | <% }) -%> 13 | 14 |
15 | <% } %> 16 | 17 | <% if (locals.dictionary) { %> 18 | <% for (item in dictionary) { -%> 19 | <%- item.charAt(0).toUpperCase() + item.slice(1) %>: <%- dictionary[item] %> 20 |
21 | <% } -%> 22 | 23 |
24 | <% } %> 25 | 26 | <% if (locals.action) { %> 27 | <% action.forEach(function (actionItem) { -%> 28 | <%- actionItem.instructions %> 29 |
30 | <% actionItem.button.forEach(function (actionButton) { -%> 31 | <%- actionButton.link %> 32 |
33 | <% }) -%> 34 | 35 |
36 | <% }) -%> 37 | <% } %> 38 | 39 | <% if (locals.outro) { %> 40 | <% outro.forEach(function (outroItem) { -%> 41 | <%- outroItem %> 42 |
43 | <% }) -%> 44 | 45 |
46 | <% } %> 47 | 48 | <% if (signature) { %> 49 | <%- signature %>, 50 |
51 | <%- product.name %> 52 | <% } %> 53 | 54 |
55 |
56 | <%- product.copyright %> 57 | -------------------------------------------------------------------------------- /themes/salted/index.html: -------------------------------------------------------------------------------- 1 | <% 2 | if (locals.action) { 3 | // Make it possible to override action button color (specify fallback color if no color specified) 4 | locals.action.forEach(function(actionItem) { 5 | actionItem.button.forEach(function(button) { 6 | // Make it possible to override action button color (specify fallback color if no color specified) 7 | if (!button.color) { 8 | button.color = '#48CFAD'; 9 | } 10 | 11 | deprecatedColorToHex(button); 12 | }); 13 | }); 14 | 15 | // Convert deprecated red/green/blue action button color to hex since we switched to a hex-based button color 16 | function deprecatedColorToHex(button) { 17 | if (button.color === 'red') { 18 | button.color = '#DC4D2F'; 19 | } 20 | else if (button.color === 'green') { 21 | button.color = '#22BC66'; 22 | } 23 | else if (button.color === 'blue') { 24 | button.color = '#3869D4'; 25 | } 26 | } 27 | } 28 | %> 29 | 30 | 31 | 32 | 48 | 49 | 50 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 256 | 257 |
221 |
222 | 223 | 224 | 225 | 252 | 253 |
254 |
255 |
258 | 259 | 260 | 261 | 262 | 345 | 346 |
263 | 264 | 265 | 342 | 343 |
266 | 267 | 268 | 300 | 301 | 302 | 339 | 340 |
269 | 270 | 271 | <% if (locals.title) { %> 272 | 273 | 276 | 277 | <% } %> 278 | 279 | 297 | 298 |
274 | <%- title %> 275 |
280 | 281 | <% if (locals.intro) { %> 282 | <% intro.forEach(function (introItem) { -%> 283 |

<%- introItem %>

284 | <% }) -%> 285 | <% } %> 286 | 287 | 288 | <% if (locals.dictionary) { %> 289 |
290 | <% for (item in dictionary) { -%> 291 |
<%- item.charAt(0).toUpperCase() + item.slice(1) %>:
292 |
<%- dictionary[item] %>
293 | <% } -%> 294 |
295 | <% } %> 296 |
299 |
303 | 304 | <% if (locals.table) { %> 305 | <% table.forEach(function (tableItem, i) { -%> 306 |
<%- tableItem.title %>
307 | 308 | 309 | <% for (var column in tableItem.data[0]) {%> 310 | 320 | <% } %> 321 | 322 | <% for (var i in tableItem.data) {%> 323 | 324 | <% for (var column in tableItem.data[i]) {%> 325 | 332 | <% } %> 333 | 334 | <% } %> 335 |
312 | width="<%= tableItem.columns.customWidth[column] %>" 313 | <% } %> 314 | <% if(tableItem.columns && tableItem.columns.customAlignment && tableItem.columns.customAlignment[column]) { %> 315 | style="text-align:<%= tableItem.columns.customAlignment[column] %>" 316 | <% } %> 317 | > 318 |

<%- column.charAt(0).toUpperCase() + column.slice(1) %>

319 |
327 | style="text-align:<%= tableItem.columns.customAlignment[column] %>" 328 | <% } %> 329 | > 330 | <%- tableItem.data[i][column] %> 331 |
336 | <% }) %> 337 | <% } %> 338 |
341 |
344 |
347 | 348 | 349 | 350 | 351 | 412 | 413 | 414 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %> 415 | 416 | 435 | 436 | <% } %> 437 | 438 | 439 | 458 | 459 |
352 | 353 | 354 | 409 | 410 |
355 | 356 | 357 | <% if (locals.action.length !== 0) { %> 358 | <% action.forEach(function (actionItem) { -%> 359 | 360 | 370 | 371 | 372 | 388 | 389 | <% }) -%> 390 | <% } %> 391 | 392 | <% if (locals.outro) { %> 393 | 394 | 405 | 406 | <% } %> 407 |
361 | 362 | 363 | 364 | 367 | 368 |
365 |

<%- actionItem.instructions %>

366 |
369 |
373 | 374 | 375 | 376 | 385 | 386 |
377 | 378 | 379 | <% actionItem.button.forEach(function (actionButton) { -%> 380 | 381 | <% }) -%> 382 | 383 |
<%- actionButton.text %> →
384 |
387 |
395 | 396 | 397 | 402 | 403 |
398 | <% outro.forEach(function (outroItem) { -%> 399 |

<%- outroItem %>

400 | <% }) -%> 401 |
404 |
408 |
411 |
417 |
418 | 419 | 420 | 432 | 433 |
421 | <% action.forEach(function (actionItem) { -%> 422 | <% actionItem.button.forEach(function (actionButton) { -%> 423 | <% if (actionButton.fallback != null) { %> 424 |

425 | <%- actionButton.fallback.text %>
426 | <%- actionButton.link %> 427 |

428 | <% } %> 429 | <% }) -%> 430 | <% }) -%> 431 |
434 |
440 | 441 | 442 | 455 | 456 |
443 | 444 | 445 | 446 | 452 | 453 |
447 | 448 | <%- product.copyright %> 449 | 450 |
451 |
454 |
457 |
460 | 461 | 462 | <% if (locals.goToAction) { %> 463 | 476 | <% } %> 477 | 478 | 479 | 480 | -------------------------------------------------------------------------------- /themes/salted/index.txt: -------------------------------------------------------------------------------- 1 | <% if (locals.title) { %> 2 | <%- title %> 3 | <% } %> 4 | 5 |
6 |
7 | 8 | <% if (locals.intro) { %> 9 | <% intro.forEach(function (introItem) { -%> 10 | <%- introItem %> 11 |
12 | <% }) -%> 13 | 14 |
15 | <% } %> 16 | 17 | <% if (locals.dictionary) { %> 18 | <% for (item in dictionary) { -%> 19 | <%- item.charAt(0).toUpperCase() + item.slice(1) %>: <%- dictionary[item] %> 20 |
21 | <% } -%> 22 | 23 |
24 | <% } %> 25 | 26 | <% if (locals.action) { %> 27 | <% action.forEach(function (actionItem) { -%> 28 | <%- actionItem.instructions %> 29 |
30 | <% actionItem.button.forEach(function (actionButton) { -%> 31 | <%- actionButton.link %> 32 |
33 | <% }) -%> 34 | 35 |
36 | <% }) -%> 37 | <% } %> 38 | 39 | <% if (locals.outro) { %> 40 | <% outro.forEach(function (outroItem) { -%> 41 | <%- outroItem %> 42 |
43 | <% }) -%> 44 | 45 |
46 | <% } %> 47 | 48 | <% if (signature) { %> 49 | <%- signature %>, 50 |
51 | <%- product.name %> 52 | <% } %> 53 | 54 |
55 |
56 | <%- product.copyright %> 57 | --------------------------------------------------------------------------------