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 +5 -1
- package/lib/parser.js +28 -56
- package/lib/utils.js +8 -13
- package/package.json +4 -6
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 (
|
|
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\:\<(
|
|
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})?) (
|
|
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
|
|
92
|
-
/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,
|
|
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
|
-
/^(
|
|
102
|
-
/^(
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
116
|
-
/there was a temporary problem (?:while )?delivering your message to (
|
|
117
|
-
/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,
|
|
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
|
-
/^(
|
|
122
|
-
/^\<(
|
|
123
|
-
/\<(
|
|
124
|
-
/\<(
|
|
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 (
|
|
138
|
-
/((?: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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
// Build the regex
|
|
34
|
-
_regex = new RE2(regexes[_i]);
|
|
35
|
-
}
|
|
26
|
+
var _currentMatch;
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
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": "
|
|
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": ">=
|
|
17
|
+
"node": ">= 22.0.0"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"
|
|
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",
|