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/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
+ }