postal-mime 2.5.0 → 2.6.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.1](https://github.com/postalsys/postal-mime/compare/v2.6.0...v2.6.1) (2025-11-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * prevent DoS from deeply nested address groups ([f509eaf](https://github.com/postalsys/postal-mime/commit/f509eafec31bf448482133041c086f7aefa2b3fa))
9
+ * update TypeScript typings to match source code ([df5640a](https://github.com/postalsys/postal-mime/commit/df5640a3166b0a1fe3ca563e1364301bb4e6a025))
10
+
11
+ ## [2.6.0](https://github.com/postalsys/postal-mime/compare/v2.5.0...v2.6.0) (2025-10-24)
12
+
13
+
14
+ ### Features
15
+
16
+ * add CommonJS build support for dual package compatibility ([0ea3de3](https://github.com/postalsys/postal-mime/commit/0ea3de39c713aac2bc79218dfb496c0e2ff4b0b8))
17
+
3
18
  ## [2.5.0](https://github.com/postalsys/postal-mime/compare/v2.4.7...v2.5.0) (2025-10-07)
4
19
 
5
20
 
package/README.md CHANGED
@@ -125,6 +125,25 @@ console.log(util.inspect(email, false, 22, true));
125
125
 
126
126
  </details>
127
127
 
128
+ ### CommonJS
129
+
130
+ For projects using CommonJS (with `require()`), postal-mime automatically provides the CommonJS build:
131
+
132
+ ```js
133
+ const PostalMime = require('postal-mime');
134
+ const { addressParser, decodeWords } = require('postal-mime');
135
+
136
+ const email = await PostalMime.parse(`Subject: My awesome email 🤓
137
+ Content-Type: text/html; charset=utf-8
138
+
139
+ <p>Hello world 😵‍💫</p>`);
140
+
141
+ console.log(email.subject); // "My awesome email 🤓"
142
+ ```
143
+
144
+ > [!NOTE]
145
+ > The CommonJS build is automatically generated from the ESM source code during the build process. The package supports dual module format, so both `import` and `require()` work seamlessly.
146
+
128
147
  ### Cloudflare Email Workers
129
148
 
130
149
  Use the `message.raw` as the raw email data for parsing:
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var address_parser_exports = {};
20
+ __export(address_parser_exports, {
21
+ default: () => address_parser_default
22
+ });
23
+ module.exports = __toCommonJS(address_parser_exports);
24
+ var import_decode_strings = require("./decode-strings.cjs");
25
+ function _handleAddress(tokens, depth) {
26
+ let isGroup = false;
27
+ let state = "text";
28
+ let address;
29
+ let addresses = [];
30
+ let data = {
31
+ address: [],
32
+ comment: [],
33
+ group: [],
34
+ text: [],
35
+ textWasQuoted: []
36
+ // Track which text tokens came from inside quotes
37
+ };
38
+ let i;
39
+ let len;
40
+ let insideQuotes = false;
41
+ for (i = 0, len = tokens.length; i < len; i++) {
42
+ let token = tokens[i];
43
+ let prevToken = i ? tokens[i - 1] : null;
44
+ if (token.type === "operator") {
45
+ switch (token.value) {
46
+ case "<":
47
+ state = "address";
48
+ insideQuotes = false;
49
+ break;
50
+ case "(":
51
+ state = "comment";
52
+ insideQuotes = false;
53
+ break;
54
+ case ":":
55
+ state = "group";
56
+ isGroup = true;
57
+ insideQuotes = false;
58
+ break;
59
+ case '"':
60
+ insideQuotes = !insideQuotes;
61
+ state = "text";
62
+ break;
63
+ default:
64
+ state = "text";
65
+ insideQuotes = false;
66
+ break;
67
+ }
68
+ } else if (token.value) {
69
+ if (state === "address") {
70
+ token.value = token.value.replace(/^[^<]*<\s*/, "");
71
+ }
72
+ if (prevToken && prevToken.noBreak && data[state].length) {
73
+ data[state][data[state].length - 1] += token.value;
74
+ if (state === "text" && insideQuotes) {
75
+ data.textWasQuoted[data.textWasQuoted.length - 1] = true;
76
+ }
77
+ } else {
78
+ data[state].push(token.value);
79
+ if (state === "text") {
80
+ data.textWasQuoted.push(insideQuotes);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ if (!data.text.length && data.comment.length) {
86
+ data.text = data.comment;
87
+ data.comment = [];
88
+ }
89
+ if (isGroup) {
90
+ data.text = data.text.join(" ");
91
+ let groupMembers = [];
92
+ if (data.group.length) {
93
+ let parsedGroup = addressParser(data.group.join(","), { _depth: depth + 1 });
94
+ parsedGroup.forEach((member) => {
95
+ if (member.group) {
96
+ groupMembers = groupMembers.concat(member.group);
97
+ } else {
98
+ groupMembers.push(member);
99
+ }
100
+ });
101
+ }
102
+ addresses.push({
103
+ name: (0, import_decode_strings.decodeWords)(data.text || address && address.name),
104
+ group: groupMembers
105
+ });
106
+ } else {
107
+ if (!data.address.length && data.text.length) {
108
+ for (i = data.text.length - 1; i >= 0; i--) {
109
+ if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
110
+ data.address = data.text.splice(i, 1);
111
+ data.textWasQuoted.splice(i, 1);
112
+ break;
113
+ }
114
+ }
115
+ let _regexHandler = function(address2) {
116
+ if (!data.address.length) {
117
+ data.address = [address2.trim()];
118
+ return " ";
119
+ } else {
120
+ return address2;
121
+ }
122
+ };
123
+ if (!data.address.length) {
124
+ for (i = data.text.length - 1; i >= 0; i--) {
125
+ if (!data.textWasQuoted[i]) {
126
+ data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
127
+ if (data.address.length) {
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ if (!data.text.length && data.comment.length) {
135
+ data.text = data.comment;
136
+ data.comment = [];
137
+ }
138
+ if (data.address.length > 1) {
139
+ data.text = data.text.concat(data.address.splice(1));
140
+ }
141
+ data.text = data.text.join(" ");
142
+ data.address = data.address.join(" ");
143
+ if (!data.address && /^=\?[^=]+?=$/.test(data.text.trim())) {
144
+ const parsedSubAddresses = addressParser((0, import_decode_strings.decodeWords)(data.text));
145
+ if (parsedSubAddresses && parsedSubAddresses.length) {
146
+ return parsedSubAddresses;
147
+ }
148
+ }
149
+ if (!data.address && isGroup) {
150
+ return [];
151
+ } else {
152
+ address = {
153
+ address: data.address || data.text || "",
154
+ name: (0, import_decode_strings.decodeWords)(data.text || data.address || "")
155
+ };
156
+ if (address.address === address.name) {
157
+ if ((address.address || "").match(/@/)) {
158
+ address.name = "";
159
+ } else {
160
+ address.address = "";
161
+ }
162
+ }
163
+ addresses.push(address);
164
+ }
165
+ }
166
+ return addresses;
167
+ }
168
+ class Tokenizer {
169
+ constructor(str) {
170
+ this.str = (str || "").toString();
171
+ this.operatorCurrent = "";
172
+ this.operatorExpecting = "";
173
+ this.node = null;
174
+ this.escaped = false;
175
+ this.list = [];
176
+ this.operators = {
177
+ '"': '"',
178
+ "(": ")",
179
+ "<": ">",
180
+ ",": "",
181
+ ":": ";",
182
+ // Semicolons are not a legal delimiter per the RFC2822 grammar other
183
+ // than for terminating a group, but they are also not valid for any
184
+ // other use in this context. Given that some mail clients have
185
+ // historically allowed the semicolon as a delimiter equivalent to the
186
+ // comma in their UI, it makes sense to treat them the same as a comma
187
+ // when used outside of a group.
188
+ ";": ""
189
+ };
190
+ }
191
+ /**
192
+ * Tokenizes the original input string
193
+ *
194
+ * @return {Array} An array of operator|text tokens
195
+ */
196
+ tokenize() {
197
+ let list = [];
198
+ for (let i = 0, len = this.str.length; i < len; i++) {
199
+ let chr = this.str.charAt(i);
200
+ let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
201
+ this.checkChar(chr, nextChr);
202
+ }
203
+ this.list.forEach((node) => {
204
+ node.value = (node.value || "").toString().trim();
205
+ if (node.value) {
206
+ list.push(node);
207
+ }
208
+ });
209
+ return list;
210
+ }
211
+ /**
212
+ * Checks if a character is an operator or text and acts accordingly
213
+ *
214
+ * @param {String} chr Character from the address field
215
+ */
216
+ checkChar(chr, nextChr) {
217
+ if (this.escaped) {
218
+ } else if (chr === this.operatorExpecting) {
219
+ this.node = {
220
+ type: "operator",
221
+ value: chr
222
+ };
223
+ if (nextChr && ![" ", " ", "\r", "\n", ",", ";"].includes(nextChr)) {
224
+ this.node.noBreak = true;
225
+ }
226
+ this.list.push(this.node);
227
+ this.node = null;
228
+ this.operatorExpecting = "";
229
+ this.escaped = false;
230
+ return;
231
+ } else if (!this.operatorExpecting && chr in this.operators) {
232
+ this.node = {
233
+ type: "operator",
234
+ value: chr
235
+ };
236
+ this.list.push(this.node);
237
+ this.node = null;
238
+ this.operatorExpecting = this.operators[chr];
239
+ this.escaped = false;
240
+ return;
241
+ } else if (['"', "'"].includes(this.operatorExpecting) && chr === "\\") {
242
+ this.escaped = true;
243
+ return;
244
+ }
245
+ if (!this.node) {
246
+ this.node = {
247
+ type: "text",
248
+ value: ""
249
+ };
250
+ this.list.push(this.node);
251
+ }
252
+ if (chr === "\n") {
253
+ chr = " ";
254
+ }
255
+ if (chr.charCodeAt(0) >= 33 || [" ", " "].includes(chr)) {
256
+ this.node.value += chr;
257
+ }
258
+ this.escaped = false;
259
+ }
260
+ }
261
+ const MAX_NESTED_GROUP_DEPTH = 50;
262
+ function addressParser(str, options) {
263
+ options = options || {};
264
+ let depth = options._depth || 0;
265
+ if (depth > MAX_NESTED_GROUP_DEPTH) {
266
+ return [];
267
+ }
268
+ let tokenizer = new Tokenizer(str);
269
+ let tokens = tokenizer.tokenize();
270
+ let addresses = [];
271
+ let address = [];
272
+ let parsedAddresses = [];
273
+ tokens.forEach((token) => {
274
+ if (token.type === "operator" && (token.value === "," || token.value === ";")) {
275
+ if (address.length) {
276
+ addresses.push(address);
277
+ }
278
+ address = [];
279
+ } else {
280
+ address.push(token);
281
+ }
282
+ });
283
+ if (address.length) {
284
+ addresses.push(address);
285
+ }
286
+ addresses.forEach((address2) => {
287
+ address2 = _handleAddress(address2, depth);
288
+ if (address2.length) {
289
+ parsedAddresses = parsedAddresses.concat(address2);
290
+ }
291
+ });
292
+ if (options.flatten) {
293
+ let addresses2 = [];
294
+ let walkAddressList = (list) => {
295
+ list.forEach((address2) => {
296
+ if (address2.group) {
297
+ return walkAddressList(address2.group);
298
+ } else {
299
+ addresses2.push(address2);
300
+ }
301
+ });
302
+ };
303
+ walkAddressList(parsedAddresses);
304
+ return addresses2;
305
+ }
306
+ return parsedAddresses;
307
+ }
308
+ var address_parser_default = addressParser;
309
+
310
+ // Make default export work naturally with require()
311
+ if (module.exports.default) {
312
+ var defaultExport = module.exports.default;
313
+ var namedExports = {};
314
+ for (var key in module.exports) {
315
+ if (key !== 'default') {
316
+ namedExports[key] = module.exports[key];
317
+ }
318
+ }
319
+ module.exports = defaultExport;
320
+ Object.assign(module.exports, namedExports);
321
+ }
322
+
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var base64_decoder_exports = {};
20
+ __export(base64_decoder_exports, {
21
+ default: () => Base64Decoder
22
+ });
23
+ module.exports = __toCommonJS(base64_decoder_exports);
24
+ var import_decode_strings = require("./decode-strings.cjs");
25
+ class Base64Decoder {
26
+ constructor(opts) {
27
+ opts = opts || {};
28
+ this.decoder = opts.decoder || new TextDecoder();
29
+ this.maxChunkSize = 100 * 1024;
30
+ this.chunks = [];
31
+ this.remainder = "";
32
+ }
33
+ update(buffer) {
34
+ let str = this.decoder.decode(buffer);
35
+ if (/[^a-zA-Z0-9+\/]/.test(str)) {
36
+ str = str.replace(/[^a-zA-Z0-9+\/]+/g, "");
37
+ }
38
+ this.remainder += str;
39
+ if (this.remainder.length >= this.maxChunkSize) {
40
+ let allowedBytes = Math.floor(this.remainder.length / 4) * 4;
41
+ let base64Str;
42
+ if (allowedBytes === this.remainder.length) {
43
+ base64Str = this.remainder;
44
+ this.remainder = "";
45
+ } else {
46
+ base64Str = this.remainder.substr(0, allowedBytes);
47
+ this.remainder = this.remainder.substr(allowedBytes);
48
+ }
49
+ if (base64Str.length) {
50
+ this.chunks.push((0, import_decode_strings.decodeBase64)(base64Str));
51
+ }
52
+ }
53
+ }
54
+ finalize() {
55
+ if (this.remainder && !/^=+$/.test(this.remainder)) {
56
+ this.chunks.push((0, import_decode_strings.decodeBase64)(this.remainder));
57
+ }
58
+ return (0, import_decode_strings.blobToArrayBuffer)(new Blob(this.chunks, { type: "application/octet-stream" }));
59
+ }
60
+ }
61
+
62
+ // Make default export work naturally with require()
63
+ if (module.exports.default) {
64
+ var defaultExport = module.exports.default;
65
+ var namedExports = {};
66
+ for (var key in module.exports) {
67
+ if (key !== 'default') {
68
+ namedExports[key] = module.exports[key];
69
+ }
70
+ }
71
+ module.exports = defaultExport;
72
+ Object.assign(module.exports, namedExports);
73
+ }
74
+
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var base64_encoder_exports = {};
20
+ __export(base64_encoder_exports, {
21
+ base64ArrayBuffer: () => base64ArrayBuffer
22
+ });
23
+ module.exports = __toCommonJS(base64_encoder_exports);
24
+ function base64ArrayBuffer(arrayBuffer) {
25
+ var base64 = "";
26
+ var encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
27
+ var bytes = new Uint8Array(arrayBuffer);
28
+ var byteLength = bytes.byteLength;
29
+ var byteRemainder = byteLength % 3;
30
+ var mainLength = byteLength - byteRemainder;
31
+ var a, b, c, d;
32
+ var chunk;
33
+ for (var i = 0; i < mainLength; i = i + 3) {
34
+ chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
35
+ a = (chunk & 16515072) >> 18;
36
+ b = (chunk & 258048) >> 12;
37
+ c = (chunk & 4032) >> 6;
38
+ d = chunk & 63;
39
+ base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
40
+ }
41
+ if (byteRemainder == 1) {
42
+ chunk = bytes[mainLength];
43
+ a = (chunk & 252) >> 2;
44
+ b = (chunk & 3) << 4;
45
+ base64 += encodings[a] + encodings[b] + "==";
46
+ } else if (byteRemainder == 2) {
47
+ chunk = bytes[mainLength] << 8 | bytes[mainLength + 1];
48
+ a = (chunk & 64512) >> 10;
49
+ b = (chunk & 1008) >> 4;
50
+ c = (chunk & 15) << 2;
51
+ base64 += encodings[a] + encodings[b] + encodings[c] + "=";
52
+ }
53
+ return base64;
54
+ }
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ base64ArrayBuffer
58
+ });
59
+
60
+ // Make default export work naturally with require()
61
+ if (module.exports.default) {
62
+ var defaultExport = module.exports.default;
63
+ var namedExports = {};
64
+ for (var key in module.exports) {
65
+ if (key !== 'default') {
66
+ namedExports[key] = module.exports[key];
67
+ }
68
+ }
69
+ module.exports = defaultExport;
70
+ Object.assign(module.exports, namedExports);
71
+ }
72
+