Haraka 3.0.3 → 3.0.4
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/.eslintrc.yaml +5 -9
- package/.prettierrc.yml +1 -0
- package/CONTRIBUTORS.md +11 -0
- package/Changes.md +1365 -1214
- package/Plugins.md +117 -105
- package/README.md +4 -13
- package/bin/haraka +197 -298
- package/config/auth_flat_file.ini +1 -0
- package/config/dhparams.pem +8 -0
- package/config/mail_from.is_resolvable.ini +4 -2
- package/config/me +1 -0
- package/config/outbound.ini +0 -2
- package/config/plugins +36 -35
- package/config/smtp.ini +0 -1
- package/config/smtp.json +17 -0
- package/config/tls_cert.pem +23 -0
- package/config/tls_key.pem +28 -0
- package/connection.js +46 -73
- package/contrib/bsd-rc.d/haraka +3 -1
- package/contrib/plugin2npm.sh +6 -36
- package/docs/CoreConfig.md +2 -2
- package/docs/Logging.md +7 -21
- package/docs/Outbound.md +104 -201
- package/docs/Plugins.md +2 -2
- package/docs/Transaction.md +59 -82
- package/docs/plugins/queue/smtp_proxy.md +5 -10
- package/docs/plugins/tls.md +29 -9
- package/endpoint.js +16 -13
- package/haraka.js +10 -14
- package/host_pool.js +5 -5
- package/line_socket.js +3 -4
- package/logger.js +44 -28
- package/outbound/client_pool.js +27 -23
- package/outbound/config.js +4 -6
- package/outbound/fsync_writestream.js +1 -1
- package/outbound/hmail.js +178 -218
- package/outbound/index.js +86 -99
- package/outbound/qfile.js +1 -1
- package/outbound/queue.js +51 -44
- package/outbound/timer_queue.js +3 -2
- package/outbound/tls.js +19 -7
- package/package.json +59 -48
- package/plugins/.eslintrc.yaml +0 -6
- package/plugins/auth/auth_base.js +4 -2
- package/plugins/auth/auth_proxy.js +14 -12
- package/plugins/auth/auth_vpopmaild.js +1 -1
- package/plugins/block_me.js +1 -1
- package/plugins/data.signatures.js +2 -4
- package/plugins/early_talker.js +2 -1
- package/plugins/mail_from.is_resolvable.js +65 -135
- package/plugins/queue/deliver.js +4 -5
- package/plugins/queue/lmtp.js +11 -14
- package/plugins/queue/qmail-queue.js +2 -2
- package/plugins/queue/quarantine.js +2 -2
- package/plugins/queue/rabbitmq.js +16 -17
- package/plugins/queue/smtp_forward.js +3 -3
- package/plugins/queue/smtp_proxy.js +10 -1
- package/plugins/queue/test.js +2 -2
- package/plugins/rcpt_to.host_list_base.js +5 -5
- package/plugins/rcpt_to.in_host_list.js +2 -2
- package/plugins/relay.js +6 -7
- package/plugins/reseed_rng.js +1 -1
- package/plugins/status.js +37 -33
- package/plugins/tls.js +2 -2
- package/plugins/xclient.js +3 -2
- package/plugins.js +50 -54
- package/run_tests +3 -30
- package/server.js +190 -190
- package/smtp_client.js +30 -23
- package/{tests → test}/config/plugins +0 -2
- package/{tests → test}/config/smtp.ini +1 -1
- package/test/config/tls/example.com/_.example.com.key +28 -0
- package/test/config/tls/example.com/example.com.crt +25 -0
- package/test/connection.js +302 -0
- package/test/endpoint.js +94 -0
- package/{tests → test}/fixtures/line_socket.js +1 -1
- package/{tests → test}/fixtures/util_hmailitem.js +19 -25
- package/{tests → test}/host_pool.js +42 -57
- package/test/logger.js +258 -0
- package/test/outbound/hmail.js +141 -0
- package/test/outbound/index.js +220 -0
- package/test/outbound/qfile.js +126 -0
- package/test/outbound_bounce_net_errors.js +142 -0
- package/{tests → test}/outbound_bounce_rfc3464.js +110 -122
- package/test/plugins/auth/auth_base.js +484 -0
- package/test/plugins/auth/auth_vpopmaild.js +83 -0
- package/test/plugins/early_talker.js +104 -0
- package/test/plugins/mail_from.is_resolvable.js +35 -0
- package/test/plugins/queue/smtp_forward.js +206 -0
- package/test/plugins/rcpt_to.host_list_base.js +122 -0
- package/test/plugins/rcpt_to.in_host_list.js +193 -0
- package/test/plugins/relay.js +303 -0
- package/test/plugins/status.js +130 -0
- package/test/plugins/tls.js +70 -0
- package/test/plugins.js +228 -0
- package/test/rfc1869.js +73 -0
- package/test/server.js +491 -0
- package/test/smtp_client.js +299 -0
- package/test/tls_socket.js +273 -0
- package/test/transaction.js +270 -0
- package/tls_socket.js +202 -252
- package/transaction.js +8 -23
- package/CONTRIBUTING.md +0 -1
- package/bin/dkimverify +0 -40
- package/config/access.domains +0 -13
- package/config/attachment.ctype.regex +0 -2
- package/config/attachment.filename.regex +0 -1
- package/config/avg.ini +0 -5
- package/config/bounce.ini +0 -15
- package/config/data.headers.ini +0 -61
- package/config/dkim/dkim_key_gen.sh +0 -78
- package/config/dkim_sign.ini +0 -4
- package/config/dkim_verify.ini +0 -7
- package/config/dnsbl.ini +0 -23
- package/config/greylist.ini +0 -43
- package/config/helo.checks.ini +0 -52
- package/config/messagesniffer.ini +0 -18
- package/config/spamassassin.ini +0 -56
- package/dkim.js +0 -614
- package/docs/plugins/avg.md +0 -35
- package/docs/plugins/bounce.md +0 -69
- package/docs/plugins/clamd.md +0 -147
- package/docs/plugins/esets.md +0 -8
- package/docs/plugins/greylist.md +0 -90
- package/docs/plugins/helo.checks.md +0 -135
- package/docs/plugins/messagesniffer.md +0 -163
- package/docs/plugins/spamassassin.md +0 -180
- package/outbound/mx_lookup.js +0 -70
- package/plugins/auth/auth_ldap.js +0 -3
- package/plugins/avg.js +0 -162
- package/plugins/backscatterer.js +0 -25
- package/plugins/bounce.js +0 -381
- package/plugins/clamd.js +0 -382
- package/plugins/data.uribl.js +0 -4
- package/plugins/dkim_sign.js +0 -395
- package/plugins/dkim_verify.js +0 -62
- package/plugins/dns_list_base.js +0 -221
- package/plugins/dnsbl.js +0 -146
- package/plugins/dnswl.js +0 -58
- package/plugins/esets.js +0 -71
- package/plugins/graph.js +0 -5
- package/plugins/greylist.js +0 -645
- package/plugins/helo.checks.js +0 -533
- package/plugins/messagesniffer.js +0 -381
- package/plugins/rcpt_to.ldap.js +0 -3
- package/plugins/rcpt_to.max_count.js +0 -24
- package/plugins/spamassassin.js +0 -384
- package/tests/config/dkim/example.com/dns +0 -29
- package/tests/config/dkim/example.com/private +0 -6
- package/tests/config/dkim/example.com/public +0 -4
- package/tests/config/dkim/example.com/selector +0 -1
- package/tests/config/dkim.private.key +0 -6
- package/tests/config/dkim_sign.ini +0 -4
- package/tests/config/helo.checks.ini +0 -52
- package/tests/connection.js +0 -327
- package/tests/endpoint.js +0 -128
- package/tests/fixtures/vm_harness.js +0 -59
- package/tests/logger.js +0 -327
- package/tests/outbound/hmail.js +0 -112
- package/tests/outbound/index.js +0 -324
- package/tests/outbound/qfile.js +0 -67
- package/tests/outbound_bounce_net_errors.js +0 -173
- package/tests/plugins/auth/auth_base.js +0 -463
- package/tests/plugins/auth/auth_vpopmaild.js +0 -91
- package/tests/plugins/bounce.js +0 -307
- package/tests/plugins/clamd.js +0 -224
- package/tests/plugins/deprecated/relay_acl.js +0 -140
- package/tests/plugins/deprecated/relay_all.js +0 -59
- package/tests/plugins/dkim_sign.js +0 -315
- package/tests/plugins/dkim_signer.js +0 -108
- package/tests/plugins/dns_list_base.js +0 -259
- package/tests/plugins/dnsbl.js +0 -101
- package/tests/plugins/early_talker.js +0 -115
- package/tests/plugins/greylist.js +0 -58
- package/tests/plugins/helo.checks.js +0 -525
- package/tests/plugins/mail_from.is_resolvable.js +0 -116
- package/tests/plugins/queue/smtp_forward.js +0 -221
- package/tests/plugins/rcpt_to.host_list_base.js +0 -132
- package/tests/plugins/rcpt_to.in_host_list.js +0 -218
- package/tests/plugins/relay.js +0 -339
- package/tests/plugins/spamassassin.js +0 -171
- package/tests/plugins/status.js +0 -138
- package/tests/plugins/tls.js +0 -84
- package/tests/plugins.js +0 -247
- package/tests/rfc1869.js +0 -61
- package/tests/server.js +0 -510
- package/tests/smtp_client/auth.js +0 -105
- package/tests/smtp_client/basic.js +0 -101
- package/tests/smtp_client.js +0 -80
- package/tests/tls_socket.js +0 -333
- package/tests/transaction.js +0 -284
- /package/docs/{plugins → deprecated}/dkim_sign.md +0 -0
- /package/docs/{plugins → deprecated}/dkim_verify.md +0 -0
- /package/docs/{plugins → deprecated}/dnsbl.md +0 -0
- /package/docs/{plugins → deprecated}/dnswl.md +0 -0
- /package/{tests → test}/.eslintrc.yaml +0 -0
- /package/{tests → test}/config/auth_flat_file.ini +0 -0
- /package/{tests → test}/config/dhparams.pem +0 -0
- /package/{tests → test}/config/host_list +0 -0
- /package/{tests → test}/config/outbound_tls_cert.pem +0 -0
- /package/{tests → test}/config/outbound_tls_key.pem +0 -0
- /package/{tests → test}/config/smtp_forward.ini +0 -0
- /package/{tests → test}/config/tls/ec.pem +0 -0
- /package/{tests → test}/config/tls/haraka.local.pem +0 -0
- /package/{tests → test}/config/tls/mismatched.pem +0 -0
- /package/{tests → test}/config/tls.ini +0 -0
- /package/{tests → test}/config/tls_cert.pem +0 -0
- /package/{tests → test}/config/tls_key.pem +0 -0
- /package/{tests → test}/fixtures/todo_qfile.txt +0 -0
- /package/{tests → test}/installation/config/test-plugin-flat +0 -0
- /package/{tests → test}/installation/config/test-plugin.ini +0 -0
- /package/{tests → test}/installation/config/tls.ini +0 -0
- /package/{tests → test}/installation/node_modules/load_first/index.js +0 -0
- /package/{tests → test}/installation/node_modules/load_first/package.json +0 -0
- /package/{tests → test}/installation/node_modules/test-plugin/config/test-plugin-flat +0 -0
- /package/{tests → test}/installation/node_modules/test-plugin/config/test-plugin.ini +0 -0
- /package/{tests → test}/installation/node_modules/test-plugin/package.json +0 -0
- /package/{tests → test}/installation/node_modules/test-plugin/test-plugin.js +0 -0
- /package/{tests → test}/installation/plugins/base_plugin.js +0 -0
- /package/{tests → test}/installation/plugins/folder_plugin/index.js +0 -0
- /package/{tests → test}/installation/plugins/folder_plugin/package.json +0 -0
- /package/{tests → test}/installation/plugins/inherits.js +0 -0
- /package/{tests → test}/installation/plugins/load_first.js +0 -0
- /package/{tests → test}/installation/plugins/plugin.js +0 -0
- /package/{tests → test}/installation/plugins/tls.js +0 -0
- /package/{tests → test}/loud/config/dhparams.pem +0 -0
- /package/{tests → test}/loud/config/tls/goobered.pem +0 -0
- /package/{tests → test}/loud/config/tls.ini +0 -0
- /package/{tests → test}/mail_specimen/base64-root-part.txt +0 -0
- /package/{tests → test}/mail_specimen/varied-fold-lengths-preserve-data.txt +0 -0
- /package/{tests → test}/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
- /package/{tests → test}/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
- /package/{tests → test}/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
- /package/{tests → test}/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
- /package/{tests → test}/queue/multibyte +0 -0
- /package/{tests → test}/queue/plain +0 -0
- /package/{tests → test}/queue/zero-length +0 -0
- /package/{tests → test}/test-queue/delete-me +0 -0
package/plugins/dkim_sign.js
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
// dkim_signer
|
|
2
|
-
// Implements DKIM core as per www.dkimcore.org
|
|
3
|
-
|
|
4
|
-
const addrparser = require('address-rfc2822');
|
|
5
|
-
const async = require('async');
|
|
6
|
-
const crypto = require('crypto');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const { Stream } = require('stream');
|
|
10
|
-
|
|
11
|
-
const utils = require('haraka-utils');
|
|
12
|
-
|
|
13
|
-
class DKIMSignStream extends Stream {
|
|
14
|
-
constructor (props, header, done) {
|
|
15
|
-
super();
|
|
16
|
-
|
|
17
|
-
this.selector = props.selector;
|
|
18
|
-
|
|
19
|
-
// fix issue #2668 renaming reserved kw/property of 'domain' to 'domain_name'
|
|
20
|
-
this.domain_name = props.domain;
|
|
21
|
-
this.private_key = props.private_key;
|
|
22
|
-
this.headers_to_sign = props.headers;
|
|
23
|
-
this.header = header;
|
|
24
|
-
this.end_callback = done;
|
|
25
|
-
this.writable = true;
|
|
26
|
-
this.found_eoh = false;
|
|
27
|
-
this.buffer = { ar: [], len: 0 };
|
|
28
|
-
this.hash = crypto.createHash('SHA256');
|
|
29
|
-
this.line_buffer = { ar: [], len: 0 };
|
|
30
|
-
this.signer = crypto.createSign('RSA-SHA256');
|
|
31
|
-
this.body_found = false;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
write (buf) {
|
|
35
|
-
/*
|
|
36
|
-
** BODY (simple canonicalization)
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
// Merge in any partial data from last iteration
|
|
40
|
-
if (this.buffer.ar.length) {
|
|
41
|
-
this.buffer.ar.push(buf);
|
|
42
|
-
this.buffer.len += buf.length;
|
|
43
|
-
const nb = Buffer.concat(this.buffer.ar, this.buffer.len);
|
|
44
|
-
buf = nb;
|
|
45
|
-
this.buffer = { ar: [], len: 0 };
|
|
46
|
-
}
|
|
47
|
-
// Process input buffer into lines
|
|
48
|
-
let offset = 0;
|
|
49
|
-
while ((offset = utils.indexOfLF(buf)) !== -1) {
|
|
50
|
-
const line = buf.slice(0, offset+1);
|
|
51
|
-
if (buf.length > offset) {
|
|
52
|
-
buf = buf.slice(offset+1);
|
|
53
|
-
}
|
|
54
|
-
// Look for CRLF
|
|
55
|
-
if (line.length === 2 && line[0] === 0x0d && line[1] === 0x0a) {
|
|
56
|
-
// Look for end of headers marker
|
|
57
|
-
if (!this.found_eoh) {
|
|
58
|
-
this.found_eoh = true;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
// Store any empty lines so that we can discard
|
|
62
|
-
// any trailing CRLFs at the end of the message
|
|
63
|
-
this.line_buffer.ar.push(line);
|
|
64
|
-
this.line_buffer.len += line.length;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
if (!this.found_eoh) continue; // Skip headers
|
|
69
|
-
if (this.line_buffer.ar.length) {
|
|
70
|
-
// We need to process the buffered CRLFs
|
|
71
|
-
const lb = Buffer.concat(this.line_buffer.ar, this.line_buffer.len);
|
|
72
|
-
this.line_buffer = { ar: [], len: 0 };
|
|
73
|
-
this.hash.update(lb);
|
|
74
|
-
}
|
|
75
|
-
this.hash.update(line);
|
|
76
|
-
this.body_found = true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (buf.length) {
|
|
80
|
-
// We have partial data...
|
|
81
|
-
this.buffer.ar.push(buf);
|
|
82
|
-
this.buffer.len += buf.length;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
end (buf) {
|
|
87
|
-
this.writable = false;
|
|
88
|
-
|
|
89
|
-
// Add trailing CRLF if we have data left over
|
|
90
|
-
if (this.buffer.ar.length) {
|
|
91
|
-
this.buffer.ar.push(Buffer.from("\r\n"));
|
|
92
|
-
this.buffer.len += 2;
|
|
93
|
-
const le = Buffer.concat(this.buffer.ar, this.buffer.len);
|
|
94
|
-
this.hash.update(le);
|
|
95
|
-
this.buffer = { ar: [], len: 0 };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!this.body_found) {
|
|
99
|
-
this.hash.update(Buffer.from("\r\n"));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const bodyhash = this.hash.digest('base64');
|
|
103
|
-
|
|
104
|
-
/*
|
|
105
|
-
** HEADERS (relaxed canonicaliztion)
|
|
106
|
-
*/
|
|
107
|
-
|
|
108
|
-
const headers = [];
|
|
109
|
-
for (const element of this.headers_to_sign) {
|
|
110
|
-
let head = this.header.get(element);
|
|
111
|
-
if (head) {
|
|
112
|
-
head = head.replace(/\r?\n/gm, '');
|
|
113
|
-
head = head.replace(/\s+/gm, ' ');
|
|
114
|
-
head = head.replace(/\s+$/gm, '');
|
|
115
|
-
this.signer.update(`${element}:${head}\r\n`);
|
|
116
|
-
headers.push(element);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Create DKIM header
|
|
121
|
-
let dkim_header = `v=1; a=rsa-sha256; c=relaxed/simple; d=${this.domain_name}; s=${this.selector}; h=${headers.join(':')}; bh=${bodyhash}; b=`;
|
|
122
|
-
this.signer.update(`dkim-signature:${dkim_header}`);
|
|
123
|
-
const signature = this.signer.sign(this.private_key, 'base64');
|
|
124
|
-
dkim_header = `v=1; a=rsa-sha256; c=relaxed/simple;\r\n\td=${this.domain_name}; s=${this.selector};\r\n\th=${headers.join(':')};\r\n\tbh=${bodyhash};\r\n\tb=`;
|
|
125
|
-
dkim_header += signature.substring(0,74);
|
|
126
|
-
for (let i=74; i<signature.length; i+=76) {
|
|
127
|
-
dkim_header += `\r\n\t${signature.substring(i, i+76)}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (this.end_callback) this.end_callback(null, dkim_header);
|
|
131
|
-
this.end_callback = null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
destroy () {
|
|
135
|
-
this.writable = false;
|
|
136
|
-
// Stream destroyed before the callback ran
|
|
137
|
-
if (this.end_callback) {
|
|
138
|
-
this.end_callback(new Error('Stream destroyed'));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
exports.DKIMSignStream = DKIMSignStream;
|
|
144
|
-
|
|
145
|
-
exports.register = function () {
|
|
146
|
-
this.load_dkim_sign_ini();
|
|
147
|
-
this.load_dkim_default_key();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
exports.load_dkim_sign_ini = function () {
|
|
151
|
-
this.cfg = this.config.get('dkim_sign.ini', {
|
|
152
|
-
booleans: [
|
|
153
|
-
'-disabled',
|
|
154
|
-
]
|
|
155
|
-
},
|
|
156
|
-
() => { this.load_dkim_sign_ini(); }
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
this.cfg.headers_to_sign = this.get_headers_to_sign();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
exports.load_dkim_default_key = function () {
|
|
163
|
-
this.private_key = this.config.get('dkim.private.key', 'data', () => {
|
|
164
|
-
this.load_dkim_default_key();
|
|
165
|
-
}).join('\n');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
exports.load_key = function (file) {
|
|
169
|
-
return this.config.get(file, 'data').join('\n');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
exports.hook_queue_outbound = exports.hook_pre_send_trans_email = function (next, connection) {
|
|
173
|
-
if (this.cfg.main.disabled) return next();
|
|
174
|
-
if (!connection?.transaction) return next();
|
|
175
|
-
|
|
176
|
-
if (connection.transaction.notes?.dkim_signed) {
|
|
177
|
-
connection.logdebug(this, 'already signed');
|
|
178
|
-
return next();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
exports.get_sign_properties(connection, (err, props) => {
|
|
182
|
-
if (!connection?.transaction) return next();
|
|
183
|
-
// props: selector, domain, & private_key
|
|
184
|
-
if (err) connection.logerror(this, `${err.message}`);
|
|
185
|
-
|
|
186
|
-
if (!this.has_key_data(connection, props)) return next();
|
|
187
|
-
|
|
188
|
-
connection.logdebug(this, `domain: ${props.domain}`);
|
|
189
|
-
|
|
190
|
-
const txn = connection.transaction;
|
|
191
|
-
props.headers = this.cfg.headers_to_sign;
|
|
192
|
-
|
|
193
|
-
txn.message_stream.pipe(
|
|
194
|
-
new DKIMSignStream(props, txn.header, (err2, dkim_header) => {
|
|
195
|
-
if (err2) {
|
|
196
|
-
txn.results.add(this, {err: err2.message});
|
|
197
|
-
return next(err2);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
connection.loginfo(this, `signed for ${props.domain}`);
|
|
201
|
-
txn.results.add(this, {pass: dkim_header});
|
|
202
|
-
txn.add_header('DKIM-Signature', dkim_header);
|
|
203
|
-
|
|
204
|
-
connection.transaction.notes.dkim_signed = true;
|
|
205
|
-
next();
|
|
206
|
-
})
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
exports.get_sign_properties = function (connection, done) {
|
|
212
|
-
if (!connection.transaction) return;
|
|
213
|
-
|
|
214
|
-
const domain = this.get_sender_domain(connection);
|
|
215
|
-
|
|
216
|
-
if (!domain) {
|
|
217
|
-
connection.transaction.results.add(this, {msg: 'sending domain not detected', emit: true });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const props = { domain }
|
|
221
|
-
|
|
222
|
-
this.get_key_dir(connection, props, (err, keydir) => {
|
|
223
|
-
if (err) {
|
|
224
|
-
console.error(`err: ${err}`);
|
|
225
|
-
connection.logerror(this, err);
|
|
226
|
-
return done(new Error(`Error getting DKIM key_dir for ${domain}: ${err}`), props)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (!connection.transaction) return done(null, props);
|
|
230
|
-
|
|
231
|
-
// a directory for ${domain} exists
|
|
232
|
-
if (keydir) {
|
|
233
|
-
props.domain = path.basename(keydir); // keydir might be apex (vs sub)domain
|
|
234
|
-
props.private_key = this.load_key(path.join('dkim', props.domain, 'private'));
|
|
235
|
-
props.selector = this.load_key(path.join('dkim', props.domain, 'selector')).trim();
|
|
236
|
-
|
|
237
|
-
if (!props.selector) {
|
|
238
|
-
connection.transaction.results.add(this, {err: `missing selector for domain ${domain}`});
|
|
239
|
-
}
|
|
240
|
-
if (!props.private_key) {
|
|
241
|
-
connection.transaction.results.add(this, {err: `missing dkim private_key for domain ${domain}`});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (props.selector && props.private_key ) { // AND has correct files
|
|
245
|
-
return done(null, props);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// try [default / single domain] configuration
|
|
250
|
-
if (this.cfg.main.domain && this.cfg.main.selector && this.private_key) {
|
|
251
|
-
|
|
252
|
-
connection.transaction.results.add(this, {msg: 'using default key', emit: true });
|
|
253
|
-
|
|
254
|
-
props.domain = this.cfg.main.domain;
|
|
255
|
-
props.private_key = this.private_key;
|
|
256
|
-
props.selector = this.cfg.main.selector;
|
|
257
|
-
|
|
258
|
-
return done(null, props)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
console.error(`no valid DKIM properties found`)
|
|
262
|
-
done(null, props);
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
exports.get_key_dir = function (connection, props, done) {
|
|
267
|
-
|
|
268
|
-
if (!props.domain) return done();
|
|
269
|
-
|
|
270
|
-
// split the domain name into labels
|
|
271
|
-
const labels = props.domain.split('.');
|
|
272
|
-
const haraka_dir = process.env.HARAKA || '';
|
|
273
|
-
|
|
274
|
-
// list possible matches (ex: mail.example.com, example.com, com)
|
|
275
|
-
const dom_hier = [];
|
|
276
|
-
for (let i=0; i<labels.length; i++) {
|
|
277
|
-
const dom = labels.slice(i).join('.');
|
|
278
|
-
dom_hier[i] = path.resolve(haraka_dir, 'config', 'dkim', dom);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async.detectSeries(dom_hier, (filePath, iterDone) => {
|
|
282
|
-
fs.stat(filePath, (err, stats) => {
|
|
283
|
-
if (err) return iterDone(null, false);
|
|
284
|
-
iterDone(null, stats.isDirectory());
|
|
285
|
-
});
|
|
286
|
-
},
|
|
287
|
-
(err, results) => {
|
|
288
|
-
connection.logdebug(this, results);
|
|
289
|
-
done(err, results);
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
exports.has_key_data = function (conn, props) {
|
|
294
|
-
|
|
295
|
-
let missing = undefined;
|
|
296
|
-
|
|
297
|
-
// Make sure we have all the relevant configuration
|
|
298
|
-
if (!props.private_key) {
|
|
299
|
-
missing = 'private key';
|
|
300
|
-
}
|
|
301
|
-
else if (!props.selector) {
|
|
302
|
-
missing = 'selector';
|
|
303
|
-
}
|
|
304
|
-
else if (!props.domain) {
|
|
305
|
-
missing = 'domain';
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (missing) {
|
|
309
|
-
if (props.domain) {
|
|
310
|
-
conn.lognotice(this, `skipped: no ${missing} for ${props.domain}`);
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
conn.lognotice(this, `skipped: no ${missing}`);
|
|
314
|
-
}
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
conn.logprotocol(this, `using selector: ${props.selector} at domain ${props.domain}`);
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
exports.get_headers_to_sign = function (cfg) {
|
|
323
|
-
|
|
324
|
-
if (!cfg) cfg = this.cfg;
|
|
325
|
-
if (!cfg.main.headers_to_sign) return [ 'from' ];
|
|
326
|
-
|
|
327
|
-
const headers = cfg.main.headers_to_sign
|
|
328
|
-
.toLowerCase()
|
|
329
|
-
.replace(/\s+/g,'')
|
|
330
|
-
.split(/[,;:]/);
|
|
331
|
-
|
|
332
|
-
// From MUST be present
|
|
333
|
-
if (!headers.includes('from')) headers.push('from');
|
|
334
|
-
|
|
335
|
-
return headers;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
exports.get_sender_domain = function (connection) {
|
|
339
|
-
|
|
340
|
-
const txn = connection?.transaction;
|
|
341
|
-
if (!txn) return;
|
|
342
|
-
|
|
343
|
-
// fallback: use Envelope FROM when header parsing fails
|
|
344
|
-
let domain;
|
|
345
|
-
if (txn.mail_from.host) {
|
|
346
|
-
try { domain = txn.mail_from.host.toLowerCase(); }
|
|
347
|
-
catch (e) {
|
|
348
|
-
connection.logerror(this, e);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// In case of forwarding, only use the Envelope
|
|
353
|
-
if (txn.notes.forward) return domain;
|
|
354
|
-
if (!txn.header) return domain;
|
|
355
|
-
|
|
356
|
-
// the DKIM signing key should be aligned with the domain in the From
|
|
357
|
-
// header (see DMARC). Try to parse the domain from there.
|
|
358
|
-
const from_hdr = txn.header.get_decoded('From');
|
|
359
|
-
if (!from_hdr) return domain;
|
|
360
|
-
|
|
361
|
-
// The From header can contain multiple addresses and should be
|
|
362
|
-
// parsed as described in RFC 2822 3.6.2.
|
|
363
|
-
let addrs;
|
|
364
|
-
try {
|
|
365
|
-
addrs = addrparser.parse(from_hdr);
|
|
366
|
-
}
|
|
367
|
-
catch (e) {
|
|
368
|
-
connection.logerror(this, `address-rfc2822 failed to parse From header: ${from_hdr}`)
|
|
369
|
-
return domain;
|
|
370
|
-
}
|
|
371
|
-
if (!addrs || ! addrs.length) return domain;
|
|
372
|
-
|
|
373
|
-
// If From has a single address, we're done
|
|
374
|
-
if (addrs.length === 1 && addrs[0].host) {
|
|
375
|
-
let fromHost = addrs[0].host();
|
|
376
|
-
if (fromHost) {
|
|
377
|
-
// don't attempt to lower a null or undefined value #1575
|
|
378
|
-
fromHost = fromHost.toLowerCase();
|
|
379
|
-
}
|
|
380
|
-
return fromHost;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// If From has multiple-addresses, we must parse and
|
|
384
|
-
// use the domain in the Sender header.
|
|
385
|
-
const sender = txn.header.get_decoded('Sender');
|
|
386
|
-
if (sender) {
|
|
387
|
-
try {
|
|
388
|
-
domain = (addrparser.parse(sender))[0].host().toLowerCase();
|
|
389
|
-
}
|
|
390
|
-
catch (e) {
|
|
391
|
-
connection.logerror(this, e);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return domain;
|
|
395
|
-
}
|
package/plugins/dkim_verify.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
const dkim = require('./dkim');
|
|
3
|
-
|
|
4
|
-
const { DKIMVerifyStream } = dkim;
|
|
5
|
-
|
|
6
|
-
const plugin = exports;
|
|
7
|
-
|
|
8
|
-
dkim.DKIMObject.prototype.debug = str => {
|
|
9
|
-
plugin.logdebug(str);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
DKIMVerifyStream.prototype.debug = str => {
|
|
13
|
-
plugin.logdebug(str);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
exports.register = function () {
|
|
17
|
-
this.load_config()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
exports.load_config = function () {
|
|
21
|
-
const cfg = this.config.get('dkim_verify.ini', {}, () => this.load_config())
|
|
22
|
-
|
|
23
|
-
this.cfg = Object.assign({}, cfg.main, {
|
|
24
|
-
timeout: plugin.timeout ? plugin.timeout - 1 : 0
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
exports.hook_data_post = function (next, connection) {
|
|
29
|
-
const txn = connection?.transaction;
|
|
30
|
-
if (!txn) return next();
|
|
31
|
-
|
|
32
|
-
const verifier = new DKIMVerifyStream(this.cfg, (err, result, results) => {
|
|
33
|
-
if (err) {
|
|
34
|
-
txn.results.add(this, { err });
|
|
35
|
-
return next();
|
|
36
|
-
}
|
|
37
|
-
if (!results || results.length === 0) {
|
|
38
|
-
txn.results.add(this, { skip: 'no/bad dkim signature' });
|
|
39
|
-
return next(CONT, 'no/bad signature')
|
|
40
|
-
}
|
|
41
|
-
results.forEach((res) => {
|
|
42
|
-
let res_err = '';
|
|
43
|
-
if (res.error) res_err = ` (${res.error})`;
|
|
44
|
-
connection.auth_results(`dkim=${res.result}${res_err} header.i=${res.identity} header.d=${res.domain} header.s=${res.selector}`);
|
|
45
|
-
connection.loginfo(this, `identity="${res.identity}" domain="${res.domain}" selector="${res.selector}" result=${res.result} ${res_err}`);
|
|
46
|
-
|
|
47
|
-
// save to ResultStore
|
|
48
|
-
const rs_obj = JSON.parse(JSON.stringify(res));
|
|
49
|
-
if (res.result === 'pass') { rs_obj.pass = res.domain; }
|
|
50
|
-
else if (res.result === 'fail') { rs_obj.fail = res.domain + res_err; }
|
|
51
|
-
else { rs_obj.err = res.domain + res_err; }
|
|
52
|
-
txn.results.add(this, rs_obj);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
connection.logdebug(this, JSON.stringify(results));
|
|
56
|
-
// Store results for other plugins
|
|
57
|
-
txn.notes.dkim_results = results;
|
|
58
|
-
next();
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
txn.message_stream.pipe(verifier, { line_endings: '\r\n' });
|
|
62
|
-
}
|
package/plugins/dns_list_base.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// DNS list module
|
|
2
|
-
const dns = require('dns');
|
|
3
|
-
const net = require('net');
|
|
4
|
-
const net_utils = require('haraka-net-utils');
|
|
5
|
-
const async = require('async');
|
|
6
|
-
|
|
7
|
-
exports.enable_stats = false;
|
|
8
|
-
exports.disable_allowed = false;
|
|
9
|
-
exports.redis_host = '127.0.0.1:6379';
|
|
10
|
-
let redis_client;
|
|
11
|
-
|
|
12
|
-
exports.lookup = function (lookup, zone, cb) {
|
|
13
|
-
|
|
14
|
-
if (!lookup || !zone) {
|
|
15
|
-
return setImmediate(() => cb(new Error('missing data')));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (this.enable_stats) { this.init_redis(); }
|
|
19
|
-
|
|
20
|
-
// Reverse lookup if IPv4 address
|
|
21
|
-
if (net.isIPv4(lookup)) {
|
|
22
|
-
lookup = lookup.split('.').reverse().join('.');
|
|
23
|
-
}
|
|
24
|
-
else if (net.isIPv6(lookup)) {
|
|
25
|
-
lookup = net_utils.ipv6_reverse(lookup);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let start;
|
|
29
|
-
if (this.enable_stats) {
|
|
30
|
-
start = new Date().getTime();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Build the query, adding the root dot if missing
|
|
34
|
-
let query = [lookup, zone].join('.');
|
|
35
|
-
if (!query.endsWith('.')) {
|
|
36
|
-
query += '.';
|
|
37
|
-
}
|
|
38
|
-
this.logdebug(`looking up: ${query}`);
|
|
39
|
-
// IS: IPv6 compatible (maybe; only if BL return IPv4 answers)
|
|
40
|
-
dns.resolve(query, 'A', (err, a) => {
|
|
41
|
-
this.stats_incr_zone(err, zone, start); // Statistics
|
|
42
|
-
|
|
43
|
-
// Check for a result of 127.0.0.1 or outside 127/8
|
|
44
|
-
// This should *never* happen on a proper DNS list
|
|
45
|
-
if (a && ((!this.lookback_is_rejected && a.includes('127.0.0.1')) ||
|
|
46
|
-
a.find((rec) => { return rec.split('.')[0] !== '127' }))
|
|
47
|
-
) {
|
|
48
|
-
this.disable_zone(zone, a);
|
|
49
|
-
return cb(err, null); // Return a null A record
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// <https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now>
|
|
53
|
-
if (a?.includes('127.255.255.')) {
|
|
54
|
-
this.disable_zone(zone, a);
|
|
55
|
-
return cb(err, null); // Return a null A record
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (err) {
|
|
59
|
-
if (err.code === dns.TIMEOUT) { // list timed out
|
|
60
|
-
this.disable_zone(zone, err.code); // disable it
|
|
61
|
-
}
|
|
62
|
-
if (err.code === dns.NOTFOUND) { // unlisted
|
|
63
|
-
return cb(null, a); // not an error for a DNSBL
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return cb(err, a);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
exports.stats_incr_zone = function (err, zone, start) {
|
|
71
|
-
if (!this.enable_stats) return;
|
|
72
|
-
|
|
73
|
-
const rkey = `dns-list-stat:${zone}`;
|
|
74
|
-
const elapsed = new Date().getTime() - start;
|
|
75
|
-
redis_client.hIncrBy(rkey, 'TOTAL', 1);
|
|
76
|
-
const foo = (err) ? err.code : 'LISTED';
|
|
77
|
-
redis_client.hIncrBy(rkey, foo, 1);
|
|
78
|
-
redis_client.hGet(rkey, 'AVG_RT').then(rt => {
|
|
79
|
-
const avg = parseInt(rt) ? (parseInt(elapsed) + parseInt(rt))/2
|
|
80
|
-
: parseInt(elapsed);
|
|
81
|
-
redis_client.hSet(rkey, 'AVG_RT', avg);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
exports.init_redis = function () {
|
|
86
|
-
if (redis_client) { return; }
|
|
87
|
-
|
|
88
|
-
const redis = require('redis');
|
|
89
|
-
const host_port = this.redis_host.split(':');
|
|
90
|
-
const host = host_port[0] || '127.0.0.1';
|
|
91
|
-
const port = parseInt(host_port[1], 10) || 6379;
|
|
92
|
-
|
|
93
|
-
redis_client = redis.createClient(port, host);
|
|
94
|
-
redis_client.connect().then(() => {
|
|
95
|
-
redis_client.on('error', err => {
|
|
96
|
-
this.logerror(`Redis error: ${err}`);
|
|
97
|
-
redis_client.quit();
|
|
98
|
-
redis_client = null; // should force a reconnect
|
|
99
|
-
// not sure if that's the right thing but better than nothing...
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
exports.multi = function (lookup, zones, cb) {
|
|
105
|
-
if (!lookup) return cb();
|
|
106
|
-
if (!zones ) return cb();
|
|
107
|
-
if (typeof zones === 'string') zones = [ `${zones}` ];
|
|
108
|
-
const self = this;
|
|
109
|
-
const listed = [];
|
|
110
|
-
|
|
111
|
-
function redis_incr (zone) {
|
|
112
|
-
if (!self.enable_stats) return;
|
|
113
|
-
|
|
114
|
-
// Statistics: check hit overlap
|
|
115
|
-
for (const element of listed) {
|
|
116
|
-
const foo = (element === zone) ? 'TOTAL' : element;
|
|
117
|
-
redis_client.hIncrBy(`dns-list-overlap:${zone}`, foo, 1);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function zoneIter (zone, done) {
|
|
122
|
-
self.lookup(lookup, zone, (err, a) => {
|
|
123
|
-
if (a) {
|
|
124
|
-
listed.push(zone);
|
|
125
|
-
redis_incr(zone);
|
|
126
|
-
}
|
|
127
|
-
cb(err, zone, a, true);
|
|
128
|
-
done();
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
function zonesDone (err) {
|
|
132
|
-
cb(err, null, null, false);
|
|
133
|
-
}
|
|
134
|
-
async.each(zones, zoneIter, zonesDone);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Return first positive or last result.
|
|
138
|
-
exports.first = function (lookup, zones, cb, cb_each) {
|
|
139
|
-
if (!lookup || !zones) return cb();
|
|
140
|
-
if (typeof zones === 'string') zones = [ `${zones}` ];
|
|
141
|
-
let ran_cb = false;
|
|
142
|
-
this.multi(lookup, zones, (err, zone, a, pending) => {
|
|
143
|
-
if (zone && cb_each && typeof cb_each === 'function') {
|
|
144
|
-
cb_each(err, zone, a);
|
|
145
|
-
}
|
|
146
|
-
if (ran_cb) return;
|
|
147
|
-
if (pending && (err || !a)) return;
|
|
148
|
-
|
|
149
|
-
// has pending queries OR this one is a positive result
|
|
150
|
-
ran_cb = true;
|
|
151
|
-
cb(err, zone, a);
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
exports.check_zones = function (interval) {
|
|
156
|
-
this.disable_allowed = true;
|
|
157
|
-
if (interval) interval = parseInt(interval);
|
|
158
|
-
if ((this.zones?.length) ||
|
|
159
|
-
(this.disabled_zones?.length)) {
|
|
160
|
-
let zones = [];
|
|
161
|
-
if (this.zones?.length) zones = zones.concat(this.zones);
|
|
162
|
-
if (this.disabled_zones?.length) {
|
|
163
|
-
zones = zones.concat(this.disabled_zones);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// A DNS list should never return positive or an error for this lookup
|
|
167
|
-
// If it does, move it to the disabled list
|
|
168
|
-
this.multi('127.0.0.1', zones, (err, zone, a, pending) => {
|
|
169
|
-
if (!zone) return;
|
|
170
|
-
|
|
171
|
-
if ((!this.lookback_is_rejected && a) || (err && err.code === 'ETIMEOUT')) {
|
|
172
|
-
return this.disable_zone(zone, ((a) ? a : err.code));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Try the test point
|
|
176
|
-
this.lookup('127.0.0.2', zone, (err2, a2) => {
|
|
177
|
-
if (!a2) {
|
|
178
|
-
this.logwarn(`zone '${zone}' did not respond to test point (${err2})`);
|
|
179
|
-
return this.disable_zone(zone, a2);
|
|
180
|
-
}
|
|
181
|
-
// Was this zone previously disabled?
|
|
182
|
-
if (!this.zones.includes(zone)) {
|
|
183
|
-
this.loginfo(`re-enabling zone ${zone}`);
|
|
184
|
-
this.zones.push(zone);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
// Set a timer to re-test
|
|
190
|
-
if (interval && interval >= 5 && !this._interval) {
|
|
191
|
-
this.logdebug(`will re-test list zones every ${interval} minutes`);
|
|
192
|
-
this._interval = setInterval(() => {
|
|
193
|
-
this.check_zones();
|
|
194
|
-
}, (interval * 60) * 1000);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
exports.shutdown = function () {
|
|
199
|
-
clearInterval(this._interval);
|
|
200
|
-
if (redis_client) redis_client.quit();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
exports.disable_zone = function (zone, result) {
|
|
204
|
-
if (!zone) return false;
|
|
205
|
-
if (!this.zones) return false;
|
|
206
|
-
if (!this.zones.length) return false;
|
|
207
|
-
if (!this.disable_allowed) return false;
|
|
208
|
-
|
|
209
|
-
const idx = this.zones.indexOf(zone);
|
|
210
|
-
if (idx === -1) return false; // not enabled
|
|
211
|
-
|
|
212
|
-
this.zones.splice(idx, 1);
|
|
213
|
-
if (!(this.disabled_zones?.length)) {
|
|
214
|
-
this.disabled_zones = [];
|
|
215
|
-
}
|
|
216
|
-
if (!this.disabled_zones.includes(zone)) {
|
|
217
|
-
this.disabled_zones.push(zone);
|
|
218
|
-
}
|
|
219
|
-
this.logwarn(`disabling zone '${zone}'${result ? `: ${result}` : ''}`);
|
|
220
|
-
return true;
|
|
221
|
-
}
|