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 +2 -14
- package/lib/parser.js +26 -53
- package/lib/utils.js +4 -51
- package/package.json +3 -16
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
|
-
##
|
|
27
|
+
## ReDoS Protection
|
|
28
28
|
|
|
29
|
-
This library
|
|
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 {
|
|
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 (
|
|
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\:\<(
|
|
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})?) (
|
|
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
|
|
91
|
-
/remote server returned \'(\d{3}(?:[\s\-\#\']{1,2}\d\.\d\.\d{1,3})?) (
|
|
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
|
-
/^(
|
|
101
|
-
/^(
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
115
|
-
/there was a temporary problem (?:while )?delivering your message to (
|
|
116
|
-
/the following message to \<(
|
|
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
|
-
/^(
|
|
121
|
-
/^\<(
|
|
122
|
-
/\<(
|
|
123
|
-
/\<(
|
|
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 (
|
|
137
|
-
/((?:account|user|mailbox|recipient) (
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
142
|
-
exports.
|
|
143
|
-
exports.trimString = trimString;
|
|
96
|
+
exports.loopRegexes = loopRegexes;
|
|
97
|
+
exports.trimString = trimString;
|
|
144
98
|
|
|
145
99
|
exports.default = {
|
|
146
|
-
|
|
147
|
-
|
|
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": "
|
|
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
|
-
"
|
|
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",
|