emailengine-app 2.60.1 → 2.61.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/CHANGELOG.md +19 -0
- package/bin/emailengine.js +388 -8
- package/data/google-crawlers.json +1 -1
- package/lib/arf-detect.js +3 -1
- package/lib/bounce-detect.js +271 -53
- package/lib/email-client/base-client.js +44 -6
- package/lib/email-client/imap/mailbox.js +45 -6
- package/package.json +4 -5
- package/sbom.json +1 -1
- package/static/licenses.html +6 -6
- package/test/autoreply-test.js +327 -0
- package/test/complaint-test.js +256 -0
- package/test/fixtures/autoreply/LICENSE +27 -0
- package/test/fixtures/autoreply/rfc3834-01.eml +23 -0
- package/test/fixtures/autoreply/rfc3834-02.eml +24 -0
- package/test/fixtures/autoreply/rfc3834-03.eml +26 -0
- package/test/fixtures/autoreply/rfc3834-04.eml +48 -0
- package/test/fixtures/autoreply/rfc3834-05.eml +19 -0
- package/test/fixtures/autoreply/rfc3834-06.eml +59 -0
- package/test/fixtures/complaints/LICENSE +27 -0
- package/test/fixtures/complaints/amazonses.eml +72 -0
- package/test/fixtures/complaints/dmarc.eml +59 -0
- package/test/fixtures/complaints/hotmail.eml +49 -0
- package/test/fixtures/complaints/optout.eml +40 -0
- package/test/fixtures/complaints/standard-arf.eml +68 -0
- package/test/fixtures/complaints/yahoo.eml +68 -0
- package/translations/messages.pot +13 -13
- package/workers/api.js +4 -4
- package/help.txt +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.61.0](https://github.com/postalsys/emailengine/compare/v2.60.1...v2.61.0) (2025-12-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Add check-bounce CLI command for analyzing bounce emails ([ae3a85d](https://github.com/postalsys/emailengine/commit/ae3a85dd5acdf9c893196284487e536ac3d6d9ec))
|
|
9
|
+
* Add Exim-style bounce detection for diagnostic messages ([b82e588](https://github.com/postalsys/emailengine/commit/b82e588d3e699f3c757c6a01f3fb9933c94bbb4c))
|
|
10
|
+
* Improve ARF complaint detection and add comprehensive tests ([5533552](https://github.com/postalsys/emailengine/commit/55335526a19c6948675d126d1dbb14db353253a0))
|
|
11
|
+
* Improve autoreply detection and add comprehensive tests ([1cf179f](https://github.com/postalsys/emailengine/commit/1cf179f8289eb05e8add420fe02d6410f61718cc))
|
|
12
|
+
* Improve bounce detection coverage for non-standard formats ([5393872](https://github.com/postalsys/emailengine/commit/53938724c699b32177ece83c3d88be9a5432c067))
|
|
13
|
+
* Improve bounce detection for legacy formats ([90a0289](https://github.com/postalsys/emailengine/commit/90a0289f7999e23149e14ff6ea5d703de056d5b0))
|
|
14
|
+
* Replace static help.txt with dynamic CLI help system ([4cd5fb0](https://github.com/postalsys/emailengine/commit/4cd5fb059c2bf7436c4262a7daee1e85f535c322))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* Detect "Out of the Office" autoreply subject pattern ([7191c50](https://github.com/postalsys/emailengine/commit/7191c50ae62930a66bf6598cb55f94130e022cf9))
|
|
20
|
+
* Harden bounce detection against edge cases and ReDoS attacks ([d6c72c2](https://github.com/postalsys/emailengine/commit/d6c72c29d4c2bc1a20025204cc0cf20f803b04a6))
|
|
21
|
+
|
|
3
22
|
## [2.60.1](https://github.com/postalsys/emailengine/compare/v2.60.0...v2.60.1) (2025-12-17)
|
|
4
23
|
|
|
5
24
|
|
package/bin/emailengine.js
CHANGED
|
@@ -17,6 +17,309 @@ const argv = require('minimist')(process.argv.slice(2));
|
|
|
17
17
|
const msgpack = require('msgpack5')();
|
|
18
18
|
const crypto = require('crypto');
|
|
19
19
|
|
|
20
|
+
// Command definitions for dynamic help generation
|
|
21
|
+
const COMMANDS = {
|
|
22
|
+
'': {
|
|
23
|
+
description: 'Start the EmailEngine server'
|
|
24
|
+
},
|
|
25
|
+
help: {
|
|
26
|
+
description: 'Show help for a command'
|
|
27
|
+
},
|
|
28
|
+
version: {
|
|
29
|
+
description: 'Show version number'
|
|
30
|
+
},
|
|
31
|
+
license: {
|
|
32
|
+
description: 'Show license information',
|
|
33
|
+
subcommands: {
|
|
34
|
+
export: { description: 'Export license key for backup' },
|
|
35
|
+
import: {
|
|
36
|
+
description: 'Import license key',
|
|
37
|
+
options: [{ name: '--license, -l', description: 'Encoded license key', type: 'string' }]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
password: {
|
|
42
|
+
description: 'Set or reset admin password',
|
|
43
|
+
options: [
|
|
44
|
+
{ name: '--password, -p', description: 'Password to set (auto-generated if not provided)', type: 'string' },
|
|
45
|
+
{ name: '--hash, -r', description: 'Output password hash instead of plaintext', type: 'boolean' }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
scan: {
|
|
49
|
+
description: 'Scan Redis keyspace and output as CSV'
|
|
50
|
+
},
|
|
51
|
+
encrypt: {
|
|
52
|
+
description: 'Manage field-level encryption for stored credentials',
|
|
53
|
+
options: [{ name: '--decrypt', description: 'Previous secret for re-encryption (repeatable)', type: 'string' }]
|
|
54
|
+
},
|
|
55
|
+
tokens: {
|
|
56
|
+
description: 'Manage API access tokens',
|
|
57
|
+
subcommands: {
|
|
58
|
+
issue: {
|
|
59
|
+
description: 'Create a new access token',
|
|
60
|
+
options: [
|
|
61
|
+
{ name: '--description, -d', description: 'Token description', type: 'string' },
|
|
62
|
+
{ name: '--scope, -s', description: 'Access scope', type: 'string', default: '*' },
|
|
63
|
+
{ name: '--account, -a', description: 'Limit token to specific account', type: 'string' }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
export: {
|
|
67
|
+
description: 'Export token for backup',
|
|
68
|
+
options: [{ name: '--token, -t', description: 'Token to export', type: 'string' }]
|
|
69
|
+
},
|
|
70
|
+
import: {
|
|
71
|
+
description: 'Import previously exported token',
|
|
72
|
+
options: [{ name: '--token, -t', description: 'Exported token data', type: 'string' }]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
export: {
|
|
77
|
+
description: 'Export account data including credentials',
|
|
78
|
+
options: [{ name: '--account, -a', description: 'Account ID to export', type: 'string' }]
|
|
79
|
+
},
|
|
80
|
+
'check-bounce': {
|
|
81
|
+
description: 'Analyze a bounce email and classify it',
|
|
82
|
+
options: [{ name: '--file, -f', description: 'Path to EML file', type: 'string' }],
|
|
83
|
+
example: 'emailengine check-bounce /path/to/bounce.eml'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const GLOBAL_OPTIONS = [
|
|
88
|
+
{ name: '-h, --help', description: 'Show this help message' },
|
|
89
|
+
{ name: '--dbs.redis', description: 'Redis connection URL', type: 'string', group: 'General' },
|
|
90
|
+
{ name: '--workers.imap', description: 'Number of IMAP worker threads', type: 'number', default: 4, group: 'General' },
|
|
91
|
+
{ name: '--settings', description: 'Pre-configured settings as JSON', type: 'json-string', group: 'General' },
|
|
92
|
+
{ name: '--service.secret', description: 'Secret key for encrypting stored credentials', type: 'string', group: 'General' },
|
|
93
|
+
{ name: '--service.commandTimeout', description: 'Maximum time for IMAP commands', type: 'number/string', default: '10s', group: 'General' },
|
|
94
|
+
{ name: '--service.setupDelay', description: 'Delay between worker connection assignments', type: 'number/string', default: '0ms', group: 'General' },
|
|
95
|
+
{ name: '--log.level', description: 'Logging level', type: 'string', default: 'trace', group: 'General' },
|
|
96
|
+
{ name: '--log.raw', description: 'Log raw IMAP traffic', type: 'boolean', default: false, group: 'General' },
|
|
97
|
+
{ name: '--workers.webhooks', description: 'Number of webhook worker threads', type: 'number', default: 1, group: 'General' },
|
|
98
|
+
{ name: '--api.host', description: 'API server bind address', type: 'string', default: '127.0.0.1', group: 'API server' },
|
|
99
|
+
{ name: '--api.port', description: 'API server port', type: 'number', default: 3000, group: 'API server' },
|
|
100
|
+
{ name: '--api.maxSize', description: 'Maximum attachment size', type: 'number/string', default: '5M', group: 'API server' },
|
|
101
|
+
{ name: '--queues.notify', description: 'Concurrent webhook deliveries', type: 'number', default: 1, group: 'Background tasks' },
|
|
102
|
+
{ name: '--queues.submit', description: 'Concurrent email submissions', type: 'number', default: 1, group: 'Background tasks' },
|
|
103
|
+
{ name: '--smtp.enabled', description: 'Enable SMTP submission server', type: 'boolean', default: false, group: 'SMTP server' },
|
|
104
|
+
{ name: '--smtp.secret', description: 'Shared SMTP password for all accounts', type: 'string', group: 'SMTP server' },
|
|
105
|
+
{ name: '--smtp.host', description: 'SMTP server bind address', type: 'string', default: '127.0.0.1', group: 'SMTP server' },
|
|
106
|
+
{ name: '--smtp.port', description: 'SMTP server port', type: 'number', default: 2525, group: 'SMTP server' },
|
|
107
|
+
{ name: '--smtp.proxy', description: 'Enable HAProxy PROXY protocol', type: 'boolean', default: false, group: 'SMTP server' },
|
|
108
|
+
{ name: '--smtp.maxMessageSize', description: 'Maximum email size', type: 'number/string', default: '25M', group: 'SMTP server' }
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Help formatting functions
|
|
112
|
+
function getTerminalWidth() {
|
|
113
|
+
// Check stderr first since help outputs there, fallback to stdout, then 80
|
|
114
|
+
return process.stderr.columns || process.stdout.columns || 80;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function wrapText(text, width, indent) {
|
|
118
|
+
if (width <= 0) {
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
const words = text.split(' ');
|
|
122
|
+
const lines = [];
|
|
123
|
+
let currentLine = '';
|
|
124
|
+
|
|
125
|
+
for (const word of words) {
|
|
126
|
+
if (currentLine.length === 0) {
|
|
127
|
+
currentLine = word;
|
|
128
|
+
} else if (currentLine.length + 1 + word.length <= width) {
|
|
129
|
+
currentLine += ' ' + word;
|
|
130
|
+
} else {
|
|
131
|
+
lines.push(currentLine);
|
|
132
|
+
currentLine = word;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (currentLine) {
|
|
136
|
+
lines.push(currentLine);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return lines.join('\n' + ' '.repeat(indent));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatLine(name, description, type, defaultVal, width, nameWidth) {
|
|
143
|
+
const indent = 2;
|
|
144
|
+
const gap = 2;
|
|
145
|
+
let line = ' '.repeat(indent) + name.padEnd(nameWidth);
|
|
146
|
+
|
|
147
|
+
let descParts = [description];
|
|
148
|
+
if (type) {
|
|
149
|
+
descParts.push(`[${type}]`);
|
|
150
|
+
}
|
|
151
|
+
if (defaultVal !== undefined && defaultVal !== null) {
|
|
152
|
+
descParts.push(`[default: ${defaultVal}]`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const descText = descParts.join(' ');
|
|
156
|
+
const descWidth = width - nameWidth - indent - gap;
|
|
157
|
+
|
|
158
|
+
if (descWidth > 20) {
|
|
159
|
+
line += ' '.repeat(gap) + wrapText(descText, descWidth, nameWidth + indent + gap);
|
|
160
|
+
} else {
|
|
161
|
+
line += ' '.repeat(gap) + descText;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return line;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Calculate global name width for consistent alignment
|
|
168
|
+
function calculateGlobalNameWidth() {
|
|
169
|
+
let maxWidth = 0;
|
|
170
|
+
|
|
171
|
+
// Commands
|
|
172
|
+
for (const cmd of Object.keys(COMMANDS)) {
|
|
173
|
+
const cmdName = cmd ? `emailengine ${cmd}` : 'emailengine';
|
|
174
|
+
maxWidth = Math.max(maxWidth, cmdName.length);
|
|
175
|
+
if (COMMANDS[cmd].subcommands) {
|
|
176
|
+
maxWidth = Math.max(maxWidth, `emailengine ${cmd} [command]`.length);
|
|
177
|
+
for (const subCmd of Object.keys(COMMANDS[cmd].subcommands)) {
|
|
178
|
+
maxWidth = Math.max(maxWidth, `emailengine ${cmd} ${subCmd}`.length);
|
|
179
|
+
const subDef = COMMANDS[cmd].subcommands[subCmd];
|
|
180
|
+
if (subDef.options) {
|
|
181
|
+
for (const opt of subDef.options) {
|
|
182
|
+
maxWidth = Math.max(maxWidth, opt.name.length);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (COMMANDS[cmd].options) {
|
|
188
|
+
for (const opt of COMMANDS[cmd].options) {
|
|
189
|
+
maxWidth = Math.max(maxWidth, opt.name.length);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Global options
|
|
195
|
+
for (const opt of GLOBAL_OPTIONS) {
|
|
196
|
+
maxWidth = Math.max(maxWidth, opt.name.length);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return maxWidth;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function generateHelp() {
|
|
203
|
+
const width = getTerminalWidth();
|
|
204
|
+
const nameWidth = calculateGlobalNameWidth();
|
|
205
|
+
const lines = [];
|
|
206
|
+
|
|
207
|
+
lines.push('emailengine [command] [options]');
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push(wrapText(
|
|
210
|
+
'EmailEngine is the self-hosted service that allows you to access any email account using an easy-to-use REST API.',
|
|
211
|
+
width,
|
|
212
|
+
0
|
|
213
|
+
));
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push('Commands:');
|
|
216
|
+
|
|
217
|
+
// Output commands
|
|
218
|
+
for (const [cmd, def] of Object.entries(COMMANDS)) {
|
|
219
|
+
const cmdName = cmd ? `emailengine ${cmd}` : 'emailengine';
|
|
220
|
+
lines.push(formatLine(cmdName, def.description, null, null, width, nameWidth));
|
|
221
|
+
if (def.subcommands) {
|
|
222
|
+
lines.push(formatLine(
|
|
223
|
+
`emailengine ${cmd} [command]`,
|
|
224
|
+
`${cmd.charAt(0).toUpperCase() + cmd.slice(1)} management`,
|
|
225
|
+
null,
|
|
226
|
+
null,
|
|
227
|
+
width,
|
|
228
|
+
nameWidth
|
|
229
|
+
));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Output global options by group
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push('Options:');
|
|
236
|
+
|
|
237
|
+
let currentGroup = null;
|
|
238
|
+
for (const opt of GLOBAL_OPTIONS) {
|
|
239
|
+
if (opt.group && opt.group !== currentGroup) {
|
|
240
|
+
currentGroup = opt.group;
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(' ' + currentGroup + ':');
|
|
243
|
+
}
|
|
244
|
+
lines.push(formatLine(opt.name, opt.description, opt.type, opt.default, width, nameWidth));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Output subcommand details
|
|
248
|
+
for (const [cmd, def] of Object.entries(COMMANDS)) {
|
|
249
|
+
if (def.subcommands) {
|
|
250
|
+
lines.push('');
|
|
251
|
+
lines.push(`${cmd.charAt(0).toUpperCase() + cmd.slice(1)} management commands:`);
|
|
252
|
+
for (const [subCmd, subDef] of Object.entries(def.subcommands)) {
|
|
253
|
+
lines.push(formatLine(`emailengine ${cmd} ${subCmd}`, subDef.description, null, null, width, nameWidth));
|
|
254
|
+
if (subDef.options) {
|
|
255
|
+
for (const opt of subDef.options) {
|
|
256
|
+
lines.push(formatLine(opt.name, opt.description, opt.type, opt.default, width, nameWidth));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else if (def.options) {
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push(`${cmd.charAt(0).toUpperCase() + cmd.slice(1).replace(/-/g, ' ')} options:`);
|
|
263
|
+
lines.push(formatLine(`emailengine ${cmd}`, def.description, null, null, width, nameWidth));
|
|
264
|
+
for (const opt of def.options) {
|
|
265
|
+
lines.push(formatLine(opt.name, opt.description, opt.type, opt.default, width, nameWidth));
|
|
266
|
+
}
|
|
267
|
+
if (def.example) {
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push(' Example: ' + def.example);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return lines.join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function generateCommandHelp(cmdName) {
|
|
278
|
+
const width = getTerminalWidth();
|
|
279
|
+
const nameWidth = calculateGlobalNameWidth();
|
|
280
|
+
const lines = [];
|
|
281
|
+
|
|
282
|
+
// Check if command exists
|
|
283
|
+
if (!COMMANDS[cmdName]) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const def = COMMANDS[cmdName];
|
|
288
|
+
|
|
289
|
+
lines.push(`emailengine ${cmdName} [options]`);
|
|
290
|
+
lines.push('');
|
|
291
|
+
lines.push(wrapText(def.description, width, 0));
|
|
292
|
+
|
|
293
|
+
if (def.subcommands) {
|
|
294
|
+
lines.push('');
|
|
295
|
+
lines.push('Commands:');
|
|
296
|
+
for (const [subCmd, subDef] of Object.entries(def.subcommands)) {
|
|
297
|
+
lines.push(formatLine(`emailengine ${cmdName} ${subCmd}`, subDef.description, null, null, width, nameWidth));
|
|
298
|
+
if (subDef.options) {
|
|
299
|
+
lines.push('');
|
|
300
|
+
lines.push(' Options:');
|
|
301
|
+
for (const opt of subDef.options) {
|
|
302
|
+
lines.push(formatLine(opt.name, opt.description, opt.type, opt.default, width, nameWidth));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} else if (def.options) {
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push('Options:');
|
|
309
|
+
for (const opt of def.options) {
|
|
310
|
+
lines.push(formatLine(opt.name, opt.description, opt.type, opt.default, width, nameWidth));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (def.example) {
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push('Example:');
|
|
317
|
+
lines.push(' ' + def.example);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return lines.join('\n');
|
|
321
|
+
}
|
|
322
|
+
|
|
20
323
|
function run() {
|
|
21
324
|
let cmd = ((argv._ && argv._[0]) || '').toLowerCase();
|
|
22
325
|
if (!cmd) {
|
|
@@ -88,16 +391,24 @@ function run() {
|
|
|
88
391
|
break;
|
|
89
392
|
|
|
90
393
|
case 'help':
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
394
|
+
{
|
|
395
|
+
// Show help for specific command or general help
|
|
396
|
+
let helpCmd = ((argv._ && argv._[1]) || '').toLowerCase();
|
|
397
|
+
if (helpCmd) {
|
|
398
|
+
let cmdHelp = generateCommandHelp(helpCmd);
|
|
399
|
+
if (cmdHelp) {
|
|
400
|
+
console.error(cmdHelp);
|
|
401
|
+
} else {
|
|
402
|
+
console.error(`Unknown command: ${helpCmd}`);
|
|
403
|
+
console.error('');
|
|
404
|
+
console.error('Run "emailengine help" to see available commands.');
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
console.error(generateHelp());
|
|
97
409
|
}
|
|
98
|
-
console.error(helpText.toString().trim());
|
|
99
410
|
process.exit();
|
|
100
|
-
}
|
|
411
|
+
}
|
|
101
412
|
break;
|
|
102
413
|
|
|
103
414
|
case 'version':
|
|
@@ -290,6 +601,75 @@ function run() {
|
|
|
290
601
|
}
|
|
291
602
|
break;
|
|
292
603
|
|
|
604
|
+
case 'check-bounce':
|
|
605
|
+
{
|
|
606
|
+
process.title = 'emailengine-check-bounce';
|
|
607
|
+
const { bounceDetect } = require('../lib/bounce-detect');
|
|
608
|
+
const bounceClassifier = require('@postalsys/bounce-classifier');
|
|
609
|
+
|
|
610
|
+
let emlPath = argv._[1] || argv.file || argv.f;
|
|
611
|
+
|
|
612
|
+
if (!emlPath) {
|
|
613
|
+
console.error('Error: EML file path is required');
|
|
614
|
+
console.error('Usage: emailengine check-bounce <path-to-eml-file>');
|
|
615
|
+
console.error(' emailengine check-bounce --file <path-to-eml-file>');
|
|
616
|
+
return process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Resolve the path
|
|
620
|
+
emlPath = pathlib.resolve(emlPath);
|
|
621
|
+
|
|
622
|
+
// Check if file exists
|
|
623
|
+
if (!fs.existsSync(emlPath)) {
|
|
624
|
+
console.error(`Error: File not found: ${emlPath}`);
|
|
625
|
+
return process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const checkBounce = async () => {
|
|
629
|
+
// Initialize the bounce classifier
|
|
630
|
+
await bounceClassifier.initialize();
|
|
631
|
+
|
|
632
|
+
// Read the EML file
|
|
633
|
+
const emlStream = fs.createReadStream(emlPath);
|
|
634
|
+
|
|
635
|
+
// Detect bounce information
|
|
636
|
+
const bounce = await bounceDetect(emlStream);
|
|
637
|
+
|
|
638
|
+
// Classify the bounce if we have a response message
|
|
639
|
+
if (bounce?.response?.message) {
|
|
640
|
+
const classification = await bounceClassifier.classify(bounce.response.message);
|
|
641
|
+
|
|
642
|
+
if (classification?.label) {
|
|
643
|
+
bounce.response.category = classification.label;
|
|
644
|
+
}
|
|
645
|
+
if (classification?.action) {
|
|
646
|
+
bounce.response.recommendedAction = classification.action;
|
|
647
|
+
}
|
|
648
|
+
if (classification?.blocklist) {
|
|
649
|
+
bounce.response.blocklist = classification.blocklist;
|
|
650
|
+
}
|
|
651
|
+
if (classification?.retryAfter) {
|
|
652
|
+
bounce.response.retryAfter = classification.retryAfter;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return bounce;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
checkBounce()
|
|
660
|
+
.then(result => {
|
|
661
|
+
process.stdout.write(JSON.stringify(result, null, 2));
|
|
662
|
+
process.stdout.write('\n');
|
|
663
|
+
return process.exit(0);
|
|
664
|
+
})
|
|
665
|
+
.catch(err => {
|
|
666
|
+
console.error('Failed to analyze bounce email');
|
|
667
|
+
console.error(err);
|
|
668
|
+
return process.exit(1);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
|
|
293
673
|
default:
|
|
294
674
|
// run normally
|
|
295
675
|
require('../server');
|
package/lib/arf-detect.js
CHANGED
|
@@ -77,7 +77,9 @@ const arfDetect = async messageInfo => {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
case 'message/rfc822':
|
|
80
|
-
case 'message/rfc822-headers':
|
|
80
|
+
case 'message/rfc822-headers':
|
|
81
|
+
case 'text/rfc822-headers':
|
|
82
|
+
case 'text/rfc822-header': {
|
|
81
83
|
let contents = (attachment.content || '').toString();
|
|
82
84
|
const headerPos = contents.match(/\r?\n\r?\n/);
|
|
83
85
|
|