email-bounce-parser 1.0.22 → 2.0.0

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/README.md CHANGED
@@ -10,7 +10,7 @@ Parses bounce emails and extracts details.
10
10
 
11
11
  <table>
12
12
  <tr>
13
- <td align="center"><a href="https://crisp.chat/"><img src="https://crisp.chat/favicon-256x256.png" height="64" /></a></td>
13
+ <td align="center"><a href="https://crisp.chat/"><img src="https://crisp.chat/favicons/favicon-256x256.png" height="64" /></a></td>
14
14
  </tr>
15
15
  <tr>
16
16
  <td align="center">Crisp</td>
@@ -24,6 +24,10 @@ _👋 You use this library and you want to be listed there? [Contact us](https:/
24
24
  This library is used at [Crisp](https://crisp.chat/) everyday to parse bounce emails.
25
25
  It supports most variations of bounce emails (Postfix, Dovecat, Gmail, Outlook).
26
26
 
27
+ ## ReDoS Protection
28
+
29
+ This library has no native dependency. Its patterns are written to run in linear time on the native JavaScript `RegExp` engine, which protects against [ReDoS](https://en.wikipedia.org/wiki/ReDoS) on crafted emails.
30
+
27
31
  ## Usage
28
32
 
29
33
  ```js
package/lib/parser.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
 
4
- var RE2 = require("re2");
5
4
  var { loopRegexes, trimString } = require("./utils");
6
5
 
7
6
  var ERROR_CODES_TYPES = {
@@ -48,7 +47,12 @@ var ERROR_CODES_TYPES = {
48
47
  "555 5.5.2" : "syntax_error"
49
48
  };
50
49
 
50
+ // Notice: patterns must stay linear on backtracking engines: no nested or \
51
+ // adjacent unbounded quantifiers, and every scan is either bounded, or \
52
+ // stopped by a character that also starts the pattern (so that repeated \
53
+ // starts never rescan the same text)
51
54
  var REGEXES = {
55
+ line_break_single : /(?<!\n)\n(?!\n)/gm,
52
56
  line_break_double : /(\n\n)/gm,
53
57
  carriage_return : /\r\n/gm,
54
58
  byte_order_mark : /\uFEFF/gm,
@@ -74,9 +78,9 @@ var REGEXES = {
74
78
  email_server : [
75
79
  /host (\S+)\[([\d\.]+)\](?:\:(\d+)\:)? (?:said|refused to talk)/m,
76
80
  /^(\S+) rejected your message/m,
77
- /\: connect to (.+)\[(.+)\](?:\:(\d+)\:)?/m, // "connect to acme.com[132.145.223.95]:25:"
81
+ /\: connect to ([^:\[\n\r\u2028\u2029]+)\[([^\[\]\n\r\u2028\u2029]+)\](?:\:(\d+)\:)?/m, // "connect to acme.com[132.145.223.95]:25:"
78
82
  /Name service error for name\=(\S+)\s/m,
79
- /RCPT TO\:\<(.+?)\>/m,
83
+ /RCPT TO\:\<([^<>\n\r\u2028\u2029]+)\>/m,
80
84
  /Message rejected by\:\s+([\S\.]+?)$/m, // Outlook error
81
85
  /from (\S+) \[([\d\.]+)\](?:\:(\d+)\:)?/m,
82
86
  /conversation with (\S+)\[([\d\.]+)\](?:\:(\d+)\:)? timed out/m,
@@ -85,11 +89,11 @@ var REGEXES = {
85
89
  ],
86
90
 
87
91
  error : [
88
- /said\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+) \(in reply to/,
92
+ /said\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.{1,1000}) \(in reply to/,
89
93
  /said\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?)-(.+)/,
90
94
  /(\d{3}\-\'\d\.\d\.\d{1,3}) (.+)/,
91
- /LMTP error after .+?\:\<.+?\>\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) <.+?\>(.+)$/m,
92
- /remote server returned \'(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)\'/i,
95
+ /LMTP error after [^:<\n\r\u2028\u2029]{1,40}\:\<[^<>\n\r\u2028\u2029]{1,320}\>\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) <[^<>\n\r\u2028\u2029]{1,320}\>(.+)$/m,
96
+ /remote server returned \'(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.{1,1000})\'/i,
93
97
  /The response from the remote server was:\s(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)/,
94
98
  /refused to talk to me\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)/,
95
99
  /(?:error|reason)\:\s*?(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)/i,
@@ -98,13 +102,13 @@ var REGEXES = {
98
102
  ],
99
103
 
100
104
  error_lax : [
101
- /^(?:\<.+?\> \(expanded from \<.+?\>\)\:)(.+)/m, // "<john@acme.com> (expanded from <john@digiflex.com>): Domain name not found"
102
- /^(?:\<.+?\>\:)(.+)/m, // "<john@acme.com>: Domain name not found"
105
+ /^(?:\<[^<>\n\r\u2028\u2029]+\> \(expanded from \<[^<>\n\r\u2028\u2029]+\>\)\:)(.+)/m, // "<john@acme.com> (expanded from <john@digiflex.com>): Domain name not found"
106
+ /^(?:\<[^<>\n\r\u2028\u2029]+\>\:)(.+)/m, // "<john@acme.com>: Domain name not found"
103
107
  ],
104
108
 
105
109
  error_label_cleanup_recipient : [
106
- /^\<.+?\> \(expanded from \<.+?\>\)\:/m, // "<john@acme.com> (expanded from <john@digiflex.com>): Domain name not found"
107
- /^\<.+?\>\:/m, // "<john@acme.com>: Domain name not found"
110
+ /^\<[^<>\n\r\u2028\u2029]+\> \(expanded from \<[^<>\n\r\u2028\u2029]+\>\)\:/m, // "<john@acme.com> (expanded from <john@digiflex.com>): Domain name not found"
111
+ /^\<[^<>\n\r\u2028\u2029]+\>\:/m, // "<john@acme.com>: Domain name not found"
108
112
  ],
109
113
 
110
114
  error_label_cleanup_command : [
@@ -112,16 +116,16 @@ var REGEXES = {
112
116
  ],
113
117
 
114
118
  recipient : [
115
- /the following addresses had permanent fatal errors (?:.*?)\s\<(.+?)\>/i,
116
- /there was a temporary problem (?:while )?delivering your message to (.+?). Gmail will retry/i,
117
- /the following message to \<(.+?)\> was undeliverable/i,
119
+ /the following addresses had permanent fatal errors [^<\n\r\u2028\u2029]{0,200}\s\<([^<>\n\r\u2028\u2029]+)\>/i,
120
+ /there was a temporary problem (?:while )?delivering your message to (.{1,320}?). Gmail will retry/i,
121
+ /the following message to \<([^<>\n\r\u2028\u2029]+)\> was undeliverable/i,
118
122
  /to the following addresses:\s*(.+)/i,
119
123
  /^your message wasn't delivered to (.+?)\s/mi, // "Your message wasn't delivered to john@acme.com"
120
124
  /^your message to (.+?) couldn\'t/mi, // "Your message to john@acme.com couldn't"
121
- /^(.+?)\s?(?:\s\(.*?\))?\n?\s*[\[|<]mailto\:(.+?)[\]|>]/mi, // "john@acme.com<mailto:john@acme.com>" or "john@acme.com[mailto:john@acme.com]" or "john@acme.com (john@acme.com)<mailto:john@acme.com>"
122
- /^\<(.+?)\>:/m,
123
- /\<(.+?)\> \(expanded from \<.+?\>\)\:/, // "<john@acme.com> (expanded from <john@digiflex.com>): "
124
- /\<(.+?)\>:/ // "<john@acme.com>:"
125
+ /^((?:\S+?[^\S\n\r\u2028\u2029]+)*?\S+?)(?:[^\S\n\r\u2028\u2029]+\([^()\n\r\u2028\u2029]*\))?\s*[\[|<]mailto\:([^\[\]|<>\n\r\u2028\u2029]+)[\]|>]/mi, // "john@acme.com<mailto:john@acme.com>" or "john@acme.com[mailto:john@acme.com]" or "john@acme.com (john@acme.com)<mailto:john@acme.com>"
126
+ /^\<([^<>\n\r\u2028\u2029]+)\>:/m,
127
+ /\<([^<>\n\r\u2028\u2029]+)\> \(expanded from \<[^<>\n\r\u2028\u2029]+\>\)\:/, // "<john@acme.com> (expanded from <john@digiflex.com>): "
128
+ /\<([^<>\n\r\u2028\u2029]+)\>:/ // "<john@acme.com>:"
125
129
  ],
126
130
 
127
131
  temporary_safe : /(temporary|temporarily)/i,
@@ -134,8 +138,8 @@ var REGEXES = {
134
138
  /(no such (?:account|user|mailbox|recipient))/i,
135
139
  /((?:terminated|disabled|inactive|not found|unavailable|invalid) (?:account|user|mailbox|recipient))/i,
136
140
  /(do(e)?sn\'t exist)/i, // Some Chinese SMTP return "dosn't" instead of "doesn't"
137
- /(user (?:.+?) does not exist)/i,
138
- /((?:account|user|mailbox|recipient) (?:.+@.+ )?(?:is )?(?:terminated|disabled|inactive|not found|unavailable|invalid|rejected))/i
141
+ /(user (?:.{1,320}?) does not exist)/i,
142
+ /((?:account|user|mailbox|recipient) (?:[^@\n\r\u2028\u2029]{1,100}@[^\n\r\u2028\u2029]{1,200}? )?(?:is )?(?:terminated|disabled|inactive|not found|unavailable|invalid|rejected))/i
139
143
  ],
140
144
 
141
145
  server_unknown : [
@@ -174,7 +178,7 @@ class Parser {
174
178
  * Constructor
175
179
  */
176
180
  constructor() {
177
- this.__regexes = this.__initRegexes(REGEXES);
181
+ this.__regexes = REGEXES;
178
182
  }
179
183
 
180
184
 
@@ -248,37 +252,6 @@ class Parser {
248
252
  }
249
253
 
250
254
 
251
- /**
252
- * Initializes regexes
253
- * @private
254
- * @param {object} obj
255
- * @return {object} Regexes
256
- */
257
- __initRegexes(obj) {
258
- var _regexes = {};
259
-
260
- for (var _key in obj) {
261
- var _entry = obj[_key];
262
-
263
- if (Array.isArray(_entry)) {
264
- _regexes[_key] = [];
265
-
266
- for (var _i = 0; _i < _entry.length; _i++) {
267
- _regexes[_key].push(
268
- new RE2(_entry[_i])
269
- );
270
- }
271
- } else if (typeof _entry === "object" && Object.keys(_entry).length > 0) {
272
- _regexes[_key] = this.__initRegexes(_entry);
273
- } else {
274
- _regexes[_key] = new RE2(_entry);
275
- }
276
- }
277
-
278
- return _regexes;
279
- }
280
-
281
-
282
255
  /**
283
256
  * Parses the bounce error
284
257
  * @private
@@ -316,7 +289,7 @@ class Parser {
316
289
  // In cases where the bounce is detected as permanent, or the \
317
290
  // temporary / permanent status is still unclear, run additional detection
318
291
  if (_permanent === true || (_temporary === false && _permanent === false)) {
319
- var _match = this.__regexes.temporary_safe.match(str);
292
+ var _match = str.match(this.__regexes.temporary_safe);
320
293
 
321
294
  if (_match && _match.length > 1) {
322
295
  _temporary = true;
@@ -415,14 +388,14 @@ class Parser {
415
388
  }
416
389
 
417
390
  // Check if blocked
418
- var _blocked_match = this.__regexes.blocked.match(str);
391
+ var _blocked_match = str.match(this.__regexes.blocked);
419
392
 
420
393
  if (_blocked_match && _blocked_match.length > 1) {
421
394
  _blocked = true;
422
395
  }
423
396
 
424
397
  // Check if spam
425
- var _spam_match = this.__regexes.spam.match(str);
398
+ var _spam_match = str.match(this.__regexes.spam);
426
399
 
427
400
  if (_spam_match && _spam_match.length > 1) {
428
401
  _spam = true;
@@ -555,8 +528,7 @@ class Parser {
555
528
  _str = _str.replace(this.__regexes.line_break_double_indent_single, "\n\n");
556
529
 
557
530
  // Replace single Line-break with regular space
558
- // TODO: re2 doesn't support lookbehinds, so for now we use a pure-JS regex
559
- _str = _str.replace(/(?<!\n)\n(?!\n)/gm, " ");
531
+ _str = _str.replace(this.__regexes.line_break_single, " ");
560
532
 
561
533
  // Replace long indent with double Line-break
562
534
  _str = _str.replace(this.__regexes.indent_long, "\n\n");
package/lib/utils.js CHANGED
@@ -1,11 +1,5 @@
1
1
  "use strict";
2
2
 
3
- /**************************************************************************
4
- * IMPORTS
5
- ***************************************************************************/
6
-
7
- var RE2 = require("re2");
8
-
9
3
  /**************************************************************************
10
4
  * FUNCTIONS
11
5
  ***************************************************************************/
@@ -29,14 +23,15 @@ var loopRegexes = function(
29
23
  for (var _i = 0; _i < regexes.length; _i++) {
30
24
  var _regex = regexes[_i];
31
25
 
32
- if (typeof regexes[_i] === "string") {
33
- // Build the regex
34
- _regex = new RE2(regexes[_i]);
35
- }
26
+ var _currentMatch;
36
27
 
37
- var _currentMatch = (
38
- _regex[mode](str || "", mode === "replace" ? "" : undefined)
39
- );
28
+ if (mode === "replace") {
29
+ _currentMatch = (str || "").replace(_regex, "");
30
+ } else if (mode === "split") {
31
+ _currentMatch = (str || "").split(_regex);
32
+ } else {
33
+ _currentMatch = (str || "").match(_regex);
34
+ }
40
35
 
41
36
  if (mode === "replace") {
42
37
  _match = _currentMatch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-bounce-parser",
3
- "version": "1.0.22",
3
+ "version": "2.0.0",
4
4
  "description": "Parses bounce emails and extract errors",
5
5
  "author": "Eliott Vincent <eliott@crisp.chat>",
6
6
  "main": "lib/index.js",
@@ -14,17 +14,15 @@
14
14
  },
15
15
  "engineStrict": true,
16
16
  "engines": {
17
- "node": ">=14.18.0"
17
+ "node": ">= 22.0.0"
18
18
  },
19
19
  "scripts": {
20
- "test": "nodeunit ./test/index.js"
20
+ "unit": "nodeunit ./test/index.js",
21
+ "test": "npm run unit"
21
22
  },
22
23
  "devDependencies": {
23
24
  "nodeunit-x": "0.15.0"
24
25
  },
25
- "dependencies": {
26
- "re2": "1.20.7"
27
- },
28
26
  "keywords": [
29
27
  "parser",
30
28
  "mail",