├── .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 | [](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 |
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 | |
160 | <% } %>
161 |
162 | <% for (var i in table.data) {%>
163 |
164 | <% for (var column in table.data[i]) {%>
165 |
167 | style="text-align:<%= table.columns.customAlignment[column] %>"
168 | <% } %>
169 | >
170 | <%- table.data[i][column] %>
171 | |
172 | <% } %>
173 |
174 | <% } %>
175 |
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 |
248 |
249 |
250 |
251 |
252 |
253 |
254 | <% if (locals.title) { %>
255 |
256 |
257 |
258 | <%- title %>
259 |
260 | |
261 |
262 | <% } %>
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
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 |
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 | |
307 | <% } %>
308 |
309 | <% for (var i in tableItem.data) {%>
310 |
311 | <% for (var column in tableItem.data[i]) {%>
312 |
314 | style="text-align:<%= tableItem.columns.customAlignment[column] %>"
315 | <% } %>
316 | >
317 | <%- tableItem.data[i][column] %>
318 | |
319 | <% } %>
320 |
321 | <% } %>
322 |
323 | <% }) %>
324 | <% } %>
325 | |
326 |
327 |
328 | |
329 |
330 | <% if (locals.action.length !== 0) { %>
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 | <% action.forEach(function (actionItem) { -%>
339 | <%- actionItem.instructions %>
340 |
341 |
342 |
354 |
355 |
366 |
367 |
368 |
369 | <% }) -%>
370 | |
371 |
372 |
373 | |
374 |
375 | <% } %>
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 | <% if (locals.outro) { %>
385 | <% outro.forEach(function (outroItem) { -%>
386 | <%- outroItem %>
387 | <% }) -%>
388 | <% } %>
389 | |
390 |
391 | <% if (signature) { %>
392 |
393 |
394 | <%- signature %>,
395 |
396 | <%- product.name %>
397 | |
398 |
399 | <% } %>
400 |
401 | |
402 |
403 |
404 |
405 |
406 |
407 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %>
408 |
409 |
410 |
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 | |
423 |
424 |
425 | <% } %>
426 |
427 |
428 |
429 |
430 |
431 | <%- product.copyright %>
432 | |
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 |
244 |
245 |
246 |
247 |
248 |
249 | <% if (locals.product.logo) { %>
250 | <% if (product.logoHeight) { %>
251 |
252 | <% } else { %>
253 |
254 | <% } %>
255 | <% } else { %>
256 | <%- product.name %>
257 | <% } %>
258 |
259 | |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | <% if (locals.title) { %>
269 | <%- title %>
270 | <% } %>
271 | <% if (locals.intro) { %>
272 | <% intro.forEach(function (introItem) { -%>
273 | <%- introItem %>
274 | <% }) -%>
275 | <% } %>
276 |
277 |
278 | <% if (locals.dictionary) { %>
279 |
280 | <% for (item in dictionary) { -%>
281 | - <%- item.charAt(0).toUpperCase() + item.slice(1) %>:
282 | - <%- dictionary[item] %>
283 | <% } -%>
284 |
285 | <% } %>
286 |
287 |
288 | <% if (locals.table) { %>
289 | <% table.forEach(function (tableItem, i) { -%>
290 | <%- tableItem.title %>
291 |
292 |
293 |
294 |
295 |
296 | <% for (var column in tableItem.data[0]) {%>
297 |
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 | |
307 | <% } %>
308 |
309 | <% for (var i in tableItem.data) {%>
310 |
311 | <% for (var column in tableItem.data[i]) {%>
312 |
314 | style="text-align:<%= tableItem.columns.customAlignment[column] %>"
315 | <% } %>
316 | >
317 | <%- tableItem.data[i][column] %>
318 | |
319 | <% } %>
320 |
321 | <% } %>
322 |
323 | |
324 |
325 |
326 | <% }) %>
327 | <% } %>
328 |
329 |
330 | <% if (locals.action.length !== 0) { %>
331 | <% action.forEach(function (actionItem) { -%>
332 | <%- actionItem.instructions %>
333 |
345 |
346 |
347 |
348 | <% actionItem.button.forEach(function (actionButton) { -%>
349 |
350 |
351 | <%- actionButton.text %>
352 |
353 | |
354 | <% }) -%>
355 |
356 |
357 |
358 | <% }) -%>
359 | <% } %>
360 |
361 |
362 | <% if (locals.goToAction) { %>
363 |
376 | <% } %>
377 |
378 | <% if (locals.outro) { %>
379 | <% outro.forEach(function (outroItem) { -%>
380 | <%- outroItem %>
381 | <% }) -%>
382 | <% } %>
383 |
384 | <% if (signature) { %>
385 |
386 | <%- signature %>,
387 |
388 | <%- product.name %>
389 |
390 | <% } %>
391 | |
392 |
393 |
394 | |
395 |
396 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %>
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 | <% action.forEach(function (actionItem) { -%>
405 | <% actionItem.button.forEach(function (actionButton) { -%>
406 | <% if (actionButton.fallback != null) { %>
407 |
408 | <%- actionButton.fallback.text %>
409 | <%- actionButton.link %>
410 |
411 | <% } %>
412 | <% }) -%>
413 | <% }) -%>
414 | |
415 |
416 |
417 | |
418 |
419 | <% } %>
420 |
421 |
422 |
431 | |
432 |
433 |
434 | |
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 |
177 |
178 |
179 |
180 |
181 |
198 |
199 |
200 | <% if (locals.title) { %>
201 |
202 |
203 |
204 | <%- title %>
205 |
206 | |
207 |
208 | <% } %>
209 |
210 |
211 |
212 |
213 |
214 |
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 | |
231 |
232 |
233 | |
234 |
235 |
236 |
237 |
238 | <% if (locals.table) { %>
239 | <% table.forEach(function (tableItem, i) { -%>
240 |
241 |
242 | <%- tableItem.title %>
243 |
244 |
245 |
246 |
247 |
248 | <% for (var column in tableItem.data[0]) {%>
249 |
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 | |
259 | <% } %>
260 |
261 | <% for (var i in tableItem.data) {%>
262 |
263 | <% for (var column in tableItem.data[i]) {%>
264 |
266 | style="text-align:<%= tableItem.columns.customAlignment[column] %>"
267 | <% } %>
268 | >
269 | <%- tableItem.data[i][column] %>
270 | |
271 | <% } %>
272 |
273 | <% } %>
274 |
275 | |
276 |
277 |
278 | <% }) %>
279 | <% } %>
280 |
281 |
282 | |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 | <% if (locals.action.length !== 0) { %>
295 | <% action.forEach(function (actionItem) { -%>
296 |
297 |
298 |
299 |
300 |
301 |
302 | <%- actionItem.instructions %>
303 |
304 |
305 | |
306 |
307 |
308 |
309 | |
310 |
311 |
312 |
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 | |
349 |
350 |
351 |
352 | |
353 |
354 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %>
355 |
356 |
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 | |
368 |
369 | <% } %>
370 |
371 |
372 | <%- product.copyright %>
373 | |
374 |
375 |
376 | |
377 |
378 |
379 |
380 | |
381 |
382 |
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 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
248 | |
249 |
250 |
251 | |
252 |
253 |
254 |
255 | |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 | <% if (locals.title) { %>
272 |
273 |
274 | <%- title %>
275 | |
276 |
277 | <% } %>
278 |
279 |
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 | |
297 |
298 |
299 | |
300 |
301 |
302 |
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 |
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 | |
320 | <% } %>
321 |
322 | <% for (var i in tableItem.data) {%>
323 |
324 | <% for (var column in tableItem.data[i]) {%>
325 |
327 | style="text-align:<%= tableItem.columns.customAlignment[column] %>"
328 | <% } %>
329 | >
330 | <%- tableItem.data[i][column] %>
331 | |
332 | <% } %>
333 |
334 | <% } %>
335 |
336 | <% }) %>
337 | <% } %>
338 | |
339 |
340 |
341 | |
342 |
343 |
344 | |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 | <% if (locals.action.length !== 0) { %>
358 | <% action.forEach(function (actionItem) { -%>
359 |
360 |
361 |
362 |
363 |
364 |
365 | <%- actionItem.instructions %>
366 | |
367 |
368 |
369 | |
370 |
371 |
372 |
373 |
374 |
387 | |
388 |
389 | <% }) -%>
390 | <% } %>
391 |
392 | <% if (locals.outro) { %>
393 |
394 |
395 |
396 |
397 |
398 | <% outro.forEach(function (outroItem) { -%>
399 | <%- outroItem %>
400 | <% }) -%>
401 | |
402 |
403 |
404 | |
405 |
406 | <% } %>
407 |
408 | |
409 |
410 |
411 | |
412 |
413 |
414 | <% if (locals.action.length !== 0 && locals.action[0].button[0].fallback) { %>
415 |
416 |
417 |
418 |
419 |
420 |
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 | |
432 |
433 |
434 | |
435 |
436 | <% } %>
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
450 |
451 | |
452 |
453 |
454 | |
455 |
456 |
457 | |
458 |
459 |
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 |
--------------------------------------------------------------------------------