Haraka 2.8.28 → 3.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/.eslintrc.yaml +2 -10
- package/Changes.md +84 -2
- package/Dockerfile +1 -1
- package/Plugins.md +9 -4
- package/README.md +2 -6
- package/bin/haraka +5 -4
- package/config/outbound.ini +0 -7
- package/config/plugins +1 -1
- package/config/smtp.ini +1 -1
- package/config/smtp_forward.ini +2 -8
- package/config/smtp_proxy.ini +0 -6
- package/connection.js +178 -204
- package/coverage/lcov.info +13863 -0
- package/coverage/tmp/coverage-42958-1658373250585-0.json +1 -0
- package/coverage/tmp/coverage-42961-1658373250529-0.json +1 -0
- package/dkim.js +66 -73
- package/docs/Body.md +1 -22
- package/docs/CoreConfig.md +2 -2
- package/docs/Header.md +1 -47
- package/docs/Outbound.md +8 -36
- package/endpoint.js +1 -1
- package/haraka.js +1 -1
- package/host_pool.js +8 -12
- package/logger.js +25 -32
- package/outbound/client_pool.js +11 -153
- package/outbound/config.js +5 -11
- package/outbound/hmail.js +109 -143
- package/outbound/index.js +13 -25
- package/outbound/mx_lookup.js +10 -7
- package/outbound/queue.js +8 -12
- package/outbound/timer_queue.js +2 -4
- package/outbound/tls.js +17 -18
- package/outbound/todo.js +1 -0
- package/package.json +57 -55
- package/plugins/auth/auth_base.js +39 -63
- package/plugins/auth/auth_bridge.js +3 -4
- package/plugins/auth/auth_proxy.js +16 -16
- package/plugins/auth/auth_vpopmaild.js +30 -37
- package/plugins/auth/flat_file.js +9 -13
- package/plugins/avg.js +9 -11
- package/plugins/backscatterer.js +1 -1
- package/plugins/block_me.js +2 -6
- package/plugins/bounce.js +106 -124
- package/plugins/clamd.js +59 -63
- package/plugins/data.signatures.js +6 -6
- package/plugins/data.uribl.js +1 -415
- package/plugins/delay_deny.js +19 -20
- package/plugins/dkim_sign.js +56 -62
- package/plugins/dkim_verify.js +9 -8
- package/plugins/dns_list_base.js +43 -42
- package/plugins/dnsbl.js +41 -46
- package/plugins/dnswl.js +23 -26
- package/plugins/early_talker.js +24 -28
- package/plugins/esets.js +8 -11
- package/plugins/greylist.js +161 -190
- package/plugins/helo.checks.js +175 -197
- package/plugins/mail_from.is_resolvable.js +38 -38
- package/plugins/messagesniffer.js +33 -40
- package/plugins/prevent_credential_leaks.js +7 -5
- package/plugins/process_title.js +16 -17
- package/plugins/queue/deliver.js +2 -2
- package/plugins/queue/lmtp.js +5 -6
- package/plugins/queue/qmail-queue.js +11 -13
- package/plugins/queue/quarantine.js +25 -34
- package/plugins/queue/rabbitmq.js +3 -2
- package/plugins/queue/rabbitmq_amqplib.js +9 -9
- package/plugins/queue/smtp_bridge.js +5 -4
- package/plugins/queue/smtp_forward.js +81 -89
- package/plugins/queue/smtp_proxy.js +21 -22
- package/plugins/queue/test.js +2 -1
- package/plugins/rcpt_to.host_list_base.js +20 -30
- package/plugins/rcpt_to.in_host_list.js +12 -14
- package/plugins/rcpt_to.max_count.js +7 -5
- package/plugins/record_envelope_addresses.js +4 -6
- package/plugins/relay.js +64 -74
- package/plugins/reseed_rng.js +1 -2
- package/plugins/spamassassin.js +56 -68
- package/plugins/status.js +2 -3
- package/plugins/tarpit.js +8 -11
- package/plugins/tls.js +14 -17
- package/plugins/toobusy.js +6 -8
- package/plugins/xclient.js +14 -25
- package/plugins.js +24 -29
- package/rfc1869.js +2 -2
- package/server.js +3 -13
- package/smtp_client.js +138 -215
- package/tests/config/smtp_forward.ini +0 -6
- package/tests/fixtures/line_socket.js +1 -1
- package/tests/fixtures/util_hmailitem.js +5 -7
- package/tests/fixtures/vm_harness.js +2 -2
- package/tests/host_pool.js +13 -14
- package/tests/installation/plugins/inherits.js +1 -2
- package/tests/logger.js +2 -2
- package/tests/plugins/bounce.js +6 -8
- package/tests/plugins/dkim_signer.js +7 -7
- package/tests/plugins/dns_list_base.js +7 -7
- package/tests/plugins/helo.checks.js +1 -1
- package/tests/plugins/mail_from.is_resolvable.js +10 -54
- package/tests/plugins/queue/smtp_forward.js +11 -11
- package/tests/plugins/rcpt_to.host_list_base.js +1 -1
- package/tests/plugins/rcpt_to.in_host_list.js +1 -1
- package/tests/plugins/spamassassin.js +1 -1
- package/tests/queue/multibyte +0 -0
- package/tests/queue/plain +0 -0
- package/tests/rfc1869.js +4 -1
- package/tests/server.js +15 -9
- package/tests/smtp_client/auth.js +4 -14
- package/tests/smtp_client/basic.js +5 -15
- package/tests/smtp_client.js +7 -3
- package/tests/transaction.js +72 -19
- package/tls_socket.js +75 -85
- package/transaction.js +7 -9
- package/attachment_stream.js +0 -118
- package/bin/spf +0 -48
- package/chunkemitter.js +0 -75
- package/config/data.uribl.excludes +0 -202
- package/config/data.uribl.ini +0 -37
- package/config/spf.ini +0 -1
- package/docs/plugins/attachment.md +0 -92
- package/docs/plugins/data.uribl.md +0 -120
- package/docs/plugins/spf.md +0 -142
- package/mailbody.js +0 -502
- package/mailheader.js +0 -304
- package/messagestream.js +0 -441
- package/plugins/aliases.js +0 -120
- package/plugins/attachment.js +0 -503
- package/plugins/connect.p0f.js +0 -5
- package/plugins/spf.js +0 -327
- package/spf.js +0 -689
- package/tests/mailbody.js +0 -348
- package/tests/mailheader.js +0 -138
- package/tests/messagestream.js +0 -34
- package/tests/plugins/aliases.js +0 -376
- package/tests/plugins/spf.js +0 -251
- package/tests/spf.js +0 -96
package/docs/plugins/spf.md
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
spf
|
|
2
|
-
===
|
|
3
|
-
|
|
4
|
-
This plugin implements RFC 4408 Sender Policy Framework (SPF)
|
|
5
|
-
See the [Wikipedia article on SPF](http://en.wikipedia.org/wiki/Sender_Policy_Framework) for details.
|
|
6
|
-
|
|
7
|
-
By default this plugin with only add trace Received-SPF headers to a message.
|
|
8
|
-
To make it reject mail then you will need to enable the relevant options below.
|
|
9
|
-
`[deny]helo_fail` and `[deny]mfrom_fail` are the closest match for the intent
|
|
10
|
-
of SPF but you will need to whitelist any hosts forwarding mail from another
|
|
11
|
-
domain whilst preserving the original return-path.
|
|
12
|
-
|
|
13
|
-
Configuration
|
|
14
|
-
-------------
|
|
15
|
-
|
|
16
|
-
This plugin uses spf.ini for configuration and the following options are
|
|
17
|
-
available:
|
|
18
|
-
|
|
19
|
-
[relay]
|
|
20
|
-
context=sender (default: sender)
|
|
21
|
-
|
|
22
|
-
On connections with relaying privileges (MSA or mail relay), it is often
|
|
23
|
-
desirable to evaluate SPF from the context of Haraka's public IP(s), in the
|
|
24
|
-
same fashion the next mail server will evaluate it when we send to them.
|
|
25
|
-
In that use case, Haraka should use context=myself.
|
|
26
|
-
|
|
27
|
-
* context=sender evaluate SPF based on the sender (connection.remote.ip)
|
|
28
|
-
* context=myself evaluate SPF based on Haraka's public IP
|
|
29
|
-
|
|
30
|
-
The rest of the optional settings (disabled by default) permit deferring or
|
|
31
|
-
denying mail from senders whose SPF fails the checks.
|
|
32
|
-
|
|
33
|
-
Additional settings allow you to control the small things (defaults are shown):
|
|
34
|
-
|
|
35
|
-
; The lookup timeout, in seconds. Better set it to something much lower than this.
|
|
36
|
-
lookup_timeout = 29
|
|
37
|
-
|
|
38
|
-
; bypass hosts that match these conditions
|
|
39
|
-
[skip]
|
|
40
|
-
; hosts that relay through us
|
|
41
|
-
relaying = false
|
|
42
|
-
; hosts that are SMTP AUTH'ed
|
|
43
|
-
auth = false
|
|
44
|
-
|
|
45
|
-
There's a special setting that would allow the plugin to emit a funny explanation text on SPF DENY, essentially meant to be visible to end-users that will receive the bounce. The text is `http://www.openspf.org/Why?s=${scope}&id=${sender_id}&ip=${connection.remote.ip}` and is enabled by:
|
|
46
|
-
|
|
47
|
-
[deny]
|
|
48
|
-
openspf_text = true
|
|
49
|
-
|
|
50
|
-
; in case you DENY on failing SPF on hosts that are relaying (but why?)
|
|
51
|
-
[deny_relay]
|
|
52
|
-
openspf_text = true
|
|
53
|
-
|
|
54
|
-
### Things to Know
|
|
55
|
-
|
|
56
|
-
* Most senders do not publish SPF records for their mail server *hostname*,
|
|
57
|
-
which means that the SPF HELO test rarely passes. During observation in 2014,
|
|
58
|
-
more spam senders have valid SPF HELO than ham senders. If you expect very
|
|
59
|
-
little from SPF HELO validation, you might still be disappointed.
|
|
60
|
-
|
|
61
|
-
* Enabling error deferrals will cause excessive delays and perhaps bounced
|
|
62
|
-
mail for senders with broken DNS. Enable this only if you are willing to
|
|
63
|
-
delay and sometimes lose valid mail.
|
|
64
|
-
|
|
65
|
-
* Broken SPF records by valid senders are common. Keep that in mind when
|
|
66
|
-
considering denial of SPF error results. If you deny on error, budget
|
|
67
|
-
time for instructing senders on how to correct their SPF records so they
|
|
68
|
-
can email you.
|
|
69
|
-
|
|
70
|
-
* The only deny option most sites should consider is `mfrom_fail`. That will
|
|
71
|
-
reject messages that explicitely fail SPF tests. SPF failures have a high
|
|
72
|
-
correlation with spam. However, up to 10% of ham transits forwarders and/or
|
|
73
|
-
email lists which frequently break SPF. SPF results are best used as inputs
|
|
74
|
-
to other plugins such as DMARC, [spamassassin](http://haraka.github.io/manual/plugins/spamassassin.html), and [karma](http://haraka.github.io/manual/plugins/karma.html).
|
|
75
|
-
|
|
76
|
-
* Heed well the implications of SPF, as described in [RFC 4408](http://tools.ietf.org/html/rfc4408#section-9.3)
|
|
77
|
-
|
|
78
|
-
[defer]
|
|
79
|
-
helo_temperror
|
|
80
|
-
mfrom_temperror
|
|
81
|
-
|
|
82
|
-
[deny]
|
|
83
|
-
helo_none
|
|
84
|
-
helo_softfail
|
|
85
|
-
helo_fail
|
|
86
|
-
helo_permerror
|
|
87
|
-
|
|
88
|
-
mfrom_none
|
|
89
|
-
mfrom_softfail
|
|
90
|
-
mfrom_fail
|
|
91
|
-
mfrom_permerror
|
|
92
|
-
|
|
93
|
-
openspf_text
|
|
94
|
-
|
|
95
|
-
; SPF settings used when connection.relaying=true
|
|
96
|
-
[defer_relay]
|
|
97
|
-
helo_temperror
|
|
98
|
-
mfrom_temperror
|
|
99
|
-
|
|
100
|
-
[deny_relay]
|
|
101
|
-
helo_none
|
|
102
|
-
helo_softfail
|
|
103
|
-
helo_fail
|
|
104
|
-
helo_permerror
|
|
105
|
-
|
|
106
|
-
mfrom_none
|
|
107
|
-
mfrom_softfail
|
|
108
|
-
mfrom_fail
|
|
109
|
-
mfrom_permerror
|
|
110
|
-
|
|
111
|
-
openspf_text
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Testing
|
|
115
|
-
-------
|
|
116
|
-
|
|
117
|
-
This plugin also provides a command-line test tool that can be used to debug SPF issues or to check results.
|
|
118
|
-
|
|
119
|
-
To check the SPF record for a domain:
|
|
120
|
-
|
|
121
|
-
````
|
|
122
|
-
# spf --ip 1.2.3.4 --domain fsl.com
|
|
123
|
-
ip=1.2.3.4 helo="" domain="fsl.com" result=Fail
|
|
124
|
-
````
|
|
125
|
-
|
|
126
|
-
To check the SPF record for a HELO/EHLO name:
|
|
127
|
-
|
|
128
|
-
````
|
|
129
|
-
# spf --ip 1.2.3.4 --helo foo.bar.com
|
|
130
|
-
ip=1.2.3.4 helo="foo.bar.com" domain="" result=None
|
|
131
|
-
````
|
|
132
|
-
|
|
133
|
-
You can add `--debug` to the option arguments to see a full trace of the SPF processing.
|
|
134
|
-
|
|
135
|
-
### SPF Resource Record Type
|
|
136
|
-
|
|
137
|
-
Node does not support the SPF DNS Resource Record type. Only TXT records are
|
|
138
|
-
checked.
|
|
139
|
-
|
|
140
|
-
This is a non-issue as < 1% (as of 2014) of SPF records use the SPF RR type.
|
|
141
|
-
Due to lack of adoption, the next SPF revision will like likely deprecate the
|
|
142
|
-
SPF RR type.
|
package/mailbody.js
DELETED
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const events = require('events');
|
|
4
|
-
const config = require('haraka-config');
|
|
5
|
-
const libqp = require('libqp');
|
|
6
|
-
|
|
7
|
-
// Mail Body Parser
|
|
8
|
-
const logger = require('./logger');
|
|
9
|
-
const Header = require('./mailheader').Header;
|
|
10
|
-
const Iconv = require('./mailheader').Iconv;
|
|
11
|
-
const attstr = require('./attachment_stream');
|
|
12
|
-
|
|
13
|
-
const buf_siz = config.get('mailparser.bufsize') || 65536;
|
|
14
|
-
|
|
15
|
-
class Body extends events.EventEmitter {
|
|
16
|
-
constructor (header, options) {
|
|
17
|
-
super();
|
|
18
|
-
this.header = header || new Header();
|
|
19
|
-
this.header_lines = [];
|
|
20
|
-
this.is_html = false;
|
|
21
|
-
this.options = options || {};
|
|
22
|
-
this.filters = [];
|
|
23
|
-
this.bodytext = '';
|
|
24
|
-
|
|
25
|
-
// Caution: slice before using! We build up data in this buffer, and
|
|
26
|
-
// it always has extra space at the end. Use
|
|
27
|
-
// this.body_text_encoded.slice(0, this.body_text_encoded_pos).
|
|
28
|
-
this.body_text_encoded = Buffer.alloc(buf_siz);
|
|
29
|
-
this.body_text_encoded_pos = 0;
|
|
30
|
-
|
|
31
|
-
this.body_encoding = null;
|
|
32
|
-
this.boundary = null;
|
|
33
|
-
this.ct = null;
|
|
34
|
-
this.decode_function = null;
|
|
35
|
-
this.children = []; // if multipart
|
|
36
|
-
this.state = 'start';
|
|
37
|
-
this.buf = Buffer.alloc(buf_siz);
|
|
38
|
-
this.buf_fill = 0;
|
|
39
|
-
this.decode_accumulator = '';
|
|
40
|
-
this.decode_qp = line => libqp.decode(line.toString());
|
|
41
|
-
this.decode_7bit = this.decode_8bit;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
add_filter (filter) {
|
|
45
|
-
this.filters.push(filter);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
set_banner (banners) {
|
|
49
|
-
this.add_filter((ct, enc, buf) => insert_banner(ct, enc, buf, banners));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
parse_more (line) {
|
|
53
|
-
// Ensure we're working in buffers, for the tests (transaction should
|
|
54
|
-
// always pass buffers).
|
|
55
|
-
if (!Buffer.isBuffer(line)) line = Buffer.from(line);
|
|
56
|
-
|
|
57
|
-
return this[`parse_${this.state}`](line);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
parse_child (line) {
|
|
61
|
-
const line_string = line.toString();
|
|
62
|
-
|
|
63
|
-
// check for MIME boundary
|
|
64
|
-
if (line_string.substr(0, (this.boundary.length + 2)) === (`--${this.boundary}`)) {
|
|
65
|
-
|
|
66
|
-
line = this.children[this.children.length -1].parse_end(line);
|
|
67
|
-
|
|
68
|
-
if (line_string.substr(this.boundary.length + 2, 2) === '--') {
|
|
69
|
-
// end
|
|
70
|
-
this.state = 'end';
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
this.emit('mime_boundary', line_string);
|
|
74
|
-
const bod = new Body(new Header(), this.options);
|
|
75
|
-
this.listeners('attachment_start').forEach(cb => { bod.on('attachment_start', cb) });
|
|
76
|
-
this.listeners('attachment_data' ).forEach(cb => { bod.on('attachment_data', cb) });
|
|
77
|
-
this.listeners('attachment_end' ).forEach(cb => { bod.on('attachment_end', cb) });
|
|
78
|
-
this.listeners('mime_boundary').forEach(cb => bod.on('mime_boundary', cb));
|
|
79
|
-
this.filters.forEach(f => { bod.add_filter(f); });
|
|
80
|
-
this.children.push(bod);
|
|
81
|
-
bod.state = 'headers';
|
|
82
|
-
}
|
|
83
|
-
return line;
|
|
84
|
-
}
|
|
85
|
-
// Pass data into last child
|
|
86
|
-
return this.children[this.children.length - 1].parse_more(line);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
parse_headers (line) {
|
|
90
|
-
const line_string = line.toString();
|
|
91
|
-
|
|
92
|
-
if (/^\s*$/.test(line_string)) {
|
|
93
|
-
// end of headers
|
|
94
|
-
this.header.parse(this.header_lines);
|
|
95
|
-
delete this.header_lines;
|
|
96
|
-
this.state = 'start';
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
this.header_lines.push(line_string);
|
|
100
|
-
}
|
|
101
|
-
return line;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
parse_start (line) {
|
|
105
|
-
const ct = this.header.get_decoded('content-type') || 'text/plain';
|
|
106
|
-
let enc = this.header.get_decoded('content-transfer-encoding') || '8bit';
|
|
107
|
-
const cd = this.header.get_decoded('content-disposition') || '';
|
|
108
|
-
|
|
109
|
-
if (/text\/html/i.test(ct)) {
|
|
110
|
-
this.is_html = true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
enc = enc.toLowerCase().split("\n").pop().trim();
|
|
114
|
-
if (!enc.match(/^base64|quoted-printable|[78]bit$/i)) {
|
|
115
|
-
logger.logwarn(`Invalid CTE on email: ${enc}, using 8bit`);
|
|
116
|
-
enc = '8bit';
|
|
117
|
-
}
|
|
118
|
-
enc = enc.replace(/^quoted-printable$/i, 'qp');
|
|
119
|
-
|
|
120
|
-
this.decode_function = this[`decode_${enc}`];
|
|
121
|
-
if (!this.decode_function) {
|
|
122
|
-
logger.logerror(`No decode function found for: ${enc}`);
|
|
123
|
-
this.decode_function = this.decode_8bit;
|
|
124
|
-
}
|
|
125
|
-
this.ct = ct;
|
|
126
|
-
|
|
127
|
-
let match;
|
|
128
|
-
if (/^(?:text|message)\//i.test(ct) && !/^attachment/i.test(cd) ) {
|
|
129
|
-
this.state = 'body';
|
|
130
|
-
}
|
|
131
|
-
else if (/^multipart\//i.test(ct)) {
|
|
132
|
-
match = ct.match(/boundary\s*=\s*"?([^";]+)"?/i);
|
|
133
|
-
this.boundary = match ? match[1] : '';
|
|
134
|
-
this.state = 'multipart_preamble';
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
match = cd.match(/name\s*=\s*"?([^";]+)"?/i);
|
|
138
|
-
if (!match) {
|
|
139
|
-
match = ct.match(/name\s*=\s*"?([^";]+)"?/i);
|
|
140
|
-
}
|
|
141
|
-
const filename = match ? match[1] : '';
|
|
142
|
-
this.attachment_stream = attstr.createStream(this.header);
|
|
143
|
-
this.emit('attachment_start', ct, filename, this, this.attachment_stream);
|
|
144
|
-
this.buf_fill = 0;
|
|
145
|
-
this.state = 'attachment';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return this[`parse_${this.state}`](line);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
_empty_filter (ct, enc) {
|
|
152
|
-
let new_buf = Buffer.from('');
|
|
153
|
-
this.filters.forEach(filter => {
|
|
154
|
-
new_buf = filter(ct, enc, new_buf) || new_buf;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return new_buf;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
force_end () {
|
|
161
|
-
if (this.state === 'attachment') {
|
|
162
|
-
if (this.buf_fill > 0) {
|
|
163
|
-
// see below for why we create a new buffer here.
|
|
164
|
-
const to_emit = Buffer.alloc(this.buf_fill);
|
|
165
|
-
this.buf.copy(to_emit, 0, 0, this.buf_fill);
|
|
166
|
-
this.attachment_stream.emit_data(to_emit);
|
|
167
|
-
this.buf_fill = 0;
|
|
168
|
-
}
|
|
169
|
-
this.attachment_stream.emit_end(true);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
parse_end (line) {
|
|
174
|
-
if (!line) {
|
|
175
|
-
line = Buffer.from('');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (this.state === 'attachment') {
|
|
179
|
-
if (this.buf_fill > 0) {
|
|
180
|
-
// see below for why we create a new buffer here.
|
|
181
|
-
const to_emit = Buffer.alloc(this.buf_fill);
|
|
182
|
-
this.buf.copy(to_emit, 0, 0, this.buf_fill);
|
|
183
|
-
this.attachment_stream.emit_data(to_emit);
|
|
184
|
-
this.buf_fill = 0;
|
|
185
|
-
}
|
|
186
|
-
this.attachment_stream.emit_end();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const ct = this.header.get_decoded('content-type') || 'text/plain';
|
|
190
|
-
let enc = 'UTF-8';
|
|
191
|
-
let pre_enc = '';
|
|
192
|
-
const matches = /\bcharset\s*=\s*(?:"|3D|')?([\w_-]*)(?:"|3D|')?/.exec(ct);
|
|
193
|
-
if (matches) {
|
|
194
|
-
pre_enc = (matches[1]).trim();
|
|
195
|
-
if (pre_enc.length > 0) {
|
|
196
|
-
enc = pre_enc;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
this.body_encoding = enc;
|
|
200
|
-
|
|
201
|
-
if (!this.body_text_encoded_pos) { // nothing to decode
|
|
202
|
-
return Buffer.concat([this._empty_filter(ct, enc) || Buffer.from(''), line]);
|
|
203
|
-
}
|
|
204
|
-
if (this.bodytext.length !== 0) return line; // already decoded?
|
|
205
|
-
|
|
206
|
-
let buf = this.decode_function(this.body_text_encoded.slice(0, this.body_text_encoded_pos));
|
|
207
|
-
|
|
208
|
-
if (this.filters.length) {
|
|
209
|
-
// up until this point we've returned '' for line, so now we run
|
|
210
|
-
// the filters and return the whole lot as one line, re-encoded using
|
|
211
|
-
// whatever encoding scheme we used to decode it.
|
|
212
|
-
|
|
213
|
-
let new_buf = buf;
|
|
214
|
-
this.filters.forEach(filter => {
|
|
215
|
-
new_buf = filter(ct, enc, new_buf) || new_buf;
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// convert back to base_64 or QP if required:
|
|
219
|
-
if (this.decode_function === this.decode_qp) {
|
|
220
|
-
line = Buffer.from(`${libqp.wrap(libqp.encode(new_buf))}\n${line}`);
|
|
221
|
-
}
|
|
222
|
-
else if (this.decode_function === this.decode_base64) {
|
|
223
|
-
line = Buffer.from(new_buf.toString("base64").replace(/(.{1,76})/g, "$1\n") + line);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
line = Buffer.concat([new_buf, line]);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
buf = new_buf;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// convert the buffer to UTF-8, stored in this.bodytext
|
|
233
|
-
this.try_iconv(buf, enc);
|
|
234
|
-
|
|
235
|
-
// delete this.body_text_encoded;
|
|
236
|
-
return line;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
try_iconv (buf, enc) {
|
|
240
|
-
|
|
241
|
-
if (!Iconv) {
|
|
242
|
-
this.body_encoding = 'no_iconv';
|
|
243
|
-
this.bodytext = buf.toString();
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (/UTF-?8/i.test(enc)) {
|
|
248
|
-
this.bodytext = buf.toString();
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
const converter = new Iconv(enc, "UTF-8");
|
|
254
|
-
this.bodytext = converter.convert(buf).toString();
|
|
255
|
-
}
|
|
256
|
-
catch (err) {
|
|
257
|
-
logger.logwarn(`initial iconv conversion from ${enc} to UTF-8 failed: ${err.message}`);
|
|
258
|
-
this.body_encoding = `broken//${enc}`;
|
|
259
|
-
// EINVAL is returned when the encoding type is not recognized/supported (e.g. ANSI_X3)
|
|
260
|
-
if (err.code !== 'EINVAL') {
|
|
261
|
-
// Perform the conversion again, but ignore any errors
|
|
262
|
-
try {
|
|
263
|
-
const converter = new Iconv(enc, 'UTF-8//TRANSLIT//IGNORE');
|
|
264
|
-
this.bodytext = converter.convert(buf).toString();
|
|
265
|
-
}
|
|
266
|
-
catch (e) {
|
|
267
|
-
logger.logwarn(`iconv conversion from ${enc} to UTF-8 failed: ${e.message}`);
|
|
268
|
-
this.bodytext = buf.toString();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
parse_body (line) {
|
|
275
|
-
if (!Buffer.isBuffer(line)) line = Buffer.from(line);
|
|
276
|
-
|
|
277
|
-
// Grow the body_text_encoded buffer if we need more space. Doing this
|
|
278
|
-
// instead of constant Buffer.concat()s means we allocate/copy way less
|
|
279
|
-
// often.
|
|
280
|
-
if (this.body_text_encoded_pos + line.length > this.body_text_encoded.length) {
|
|
281
|
-
let new_size = this.body_text_encoded.length * 2;
|
|
282
|
-
while (this.body_text_encoded_pos + line.length > new_size) new_size *= 2;
|
|
283
|
-
|
|
284
|
-
this.body_text_encoded = Buffer.alloc(
|
|
285
|
-
new_size, this.body_text_encoded.slice(0, this.body_text_encoded_pos));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
line.copy(this.body_text_encoded, this.body_text_encoded_pos);
|
|
289
|
-
this.body_text_encoded_pos += line.length;
|
|
290
|
-
|
|
291
|
-
if (this.filters.length) return '';
|
|
292
|
-
return line;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
parse_multipart_preamble (line) {
|
|
296
|
-
if (!this.boundary) return line;
|
|
297
|
-
const line_string = line.toString();
|
|
298
|
-
|
|
299
|
-
if (line_string.substr(0, (this.boundary.length + 2)) === (`--${this.boundary}`)) {
|
|
300
|
-
if (line_string.substr(this.boundary.length + 2, 2) === '--') {
|
|
301
|
-
// end
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
// next section
|
|
305
|
-
this.emit('mime_boundary', line_string);
|
|
306
|
-
const bod = new Body(new Header(), this.options);
|
|
307
|
-
this.listeners('attachment_start').forEach(cb => { bod.on('attachment_start', cb) });
|
|
308
|
-
this.listeners('mime_boundary').forEach(cb => bod.on('mime_boundary', cb));
|
|
309
|
-
this.filters.forEach(f => { bod.add_filter(f); });
|
|
310
|
-
this.children.push(bod);
|
|
311
|
-
bod.state = 'headers';
|
|
312
|
-
this.state = 'child';
|
|
313
|
-
}
|
|
314
|
-
return line;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return line;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
parse_attachment (line) {
|
|
321
|
-
const line_string = line.toString();
|
|
322
|
-
|
|
323
|
-
if (this.boundary) {
|
|
324
|
-
if (line_string.substr(0, (this.boundary.length + 2)) === (`--${this.boundary}`)) {
|
|
325
|
-
if (line_string.substr(this.boundary.length + 2, 2) === '--') {
|
|
326
|
-
// end
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
// next section
|
|
330
|
-
this.state = 'headers';
|
|
331
|
-
}
|
|
332
|
-
return line;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const buf = this.decode_function(line);
|
|
337
|
-
if ((buf.length + this.buf_fill) > buf_siz) {
|
|
338
|
-
// now we have to create a new buffer, because if we write this out
|
|
339
|
-
// using async code, it will get overwritten under us. Creating a new
|
|
340
|
-
// buffer eliminates that problem (at the expense of a malloc and a
|
|
341
|
-
// memcpy())
|
|
342
|
-
const to_emit = Buffer.alloc(this.buf_fill);
|
|
343
|
-
this.buf.copy(to_emit, 0, 0, this.buf_fill);
|
|
344
|
-
this.attachment_stream.emit_data(to_emit);
|
|
345
|
-
if (buf.length > buf_siz) {
|
|
346
|
-
// this is an unusual case - the base64/whatever data is larger
|
|
347
|
-
// than our buffer size, so we just emit it and reset the counter.
|
|
348
|
-
this.attachment_stream.emit_data(buf);
|
|
349
|
-
this.buf_fill = 0;
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
buf.copy(this.buf);
|
|
353
|
-
this.buf_fill = buf.length;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
buf.copy(this.buf, this.buf_fill);
|
|
358
|
-
this.buf_fill += buf.length;
|
|
359
|
-
}
|
|
360
|
-
return line;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
decode_base64 (line) {
|
|
364
|
-
// Remove all whitespace (such as newlines and errant spaces) from base64
|
|
365
|
-
// before combining it with any previously unprocessed data.
|
|
366
|
-
let to_process = this.decode_accumulator + line.toString().trim().replace(/[\s]+/g,'');
|
|
367
|
-
|
|
368
|
-
// Sometimes base64 data lines will not be aligned with
|
|
369
|
-
// byte boundaries. This is because each char in base64
|
|
370
|
-
// represents 6 bits. 24 is the LCM between 6 and 8 bits.
|
|
371
|
-
// (i.e. 4 * 6-bit chars === 3 * bytes)
|
|
372
|
-
|
|
373
|
-
// As a result, 24 bits is our word boundary for base64.
|
|
374
|
-
// Failure to align here will result in truncated/incorrect
|
|
375
|
-
// node Buffers later on.
|
|
376
|
-
|
|
377
|
-
// Walk back from the toProcess.length to the first
|
|
378
|
-
// position that aligns with a 24-bit boundary.
|
|
379
|
-
const emit_length = to_process.length - (to_process.length % 4)
|
|
380
|
-
|
|
381
|
-
if (emit_length > 0) {
|
|
382
|
-
const emit_now = to_process.substring(0, emit_length);
|
|
383
|
-
this.decode_accumulator = to_process.substring(emit_length);
|
|
384
|
-
return Buffer.from(emit_now, 'base64');
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
this.decode_accumulator = '';
|
|
388
|
-
// This is the end of the base64 data, we don't really have enough bits
|
|
389
|
-
// to fill up the bytes, but that's because we're on the last line, and ==
|
|
390
|
-
// might have been elided.
|
|
391
|
-
|
|
392
|
-
// In order to prevent any weird boundary issues, we'll re-pad
|
|
393
|
-
// the string if there's any data left. As above, our target
|
|
394
|
-
// is a 24-bit boundary, pad to 4 characters.
|
|
395
|
-
while (to_process.length > 0 && to_process.length < 4) {
|
|
396
|
-
to_process += '=';
|
|
397
|
-
}
|
|
398
|
-
return Buffer.from(to_process, 'base64');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
decode_8bit (line) {
|
|
403
|
-
return Buffer.from(line, 'binary');
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
exports.Body = Body;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
function _get_html_insert_position (buf) {
|
|
411
|
-
|
|
412
|
-
// otherwise, if we return -1 then the buf.copy will die with
|
|
413
|
-
// RangeError: out of range index
|
|
414
|
-
if (buf.length === 0) return 0;
|
|
415
|
-
|
|
416
|
-
// TODO: consider re-writing this to go backwards from the end
|
|
417
|
-
for (let i=0,l=buf.length; i<l; i++) {
|
|
418
|
-
if (buf[i] === 60 && buf[i+1] === 47) { // found: "</"
|
|
419
|
-
if ( (buf[i+2] === 98 || buf[i+2] === 66) && // "b" or "B"
|
|
420
|
-
(buf[i+3] === 111 || buf[i+3] === 79) && // "o" or "O"
|
|
421
|
-
(buf[i+4] === 100 || buf[i+4] === 68) && // "d" or "D"
|
|
422
|
-
(buf[i+5] === 121 || buf[i+5] === 89) && // "y" or "Y"
|
|
423
|
-
buf[i+6] === 62
|
|
424
|
-
) {
|
|
425
|
-
// matched </body>
|
|
426
|
-
return i;
|
|
427
|
-
}
|
|
428
|
-
if ( (buf[i+2] === 104 || buf[i+2] === 72) && // "h" or "H"
|
|
429
|
-
(buf[i+3] === 116 || buf[i+3] === 84) && // "t" or "T"
|
|
430
|
-
(buf[i+4] === 109 || buf[i+4] === 77) && // "m" or "M"
|
|
431
|
-
(buf[i+5] === 108 || buf[i+5] === 76) && // "l" or "L"
|
|
432
|
-
buf[i+6] === 62
|
|
433
|
-
) {
|
|
434
|
-
// matched </html>
|
|
435
|
-
return i;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return buf.length - 1; // default is at the end
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function insert_banner (ct, enc, buf, banners) {
|
|
443
|
-
if (!banners || !/^text\//i.test(ct)) {
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const is_html = /text\/html/i.test(ct);
|
|
447
|
-
|
|
448
|
-
// First we convert the banner to the same encoding as the buf
|
|
449
|
-
const banner_str = banners[is_html ? 1 : 0];
|
|
450
|
-
let banner_buf = null;
|
|
451
|
-
if (Iconv) {
|
|
452
|
-
try {
|
|
453
|
-
const converter = new Iconv("UTF-8", `${enc}//IGNORE`);
|
|
454
|
-
banner_buf = converter.convert(banner_str);
|
|
455
|
-
}
|
|
456
|
-
catch (err) {
|
|
457
|
-
logger.logerror(`iconv conversion of banner to ${enc} failed: ${err}`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (!banner_buf) {
|
|
462
|
-
banner_buf = Buffer.from(banner_str);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Allocate a new buffer: (7 or 2 is <P>...</P> vs \n...\n - correct that if you change those!)
|
|
466
|
-
const new_buf = Buffer.alloc(buf.length + banner_buf.length + (is_html ? 7 : 2));
|
|
467
|
-
|
|
468
|
-
// Now we find where to insert it and combine it with the original buf:
|
|
469
|
-
if (is_html) {
|
|
470
|
-
let insert_pos = _get_html_insert_position(buf);
|
|
471
|
-
|
|
472
|
-
// copy start of buf into new_buf
|
|
473
|
-
buf.copy(new_buf, 0, 0, insert_pos);
|
|
474
|
-
|
|
475
|
-
// add in <P>
|
|
476
|
-
new_buf[insert_pos++] = 60;
|
|
477
|
-
new_buf[insert_pos++] = 80;
|
|
478
|
-
new_buf[insert_pos++] = 62;
|
|
479
|
-
|
|
480
|
-
// copy all of banner into new_buf
|
|
481
|
-
banner_buf.copy(new_buf, insert_pos);
|
|
482
|
-
|
|
483
|
-
// add in </P>
|
|
484
|
-
new_buf[banner_buf.length + insert_pos++] = 60;
|
|
485
|
-
new_buf[banner_buf.length + insert_pos++] = 47;
|
|
486
|
-
new_buf[banner_buf.length + insert_pos++] = 80;
|
|
487
|
-
new_buf[banner_buf.length + insert_pos++] = 62;
|
|
488
|
-
|
|
489
|
-
// copy remainder of buf into new_buf, if there is buf remaining
|
|
490
|
-
if (buf.length > (insert_pos - 7)) {
|
|
491
|
-
buf.copy(new_buf, insert_pos + banner_buf.length, insert_pos - 7);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
buf.copy(new_buf);
|
|
496
|
-
new_buf[buf.length] = 10; // \n
|
|
497
|
-
banner_buf.copy(new_buf, buf.length + 1);
|
|
498
|
-
new_buf[buf.length + banner_buf.length + 1] = 10; // \n
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return new_buf;
|
|
502
|
-
}
|