Haraka 3.0.5 → 3.1.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/CONTRIBUTORS.md +6 -5
- package/Changes.md +172 -27
- package/LICENSE +1 -1
- package/Plugins.md +3 -3
- package/bin/haraka +2 -1
- package/config/connection.ini +63 -0
- package/config/smtp.ini +0 -18
- package/connection.js +53 -63
- package/docs/CoreConfig.md +17 -78
- package/docs/HAProxy.md +10 -28
- package/docs/Outbound.md +8 -9
- package/docs/plugins/aliases.md +4 -6
- package/docs/plugins/block_me.md +2 -4
- package/docs/plugins/data.signatures.md +2 -4
- package/docs/plugins/max_unrecognized_commands.md +2 -4
- package/docs/plugins/prevent_credential_leaks.md +2 -5
- package/docs/plugins/process_title.md +1 -2
- package/docs/plugins/queue/test.md +9 -0
- package/docs/plugins/rcpt_to.in_host_list.md +1 -2
- package/docs/plugins/rcpt_to.max_count.md +2 -10
- package/docs/plugins/record_envelope_addresses.md +3 -6
- package/docs/plugins/relay.md +1 -155
- package/docs/plugins/reseed_rng.md +1 -2
- package/docs/plugins/tarpit.md +7 -10
- package/docs/plugins/toobusy.md +2 -4
- package/docs/plugins/xclient.md +1 -2
- package/eslint.config.mjs +27 -0
- package/logger.js +1 -8
- package/outbound/hmail.js +8 -1
- package/package.json +56 -53
- package/plugins/auth/auth_base.js +0 -1
- package/plugins/queue/test.js +5 -3
- package/server.js +0 -14
- package/test/connection.js +18 -7
- package/.eslintrc.yaml +0 -11
- package/config/connection_close_message +0 -1
- package/config/databytes +0 -1
- package/config/dhparams.pem +0 -8
- package/config/early_talker.ini +0 -11
- package/config/mail_from.is_resolvable.ini +0 -6
- package/config/max_unrecognized_commands +0 -1
- package/config/smtp.json +0 -17
- package/docs/plugins/early_talker.md +0 -22
- package/docs/plugins/mail_from.is_resolvable.md +0 -29
- package/plugins/early_talker.js +0 -155
- package/plugins/mail_from.is_resolvable.js +0 -132
- package/plugins/relay.js +0 -207
- package/test/plugins/early_talker.js +0 -104
- package/test/plugins/mail_from.is_resolvable.js +0 -35
- package/test/plugins/relay.js +0 -303
- /package/docs/{plugins → deprecated}/access.md +0 -0
- /package/docs/{plugins → deprecated}/backscatterer.md +0 -0
- /package/docs/{plugins → deprecated}/data.headers.md +0 -0
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
early\_talker
|
|
2
|
-
============
|
|
3
|
-
|
|
4
|
-
Early talkers are violators of the SMTP specification, which require that
|
|
5
|
-
clients must wait for certain responses before sending the next command.
|
|
6
|
-
|
|
7
|
-
This plugin introduces a configurable delay before the connection banner
|
|
8
|
-
and after the DATA command for Haraka to detect if it talks early.
|
|
9
|
-
|
|
10
|
-
If an early talker is detected at connection or DATA, then a DENY is
|
|
11
|
-
returned with the message 'You talk too soon'.
|
|
12
|
-
|
|
13
|
-
Configuration
|
|
14
|
-
-------------
|
|
15
|
-
|
|
16
|
-
The config file early\_talker.ini has two options:
|
|
17
|
-
|
|
18
|
-
- pause: the delay in seconds before each SMTP command. Default is no pause.
|
|
19
|
-
|
|
20
|
-
- reject: whether or not to reject for early talkers. Default is true;
|
|
21
|
-
|
|
22
|
-
- [ip_whitelist]: list of IP addresses and/or subnets to whitelist
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
mail\_from.is\_resolvable
|
|
2
|
-
=======================
|
|
3
|
-
|
|
4
|
-
This plugin checks that the domain used in MAIL FROM is resolvable to an MX
|
|
5
|
-
record.
|
|
6
|
-
|
|
7
|
-
Configuration
|
|
8
|
-
-------------
|
|
9
|
-
|
|
10
|
-
This plugin uses the INI-style file format and accepts the following options:
|
|
11
|
-
|
|
12
|
-
* timeout
|
|
13
|
-
|
|
14
|
-
Default: 30
|
|
15
|
-
|
|
16
|
-
Maximum limit in seconds for queries to complete. If the timeout is
|
|
17
|
-
reached a TEMPFAIL is returned to the client.
|
|
18
|
-
|
|
19
|
-
* allow\_mx\_ip=[0|1]
|
|
20
|
-
|
|
21
|
-
Allow MX records that return IP addresses instead of hostnames.
|
|
22
|
-
This is not allowed as per the RFC, but some MTAs allow it.
|
|
23
|
-
|
|
24
|
-
* reject\_no\_mx=[0|1]
|
|
25
|
-
|
|
26
|
-
Return DENY and reject the command if no MX record is found. Otherwise a
|
|
27
|
-
DENYSOFT (TEMPFAIL) is returned and the client will retry later.
|
|
28
|
-
|
|
29
|
-
DNS errors always return DENYSOFT, so this should be safe to enable.
|
package/plugins/early_talker.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
// This plugin checks for clients that talk before we sent a response
|
|
2
|
-
|
|
3
|
-
const { isIPv6 } = require('node:net');
|
|
4
|
-
|
|
5
|
-
const ipaddr = require('ipaddr.js');
|
|
6
|
-
|
|
7
|
-
exports.register = function () {
|
|
8
|
-
this.load_config();
|
|
9
|
-
this.register_hook('connect_init', 'early_talker');
|
|
10
|
-
this.register_hook('data', 'early_talker');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
exports.load_config = function () {
|
|
14
|
-
|
|
15
|
-
this.cfg = this.config.get('early_talker.ini', {
|
|
16
|
-
booleans: [
|
|
17
|
-
'+main.reject'
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
() => {
|
|
21
|
-
this.load_config();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Generate a white list of IP addresses
|
|
25
|
-
this.whitelist = this.load_ip_list(Object.keys(this.cfg.ip_whitelist));
|
|
26
|
-
|
|
27
|
-
if (this.cfg.main?.pause) {
|
|
28
|
-
this.pause = this.cfg.main.pause * 1000;
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// config/early_talker.pause is in milliseconds
|
|
33
|
-
this.pause = this.config.get('early_talker.pause', () => {
|
|
34
|
-
this.load_config();
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
exports.early_talker = function (next, connection) {
|
|
39
|
-
const plugin = this;
|
|
40
|
-
if (!plugin.pause) return next();
|
|
41
|
-
if (!plugin.should_check(connection)) return next();
|
|
42
|
-
|
|
43
|
-
function check () {
|
|
44
|
-
if (!connection) return next();
|
|
45
|
-
if (!connection.early_talker) {
|
|
46
|
-
connection.results.add(plugin, {pass: 'early'});
|
|
47
|
-
return next();
|
|
48
|
-
}
|
|
49
|
-
connection.results.add(plugin, {fail: 'early'});
|
|
50
|
-
if (!plugin.cfg.main.reject) return next();
|
|
51
|
-
return next(DENYDISCONNECT, "You talk too soon");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let { pause } = plugin;
|
|
55
|
-
if (plugin.hook === 'connect_init') {
|
|
56
|
-
const elapsed = (Date.now() - connection.start_time);
|
|
57
|
-
if (elapsed > plugin.pause) {
|
|
58
|
-
// Something else already waited
|
|
59
|
-
return check();
|
|
60
|
-
}
|
|
61
|
-
pause = plugin.pause - elapsed;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
setTimeout(() => { check(); }, pause);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if an ip is whitelisted
|
|
70
|
-
*
|
|
71
|
-
* @param {String} ip The remote IP to verify
|
|
72
|
-
* @return {Boolean} True if is whitelisted
|
|
73
|
-
*/
|
|
74
|
-
exports.ip_in_list = function (ip) {
|
|
75
|
-
|
|
76
|
-
if (!this.whitelist) return false;
|
|
77
|
-
|
|
78
|
-
const ipobj = ipaddr.parse(ip);
|
|
79
|
-
|
|
80
|
-
for (const element of this.whitelist) {
|
|
81
|
-
try {
|
|
82
|
-
if (ipobj.match(element)) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (ignore) {
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Convert config ip to ipaddr objects
|
|
95
|
-
*
|
|
96
|
-
* @param {Array} list A list of IP addresses / subnets
|
|
97
|
-
* @return {Array} The converted array
|
|
98
|
-
*/
|
|
99
|
-
exports.load_ip_list = list => {
|
|
100
|
-
const whitelist = [];
|
|
101
|
-
|
|
102
|
-
for (const element of list) {
|
|
103
|
-
try {
|
|
104
|
-
let addr = element;
|
|
105
|
-
if (addr.match(/\/\d+$/)) {
|
|
106
|
-
addr = ipaddr.parseCIDR(addr);
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
addr = ipaddr.parseCIDR(addr + ((isIPv6(addr)) ? '/128' : '/32'));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
whitelist.push(addr);
|
|
113
|
-
}
|
|
114
|
-
catch (ignore) {
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return whitelist;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
exports.should_check = function (connection) {
|
|
121
|
-
// Skip delays for privileged senders
|
|
122
|
-
|
|
123
|
-
if (connection.notes.auth_user) {
|
|
124
|
-
connection.results.add(this, { skip: 'authed'});
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (connection.relaying) {
|
|
129
|
-
connection.results.add(this, { skip: 'relay'});
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (this.ip_in_list(connection.remote.ip)) {
|
|
134
|
-
connection.results.add(this, { skip: 'whitelist' });
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const karma = connection.results.get('karma');
|
|
139
|
-
if (karma && karma.good > 0) {
|
|
140
|
-
connection.results.add(this, { skip: '+karma' });
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (connection.remote.is_local) {
|
|
145
|
-
connection.results.add(this, { skip: 'local_ip'});
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (connection.remote.is_private) {
|
|
150
|
-
connection.results.add(this, { skip: 'private_ip'});
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Check MAIL FROM domain is resolvable to an MX
|
|
4
|
-
const net = require('node:net');
|
|
5
|
-
|
|
6
|
-
const net_utils = require('haraka-net-utils');
|
|
7
|
-
|
|
8
|
-
exports.register = function () {
|
|
9
|
-
this.load_ini();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
exports.load_ini = function () {
|
|
13
|
-
this.cfg = this.config.get('mail_from.is_resolvable.ini', {
|
|
14
|
-
booleans: [
|
|
15
|
-
'-main.allow_mx_ip',
|
|
16
|
-
'+reject.no_mx',
|
|
17
|
-
],
|
|
18
|
-
}, () => {
|
|
19
|
-
this.load_ini();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// compat. Sunset 4.0
|
|
23
|
-
if (this.cfg.main.reject_no_mx) {
|
|
24
|
-
this.cfg.reject.no_mx = this.cfg.main.reject_no_mx
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (isNaN(this.cfg.main.timeout)) {
|
|
28
|
-
this.cfg.main.timeout = 29;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (this.timeout) {
|
|
32
|
-
if (this.timeout <= this.cfg.main.timeout) {
|
|
33
|
-
this.cfg.main.timeout = this.timeout - 1;
|
|
34
|
-
this.logwarn(`reducing plugin timeout to ${this.cfg.main.timeout}s`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
this.re_bogus_ip = new RegExp(this.cfg.main.re_bogus_ip ||
|
|
39
|
-
'^(?:0\\.0\\.0\\.0|255\\.255\\.255\\.255|127\\.)' );
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
exports.hook_mail = function (next, connection, params) {
|
|
43
|
-
const plugin = this;
|
|
44
|
-
const mail_from = params[0];
|
|
45
|
-
const txn = connection?.transaction;
|
|
46
|
-
if (!txn) return next();
|
|
47
|
-
const { results } = txn;
|
|
48
|
-
|
|
49
|
-
// ignore MAIL FROM without an @
|
|
50
|
-
if (!mail_from.host) {
|
|
51
|
-
results.add(plugin, {skip: 'null host'});
|
|
52
|
-
return next();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let called_next = 0;
|
|
56
|
-
const domain = mail_from.host;
|
|
57
|
-
const timeout_id = setTimeout(() => {
|
|
58
|
-
connection.logdebug(plugin, `DNS timeout resolving MX for ${domain}`);
|
|
59
|
-
called_next++;
|
|
60
|
-
if (txn) results.add(plugin, {err: `timeout(${domain})`});
|
|
61
|
-
next(DENYSOFT, 'Temporary resolver error (timeout)');
|
|
62
|
-
}, this.cfg.main.timeout * 1000);
|
|
63
|
-
|
|
64
|
-
function mxDone (code, reply) {
|
|
65
|
-
if (called_next) return;
|
|
66
|
-
clearTimeout(timeout_id);
|
|
67
|
-
called_next++;
|
|
68
|
-
next(...arguments);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function mxErr (err) {
|
|
72
|
-
if (!connection.transaction) return;
|
|
73
|
-
results.add(plugin, {err: `${domain}:${err.message}`});
|
|
74
|
-
mxDone(DENYSOFT, `Temp. resolver error (${err.code})`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
connection.logdebug(plugin, `resolving MX for domain ${domain}`)
|
|
78
|
-
|
|
79
|
-
net_utils
|
|
80
|
-
.get_mx(domain)
|
|
81
|
-
.then((exchanges) => {
|
|
82
|
-
if (!txn) return;
|
|
83
|
-
|
|
84
|
-
connection.logdebug(plugin, `${domain}: MX => ${JSON.stringify(exchanges)}`)
|
|
85
|
-
|
|
86
|
-
if (!exchanges || !exchanges.length) {
|
|
87
|
-
results.add(this, {fail: 'has_fwd_dns'});
|
|
88
|
-
return mxDone(
|
|
89
|
-
((this.cfg.reject.no_mx) ? DENY : DENYSOFT),
|
|
90
|
-
'No MX for your FROM address'
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this.cfg.main.allow_mx_ip) {
|
|
95
|
-
for (const mx of exchanges) {
|
|
96
|
-
if (net.isIPv4(mx.exchange) && !this.re_bogus_ip.test(mx.exchange)) {
|
|
97
|
-
txn.results.add(this, {pass: 'implicit_mx', emit: true});
|
|
98
|
-
return mxDone()
|
|
99
|
-
}
|
|
100
|
-
if (net.isIPv6(mx.exchange) && !net_utils.ipv6_bogus(mx.exchange)) {
|
|
101
|
-
txn.results.add(this, {pass: 'implicit_mx', emit: true});
|
|
102
|
-
return mxDone()
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// filter out the implicit MX and resolve the MX hostnames
|
|
108
|
-
net_utils
|
|
109
|
-
.resolve_mx_hosts(exchanges.filter(a => !net.isIP(a.exchange)))
|
|
110
|
-
.then(resolved => {
|
|
111
|
-
connection.logdebug(plugin, `resolved MX => ${JSON.stringify(resolved)}`);
|
|
112
|
-
|
|
113
|
-
for (const mx of resolved) {
|
|
114
|
-
if (net.isIPv4(mx.exchange) && !this.re_bogus_ip.test(mx.exchange)) {
|
|
115
|
-
txn.results.add(this, {pass: 'has_fwd_dns', emit: true});
|
|
116
|
-
return mxDone()
|
|
117
|
-
}
|
|
118
|
-
if (net.isIPv6(mx.exchange) && !net_utils.ipv6_bogus(mx.exchange)) {
|
|
119
|
-
txn.results.add(this, {pass: 'has_fwd_dns', emit: true});
|
|
120
|
-
return mxDone()
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
mxDone(
|
|
125
|
-
((this.cfg.main.reject_no_mx) ? DENY : DENYSOFT),
|
|
126
|
-
'No valid MX for your FROM address'
|
|
127
|
-
);
|
|
128
|
-
})
|
|
129
|
-
.catch(mxErr)
|
|
130
|
-
})
|
|
131
|
-
.catch(mxErr)
|
|
132
|
-
}
|
package/plugins/relay.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// relay
|
|
2
|
-
//
|
|
3
|
-
// documentation via: haraka -h relay
|
|
4
|
-
|
|
5
|
-
const net = require('node:net');
|
|
6
|
-
|
|
7
|
-
const ipaddr = require('ipaddr.js');
|
|
8
|
-
|
|
9
|
-
exports.register = function () {
|
|
10
|
-
|
|
11
|
-
this.load_relay_ini(); // plugin.cfg = { }
|
|
12
|
-
|
|
13
|
-
if (this.cfg.relay.acl) {
|
|
14
|
-
this.load_acls(); // plugin.acl_allow = [..]
|
|
15
|
-
this.register_hook('connect_init', 'acl');
|
|
16
|
-
this.register_hook('connect', 'pass_relaying');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (this.cfg.relay.force_routing || this.cfg.relay.dest_domains) {
|
|
20
|
-
this.load_dest_domains(); // plugin.dest.domains = { }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (this.cfg.relay.force_routing) {
|
|
24
|
-
this.register_hook('get_mx', 'force_routing');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (this.cfg.relay.dest_domains) {
|
|
28
|
-
this.register_hook('rcpt', 'dest_domains');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (this.cfg.relay.all) {
|
|
32
|
-
this.register_hook('rcpt', 'all');
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
exports.load_relay_ini = function () {
|
|
37
|
-
this.cfg = this.config.get('relay.ini', {
|
|
38
|
-
booleans: [
|
|
39
|
-
'+relay.acl',
|
|
40
|
-
'+relay.force_routing',
|
|
41
|
-
'-relay.all',
|
|
42
|
-
'-relay.dest_domains',
|
|
43
|
-
],
|
|
44
|
-
}, () => {
|
|
45
|
-
this.load_relay_ini();
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
exports.load_dest_domains = function () {
|
|
50
|
-
this.dest = this.config.get(
|
|
51
|
-
'relay_dest_domains.ini',
|
|
52
|
-
'ini',
|
|
53
|
-
() => { this.load_dest_domains(); }
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
exports.load_acls = function () {
|
|
58
|
-
const file_name = 'relay_acl_allow';
|
|
59
|
-
|
|
60
|
-
// load with a self-referential callback
|
|
61
|
-
this.acl_allow = this.config.get(file_name, 'list', () => {
|
|
62
|
-
this.load_acls();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
for (let i=0; i<this.acl_allow.length; i++) {
|
|
66
|
-
const cidr = this.acl_allow[i].split('/');
|
|
67
|
-
if (!net.isIP(cidr[0])) {
|
|
68
|
-
this.logerror(this, `invalid entry in ${file_name}: ${cidr[0]}`);
|
|
69
|
-
}
|
|
70
|
-
if (!cidr[1]) {
|
|
71
|
-
this.logerror(this, `appending missing CIDR suffix in: ${file_name}`);
|
|
72
|
-
this.acl_allow[i] = `${cidr[0] }/32`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
exports.acl = function (next, connection) {
|
|
78
|
-
if (!this.cfg.relay.acl) { return next(); }
|
|
79
|
-
|
|
80
|
-
connection.logdebug(this, `checking ${connection.remote.ip} in relay_acl_allow`);
|
|
81
|
-
|
|
82
|
-
if (!this.is_acl_allowed(connection)) {
|
|
83
|
-
connection.results.add(this, {skip: 'acl(unlisted)'});
|
|
84
|
-
return next();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
connection.results.add(this, {pass: 'acl'});
|
|
88
|
-
connection.relaying = true;
|
|
89
|
-
return next(OK);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
exports.pass_relaying = (next, connection) => {
|
|
93
|
-
if (connection.relaying) return next(OK);
|
|
94
|
-
|
|
95
|
-
next();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
exports.is_acl_allowed = function (connection) {
|
|
99
|
-
if (!this.acl_allow) { return false; }
|
|
100
|
-
if (!this.acl_allow.length) { return false; }
|
|
101
|
-
|
|
102
|
-
const { ip } = connection.remote;
|
|
103
|
-
|
|
104
|
-
for (const item of this.acl_allow) {
|
|
105
|
-
connection.logdebug(this, `checking if ${ip} is in ${item}`);
|
|
106
|
-
const cidr = item.split('/');
|
|
107
|
-
const c_net = cidr[0];
|
|
108
|
-
const c_mask = cidr[1] || 32;
|
|
109
|
-
|
|
110
|
-
if (!net.isIP(c_net)) continue; // bad config entry
|
|
111
|
-
if (net.isIPv4(ip) && net.isIPv6(c_net)) continue;
|
|
112
|
-
if (net.isIPv6(ip) && net.isIPv4(c_net)) continue;
|
|
113
|
-
|
|
114
|
-
if (ipaddr.parse(ip).match(ipaddr.parse(c_net), c_mask)) {
|
|
115
|
-
connection.logdebug(this, `checking if ${ip} is in ${item}: yes`);
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
exports.dest_domains = function (next, connection, params) {
|
|
123
|
-
if (!this.cfg.relay.dest_domains) { return next(); }
|
|
124
|
-
const { relaying, transaction } = connection ?? {}
|
|
125
|
-
if (!transaction) return next();
|
|
126
|
-
|
|
127
|
-
// Skip this if the host is already allowed to relay
|
|
128
|
-
if (relaying) {
|
|
129
|
-
transaction.results.add(this, {skip: 'relay_dest_domain(relay)'});
|
|
130
|
-
return next();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!this.dest) {
|
|
134
|
-
transaction.results.add(this, {err: 'relay_dest_domain(no config!)'});
|
|
135
|
-
return next();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!this.dest.domains) {
|
|
139
|
-
transaction.results.add(this, {skip: 'relay_dest_domain(config)'});
|
|
140
|
-
return next();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const dest_domain = params[0].host;
|
|
144
|
-
connection.logdebug(this, `dest_domain = ${dest_domain}`);
|
|
145
|
-
|
|
146
|
-
const dst_cfg = this.dest.domains[dest_domain];
|
|
147
|
-
if (!dst_cfg) {
|
|
148
|
-
transaction.results.add(this, {fail: 'relay_dest_domain'});
|
|
149
|
-
return next(DENY, "You are not allowed to relay");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const { action } = JSON.parse(dst_cfg);
|
|
153
|
-
connection.logdebug(this, `found config for ${dest_domain}: ${action}`);
|
|
154
|
-
|
|
155
|
-
switch (action) {
|
|
156
|
-
case "accept":
|
|
157
|
-
// why enable relaying here? Returning next(OK) will allow the
|
|
158
|
-
// address to be considered 'local'. What advantage does relaying
|
|
159
|
-
// bring?
|
|
160
|
-
connection.relaying = true;
|
|
161
|
-
transaction.results.add(this, {pass: 'relay_dest_domain'});
|
|
162
|
-
return next(OK);
|
|
163
|
-
case "continue":
|
|
164
|
-
// why oh why? Only reason I can think of is to enable outbound.
|
|
165
|
-
connection.relaying = true;
|
|
166
|
-
transaction.results.add(this, {pass: 'relay_dest_domain'});
|
|
167
|
-
return next(CONT); // same as next()
|
|
168
|
-
case "deny":
|
|
169
|
-
transaction.results.add(this, {fail: 'relay_dest_domain'});
|
|
170
|
-
return next(DENY, "You are not allowed to relay");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
transaction.results.add(this, {fail: 'relay_dest_domain'});
|
|
174
|
-
next(DENY, "Mail for that recipient is not accepted here.");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
exports.force_routing = function (next, hmail, domain) {
|
|
178
|
-
if (!this.cfg.relay.force_routing) { return next(); }
|
|
179
|
-
if (!this.dest) { return next(); }
|
|
180
|
-
if (!this.dest.domains) { return next(); }
|
|
181
|
-
let route = this.dest.domains[domain];
|
|
182
|
-
|
|
183
|
-
if (!route) {
|
|
184
|
-
route = this.dest.domains.any;
|
|
185
|
-
if (!route) {
|
|
186
|
-
this.logdebug(this, `using normal MX lookup for: ${domain}`);
|
|
187
|
-
return next();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const { nexthop } = JSON.parse(route);
|
|
192
|
-
if (!nexthop) {
|
|
193
|
-
this.logdebug(this, `using normal MX lookup for: ${domain}`);
|
|
194
|
-
return next();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
this.logdebug(this, `using ${nexthop} for: ${domain}`);
|
|
198
|
-
next(OK, nexthop);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
exports.all = function (next, connection, params) {
|
|
202
|
-
if (!this.cfg.relay.all) { return next(); }
|
|
203
|
-
|
|
204
|
-
connection.loginfo(this, `confirming recipient ${params[0]}`);
|
|
205
|
-
connection.relaying = true;
|
|
206
|
-
next(OK);
|
|
207
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert')
|
|
3
|
-
|
|
4
|
-
const fixtures = require('haraka-test-fixtures');
|
|
5
|
-
|
|
6
|
-
const _set_up = (done) => {
|
|
7
|
-
this.plugin = new fixtures.plugin('early_talker');
|
|
8
|
-
this.plugin.cfg = { main: { reject: true } };
|
|
9
|
-
|
|
10
|
-
this.connection = fixtures.connection.createConnection();
|
|
11
|
-
done();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('early_talker', () => {
|
|
15
|
-
beforeEach(_set_up)
|
|
16
|
-
|
|
17
|
-
it('no config', (done) => {
|
|
18
|
-
this.plugin.early_talker((rc, msg) => {
|
|
19
|
-
assert.equal(rc, undefined);
|
|
20
|
-
assert.equal(msg, undefined);
|
|
21
|
-
done();
|
|
22
|
-
}, this.connection);
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('relaying', (done) => {
|
|
26
|
-
this.plugin.pause = 1;
|
|
27
|
-
this.connection.relaying = true;
|
|
28
|
-
this.plugin.early_talker((rc, msg) => {
|
|
29
|
-
assert.equal(rc, undefined);
|
|
30
|
-
assert.equal(msg, undefined);
|
|
31
|
-
done();
|
|
32
|
-
}, this.connection);
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('is an early talker', (done) => {
|
|
36
|
-
const before = Date.now();
|
|
37
|
-
this.plugin.pause = 1001;
|
|
38
|
-
this.connection.early_talker = true;
|
|
39
|
-
this.plugin.early_talker((rc, msg) => {
|
|
40
|
-
assert.ok(Date.now() >= before + 1000);
|
|
41
|
-
assert.equal(rc, DENYDISCONNECT);
|
|
42
|
-
assert.equal(msg, 'You talk too soon');
|
|
43
|
-
done();
|
|
44
|
-
}, this.connection);
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('is an early talker, reject=false', (done) => {
|
|
48
|
-
const before = Date.now();
|
|
49
|
-
this.plugin.pause = 1001;
|
|
50
|
-
this.plugin.cfg.main.reject = false;
|
|
51
|
-
this.connection.early_talker = true;
|
|
52
|
-
this.plugin.early_talker((rc, msg) => {
|
|
53
|
-
assert.ok(Date.now() >= before + 1000);
|
|
54
|
-
assert.equal(undefined, rc);
|
|
55
|
-
assert.equal(undefined, msg);
|
|
56
|
-
assert.ok(this.connection.results.has('early_talker', 'fail', 'early'));
|
|
57
|
-
done();
|
|
58
|
-
}, this.connection);
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('relay whitelisted ip', (done) => {
|
|
62
|
-
this.plugin.pause = 1000;
|
|
63
|
-
this.plugin.whitelist = this.plugin.load_ip_list(['127.0.0.1']);
|
|
64
|
-
this.connection.remote.ip = '127.0.0.1';
|
|
65
|
-
this.connection.early_talker = true;
|
|
66
|
-
this.plugin.early_talker((rc, msg) => {
|
|
67
|
-
assert.equal(undefined, rc);
|
|
68
|
-
assert.equal(undefined, msg);
|
|
69
|
-
assert.ok(this.connection.results.has('early_talker', 'skip', 'whitelist'));
|
|
70
|
-
done();
|
|
71
|
-
}, this.connection);
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('relay whitelisted subnet', (done) => {
|
|
75
|
-
this.plugin.pause = 1000;
|
|
76
|
-
this.plugin.whitelist = this.plugin.load_ip_list(['127.0.0.0/16']);
|
|
77
|
-
this.connection.remote.ip = '127.0.0.88';
|
|
78
|
-
this.connection.early_talker = true;
|
|
79
|
-
this.plugin.early_talker((rc, msg) => {
|
|
80
|
-
assert.equal(undefined, rc);
|
|
81
|
-
assert.equal(undefined, msg);
|
|
82
|
-
assert.ok(this.connection.results.has('early_talker', 'skip', 'whitelist'));
|
|
83
|
-
done();
|
|
84
|
-
}, this.connection);
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('relay good senders', (done) => {
|
|
88
|
-
this.plugin.pause = 1000;
|
|
89
|
-
this.connection.results.add('karma', {good: 10});
|
|
90
|
-
this.connection.early_talker = true;
|
|
91
|
-
this.plugin.early_talker((rc, msg) => {
|
|
92
|
-
assert.equal(undefined, rc);
|
|
93
|
-
assert.equal(undefined, msg);
|
|
94
|
-
assert.ok(this.connection.results.has('early_talker', 'skip', '+karma'));
|
|
95
|
-
done();
|
|
96
|
-
}, this.connection);
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('test loading ip list', () => {
|
|
100
|
-
const whitelist = this.plugin.load_ip_list(['123.123.123.123', '127.0.0.0/16']);
|
|
101
|
-
assert.equal(whitelist[0][1], 32);
|
|
102
|
-
assert.equal(whitelist[1][1], 16);
|
|
103
|
-
})
|
|
104
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert')
|
|
3
|
-
const dns = require('node:dns');
|
|
4
|
-
|
|
5
|
-
const fixtures = require('haraka-test-fixtures');
|
|
6
|
-
const Address = require('address-rfc2821').Address
|
|
7
|
-
|
|
8
|
-
const _set_up = (done) => {
|
|
9
|
-
|
|
10
|
-
this.plugin = new fixtures.plugin('mail_from.is_resolvable');
|
|
11
|
-
this.plugin.register();
|
|
12
|
-
|
|
13
|
-
this.connection = fixtures.connection.createConnection();
|
|
14
|
-
this.connection.init_transaction()
|
|
15
|
-
|
|
16
|
-
done();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('mail_from.is_resolvable', () => {
|
|
20
|
-
beforeEach(_set_up)
|
|
21
|
-
|
|
22
|
-
describe('hook_mail', () => {
|
|
23
|
-
it('any.com, no err code', (done) => {
|
|
24
|
-
const txn = this.connection.transaction;
|
|
25
|
-
this.plugin.hook_mail((code, msg) => {
|
|
26
|
-
// console.log()
|
|
27
|
-
assert.deepEqual(txn.results.get('mail_from.is_resolvable').pass, ['has_fwd_dns']);
|
|
28
|
-
done();
|
|
29
|
-
},
|
|
30
|
-
this.connection,
|
|
31
|
-
[new Address('<test@any.com>')]
|
|
32
|
-
)
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
})
|