@designsystemsinternational/email 0.0.2 → 0.0.3
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.
- package/bin/commands/index.js +5 -0
- package/bin/commands/mailchimp.js +57 -0
- package/bin/commands/s3.js +57 -0
- package/bin/commands/scaffold.js +22 -0
- package/bin/commands/send.js +69 -0
- package/bin/commands/zip.js +39 -0
- package/bin/defaultConfig.js +39 -0
- package/dist/cjs/browser.cjs +368 -0
- package/dist/cjs/index.cjs +631 -0
- package/dist/esm/browser.js +361 -0
- package/dist/esm/index.js +620 -0
- package/dist/index.cjs +526 -0
- package/dist/index.js +361 -0
- package/package.json +23 -5
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { prepareHtml, packageToMailchimp } from '../../src/index.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
const mailchimpCommand = (config) => async (src, opts) => {
|
|
8
|
+
const inFile = path.join(process.cwd(), src);
|
|
9
|
+
|
|
10
|
+
const { mailchimpAPIKey, mailchimpServerPrefix } = config;
|
|
11
|
+
|
|
12
|
+
if (!opts.id) {
|
|
13
|
+
console.error('No campaign ID provided');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!mailchimpAPIKey) {
|
|
18
|
+
console.error('No Mailchimp API key found in config');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(inFile)) {
|
|
23
|
+
console.error(`File ${inFile} does not exist`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const campaignId = opts.id;
|
|
28
|
+
|
|
29
|
+
const input = fs.readFileSync(inFile, 'utf-8');
|
|
30
|
+
let prepared;
|
|
31
|
+
|
|
32
|
+
if (!opts['skip-preparation']) {
|
|
33
|
+
const prepareSpinner = ora('Preparing HTML').start();
|
|
34
|
+
prepared = await prepareHtml(input, config.template);
|
|
35
|
+
prepareSpinner.succeed();
|
|
36
|
+
} else {
|
|
37
|
+
prepared = input;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const publishSpinner = ora(`Publishing to mailchimp`).start();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const mailchimpReturn = await packageToMailchimp(prepared, {
|
|
44
|
+
mailchimpId: campaignId,
|
|
45
|
+
mailchimpAPIKey,
|
|
46
|
+
mailchimpServerPrefix,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
publishSpinner.succeed(`Published to campaign ${mailchimpReturn}`);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
publishSpinner.fail();
|
|
52
|
+
console.error(e);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default mailchimpCommand;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { prepareHtml, packageToS3 } from '../../src/index.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
const panic = (msg) => {
|
|
8
|
+
console.error(msg);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const s3Command = (config) => async (src, dest, opts) => {
|
|
13
|
+
const inFile = path.join(process.cwd(), src);
|
|
14
|
+
const outFile = path.join(process.cwd(), dest);
|
|
15
|
+
|
|
16
|
+
if (!config.awsAccessKeyId) panic('Missing AWS Access Key ID');
|
|
17
|
+
if (!config.awsSecretAccessKey) panic('Missing AWS Secret Access Key');
|
|
18
|
+
if (!config.awsBucket) panic('Missing AWS Bucket');
|
|
19
|
+
if (!config.awsRegion) panic('Missing AWS Region');
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(inFile)) {
|
|
22
|
+
console.error(`File ${inFile} does not exist`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (fs.existsSync(outFile) && !opts.force) {
|
|
27
|
+
console.error(`File ${outFile} already exists. Pass --force to overwrite.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const input = fs.readFileSync(inFile, 'utf-8');
|
|
32
|
+
let prepared;
|
|
33
|
+
|
|
34
|
+
if (!opts['skip-preparation']) {
|
|
35
|
+
const prepareSpinner = ora('Preparing HTML').start();
|
|
36
|
+
prepared = await prepareHtml(input, config.template);
|
|
37
|
+
prepareSpinner.succeed();
|
|
38
|
+
} else {
|
|
39
|
+
prepared = input;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const url = `https://${config.awsBucket}.s3.${config.awsRegion}.amazonaws.com/`;
|
|
43
|
+
const s3Spinner = ora(`Pushing to S3`).start();
|
|
44
|
+
|
|
45
|
+
const result = await packageToS3(prepared, {
|
|
46
|
+
awsAccessKeyId: config.awsAccessKeyId,
|
|
47
|
+
awsSecretAccessKey: config.awsSecretAccessKey,
|
|
48
|
+
awsBucket: config.awsBucket,
|
|
49
|
+
url,
|
|
50
|
+
prefix: opts.folder ?? '',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(outFile, result);
|
|
54
|
+
s3Spinner.succeed(`Pushed to ${url}`);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default s3Command;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { defaultConfigTemplate } from '../defaultConfig.js';
|
|
5
|
+
|
|
6
|
+
const scaffoldCommand = () => {
|
|
7
|
+
const resolveFileExtension = (rootPath) => {
|
|
8
|
+
if (!fs.existsSync(path.join(rootPath, 'package.json'))) return 'js';
|
|
9
|
+
|
|
10
|
+
const pkg = JSON.parse(
|
|
11
|
+
fs.readFileSync(path.join(rootPath, 'package.json')),
|
|
12
|
+
);
|
|
13
|
+
return pkg.type === 'module' ? 'cjs' : 'js';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const outName = `email-cli.config.${resolveFileExtension(process.cwd())}`;
|
|
17
|
+
const outputPath = path.join(process.cwd(), outName);
|
|
18
|
+
fs.writeFileSync(outputPath, defaultConfigTemplate, 'utf-8');
|
|
19
|
+
console.log(`Created config at ${outName}`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default scaffoldCommand;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { sendEmailSes, sendEmailSMTP } from '../../src/index.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
const panic = (msg) => {
|
|
8
|
+
console.error(msg);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/* Takes in an HTML file and sends it as a sample email */
|
|
13
|
+
const sendCommand = (config) => async (src, opts) => {
|
|
14
|
+
const inFile = path.join(process.cwd(), src);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(inFile)) {
|
|
17
|
+
console.error(`File ${inFile} does not exist`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const input = fs.readFileSync(inFile, 'utf-8');
|
|
22
|
+
|
|
23
|
+
const message = {
|
|
24
|
+
toEmail: opts.to,
|
|
25
|
+
fromEmail: opts.from,
|
|
26
|
+
subject: opts.subject,
|
|
27
|
+
textBody: 'Text body',
|
|
28
|
+
htmlBody: input,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
switch (opts.method) {
|
|
33
|
+
case 'smtp': {
|
|
34
|
+
if (!config.smtpHost) panic('SMTP host not configured');
|
|
35
|
+
if (!config.smtpUser) panic('SMTP user not configured');
|
|
36
|
+
if (!config.smtpPass) panic('SMTP password not configured');
|
|
37
|
+
|
|
38
|
+
const sendSpinner = ora('Sending email').start();
|
|
39
|
+
await sendEmailSMTP(message, {
|
|
40
|
+
smtpHost: config.smtpHost,
|
|
41
|
+
smtpPort: config.smtpPort ?? 25,
|
|
42
|
+
smtpUser: config.smtpUser,
|
|
43
|
+
smtpPass: config.smtpPass,
|
|
44
|
+
useTLS: config.smtpUseTLS ?? false,
|
|
45
|
+
});
|
|
46
|
+
sendSpinner.succeed();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case 'ses': {
|
|
50
|
+
if (!config.awsAccessKeyId) panic('Missing AWS Access Key ID');
|
|
51
|
+
if (!config.awsSecretAccessKey) panic('Missing AWS Secret Access Key');
|
|
52
|
+
|
|
53
|
+
const sendSpinner = ora('Sending email').start();
|
|
54
|
+
await sendEmailSes(message, {
|
|
55
|
+
awsAccessKeyId: config.awsAccessKeyId,
|
|
56
|
+
awsSecretAccessKey: config.awsSecretAccessKey,
|
|
57
|
+
});
|
|
58
|
+
sendSpinner.succeed();
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
default:
|
|
62
|
+
panic(`Unknown method ${opts.method}`);
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
panic(err);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default sendCommand;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { prepareHtml, packageToZip } from '../../src/index.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
const zipCommand = (config) => async (src, dest, opts) => {
|
|
8
|
+
const inFile = path.join(process.cwd(), src);
|
|
9
|
+
const outFile = path.join(process.cwd(), dest);
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(inFile)) {
|
|
12
|
+
console.error(`File ${inFile} does not exist`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(outFile) && !opts.force) {
|
|
17
|
+
console.error(`File ${outFile} already exists. Pass --force to overwrite.`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const input = fs.readFileSync(inFile, 'utf-8');
|
|
22
|
+
let prepared;
|
|
23
|
+
|
|
24
|
+
if (!opts['skip-preparation']) {
|
|
25
|
+
const prepareSpinner = ora('Preparing HTML').start();
|
|
26
|
+
prepared = await prepareHtml(input, config.template);
|
|
27
|
+
prepareSpinner.succeed();
|
|
28
|
+
} else {
|
|
29
|
+
prepared = input;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const zipSpinner = ora(`Zipping ${src} to ${dest}`).start();
|
|
33
|
+
const zipBuffer = await packageToZip(prepared);
|
|
34
|
+
|
|
35
|
+
fs.writeFileSync(outFile, zipBuffer);
|
|
36
|
+
zipSpinner.succeed();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default zipCommand;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defaultTemplate } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
template: defaultTemplate,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const mergeWithDefaultConfig = (config) => ({
|
|
8
|
+
...defaultConfig,
|
|
9
|
+
...config,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const defaultConfigTemplate = `module.exports = {
|
|
13
|
+
/* You can optionally pass a template as a function.
|
|
14
|
+
* The body of your email will be wrapped into this. */
|
|
15
|
+
// template: (body) => \`<html><body>\${body}</body></html>\`,
|
|
16
|
+
|
|
17
|
+
/* If you want to use Mailchimp you need to add API credentials
|
|
18
|
+
* here. It is recommended to use environment variables. */
|
|
19
|
+
// mailchimpAPIKey: process.env.MAILCHIMP_API_KEY,
|
|
20
|
+
// mailchimpServerPrefix: process.env.MAILCHIMP_SERVER_PREFIX,
|
|
21
|
+
|
|
22
|
+
/* If you want to use AWS services (like sending through SES or
|
|
23
|
+
* storing on S3) you need to add credentials here. It is
|
|
24
|
+
* recommended to use environment variables. */
|
|
25
|
+
// awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
26
|
+
// awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
27
|
+
// awsBucket: process.env.AWS_BUCKET,
|
|
28
|
+
// awsRegion: process.env.AWS_REGION,
|
|
29
|
+
|
|
30
|
+
/* If you want to use SMTP you need to add credentials here. It is
|
|
31
|
+
* recommended to use environment variables. */
|
|
32
|
+
// smtpHost: process.env.SMTP_HOST,
|
|
33
|
+
// smtpPort: process.env.SMTP_PORT,
|
|
34
|
+
// smtpUser: process.env.SMTP_USER,
|
|
35
|
+
// smtpPass: process.env.SMTP_PASS,
|
|
36
|
+
// smtpUseTLS: process.env.SMTP_USE_TLS,
|
|
37
|
+
};`;
|
|
38
|
+
|
|
39
|
+
export default defaultConfig;
|
|
@@ -0,0 +1,368 @@
|
|
|
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 JSZip = require('jszip');
|
|
10
|
+
var fileSaver = require('file-saver');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a deterministic random string based on the input string.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} str Input string
|
|
16
|
+
* @return {string} hash of str
|
|
17
|
+
*/
|
|
18
|
+
const hashFromString = (str) => {
|
|
19
|
+
let hash = 0;
|
|
20
|
+
for (let i = 0; i < str.length; i++) {
|
|
21
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
22
|
+
}
|
|
23
|
+
return hash.toString();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Formats a string to a filename by turning it into kebab case
|
|
28
|
+
*
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @return {string} formatted filename
|
|
31
|
+
*/
|
|
32
|
+
const formatFilename = (text) => {
|
|
33
|
+
return text
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9-]+/g, ' ')
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/\s+/g, '-');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Converts all rgb defined colors with their hex equivalent
|
|
42
|
+
* as some email clients only support hex color format.
|
|
43
|
+
*/
|
|
44
|
+
const convertRgbToHex = () => {
|
|
45
|
+
return (tree) => {
|
|
46
|
+
tree.match(matchHelper('[style]'), (node) => {
|
|
47
|
+
let styleBlock = node.attrs.style;
|
|
48
|
+
styleBlock = styleBlock.replace(
|
|
49
|
+
/rgb\(\s*\d{1,3},\s*\d{1,3},\s*\d{1,3}\)/g,
|
|
50
|
+
(match) => {
|
|
51
|
+
const values = match
|
|
52
|
+
.replace(/[rgb() ]/g, '')
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((n) => parseInt(n));
|
|
55
|
+
return '#' + colorConvert.rgb.hex(values);
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
node.attrs.style = styleBlock;
|
|
59
|
+
return node;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return tree;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Prepares HTML by only returning the body tag, to make sure
|
|
68
|
+
*/
|
|
69
|
+
const onlyReturnBody = () => {
|
|
70
|
+
return (tree) => {
|
|
71
|
+
tree.match({ tag: 'body' }, (node) => {
|
|
72
|
+
return node.content;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return tree;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rewrites the markup by turning all conditional comments returned from the
|
|
81
|
+
* <ConditionalComment> component into true conditional comments that can be
|
|
82
|
+
* picked up by Outlook.
|
|
83
|
+
*/
|
|
84
|
+
const injectConditionalComments = () => {
|
|
85
|
+
return (tree) => {
|
|
86
|
+
tree.match(matchHelper('[data-conditional-comment]'), (node) => {
|
|
87
|
+
const comment = node.attrs['data-conditional-comment'];
|
|
88
|
+
|
|
89
|
+
// Remove HTML comment delimiters
|
|
90
|
+
const cleanedComment = unescape(comment).replace(/<!--|-->/g, '');
|
|
91
|
+
|
|
92
|
+
// Remove Line breaks from comment
|
|
93
|
+
const cleanedCommentLines = cleanedComment.split('\n').join('');
|
|
94
|
+
|
|
95
|
+
node = `<!--${cleanedCommentLines}-->`;
|
|
96
|
+
return node;
|
|
97
|
+
});
|
|
98
|
+
return tree;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Rewrites the markup by adding all styles defined in the data-mso-style
|
|
104
|
+
* attribute to the <style> tag. This is needed because the mso styles are
|
|
105
|
+
* non-standard CSS, so they cannot be added to React's style tag.
|
|
106
|
+
*/
|
|
107
|
+
const injectMsoStyles = () => {
|
|
108
|
+
return (tree) => {
|
|
109
|
+
tree.match(matchHelper('[data-mso-style]'), (node) => {
|
|
110
|
+
if (node.attrs?.style == null) node.attrs.style = '';
|
|
111
|
+
node.attrs.style += node.attrs['data-mso-style'];
|
|
112
|
+
node.attrs['data-mso-style'] = null;
|
|
113
|
+
return node;
|
|
114
|
+
});
|
|
115
|
+
return tree;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Collects all inline styles defined in the components, de-dedupes them and
|
|
121
|
+
* moves them to a single unified style tag in the head of the document.
|
|
122
|
+
*/
|
|
123
|
+
const moveInlineStylesToHead = () => {
|
|
124
|
+
let styles = {};
|
|
125
|
+
|
|
126
|
+
return (tree) => {
|
|
127
|
+
tree.match({ tag: 'style' }, (node) => {
|
|
128
|
+
if (!node.content) return '';
|
|
129
|
+
const styleBlock = node.content[0].trim();
|
|
130
|
+
const hash = hashFromString(styleBlock);
|
|
131
|
+
|
|
132
|
+
if (styleBlock) {
|
|
133
|
+
styles[hash] = styleBlock;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Remove the inline style node
|
|
137
|
+
return '';
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
tree.match({ tag: 'head' }, (node) => {
|
|
141
|
+
node.content.push({
|
|
142
|
+
tag: 'style',
|
|
143
|
+
content: Object.values(styles).join(''),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return node;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return tree;
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const prepareHtmlFactory = (plugins) => {
|
|
154
|
+
const htmlProcessor = posthtml();
|
|
155
|
+
|
|
156
|
+
plugins.forEach((plugin) => {
|
|
157
|
+
htmlProcessor.use(plugin);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return async (body, template = (str) => str) => {
|
|
161
|
+
const content = template(body);
|
|
162
|
+
|
|
163
|
+
const processed = await htmlProcessor.process(content);
|
|
164
|
+
|
|
165
|
+
return processed.html;
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* This default pipeline removes all script and link tags from the HTML,
|
|
171
|
+
* moves all inline styles to the head and wraps everything in the email template.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} body - the html body
|
|
174
|
+
* @param {function} template - the template function, defaults to no template
|
|
175
|
+
*/
|
|
176
|
+
const defaultPipeline = prepareHtmlFactory([
|
|
177
|
+
onlyReturnBody(),
|
|
178
|
+
removeTags({ tags: ['script', 'link'] }),
|
|
179
|
+
convertRgbToHex(),
|
|
180
|
+
moveInlineStylesToHead(),
|
|
181
|
+
injectMsoStyles(),
|
|
182
|
+
injectConditionalComments(),
|
|
183
|
+
removeAttributes(['data-reactid', 'data-reactroot']),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
const BASE64_REGEX = /data:image\/([a-zA-Z]*);base64,(.*)/;
|
|
187
|
+
|
|
188
|
+
// Browser packacking factory
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Turns base64 inlined images into image buffers and updates their src
|
|
192
|
+
* attribute accordingly. Returns an object containing the updated html and
|
|
193
|
+
* an array of image buffers. Different adapters can then hook into this to
|
|
194
|
+
* process the email into a ZIP archive or pushing it to an API.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} body - the prepared html body
|
|
197
|
+
* @return {object} the processed html and base64 strings for all images
|
|
198
|
+
*/
|
|
199
|
+
const preparePackageFactory =
|
|
200
|
+
({ urlPrefix = '', handlePackage }) =>
|
|
201
|
+
async (body, opts = {}) => {
|
|
202
|
+
let counter = 0;
|
|
203
|
+
const images = [];
|
|
204
|
+
|
|
205
|
+
const processed = await posthtml()
|
|
206
|
+
.use((tree) => {
|
|
207
|
+
tree.match({ tag: 'img' }, (node) => {
|
|
208
|
+
const { src, alt } = node.attrs;
|
|
209
|
+
|
|
210
|
+
if (BASE64_REGEX.test(src)) {
|
|
211
|
+
const [_, ext, data] = src.match(
|
|
212
|
+
/data:image\/([a-zA-Z]*);base64,(.*)/,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const hash = Math.random().toString(36).substring(2, 7);
|
|
216
|
+
|
|
217
|
+
const filename = alt
|
|
218
|
+
? `${formatFilename(alt)}-text-${hash}.${ext}`
|
|
219
|
+
: `image-${counter}.${ext}`;
|
|
220
|
+
|
|
221
|
+
node.attrs.src = `${urlPrefix}${filename}`;
|
|
222
|
+
|
|
223
|
+
images.push({
|
|
224
|
+
filename,
|
|
225
|
+
buffer: data,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
counter++;
|
|
229
|
+
}
|
|
230
|
+
return node;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return tree;
|
|
234
|
+
})
|
|
235
|
+
.process(body);
|
|
236
|
+
|
|
237
|
+
return await handlePackage(
|
|
238
|
+
{
|
|
239
|
+
html: processed.html,
|
|
240
|
+
images,
|
|
241
|
+
},
|
|
242
|
+
opts,
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const makeDefaultTemplate =
|
|
247
|
+
({ lang = 'en-US', dir = 'ltr' } = {}) =>
|
|
248
|
+
(body) =>
|
|
249
|
+
`<!doctype html>
|
|
250
|
+
<html
|
|
251
|
+
lang="${lang}"
|
|
252
|
+
xml:lang="${lang}"
|
|
253
|
+
dir="${dir}"
|
|
254
|
+
xmlns="http://www.w3.org/1999/xhtml"
|
|
255
|
+
xmlns:v="urn:schemas-microsoft-com:vml"
|
|
256
|
+
xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
257
|
+
>
|
|
258
|
+
<head>
|
|
259
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
260
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
261
|
+
<title>Chief</title>
|
|
262
|
+
<!--[if mso]>
|
|
263
|
+
<style type="text/css">
|
|
264
|
+
table {
|
|
265
|
+
border-collapse: collapse;
|
|
266
|
+
border: 0;
|
|
267
|
+
border-spacing: 0;
|
|
268
|
+
margin: 0;
|
|
269
|
+
mso-table-lspace: 0pt !important;
|
|
270
|
+
mso-table-rspace: 0pt !important;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
img {
|
|
274
|
+
display: block !important;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
div, td {padding:0;}
|
|
278
|
+
|
|
279
|
+
div {margin:0 !important;}
|
|
280
|
+
|
|
281
|
+
.notMso {
|
|
282
|
+
display: none;
|
|
283
|
+
mso-hide: all;
|
|
284
|
+
}
|
|
285
|
+
</style>
|
|
286
|
+
<noscript>
|
|
287
|
+
<xml>
|
|
288
|
+
<o:OfficeDocumentSettings>
|
|
289
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
290
|
+
</o:OfficeDocumentSettings>
|
|
291
|
+
</xml>
|
|
292
|
+
</noscript>
|
|
293
|
+
<![endif]-->
|
|
294
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
295
|
+
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
|
|
296
|
+
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
|
297
|
+
<meta name="viewport" content="width=device-width" />
|
|
298
|
+
<meta name="color-scheme" content="light" />
|
|
299
|
+
<meta name="supported-color-schemes" content="light" />
|
|
300
|
+
|
|
301
|
+
<meta name="format-detection" content="telephone=no">
|
|
302
|
+
<meta name="format-detection" content="date=no">
|
|
303
|
+
<meta name="format-detection" content="address=no">
|
|
304
|
+
<meta name="format-detection" content="email=no">
|
|
305
|
+
|
|
306
|
+
<style>
|
|
307
|
+
a[x-apple-data-detectors] {
|
|
308
|
+
color: inherit !important;
|
|
309
|
+
|
|
310
|
+
font-size: inherit !important;
|
|
311
|
+
font-family: inherit !important;
|
|
312
|
+
font-weight: inherit !important;
|
|
313
|
+
line-height: inherit !important;
|
|
314
|
+
text-decoration: inherit !important;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#MessageViewBody a:not([style]) {
|
|
318
|
+
color: inherit !important;
|
|
319
|
+
font-size: inherit !important;
|
|
320
|
+
font-family: inherit !important;
|
|
321
|
+
font-weight: inherit !important;
|
|
322
|
+
line-height: inherit !important;
|
|
323
|
+
text-decoration: inherit !important;
|
|
324
|
+
}
|
|
325
|
+
</style>
|
|
326
|
+
</head>
|
|
327
|
+
<body class="body" style="padding:0;">
|
|
328
|
+
${body}
|
|
329
|
+
</body>
|
|
330
|
+
</html>`;
|
|
331
|
+
|
|
332
|
+
const defaultTemplate = makeDefaultTemplate();
|
|
333
|
+
|
|
334
|
+
const defaultConfig = {
|
|
335
|
+
name: 'email',
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @param {string} body - the prepared html body
|
|
340
|
+
* @return {function} A function that can be called to download the email as a ZIP archive
|
|
341
|
+
*/
|
|
342
|
+
const packageToZip = preparePackageFactory({
|
|
343
|
+
urlPrefix: '',
|
|
344
|
+
handlePackage: ({ html, images }, opts) => {
|
|
345
|
+
const config = { ...defaultConfig, ...opts };
|
|
346
|
+
|
|
347
|
+
const zip = new JSZip();
|
|
348
|
+
const files = zip.folder(config.name);
|
|
349
|
+
|
|
350
|
+
files.file('index.html', html);
|
|
351
|
+
images.forEach(({ filename, buffer }) => {
|
|
352
|
+
files.file(filename, buffer, { base64: true });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return async () => {
|
|
356
|
+
files.generateAsync({ type: 'blob' }).then((content) => {
|
|
357
|
+
fileSaver.saveAs(content, `${formatFilename(config.name)}.zip`);
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
exports.defaultTemplate = defaultTemplate;
|
|
364
|
+
exports.makeDefaultTemplate = makeDefaultTemplate;
|
|
365
|
+
exports.packageToZip = packageToZip;
|
|
366
|
+
exports.prepareHtml = defaultPipeline;
|
|
367
|
+
exports.prepareHtmlFactory = prepareHtmlFactory;
|
|
368
|
+
exports.preparePackageFactory = preparePackageFactory;
|