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 +21 -0
- package/README.md +102 -0
- package/lib/index.js +59 -0
- package/lib/parser.js +426 -0
- package/lib/utils.js +108 -0
- package/package.json +36 -0
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
|
+
[](https://github.com/crisp-oss/email-bounce-parser/actions?query=workflow%3A%22Test+and+Build%22) [](https://github.com/crisp-oss/email-bounce-parser/actions?query=workflow%3A%22Build+and+Release%22) [](https://www.npmjs.com/package/email-bounce-parser) [](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
|
+
}
|