-
-
Notifications
You must be signed in to change notification settings - Fork 650
/
mail_from.is_resolvable.js
202 lines (178 loc) · 6.71 KB
/
mail_from.is_resolvable.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
'use strict';
// Check MAIL FROM domain is resolvable to an MX
const dns = require('dns');
const net = require('net');
const net_utils = require('haraka-net-utils');
exports.register = function () {
this.load_ini();
}
exports.load_ini = function () {
this.cfg = this.config.get('mail_from.is_resolvable.ini', {
booleans: [
'-main.allow_mx_ip',
'+main.reject_no_mx',
],
}, () => {
this.load_ini();
});
if (isNaN(this.cfg.main.timeout)) {
this.cfg.main.timeout = 29;
}
if (this.timeout) {
if (this.timeout <= this.cfg.main.timeout) {
this.cfg.main.timeout = this.timeout - 1;
this.logwarn(`reducing plugin timeout to ${this.cfg.main.timeout}s`);
}
}
this.re_bogus_ip = new RegExp(this.cfg.main.re_bogus_ip ||
'^(?:0\\.0\\.0\\.0|255\\.255\\.255\\.255|127\\.)' );
}
exports.hook_mail = function (next, connection, params) {
const plugin = this;
const mail_from = params[0];
const txn = connection?.transaction;
if (!txn) return next();
const { results } = txn;
// Check for MAIL FROM without an @ first - ignore those here
if (!mail_from.host) {
results.add(plugin, {skip: 'null host'});
return next();
}
let called_next = 0;
const domain = mail_from.host;
const c = plugin.cfg.main;
const timeout_id = setTimeout(() => {
// DNS answer didn't return (UDP)
connection.loginfo(plugin, `timed out resolving MX for ${domain}`);
called_next++;
if (txn) results.add(plugin, {err: `timeout(${domain})`});
next(DENYSOFT, 'Temporary resolver error (timeout)');
}, c.timeout * 1000);
function mxDone (code, reply) {
if (called_next) return;
clearTimeout(timeout_id);
called_next++;
next(code, reply);
}
// IS: IPv6 compatible
net_utils.get_mx(domain, (err, addresses) => {
if (!txn) return;
if (err && plugin.mxErr(connection, domain, 'MX', err, mxDone)) return;
if (!addresses || !addresses.length) {
// Check for implicit MX 0 record
return plugin.implicit_mx(connection, domain, mxDone);
}
// Verify that the MX records resolve to valid addresses
let records = {};
let pending_queries = 0;
function check_results () {
if (pending_queries !== 0) return;
records = Object.keys(records);
if (records?.length) {
connection.logdebug(plugin, `${domain}: ${records}`);
results.add(plugin, {pass: 'has_fwd_dns'});
return mxDone();
}
results.add(plugin, {fail: 'has_fwd_dns'});
return mxDone(((c.reject_no_mx) ? DENY : DENYSOFT),
'MX without A/AAAA records');
}
addresses.forEach(addr => {
// Handle MX records that are IP addresses
// This is invalid - but a lot of MTAs allow it.
if (net_utils.get_ipany_re('^\\[','\\]$','').test(addr.exchange)) {
connection.logwarn(plugin, `${domain}: invalid MX ${addr.exchange}`);
if (c.allow_mx_ip) {
records[addr.exchange] = 1;
}
return;
}
pending_queries++;
net_utils.get_ips_by_host(addr.exchange, (err2, addresses2) => {
pending_queries--;
if (!txn) return;
if (err2 && err2.length === 2) {
results.add(plugin, {msg: err2[0].message});
connection.logdebug(plugin, `${domain}: MX ${addr.priority} ${addr.exchange} => ${err2[0].message}`);
check_results();
return;
}
connection.logdebug(plugin, `${domain}: MX ${addr.priority} ${addr.exchange} => ${addresses2}`);
for (const element of addresses2) {
// Ignore anything obviously bogus
if (net.isIPv4(element)){
if (plugin.re_bogus_ip.test(element)) {
connection.logdebug(plugin, `${addr.exchange}: discarding ${element}`);
continue;
}
}
if (net.isIPv6(element)){
if (net_utils.ipv6_bogus(element)) {
connection.logdebug(plugin, `${addr.exchange}: discarding ${element}`);
continue;
}
}
records[element] = 1;
}
check_results();
});
});
// In case we don't run any queries
check_results();
});
}
exports.mxErr = function (connection, domain, type, err, mxDone) {
const txn = connection?.transaction;
if (!txn) return;
txn.results.add(this, {msg: `${domain}:${type}:${err.message}`});
connection.logdebug(this, `${domain}:${type} => ${err.message}`);
switch (err.code) {
case dns.NXDOMAIN:
case dns.NOTFOUND:
case dns.NODATA:
// Ignore
break;
default:
mxDone(DENYSOFT, `Temp. resolver error (${err.code})`);
return true;
}
return false;
}
// IS: IPv6 compatible
exports.implicit_mx = function (connection, domain, mxDone) {
const txn = connection?.transaction;
if (!txn) return;
net_utils.get_ips_by_host(domain, (err, addresses) => {
if (!txn) return;
if (!addresses || !addresses.length) {
txn.results.add(this, {fail: 'has_fwd_dns'});
return mxDone(((this.cfg.main.reject_no_mx) ? DENY : DENYSOFT),
'No MX for your FROM address');
}
connection.logdebug(this, `${domain}: A/AAAA => ${addresses}`);
let records = {};
for (const addr of addresses) {
// Ignore anything obviously bogus
if (net.isIPv4(addr)) {
if (this.re_bogus_ip.test(addr)) {
connection.logdebug(this, `${domain}: discarding ${addr}`);
continue;
}
}
if (net.isIPv6(addr)) {
if (net_utils.ipv6_bogus(addr)) {
connection.logdebug(this, `${domain}: discarding ${addr}`);
continue;
}
}
records[addr] = true;
}
records = Object.keys(records);
if (records?.length) {
txn.results.add(this, {pass: 'implicit_mx'});
return mxDone();
}
txn.results.add(this, {fail: `implicit_mx(${domain})`});
return mxDone();
});
}