Skip to content

Commit

Permalink
connection.ini: replaces ...
Browse files Browse the repository at this point in the history
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 from the old config files to the new connection.ini file and for tidiness, delete the old config files.

- 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
  • Loading branch information
msimerson committed Jan 20, 2025
1 parent 64a15d3 commit 0c1cf58
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 170 deletions.
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
64 changes: 64 additions & 0 deletions config/connection.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
;
[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]
; Arrays: hosts that Haraka should enable the PROXY protocol from.
hosts_ipv4[]
; hosts_ipv4[] = 192.0.2.4/32
; hosts_ipv4[] = 192.0.2.5/32
hosts_ipv6[]
; hosts_ipv6[] = [2001:db8::1/64]
; hosts_ipv6[] = [2001:db8::2/64]


[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=6
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
105 changes: 41 additions & 64 deletions connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,20 @@ const outbound = require('./outbound');

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)];
}
}
haproxy_hosts_ipv4 = new_ipv4_hosts;
haproxy_hosts_ipv6 = new_ipv6_hosts;
}
loadHAProxyHosts();
const cfg = config.get('connection.ini', {
booleans: [
'-main.strict_rfc1869',
'+main.smtputf8',
'+headers.add_received',
'+headers.show_version',
'+headers.clean_auth_results',
]
});

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 +84,6 @@ class Connection {
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 +92,6 @@ class Connection {
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 +103,11 @@ class Connection {
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 @@ -199,7 +178,7 @@ class Connection {
self.process_data(data);
});

const ha_list = net.isIPv6(self.remote.ip) ? haproxy_hosts_ipv6 : haproxy_hosts_ipv4;
const ha_list = net.isIPv6(self.remote.ip) ? cfg.haproxy.hosts_ipv6 : cfg.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 +380,10 @@ class Connection {

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 +514,10 @@ class Connection {

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);
}
}
}
Expand Down Expand Up @@ -649,7 +628,7 @@ class Connection {
}
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 +771,19 @@ class Connection {
});
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)})`;
}
}
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 +829,7 @@ class Connection {
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}`);
}
}
ehlo_respond (retval, msg) {
Expand Down Expand Up @@ -883,16 +862,14 @@ class Connection {
// 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 +882,7 @@ class Connection {
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}`, () => {
this.disconnect();
});
}
Expand Down Expand Up @@ -1314,7 +1291,7 @@ class Connection {

let results;
try {
results = rfc1869.parse('mail', line, this.cfg.main.strict_rfc1869 && !this.relaying);
results = rfc1869.parse('mail', line, (!relaying && cfg.main.strict_rfc1869));

Check warning on line 1294 in connection.js

View workflow job for this annotation

GitHub Actions / lint / lint

'relaying' is not defined

Check warning on line 1294 in connection.js

View workflow job for this annotation

GitHub Actions / lint / lint

'relaying' is not defined
}
catch (err) {
this.errors++;
Expand Down Expand Up @@ -1356,7 +1333,7 @@ class Connection {

// 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) {
return this.respond(550, 'Message too big!');
}
}
Expand All @@ -1378,7 +1355,7 @@ class Connection {

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 +1489,7 @@ class Connection {
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 +1554,11 @@ class Connection {
}

// 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 +1573,13 @@ class Connection {
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}`);
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 +1588,11 @@ class Connection {
}

// 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`);
}

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

0 comments on commit 0c1cf58

Please sign in to comment.