Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: connection.ini: replaces ... #3435

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

### Unreleased

#### BREAKING, ACTION REQUIRED

- connection.ini: new config file, replaces haproxy_hosts, ehlo_hello_message, connection_close_message, banner_includes_uuid, deny_includes_uuid, databytes, max_mime_parts, max_line_length, max_data_line_length, and smtpgreeting. To upgrade, apply any localized settings to the new connection.ini file.
- moved the following settings from smtp.ini to connection.ini:
- headers.*
- main.smtp_utf8
- main.strict_rfc1869
- early_talker.pause, removed support, use earlytalker.ini

#### Changes

- deps(eslint): update to v9
- docs(plugins/\*.md): use \# to indicate heading levels
- deps(various): bump to latest versions
- docs(CoreConfig): removed incorrect early_talker.delay reference (hasn't worked in years).

#### Fixes

- fix(outbound): in outbound hook_delivered, when mx.exchange contains
an IP, use mx.from_dns
- fix(bin/haraka): fix for finding path to config/docs/Plugins.md
Expand Down
63 changes: 63 additions & 0 deletions config/connection.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
;
[main]

; Require senders to conform to RFC 1869 and RFC 821 when sending the MAIL FROM and RCPT TO commands. In particular, the inclusion of spurious spaces or missing angle brackets will be rejected.
; strict_rfc1869 = false

; Advertise support for SMTPUTF8 (RFC-6531)
; smtputf8=true


[haproxy]
; Array: hosts or CIDRs that Haraka should enable the PROXY protocol from. See docs/HAProxy for format
hosts[] =
; hosts[] = 192.0.2.4
; hosts[] = 192.0.2.5
; hosts[] = [2001:db8::1]
; hosts[] = [2001:db8::2]


[headers]
; add_received=true
; clean_auth_results=true

; show_version=true

max_lines=1000

max_received=100


[max]
; Integer. The maximum SIZE of an email
bytes=26214400

; Integer. Limit a potential denial of service in potentially hostile emails.
mime_parts=1000

; Integer. The maximum length of lines in SMTP session commands (e.g. RCPT, HELO etc). Defaults to 512 (bytes) as mandated by RFC 5321 §4.5.3.1.4. Clients exceeding this limit will be immediately disconnected with a "521 Command line too long" error.
line_length=512

; Integer. The maximum length of lines in the DATA section of emails. Defaults to 992 (bytes), the limit set by Sendmail. When this limit is exceeded the three bytes "\r\n " (0x0d 0x0a 0x20) are inserted into the stream to "fix" it. This has the potential to "break" some email, but makes it more likely to be accepted by upstream/downstream services, and is the same behaviour as Sendmail. Also when the data line length limit is exceeded `transaction.notes.data_line_length_exceeded` is set to `true`.
data_line_length=992


[message]
; Array. The greeting used when a client connects.
; greeting[]=My Custom
; greeting[]=Greeting Message

helo=Haraka is at your service.

; String. Override the default connection close message.
close=closing connection. Have a jolly good day.


[uuid]
; integer, how many UUID chars to show.
; 0 = none, 6 is enough to be unique per day, 40 will include the
; full connection and transaction UUID
banner_chars=6

; include N characters of the uuid (in brackets) at the start of each line of the deny message
deny_chars=0
1 change: 0 additions & 1 deletion config/connection_close_message

This file was deleted.

1 change: 0 additions & 1 deletion config/databytes

This file was deleted.

1 change: 0 additions & 1 deletion config/max_unrecognized_commands

This file was deleted.

18 changes: 0 additions & 18 deletions config/smtp.ini
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,3 @@
; after this time it will hard close. 30s is usually long enough to
; wait for outbound connections to finish.
;force_shutdown_timeout=30

; SMTP service extensions: https://tools.ietf.org/html/rfc1869
; strict_rfc1869 = false

; Advertise support for SMTPUTF8 (RFC-6531)
;smtputf8=true

[headers]
;add_received=true
;clean_auth_results=true

;show_version=true

; replace max_header_lines
max_lines=1000

; replace max_received_count
max_received=100
113 changes: 51 additions & 62 deletions connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,33 @@

const states = constants.connection.state;

// Load HAProxy hosts into an object for fast lookups
// as this list is checked on every new connection.
let haproxy_hosts_ipv4 = [];
let haproxy_hosts_ipv6 = [];
function loadHAProxyHosts () {
const hosts = config.get('haproxy_hosts', 'list', loadHAProxyHosts);
const new_ipv4_hosts = [];
const new_ipv6_hosts = [];
for (let i=0; i<hosts.length; i++) {
const host = hosts[i].split(/\//);
if (net.isIPv6(host[0])) {
new_ipv6_hosts[i] = [ipaddr.IPv6.parse(host[0]), parseInt(host[1] || 64)];
}
else {
new_ipv4_hosts[i] = [ipaddr.IPv4.parse(host[0]), parseInt(host[1] || 32)];
}
const cfg = config.get('connection.ini', {
booleans: [
'-main.strict_rfc1869',
'+main.smtputf8',
'+headers.add_received',
'+headers.show_version',
'+headers.clean_auth_results',
]
});

const haproxy_hosts_ipv4 = [];
const haproxy_hosts_ipv6 = [];

for (const ip of cfg.haproxy.hosts) {
if (!ip) continue;
if (net.isIPv6(ip.split('/')[0])) {
haproxy_hosts_ipv6.push([ipaddr.IPv6.parse(ip.split('/')[0]), parseInt(ip.split('/')[1] || 64)]);
}
else {
haproxy_hosts_ipv4.push([ipaddr.IPv4.parse(ip.split('/')[0]), parseInt(ip.split('/')[1] || 32)]);

Check warning on line 46 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L42-L46

Added lines #L42 - L46 were not covered by tests
}
haproxy_hosts_ipv4 = new_ipv4_hosts;
haproxy_hosts_ipv6 = new_ipv6_hosts;
}
loadHAProxyHosts();

class Connection {
constructor (client, server, cfg) {
constructor (client, server, smtp_cfg) {
this.client = client;
this.server = server;
this.cfg = cfg;

this.local = {
ip: null,
Expand Down Expand Up @@ -97,10 +97,6 @@
this.transaction = null;
this.tran_count = 0;
this.capabilities = null;
this.ehlo_hello_message = config.get('ehlo_hello_message') || 'Haraka is at your service.';
this.connection_close_message = config.get('connection_close_message') || 'closing connection. Have a jolly good day.';
this.banner_includes_uuid = !!config.get('banner_includes_uuid');
this.deny_includes_uuid = config.get('deny_includes_uuid') || null;
this.early_talker = false;
this.pipelining = false;
this._relaying = false;
Expand All @@ -109,8 +105,6 @@
this.hooks_to_run = [];
this.start_time = Date.now();
this.last_reject = '';
this.max_bytes = parseInt(config.get('databytes')) || 0;
this.max_mime_parts = parseInt(config.get('max_mime_parts')) || 1000;
this.totalbytes = 0;
this.rcpt_count = {
accept: 0,
Expand All @@ -122,13 +116,11 @@
tempfail: 0,
reject: 0,
};
this.max_line_length = parseInt(config.get('max_line_length')) || 512;
this.max_data_line_length = parseInt(config.get('max_data_line_length')) || 992;
this.results = new ResultStore(this);
this.errors = 0;
this.last_rcpt_msg = null;
this.hook = null;
if (this.cfg.headers.show_version) {
if (cfg.headers.show_version) {
this.local.info += `/${utils.getVersion(__dirname)}`;
}
Connection.setupClient(this);
Expand Down Expand Up @@ -200,7 +192,6 @@
});

const ha_list = net.isIPv6(self.remote.ip) ? haproxy_hosts_ipv6 : haproxy_hosts_ipv4;

if (ha_list.some((element, index, array) => {
return ipaddr.parse(self.remote.ip).match(element[0], element[1]);
})) {
Expand Down Expand Up @@ -401,10 +392,10 @@

let maxlength;
if (this.state === states.PAUSE_DATA || this.state === states.DATA) {
maxlength = this.max_data_line_length;
maxlength = cfg.max.data_line_length;
}
else {
maxlength = this.max_line_length;
maxlength = cfg.max.line_length;
}

let offset;
Expand Down Expand Up @@ -535,10 +526,10 @@

if (code >= 400) {
this.last_reject = `${code} ${messages.join(' ')}`;
if (this.deny_includes_uuid) {
if (cfg.uuid.deny_chars) {
uuid = (this.transaction || this).uuid;
if (this.deny_includes_uuid > 1) {
uuid = uuid.substr(0, this.deny_includes_uuid);
if (cfg.uuid.deny_chars > 1) {
uuid = uuid.substr(0, cfg.uuid.deny_chars);

Check warning on line 532 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L531-L532

Added lines #L531 - L532 were not covered by tests
}
}
}
Expand Down Expand Up @@ -649,7 +640,7 @@
}
init_transaction (cb) {
this.reset_transaction(() => {
this.transaction = trans.createTransaction(this.tran_uuid(), this.cfg);
this.transaction = trans.createTransaction(this.tran_uuid(), cfg);
// Catch any errors from the message_stream
this.transaction.message_stream.on('error', (err) => {
this.logcrit(`message_stream error: ${err.message}`);
Expand Down Expand Up @@ -792,19 +783,19 @@
});
break;
default: {
let greeting = config.get('smtpgreeting', 'list');
if (greeting.length) {
let greeting = cfg.message.greeting;
if (greeting?.length) {
// RFC5321 section 4.2
// Hostname/domain should appear after the 220
greeting[0] = `${this.local.host} ESMTP ${greeting[0]}`;
if (this.banner_includes_uuid) {
greeting[0] += ` (${this.uuid})`;
if (cfg.uuid.banner_chars) {
greeting[0] += ` (${this.uuid.substr(0, cfg.uuid.banner_chars)})`;

Check warning on line 792 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L791-L792

Added lines #L791 - L792 were not covered by tests
}
}
else {
greeting = `${this.local.host} ESMTP ${this.local.info} ready`;
if (this.banner_includes_uuid) {
greeting += ` (${this.uuid})`;
if (cfg.uuid.banner_chars) {
greeting += ` (${this.uuid.substr(0, cfg.uuid.banner_chars)})`;
}
}
this.respond(220, msg || greeting);
Expand Down Expand Up @@ -850,7 +841,7 @@
default:
// RFC5321 section 4.1.1.1
// Hostname/domain should appear after 250
this.respond(250, `${this.local.host} Hello ${this.get_remote('host')}, ${this.ehlo_hello_message}`);
this.respond(250, `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`);

Check warning on line 844 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L844

Added line #L844 was not covered by tests
}
}
ehlo_respond (retval, msg) {
Expand Down Expand Up @@ -883,16 +874,14 @@
// Hostname/domain should appear after 250

const response = [
`${this.local.host} Hello ${this.get_remote('host')}, ${this.ehlo_hello_message}`,
`${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`,
"PIPELINING",
"8BITMIME",
];

if (this.cfg.main.smtputf8) {
response.push("SMTPUTF8");
}
if (cfg.main.smtputf8) response.push("SMTPUTF8");

response.push(`SIZE ${this.max_bytes}`);
response.push(`SIZE ${cfg.max.bytes}`);

this.capabilities = response;

Expand All @@ -905,7 +894,7 @@
this.respond(250, this.capabilities);
}
quit_respond (retval, msg) {
this.respond(221, msg || `${this.local.host} ${this.connection_close_message}`, () => {
this.respond(221, msg || `${this.local.host} ${cfg.message.close}`, () => {

Check warning on line 897 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L897

Added line #L897 was not covered by tests
this.disconnect();
});
}
Expand Down Expand Up @@ -1314,7 +1303,7 @@

let results;
try {
results = rfc1869.parse('mail', line, this.cfg.main.strict_rfc1869 && !this.relaying);
results = rfc1869.parse('mail', line, (!this.relaying && cfg.main.strict_rfc1869));
}
catch (err) {
this.errors++;
Expand Down Expand Up @@ -1356,7 +1345,7 @@

// Handle SIZE extension
if (params?.SIZE && params.SIZE > 0) {
if (this.max_bytes > 0 && params.SIZE > this.max_bytes) {
if (cfg.max.bytes > 0 && params.SIZE > cfg.max.bytes) {

Check warning on line 1348 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L1348

Added line #L1348 was not covered by tests
return this.respond(550, 'Message too big!');
}
}
Expand All @@ -1378,7 +1367,7 @@

let results;
try {
results = rfc1869.parse('rcpt', line, this.cfg.main.strict_rfc1869 && !this.relaying);
results = rfc1869.parse('rcpt', line, cfg.main.strict_rfc1869 && !this.relaying);
}
catch (err) {
this.errors++;
Expand Down Expand Up @@ -1512,7 +1501,7 @@
return this.respond(503, "RCPT required first");
}

if (this.cfg.headers.add_received) {
if (cfg.headers.add_received) {
this.accumulate_data(`Received: ${this.received_line()}\r\n`);
}
plugins.run_hooks('data', this);
Expand Down Expand Up @@ -1577,11 +1566,11 @@
}

// Stop accumulating data as we're going to reject at dot.
if (this.max_bytes && this.transaction.data_bytes > this.max_bytes) {
if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
return;
}

if (this.transaction.mime_part_count >= this.max_mime_parts) {
if (this.transaction.mime_part_count >= cfg.max.mime_parts) {
this.logcrit("Possible DoS attempt - too many MIME parts");
this.respond(554, "Transaction failed due to too many MIME parts", () => {
this.disconnect();
Expand All @@ -1596,13 +1585,13 @@
this.totalbytes += this.transaction.data_bytes;

// Check message size limit
if (this.max_bytes && this.transaction.data_bytes > this.max_bytes) {
this.lognotice(`Incoming message exceeded databytes size of ${this.max_bytes}`);
if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
this.lognotice(`Incoming message exceeded max size of ${cfg.max.bytes}`);

Check warning on line 1589 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L1589

Added line #L1589 was not covered by tests
return plugins.run_hooks('max_data_exceeded', this);
}

// Check max received headers count
if (this.transaction.header.get_all('received').length > this.cfg.headers.max_received) {
if (this.transaction.header.get_all('received').length > cfg.headers.max_received) {
this.logerror("Incoming message had too many Received headers");
this.respond(550, "Too many received headers - possible mail loop", () => {
this.reset_transaction();
Expand All @@ -1611,11 +1600,11 @@
}

// Warn if we hit the maximum parsed header lines limit
if (this.transaction.header_lines.length >= this.cfg.headers.max_lines) {
this.logwarn(`Incoming message reached maximum parsing limit of ${this.cfg.headers.max_lines} header lines`);
if (this.transaction.header_lines.length >= cfg.headers.max_lines) {
this.logwarn(`Incoming message reached maximum parsing limit of ${cfg.headers.max_lines} header lines`);

Check warning on line 1604 in connection.js

View check run for this annotation

Codecov / codecov/patch

connection.js#L1604

Added line #L1604 was not covered by tests
}

if (this.cfg.headers.clean_auth_results) {
if (cfg.headers.clean_auth_results) {
this.auth_results_clean(); // rename old A-R headers
}
const ar_field = this.auth_results(); // assemble new one
Expand Down
Loading
Loading