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.
Files changed (135) hide show
  1. package/.eslintrc.yaml +2 -10
  2. package/Changes.md +84 -2
  3. package/Dockerfile +1 -1
  4. package/Plugins.md +9 -4
  5. package/README.md +2 -6
  6. package/bin/haraka +5 -4
  7. package/config/outbound.ini +0 -7
  8. package/config/plugins +1 -1
  9. package/config/smtp.ini +1 -1
  10. package/config/smtp_forward.ini +2 -8
  11. package/config/smtp_proxy.ini +0 -6
  12. package/connection.js +178 -204
  13. package/coverage/lcov.info +13863 -0
  14. package/coverage/tmp/coverage-42958-1658373250585-0.json +1 -0
  15. package/coverage/tmp/coverage-42961-1658373250529-0.json +1 -0
  16. package/dkim.js +66 -73
  17. package/docs/Body.md +1 -22
  18. package/docs/CoreConfig.md +2 -2
  19. package/docs/Header.md +1 -47
  20. package/docs/Outbound.md +8 -36
  21. package/endpoint.js +1 -1
  22. package/haraka.js +1 -1
  23. package/host_pool.js +8 -12
  24. package/logger.js +25 -32
  25. package/outbound/client_pool.js +11 -153
  26. package/outbound/config.js +5 -11
  27. package/outbound/hmail.js +109 -143
  28. package/outbound/index.js +13 -25
  29. package/outbound/mx_lookup.js +10 -7
  30. package/outbound/queue.js +8 -12
  31. package/outbound/timer_queue.js +2 -4
  32. package/outbound/tls.js +17 -18
  33. package/outbound/todo.js +1 -0
  34. package/package.json +57 -55
  35. package/plugins/auth/auth_base.js +39 -63
  36. package/plugins/auth/auth_bridge.js +3 -4
  37. package/plugins/auth/auth_proxy.js +16 -16
  38. package/plugins/auth/auth_vpopmaild.js +30 -37
  39. package/plugins/auth/flat_file.js +9 -13
  40. package/plugins/avg.js +9 -11
  41. package/plugins/backscatterer.js +1 -1
  42. package/plugins/block_me.js +2 -6
  43. package/plugins/bounce.js +106 -124
  44. package/plugins/clamd.js +59 -63
  45. package/plugins/data.signatures.js +6 -6
  46. package/plugins/data.uribl.js +1 -415
  47. package/plugins/delay_deny.js +19 -20
  48. package/plugins/dkim_sign.js +56 -62
  49. package/plugins/dkim_verify.js +9 -8
  50. package/plugins/dns_list_base.js +43 -42
  51. package/plugins/dnsbl.js +41 -46
  52. package/plugins/dnswl.js +23 -26
  53. package/plugins/early_talker.js +24 -28
  54. package/plugins/esets.js +8 -11
  55. package/plugins/greylist.js +161 -190
  56. package/plugins/helo.checks.js +175 -197
  57. package/plugins/mail_from.is_resolvable.js +38 -38
  58. package/plugins/messagesniffer.js +33 -40
  59. package/plugins/prevent_credential_leaks.js +7 -5
  60. package/plugins/process_title.js +16 -17
  61. package/plugins/queue/deliver.js +2 -2
  62. package/plugins/queue/lmtp.js +5 -6
  63. package/plugins/queue/qmail-queue.js +11 -13
  64. package/plugins/queue/quarantine.js +25 -34
  65. package/plugins/queue/rabbitmq.js +3 -2
  66. package/plugins/queue/rabbitmq_amqplib.js +9 -9
  67. package/plugins/queue/smtp_bridge.js +5 -4
  68. package/plugins/queue/smtp_forward.js +81 -89
  69. package/plugins/queue/smtp_proxy.js +21 -22
  70. package/plugins/queue/test.js +2 -1
  71. package/plugins/rcpt_to.host_list_base.js +20 -30
  72. package/plugins/rcpt_to.in_host_list.js +12 -14
  73. package/plugins/rcpt_to.max_count.js +7 -5
  74. package/plugins/record_envelope_addresses.js +4 -6
  75. package/plugins/relay.js +64 -74
  76. package/plugins/reseed_rng.js +1 -2
  77. package/plugins/spamassassin.js +56 -68
  78. package/plugins/status.js +2 -3
  79. package/plugins/tarpit.js +8 -11
  80. package/plugins/tls.js +14 -17
  81. package/plugins/toobusy.js +6 -8
  82. package/plugins/xclient.js +14 -25
  83. package/plugins.js +24 -29
  84. package/rfc1869.js +2 -2
  85. package/server.js +3 -13
  86. package/smtp_client.js +138 -215
  87. package/tests/config/smtp_forward.ini +0 -6
  88. package/tests/fixtures/line_socket.js +1 -1
  89. package/tests/fixtures/util_hmailitem.js +5 -7
  90. package/tests/fixtures/vm_harness.js +2 -2
  91. package/tests/host_pool.js +13 -14
  92. package/tests/installation/plugins/inherits.js +1 -2
  93. package/tests/logger.js +2 -2
  94. package/tests/plugins/bounce.js +6 -8
  95. package/tests/plugins/dkim_signer.js +7 -7
  96. package/tests/plugins/dns_list_base.js +7 -7
  97. package/tests/plugins/helo.checks.js +1 -1
  98. package/tests/plugins/mail_from.is_resolvable.js +10 -54
  99. package/tests/plugins/queue/smtp_forward.js +11 -11
  100. package/tests/plugins/rcpt_to.host_list_base.js +1 -1
  101. package/tests/plugins/rcpt_to.in_host_list.js +1 -1
  102. package/tests/plugins/spamassassin.js +1 -1
  103. package/tests/queue/multibyte +0 -0
  104. package/tests/queue/plain +0 -0
  105. package/tests/rfc1869.js +4 -1
  106. package/tests/server.js +15 -9
  107. package/tests/smtp_client/auth.js +4 -14
  108. package/tests/smtp_client/basic.js +5 -15
  109. package/tests/smtp_client.js +7 -3
  110. package/tests/transaction.js +72 -19
  111. package/tls_socket.js +75 -85
  112. package/transaction.js +7 -9
  113. package/attachment_stream.js +0 -118
  114. package/bin/spf +0 -48
  115. package/chunkemitter.js +0 -75
  116. package/config/data.uribl.excludes +0 -202
  117. package/config/data.uribl.ini +0 -37
  118. package/config/spf.ini +0 -1
  119. package/docs/plugins/attachment.md +0 -92
  120. package/docs/plugins/data.uribl.md +0 -120
  121. package/docs/plugins/spf.md +0 -142
  122. package/mailbody.js +0 -502
  123. package/mailheader.js +0 -304
  124. package/messagestream.js +0 -441
  125. package/plugins/aliases.js +0 -120
  126. package/plugins/attachment.js +0 -503
  127. package/plugins/connect.p0f.js +0 -5
  128. package/plugins/spf.js +0 -327
  129. package/spf.js +0 -689
  130. package/tests/mailbody.js +0 -348
  131. package/tests/mailheader.js +0 -138
  132. package/tests/messagestream.js +0 -34
  133. package/tests/plugins/aliases.js +0 -376
  134. package/tests/plugins/spf.js +0 -251
  135. package/tests/spf.js +0 -96
@@ -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
- }