email-bounce-parser 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Crisp IM SAS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Email Bounce Parser
2
+
3
+ [![Test and Build](https://github.com/crisp-oss/email-bounce-parser/workflows/Test%20and%20Build/badge.svg?branch=master)](https://github.com/crisp-oss/email-bounce-parser/actions?query=workflow%3A%22Test+and+Build%22) [![Build and Release](https://github.com/crisp-oss/email-bounce-parser/workflows/Build%20and%20Release/badge.svg)](https://github.com/crisp-oss/email-bounce-parser/actions?query=workflow%3A%22Build+and+Release%22) [![NPM](https://img.shields.io/npm/v/email-bounce-parser.svg)](https://www.npmjs.com/package/email-bounce-parser) [![Downloads](https://img.shields.io/npm/dt/email-bounce-parser.svg)](https://www.npmjs.com/package/email-bounce-parser)
4
+
5
+ Parses bounce emails and extracts details.
6
+
7
+ **😘 Maintainer**: [@eliottvincent](https://github.com/eliottvincent)
8
+
9
+ ## Who uses it?
10
+
11
+ <table>
12
+ <tr>
13
+ <td align="center"><a href="https://crisp.chat/"><img src="https://crisp.chat/favicon-256x256.png" height="64" /></a></td>
14
+ </tr>
15
+ <tr>
16
+ <td align="center">Crisp</td>
17
+ </tr>
18
+ </table>
19
+
20
+ _👋 You use this library and you want to be listed there? [Contact us](https://crisp.chat/)._
21
+
22
+ ## Features
23
+
24
+ This library is used at [Crisp](https://crisp.chat/) everyday to parse bounce emails.
25
+ It supports most variations of bounce emails (Postfix, Dovecat, Gmail, Outlook).
26
+
27
+ ## Usage
28
+
29
+ ```js
30
+ const EmailBounceParse = require("email-bounce-parser");
31
+
32
+ const result = new EmailBounceParse().read(MY_EMAIL_STRING);
33
+
34
+ console.log(result);
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### Parse a bounce email
40
+
41
+ `read(body)` parses the bounce email and extracts available information (error code and type, server, original recipient, etc.):
42
+ * `body` must be a string representing the bounce email body (as returned by [mailparser](https://github.com/nodemailer/mailparser), for example)
43
+
44
+ ```js
45
+ const EmailBounceParse = require("email-bounce-parser");
46
+
47
+ const result = new EmailBounceParse().read(MY_EMAIL_STRING);
48
+
49
+ console.log(result);
50
+ // {
51
+ // bounce: true,
52
+ //
53
+ // email: {
54
+ // body: "This is the mail system at host mailer.acme.email [...] (in reply to RCPT TO command)",
55
+ // intro: "This is the mail system at host mailer.acme.email [...] The mail system",
56
+ // error: "<support@yipee.com>: host smtp.secureserver.net[92.240.81.0] said: [...] (in reply to RCPT TO command)"
57
+ // },
58
+ //
59
+ // data: {
60
+ // error: {
61
+ // code: {
62
+ // basic: "550",
63
+ // enhanced: "5.1.1"
64
+ // },
65
+ // label: "<support@yipee.com> Recipient not found.",
66
+ //
67
+ // type: "action_not_taken",
68
+ //
69
+ // temporary: false,
70
+ // permanent: true,
71
+ //
72
+ // data: {
73
+ // type: "no_such_user",
74
+ // }
75
+ // },
76
+ //
77
+ // server: {
78
+ // hostname: "smtp.secureserver.net",
79
+ // ip: "92.240.81.0",
80
+ // port: "25"
81
+ // },
82
+ //
83
+ // command: "RCPT TO"
84
+ // }
85
+ // }
86
+ ```
87
+
88
+ Head over to `/test/fixtures/` for more bounce examples and parsing results (anonymized data).
89
+
90
+ ## Contributing
91
+
92
+ Feel free to fork this project and submit fixes. We may adapt your code to fit the codebase.
93
+
94
+ You can run unit tests using:
95
+
96
+ ```
97
+ npm test
98
+ ```
99
+
100
+ ## License
101
+
102
+ email-bounce-parser is released under the MIT License. See the bundled LICENSE file for details.
package/lib/index.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+
4
+ var Parser = require("./parser");
5
+
6
+
7
+ /**
8
+ * EmailBounceParser
9
+ * @class
10
+ */
11
+ class EmailBounceParser {
12
+ /**
13
+ * Constructor
14
+ */
15
+ constructor() {
16
+ this.parser = new Parser();
17
+ }
18
+
19
+ /**
20
+ * Attempts to parse a bounce email
21
+ * @public
22
+ * @param {string} body
23
+ * @return {object} The parsed email
24
+ */
25
+ read(body) {
26
+ if (!body) {
27
+ return {
28
+ bounce : false
29
+ };
30
+ }
31
+
32
+ var _result = this.parser.parseBody(body);
33
+
34
+ var _data = this.parser.parseBounceData(_result.body, _result.error)
35
+
36
+ return {
37
+ bounce : true,
38
+
39
+ email : {
40
+ body : _result.body,
41
+ intro : _result.intro || null,
42
+ error : _result.error || null
43
+ },
44
+
45
+ data : {
46
+ error : _data.error,
47
+
48
+ recipient : _data.recipient,
49
+
50
+ server : _data.server,
51
+ command : _data.command
52
+ }
53
+ }
54
+ };
55
+ };
56
+
57
+
58
+ module.exports = EmailBounceParser;
59
+
package/lib/parser.js ADDED
@@ -0,0 +1,426 @@
1
+ "use strict";
2
+
3
+
4
+ var RE2 = require("re2");
5
+ var { loopRegexes, trimString } = require("./utils");
6
+
7
+ var ERROR_CODES_TYPES = {
8
+ "421" : "service_not_available",
9
+ "421 4.3.0" : "temporary_system_problem",
10
+ "421 4.4.5" : "server_busy",
11
+ "432 4.7.12" : "password_needed",
12
+ "450" : "mailbox_unavailable",
13
+ "451" : "local_error",
14
+ "452" : "insufficient_system_storage",
15
+ "503" : "bad_commands_sequence",
16
+ "550" : "action_not_taken",
17
+ "550 5.1.1" : "no_such_recipient",
18
+ "550 5.2.1" : "recipient_disabled_or_recipient_rate_limited",
19
+ "550 5.7.1" : "spam_content",
20
+ "554" : "transaction_failed",
21
+ "554 5.7.1" : "spam_content",
22
+ };
23
+
24
+ var REGEXES = {
25
+ line_break_double : /(\n\n)/gm,
26
+ carriage_return : /\r\n/gm,
27
+ byte_order_mark : /\uFEFF/gm,
28
+ trailing_non_breaking_space : /\u00A0$/gm,
29
+ non_breaking_space : /\u00A0/gm,
30
+ line_break_double_indent_single : /(\n\n\ {2})/gm,
31
+ line_break_single_indent_double : /(\n\ {4})/gm,
32
+ indent_long : /(\ {14,})/gm,
33
+
34
+ separator : [
35
+ /(The mail system)/m,
36
+ /(The following address\(es\) failed\:)/m
37
+ ],
38
+
39
+ command : [
40
+ /\(in reply to end of ([A-Z\s]+) command\)$/m, // 'end of' command
41
+ /\(in reply to ([A-Z\s]+) command\)$/m, // default
42
+ /timed out while sending ([A-Z\s]+)$/m, // command timed out
43
+ /error after ([A-Z\s]+)/m // 'after' command
44
+ ],
45
+
46
+ email_server : [
47
+ /host (\S+)\[([\d\.]+)\](?:\:(\d+)\:)? said/m,
48
+ /\: connect to (.+)\[(.+)\](?:\:(\d+)\:)?/m,
49
+ /Name service error for name\=(\S+)\s/m,
50
+ /RCPT TO\:\<(.+?)\>/m,
51
+ /Message rejected by\:\s+([\S\.]+?)$/m, // Outlook error
52
+ /from (\S+) \[([\d\.]+)\](?:\:(\d+)\:)?/m
53
+ ],
54
+
55
+ error : [
56
+ /said\: (\d{3}(?:[\s\-]\d\.\d\.\d\d?)?) (.+) \(in reply to/m,
57
+ /LMTP error after (?:.+?)\:\<.+?\>\: (\d{3}(?:[\s\-]\d\.\d\.\d\d?)?) <.+?\>(.+)$/m,
58
+ /Remote Server returned \'(\d{3}(?:[\s\-]\d\.\d\.\d\d?)?) (.+)\'/m,
59
+ /The response from the remote server was:\s(\d{3}(?:[\s\-]\d\.\d\.\d\d?)?) (.+)/m,
60
+ /Error\:\s*?(\d{3}(?:[\s\-]\d\.\d\.\d\d?)?) (.+)/m,
61
+ /reason: (\d{3}[\s\-]\d\.\d\.\d\d?) (.+)/m,
62
+ /(\d{3}[\s\-]\d\.\d\.\d\d?)/m
63
+ ],
64
+
65
+ recipient : [
66
+ /The following addresses had permanent fatal errors (?:.*?)\s\<(.+?)\>/,
67
+ /^\<(.+?)\>:/m,
68
+ /^(.+?)\s?\n?\s*[\[|<]mailto\:(.+?)[\]|>]/m,
69
+ /^Your message wasn't delivered to (.+?)\s/m,
70
+ /^Your message to (.+?) couldn\'t/m,
71
+ /\<(.+?)\>:/
72
+ ],
73
+
74
+ error_code : [
75
+ /(\d{3})[\s\-](\d\.\d\.\d)/,
76
+ /(\d{3})/
77
+ ],
78
+
79
+ mailbox_address : [
80
+ /^(([^\s@]+)@([^\s@]+)\.([^\s@]+))$/
81
+ ]
82
+ };
83
+
84
+
85
+ /**
86
+ * Parser
87
+ * @class
88
+ */
89
+ class Parser {
90
+ /**
91
+ * Constructor
92
+ */
93
+ constructor() {
94
+ this.__regexes = {};
95
+
96
+ this.__initRegexes();
97
+ }
98
+
99
+
100
+ /**
101
+ * Parses the body of the email
102
+ * @public
103
+ * @param {string} body
104
+ * @return {object} The result
105
+ */
106
+ parseBody(body) {
107
+ // Replace carriage return by regular line break
108
+ var _body = body.replace(this.__regexes.carriage_return, "\n");
109
+
110
+ // Remove Byte Order Mark
111
+ _body = _body.replace(this.__regexes.byte_order_mark, "");
112
+
113
+ // Remove trailing Non-breaking space
114
+ _body = _body.replace(this.__regexes.trailing_non_breaking_space, "");
115
+
116
+ // Replace Non-breaking space with regular space
117
+ _body = _body.replace(this.__regexes.non_breaking_space, " ");
118
+
119
+ var _match = loopRegexes(this.__regexes.separator, _body, "split");
120
+
121
+ if (_match && _match.length > 2) {
122
+ var _intro = trimString(_match[0] + _match[1]),
123
+ _error = trimString(_match[2]);
124
+
125
+ // Flatten intro?
126
+ if (this.__lineLengthRestricted(_intro, 75)) {
127
+ _intro = this.__flattenText(_intro);
128
+ }
129
+
130
+ // Flatten error?
131
+ if (this.__lineLengthRestricted(_error, 80)) {
132
+ _error = this.__flattenText(_error);
133
+ }
134
+
135
+ _body = _intro + "\n\n" + _error;
136
+
137
+ return {
138
+ body : _body,
139
+ intro : _intro,
140
+ error : _error
141
+ };
142
+ }
143
+
144
+ return {
145
+ body : trimString(_body)
146
+ };
147
+ }
148
+
149
+
150
+ /**
151
+ * Parses the bounce email data
152
+ * @public
153
+ * @param {string} body
154
+ * @param {string} [error]
155
+ * @return {object} The parsed data
156
+ */
157
+ parseBounceData(body, error=null) {
158
+ var _str = error || body;
159
+
160
+ return {
161
+ error : this.__parseBounceDataError(_str),
162
+ recipient : this.__parseBounceDataRecipient(_str),
163
+
164
+ server : this.__parseBounceDataEmailServer(_str),
165
+ command : this.__parseBounceDataCommand(_str)
166
+ };
167
+ }
168
+
169
+
170
+ /**
171
+ * Initializes regexes
172
+ * @private
173
+ * @return {undefined}
174
+ */
175
+ __initRegexes() {
176
+ for (var _key in REGEXES) {
177
+ var _key_line = `${_key}_line`;
178
+ var _entry = REGEXES[_key];
179
+
180
+ if (Array.isArray(_entry)) {
181
+ this.__regexes[_key] = [];
182
+ this.__regexes[_key_line] = [];
183
+
184
+ for (var _i = 0; _i < _entry.length; _i++) {
185
+ var _regex = _entry[_i];
186
+
187
+ this.__regexes[_key].push(
188
+ new RE2(_regex)
189
+ );
190
+ }
191
+ } else {
192
+ var _regex = _entry;
193
+
194
+ this.__regexes[_key] = new RE2(_regex);
195
+ }
196
+ }
197
+ }
198
+
199
+
200
+ /**
201
+ * Parses the bounce error
202
+ * @private
203
+ * @param {string} str
204
+ * @return {object} The bounce error
205
+ */
206
+ __parseBounceDataError(str) {
207
+ var _code = {
208
+ basic : null,
209
+ enhanced : null
210
+ },
211
+ _label = null,
212
+ _temporary = false,
213
+ _permanent = false;
214
+
215
+ var _match = loopRegexes(this.__regexes.error, str, "match", false);
216
+
217
+ if (_match && _match.length > 1) {
218
+ _code = this.__parseErrorCode(_match[1]);
219
+ _label = trimString(_match[2]);
220
+ }
221
+
222
+ // Check temporary / permanent
223
+ if (_code.basic[0] === "4") {
224
+ _temporary = true;
225
+ } else if (_code.basic[0] === "5") {
226
+ _permanent = true;
227
+ }
228
+
229
+ // Extract further data
230
+ var _data = this.__parseErrorData(str, _code, _label);
231
+
232
+ return {
233
+ code : _code,
234
+ label : _label,
235
+
236
+ type : ERROR_CODES_TYPES[(_code || {}).basic],
237
+
238
+ temporary : _temporary,
239
+ permanent : _permanent,
240
+
241
+ data : _data
242
+ }
243
+ }
244
+
245
+
246
+ /**
247
+ * Parses the error code
248
+ * @private
249
+ * @param {string} code
250
+ * @return {object} The error code
251
+ */
252
+ __parseErrorCode(code) {
253
+ var _basic = null,
254
+ _enhanced = null;
255
+
256
+ var _match = loopRegexes(this.__regexes.error_code, code);
257
+
258
+ if (_match && _match.length > 1) {
259
+ _basic = _match[1];
260
+ _enhanced = _match[2] || null;
261
+ }
262
+
263
+ // Basic code is the enhanced code? This is wrong and should be fixed
264
+ if (_basic === "511") {
265
+ _basic = "550";
266
+ _enhanced = "5.1.1";
267
+ }
268
+
269
+ return {
270
+ basic : _basic,
271
+ enhanced : _enhanced
272
+ }
273
+ }
274
+
275
+
276
+ /**
277
+ * Parses the error data
278
+ * @private
279
+ * @param {string} str
280
+ * @param {object} code
281
+ * @param {string} label
282
+ * @return {object} The error data
283
+ */
284
+ __parseErrorData(str, code, label) {
285
+ var _type = null;
286
+
287
+ if (code.basic && code.enhanced) {
288
+ _type = ERROR_CODES_TYPES[`${code.basic} ${code.enhanced}`] || null;
289
+ }
290
+
291
+ return {
292
+ type : _type
293
+ }
294
+ }
295
+
296
+
297
+ /**
298
+ * Parses the recipient
299
+ * @private
300
+ * @param {string} str
301
+ * @return {object} The recipient email address
302
+ */
303
+ __parseBounceDataRecipient(str) {
304
+ var _recipient = null;
305
+
306
+ var _match = loopRegexes(this.__regexes.recipient, str);
307
+
308
+ if (_match && _match.length > 1) {
309
+ _recipient = trimString(_match[1]);
310
+ }
311
+
312
+ return _recipient;
313
+ }
314
+
315
+
316
+ /**
317
+ * Parses the email server
318
+ * @private
319
+ * @param {string} str
320
+ * @return {object} The email server
321
+ */
322
+ __parseBounceDataEmailServer(str) {
323
+ var _hostname = null,
324
+ _ip = null,
325
+ _port = null;
326
+
327
+ var _match = loopRegexes(this.__regexes.email_server, str);
328
+
329
+ if (_match && _match.length > 1) {
330
+ _hostname = this.__parseHostname(_match[1]);
331
+ _ip = _match[2] || null;
332
+ _port = _match[3] || null;
333
+ }
334
+
335
+ return {
336
+ hostname : _hostname,
337
+ ip : _ip,
338
+ port : _port || null
339
+ }
340
+ }
341
+
342
+
343
+ /**
344
+ * Parses the server hostname
345
+ * @private
346
+ * @param {string} hostname
347
+ * @return {object} The server hostname
348
+ */
349
+ __parseHostname(hostname) {
350
+ if (hostname.includes("@")) {
351
+ return hostname.split("@")[1];
352
+ }
353
+
354
+ return hostname;
355
+ }
356
+
357
+
358
+ /**
359
+ * Parses the command
360
+ * @private
361
+ * @param {string} str
362
+ * @return {object} The parsed command
363
+ */
364
+ __parseBounceDataCommand(str) {
365
+ var _command = null;
366
+
367
+ var _match = loopRegexes(this.__regexes.command, str);
368
+
369
+ if (_match && _match.length > 1) {
370
+ _command = _match[1];
371
+ }
372
+
373
+ return _command;
374
+ }
375
+
376
+
377
+ /**
378
+ * Checks whether line length is restricted
379
+ * @private
380
+ * @param {string} str
381
+ * @param {string} length
382
+ * @return {boolean} Whether line length is restricted or not
383
+ */
384
+ __lineLengthRestricted(str, length) {
385
+ var _lines = str.split("\n");
386
+
387
+ for (var _i = 0; _i < _lines.length; _i++) {
388
+ var _line = _lines[_i];
389
+
390
+ // Skip lines with URL, as they are usually not restricted in length
391
+ if (_line.length > length && !_line.includes("http")) {
392
+ return false;
393
+ }
394
+ }
395
+
396
+ return true;
397
+ }
398
+
399
+
400
+ /**
401
+ * Flattens text
402
+ * @private
403
+ * @param {string} str
404
+ * @return {string} Flattened text
405
+ */
406
+ __flattenText(str) {
407
+ var _str = str;
408
+
409
+ // Replace Line-break followed by double indent with regular space
410
+ _str = _str.replace(this.__regexes.line_break_single_indent_double, " ");
411
+
412
+ // Replace double Line-break followed by single indent with double Line-break
413
+ _str = _str.replace(this.__regexes.line_break_double_indent_single, "\n\n");
414
+
415
+ // Replace single Line-break with regular space
416
+ _str = _str.replace(/(?<!\n)\n(?!\n)/gm, " ");
417
+
418
+ // Replace long indent with double Line-break
419
+ _str = _str.replace(this.__regexes.indent_long, "\n\n");
420
+
421
+ return _str;
422
+ }
423
+ }
424
+
425
+
426
+ module.exports = Parser;
package/lib/utils.js ADDED
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+
3
+ /**************************************************************************
4
+ * IMPORTS
5
+ ***************************************************************************/
6
+
7
+ var RE2 = require("re2");
8
+
9
+ /**************************************************************************
10
+ * FUNCTIONS
11
+ ***************************************************************************/
12
+
13
+ /**
14
+ * Executes multiple regular expressions until one succeeds
15
+ * @public
16
+ * @param {object} regexes
17
+ * @param {string} str
18
+ * @param {string} [mode]
19
+ * @param {boolean} [highestPosition]
20
+ * @return {object} The result of the first successfull regular expression
21
+ */
22
+ var loopRegexes = function(
23
+ regexes, str, mode = "match", highestPosition = true
24
+ ) {
25
+ var _match;
26
+
27
+ for (var _i = 0; _i < regexes.length; _i++) {
28
+ var _regex = regexes[_i];
29
+
30
+ if (typeof regexes[_i] === "string") {
31
+ // Build the regex
32
+ _regex = new RE2(regexes[_i]);
33
+ }
34
+
35
+ var _currentMatch = (
36
+ _regex[mode](str || "", mode === "replace" ? "" : undefined)
37
+ );
38
+
39
+ if (mode === "replace") {
40
+ _match = _currentMatch;
41
+
42
+ // A successfull replace must return a smaller string than the input
43
+ var _maxLength = str.length;
44
+
45
+ if (_match.length < _maxLength) {
46
+ break;
47
+ }
48
+ } else if (mode === "split" || mode === "match") {
49
+ // A successfull split must return an array with at least two items
50
+ var _minLength = mode === "split" ? 1 : 0;
51
+
52
+ if ((_currentMatch || []).length > _minLength) {
53
+ if (highestPosition === true) {
54
+ // No previous match?
55
+ if (!_match) {
56
+ // Save match and continue looking for other matches
57
+ _match = _currentMatch;
58
+ } else {
59
+ var _higher;
60
+
61
+ if (mode === "match") {
62
+ _higher = _match.index > _currentMatch.index;
63
+ } else if (mode === "split") {
64
+ _higher = _match[0].length > _currentMatch[0].length;
65
+ }
66
+
67
+ // Match positioned higher than previous one?
68
+ if (_higher) {
69
+ // Replace match and continue looking for other matches
70
+ _match = _currentMatch;
71
+ }
72
+ }
73
+ } else {
74
+ // Save match and stop
75
+ _match = _currentMatch;
76
+
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ return (_match || []);
84
+ }
85
+
86
+
87
+ /**
88
+ * Trims a string
89
+ * @public
90
+ * @param {string} str
91
+ * @return {string} The trimed string
92
+ */
93
+ var trimString = function(str) {
94
+ return ((str || "").trim() || null)
95
+ }
96
+
97
+
98
+ /**************************************************************************
99
+ * EXPORTS
100
+ ***************************************************************************/
101
+
102
+ exports.loopRegexes = loopRegexes;
103
+ exports.trimString = trimString;
104
+
105
+ exports.default = {
106
+ loopRegexes : loopRegexes,
107
+ trimString : trimString
108
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "email-bounce-parser",
3
+ "version": "1.0.1",
4
+ "description": "Parses bounce emails and extract errors",
5
+ "author": "Eliott Vincent <eliott@crisp.chat>",
6
+ "main": "lib/index.js",
7
+ "homepage": "https://github.com/crisp-oss/email-bounce-parser",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git://github.com/crisp-oss/email-bounce-parser"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/crisp-oss/email-bounce-parser/issues"
14
+ },
15
+ "engineStrict": true,
16
+ "engines": {
17
+ "node": ">=14.18.0"
18
+ },
19
+ "scripts": {
20
+ "test": "nodeunit ./test/index.js"
21
+ },
22
+ "devDependencies": {
23
+ "nodeunit-x": "0.15.0"
24
+ },
25
+ "dependencies": {
26
+ "re2": "1.20.1"
27
+ },
28
+ "keywords": [
29
+ "parser",
30
+ "mail",
31
+ "email",
32
+ "bounce",
33
+ "bounces"
34
+ ],
35
+ "license": "MIT"
36
+ }