mjmlhelper 2.3.0 → 2.3.2
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/.github/workflows/ci.yml +41 -0
- package/.gitignore +18 -15
- package/README.md +1 -0
- package/boilerplate/_inc/bottom.mjml +18 -18
- package/boilerplate/_inc/button.mjml +6 -6
- package/boilerplate/_inc/columns-inline.mjml +47 -47
- package/boilerplate/_inc/columns.mjml +25 -25
- package/boilerplate/_inc/divider.mjml +24 -24
- package/boilerplate/_inc/responsive.mjml +70 -70
- package/boilerplate/_inc/simple.mjml +37 -37
- package/boilerplate/_inc/top.mjml +8 -8
- package/boilerplate/_inc/two-col-right.mjml +11 -11
- package/boilerplate/example.gitignore +9 -9
- package/boilerplate/example.mjmlhelper.json +27 -27
- package/boilerplate/index.mjml +113 -113
- package/mjmlhelper.js +576 -576
- package/package.json +1 -1
- package/LICENSE.md +0 -21
package/mjmlhelper.js
CHANGED
|
@@ -1,576 +1,576 @@
|
|
|
1
|
-
class mjmlhelper {
|
|
2
|
-
static config() {
|
|
3
|
-
return require(process.cwd() + '/mjmlhelper.json');
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
static runMjml(type) {
|
|
7
|
-
const { execSync } = require('child_process'),
|
|
8
|
-
fs = require('fs');
|
|
9
|
-
let path = '.';
|
|
10
|
-
if (!fs.existsSync('./node_modules')) {
|
|
11
|
-
path = '..';
|
|
12
|
-
}
|
|
13
|
-
execSync('node ' + path + '/node_modules/mjml/bin/mjml index.mjml -o index.html');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
static runMjmlAfterwork(type) {
|
|
17
|
-
let data = fs.readFileSync(process.cwd() + '/index.html', 'utf-8');
|
|
18
|
-
data = this.modifyHtml(data, type);
|
|
19
|
-
fs.writeFileSync(process.cwd() + '/index.html', data, 'utf-8');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static generateZip(type) {
|
|
23
|
-
fs.copySync(process.cwd() + '/index.html', process.cwd() + '/index-original.html', {
|
|
24
|
-
overwrite: true
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
let data = fs.readFileSync(process.cwd() + '/index.html', 'utf-8');
|
|
28
|
-
if (type === 'cleverreach') {
|
|
29
|
-
data = this.addCleverReachStyles(data);
|
|
30
|
-
}
|
|
31
|
-
if (type === 'mailchimp') {
|
|
32
|
-
data = this.addMailchimpStyles(data);
|
|
33
|
-
}
|
|
34
|
-
data = this.moveImagesFolder(data);
|
|
35
|
-
fs.writeFileSync(process.cwd() + '/index.html', data, 'utf-8');
|
|
36
|
-
|
|
37
|
-
fs.copySync(process.cwd() + '/index.html', process.cwd() + '/index-converted.html', {
|
|
38
|
-
overwrite: true
|
|
39
|
-
});
|
|
40
|
-
this.zipFolder(() => {
|
|
41
|
-
fs.moveSync(process.cwd() + '/index-original.html', process.cwd() + '/index.html', {
|
|
42
|
-
overwrite: true
|
|
43
|
-
});
|
|
44
|
-
fs.removeSync(process.cwd() + '/index-original.html');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
console.log('successfully created index.zip');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
static zipFolder(callback) {
|
|
51
|
-
let output = fs.createWriteStream(process.cwd() + '/index.zip'),
|
|
52
|
-
archive = archiver('zip', { zlib: { level: 9 } });
|
|
53
|
-
archive.pipe(output);
|
|
54
|
-
archive.file('index.html');
|
|
55
|
-
archive.directory('_assets/', false);
|
|
56
|
-
output.on('close', callback);
|
|
57
|
-
archive.finalize();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static modifyHtml(data, type) {
|
|
61
|
-
data = this.doSomeHacks(data);
|
|
62
|
-
data = this.replaceDummyLinks(data, type === 'mail' ? true : false);
|
|
63
|
-
data = this.mergeStyleTagsIntoOne(data);
|
|
64
|
-
data = this.addHelperClasses(data);
|
|
65
|
-
return data;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static addCleverReachStyles(data) {
|
|
69
|
-
let style_tag = '<style type="text/css">',
|
|
70
|
-
pos = data.indexOf(style_tag) + style_tag.length,
|
|
71
|
-
positions = null,
|
|
72
|
-
shift = 0;
|
|
73
|
-
// set max width to container so that in cleverreach editor modules are good visible
|
|
74
|
-
data =
|
|
75
|
-
data.substring(0, pos) +
|
|
76
|
-
' body[spellcheck="false"] > div:first-child, body[spellcheck="false"] > div:first-child + div { max-width:600px;margin:0 auto; } ' +
|
|
77
|
-
data.substring(pos);
|
|
78
|
-
// move inline wysiwyg overlay to bottom
|
|
79
|
-
data =
|
|
80
|
-
data.substring(0, pos) +
|
|
81
|
-
' .tox-editor-header[style*="top: 0px"] { top:auto !important; bottom:0 !important; } ' +
|
|
82
|
-
data.substring(pos);
|
|
83
|
-
// hide tinymce overlay that always goes in the way
|
|
84
|
-
data =
|
|
85
|
-
data.substring(0, pos) +
|
|
86
|
-
' .mce-tinymce-inline.mce-floatpanel { display:none !important; } ' +
|
|
87
|
-
data.substring(pos);
|
|
88
|
-
// hide modal overlay
|
|
89
|
-
data = data.substring(0, pos) + ' body.cred_modal:before { display: none !important; } ' + data.substring(pos);
|
|
90
|
-
// fix font size 0 problem (https://github.com/vielhuber/mjmlhelper/issues/1)
|
|
91
|
-
data = data.substring(0, pos) + ' .mce-content-body { font-size: 13px; } ' + data.substring(pos);
|
|
92
|
-
// fix problem where img sizes get overwritten
|
|
93
|
-
data =
|
|
94
|
-
data.substring(0, pos) +
|
|
95
|
-
' .preserve-cr-full-size { width:100% !important; height:auto !important; } ' +
|
|
96
|
-
data.substring(pos);
|
|
97
|
-
positions = this.findAllPositions('<img', data);
|
|
98
|
-
shift = 0;
|
|
99
|
-
positions.forEach(positions__value => {
|
|
100
|
-
let begin = positions__value + shift,
|
|
101
|
-
end = data.indexOf('>', begin) + '>'.length,
|
|
102
|
-
tag = data.substring(begin, end);
|
|
103
|
-
if (tag.indexOf('width:100%') > -1) {
|
|
104
|
-
if (tag.indexOf('class="') > -1) {
|
|
105
|
-
data =
|
|
106
|
-
data.substring(0, begin) +
|
|
107
|
-
tag.replace('class="', 'class="preserve-cr-full-size ') +
|
|
108
|
-
data.substring(end);
|
|
109
|
-
shift += 'class="preserve-cr-full-size '.length - 'class="'.length;
|
|
110
|
-
} else {
|
|
111
|
-
data =
|
|
112
|
-
data.substring(0, begin) +
|
|
113
|
-
tag.replace('<img ', '<img class="preserve-cr-full-size" ') +
|
|
114
|
-
data.substring(end);
|
|
115
|
-
shift += '<img class="preserve-cr-full-size" '.length - '<img '.length;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// placeholders
|
|
121
|
-
data = this.replaceAll(data, '%UNSUBSCRIBE%', '{UNSUBSCRIBE}');
|
|
122
|
-
data = this.replaceAll(data, '%WEBVERSION%', '{ONLINE_VERSION}');
|
|
123
|
-
data = this.replaceAll(data, '%PREVIEWTEXT%', '{CAMPAIGN}');
|
|
124
|
-
|
|
125
|
-
// loop items
|
|
126
|
-
data = this.replaceFirst(data, '<!-- flexibles -->', '<!--#loop#--><!--#loopitem#-->');
|
|
127
|
-
data = this.replaceLast(data, '<!-- /flexibles -->', '<!--#/loopitem#--><!--#/loop#-->');
|
|
128
|
-
positions = this.findAllPositions('<!-- flexible', data);
|
|
129
|
-
shift = 0;
|
|
130
|
-
positions.forEach((positions__value, positions__key) => {
|
|
131
|
-
if (positions__key === 0) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
let begin = positions__value + shift,
|
|
135
|
-
end = data.indexOf('-->', begin) + '-->'.length,
|
|
136
|
-
tag = '<!--#/loopitem#--><!--#loopitem#-->';
|
|
137
|
-
data = data.substring(0, begin) + tag + data.substring(end);
|
|
138
|
-
shift += tag.length - (end - begin);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// first and last tag
|
|
142
|
-
positions = this.findAllPositions(' mj-outlook-group-fix', data).concat(
|
|
143
|
-
this.findAllPositions('mj-hero-content', data).concat([])
|
|
144
|
-
);
|
|
145
|
-
positions.sort((a, b) => {
|
|
146
|
-
return a - b;
|
|
147
|
-
});
|
|
148
|
-
shift = 0;
|
|
149
|
-
positions.forEach(positions__value => {
|
|
150
|
-
let begin = data.indexOf('>', positions__value + shift) + '>'.length,
|
|
151
|
-
pointer = begin,
|
|
152
|
-
level = 1;
|
|
153
|
-
while (level != 0) {
|
|
154
|
-
if (data.substring(pointer, pointer + '<div'.length) === '<div') {
|
|
155
|
-
level++;
|
|
156
|
-
}
|
|
157
|
-
if (data.substring(pointer, pointer + '</div>'.length) === '</div>') {
|
|
158
|
-
level--;
|
|
159
|
-
}
|
|
160
|
-
pointer++;
|
|
161
|
-
}
|
|
162
|
-
let end = pointer - 1;
|
|
163
|
-
data = data.substring(0, begin) + '<!--#html #-->' + data.substring(begin);
|
|
164
|
-
end += '<!--#html #-->'.length;
|
|
165
|
-
data = data.substring(0, end) + '<!--#/html#-->' + data.substring(end);
|
|
166
|
-
shift += '<!--#html #-->'.length + '<!--#/html#-->'.length;
|
|
167
|
-
});
|
|
168
|
-
// prevent nested html tags (cleverreach has problems with that)
|
|
169
|
-
data = data.replace(/(<!--#html #-->)(((?!<!--#\/html#-->).)*(<!--#html #-->))/gs, '$2');
|
|
170
|
-
data = data.replace(/((<!--#\/html#-->)((?!<!--#html #-->).)*)(<!--#\/html#-->)/gs, '$1');
|
|
171
|
-
|
|
172
|
-
return data;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
static addMailchimpStyles(data) {
|
|
176
|
-
let positions,
|
|
177
|
-
shift,
|
|
178
|
-
style_tag = '<style type="text/css">',
|
|
179
|
-
pos = null;
|
|
180
|
-
|
|
181
|
-
// mailchimp has problems with external fonts: when embedding google fonts, we get the error
|
|
182
|
-
// "An error occurred parsing your template CSS: Cannot find a CSS file at: https://fonts.googleapis.com/css?family=Tinos:400"
|
|
183
|
-
// see: https://github.com/sendwithus/templates/issues/21
|
|
184
|
-
// we strip out all occurences of @import (<link href="https://fonts.googleapis.com/..."> is still left behind)
|
|
185
|
-
pos = data.indexOf(style_tag) + style_tag.length;
|
|
186
|
-
data = data.replace(/@import url\(https:\/\/fonts\.googleapis\.com.*?\);/g, '');
|
|
187
|
-
|
|
188
|
-
// when replacing images in mailchimp, style="width:" is set to a fixed size. we prevent this with
|
|
189
|
-
pos = data.indexOf(style_tag) + style_tag.length;
|
|
190
|
-
data = data.substring(0, pos) + ' img { max-width:100%; height:auto !important; } ' + data.substring(pos);
|
|
191
|
-
|
|
192
|
-
// increase ordering icon
|
|
193
|
-
// also there is a bug in mailchimp: when adding a new module and changing it's type, the ID is lost and the element cannot be moved; we fix this also here (we simply hide the move icon when the ID is missing)
|
|
194
|
-
pos = data.indexOf(style_tag) + style_tag.length;
|
|
195
|
-
data =
|
|
196
|
-
data.substring(0, pos) +
|
|
197
|
-
`
|
|
198
|
-
.extra-controls .tpl-repeatwrap {
|
|
199
|
-
transform: translateX(-100%) translateY(-50px) !important;
|
|
200
|
-
border: 2px solid grey !important;
|
|
201
|
-
}
|
|
202
|
-
.tpl-repeatmovewrap > .tpl-repeatmove {
|
|
203
|
-
background-color: #fff !important;
|
|
204
|
-
width: 100px !important;
|
|
205
|
-
height: 50px !important;
|
|
206
|
-
border: 2px solid grey !important;
|
|
207
|
-
background-position: center !important;
|
|
208
|
-
transform: translateX(-95px) translateY(-20px) !important;
|
|
209
|
-
display: none !important;
|
|
210
|
-
}
|
|
211
|
-
div[mcrepeatable][id] .tpl-repeatmovewrap > .tpl-repeatmove {
|
|
212
|
-
display: block !important;
|
|
213
|
-
}
|
|
214
|
-
` +
|
|
215
|
-
data.substring(pos);
|
|
216
|
-
|
|
217
|
-
// remove hide icon
|
|
218
|
-
pos = data.indexOf(style_tag) + style_tag.length;
|
|
219
|
-
data =
|
|
220
|
-
data.substring(0, pos) +
|
|
221
|
-
' .tpl-repeatwrap.can-hide .tpl-hidetoggle { display:none !important; } ' +
|
|
222
|
-
data.substring(pos);
|
|
223
|
-
|
|
224
|
-
// placeholders
|
|
225
|
-
data = this.replaceAll(data, '%UNSUBSCRIBE%', '*|UNSUB|*');
|
|
226
|
-
data = this.replaceAll(data, '%WEBVERSION%', '*|ARCHIVE|*');
|
|
227
|
-
data = this.replaceAll(data, '%PREVIEWTEXT%', '*|MC_PREVIEW_TEXT|*');
|
|
228
|
-
|
|
229
|
-
// mc:edit
|
|
230
|
-
positions = this.findAllPositions(' mj-outlook-group-fix', data);
|
|
231
|
-
shift = 0;
|
|
232
|
-
positions.forEach(positions__value => {
|
|
233
|
-
let pos = data.indexOf('"', positions__value + shift) + '"'.length,
|
|
234
|
-
tag = ' mc:edit="_' + positions__value + '"';
|
|
235
|
-
data = data.substring(0, pos) + tag + data.substring(pos);
|
|
236
|
-
shift += tag.length;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// mc:repeatable
|
|
240
|
-
data = this.replaceFirst(data, '<!-- flexibles -->', '');
|
|
241
|
-
data = this.replaceLast(data, '<!-- /flexibles -->', '</div>');
|
|
242
|
-
positions = this.findAllPositions('<!-- flexible', data);
|
|
243
|
-
shift = 0;
|
|
244
|
-
positions.forEach((positions__value, positions__key) => {
|
|
245
|
-
let begin = positions__value + shift,
|
|
246
|
-
end = data.indexOf('-->', begin) + '-->'.length,
|
|
247
|
-
begin_name = data.indexOf('"', positions__value + shift) + '"'.length,
|
|
248
|
-
end_name = data.indexOf('"', begin_name),
|
|
249
|
-
tag = '<div mc:repeatable="content" mc:variant="' + data.substring(begin_name, end_name) + '">';
|
|
250
|
-
if (positions__key > 0) {
|
|
251
|
-
tag = '</div>' + tag;
|
|
252
|
-
}
|
|
253
|
-
data = data.substring(0, begin) + tag + data.substring(end);
|
|
254
|
-
shift += tag.length - (end - begin);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
return data;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
static addHelperClasses(data) {
|
|
261
|
-
// these classes are important, because gmail does not support any data attributes to target iphone responsive styles
|
|
262
|
-
const jsdom = require('jsdom'),
|
|
263
|
-
{ JSDOM } = jsdom;
|
|
264
|
-
let dom = new JSDOM(data);
|
|
265
|
-
dom.window.document.querySelectorAll('[style*="font-size"]').forEach(el => {
|
|
266
|
-
el.classList.add('font-size-' + el.style.fontSize);
|
|
267
|
-
});
|
|
268
|
-
dom.window.document.querySelectorAll('[style*="text-align"]').forEach(el => {
|
|
269
|
-
el.classList.add('text-align-' + el.style.textAlign);
|
|
270
|
-
});
|
|
271
|
-
dom.window.document.querySelectorAll('[class*="mj-column-per-"]').forEach(el => {
|
|
272
|
-
el.classList.add('mj-column-per-X');
|
|
273
|
-
});
|
|
274
|
-
dom.window.document.querySelectorAll('[class*="mj-column-"]').forEach(el => {
|
|
275
|
-
el.classList.add('mj-column-X');
|
|
276
|
-
});
|
|
277
|
-
return dom.serialize();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
static mergeStyleTagsIntoOne(data) {
|
|
281
|
-
let css = '',
|
|
282
|
-
positions = this.findAllPositions('<style type="text/css">', data),
|
|
283
|
-
shift = 0;
|
|
284
|
-
positions.forEach(positions__value => {
|
|
285
|
-
let begin = positions__value + shift,
|
|
286
|
-
end = data.indexOf('</style>', begin) + '</style>'.length,
|
|
287
|
-
begin_inner = data.indexOf('>', begin) + '>'.length,
|
|
288
|
-
end_inner = data.indexOf('</style>', begin);
|
|
289
|
-
// if this is inside ie condition, skip
|
|
290
|
-
let pointer = begin;
|
|
291
|
-
while (data.substring(pointer - 1, pointer) == ' ') {
|
|
292
|
-
pointer--;
|
|
293
|
-
}
|
|
294
|
-
if (data.substring(pointer - 3, pointer - 1) == ']>') {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
css += data.substring(begin_inner, end_inner);
|
|
298
|
-
data = data.substring(0, begin) + data.substring(end);
|
|
299
|
-
shift = shift - (end - begin);
|
|
300
|
-
});
|
|
301
|
-
// minify
|
|
302
|
-
css = css.replace(/\r?\n?/g, '').trim();
|
|
303
|
-
data = data.replace('<head>', '<head><style type="text/css">' + css + '</style>');
|
|
304
|
-
return data;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
static doSomeHacks(data) {
|
|
308
|
-
// gmx/web.de desktop: enable multi column layout
|
|
309
|
-
for (var i = 0; i <= 100; i++) {
|
|
310
|
-
data = data
|
|
311
|
-
.split('.mj-column-per-' + i + ' { width:' + i + '% !important; }')
|
|
312
|
-
.join('.mj-column-per-' + i + ' { width:' + i + '% !important;max-width:' + i + '%; }');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// yahoo.com has a weird bug: when using google fonts, @import gets stripped out and the follow rule is killed; circumvent that
|
|
316
|
-
if (data.indexOf('@import url(') > -1) {
|
|
317
|
-
let pos = data.indexOf(');', data.indexOf('@import url('));
|
|
318
|
-
data = data.substring(0, pos + 2) + ' .dummyclass{} ' + data.substring(pos + 2);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// prevent times new roman on outlook <2016
|
|
322
|
-
let pos = data.indexOf('font-family:');
|
|
323
|
-
if (pos > -1) {
|
|
324
|
-
let font_family = data.substring(pos + 'font-family:'.length, data.indexOf(';', pos)).trim();
|
|
325
|
-
// we have to do this exactly like this, because cleverreach merges together all styles and ignores mso
|
|
326
|
-
data = data.replace(
|
|
327
|
-
'<style type="text/css">',
|
|
328
|
-
'<style type="text/css">.outlook, .outlook table, .outlook td, .outlook h1, .outlook h2, .outlook h3, .outlook h4, .outlook h5, .outlook h6, .outlook p, .outlook span, .outlook strong, .outlook div, .outlook a { font-family: ' +
|
|
329
|
-
font_family +
|
|
330
|
-
', Arial, Helvetica, sans-serif !important; }</style>\n<style type="text/css">'
|
|
331
|
-
);
|
|
332
|
-
let pos2 = data.indexOf('>', data.indexOf('<body')) + '>'.length;
|
|
333
|
-
data = data.substring(0, pos2) + '<!--[if mso]><div class="outlook"><![endif]-->' + data.substring(pos2);
|
|
334
|
-
data = data.replace('</body>', '<!--[if mso]></div><![endif]--></body>');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return data;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
static replaceDummyLinks(data, with_codes = true) {
|
|
341
|
-
data = data.split('href="#"').join('href="https://test.de"');
|
|
342
|
-
if (with_codes === true) {
|
|
343
|
-
data = data
|
|
344
|
-
.split('href="%UNSUBSCRIBE%"')
|
|
345
|
-
.join('href="https://test.de"')
|
|
346
|
-
.split('href="%WEBVERSION%"')
|
|
347
|
-
.join('href="https://test.de"')
|
|
348
|
-
.split('href="%PREVIEWTEXT%"')
|
|
349
|
-
.join('href="https://test.de"');
|
|
350
|
-
}
|
|
351
|
-
return data;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
static moveImagesFolder(data) {
|
|
355
|
-
return data.split('_assets/').join('');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
static findAllPositions(searchStr, str) {
|
|
359
|
-
let searchStrLen = searchStr.length,
|
|
360
|
-
startIndex = 0,
|
|
361
|
-
index,
|
|
362
|
-
indices = [];
|
|
363
|
-
if (searchStrLen == 0) {
|
|
364
|
-
return [];
|
|
365
|
-
}
|
|
366
|
-
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
|
|
367
|
-
indices.push(index);
|
|
368
|
-
startIndex = index + searchStrLen;
|
|
369
|
-
}
|
|
370
|
-
return indices;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
static mail(type) {
|
|
374
|
-
let transporter = nodemailer.createTransport({
|
|
375
|
-
host: this.config().smtp,
|
|
376
|
-
port: this.config().port,
|
|
377
|
-
secure: this.config().ssl !== false ? true : false,
|
|
378
|
-
auth: {
|
|
379
|
-
user: this.config().username,
|
|
380
|
-
pass: this.config().password
|
|
381
|
-
}
|
|
382
|
-
}),
|
|
383
|
-
data = fs.readFileSync(process.cwd() + '/index.html', 'utf-8'),
|
|
384
|
-
message = {
|
|
385
|
-
from: '"' + this.config().name + '" <' + this.config().from + '>',
|
|
386
|
-
to: null, // will be set later
|
|
387
|
-
subject: this.fetchSubject(data),
|
|
388
|
-
generateTextFromHTML: true,
|
|
389
|
-
html: data,
|
|
390
|
-
attachments: []
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
if (this.config().images == 'inline') {
|
|
394
|
-
console.log('inlining images...');
|
|
395
|
-
message = this.inlineImages(message);
|
|
396
|
-
} else if (this.config().images == 'upload') {
|
|
397
|
-
console.log('uploading images...');
|
|
398
|
-
// this is asynchrionus but we simply do process further
|
|
399
|
-
message.html = this.uploadImages(message.html);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
fs.writeFileSync(process.cwd() + '/index-converted.html', message.html, 'utf-8');
|
|
403
|
-
|
|
404
|
-
let to = this.config().to;
|
|
405
|
-
if (to instanceof Array) {
|
|
406
|
-
to = to;
|
|
407
|
-
} else if (to.indexOf('.txt') > -1) {
|
|
408
|
-
to = fs.readFileSync(to, 'utf-8');
|
|
409
|
-
to = to.replace(/\r\n/g, '\n');
|
|
410
|
-
to = to.replace(/\r/g, '\n');
|
|
411
|
-
to = to.split('\n');
|
|
412
|
-
} else {
|
|
413
|
-
to = [to];
|
|
414
|
-
}
|
|
415
|
-
if (this.config().log !== undefined) {
|
|
416
|
-
fs.writeFileSync(this.config().log, '', 'utf-8');
|
|
417
|
-
}
|
|
418
|
-
to.forEach((value, index) => {
|
|
419
|
-
setTimeout(() => {
|
|
420
|
-
message.to = value;
|
|
421
|
-
transporter.sendMail(message, (error, info) => {
|
|
422
|
-
if (error) {
|
|
423
|
-
return console.log(error);
|
|
424
|
-
}
|
|
425
|
-
console.log('successfully sent out mail to ' + info.envelope.to);
|
|
426
|
-
info.time =
|
|
427
|
-
('0' + new Date().getDate()).slice(-2) +
|
|
428
|
-
'.' +
|
|
429
|
-
('0' + (new Date().getMonth() + 1)).slice(-2) +
|
|
430
|
-
'.' +
|
|
431
|
-
new Date().getFullYear() +
|
|
432
|
-
' ' +
|
|
433
|
-
('0' + new Date().getHours()).slice(-2) +
|
|
434
|
-
':' +
|
|
435
|
-
('0' + new Date().getMinutes()).slice(-2) +
|
|
436
|
-
':' +
|
|
437
|
-
('0' + new Date().getSeconds()).slice(-2);
|
|
438
|
-
if (this.config().log !== undefined) {
|
|
439
|
-
fs.appendFileSync('log.txt', JSON.stringify(info) + '\n', 'utf-8');
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
}, 1000 * index);
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
static uploadImages(data) {
|
|
447
|
-
let positions = this.findAllPositions('_assets/', data),
|
|
448
|
-
shift = 0,
|
|
449
|
-
filenames = [],
|
|
450
|
-
ftp = new Client();
|
|
451
|
-
if (positions.length === 0) {
|
|
452
|
-
return data;
|
|
453
|
-
}
|
|
454
|
-
positions.forEach((positions__value, positions__key) => {
|
|
455
|
-
positions__value += shift;
|
|
456
|
-
let begin =
|
|
457
|
-
Math.max(
|
|
458
|
-
data.lastIndexOf('"', positions__value + 1),
|
|
459
|
-
data.lastIndexOf('(', positions__value + 1),
|
|
460
|
-
data.lastIndexOf("('", positions__value + 1) + 1
|
|
461
|
-
) + 1;
|
|
462
|
-
let end = Math.min(
|
|
463
|
-
data.indexOf('"', positions__value) > -1 ? data.indexOf('"', positions__value) : data.length,
|
|
464
|
-
data.indexOf(')', positions__value) > -1 ? data.indexOf(')', positions__value) : data.length,
|
|
465
|
-
data.indexOf("')", positions__value) > -1 ? data.indexOf("')", positions__value) : data.length
|
|
466
|
-
);
|
|
467
|
-
let image = data.substring(begin, end);
|
|
468
|
-
filenames.push(image);
|
|
469
|
-
image = image.replace('_assets/', '');
|
|
470
|
-
let url = this.config().ftp.url + '/' + image;
|
|
471
|
-
data = data.substring(0, begin) + url + data.substring(end);
|
|
472
|
-
shift += url.length - (end - begin);
|
|
473
|
-
});
|
|
474
|
-
ftp.on('ready', () => {
|
|
475
|
-
filenames.forEach((filenames__value, filenames__key) => {
|
|
476
|
-
ftp.put(
|
|
477
|
-
process.cwd() + '/' + filenames__value,
|
|
478
|
-
this.config().ftp.path + filenames__value.replace('_assets/', ''),
|
|
479
|
-
error => {
|
|
480
|
-
if (error) {
|
|
481
|
-
throw error;
|
|
482
|
-
}
|
|
483
|
-
if (filenames__key >= filenames.length - 1) {
|
|
484
|
-
ftp.end();
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
);
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
ftp.connect({
|
|
491
|
-
host: this.config().ftp.host,
|
|
492
|
-
port: this.config().ftp.port,
|
|
493
|
-
user: this.config().ftp.username,
|
|
494
|
-
password: this.config().ftp.password
|
|
495
|
-
});
|
|
496
|
-
return data;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
static inlineImages(message) {
|
|
500
|
-
let positions = this.findAllPositions('_assets/', message.html);
|
|
501
|
-
if (positions.length > 0) {
|
|
502
|
-
let shift = 0;
|
|
503
|
-
positions.forEach(positions__value => {
|
|
504
|
-
let cid = ~~(Math.random() * (9999 - 1000 + 1)) + 1000 + '@possible';
|
|
505
|
-
let cid_label = 'cid:' + cid;
|
|
506
|
-
positions__value += shift;
|
|
507
|
-
let begin =
|
|
508
|
-
Math.max(
|
|
509
|
-
message.html.lastIndexOf('"', positions__value + 1),
|
|
510
|
-
message.html.lastIndexOf('(', positions__value + 1)
|
|
511
|
-
) + 1;
|
|
512
|
-
let end = Math.min(
|
|
513
|
-
message.html.indexOf('"', positions__value) > -1
|
|
514
|
-
? message.html.indexOf('"', positions__value)
|
|
515
|
-
: message.html.length,
|
|
516
|
-
message.html.indexOf(')', positions__value) > -1
|
|
517
|
-
? message.html.indexOf(')', positions__value)
|
|
518
|
-
: message.html.length
|
|
519
|
-
);
|
|
520
|
-
let url = message.html.substring(begin, end);
|
|
521
|
-
if (url.indexOf('.jpg') === -1 && url.indexOf('.gif') === -1 && url.indexOf('.png') === -1) {
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
message.html = message.html.substring(0, begin) + cid_label + message.html.substring(end);
|
|
525
|
-
shift += cid_label.length - (end - begin);
|
|
526
|
-
message.attachments.push({
|
|
527
|
-
filename: url.trim(),
|
|
528
|
-
path: process.cwd() + '/' + url.trim(),
|
|
529
|
-
cid: cid
|
|
530
|
-
});
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
return message;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
static fetchSubject(data) {
|
|
537
|
-
let begin = data.indexOf('<title>') + '<title>'.length,
|
|
538
|
-
end = data.indexOf('</title>');
|
|
539
|
-
return data.substring(begin, end);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
static replaceAll(string, search, replace) {
|
|
543
|
-
return string.split(search).join(replace);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
static replaceLast(string, search, replace) {
|
|
547
|
-
let n = string.lastIndexOf(search);
|
|
548
|
-
string = string.slice(0, n) + string.slice(n).replace(search, replace);
|
|
549
|
-
return string;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
static replaceFirst(string, search, replace) {
|
|
553
|
-
return string.replace(search, replace);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const fs = require('fs-extra'),
|
|
558
|
-
archiver = require('archiver'),
|
|
559
|
-
nodemailer = require('nodemailer'),
|
|
560
|
-
Client = require('ftp'),
|
|
561
|
-
type = process.argv.slice(2)[0];
|
|
562
|
-
|
|
563
|
-
if (type === 'cleverreach' || type === 'mailchimp') {
|
|
564
|
-
mjmlhelper.runMjml(type);
|
|
565
|
-
mjmlhelper.runMjmlAfterwork(type);
|
|
566
|
-
mjmlhelper.generateZip(type);
|
|
567
|
-
} else if (type === 'mail') {
|
|
568
|
-
mjmlhelper.runMjml(type);
|
|
569
|
-
mjmlhelper.runMjmlAfterwork(type);
|
|
570
|
-
mjmlhelper.mail(type);
|
|
571
|
-
} else if (type === 'build') {
|
|
572
|
-
mjmlhelper.runMjml(type);
|
|
573
|
-
mjmlhelper.runMjmlAfterwork(type);
|
|
574
|
-
} else {
|
|
575
|
-
console.log('missing options');
|
|
576
|
-
}
|
|
1
|
+
class mjmlhelper {
|
|
2
|
+
static config() {
|
|
3
|
+
return require(process.cwd() + '/mjmlhelper.json');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
static runMjml(type) {
|
|
7
|
+
const { execSync } = require('child_process'),
|
|
8
|
+
fs = require('fs');
|
|
9
|
+
let path = '.';
|
|
10
|
+
if (!fs.existsSync('./node_modules')) {
|
|
11
|
+
path = '..';
|
|
12
|
+
}
|
|
13
|
+
execSync('node ' + path + '/node_modules/mjml/bin/mjml index.mjml -o index.html');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static runMjmlAfterwork(type) {
|
|
17
|
+
let data = fs.readFileSync(process.cwd() + '/index.html', 'utf-8');
|
|
18
|
+
data = this.modifyHtml(data, type);
|
|
19
|
+
fs.writeFileSync(process.cwd() + '/index.html', data, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static generateZip(type) {
|
|
23
|
+
fs.copySync(process.cwd() + '/index.html', process.cwd() + '/index-original.html', {
|
|
24
|
+
overwrite: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let data = fs.readFileSync(process.cwd() + '/index.html', 'utf-8');
|
|
28
|
+
if (type === 'cleverreach') {
|
|
29
|
+
data = this.addCleverReachStyles(data);
|
|
30
|
+
}
|
|
31
|
+
if (type === 'mailchimp') {
|
|
32
|
+
data = this.addMailchimpStyles(data);
|
|
33
|
+
}
|
|
34
|
+
data = this.moveImagesFolder(data);
|
|
35
|
+
fs.writeFileSync(process.cwd() + '/index.html', data, 'utf-8');
|
|
36
|
+
|
|
37
|
+
fs.copySync(process.cwd() + '/index.html', process.cwd() + '/index-converted.html', {
|
|
38
|
+
overwrite: true
|
|
39
|
+
});
|
|
40
|
+
this.zipFolder(() => {
|
|
41
|
+
fs.moveSync(process.cwd() + '/index-original.html', process.cwd() + '/index.html', {
|
|
42
|
+
overwrite: true
|
|
43
|
+
});
|
|
44
|
+
fs.removeSync(process.cwd() + '/index-original.html');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log('successfully created index.zip');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static zipFolder(callback) {
|
|
51
|
+
let output = fs.createWriteStream(process.cwd() + '/index.zip'),
|
|
52
|
+
archive = archiver('zip', { zlib: { level: 9 } });
|
|
53
|
+
archive.pipe(output);
|
|
54
|
+
archive.file('index.html');
|
|
55
|
+
archive.directory('_assets/', false);
|
|
56
|
+
output.on('close', callback);
|
|
57
|
+
archive.finalize();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static modifyHtml(data, type) {
|
|
61
|
+
data = this.doSomeHacks(data);
|
|
62
|
+
data = this.replaceDummyLinks(data, type === 'mail' ? true : false);
|
|
63
|
+
data = this.mergeStyleTagsIntoOne(data);
|
|
64
|
+
data = this.addHelperClasses(data);
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static addCleverReachStyles(data) {
|
|
69
|
+
let style_tag = '<style type="text/css">',
|
|
70
|
+
pos = data.indexOf(style_tag) + style_tag.length,
|
|
71
|
+
positions = null,
|
|
72
|
+
shift = 0;
|
|
73
|
+
// set max width to container so that in cleverreach editor modules are good visible
|
|
74
|
+
data =
|
|
75
|
+
data.substring(0, pos) +
|
|
76
|
+
' body[spellcheck="false"] > div:first-child, body[spellcheck="false"] > div:first-child + div { max-width:600px;margin:0 auto; } ' +
|
|
77
|
+
data.substring(pos);
|
|
78
|
+
// move inline wysiwyg overlay to bottom
|
|
79
|
+
data =
|
|
80
|
+
data.substring(0, pos) +
|
|
81
|
+
' .tox-editor-header[style*="top: 0px"] { top:auto !important; bottom:0 !important; } ' +
|
|
82
|
+
data.substring(pos);
|
|
83
|
+
// hide tinymce overlay that always goes in the way
|
|
84
|
+
data =
|
|
85
|
+
data.substring(0, pos) +
|
|
86
|
+
' .mce-tinymce-inline.mce-floatpanel { display:none !important; } ' +
|
|
87
|
+
data.substring(pos);
|
|
88
|
+
// hide modal overlay
|
|
89
|
+
data = data.substring(0, pos) + ' body.cred_modal:before { display: none !important; } ' + data.substring(pos);
|
|
90
|
+
// fix font size 0 problem (https://github.com/vielhuber/mjmlhelper/issues/1)
|
|
91
|
+
data = data.substring(0, pos) + ' .mce-content-body { font-size: 13px; } ' + data.substring(pos);
|
|
92
|
+
// fix problem where img sizes get overwritten
|
|
93
|
+
data =
|
|
94
|
+
data.substring(0, pos) +
|
|
95
|
+
' .preserve-cr-full-size { width:100% !important; height:auto !important; } ' +
|
|
96
|
+
data.substring(pos);
|
|
97
|
+
positions = this.findAllPositions('<img', data);
|
|
98
|
+
shift = 0;
|
|
99
|
+
positions.forEach(positions__value => {
|
|
100
|
+
let begin = positions__value + shift,
|
|
101
|
+
end = data.indexOf('>', begin) + '>'.length,
|
|
102
|
+
tag = data.substring(begin, end);
|
|
103
|
+
if (tag.indexOf('width:100%') > -1) {
|
|
104
|
+
if (tag.indexOf('class="') > -1) {
|
|
105
|
+
data =
|
|
106
|
+
data.substring(0, begin) +
|
|
107
|
+
tag.replace('class="', 'class="preserve-cr-full-size ') +
|
|
108
|
+
data.substring(end);
|
|
109
|
+
shift += 'class="preserve-cr-full-size '.length - 'class="'.length;
|
|
110
|
+
} else {
|
|
111
|
+
data =
|
|
112
|
+
data.substring(0, begin) +
|
|
113
|
+
tag.replace('<img ', '<img class="preserve-cr-full-size" ') +
|
|
114
|
+
data.substring(end);
|
|
115
|
+
shift += '<img class="preserve-cr-full-size" '.length - '<img '.length;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// placeholders
|
|
121
|
+
data = this.replaceAll(data, '%UNSUBSCRIBE%', '{UNSUBSCRIBE}');
|
|
122
|
+
data = this.replaceAll(data, '%WEBVERSION%', '{ONLINE_VERSION}');
|
|
123
|
+
data = this.replaceAll(data, '%PREVIEWTEXT%', '{CAMPAIGN}');
|
|
124
|
+
|
|
125
|
+
// loop items
|
|
126
|
+
data = this.replaceFirst(data, '<!-- flexibles -->', '<!--#loop#--><!--#loopitem#-->');
|
|
127
|
+
data = this.replaceLast(data, '<!-- /flexibles -->', '<!--#/loopitem#--><!--#/loop#-->');
|
|
128
|
+
positions = this.findAllPositions('<!-- flexible', data);
|
|
129
|
+
shift = 0;
|
|
130
|
+
positions.forEach((positions__value, positions__key) => {
|
|
131
|
+
if (positions__key === 0) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let begin = positions__value + shift,
|
|
135
|
+
end = data.indexOf('-->', begin) + '-->'.length,
|
|
136
|
+
tag = '<!--#/loopitem#--><!--#loopitem#-->';
|
|
137
|
+
data = data.substring(0, begin) + tag + data.substring(end);
|
|
138
|
+
shift += tag.length - (end - begin);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// first and last tag
|
|
142
|
+
positions = this.findAllPositions(' mj-outlook-group-fix', data).concat(
|
|
143
|
+
this.findAllPositions('mj-hero-content', data).concat([])
|
|
144
|
+
);
|
|
145
|
+
positions.sort((a, b) => {
|
|
146
|
+
return a - b;
|
|
147
|
+
});
|
|
148
|
+
shift = 0;
|
|
149
|
+
positions.forEach(positions__value => {
|
|
150
|
+
let begin = data.indexOf('>', positions__value + shift) + '>'.length,
|
|
151
|
+
pointer = begin,
|
|
152
|
+
level = 1;
|
|
153
|
+
while (level != 0) {
|
|
154
|
+
if (data.substring(pointer, pointer + '<div'.length) === '<div') {
|
|
155
|
+
level++;
|
|
156
|
+
}
|
|
157
|
+
if (data.substring(pointer, pointer + '</div>'.length) === '</div>') {
|
|
158
|
+
level--;
|
|
159
|
+
}
|
|
160
|
+
pointer++;
|
|
161
|
+
}
|
|
162
|
+
let end = pointer - 1;
|
|
163
|
+
data = data.substring(0, begin) + '<!--#html #-->' + data.substring(begin);
|
|
164
|
+
end += '<!--#html #-->'.length;
|
|
165
|
+
data = data.substring(0, end) + '<!--#/html#-->' + data.substring(end);
|
|
166
|
+
shift += '<!--#html #-->'.length + '<!--#/html#-->'.length;
|
|
167
|
+
});
|
|
168
|
+
// prevent nested html tags (cleverreach has problems with that)
|
|
169
|
+
data = data.replace(/(<!--#html #-->)(((?!<!--#\/html#-->).)*(<!--#html #-->))/gs, '$2');
|
|
170
|
+
data = data.replace(/((<!--#\/html#-->)((?!<!--#html #-->).)*)(<!--#\/html#-->)/gs, '$1');
|
|
171
|
+
|
|
172
|
+
return data;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static addMailchimpStyles(data) {
|
|
176
|
+
let positions,
|
|
177
|
+
shift,
|
|
178
|
+
style_tag = '<style type="text/css">',
|
|
179
|
+
pos = null;
|
|
180
|
+
|
|
181
|
+
// mailchimp has problems with external fonts: when embedding google fonts, we get the error
|
|
182
|
+
// "An error occurred parsing your template CSS: Cannot find a CSS file at: https://fonts.googleapis.com/css?family=Tinos:400"
|
|
183
|
+
// see: https://github.com/sendwithus/templates/issues/21
|
|
184
|
+
// we strip out all occurences of @import (<link href="https://fonts.googleapis.com/..."> is still left behind)
|
|
185
|
+
pos = data.indexOf(style_tag) + style_tag.length;
|
|
186
|
+
data = data.replace(/@import url\(https:\/\/fonts\.googleapis\.com.*?\);/g, '');
|
|
187
|
+
|
|
188
|
+
// when replacing images in mailchimp, style="width:" is set to a fixed size. we prevent this with
|
|
189
|
+
pos = data.indexOf(style_tag) + style_tag.length;
|
|
190
|
+
data = data.substring(0, pos) + ' img { max-width:100%; height:auto !important; } ' + data.substring(pos);
|
|
191
|
+
|
|
192
|
+
// increase ordering icon
|
|
193
|
+
// also there is a bug in mailchimp: when adding a new module and changing it's type, the ID is lost and the element cannot be moved; we fix this also here (we simply hide the move icon when the ID is missing)
|
|
194
|
+
pos = data.indexOf(style_tag) + style_tag.length;
|
|
195
|
+
data =
|
|
196
|
+
data.substring(0, pos) +
|
|
197
|
+
`
|
|
198
|
+
.extra-controls .tpl-repeatwrap {
|
|
199
|
+
transform: translateX(-100%) translateY(-50px) !important;
|
|
200
|
+
border: 2px solid grey !important;
|
|
201
|
+
}
|
|
202
|
+
.tpl-repeatmovewrap > .tpl-repeatmove {
|
|
203
|
+
background-color: #fff !important;
|
|
204
|
+
width: 100px !important;
|
|
205
|
+
height: 50px !important;
|
|
206
|
+
border: 2px solid grey !important;
|
|
207
|
+
background-position: center !important;
|
|
208
|
+
transform: translateX(-95px) translateY(-20px) !important;
|
|
209
|
+
display: none !important;
|
|
210
|
+
}
|
|
211
|
+
div[mcrepeatable][id] .tpl-repeatmovewrap > .tpl-repeatmove {
|
|
212
|
+
display: block !important;
|
|
213
|
+
}
|
|
214
|
+
` +
|
|
215
|
+
data.substring(pos);
|
|
216
|
+
|
|
217
|
+
// remove hide icon
|
|
218
|
+
pos = data.indexOf(style_tag) + style_tag.length;
|
|
219
|
+
data =
|
|
220
|
+
data.substring(0, pos) +
|
|
221
|
+
' .tpl-repeatwrap.can-hide .tpl-hidetoggle { display:none !important; } ' +
|
|
222
|
+
data.substring(pos);
|
|
223
|
+
|
|
224
|
+
// placeholders
|
|
225
|
+
data = this.replaceAll(data, '%UNSUBSCRIBE%', '*|UNSUB|*');
|
|
226
|
+
data = this.replaceAll(data, '%WEBVERSION%', '*|ARCHIVE|*');
|
|
227
|
+
data = this.replaceAll(data, '%PREVIEWTEXT%', '*|MC_PREVIEW_TEXT|*');
|
|
228
|
+
|
|
229
|
+
// mc:edit
|
|
230
|
+
positions = this.findAllPositions(' mj-outlook-group-fix', data);
|
|
231
|
+
shift = 0;
|
|
232
|
+
positions.forEach(positions__value => {
|
|
233
|
+
let pos = data.indexOf('"', positions__value + shift) + '"'.length,
|
|
234
|
+
tag = ' mc:edit="_' + positions__value + '"';
|
|
235
|
+
data = data.substring(0, pos) + tag + data.substring(pos);
|
|
236
|
+
shift += tag.length;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// mc:repeatable
|
|
240
|
+
data = this.replaceFirst(data, '<!-- flexibles -->', '');
|
|
241
|
+
data = this.replaceLast(data, '<!-- /flexibles -->', '</div>');
|
|
242
|
+
positions = this.findAllPositions('<!-- flexible', data);
|
|
243
|
+
shift = 0;
|
|
244
|
+
positions.forEach((positions__value, positions__key) => {
|
|
245
|
+
let begin = positions__value + shift,
|
|
246
|
+
end = data.indexOf('-->', begin) + '-->'.length,
|
|
247
|
+
begin_name = data.indexOf('"', positions__value + shift) + '"'.length,
|
|
248
|
+
end_name = data.indexOf('"', begin_name),
|
|
249
|
+
tag = '<div mc:repeatable="content" mc:variant="' + data.substring(begin_name, end_name) + '">';
|
|
250
|
+
if (positions__key > 0) {
|
|
251
|
+
tag = '</div>' + tag;
|
|
252
|
+
}
|
|
253
|
+
data = data.substring(0, begin) + tag + data.substring(end);
|
|
254
|
+
shift += tag.length - (end - begin);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return data;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static addHelperClasses(data) {
|
|
261
|
+
// these classes are important, because gmail does not support any data attributes to target iphone responsive styles
|
|
262
|
+
const jsdom = require('jsdom'),
|
|
263
|
+
{ JSDOM } = jsdom;
|
|
264
|
+
let dom = new JSDOM(data);
|
|
265
|
+
dom.window.document.querySelectorAll('[style*="font-size"]').forEach(el => {
|
|
266
|
+
el.classList.add('font-size-' + el.style.fontSize);
|
|
267
|
+
});
|
|
268
|
+
dom.window.document.querySelectorAll('[style*="text-align"]').forEach(el => {
|
|
269
|
+
el.classList.add('text-align-' + el.style.textAlign);
|
|
270
|
+
});
|
|
271
|
+
dom.window.document.querySelectorAll('[class*="mj-column-per-"]').forEach(el => {
|
|
272
|
+
el.classList.add('mj-column-per-X');
|
|
273
|
+
});
|
|
274
|
+
dom.window.document.querySelectorAll('[class*="mj-column-"]').forEach(el => {
|
|
275
|
+
el.classList.add('mj-column-X');
|
|
276
|
+
});
|
|
277
|
+
return dom.serialize();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
static mergeStyleTagsIntoOne(data) {
|
|
281
|
+
let css = '',
|
|
282
|
+
positions = this.findAllPositions('<style type="text/css">', data),
|
|
283
|
+
shift = 0;
|
|
284
|
+
positions.forEach(positions__value => {
|
|
285
|
+
let begin = positions__value + shift,
|
|
286
|
+
end = data.indexOf('</style>', begin) + '</style>'.length,
|
|
287
|
+
begin_inner = data.indexOf('>', begin) + '>'.length,
|
|
288
|
+
end_inner = data.indexOf('</style>', begin);
|
|
289
|
+
// if this is inside ie condition, skip
|
|
290
|
+
let pointer = begin;
|
|
291
|
+
while (data.substring(pointer - 1, pointer) == ' ') {
|
|
292
|
+
pointer--;
|
|
293
|
+
}
|
|
294
|
+
if (data.substring(pointer - 3, pointer - 1) == ']>') {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
css += data.substring(begin_inner, end_inner);
|
|
298
|
+
data = data.substring(0, begin) + data.substring(end);
|
|
299
|
+
shift = shift - (end - begin);
|
|
300
|
+
});
|
|
301
|
+
// minify
|
|
302
|
+
css = css.replace(/\r?\n?/g, '').trim();
|
|
303
|
+
data = data.replace('<head>', '<head><style type="text/css">' + css + '</style>');
|
|
304
|
+
return data;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
static doSomeHacks(data) {
|
|
308
|
+
// gmx/web.de desktop: enable multi column layout
|
|
309
|
+
for (var i = 0; i <= 100; i++) {
|
|
310
|
+
data = data
|
|
311
|
+
.split('.mj-column-per-' + i + ' { width:' + i + '% !important; }')
|
|
312
|
+
.join('.mj-column-per-' + i + ' { width:' + i + '% !important;max-width:' + i + '%; }');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// yahoo.com has a weird bug: when using google fonts, @import gets stripped out and the follow rule is killed; circumvent that
|
|
316
|
+
if (data.indexOf('@import url(') > -1) {
|
|
317
|
+
let pos = data.indexOf(');', data.indexOf('@import url('));
|
|
318
|
+
data = data.substring(0, pos + 2) + ' .dummyclass{} ' + data.substring(pos + 2);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// prevent times new roman on outlook <2016
|
|
322
|
+
let pos = data.indexOf('font-family:');
|
|
323
|
+
if (pos > -1) {
|
|
324
|
+
let font_family = data.substring(pos + 'font-family:'.length, data.indexOf(';', pos)).trim();
|
|
325
|
+
// we have to do this exactly like this, because cleverreach merges together all styles and ignores mso
|
|
326
|
+
data = data.replace(
|
|
327
|
+
'<style type="text/css">',
|
|
328
|
+
'<style type="text/css">.outlook, .outlook table, .outlook td, .outlook h1, .outlook h2, .outlook h3, .outlook h4, .outlook h5, .outlook h6, .outlook p, .outlook span, .outlook strong, .outlook div, .outlook a { font-family: ' +
|
|
329
|
+
font_family +
|
|
330
|
+
', Arial, Helvetica, sans-serif !important; }</style>\n<style type="text/css">'
|
|
331
|
+
);
|
|
332
|
+
let pos2 = data.indexOf('>', data.indexOf('<body')) + '>'.length;
|
|
333
|
+
data = data.substring(0, pos2) + '<!--[if mso]><div class="outlook"><![endif]-->' + data.substring(pos2);
|
|
334
|
+
data = data.replace('</body>', '<!--[if mso]></div><![endif]--></body>');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return data;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static replaceDummyLinks(data, with_codes = true) {
|
|
341
|
+
data = data.split('href="#"').join('href="https://test.de"');
|
|
342
|
+
if (with_codes === true) {
|
|
343
|
+
data = data
|
|
344
|
+
.split('href="%UNSUBSCRIBE%"')
|
|
345
|
+
.join('href="https://test.de"')
|
|
346
|
+
.split('href="%WEBVERSION%"')
|
|
347
|
+
.join('href="https://test.de"')
|
|
348
|
+
.split('href="%PREVIEWTEXT%"')
|
|
349
|
+
.join('href="https://test.de"');
|
|
350
|
+
}
|
|
351
|
+
return data;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
static moveImagesFolder(data) {
|
|
355
|
+
return data.split('_assets/').join('');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
static findAllPositions(searchStr, str) {
|
|
359
|
+
let searchStrLen = searchStr.length,
|
|
360
|
+
startIndex = 0,
|
|
361
|
+
index,
|
|
362
|
+
indices = [];
|
|
363
|
+
if (searchStrLen == 0) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
|
|
367
|
+
indices.push(index);
|
|
368
|
+
startIndex = index + searchStrLen;
|
|
369
|
+
}
|
|
370
|
+
return indices;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
static mail(type) {
|
|
374
|
+
let transporter = nodemailer.createTransport({
|
|
375
|
+
host: this.config().smtp,
|
|
376
|
+
port: this.config().port,
|
|
377
|
+
secure: this.config().ssl !== false ? true : false,
|
|
378
|
+
auth: {
|
|
379
|
+
user: this.config().username,
|
|
380
|
+
pass: this.config().password
|
|
381
|
+
}
|
|
382
|
+
}),
|
|
383
|
+
data = fs.readFileSync(process.cwd() + '/index.html', 'utf-8'),
|
|
384
|
+
message = {
|
|
385
|
+
from: '"' + this.config().name + '" <' + this.config().from + '>',
|
|
386
|
+
to: null, // will be set later
|
|
387
|
+
subject: this.fetchSubject(data),
|
|
388
|
+
generateTextFromHTML: true,
|
|
389
|
+
html: data,
|
|
390
|
+
attachments: []
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (this.config().images == 'inline') {
|
|
394
|
+
console.log('inlining images...');
|
|
395
|
+
message = this.inlineImages(message);
|
|
396
|
+
} else if (this.config().images == 'upload') {
|
|
397
|
+
console.log('uploading images...');
|
|
398
|
+
// this is asynchrionus but we simply do process further
|
|
399
|
+
message.html = this.uploadImages(message.html);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fs.writeFileSync(process.cwd() + '/index-converted.html', message.html, 'utf-8');
|
|
403
|
+
|
|
404
|
+
let to = this.config().to;
|
|
405
|
+
if (to instanceof Array) {
|
|
406
|
+
to = to;
|
|
407
|
+
} else if (to.indexOf('.txt') > -1) {
|
|
408
|
+
to = fs.readFileSync(to, 'utf-8');
|
|
409
|
+
to = to.replace(/\r\n/g, '\n');
|
|
410
|
+
to = to.replace(/\r/g, '\n');
|
|
411
|
+
to = to.split('\n');
|
|
412
|
+
} else {
|
|
413
|
+
to = [to];
|
|
414
|
+
}
|
|
415
|
+
if (this.config().log !== undefined) {
|
|
416
|
+
fs.writeFileSync(this.config().log, '', 'utf-8');
|
|
417
|
+
}
|
|
418
|
+
to.forEach((value, index) => {
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
message.to = value;
|
|
421
|
+
transporter.sendMail(message, (error, info) => {
|
|
422
|
+
if (error) {
|
|
423
|
+
return console.log(error);
|
|
424
|
+
}
|
|
425
|
+
console.log('successfully sent out mail to ' + info.envelope.to);
|
|
426
|
+
info.time =
|
|
427
|
+
('0' + new Date().getDate()).slice(-2) +
|
|
428
|
+
'.' +
|
|
429
|
+
('0' + (new Date().getMonth() + 1)).slice(-2) +
|
|
430
|
+
'.' +
|
|
431
|
+
new Date().getFullYear() +
|
|
432
|
+
' ' +
|
|
433
|
+
('0' + new Date().getHours()).slice(-2) +
|
|
434
|
+
':' +
|
|
435
|
+
('0' + new Date().getMinutes()).slice(-2) +
|
|
436
|
+
':' +
|
|
437
|
+
('0' + new Date().getSeconds()).slice(-2);
|
|
438
|
+
if (this.config().log !== undefined) {
|
|
439
|
+
fs.appendFileSync('log.txt', JSON.stringify(info) + '\n', 'utf-8');
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}, 1000 * index);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
static uploadImages(data) {
|
|
447
|
+
let positions = this.findAllPositions('_assets/', data),
|
|
448
|
+
shift = 0,
|
|
449
|
+
filenames = [],
|
|
450
|
+
ftp = new Client();
|
|
451
|
+
if (positions.length === 0) {
|
|
452
|
+
return data;
|
|
453
|
+
}
|
|
454
|
+
positions.forEach((positions__value, positions__key) => {
|
|
455
|
+
positions__value += shift;
|
|
456
|
+
let begin =
|
|
457
|
+
Math.max(
|
|
458
|
+
data.lastIndexOf('"', positions__value + 1),
|
|
459
|
+
data.lastIndexOf('(', positions__value + 1),
|
|
460
|
+
data.lastIndexOf("('", positions__value + 1) + 1
|
|
461
|
+
) + 1;
|
|
462
|
+
let end = Math.min(
|
|
463
|
+
data.indexOf('"', positions__value) > -1 ? data.indexOf('"', positions__value) : data.length,
|
|
464
|
+
data.indexOf(')', positions__value) > -1 ? data.indexOf(')', positions__value) : data.length,
|
|
465
|
+
data.indexOf("')", positions__value) > -1 ? data.indexOf("')", positions__value) : data.length
|
|
466
|
+
);
|
|
467
|
+
let image = data.substring(begin, end);
|
|
468
|
+
filenames.push(image);
|
|
469
|
+
image = image.replace('_assets/', '');
|
|
470
|
+
let url = this.config().ftp.url + '/' + image;
|
|
471
|
+
data = data.substring(0, begin) + url + data.substring(end);
|
|
472
|
+
shift += url.length - (end - begin);
|
|
473
|
+
});
|
|
474
|
+
ftp.on('ready', () => {
|
|
475
|
+
filenames.forEach((filenames__value, filenames__key) => {
|
|
476
|
+
ftp.put(
|
|
477
|
+
process.cwd() + '/' + filenames__value,
|
|
478
|
+
this.config().ftp.path + filenames__value.replace('_assets/', ''),
|
|
479
|
+
error => {
|
|
480
|
+
if (error) {
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
if (filenames__key >= filenames.length - 1) {
|
|
484
|
+
ftp.end();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
ftp.connect({
|
|
491
|
+
host: this.config().ftp.host,
|
|
492
|
+
port: this.config().ftp.port,
|
|
493
|
+
user: this.config().ftp.username,
|
|
494
|
+
password: this.config().ftp.password
|
|
495
|
+
});
|
|
496
|
+
return data;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
static inlineImages(message) {
|
|
500
|
+
let positions = this.findAllPositions('_assets/', message.html);
|
|
501
|
+
if (positions.length > 0) {
|
|
502
|
+
let shift = 0;
|
|
503
|
+
positions.forEach(positions__value => {
|
|
504
|
+
let cid = ~~(Math.random() * (9999 - 1000 + 1)) + 1000 + '@possible';
|
|
505
|
+
let cid_label = 'cid:' + cid;
|
|
506
|
+
positions__value += shift;
|
|
507
|
+
let begin =
|
|
508
|
+
Math.max(
|
|
509
|
+
message.html.lastIndexOf('"', positions__value + 1),
|
|
510
|
+
message.html.lastIndexOf('(', positions__value + 1)
|
|
511
|
+
) + 1;
|
|
512
|
+
let end = Math.min(
|
|
513
|
+
message.html.indexOf('"', positions__value) > -1
|
|
514
|
+
? message.html.indexOf('"', positions__value)
|
|
515
|
+
: message.html.length,
|
|
516
|
+
message.html.indexOf(')', positions__value) > -1
|
|
517
|
+
? message.html.indexOf(')', positions__value)
|
|
518
|
+
: message.html.length
|
|
519
|
+
);
|
|
520
|
+
let url = message.html.substring(begin, end);
|
|
521
|
+
if (url.indexOf('.jpg') === -1 && url.indexOf('.gif') === -1 && url.indexOf('.png') === -1) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
message.html = message.html.substring(0, begin) + cid_label + message.html.substring(end);
|
|
525
|
+
shift += cid_label.length - (end - begin);
|
|
526
|
+
message.attachments.push({
|
|
527
|
+
filename: url.trim(),
|
|
528
|
+
path: process.cwd() + '/' + url.trim(),
|
|
529
|
+
cid: cid
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
return message;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
static fetchSubject(data) {
|
|
537
|
+
let begin = data.indexOf('<title>') + '<title>'.length,
|
|
538
|
+
end = data.indexOf('</title>');
|
|
539
|
+
return data.substring(begin, end);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
static replaceAll(string, search, replace) {
|
|
543
|
+
return string.split(search).join(replace);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
static replaceLast(string, search, replace) {
|
|
547
|
+
let n = string.lastIndexOf(search);
|
|
548
|
+
string = string.slice(0, n) + string.slice(n).replace(search, replace);
|
|
549
|
+
return string;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
static replaceFirst(string, search, replace) {
|
|
553
|
+
return string.replace(search, replace);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const fs = require('fs-extra'),
|
|
558
|
+
archiver = require('archiver'),
|
|
559
|
+
nodemailer = require('nodemailer'),
|
|
560
|
+
Client = require('ftp'),
|
|
561
|
+
type = process.argv.slice(2)[0];
|
|
562
|
+
|
|
563
|
+
if (type === 'cleverreach' || type === 'mailchimp') {
|
|
564
|
+
mjmlhelper.runMjml(type);
|
|
565
|
+
mjmlhelper.runMjmlAfterwork(type);
|
|
566
|
+
mjmlhelper.generateZip(type);
|
|
567
|
+
} else if (type === 'mail') {
|
|
568
|
+
mjmlhelper.runMjml(type);
|
|
569
|
+
mjmlhelper.runMjmlAfterwork(type);
|
|
570
|
+
mjmlhelper.mail(type);
|
|
571
|
+
} else if (type === 'build') {
|
|
572
|
+
mjmlhelper.runMjml(type);
|
|
573
|
+
mjmlhelper.runMjmlAfterwork(type);
|
|
574
|
+
} else {
|
|
575
|
+
console.log('missing options');
|
|
576
|
+
}
|