eml-parser-qaap 1.1.15

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/src/index.ts ADDED
@@ -0,0 +1,983 @@
1
+ /**
2
+ * @author superchow
3
+ * @emil superchow@live.cn
4
+ */
5
+
6
+ import { Base64 } from 'js-base64';
7
+
8
+ import { convert, decode, encode } from './charset';
9
+ import { GB2312UTF8, getCharsetName, guid, mimeDecode, wrap, getBoundary } from './utils';
10
+ import {
11
+ KeyValue,
12
+ EmailAddress,
13
+ ParsedEmlJson,
14
+ ReadedEmlJson,
15
+ Attachment,
16
+ EmlHeaders,
17
+ Options,
18
+ BuildOptions,
19
+ CallbackFn,
20
+ OptionOrNull,
21
+ BoundaryRawData,
22
+ BoundaryConvertedData,
23
+ } from './interface';
24
+ import { addressparser } from './addressparser';
25
+
26
+ /**
27
+ * log for test
28
+ */
29
+ let verbose: boolean = false;
30
+ const defaultCharset = 'utf-8';
31
+ const fileExtensions: KeyValue = {
32
+ 'text/plain': '.txt',
33
+ 'text/html': '.html',
34
+ 'image/png': '.png',
35
+ 'image/jpg': '.jpg',
36
+ 'image/jpeg': '.jpg',
37
+ };
38
+
39
+ /**
40
+ * Gets file extension by mime type
41
+ * @param {String} mimeType
42
+ * @returns {String}
43
+ */
44
+ // eslint-disable-next-line no-unused-vars
45
+ function getFileExtension(mimeType: string): string {
46
+ return fileExtensions[mimeType] || '';
47
+ }
48
+
49
+ /**
50
+ * create a boundary
51
+ */
52
+ function createBoundary(): string {
53
+ return '----=' + guid();
54
+ }
55
+ /**
56
+ * Builds e-mail address string, e.g. { name: 'PayPal', email: 'noreply@paypal.com' } => 'PayPal' <noreply@paypal.com>
57
+ * @param {String|EmailAddress|EmailAddress[]|null} data
58
+ */
59
+ function toEmailAddress(data?: string | EmailAddress | EmailAddress[] | null): string {
60
+ let email = '';
61
+ if (typeof data === 'undefined') {
62
+ //No e-mail address
63
+ } else if (typeof data === 'string') {
64
+ email = data;
65
+ } else if (typeof data === 'object') {
66
+ if (Array.isArray(data)) {
67
+ email += data
68
+ .map(item => {
69
+ let str = '';
70
+ if (item.name) {
71
+ str += '"' + item.name.replace(/^"|"\s*$/g, '') + '" ';
72
+ }
73
+ if (item.email) {
74
+ str += '<' + item.email + '>';
75
+ }
76
+ return str;
77
+ })
78
+ .filter(a => a)
79
+ .join(', ');
80
+ } else {
81
+ if (data) {
82
+ if (data.name) {
83
+ email += '"' + data.name.replace(/^"|"\s*$/g, '') + '" ';
84
+ }
85
+ if (data.email) {
86
+ email += '<' + data.email + '>';
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return email;
92
+ }
93
+
94
+ /**
95
+ * Gets character set name, e.g. contentType='.....charset='iso-8859-2'....'
96
+ * @param {String} contentType
97
+ * @returns {String|undefined}
98
+ */
99
+ function getCharset(contentType: string) {
100
+ const match = /charset\s*=\W*([\w\-]+)/g.exec(contentType);
101
+ return match ? match[1] : undefined;
102
+ }
103
+
104
+ /**
105
+ * Gets name and e-mail address from a string, e.g. 'PayPal' <noreply@paypal.com> => { name: 'PayPal', email: 'noreply@paypal.com' }
106
+ * @param {String} raw
107
+ * @returns { EmailAddress | EmailAddress[] | null}
108
+ */
109
+ function getEmailAddress(rawStr: string): EmailAddress | EmailAddress[] | null {
110
+ const raw = unquoteString(rawStr);
111
+ const parseList = addressparser(raw);
112
+ const list = parseList.map(v => ({ name: v.name, email: v.address } as EmailAddress));
113
+
114
+ //Return result
115
+ if (list.length === 0) {
116
+ return null; //No e-mail address
117
+ }
118
+ if (list.length === 1) {
119
+ return list[0]; //Only one record, return as object, required to preserve backward compatibility
120
+ }
121
+ return list; //Multiple e-mail addresses as array
122
+ }
123
+
124
+ /**
125
+ * decode one joint
126
+ * @param {String} str
127
+ * @returns {String}
128
+ */
129
+ function decodeJoint(str: string) {
130
+ const match = /=\?([^?]+)\?(B|Q)\?(.+?)(\?=)/gi.exec(str);
131
+ if (match) {
132
+ const charset = getCharsetName(match[1] || defaultCharset); //eq. match[1] = 'iso-8859-2'; charset = 'iso88592'
133
+ const type = match[2].toUpperCase();
134
+ const value = match[3];
135
+ if (type === 'B') {
136
+ //Base64
137
+ if (charset === 'utf8') {
138
+ return decode(encode(Base64.fromBase64(value.replace(/\r?\n/g, ''))), 'utf8');
139
+ } else {
140
+ return decode(Base64.toUint8Array(value.replace(/\r?\n/g, '')), charset);
141
+ }
142
+ } else if (type === 'Q') {
143
+ //Quoted printable
144
+ return unquotePrintable(value, charset, true);
145
+ }
146
+ }
147
+ return str;
148
+ }
149
+
150
+ /**
151
+ * decode section
152
+ * @param {String} str
153
+ * @returns {String}
154
+ */
155
+ function unquoteString(str: string): string {
156
+ const regex = /=\?([^?]+)\?(B|Q)\?(.+?)(\?=)/gi;
157
+ let decodedString = str || '';
158
+ const spinOffMatch = decodedString.match(regex);
159
+ if (spinOffMatch) {
160
+ spinOffMatch.forEach(spin => {
161
+ decodedString = decodedString.replace(spin, decodeJoint(spin));
162
+ });
163
+ }
164
+
165
+ return decodedString.replace(/\r?\n/g, '');
166
+ }
167
+ /**
168
+ * Decodes 'quoted-printable'
169
+ * @param {String} value
170
+ * @param {String} charset
171
+ * @param {String} qEncoding whether the encoding is RFC-2047’s Q-encoding, meaning special handling of underscores.
172
+ * @returns {String}
173
+ */
174
+ function unquotePrintable(value: string, charset?: string, qEncoding = false): string {
175
+ //Convert =0D to '\r', =20 to ' ', etc.
176
+ // if (!charset || charset == "utf8" || charset == "utf-8") {
177
+ // return value
178
+ // .replace(/=([\w\d]{2})=([\w\d]{2})=([\w\d]{2})/gi, function (matcher, p1, p2, p3, offset, string) {
179
+
180
+ // })
181
+ // .replace(/=([\w\d]{2})=([\w\d]{2})/gi, function (matcher, p1, p2, offset, string) {
182
+
183
+ // })
184
+ // .replace(/=([\w\d]{2})/gi, function (matcher, p1, offset, string) { return String.fromCharCode(parseInt(p1, 16)); })
185
+ // .replace(/=\r?\n/gi, ""); //Join line
186
+ // } else {
187
+ // return value
188
+ // .replace(/=([\w\d]{2})=([\w\d]{2})/gi, function (matcher, p1, p2, offset, string) {
189
+
190
+ // })
191
+ // .replace(/=([\w\d]{2})/gi, function (matcher, p1, offset, string) {
192
+
193
+ // })
194
+ // .replace(/=\r?\n/gi, ''); //Join line
195
+ // }
196
+ let rawString = value
197
+ .replace(/[\t ]+$/gm, '') // remove invalid whitespace from the end of lines
198
+ .replace(/=(?:\r?\n|$)/g, ''); // remove soft line breaks
199
+
200
+ if (qEncoding) {
201
+ rawString = rawString.replace(/_/g, decode(new Uint8Array([0x20]), charset));
202
+ }
203
+
204
+ return mimeDecode(rawString, charset);
205
+ }
206
+
207
+ /**
208
+ * Parses EML file content and returns object-oriented representation of the content.
209
+ * @param {String} eml
210
+ * @param {OptionOrNull | CallbackFn<ParsedEmlJson>} options
211
+ * @param {CallbackFn<ParsedEmlJson>} callback
212
+ * @returns {string | Error | ParsedEmlJson}
213
+ */
214
+ function parse(
215
+ eml: string,
216
+ options?: OptionOrNull | CallbackFn<ParsedEmlJson>,
217
+ callback?: CallbackFn<ParsedEmlJson>
218
+ ): string | Error | ParsedEmlJson {
219
+ //Shift arguments
220
+ if (typeof options === 'function' && typeof callback === 'undefined') {
221
+ callback = options;
222
+ options = null;
223
+ }
224
+ if (typeof options !== 'object') {
225
+ options = { headersOnly: false };
226
+ }
227
+ let error: string | Error | undefined;
228
+ let result: ParsedEmlJson | undefined = {} as ParsedEmlJson;
229
+ try {
230
+ if (typeof eml !== 'string') {
231
+ throw new Error('Argument "eml" expected to be string!');
232
+ }
233
+
234
+ const lines = eml.split(/\r?\n/);
235
+ result = parseRecursive(lines, 0, result, options as Options) as ParsedEmlJson;
236
+ } catch (e) {
237
+ error = e as string;
238
+ }
239
+ callback && callback(error, result);
240
+ return error || result || new Error('read EML failed!');
241
+ }
242
+
243
+ /**
244
+ * Parses EML file content.
245
+ * @param {String[]} lines
246
+ * @param {Number} start
247
+ * @param {Options} options
248
+ * @returns {ParsedEmlJson}
249
+ */
250
+ function parseRecursive(lines: string[], start: number, parent: any, options: Options) {
251
+ let boundary: any = null;
252
+ let lastHeaderName = '';
253
+ let findBoundary = '';
254
+ let insideBody = false;
255
+ let insideBoundary = false;
256
+ let isMultiHeader = false;
257
+ let isMultipart = false;
258
+ let checkedForCt = false;
259
+ let ctInBody = false;
260
+
261
+ parent.headers = {};
262
+ //parent.body = null;
263
+
264
+ function complete(boundary: any) {
265
+ //boundary.part = boundary.lines.join("\r\n");
266
+ boundary.part = {};
267
+ parseRecursive(boundary.lines, 0, boundary.part, options);
268
+ delete boundary.lines;
269
+ }
270
+
271
+ //Read line by line
272
+ for (let i = start; i < lines.length; i++) {
273
+ let line = lines[i];
274
+
275
+ //Header
276
+ if (!insideBody) {
277
+ //Search for empty line
278
+ if (line == '') {
279
+ insideBody = true;
280
+
281
+ if (options && options.headersOnly) {
282
+ break;
283
+ }
284
+
285
+ //Expected boundary
286
+ let ct = parent.headers['Content-Type'] || parent.headers['Content-type'];
287
+ if (!ct) {
288
+ if (checkedForCt) {
289
+ insideBody = !ctInBody;
290
+ } else {
291
+ checkedForCt = true;
292
+ const lineClone = Array.from(lines);
293
+ const string = lineClone.splice(i).join('\r\n');
294
+ const trimmedStrin = string.trim();
295
+ if (trimmedStrin.indexOf('Content-Type') === 0 || trimmedStrin.indexOf('Content-type') === 0) {
296
+ insideBody = false;
297
+ ctInBody = true;
298
+ } else {
299
+ console.warn('Warning: undefined Content-Type');
300
+ }
301
+ }
302
+ } else if (/^multipart\//g.test(ct)) {
303
+ let b = getBoundary(ct);
304
+ if (b && b.length) {
305
+ findBoundary = b;
306
+ isMultipart = true;
307
+ parent.body = [];
308
+ } else {
309
+ if (verbose) {
310
+ console.warn('Multipart without boundary! ' + ct.replace(/\r?\n/g, ' '));
311
+ }
312
+ }
313
+ }
314
+
315
+ continue;
316
+ }
317
+
318
+ //Header value with new line
319
+ let match = /^\s+([^\r\n]+)/g.exec(line);
320
+ if (match) {
321
+ if (isMultiHeader) {
322
+ parent.headers[lastHeaderName][parent.headers[lastHeaderName].length - 1] += '\r\n' + match[1];
323
+ } else {
324
+ parent.headers[lastHeaderName] += '\r\n' + match[1];
325
+ }
326
+ continue;
327
+ }
328
+
329
+ //Header name and value
330
+ match = /^([\w\d\-]+):\s*([^\r\n]*)/gi.exec(line);
331
+ if (match) {
332
+ lastHeaderName = match[1];
333
+ if (parent.headers[lastHeaderName]) {
334
+ //Multiple headers with the same name
335
+ isMultiHeader = true;
336
+ if (typeof parent.headers[lastHeaderName] == 'string') {
337
+ parent.headers[lastHeaderName] = [parent.headers[lastHeaderName]];
338
+ }
339
+ parent.headers[lastHeaderName].push(match[2]);
340
+ } else {
341
+ //Header first appeared here
342
+ isMultiHeader = false;
343
+ parent.headers[lastHeaderName] = match[2];
344
+ }
345
+ continue;
346
+ }
347
+ }
348
+ //Body
349
+ else {
350
+ //Multipart body
351
+ if (isMultipart) {
352
+ //Search for boundary start
353
+
354
+ //Updated on 2019-10-12: A line before the boundary marker is not required to be an empty line
355
+ //if (lines[i - 1] == "" && line.indexOf("--" + findBoundary) == 0 && !/\-\-(\r?\n)?$/g.test(line)) {
356
+ if (line.indexOf('--' + findBoundary) == 0 && !/\-\-(\r?\n)?$/g.test(line)) {
357
+ insideBoundary = true;
358
+
359
+ //Complete the previous boundary
360
+ if (boundary && boundary.lines) {
361
+ complete(boundary);
362
+ }
363
+
364
+ //Start a new boundary
365
+ let match = /^\-\-([^\r\n]+)(\r?\n)?$/g.exec(line) as RegExpExecArray;
366
+ boundary = { boundary: match[1], lines: [] as any[] };
367
+ parent.body.push(boundary);
368
+
369
+ if (verbose) {
370
+ console.log('Found boundary: ' + boundary.boundary);
371
+ }
372
+
373
+ continue;
374
+ }
375
+
376
+ if (insideBoundary) {
377
+ //Search for boundary end
378
+ if (boundary?.boundary && lines[i - 1] == '' && line.indexOf('--' + findBoundary + '--') == 0) {
379
+ insideBoundary = false;
380
+ complete(boundary);
381
+ continue;
382
+ }
383
+ if (boundary?.boundary && line.indexOf('--' + findBoundary + '--') == 0) {
384
+ continue;
385
+ }
386
+ boundary?.lines.push(line);
387
+ }
388
+ } else {
389
+ //Solid string body
390
+ parent.body = lines.splice(i).join('\r\n');
391
+ break;
392
+ }
393
+ }
394
+ }
395
+
396
+ //Complete the last boundary
397
+ if (parent.body && parent.body.length && parent.body[parent.body.length - 1].lines) {
398
+ complete(parent.body[parent.body.length - 1]);
399
+ }
400
+
401
+ return parent;
402
+ }
403
+
404
+ /**
405
+ * Convert BoundaryRawData to BoundaryConvertedData
406
+ * @param {BoundaryRawData} boundary
407
+ * @returns {BoundaryConvertedData} Obj
408
+ */
409
+ function completeBoundary(boundary: BoundaryRawData): BoundaryConvertedData | null {
410
+ if (!boundary || !boundary.boundary) {
411
+ return null;
412
+ }
413
+ const lines = boundary.lines || [];
414
+ const result = {
415
+ boundary: boundary.boundary,
416
+ part: {
417
+ headers: {},
418
+ },
419
+ } as BoundaryConvertedData;
420
+ let lastHeaderName = '';
421
+ let insideBody = false;
422
+ let childBoundary: BoundaryRawData | undefined;
423
+ for (let index = 0; index < lines.length; index++) {
424
+ const line = lines[index];
425
+ if (!insideBody) {
426
+ if (line === '') {
427
+ insideBody = true;
428
+ continue;
429
+ }
430
+ const match = /^([\w\d\-]+):\s*([^\r\n]*)/gi.exec(line);
431
+ if (match) {
432
+ lastHeaderName = match[1];
433
+ result.part.headers[lastHeaderName] = match[2];
434
+ continue;
435
+ }
436
+ //Header value with new line
437
+ const lineMatch = /^\s+([^\r\n]+)/g.exec(line);
438
+ if (lineMatch) {
439
+ result.part.headers[lastHeaderName] += '\r\n' + lineMatch[1];
440
+ continue;
441
+ }
442
+ } else {
443
+ // part.body
444
+ const match = /^\-\-([^\r\n]+)(\r?\n)?$/g.exec(line);
445
+ const childBoundaryStr = getBoundary(result.part.headers['Content-Type'] || result.part.headers['Content-type']);
446
+ if (verbose) {
447
+ if (match) {
448
+ console.log(`line 568: line is ${line}, ${'--' + childBoundaryStr}`, `${line.indexOf('--' + childBoundaryStr)}`);
449
+ }
450
+ }
451
+ if (match && line.indexOf('--' + childBoundaryStr) === 0 && !childBoundary) {
452
+ childBoundary = { boundary: match ? match[1] : '', lines: [] };
453
+ continue;
454
+ } else if (!!childBoundary && childBoundary.boundary) {
455
+ if (lines[index - 1] === '' && line.indexOf('--' + childBoundary.boundary) === 0) {
456
+ const child = completeBoundary(childBoundary);
457
+ if (verbose) {
458
+ console.info(`578: ${JSON.stringify(child)}`);
459
+ }
460
+ if (child) {
461
+ if (Array.isArray(result.part.body)) {
462
+ result.part.body.push(child);
463
+ } else {
464
+ result.part.body = [child];
465
+ }
466
+ } else {
467
+ result.part.body = childBoundary.lines.join('\r\n');
468
+ }
469
+ // next line child
470
+ if (!!lines[index + 1]) {
471
+ childBoundary.lines = [];
472
+ continue;
473
+ }
474
+ // end line child And this boundary's end
475
+ if (line.indexOf('--' + childBoundary.boundary + '--') === 0 && lines[index + 1] === '') {
476
+ if (verbose) {
477
+ console.info('line 601 childBoundary is over line is 534');
478
+ }
479
+ childBoundary = undefined;
480
+ break;
481
+ }
482
+ }
483
+ childBoundary.lines.push(line);
484
+ } else {
485
+ if (verbose) {
486
+ console.warn('body is string');
487
+ }
488
+ result.part.body = lines.splice(index).join('\r\n');
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ return result;
494
+ }
495
+
496
+ /**
497
+ * buid EML file by ReadedEmlJson or EML file content
498
+ * @param {ReadedEmlJson} data
499
+ * @param {BuildOptions | CallbackFn<string> | null} options
500
+ * @param {CallbackFn<string>} callback
501
+ */
502
+ function build(
503
+ data: ReadedEmlJson | string,
504
+ options?: BuildOptions | CallbackFn<string> | null,
505
+ callback?: CallbackFn<string>
506
+ ): string | Error {
507
+ //Shift arguments
508
+ if (typeof options === 'function' && typeof callback === 'undefined') {
509
+ callback = options;
510
+ options = null;
511
+ }
512
+ let error: Error | string | undefined;
513
+ let eml = '';
514
+ const EOL = '\r\n'; //End-of-line
515
+
516
+ try {
517
+ if (!data) {
518
+ throw new Error('Argument "data" expected to be an object! or string');
519
+ }
520
+ if (typeof data === 'string') {
521
+ const readResult = read(data);
522
+ if (typeof readResult === 'string') {
523
+ throw new Error(readResult);
524
+ } else if (readResult instanceof Error) {
525
+ throw readResult;
526
+ } else {
527
+ data = readResult;
528
+ }
529
+ }
530
+
531
+ if (!data.headers) {
532
+ throw new Error('Argument "data" expected to be has headers');
533
+ }
534
+
535
+ if (typeof data.subject === 'string') {
536
+ data.headers['Subject'] = data.subject;
537
+ }
538
+
539
+ if (typeof data.from !== 'undefined') {
540
+ data.headers['From'] = toEmailAddress(data.from);
541
+ }
542
+
543
+ if (typeof data.to !== 'undefined') {
544
+ data.headers['To'] = toEmailAddress(data.to);
545
+ }
546
+
547
+ if (typeof data.cc !== 'undefined') {
548
+ data.headers['Cc'] = toEmailAddress(data.cc);
549
+ }
550
+
551
+ // if (!data.headers['To']) {
552
+ // throw new Error('Missing "To" e-mail address!');
553
+ // }
554
+
555
+ const emlBoundary = getBoundary(data.headers['Content-Type'] || data.headers['Content-type'] || '');
556
+ let hasBoundary = false;
557
+ let boundary = createBoundary();
558
+ let multipartBoundary = '';
559
+ if (data.multipartAlternative) {
560
+ multipartBoundary = '' + (getBoundary(data.multipartAlternative['Content-Type']) || '');
561
+ hasBoundary = true;
562
+ }
563
+ if (emlBoundary) {
564
+ boundary = emlBoundary;
565
+ hasBoundary = true;
566
+ } else {
567
+ data.headers['Content-Type'] = data.headers['Content-type'] || 'multipart/mixed;' + EOL + 'boundary="' + boundary + '"';
568
+ // Restrained
569
+ // hasBoundary = true;
570
+ }
571
+
572
+ //Build headers
573
+ const keys = Object.keys(data.headers);
574
+ for (let i = 0; i < keys.length; i++) {
575
+ const key = keys[i];
576
+ const value: string | string[] = data.headers[key];
577
+ if (typeof value === 'undefined') {
578
+ continue; //Skip missing headers
579
+ } else if (typeof value === 'string') {
580
+ eml += key + ': ' + value.replace(/\r?\n/g, EOL + ' ') + EOL;
581
+ } else {
582
+ //Array
583
+ for (let j = 0; j < value.length; j++) {
584
+ eml += key + ': ' + value[j].replace(/\r?\n/g, EOL + ' ') + EOL;
585
+ }
586
+ }
587
+ }
588
+
589
+ if (data.multipartAlternative) {
590
+ eml += EOL;
591
+ eml += '--' + emlBoundary + EOL;
592
+ eml += 'Content-Type: ' + data.multipartAlternative['Content-Type'].replace(/\r?\n/g, EOL + ' ') + EOL;
593
+ }
594
+
595
+ //Start the body
596
+ eml += EOL;
597
+
598
+ //Plain text content
599
+ if (data.text) {
600
+ // Encode opened and self headers keeped
601
+ if (typeof options === 'object' && !!options && options.encode && data.textheaders) {
602
+ eml += '--' + boundary + EOL;
603
+ for (const key in data.textheaders) {
604
+ if (data.textheaders.hasOwnProperty(key)) {
605
+ eml += `${key}: ${data.textheaders[key].replace(/\r?\n/g, EOL + ' ')}`;
606
+ }
607
+ }
608
+ } else if (hasBoundary) {
609
+ // else Assembly
610
+ eml += '--' + (multipartBoundary ? multipartBoundary : boundary) + EOL;
611
+ eml += 'Content-Type: text/plain; charset="utf-8"' + EOL;
612
+ }
613
+ eml += EOL + data.text;
614
+ eml += EOL;
615
+ }
616
+
617
+ //HTML content
618
+ if (data.html) {
619
+ // Encode opened and self headers keeped
620
+ if (typeof options === 'object' && !!options && options.encode && data.textheaders) {
621
+ eml += '--' + boundary + EOL;
622
+ for (const key in data.textheaders) {
623
+ if (data.textheaders.hasOwnProperty(key)) {
624
+ eml += `${key}: ${data.textheaders[key].replace(/\r?\n/g, EOL + ' ')}`;
625
+ }
626
+ }
627
+ } else if (hasBoundary) {
628
+ eml += '--' + (multipartBoundary ? multipartBoundary : boundary) + EOL;
629
+ eml += 'Content-Type: text/html; charset="utf-8"' + EOL;
630
+ }
631
+ if (verbose) {
632
+ console.info(
633
+ `line 765 ${hasBoundary}, emlBoundary: ${emlBoundary}, multipartBoundary: ${multipartBoundary}, boundary: ${boundary}`
634
+ );
635
+ }
636
+ eml += EOL + data.html;
637
+ eml += EOL;
638
+ }
639
+
640
+ //Append attachments
641
+ if (data.attachments) {
642
+ for (let i = 0; i < data.attachments.length; i++) {
643
+ const attachment = data.attachments[i];
644
+ eml += '--' + boundary + EOL;
645
+ eml += 'Content-Type: ' + (attachment.contentType.replace(/\r?\n/g, EOL + ' ') || 'application/octet-stream') + EOL;
646
+ eml += 'Content-Transfer-Encoding: base64' + EOL;
647
+ eml +=
648
+ 'Content-Disposition: ' +
649
+ (attachment.inline ? 'inline' : 'attachment') +
650
+ '; filename="' +
651
+ (attachment.filename || attachment.name || 'attachment_' + (i + 1)) +
652
+ '"' +
653
+ EOL;
654
+ if (attachment.cid) {
655
+ eml += 'Content-ID: <' + attachment.cid + '>' + EOL;
656
+ }
657
+ eml += EOL;
658
+ if (typeof attachment.data === 'string') {
659
+ const content = Base64.toBase64(attachment.data);
660
+ eml += wrap(content, 72) + EOL;
661
+ } else {
662
+ //Buffer
663
+ // Uint8Array to string by new TextEncoder
664
+ const content = decode(attachment.data);
665
+ eml += wrap(content, 72) + EOL;
666
+ }
667
+ eml += EOL;
668
+ }
669
+ }
670
+
671
+ //Finish the boundary
672
+ if (hasBoundary) {
673
+ eml += '--' + boundary + '--' + EOL;
674
+ }
675
+ } catch (e) {
676
+ error = e as string;
677
+ }
678
+ callback && callback(error, eml);
679
+ return error || eml;
680
+ }
681
+
682
+ /**
683
+ * Parses EML file content and return user-friendly object.
684
+ * @param {String | ParsedEmlJson} eml EML file content or object from 'parse'
685
+ * @param { OptionOrNull | CallbackFn<ReadedEmlJson>} options EML parse options
686
+ * @param {CallbackFn<ReadedEmlJson>} callback Callback function(error, data)
687
+ */
688
+ function read(
689
+ eml: string | ParsedEmlJson,
690
+ options?: OptionOrNull | CallbackFn<ReadedEmlJson>,
691
+ callback?: CallbackFn<ReadedEmlJson>
692
+ ): ReadedEmlJson | Error | string {
693
+ //Shift arguments
694
+ if (typeof options === 'function' && typeof callback === 'undefined') {
695
+ callback = options;
696
+ options = null;
697
+ }
698
+ let error: Error | string | undefined;
699
+ let result: ReadedEmlJson | undefined;
700
+
701
+ //Appends the boundary to the result
702
+ function _append(headers: EmlHeaders, content: string | Uint8Array | Attachment, result: ReadedEmlJson) {
703
+ const contentType = headers['Content-Type'] || headers['Content-type'];
704
+ const contentDisposition = headers['Content-Disposition'];
705
+
706
+ const charset = getCharsetName(getCharset(contentType as string) || defaultCharset);
707
+ let encoding = headers['Content-Transfer-Encoding'] || headers['Content-transfer-encoding'];
708
+ if (typeof encoding === 'string') {
709
+ encoding = encoding.toLowerCase();
710
+ }
711
+ if (encoding === 'base64') {
712
+ if (contentType && contentType.indexOf('gbk') >= 0) {
713
+ // is work? I'm not sure
714
+ content = encode(GB2312UTF8.GB2312ToUTF8((content as string).replace(/\r?\n/g, '')));
715
+ } else {
716
+ // string to Uint8Array by TextEncoder
717
+ content = encode((content as string).replace(/\r?\n/g, ''));
718
+ }
719
+ } else if (encoding === 'quoted-printable') {
720
+ content = unquotePrintable(content as string, charset);
721
+ } else if (encoding && charset !== 'utf8' && encoding.search(/binary|8bit/) === 0) {
722
+ //'8bit', 'binary', '8bitmime', 'binarymime'
723
+ content = decode(content as Uint8Array, charset);
724
+ }
725
+
726
+ if (!contentDisposition && contentType && contentType.indexOf('text/html') >= 0) {
727
+ if (typeof content !== 'string') {
728
+ content = decode(content as Uint8Array, charset);
729
+ }
730
+
731
+ let htmlContent = content.replace(/\r\n|(&quot;)/g, '').replace(/\"/g, `"`);
732
+
733
+ try {
734
+ if (encoding === 'base64') {
735
+ htmlContent = Base64.decode(htmlContent);
736
+ } else if (Base64.btoa(Base64.atob(htmlContent)) == htmlContent) {
737
+ htmlContent = Base64.atob(htmlContent);
738
+ }
739
+ } catch (error) {
740
+ console.error(error);
741
+ }
742
+
743
+ if (result.html) {
744
+ result.html += htmlContent;
745
+ } else {
746
+ result.html = htmlContent;
747
+ }
748
+
749
+ result.htmlheaders = {
750
+ 'Content-Type': contentType,
751
+ 'Content-Transfer-Encoding': encoding || '',
752
+ };
753
+ // self boundary Not used at conversion
754
+ } else if (!contentDisposition && contentType && contentType.indexOf('text/plain') >= 0) {
755
+ if (typeof content !== 'string') {
756
+ content = decode(content as Uint8Array, charset);
757
+ }
758
+ if (encoding === 'base64') {
759
+ content = Base64.decode(content);
760
+ }
761
+ //Plain text message
762
+
763
+ if (result.text) {
764
+ result.text += content;
765
+ } else {
766
+ result.text = content;
767
+ }
768
+
769
+ result.textheaders = {
770
+ 'Content-Type': contentType,
771
+ 'Content-Transfer-Encoding': encoding || '',
772
+ };
773
+ // self boundary Not used at conversion
774
+ } else {
775
+ //Get the attachment
776
+ if (!result.attachments) {
777
+ result.attachments = [];
778
+ }
779
+
780
+ const attachment = {} as Attachment;
781
+
782
+ const id = headers['Content-ID'] || headers['Content-Id'];
783
+ if (id) {
784
+ attachment.id = id;
785
+ }
786
+ const qaaapId = headers['X-Qaap-Object-Id'];
787
+ if (qaaapId) {
788
+ attachment.qaapId = qaaapId;
789
+ }
790
+
791
+ const NameContainer = ['Content-Disposition', 'Content-Type', 'Content-type'];
792
+
793
+ let result_name;
794
+ for (const key of NameContainer) {
795
+ const name: string = headers[key];
796
+ if (name) {
797
+ result_name = name
798
+ .replace(/(\s|'|utf-8|\*[0-9]\*)/g, '')
799
+ .split(';')
800
+ .map(v => /name[\*]?="?(.+?)"?$/gi.exec(v))
801
+ .reduce((a, b) => {
802
+ if (b && b[1]) {
803
+ a += b[1];
804
+ }
805
+ return a;
806
+ }, '');
807
+ if (result_name) {
808
+ break;
809
+ }
810
+ }
811
+ }
812
+ if (result_name) {
813
+ attachment.name = decodeURI(result_name);
814
+ }
815
+
816
+ const ct = headers['Content-Type'] || headers['Content-type'];
817
+ if (ct) {
818
+ attachment.contentType = ct;
819
+ }
820
+
821
+ const cd = headers['Content-Disposition'];
822
+ if (cd) {
823
+ attachment.inline = /^\s*inline/g.test(cd);
824
+ }
825
+
826
+ attachment.data = content as Uint8Array;
827
+ attachment.data64 = decode(content as Uint8Array, charset);
828
+ result.attachments.push(attachment);
829
+ }
830
+ }
831
+
832
+ function _read(data: ParsedEmlJson): ReadedEmlJson | Error | string {
833
+ if (!data) {
834
+ return 'no data';
835
+ }
836
+ try {
837
+ const result = {} as ReadedEmlJson;
838
+ if (!data.headers) {
839
+ throw new Error("data does't has headers");
840
+ }
841
+ if (data.headers['Date']) {
842
+ result.date = new Date(data.headers['Date']);
843
+ }
844
+ if (data.headers['Subject']) {
845
+ result.subject = unquoteString(data.headers['Subject']);
846
+ }
847
+ if (data.headers['From']) {
848
+ result.from = getEmailAddress(data.headers['From']);
849
+ }
850
+ if (data.headers['To']) {
851
+ result.to = getEmailAddress(data.headers['To']);
852
+ }
853
+ if (data.headers['CC']) {
854
+ result.cc = getEmailAddress(data.headers['CC']);
855
+ }
856
+ if (data.headers['Cc']) {
857
+ result.cc = getEmailAddress(data.headers['Cc']);
858
+ }
859
+ result.headers = data.headers;
860
+
861
+ //Content mime type
862
+ let boundary: any = null;
863
+ const ct = data.headers['Content-Type'] || data.headers['Content-type'];
864
+ if (ct && /^multipart\//g.test(ct)) {
865
+ const b = getBoundary(ct);
866
+ if (b && b.length) {
867
+ boundary = b;
868
+ }
869
+ }
870
+
871
+ if (boundary && Array.isArray(data.body)) {
872
+ for (let i = 0; i < data.body.length; i++) {
873
+ const boundaryBlock = data.body[i];
874
+ if (!boundaryBlock) {
875
+ continue;
876
+ }
877
+ //Get the message content
878
+ if (typeof boundaryBlock.part === 'undefined') {
879
+ verbose && console.warn('Warning: undefined b.part');
880
+ } else if (typeof boundaryBlock.part === 'string') {
881
+ result.data = boundaryBlock.part;
882
+ } else {
883
+ if (typeof boundaryBlock.part.body === 'undefined') {
884
+ verbose && console.warn('Warning: undefined b.part.body');
885
+ } else if (typeof boundaryBlock.part.body === 'string') {
886
+ _append(boundaryBlock.part.headers, boundaryBlock.part.body, result);
887
+ } else {
888
+ // keep multipart/alternative
889
+ const currentHeaders = boundaryBlock.part.headers;
890
+ const currentHeadersContentType = currentHeaders['Content-Type'] || currentHeaders['Content-type'];
891
+ if (verbose) {
892
+ console.log(`line 969 currentHeadersContentType: ${currentHeadersContentType}`);
893
+ }
894
+ // Hasmore ?
895
+ if (currentHeadersContentType && currentHeadersContentType.indexOf('multipart') >= 0 && !result.multipartAlternative) {
896
+ result.multipartAlternative = {
897
+ 'Content-Type': currentHeadersContentType,
898
+ };
899
+ }
900
+ for (let j = 0; j < boundaryBlock.part.body.length; j++) {
901
+ const selfBoundary = boundaryBlock.part.body[j];
902
+ if (typeof selfBoundary === 'string') {
903
+ result.data = selfBoundary;
904
+ continue;
905
+ }
906
+
907
+ const headers = selfBoundary.part.headers;
908
+ const content = selfBoundary.part.body;
909
+ if (Array.isArray(content)) {
910
+ (content as any).forEach((bound: any) => {
911
+ _append(bound.part.headers, bound.part.body, result);
912
+ });
913
+ } else {
914
+ _append(headers, content, result);
915
+ }
916
+ }
917
+ }
918
+ }
919
+ }
920
+ } else if (typeof data.body === 'string') {
921
+ _append(data.headers, data.body, result);
922
+ }
923
+ return result;
924
+ } catch (e) {
925
+ return e as any;
926
+ }
927
+ }
928
+
929
+ if (typeof eml === 'string') {
930
+ const parseResult = parse(eml, options as OptionOrNull);
931
+ if (typeof parseResult === 'string' || parseResult instanceof Error) {
932
+ error = parseResult;
933
+ } else {
934
+ const readResult = _read(parseResult);
935
+ if (typeof readResult === 'string' || readResult instanceof Error) {
936
+ error = readResult;
937
+ } else {
938
+ result = readResult;
939
+ }
940
+ }
941
+ } else if (typeof eml === 'object') {
942
+ const readResult = _read(eml);
943
+ if (typeof readResult === 'string' || readResult instanceof Error) {
944
+ error = readResult;
945
+ } else {
946
+ result = readResult;
947
+ }
948
+ } else {
949
+ error = new Error('Missing EML file content!');
950
+ }
951
+ callback && callback(error, result);
952
+ return error || result || new Error('read EML failed!');
953
+ }
954
+
955
+ /**
956
+ * if you need
957
+ * eml-format all api
958
+ */
959
+ export {
960
+ getEmailAddress,
961
+ toEmailAddress,
962
+ createBoundary,
963
+ getBoundary,
964
+ getCharset,
965
+ unquoteString,
966
+ unquotePrintable,
967
+ mimeDecode,
968
+ Base64,
969
+ convert,
970
+ encode,
971
+ decode,
972
+ completeBoundary,
973
+ parse as parseEml,
974
+ read as readEml,
975
+ build as buildEml,
976
+ GB2312UTF8 as GBKUTF8,
977
+ };
978
+
979
+ // const GBKUTF8 = GB2312UTF8;
980
+
981
+ // const parseEml = parse;
982
+ // const readEml = read;
983
+ // const buildEml = build;