@designsystemsinternational/email 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -213,7 +213,6 @@ const preparePackageFactory =
213
213
  );
214
214
 
215
215
  const hash = hashFromString(`${data}`);
216
- console.log(hash);
217
216
 
218
217
  const filename = alt
219
218
  ? `${formatFilename(alt)}-text-${hash}.${ext}`
@@ -6,6 +6,8 @@ var removeAttributes = require('posthtml-remove-attributes');
6
6
  var matchHelper = require('posthtml-match-helper');
7
7
  var colorConvert = require('color-convert');
8
8
  var unescape = require('lodash.unescape');
9
+ var path = require('node:path');
10
+ var fs = require('node:fs');
9
11
  var compressing = require('compressing');
10
12
  var getStream = require('get-stream');
11
13
  var mailchimpAPI = require('@mailchimp/mailchimp_marketing');
@@ -201,10 +203,16 @@ const BASE64_REGEX = /data:image\/([a-zA-Z]*);base64,(.*)/;
201
203
  */
202
204
  const preparePackageFactory =
203
205
  ({ urlPrefix = '', handlePackage }) =>
204
- async (body, opts = {}) => {
206
+ async (body, _opts = {}) => {
205
207
  let counter = 0;
206
208
  const images = [];
207
209
 
210
+ const opts = Object.assign(
211
+ {},
212
+ { basePath: '', includeLocalImages: false },
213
+ _opts,
214
+ );
215
+
208
216
  const processed = await posthtml()
209
217
  .use((tree) => {
210
218
  tree.match({ tag: 'img' }, (node) => {
@@ -231,6 +239,18 @@ const preparePackageFactory =
231
239
  });
232
240
 
233
241
  counter++;
242
+ } else if (!src.startsWith('http') && opts.includeLocalImages) {
243
+ const filePath = path.join(opts.basePath, src);
244
+
245
+ const filename = src.split('/').pop();
246
+ node.attrs.src = `${urlPrefix}${filename}`;
247
+
248
+ const imgBuffer = fs.readFileSync(filePath);
249
+
250
+ images.push({
251
+ filename,
252
+ buffer: imgBuffer,
253
+ });
234
254
  }
235
255
  return node;
236
256
  });
@@ -513,7 +533,7 @@ const packageToS3 = async (body, opts = {}) => {
513
533
  },
514
534
  });
515
535
 
516
- return await runUpload(body);
536
+ return await runUpload(body, opts);
517
537
  };
518
538
 
519
539
  const sendEmailSes = async (
@@ -211,7 +211,6 @@ const preparePackageFactory =
211
211
  );
212
212
 
213
213
  const hash = hashFromString(`${data}`);
214
- console.log(hash);
215
214
 
216
215
  const filename = alt
217
216
  ? `${formatFilename(alt)}-text-${hash}.${ext}`
package/dist/esm/index.js CHANGED
@@ -4,6 +4,8 @@ import removeAttributes from 'posthtml-remove-attributes';
4
4
  import matchHelper from 'posthtml-match-helper';
5
5
  import colorConvert from 'color-convert';
6
6
  import unescape from 'lodash.unescape';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs';
7
9
  import compressing from 'compressing';
8
10
  import getStream from 'get-stream';
9
11
  import mailchimpAPI from '@mailchimp/mailchimp_marketing';
@@ -199,10 +201,16 @@ const BASE64_REGEX = /data:image\/([a-zA-Z]*);base64,(.*)/;
199
201
  */
200
202
  const preparePackageFactory =
201
203
  ({ urlPrefix = '', handlePackage }) =>
202
- async (body, opts = {}) => {
204
+ async (body, _opts = {}) => {
203
205
  let counter = 0;
204
206
  const images = [];
205
207
 
208
+ const opts = Object.assign(
209
+ {},
210
+ { basePath: '', includeLocalImages: false },
211
+ _opts,
212
+ );
213
+
206
214
  const processed = await posthtml()
207
215
  .use((tree) => {
208
216
  tree.match({ tag: 'img' }, (node) => {
@@ -229,6 +237,18 @@ const preparePackageFactory =
229
237
  });
230
238
 
231
239
  counter++;
240
+ } else if (!src.startsWith('http') && opts.includeLocalImages) {
241
+ const filePath = path.join(opts.basePath, src);
242
+
243
+ const filename = src.split('/').pop();
244
+ node.attrs.src = `${urlPrefix}${filename}`;
245
+
246
+ const imgBuffer = fs.readFileSync(filePath);
247
+
248
+ images.push({
249
+ filename,
250
+ buffer: imgBuffer,
251
+ });
232
252
  }
233
253
  return node;
234
254
  });
@@ -511,7 +531,7 @@ const packageToS3 = async (body, opts = {}) => {
511
531
  },
512
532
  });
513
533
 
514
- return await runUpload(body);
534
+ return await runUpload(body, opts);
515
535
  };
516
536
 
517
537
  const sendEmailSes = async (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designsystemsinternational/email",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Make working with email more enjoyable (if possible)",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -54,7 +54,7 @@
54
54
  "@mailchimp/mailchimp_marketing": "^3.0.80",
55
55
  "aws-sdk": "^2.1274.0",
56
56
  "color-convert": "^2.0.1",
57
- "compressing": "^1.6.2",
57
+ "compressing": "^1.10.4",
58
58
  "cosmiconfig": "^8.0.0",
59
59
  "file-saver": "^2.0.5",
60
60
  "get-stream": "^6.0.1",
@@ -31,7 +31,6 @@ export const preparePackageFactory =
31
31
  );
32
32
 
33
33
  const hash = hashFromString(`${data}`);
34
- console.log(hash);
35
34
 
36
35
  const filename = alt
37
36
  ? `${formatFilename(alt)}-text-${hash}.${ext}`
@@ -1,4 +1,7 @@
1
1
  // NODE.JS packacking factory
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+
2
5
  import posthtml from 'posthtml';
3
6
 
4
7
  import { BASE64_REGEX } from './constants.js';
@@ -15,10 +18,16 @@ import { formatFilename, hashFromString } from './util/string.js';
15
18
  */
16
19
  export const preparePackageFactory =
17
20
  ({ urlPrefix = '', handlePackage }) =>
18
- async (body, opts = {}) => {
21
+ async (body, _opts = {}) => {
19
22
  let counter = 0;
20
23
  const images = [];
21
24
 
25
+ const opts = Object.assign(
26
+ {},
27
+ { basePath: '', includeLocalImages: false },
28
+ _opts,
29
+ );
30
+
22
31
  const processed = await posthtml()
23
32
  .use((tree) => {
24
33
  tree.match({ tag: 'img' }, (node) => {
@@ -45,6 +54,18 @@ export const preparePackageFactory =
45
54
  });
46
55
 
47
56
  counter++;
57
+ } else if (!src.startsWith('http') && opts.includeLocalImages) {
58
+ const filePath = path.join(opts.basePath, src);
59
+
60
+ const filename = src.split('/').pop();
61
+ node.attrs.src = `${urlPrefix}${filename}`;
62
+
63
+ const imgBuffer = fs.readFileSync(filePath);
64
+
65
+ images.push({
66
+ filename,
67
+ buffer: imgBuffer,
68
+ });
48
69
  }
49
70
  return node;
50
71
  });
@@ -54,7 +54,7 @@ const packageToS3 = async (body, opts = {}) => {
54
54
  },
55
55
  });
56
56
 
57
- return await runUpload(body);
57
+ return await runUpload(body, opts);
58
58
  };
59
59
 
60
60
  export default packageToS3;
package/dist/index.cjs DELETED
@@ -1,526 +0,0 @@
1
- 'use strict';
2
-
3
- var posthtml = require('posthtml');
4
- var removeTags = require('posthtml-remove-tags');
5
- var removeAttributes = require('posthtml-remove-attributes');
6
- var matchHelper = require('posthtml-match-helper');
7
- var colorConvert = require('color-convert');
8
- var unescape = require('lodash.unescape');
9
- var compressing = require('compressing');
10
- var getStream = require('get-stream');
11
- var AWS = require('aws-sdk');
12
- var nodemailer = require('nodemailer');
13
-
14
- /**
15
- * Generates a deterministic random string based on the input string.
16
- *
17
- * @param {string} str Input string
18
- * @return {string} hash of str
19
- */
20
- const hashFromString = (str) => {
21
- let hash = 0;
22
- for (let i = 0; i < str.length; i++) {
23
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
24
- }
25
- return hash.toString();
26
- };
27
-
28
- /**
29
- * Formats a string to a filename by turning it into kebab case
30
- *
31
- * @param {string} text
32
- * @return {string} formatted filename
33
- */
34
- const formatFilename = (text) => {
35
- return text
36
- .toLowerCase()
37
- .replace(/[^a-z0-9-]+/g, ' ')
38
- .trim()
39
- .replace(/\s+/g, '-');
40
- };
41
-
42
- /**
43
- * Converts all rgb defined colors with their hex equivalent
44
- * as some email clients only support hex color format.
45
- */
46
- const convertRgbToHex = () => {
47
- return (tree) => {
48
- tree.match(matchHelper('[style]'), (node) => {
49
- let styleBlock = node.attrs.style;
50
- styleBlock = styleBlock.replace(
51
- /rgb\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3}\)/g,
52
- (match) => {
53
- const values = match
54
- .replace(/[rgb() ]/g, '')
55
- .split(',')
56
- .map((n) => parseInt(n));
57
- return '#' + colorConvert.rgb.hex(values);
58
- },
59
- );
60
- node.attrs.style = styleBlock;
61
- return node;
62
- });
63
-
64
- return tree;
65
- };
66
- };
67
-
68
- /**
69
- * Prepares HTML by only returning the body tag, to make sure
70
- */
71
- const onlyReturnBody = () => {
72
- return (tree) => {
73
- tree.match({ tag: 'body' }, (node) => {
74
- return node.content;
75
- });
76
-
77
- return tree;
78
- };
79
- };
80
-
81
- /**
82
- * Rewrites the markup by turning all conditional comments returned from the
83
- * <ConditionalComment> component into true conditional comments that can be
84
- * picked up by Outlook.
85
- */
86
- const injectConditionalComments = () => {
87
- return (tree) => {
88
- tree.match(matchHelper('[data-conditional-comment]'), (node) => {
89
- const comment = node.attrs['data-conditional-comment'];
90
-
91
- // Remove HTML comment delimiters
92
- const cleanedComment = unescape(comment).replace(/<!--|-->/g, '');
93
-
94
- // Remove Line breaks from comment
95
- const cleanedCommentLines = cleanedComment.split('\n').join('');
96
-
97
- node = `<!--${cleanedCommentLines}-->`;
98
- return node;
99
- });
100
- return tree;
101
- };
102
- };
103
-
104
- /**
105
- * Rewrites the markup by adding all styles defined in the data-mso-style
106
- * attribute to the <style> tag. This is needed because the mso styles are
107
- * non-standard CSS, so they cannot be added to React's style tag.
108
- */
109
- const injectMsoStyles = () => {
110
- return (tree) => {
111
- tree.match(matchHelper('[data-mso-style]'), (node) => {
112
- if (node.attrs?.style == null) node.attrs.style = '';
113
- node.attrs.style += node.attrs['data-mso-style'];
114
- node.attrs['data-mso-style'] = null;
115
- return node;
116
- });
117
- return tree;
118
- };
119
- };
120
-
121
- /**
122
- * Collects all inline styles defined in the components, de-dedupes them and
123
- * moves them to a single unified style tag in the head of the document.
124
- */
125
- const moveInlineStylesToHead = () => {
126
- let styles = {};
127
-
128
- return (tree) => {
129
- tree.match({ tag: 'style' }, (node) => {
130
- if (!node.content) return '';
131
- const styleBlock = node.content[0].trim();
132
- const hash = hashFromString(styleBlock);
133
-
134
- if (styleBlock) {
135
- styles[hash] = styleBlock;
136
- }
137
-
138
- // Remove the inline style node
139
- return '';
140
- });
141
-
142
- tree.match({ tag: 'head' }, (node) => {
143
- node.content.push({
144
- tag: 'style',
145
- content: Object.values(styles).join(''),
146
- });
147
-
148
- return node;
149
- });
150
-
151
- return tree;
152
- };
153
- };
154
-
155
- const prepareHtmlFactory = (plugins) => {
156
- const htmlProcessor = posthtml();
157
-
158
- plugins.forEach((plugin) => {
159
- htmlProcessor.use(plugin);
160
- });
161
-
162
- return async (body, template = (str) => str) => {
163
- const content = template(body);
164
-
165
- const processed = await htmlProcessor.process(content);
166
-
167
- return processed.html;
168
- };
169
- };
170
-
171
- /**
172
- * This default pipeline removes all script and link tags from the HTML,
173
- * moves all inline styles to the head and wraps everything in the email template.
174
- *
175
- * @param {string} body - the html body
176
- * @param {function} template - the template function, defaults to no template
177
- */
178
- const defaultPipeline = prepareHtmlFactory([
179
- onlyReturnBody(),
180
- removeTags({ tags: ['script', 'link'] }),
181
- convertRgbToHex(),
182
- moveInlineStylesToHead(),
183
- injectMsoStyles(),
184
- injectConditionalComments(),
185
- removeAttributes(['data-reactid', 'data-reactroot']),
186
- ]);
187
-
188
- const BASE64_REGEX = /data:image\/([a-zA-Z]*);base64,(.*)/;
189
-
190
- // NODE.JS packacking factory
191
-
192
- /**
193
- * Turns base64 inlined images into image buffers and updates their src
194
- * attribute accordingly. Returns an object containing the updated html and
195
- * an array of image buffers. Different adapters can then hook into this to
196
- * process the email into a ZIP archive or pushing it to an API.
197
- *
198
- * @param {string} body - the prepared html body
199
- * @return {object} the processed html and buffers for all images
200
- */
201
- const preparePackageFactory =
202
- ({ urlPrefix = '', handlePackage }) =>
203
- async (body, opts = {}) => {
204
- let counter = 0;
205
- const images = [];
206
-
207
- const processed = await posthtml()
208
- .use((tree) => {
209
- tree.match({ tag: 'img' }, (node) => {
210
- const { src, alt } = node.attrs;
211
-
212
- if (BASE64_REGEX.test(src)) {
213
- const [_, ext, data] = src.match(
214
- /data:image\/([a-zA-Z]*);base64,(.*)/,
215
- );
216
-
217
- const hash = Math.random().toString(36).substring(2, 7);
218
-
219
- const filename = alt
220
- ? `${formatFilename(alt)}-text-${hash}.${ext}`
221
- : `image-${counter}.${ext}`;
222
-
223
- node.attrs.src = `${urlPrefix}${filename}`;
224
-
225
- const imgBuffer = Buffer.from(data, 'base64');
226
-
227
- images.push({
228
- filename,
229
- buffer: imgBuffer,
230
- });
231
-
232
- counter++;
233
- }
234
- return node;
235
- });
236
-
237
- return tree;
238
- })
239
- .process(body);
240
-
241
- return await handlePackage(
242
- {
243
- html: processed.html,
244
- images,
245
- },
246
- opts,
247
- );
248
- };
249
-
250
- const makeDefaultTemplate =
251
- ({ lang = 'en-US', dir = 'ltr' } = {}) =>
252
- (body) =>
253
- `<!doctype html>
254
- <html
255
- lang="${lang}"
256
- xml:lang="${lang}"
257
- dir="${dir}"
258
- xmlns="http://www.w3.org/1999/xhtml"
259
- xmlns:v="urn:schemas-microsoft-com:vml"
260
- xmlns:o="urn:schemas-microsoft-com:office:office"
261
- >
262
- <head>
263
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
264
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
265
- <title>Chief</title>
266
- <!--[if mso]>
267
- <style type="text/css">
268
- table {
269
- border-collapse: collapse;
270
- border: 0;
271
- border-spacing: 0;
272
- margin: 0;
273
- mso-table-lspace: 0pt !important;
274
- mso-table-rspace: 0pt !important;
275
- }
276
-
277
- img {
278
- display: block !important;
279
- }
280
-
281
- div, td {padding:0;}
282
-
283
- div {margin:0 !important;}
284
-
285
- .notMso {
286
- display: none;
287
- mso-hide: all;
288
- }
289
- </style>
290
- <noscript>
291
- <xml>
292
- <o:OfficeDocumentSettings>
293
- <o:PixelsPerInch>96</o:PixelsPerInch>
294
- </o:OfficeDocumentSettings>
295
- </xml>
296
- </noscript>
297
- <![endif]-->
298
- <meta name="x-apple-disable-message-reformatting" />
299
- <meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
300
- <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
301
- <meta name="viewport" content="width=device-width" />
302
- <meta name="color-scheme" content="light" />
303
- <meta name="supported-color-schemes" content="light" />
304
-
305
- <meta name="format-detection" content="telephone=no">
306
- <meta name="format-detection" content="date=no">
307
- <meta name="format-detection" content="address=no">
308
- <meta name="format-detection" content="email=no">
309
-
310
- <style>
311
- a[x-apple-data-detectors] {
312
- color: inherit !important;
313
-
314
- font-size: inherit !important;
315
- font-family: inherit !important;
316
- font-weight: inherit !important;
317
- line-height: inherit !important;
318
- text-decoration: inherit !important;
319
- }
320
-
321
- #MessageViewBody a:not([style]) {
322
- color: inherit !important;
323
- font-size: inherit !important;
324
- font-family: inherit !important;
325
- font-weight: inherit !important;
326
- line-height: inherit !important;
327
- text-decoration: inherit !important;
328
- }
329
- </style>
330
- </head>
331
- <body class="body" style="padding:0;">
332
- ${body}
333
- </body>
334
- </html>`;
335
-
336
- const defaultTemplate = makeDefaultTemplate();
337
-
338
- /**
339
- * Turns an HTML email into a ZIP package and saves that ZIP file to disk.
340
- *
341
- * @param {string} body - the prepared html body
342
- * @return {Buffer} the zip file buffer
343
- */
344
- const packageToZip = preparePackageFactory({
345
- urlPrefix: '',
346
- handlePackage: async ({ html, images }) => {
347
- const zipStream = new compressing.zip.Stream();
348
- const htmlEntry = Buffer.from(html, 'utf8');
349
- zipStream.addEntry(htmlEntry, { relativePath: `./index.html` });
350
-
351
- images.forEach(({ filename, buffer }) => {
352
- zipStream.addEntry(buffer, { relativePath: `./${filename}` });
353
- });
354
-
355
- const zipBuffer = await getStream.buffer(zipStream);
356
- return zipBuffer;
357
- },
358
- });
359
-
360
- /**
361
- * Extracts the images from the HTML and publishes the package to S3
362
- * Returns the HTML with all image URLs updated to point to S3
363
- *
364
- * @param {string} body - the prepared html body
365
- * @param {object} options - the options object containing { awsAccessKeyId, awsSecretAccessKey, awsBucket, url, prefix }
366
- * @return {string} HTML of the mail with all image URLs updated to point to S3
367
- */
368
- const packageToS3 = async (body, opts = {}) => {
369
- const {
370
- awsAccessKeyId,
371
- awsSecretAccessKey,
372
- awsBucket,
373
- url,
374
- prefix = '',
375
- } = opts;
376
-
377
- if (!awsAccessKeyId) throw new Error('awsAccessKeyId is required');
378
- if (!awsSecretAccessKey) throw new Error('awsSecretAccessKey is required');
379
- if (!awsBucket) throw new Error('awsBucket is required');
380
-
381
- const s3 = new AWS.S3({
382
- accessKeyId: awsAccessKeyId,
383
- secretAccessKey: awsSecretAccessKey,
384
- });
385
-
386
- const uploadFileToS3 = async (buffer, filename) => {
387
- await s3
388
- .upload({
389
- Bucket: awsBucket,
390
- Key: `${prefix}${filename}`,
391
- Body: buffer,
392
- ACL: 'public-read',
393
- CacheControl: 'no-store',
394
- })
395
- .promise();
396
- };
397
-
398
- const runUpload = preparePackageFactory({
399
- urlPrefix: `${url}${prefix}`,
400
- handlePackage: async ({ html, images }) => {
401
- const htmlBuffer = Buffer.from(html, 'utf8');
402
- await uploadFileToS3(htmlBuffer, 'index.html');
403
-
404
- for (const image of images) {
405
- await uploadFileToS3(image.buffer, image.filename);
406
- }
407
-
408
- return html;
409
- },
410
- });
411
-
412
- return await runUpload(body);
413
- };
414
-
415
- const sendEmailSes = async (
416
- { fromName, fromEmail, toName, toEmail, subject, plainBody, htmlBody },
417
- opts = {},
418
- ) => {
419
- const { awsAccessKeyId, awsSecretAccessKey } = opts;
420
-
421
- if (!awsAccessKeyId) throw new Error('awsAccessKeyId is required');
422
- if (!awsSecretAccessKey) throw new Error('awsSecretAccessKey is required');
423
-
424
- const fromAddress = fromName ? `"${fromName}" <${fromEmail}>` : fromEmail;
425
- const toAddress = toName ? `"${toName}" <${toEmail}>` : toEmail;
426
-
427
- const ses = new AWS.SES({
428
- accessKeyId: awsAccessKeyId,
429
- secretAccessKey: awsSecretAccessKey,
430
- region: 'us-east-1',
431
- httpOptions: { timeout: 5000 },
432
- });
433
-
434
- const emailRes = await ses
435
- .sendEmail({
436
- Source: fromAddress,
437
- Destination: {
438
- ToAddresses: [toAddress],
439
- },
440
- Message: {
441
- Subject: {
442
- Charset: 'UTF-8',
443
- Data: subject,
444
- },
445
- Body: {
446
- Html: {
447
- Charset: 'UTF-8',
448
- Data: htmlBody,
449
- },
450
- Text: {
451
- Charset: 'UTF-8',
452
- Data: plainBody,
453
- },
454
- },
455
- },
456
- })
457
- .promise();
458
- if (!emailRes.MessageId) {
459
- throw new Error('Failed to send email');
460
- } else {
461
- return true;
462
- }
463
- };
464
-
465
- /**
466
- * Turns nodemailer's callback style API into a promise
467
- */
468
- const performSend = async (transporter, message) => {
469
- return new Promise((resolve, reject) => {
470
- transporter.sendMail(message, (error, info) => {
471
- if (error) {
472
- reject(error);
473
- } else {
474
- resolve(info);
475
- }
476
- });
477
- });
478
- };
479
-
480
- /**
481
- * Sends an email via SMTP
482
- *
483
- * @param {object} email - The email object { fromName, fromEmail, toName, toEmail, subject, plainBody, htmlBody }
484
- * @param {object} options - The options object { smtpHost, smtpPort, smtpUser, smtpPass, useTLS }
485
- */
486
- const sendEmailSMTP = async (
487
- { fromName, fromEmail, toName, toEmail, subject, plainBody, htmlBody },
488
- { smtpHost, smtpPort = 25, smtpUser, smtpPass, useTLS = true } = {},
489
- ) => {
490
- if (!smtpHost) throw new Error('SMTP host is required');
491
- if (!smtpUser) throw new Error('SMTP user is required');
492
- if (!smtpPass) throw new Error('SMTP pass is required');
493
-
494
- const fromAddress = fromName ? `"${fromName}" <${fromEmail}>` : fromEmail;
495
- const toAddress = toName ? `"${toName}" <${toEmail}>` : toEmail;
496
-
497
- const transporter = nodemailer.createTransport({
498
- host: smtpHost,
499
- port: smtpPort,
500
- secure: useTLS,
501
- auth: {
502
- user: smtpUser,
503
- pass: smtpPass,
504
- },
505
- });
506
-
507
- const message = {
508
- from: fromAddress,
509
- to: toAddress,
510
- subject,
511
- text: plainBody,
512
- html: htmlBody,
513
- };
514
-
515
- await performSend(transporter, message);
516
- };
517
-
518
- exports.defaultTemplate = defaultTemplate;
519
- exports.makeDefaultTemplate = makeDefaultTemplate;
520
- exports.packageToS3 = packageToS3;
521
- exports.packageToZip = packageToZip;
522
- exports.prepareHtml = defaultPipeline;
523
- exports.prepareHtmlFactory = prepareHtmlFactory;
524
- exports.preparePackageFactory = preparePackageFactory;
525
- exports.sendEmailSMTP = sendEmailSMTP;
526
- exports.sendEmailSes = sendEmailSes;
package/dist/index.js DELETED
@@ -1,361 +0,0 @@
1
- import posthtml from 'posthtml';
2
- import removeTags from 'posthtml-remove-tags';
3
- import removeAttributes from 'posthtml-remove-attributes';
4
- import matchHelper from 'posthtml-match-helper';
5
- import colorConvert from 'color-convert';
6
- import unescape from 'lodash.unescape';
7
- import JSZip from 'jszip';
8
- import { saveAs } from 'file-saver';
9
-
10
- /**
11
- * Generates a deterministic random string based on the input string.
12
- *
13
- * @param {string} str Input string
14
- * @return {string} hash of str
15
- */
16
- const hashFromString = (str) => {
17
- let hash = 0;
18
- for (let i = 0; i < str.length; i++) {
19
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
20
- }
21
- return hash.toString();
22
- };
23
-
24
- /**
25
- * Formats a string to a filename by turning it into kebab case
26
- *
27
- * @param {string} text
28
- * @return {string} formatted filename
29
- */
30
- const formatFilename = (text) => {
31
- return text
32
- .toLowerCase()
33
- .replace(/[^a-z0-9-]+/g, ' ')
34
- .trim()
35
- .replace(/\s+/g, '-');
36
- };
37
-
38
- /**
39
- * Converts all rgb defined colors with their hex equivalent
40
- * as some email clients only support hex color format.
41
- */
42
- const convertRgbToHex = () => {
43
- return (tree) => {
44
- tree.match(matchHelper('[style]'), (node) => {
45
- let styleBlock = node.attrs.style;
46
- styleBlock = styleBlock.replace(
47
- /rgb\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3}\)/g,
48
- (match) => {
49
- const values = match
50
- .replace(/[rgb() ]/g, '')
51
- .split(',')
52
- .map((n) => parseInt(n));
53
- return '#' + colorConvert.rgb.hex(values);
54
- },
55
- );
56
- node.attrs.style = styleBlock;
57
- return node;
58
- });
59
-
60
- return tree;
61
- };
62
- };
63
-
64
- /**
65
- * Prepares HTML by only returning the body tag, to make sure
66
- */
67
- const onlyReturnBody = () => {
68
- return (tree) => {
69
- tree.match({ tag: 'body' }, (node) => {
70
- return node.content;
71
- });
72
-
73
- return tree;
74
- };
75
- };
76
-
77
- /**
78
- * Rewrites the markup by turning all conditional comments returned from the
79
- * <ConditionalComment> component into true conditional comments that can be
80
- * picked up by Outlook.
81
- */
82
- const injectConditionalComments = () => {
83
- return (tree) => {
84
- tree.match(matchHelper('[data-conditional-comment]'), (node) => {
85
- const comment = node.attrs['data-conditional-comment'];
86
-
87
- // Remove HTML comment delimiters
88
- const cleanedComment = unescape(comment).replace(/<!--|-->/g, '');
89
-
90
- // Remove Line breaks from comment
91
- const cleanedCommentLines = cleanedComment.split('\n').join('');
92
-
93
- node = `<!--${cleanedCommentLines}-->`;
94
- return node;
95
- });
96
- return tree;
97
- };
98
- };
99
-
100
- /**
101
- * Rewrites the markup by adding all styles defined in the data-mso-style
102
- * attribute to the <style> tag. This is needed because the mso styles are
103
- * non-standard CSS, so they cannot be added to React's style tag.
104
- */
105
- const injectMsoStyles = () => {
106
- return (tree) => {
107
- tree.match(matchHelper('[data-mso-style]'), (node) => {
108
- if (node.attrs?.style == null) node.attrs.style = '';
109
- node.attrs.style += node.attrs['data-mso-style'];
110
- node.attrs['data-mso-style'] = null;
111
- return node;
112
- });
113
- return tree;
114
- };
115
- };
116
-
117
- /**
118
- * Collects all inline styles defined in the components, de-dedupes them and
119
- * moves them to a single unified style tag in the head of the document.
120
- */
121
- const moveInlineStylesToHead = () => {
122
- let styles = {};
123
-
124
- return (tree) => {
125
- tree.match({ tag: 'style' }, (node) => {
126
- if (!node.content) return '';
127
- const styleBlock = node.content[0].trim();
128
- const hash = hashFromString(styleBlock);
129
-
130
- if (styleBlock) {
131
- styles[hash] = styleBlock;
132
- }
133
-
134
- // Remove the inline style node
135
- return '';
136
- });
137
-
138
- tree.match({ tag: 'head' }, (node) => {
139
- node.content.push({
140
- tag: 'style',
141
- content: Object.values(styles).join(''),
142
- });
143
-
144
- return node;
145
- });
146
-
147
- return tree;
148
- };
149
- };
150
-
151
- const prepareHtmlFactory = (plugins) => {
152
- const htmlProcessor = posthtml();
153
-
154
- plugins.forEach((plugin) => {
155
- htmlProcessor.use(plugin);
156
- });
157
-
158
- return async (body, template = (str) => str) => {
159
- const content = template(body);
160
-
161
- const processed = await htmlProcessor.process(content);
162
-
163
- return processed.html;
164
- };
165
- };
166
-
167
- /**
168
- * This default pipeline removes all script and link tags from the HTML,
169
- * moves all inline styles to the head and wraps everything in the email template.
170
- *
171
- * @param {string} body - the html body
172
- * @param {function} template - the template function, defaults to no template
173
- */
174
- const defaultPipeline = prepareHtmlFactory([
175
- onlyReturnBody(),
176
- removeTags({ tags: ['script', 'link'] }),
177
- convertRgbToHex(),
178
- moveInlineStylesToHead(),
179
- injectMsoStyles(),
180
- injectConditionalComments(),
181
- removeAttributes(['data-reactid', 'data-reactroot']),
182
- ]);
183
-
184
- const BASE64_REGEX = /data:image\/([a-zA-Z]*);base64,(.*)/;
185
-
186
- // Browser packacking factory
187
-
188
- /**
189
- * Turns base64 inlined images into image buffers and updates their src
190
- * attribute accordingly. Returns an object containing the updated html and
191
- * an array of image buffers. Different adapters can then hook into this to
192
- * process the email into a ZIP archive or pushing it to an API.
193
- *
194
- * @param {string} body - the prepared html body
195
- * @return {object} the processed html and base64 strings for all images
196
- */
197
- const preparePackageFactory =
198
- ({ urlPrefix = '', handlePackage }) =>
199
- async (body, opts = {}) => {
200
- let counter = 0;
201
- const images = [];
202
-
203
- const processed = await posthtml()
204
- .use((tree) => {
205
- tree.match({ tag: 'img' }, (node) => {
206
- const { src, alt } = node.attrs;
207
-
208
- if (BASE64_REGEX.test(src)) {
209
- const [_, ext, data] = src.match(
210
- /data:image\/([a-zA-Z]*);base64,(.*)/,
211
- );
212
-
213
- const hash = Math.random().toString(36).substring(2, 7);
214
-
215
- const filename = alt
216
- ? `${formatFilename(alt)}-text-${hash}.${ext}`
217
- : `image-${counter}.${ext}`;
218
-
219
- node.attrs.src = `${urlPrefix}${filename}`;
220
-
221
- images.push({
222
- filename,
223
- buffer: data,
224
- });
225
-
226
- counter++;
227
- }
228
- return node;
229
- });
230
-
231
- return tree;
232
- })
233
- .process(body);
234
-
235
- return await handlePackage(
236
- {
237
- html: processed.html,
238
- images,
239
- },
240
- opts,
241
- );
242
- };
243
-
244
- const makeDefaultTemplate =
245
- ({ lang = 'en-US', dir = 'ltr' } = {}) =>
246
- (body) =>
247
- `<!doctype html>
248
- <html
249
- lang="${lang}"
250
- xml:lang="${lang}"
251
- dir="${dir}"
252
- xmlns="http://www.w3.org/1999/xhtml"
253
- xmlns:v="urn:schemas-microsoft-com:vml"
254
- xmlns:o="urn:schemas-microsoft-com:office:office"
255
- >
256
- <head>
257
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
258
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
259
- <title>Chief</title>
260
- <!--[if mso]>
261
- <style type="text/css">
262
- table {
263
- border-collapse: collapse;
264
- border: 0;
265
- border-spacing: 0;
266
- margin: 0;
267
- mso-table-lspace: 0pt !important;
268
- mso-table-rspace: 0pt !important;
269
- }
270
-
271
- img {
272
- display: block !important;
273
- }
274
-
275
- div, td {padding:0;}
276
-
277
- div {margin:0 !important;}
278
-
279
- .notMso {
280
- display: none;
281
- mso-hide: all;
282
- }
283
- </style>
284
- <noscript>
285
- <xml>
286
- <o:OfficeDocumentSettings>
287
- <o:PixelsPerInch>96</o:PixelsPerInch>
288
- </o:OfficeDocumentSettings>
289
- </xml>
290
- </noscript>
291
- <![endif]-->
292
- <meta name="x-apple-disable-message-reformatting" />
293
- <meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
294
- <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
295
- <meta name="viewport" content="width=device-width" />
296
- <meta name="color-scheme" content="light" />
297
- <meta name="supported-color-schemes" content="light" />
298
-
299
- <meta name="format-detection" content="telephone=no">
300
- <meta name="format-detection" content="date=no">
301
- <meta name="format-detection" content="address=no">
302
- <meta name="format-detection" content="email=no">
303
-
304
- <style>
305
- a[x-apple-data-detectors] {
306
- color: inherit !important;
307
-
308
- font-size: inherit !important;
309
- font-family: inherit !important;
310
- font-weight: inherit !important;
311
- line-height: inherit !important;
312
- text-decoration: inherit !important;
313
- }
314
-
315
- #MessageViewBody a:not([style]) {
316
- color: inherit !important;
317
- font-size: inherit !important;
318
- font-family: inherit !important;
319
- font-weight: inherit !important;
320
- line-height: inherit !important;
321
- text-decoration: inherit !important;
322
- }
323
- </style>
324
- </head>
325
- <body class="body" style="padding:0;">
326
- ${body}
327
- </body>
328
- </html>`;
329
-
330
- const defaultTemplate = makeDefaultTemplate();
331
-
332
- const defaultConfig = {
333
- name: 'email',
334
- };
335
-
336
- /**
337
- * @param {string} body - the prepared html body
338
- * @return {function} A function that can be called to download the email as a ZIP archive
339
- */
340
- const packageToZip = preparePackageFactory({
341
- urlPrefix: '',
342
- handlePackage: ({ html, images }, opts) => {
343
- const config = { ...defaultConfig, ...opts };
344
-
345
- const zip = new JSZip();
346
- const files = zip.folder(config.name);
347
-
348
- files.file('index.html', html);
349
- images.forEach(({ filename, buffer }) => {
350
- files.file(filename, buffer, { base64: true });
351
- });
352
-
353
- return async () => {
354
- files.generateAsync({ type: 'blob' }).then((content) => {
355
- saveAs(content, `${formatFilename(config.name)}.zip`);
356
- });
357
- };
358
- },
359
- });
360
-
361
- export { defaultTemplate, makeDefaultTemplate, packageToZip, defaultPipeline as prepareHtml, prepareHtmlFactory, preparePackageFactory };
Binary file