email-bounce-parser 1.1.1 → 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
@@ -24,21 +24,9 @@ _👋 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
- ## RE2 Support
27
+ ## ReDoS Protection
28
28
 
29
- This library uses the [RE2](https://github.com/uhop/node-re2) regex engine when available for better performance and [ReDoS](https://en.wikipedia.org/wiki/ReDoS) protection. It automatically falls back to native JavaScript `RegExp` if RE2 is unavailable or fails to load.
30
-
31
- RE2 is an optional dependency. To use it, install it alongside this library:
32
-
33
- ```bash
34
- npm install re2
35
- ```
36
-
37
- After a Node.js upgrade, you may need to rebuild RE2:
38
-
39
- ```bash
40
- npm rebuild re2
41
- ```
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.
42
30
 
43
31
  ## Usage
44
32
 
package/lib/parser.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
 
4
- var { buildSafeRegExp, loopRegexes, trimString } = require("./utils");
4
+ var { loopRegexes, trimString } = require("./utils");
5
5
 
6
6
  var ERROR_CODES_TYPES = {
7
7
  "421" : "service_not_available",
@@ -47,7 +47,12 @@ var ERROR_CODES_TYPES = {
47
47
  "555 5.5.2" : "syntax_error"
48
48
  };
49
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)
50
54
  var REGEXES = {
55
+ line_break_single : /(?<!\n)\n(?!\n)/gm,
51
56
  line_break_double : /(\n\n)/gm,
52
57
  carriage_return : /\r\n/gm,
53
58
  byte_order_mark : /\uFEFF/gm,
@@ -73,9 +78,9 @@ var REGEXES = {
73
78
  email_server : [
74
79
  /host (\S+)\[([\d\.]+)\](?:\:(\d+)\:)? (?:said|refused to talk)/m,
75
80
  /^(\S+) rejected your message/m,
76
- /\: 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:"
77
82
  /Name service error for name\=(\S+)\s/m,
78
- /RCPT TO\:\<(.+?)\>/m,
83
+ /RCPT TO\:\<([^<>\n\r\u2028\u2029]+)\>/m,
79
84
  /Message rejected by\:\s+([\S\.]+?)$/m, // Outlook error
80
85
  /from (\S+) \[([\d\.]+)\](?:\:(\d+)\:)?/m,
81
86
  /conversation with (\S+)\[([\d\.]+)\](?:\:(\d+)\:)? timed out/m,
@@ -84,11 +89,11 @@ var REGEXES = {
84
89
  ],
85
90
 
86
91
  error : [
87
- /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/,
88
93
  /said\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?)-(.+)/,
89
94
  /(\d{3}\-\'\d\.\d\.\d{1,3}) (.+)/,
90
- /LMTP error after .+?\:\<.+?\>\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) <.+?\>(.+)$/m,
91
- /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,
92
97
  /The response from the remote server was:\s(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)/,
93
98
  /refused to talk to me\: (\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)/,
94
99
  /(?:error|reason)\:\s*?(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (.+)/i,
@@ -97,13 +102,13 @@ var REGEXES = {
97
102
  ],
98
103
 
99
104
  error_lax : [
100
- /^(?:\<.+?\> \(expanded from \<.+?\>\)\:)(.+)/m, // "<john@acme.com> (expanded from <john@digiflex.com>): Domain name not found"
101
- /^(?:\<.+?\>\:)(.+)/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"
102
107
  ],
103
108
 
104
109
  error_label_cleanup_recipient : [
105
- /^\<.+?\> \(expanded from \<.+?\>\)\:/m, // "<john@acme.com> (expanded from <john@digiflex.com>): Domain name not found"
106
- /^\<.+?\>\:/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"
107
112
  ],
108
113
 
109
114
  error_label_cleanup_command : [
@@ -111,16 +116,16 @@ var REGEXES = {
111
116
  ],
112
117
 
113
118
  recipient : [
114
- /the following addresses had permanent fatal errors (?:.*?)\s\<(.+?)\>/i,
115
- /there was a temporary problem (?:while )?delivering your message to (.+?). Gmail will retry/i,
116
- /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,
117
122
  /to the following addresses:\s*(.+)/i,
118
123
  /^your message wasn't delivered to (.+?)\s/mi, // "Your message wasn't delivered to john@acme.com"
119
124
  /^your message to (.+?) couldn\'t/mi, // "Your message to john@acme.com couldn't"
120
- /^(.+?)\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>"
121
- /^\<(.+?)\>:/m,
122
- /\<(.+?)\> \(expanded from \<.+?\>\)\:/, // "<john@acme.com> (expanded from <john@digiflex.com>): "
123
- /\<(.+?)\>:/ // "<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>:"
124
129
  ],
125
130
 
126
131
  temporary_safe : /(temporary|temporarily)/i,
@@ -133,8 +138,8 @@ var REGEXES = {
133
138
  /(no such (?:account|user|mailbox|recipient))/i,
134
139
  /((?:terminated|disabled|inactive|not found|unavailable|invalid) (?:account|user|mailbox|recipient))/i,
135
140
  /(do(e)?sn\'t exist)/i, // Some Chinese SMTP return "dosn't" instead of "doesn't"
136
- /(user (?:.+?) does not exist)/i,
137
- /((?: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
138
143
  ],
139
144
 
140
145
  server_unknown : [
@@ -173,7 +178,7 @@ class Parser {
173
178
  * Constructor
174
179
  */
175
180
  constructor() {
176
- this.__regexes = this.__initRegexes(REGEXES);
181
+ this.__regexes = REGEXES;
177
182
  }
178
183
 
179
184
 
@@ -247,37 +252,6 @@ class Parser {
247
252
  }
248
253
 
249
254
 
250
- /**
251
- * Initializes regexes
252
- * @private
253
- * @param {object} obj
254
- * @return {object} Regexes
255
- */
256
- __initRegexes(obj) {
257
- var _regexes = {};
258
-
259
- for (var _key in obj) {
260
- var _entry = obj[_key];
261
-
262
- if (Array.isArray(_entry)) {
263
- _regexes[_key] = [];
264
-
265
- for (var _i = 0; _i < _entry.length; _i++) {
266
- _regexes[_key].push(
267
- buildSafeRegExp(_entry[_i])
268
- );
269
- }
270
- } else if (typeof _entry === "object" && Object.keys(_entry).length > 0) {
271
- _regexes[_key] = this.__initRegexes(_entry);
272
- } else {
273
- _regexes[_key] = buildSafeRegExp(_entry);
274
- }
275
- }
276
-
277
- return _regexes;
278
- }
279
-
280
-
281
255
  /**
282
256
  * Parses the bounce error
283
257
  * @private
@@ -554,8 +528,7 @@ class Parser {
554
528
  _str = _str.replace(this.__regexes.line_break_double_indent_single, "\n\n");
555
529
 
556
530
  // Replace single Line-break with regular space
557
- // TODO: re2 doesn't support lookbehinds, so for now we use a pure-JS regex
558
- _str = _str.replace(/(?<!\n)\n(?!\n)/gm, " ");
531
+ _str = _str.replace(this.__regexes.line_break_single, " ");
559
532
 
560
533
  // Replace long indent with double Line-break
561
534
  _str = _str.replace(this.__regexes.indent_long, "\n\n");
package/lib/utils.js CHANGED
@@ -1,48 +1,9 @@
1
1
  "use strict";
2
2
 
3
- /**************************************************************************
4
- * IMPORTS
5
- ***************************************************************************/
6
-
7
- /**************************************************************************
8
- * INSTANCES
9
- ***************************************************************************/
10
-
11
- var RE2Engine = detectRE2();
12
-
13
3
  /**************************************************************************
14
4
  * FUNCTIONS
15
5
  ***************************************************************************/
16
6
 
17
- /**
18
- * Detects whether the RE2 engine is available
19
- * @private
20
- * @return {function|null} The RE2 engine, or null if unavailable
21
- */
22
- function detectRE2() {
23
- // Notice: RE2 can be explicitly disabled, to test the RegExp fallback
24
- if (process.env.EMAIL_BOUNCE_PARSER_DISABLE_RE2) {
25
- return null;
26
- }
27
-
28
- try {
29
- return require("re2");
30
- } catch (error) {
31
- return null;
32
- }
33
- }
34
-
35
- /**
36
- * Builds a safe regular expression (RE2 when available, RegExp otherwise)
37
- * @public
38
- * @param {string|RegExp} source
39
- * @return {RegExp} The regular expression
40
- */
41
- var buildSafeRegExp = function(source) {
42
- return (RE2Engine !== null) ? new RE2Engine(source) : new RegExp(source);
43
- }
44
-
45
-
46
7
  /**
47
8
  * Executes multiple regular expressions until one succeeds
48
9
  * @public
@@ -62,12 +23,6 @@ var loopRegexes = function(
62
23
  for (var _i = 0; _i < regexes.length; _i++) {
63
24
  var _regex = regexes[_i];
64
25
 
65
- if (typeof regexes[_i] === "string") {
66
- // Build the regex
67
- _regex = buildSafeRegExp(regexes[_i]);
68
- }
69
-
70
- // Notice: call String methods, as they support both RE2 and RegExp
71
26
  var _currentMatch;
72
27
 
73
28
  if (mode === "replace") {
@@ -138,12 +93,10 @@ var trimString = function(str) {
138
93
  * EXPORTS
139
94
  ***************************************************************************/
140
95
 
141
- exports.buildSafeRegExp = buildSafeRegExp;
142
- exports.loopRegexes = loopRegexes;
143
- exports.trimString = trimString;
96
+ exports.loopRegexes = loopRegexes;
97
+ exports.trimString = trimString;
144
98
 
145
99
  exports.default = {
146
- buildSafeRegExp : buildSafeRegExp,
147
- loopRegexes : loopRegexes,
148
- trimString : trimString
100
+ loopRegexes : loopRegexes,
101
+ trimString : trimString
149
102
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email-bounce-parser",
3
- "version": "1.1.1",
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",
@@ -18,23 +18,10 @@
18
18
  },
19
19
  "scripts": {
20
20
  "unit": "nodeunit ./test/index.js",
21
- "unit:regexp": "EMAIL_BOUNCE_PARSER_DISABLE_RE2=1 npm run unit",
22
- "test": "npm run unit && npm run unit:regexp"
21
+ "test": "npm run unit"
23
22
  },
24
23
  "devDependencies": {
25
- "nodeunit-x": "0.15.0",
26
- "re2": "1.27.0"
27
- },
28
- "peerDependencies": {
29
- "re2": "1.x"
30
- },
31
- "peerDependenciesMeta": {
32
- "re2": {
33
- "optional": true
34
- }
35
- },
36
- "allowScripts": {
37
- "re2@1.27.0": true
24
+ "nodeunit-x": "0.15.0"
38
25
  },
39
26
  "keywords": [
40
27
  "parser",