postal-mime 2.4.4 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +9 -0
- package/.prettierrc.json +10 -0
- package/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/package.json +6 -3
- package/postal-mime.d.ts +9 -4
- package/src/decode-strings.js +33 -21
- package/src/mime-node.js +25 -6
- package/src/postal-mime.js +19 -5
- package/src/qp-decoder.js +36 -9
- package/src/text-format.js +24 -7
package/.prettierignore
ADDED
package/.prettierrc.json
ADDED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.6](https://github.com/postalsys/postal-mime/compare/v2.4.5...v2.4.6) (2025-10-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* add security limits for MIME parsing ([defbf11](https://github.com/postalsys/postal-mime/commit/defbf11e85c8233e6ff01a3d6fc10534b784c499))
|
|
9
|
+
|
|
10
|
+
## [2.4.5](https://github.com/postalsys/postal-mime/compare/v2.4.4...v2.4.5) (2025-09-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* handle broken quoted-printable sequences and add prettier formatter ([ec3bc64](https://github.com/postalsys/postal-mime/commit/ec3bc647f6e262a7ac3a51e30bbe3b07b6e7db7a))
|
|
16
|
+
|
|
3
17
|
## [2.4.4](https://github.com/postalsys/postal-mime/compare/v2.4.3...v2.4.4) (2025-06-26)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -111,6 +111,8 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
|
|
|
111
111
|
- `"base64"`
|
|
112
112
|
- `"utf8"`
|
|
113
113
|
- `"arraybuffer"` (no decoding, returns `ArrayBuffer`)
|
|
114
|
+
- **maxNestingDepth** (number, default: `256`): Maximum allowed MIME part nesting depth. Throws an error if exceeded.
|
|
115
|
+
- **maxHeadersSize** (number, default: `2097152`): Maximum allowed total header size in bytes (default 2MB). Throws an error if exceeded.
|
|
114
116
|
|
|
115
117
|
**Returns**: A Promise that resolves to a structured object with the following properties:
|
|
116
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postal-mime",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
4
4
|
"description": "Email parser for browser environments",
|
|
5
5
|
"main": "./src/postal-mime.js",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"types": "postal-mime.d.ts",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "eslint && node --test",
|
|
15
|
-
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
|
|
15
|
+
"update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
|
|
16
|
+
"format": "prettier --write \"**/*.{js,mjs,json}\" --ignore-path .prettierignore",
|
|
17
|
+
"format:check": "prettier --check \"**/*.{js,mjs,json}\" --ignore-path .prettierignore"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
18
20
|
"mime",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"cross-env": "7.0.3",
|
|
34
36
|
"eslint": "8.57.0",
|
|
35
37
|
"eslint-cli": "1.1.1",
|
|
36
|
-
"iframe-resizer": "4.3.6"
|
|
38
|
+
"iframe-resizer": "4.3.6",
|
|
39
|
+
"prettier": "^3.6.2"
|
|
37
40
|
}
|
|
38
41
|
}
|
package/postal-mime.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type RawEmail = string | ArrayBuffer | Uint8Array | Blob | Buffer | ReadableStream;
|
|
2
2
|
|
|
3
|
-
export type Header =
|
|
3
|
+
export type Header = {
|
|
4
|
+
key: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
4
7
|
|
|
5
8
|
export type Address = {
|
|
6
9
|
name: string;
|
|
@@ -22,7 +25,7 @@ export type Attachment = {
|
|
|
22
25
|
|
|
23
26
|
export type Email = {
|
|
24
27
|
headers: Header[];
|
|
25
|
-
from
|
|
28
|
+
from?: Address;
|
|
26
29
|
sender?: Address;
|
|
27
30
|
replyTo?: Address[];
|
|
28
31
|
deliveredTo?: string;
|
|
@@ -31,7 +34,7 @@ export type Email = {
|
|
|
31
34
|
cc?: Address[];
|
|
32
35
|
bcc?: Address[];
|
|
33
36
|
subject?: string;
|
|
34
|
-
messageId
|
|
37
|
+
messageId?: string;
|
|
35
38
|
inReplyTo?: string;
|
|
36
39
|
references?: string;
|
|
37
40
|
date?: string;
|
|
@@ -56,7 +59,9 @@ declare function decodeWords (
|
|
|
56
59
|
declare type PostalMimeOptions = {
|
|
57
60
|
rfc822Attachments?: boolean,
|
|
58
61
|
forceRfc822Attachments?: boolean,
|
|
59
|
-
attachmentEncoding?: "base64" | "utf8" | "arraybuffer"
|
|
62
|
+
attachmentEncoding?: "base64" | "utf8" | "arraybuffer",
|
|
63
|
+
maxNestingDepth?: number,
|
|
64
|
+
maxHeadersSize?: number
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
declare class PostalMime {
|
package/src/decode-strings.js
CHANGED
|
@@ -81,7 +81,11 @@ export async function blobToArrayBuffer(blob) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function getHex(c) {
|
|
84
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
(c >= 0x30 /* 0 */ && c <= 0x39) /* 9 */ ||
|
|
86
|
+
(c >= 0x61 /* a */ && c <= 0x66) /* f */ ||
|
|
87
|
+
(c >= 0x41 /* A */ && c <= 0x46) /* F */
|
|
88
|
+
) {
|
|
85
89
|
return String.fromCharCode(c);
|
|
86
90
|
}
|
|
87
91
|
return false;
|
|
@@ -152,36 +156,44 @@ export function decodeWords(str) {
|
|
|
152
156
|
let result = (str || '')
|
|
153
157
|
.toString()
|
|
154
158
|
// find base64 words that can be joined
|
|
155
|
-
.replace(
|
|
156
|
-
|
|
159
|
+
.replace(
|
|
160
|
+
/(=\?([^?]+)\?[Bb]\?([^?]*)\?=)\s*(?==\?([^?]+)\?[Bb]\?[^?]*\?=)/g,
|
|
161
|
+
(match, left, chLeft, encodedLeftStr, chRight) => {
|
|
162
|
+
if (!joinString) {
|
|
163
|
+
return match;
|
|
164
|
+
}
|
|
165
|
+
// only mark b64 chunks to be joined if charsets match and left side does not end with =
|
|
166
|
+
if (chLeft === chRight && encodedLeftStr.length % 4 === 0 && !/=$/.test(encodedLeftStr)) {
|
|
167
|
+
// set a joiner marker
|
|
168
|
+
return left + '__\x00JOIN\x00__';
|
|
169
|
+
}
|
|
170
|
+
|
|
157
171
|
return match;
|
|
158
172
|
}
|
|
159
|
-
|
|
160
|
-
if (chLeft === chRight && encodedLeftStr.length % 4 === 0 && !/=$/.test(encodedLeftStr)) {
|
|
161
|
-
// set a joiner marker
|
|
162
|
-
return left + '__\x00JOIN\x00__';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return match;
|
|
166
|
-
})
|
|
173
|
+
)
|
|
167
174
|
// find QP words that can be joined
|
|
168
|
-
.replace(
|
|
169
|
-
|
|
175
|
+
.replace(
|
|
176
|
+
/(=\?([^?]+)\?[Qq]\?[^?]*\?=)\s*(?==\?([^?]+)\?[Qq]\?[^?]*\?=)/g,
|
|
177
|
+
(match, left, chLeft, chRight) => {
|
|
178
|
+
if (!joinString) {
|
|
179
|
+
return match;
|
|
180
|
+
}
|
|
181
|
+
// only mark QP chunks to be joined if charsets match
|
|
182
|
+
if (chLeft === chRight) {
|
|
183
|
+
// set a joiner marker
|
|
184
|
+
return left + '__\x00JOIN\x00__';
|
|
185
|
+
}
|
|
170
186
|
return match;
|
|
171
187
|
}
|
|
172
|
-
|
|
173
|
-
if (chLeft === chRight) {
|
|
174
|
-
// set a joiner marker
|
|
175
|
-
return left + '__\x00JOIN\x00__';
|
|
176
|
-
}
|
|
177
|
-
return match;
|
|
178
|
-
})
|
|
188
|
+
)
|
|
179
189
|
// join base64 encoded words
|
|
180
190
|
.replace(/(\?=)?__\x00JOIN\x00__(=\?([^?]+)\?[QqBb]\?)?/g, '')
|
|
181
191
|
// remove spaces between mime encoded words
|
|
182
192
|
.replace(/(=\?[^?]+\?[QqBb]\?[^?]*\?=)\s+(?==\?[^?]+\?[QqBb]\?[^?]*\?=)/g, '$1')
|
|
183
193
|
// decode words
|
|
184
|
-
.replace(/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g, (m, charset, encoding, text) =>
|
|
194
|
+
.replace(/=\?([\w_\-*]+)\?([QqBb])\?([^?]*)\?=/g, (m, charset, encoding, text) =>
|
|
195
|
+
decodeWord(charset, encoding, text)
|
|
196
|
+
);
|
|
185
197
|
|
|
186
198
|
if (joinString && result.indexOf('\ufffd') >= 0) {
|
|
187
199
|
// text contains \ufffd (EF BF BD), so unicode conversion failed, retry without joining strings
|
package/src/mime-node.js
CHANGED
|
@@ -4,20 +4,31 @@ import Base64Decoder from './base64-decoder.js';
|
|
|
4
4
|
import QPDecoder from './qp-decoder.js';
|
|
5
5
|
|
|
6
6
|
export default class MimeNode {
|
|
7
|
-
constructor(
|
|
8
|
-
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options || {};
|
|
9
9
|
|
|
10
|
-
this.postalMime =
|
|
10
|
+
this.postalMime = this.options.postalMime;
|
|
11
11
|
|
|
12
|
-
this.root = !!
|
|
12
|
+
this.root = !!this.options.parentNode;
|
|
13
13
|
this.childNodes = [];
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
if (this.options.parentNode) {
|
|
16
|
+
this.parentNode = this.options.parentNode;
|
|
17
|
+
|
|
18
|
+
this.depth = this.parentNode.depth + 1;
|
|
19
|
+
if (this.depth > this.options.maxNestingDepth) {
|
|
20
|
+
throw new Error(`Maximum MIME nesting depth of ${this.options.maxNestingDepth} levels exceeded`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.options.parentNode.childNodes.push(this);
|
|
24
|
+
} else {
|
|
25
|
+
this.depth = 0;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
this.state = 'header';
|
|
19
29
|
|
|
20
30
|
this.headerLines = [];
|
|
31
|
+
this.headerSize = 0;
|
|
21
32
|
|
|
22
33
|
this.contentType = {
|
|
23
34
|
value: 'text/plain',
|
|
@@ -262,6 +273,14 @@ export default class MimeNode {
|
|
|
262
273
|
this.state = 'body';
|
|
263
274
|
return this.processHeaders();
|
|
264
275
|
}
|
|
276
|
+
|
|
277
|
+
this.headerSize += line.length;
|
|
278
|
+
|
|
279
|
+
if (this.headerSize > this.options.maxHeadersSize) {
|
|
280
|
+
let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
|
|
265
284
|
this.headerLines.push(getDecoder().decode(line));
|
|
266
285
|
break;
|
|
267
286
|
case 'body': {
|
package/src/postal-mime.js
CHANGED
|
@@ -6,6 +6,9 @@ import { base64ArrayBuffer } from './base64-encoder.js';
|
|
|
6
6
|
|
|
7
7
|
export { addressParser, decodeWords };
|
|
8
8
|
|
|
9
|
+
const MAX_NESTING_DEPTH = 256;
|
|
10
|
+
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
11
|
+
|
|
9
12
|
export default class PostalMime {
|
|
10
13
|
static parse(buf, options) {
|
|
11
14
|
const parser = new PostalMime(options);
|
|
@@ -14,9 +17,14 @@ export default class PostalMime {
|
|
|
14
17
|
|
|
15
18
|
constructor(options) {
|
|
16
19
|
this.options = options || {};
|
|
20
|
+
this.mimeOptions = {
|
|
21
|
+
maxNestingDepth: this.options.maxNestingDepth || MAX_NESTING_DEPTH,
|
|
22
|
+
maxHeadersSize: this.options.maxHeadersSize || MAX_HEADERS_SIZE
|
|
23
|
+
};
|
|
17
24
|
|
|
18
25
|
this.root = this.currentNode = new MimeNode({
|
|
19
|
-
postalMime: this
|
|
26
|
+
postalMime: this,
|
|
27
|
+
...this.mimeOptions
|
|
20
28
|
});
|
|
21
29
|
this.boundaries = [];
|
|
22
30
|
|
|
@@ -78,7 +86,8 @@ export default class PostalMime {
|
|
|
78
86
|
|
|
79
87
|
this.currentNode = new MimeNode({
|
|
80
88
|
postalMime: this,
|
|
81
|
-
parentNode: boundary.node
|
|
89
|
+
parentNode: boundary.node,
|
|
90
|
+
...this.mimeOptions
|
|
82
91
|
});
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -190,7 +199,8 @@ export default class PostalMime {
|
|
|
190
199
|
|
|
191
200
|
// is it an attachment
|
|
192
201
|
else if (node.content) {
|
|
193
|
-
const filename =
|
|
202
|
+
const filename =
|
|
203
|
+
node.contentDisposition.parsed.params.filename || node.contentType.parsed.params.name || null;
|
|
194
204
|
const attachment = {
|
|
195
205
|
filename: filename ? decodeWords(filename) : null,
|
|
196
206
|
mimeType: node.contentType.parsed.value,
|
|
@@ -214,7 +224,10 @@ export default class PostalMime {
|
|
|
214
224
|
case 'text/calendar':
|
|
215
225
|
case 'application/ics': {
|
|
216
226
|
if (node.contentType.parsed.params.method) {
|
|
217
|
-
attachment.method = node.contentType.parsed.params.method
|
|
227
|
+
attachment.method = node.contentType.parsed.params.method
|
|
228
|
+
.toString()
|
|
229
|
+
.toUpperCase()
|
|
230
|
+
.trim();
|
|
218
231
|
}
|
|
219
232
|
|
|
220
233
|
// Enforce into unicode
|
|
@@ -341,7 +354,8 @@ export default class PostalMime {
|
|
|
341
354
|
if (node.contentType.parsed.value !== 'message/rfc822') {
|
|
342
355
|
return false;
|
|
343
356
|
}
|
|
344
|
-
let disposition =
|
|
357
|
+
let disposition =
|
|
358
|
+
node.contentDisposition.parsed.value || (this.options.rfc822Attachments ? 'attachment' : 'inline');
|
|
345
359
|
return disposition === 'inline';
|
|
346
360
|
}
|
|
347
361
|
|
package/src/qp-decoder.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { blobToArrayBuffer } from './decode-strings.js';
|
|
2
2
|
|
|
3
|
+
// Regex patterns compiled once for performance
|
|
4
|
+
const VALID_QP_REGEX = /^=[a-f0-9]{2}$/i;
|
|
5
|
+
const QP_SPLIT_REGEX = /(?==[a-f0-9]{2})/i;
|
|
6
|
+
const SOFT_LINE_BREAK_REGEX = /=\r?\n/g;
|
|
7
|
+
const PARTIAL_QP_ENDING_REGEX = /=[a-fA-F0-9]?$/;
|
|
8
|
+
|
|
3
9
|
export default class QPDecoder {
|
|
4
10
|
constructor(opts) {
|
|
5
11
|
opts = opts || {};
|
|
@@ -24,9 +30,9 @@ export default class QPDecoder {
|
|
|
24
30
|
|
|
25
31
|
decodeChunks(str) {
|
|
26
32
|
// unwrap newlines
|
|
27
|
-
str = str.replace(
|
|
33
|
+
str = str.replace(SOFT_LINE_BREAK_REGEX, '');
|
|
28
34
|
|
|
29
|
-
let list = str.split(
|
|
35
|
+
let list = str.split(QP_SPLIT_REGEX);
|
|
30
36
|
let encodedBytes = [];
|
|
31
37
|
for (let part of list) {
|
|
32
38
|
if (part.charAt(0) !== '=') {
|
|
@@ -39,17 +45,38 @@ export default class QPDecoder {
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
if (part.length === 3) {
|
|
42
|
-
|
|
48
|
+
// Validate that this is actually a valid QP sequence
|
|
49
|
+
if (VALID_QP_REGEX.test(part)) {
|
|
50
|
+
encodedBytes.push(part.substr(1));
|
|
51
|
+
} else {
|
|
52
|
+
// Not a valid QP sequence, treat as literal text
|
|
53
|
+
if (encodedBytes.length) {
|
|
54
|
+
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
55
|
+
encodedBytes = [];
|
|
56
|
+
}
|
|
57
|
+
this.chunks.push(part);
|
|
58
|
+
}
|
|
43
59
|
continue;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
if (part.length > 3) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
// First 3 chars should be a valid QP sequence
|
|
64
|
+
const firstThree = part.substr(0, 3);
|
|
65
|
+
if (VALID_QP_REGEX.test(firstThree)) {
|
|
66
|
+
encodedBytes.push(part.substr(1, 2));
|
|
67
|
+
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
68
|
+
encodedBytes = [];
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
part = part.substr(3);
|
|
71
|
+
this.chunks.push(part);
|
|
72
|
+
} else {
|
|
73
|
+
// Not a valid QP sequence, treat entire part as literal
|
|
74
|
+
if (encodedBytes.length) {
|
|
75
|
+
this.chunks.push(this.decodeQPBytes(encodedBytes));
|
|
76
|
+
encodedBytes = [];
|
|
77
|
+
}
|
|
78
|
+
this.chunks.push(part);
|
|
79
|
+
}
|
|
53
80
|
}
|
|
54
81
|
}
|
|
55
82
|
if (encodedBytes.length) {
|
|
@@ -71,7 +98,7 @@ export default class QPDecoder {
|
|
|
71
98
|
|
|
72
99
|
this.remainder = '';
|
|
73
100
|
|
|
74
|
-
let partialEnding = str.match(
|
|
101
|
+
let partialEnding = str.match(PARTIAL_QP_ENDING_REGEX);
|
|
75
102
|
if (partialEnding) {
|
|
76
103
|
if (partialEnding.index === 0) {
|
|
77
104
|
this.remainder = str;
|
package/src/text-format.js
CHANGED
|
@@ -177,7 +177,10 @@ function foldLines(str, lineLength, afterSpace) {
|
|
|
177
177
|
result += line;
|
|
178
178
|
pos += line.length;
|
|
179
179
|
continue;
|
|
180
|
-
} else if (
|
|
180
|
+
} else if (
|
|
181
|
+
(match = line.match(/(\s+)[^\s]*$/)) &&
|
|
182
|
+
match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length
|
|
183
|
+
) {
|
|
181
184
|
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
|
|
182
185
|
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
|
183
186
|
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
|
|
@@ -215,7 +218,10 @@ export function formatTextHeader(message) {
|
|
|
215
218
|
hour12: false
|
|
216
219
|
};
|
|
217
220
|
|
|
218
|
-
let dateStr =
|
|
221
|
+
let dateStr =
|
|
222
|
+
typeof Intl === 'undefined'
|
|
223
|
+
? message.date
|
|
224
|
+
: new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
|
|
219
225
|
|
|
220
226
|
rows.push({ key: 'Date', val: dateStr });
|
|
221
227
|
}
|
|
@@ -283,7 +289,9 @@ export function formatHtmlHeader(message) {
|
|
|
283
289
|
let rows = [];
|
|
284
290
|
|
|
285
291
|
if (message.from) {
|
|
286
|
-
rows.push(
|
|
292
|
+
rows.push(
|
|
293
|
+
`<div class="postal-email-header-key">From</div><div class="postal-email-header-value">${formatHtmlAddress(message.from)}</div>`
|
|
294
|
+
);
|
|
287
295
|
}
|
|
288
296
|
|
|
289
297
|
if (message.subject) {
|
|
@@ -305,7 +313,10 @@ export function formatHtmlHeader(message) {
|
|
|
305
313
|
hour12: false
|
|
306
314
|
};
|
|
307
315
|
|
|
308
|
-
let dateStr =
|
|
316
|
+
let dateStr =
|
|
317
|
+
typeof Intl === 'undefined'
|
|
318
|
+
? message.date
|
|
319
|
+
: new Intl.DateTimeFormat('default', dateOptions).format(new Date(message.date));
|
|
309
320
|
|
|
310
321
|
rows.push(
|
|
311
322
|
`<div class="postal-email-header-key">Date</div><div class="postal-email-header-value postal-email-header-date" data-date="${escapeHtml(
|
|
@@ -315,15 +326,21 @@ export function formatHtmlHeader(message) {
|
|
|
315
326
|
}
|
|
316
327
|
|
|
317
328
|
if (message.to && message.to.length) {
|
|
318
|
-
rows.push(
|
|
329
|
+
rows.push(
|
|
330
|
+
`<div class="postal-email-header-key">To</div><div class="postal-email-header-value">${formatHtmlAddresses(message.to)}</div>`
|
|
331
|
+
);
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
if (message.cc && message.cc.length) {
|
|
322
|
-
rows.push(
|
|
335
|
+
rows.push(
|
|
336
|
+
`<div class="postal-email-header-key">Cc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.cc)}</div>`
|
|
337
|
+
);
|
|
323
338
|
}
|
|
324
339
|
|
|
325
340
|
if (message.bcc && message.bcc.length) {
|
|
326
|
-
rows.push(
|
|
341
|
+
rows.push(
|
|
342
|
+
`<div class="postal-email-header-key">Bcc</div><div class="postal-email-header-value">${formatHtmlAddresses(message.bcc)}</div>`
|
|
343
|
+
);
|
|
327
344
|
}
|
|
328
345
|
|
|
329
346
|
let template = `<div class="postal-email-header">${rows.length ? '<div class="postal-email-header-row">' : ''}${rows.join(
|