Haraka 3.1.3 → 3.1.5
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/.prettierignore +2 -0
- package/CONTRIBUTORS.md +23 -1
- package/Changes.md +52 -0
- package/Plugins.md +81 -64
- package/README.md +1 -1
- package/bin/haraka +7 -5
- package/connection.js +15 -19
- package/docs/Plugins.md +1 -1
- package/docs/plugins/aliases.md +0 -2
- package/docs/plugins/queue/qmail-queue.md +0 -1
- package/logger.js +2 -2
- package/outbound/hmail.js +76 -83
- package/outbound/index.js +36 -34
- package/outbound/queue.js +231 -176
- package/package.json +26 -29
- package/plugins/prevent_credential_leaks.js +2 -2
- package/plugins/process_title.js +1 -1
- package/plugins/queue/smtp_forward.js +5 -5
- package/plugins/status.js +8 -5
- package/plugins/tls.js +1 -1
- package/plugins.js +19 -14
- package/rfc1869.js +10 -10
- package/run_tests +8 -2
- package/server.js +15 -10
- package/smtp_client.js +10 -15
- package/test/config/tls/haraka.local.pem +47 -47
- package/test/connection.js +286 -147
- package/test/endpoint.js +5 -4
- package/test/fixtures/line_socket.js +1 -0
- package/test/fixtures/util_hmailitem.js +1 -1
- package/test/host_pool.js +57 -31
- package/test/logger.js +75 -135
- package/test/outbound/bounce_net_errors.js +132 -0
- package/test/outbound/bounce_rfc3464.js +226 -0
- package/test/outbound/hmail.js +140 -104
- package/test/outbound/index.js +61 -101
- package/test/outbound/qfile.js +25 -25
- package/test/outbound/queue.js +233 -0
- package/test/plugins/auth/auth_base.js +39 -44
- package/test/plugins/auth/auth_vpopmaild.js +8 -9
- package/test/plugins/queue/smtp_forward.js +953 -183
- package/test/plugins/rcpt_to.host_list_base.js +58 -93
- package/test/plugins/rcpt_to.in_host_list.js +126 -175
- package/test/plugins/record_envelope_addresses.js +93 -0
- package/test/plugins/status.js +10 -10
- package/test/plugins/tls.js +11 -21
- package/test/plugins/xclient.js +102 -0
- package/test/plugins.js +10 -13
- package/test/rfc1869.js +71 -48
- package/test/server.js +281 -436
- package/test/smtp_client.js +1194 -220
- package/test/tls_socket.js +74 -243
- package/test/transaction.js +486 -201
- package/tls_socket.js +19 -23
- package/transaction.js +33 -10
- package/config/rabbitmq.ini +0 -10
- package/config/rabbitmq_amqplib.ini +0 -19
- package/docs/plugins/queue/rabbitmq.md +0 -34
- package/docs/plugins/queue/rabbitmq_amqplib.md +0 -51
- package/plugins/queue/rabbitmq.js +0 -141
- package/plugins/queue/rabbitmq_amqplib.js +0 -96
- package/test/config/tls/ec.pem +0 -23
- package/test/config/tls/mismatched.pem +0 -49
- package/test/outbound_bounce_net_errors.js +0 -157
- package/test/outbound_bounce_rfc3464.js +0 -366
package/test/server.js
CHANGED
|
@@ -1,124 +1,202 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
4
|
+
const assert = require('node:assert/strict')
|
|
5
|
+
const { createHmac } = require('node:crypto')
|
|
6
|
+
const net = require('node:net')
|
|
2
7
|
const path = require('node:path')
|
|
8
|
+
const tls = require('node:tls')
|
|
3
9
|
|
|
4
10
|
const endpoint = require('../endpoint')
|
|
5
11
|
const message = require('haraka-email-message')
|
|
12
|
+
const { get_client } = require('../smtp_client')
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
this.config = require('haraka-config')
|
|
9
|
-
this.server = require('../server')
|
|
10
|
-
done()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const _setupServer = (ip_port, done) => {
|
|
14
|
-
process.env.YES_REALLY_DO_DISCARD = 1 // for queue/discard plugin
|
|
15
|
-
process.env.HARAKA_TEST_DIR = path.resolve('test')
|
|
16
|
-
|
|
17
|
-
// test sets the default path for plugin instances to the test dir
|
|
18
|
-
const test_cfg_path = path.resolve('test')
|
|
19
|
-
|
|
20
|
-
this.server = require('../server')
|
|
21
|
-
this.config = require('haraka-config').module_config(test_cfg_path)
|
|
22
|
-
this.server.logger.loglevel = 6 // INFO
|
|
23
|
-
|
|
24
|
-
// set the default path for the plugin loader
|
|
25
|
-
this.server.config = this.config.module_config(test_cfg_path)
|
|
26
|
-
this.server.plugins.config = this.config.module_config(test_cfg_path)
|
|
27
|
-
// this.server.outbound.config = this.config.module_config(this_cfg_path);
|
|
28
|
-
|
|
29
|
-
this.server.load_smtp_ini()
|
|
30
|
-
this.server.cfg.main.listen = ip_port
|
|
31
|
-
this.server.cfg.main.smtps_port = 2465
|
|
14
|
+
// ─── CRAM-MD5 helper ──────────────────────────────────────────────────────────
|
|
32
15
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
})
|
|
16
|
+
/** Compute a CRAM-MD5 response to a server challenge. */
|
|
17
|
+
const cramMd5Response = (user, pass, challenge) => {
|
|
18
|
+
const decoded = Buffer.from(challenge, 'base64').toString()
|
|
19
|
+
const hmac = createHmac('md5', pass).update(decoded).digest('hex')
|
|
20
|
+
return Buffer.from(`${user} ${hmac}`).toString('base64')
|
|
39
21
|
}
|
|
40
22
|
|
|
41
|
-
|
|
42
|
-
delete process.env.YES_REALLY_DO_DISCARD
|
|
43
|
-
delete process.env.HARAKA_TEST_DIR
|
|
44
|
-
this.server.stopListeners()
|
|
45
|
-
this.server.plugins.registered_hooks = {}
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
done()
|
|
48
|
-
}, 200)
|
|
49
|
-
}
|
|
23
|
+
// ─── Server lifecycle helpers ─────────────────────────────────────────────────
|
|
50
24
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
25
|
+
const setupServer = (ip_port) =>
|
|
26
|
+
new Promise((resolve) => {
|
|
27
|
+
process.env.YES_REALLY_DO_DISCARD = '1'
|
|
28
|
+
process.env.HARAKA_TEST_DIR = path.resolve('test')
|
|
29
|
+
const test_cfg_path = path.resolve('test')
|
|
54
30
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
})
|
|
31
|
+
this.server = require('../server')
|
|
32
|
+
this.config = require('haraka-config').module_config(test_cfg_path)
|
|
33
|
+
this.server.logger.loglevel = 6
|
|
34
|
+
this.server.config = this.config.module_config(test_cfg_path)
|
|
35
|
+
this.server.plugins.config = this.config.module_config(test_cfg_path)
|
|
61
36
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
})
|
|
66
|
-
assert.deepEqual(['127.0.0.1:25'], listeners)
|
|
67
|
-
})
|
|
37
|
+
this.server.load_smtp_ini()
|
|
38
|
+
this.server.cfg.main.listen = ip_port
|
|
39
|
+
this.server.cfg.main.smtps_port = 2465
|
|
68
40
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
this.server.load_default_tls_config(() => {
|
|
42
|
+
this.server.createServer({})
|
|
43
|
+
setTimeout(resolve, 200)
|
|
72
44
|
})
|
|
45
|
+
})
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
47
|
+
const tearDownServer = () =>
|
|
48
|
+
new Promise((resolve) => {
|
|
49
|
+
delete process.env.YES_REALLY_DO_DISCARD
|
|
50
|
+
delete process.env.HARAKA_TEST_DIR
|
|
51
|
+
this.server.stopListeners()
|
|
52
|
+
this.server.plugins.registered_hooks = {}
|
|
53
|
+
setTimeout(resolve, 200)
|
|
54
|
+
})
|
|
80
55
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
56
|
+
// ─── SMTP session helper ──────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Deliver a message via smtp_client and return a Promise that resolves on
|
|
60
|
+
* acceptance (dot event) or rejects on any SMTP error (bad_code event).
|
|
61
|
+
*
|
|
62
|
+
* When `user`/`pass` are provided, CRAM-MD5 authentication is performed
|
|
63
|
+
* before sending the message.
|
|
64
|
+
*/
|
|
65
|
+
const sendMessage = ({
|
|
66
|
+
host = '127.0.0.1',
|
|
67
|
+
port,
|
|
68
|
+
from = '<test@haraka.local>',
|
|
69
|
+
to = '<discard@haraka.local>',
|
|
70
|
+
user,
|
|
71
|
+
pass,
|
|
72
|
+
body = 'Hello from smtp_client test',
|
|
73
|
+
} = {}) =>
|
|
74
|
+
new Promise((resolve, reject) => {
|
|
75
|
+
get_client(
|
|
76
|
+
{ notes: {} },
|
|
77
|
+
(client) => {
|
|
78
|
+
let credsSent = false
|
|
79
|
+
|
|
80
|
+
client
|
|
81
|
+
.on('greeting', (cmd) => client.send_command(cmd, host))
|
|
82
|
+
.on('helo', () => {
|
|
83
|
+
if (user && !credsSent) {
|
|
84
|
+
client.authenticating = true
|
|
85
|
+
client.send_command('AUTH', 'CRAM-MD5')
|
|
86
|
+
} else {
|
|
87
|
+
client.send_command('MAIL', `FROM:${from}`)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.on('auth', () => {
|
|
91
|
+
if (client.authenticated) {
|
|
92
|
+
client.send_command('MAIL', `FROM:${from}`)
|
|
93
|
+
} else if (!credsSent) {
|
|
94
|
+
credsSent = true
|
|
95
|
+
const resp = cramMd5Response(user, pass, client.response[0])
|
|
96
|
+
// Write CRAM-MD5 response directly (no command prefix)
|
|
97
|
+
client.command = 'auth'
|
|
98
|
+
client.response = []
|
|
99
|
+
client.socket.write(`${resp}\r\n`)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.on('mail', () => client.send_command('RCPT', `TO:${to}`))
|
|
103
|
+
.on('rcpt', () => client.send_command('DATA'))
|
|
104
|
+
.on('data', () => {
|
|
105
|
+
const stream = new message.stream({ main: { spool_after: 1024 } }, 'testId')
|
|
106
|
+
stream.on('end', () => client.socket.write('.\r\n'))
|
|
107
|
+
stream.add_line('Subject: test\r\n')
|
|
108
|
+
stream.add_line('\r\n')
|
|
109
|
+
stream.add_line(`${body}\r\n`)
|
|
110
|
+
stream.add_line_end()
|
|
111
|
+
client.start_data(stream)
|
|
112
|
+
})
|
|
113
|
+
.on('dot', () => {
|
|
114
|
+
client.release()
|
|
115
|
+
resolve()
|
|
116
|
+
})
|
|
117
|
+
.on('bad_code', (code, msg) => {
|
|
118
|
+
client.release()
|
|
119
|
+
reject(new Error(`${code} ${msg}`))
|
|
120
|
+
})
|
|
121
|
+
},
|
|
122
|
+
{ host, port, connect_timeout: 5 },
|
|
123
|
+
)
|
|
124
|
+
})
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
const listeners = this.server.get_listen_addrs({ listen: '[::1]' }, 250)
|
|
88
|
-
assert.deepEqual(['[::1]:250'], listeners)
|
|
89
|
-
})
|
|
126
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
90
127
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
describe('server', () => {
|
|
129
|
+
// ── get_listen_addrs ──────────────────────────────────────────────────────
|
|
130
|
+
describe('get_listen_addrs', () => {
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
this.config = require('haraka-config')
|
|
133
|
+
this.server = require('../server')
|
|
96
134
|
})
|
|
97
135
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
const cases = [
|
|
137
|
+
{
|
|
138
|
+
desc: 'IPv4 fully qualified',
|
|
139
|
+
args: [{ listen: '127.0.0.1:25' }],
|
|
140
|
+
expected: ['127.0.0.1:25'],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
desc: 'IPv4, default port',
|
|
144
|
+
args: [{ listen: '127.0.0.1' }],
|
|
145
|
+
expected: ['127.0.0.1:25'],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
desc: 'IPv4, custom port',
|
|
149
|
+
args: [{ listen: '127.0.0.1' }, 250],
|
|
150
|
+
expected: ['127.0.0.1:250'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
desc: 'IPv6 fully qualified',
|
|
154
|
+
args: [{ listen: '[::1]:25' }],
|
|
155
|
+
expected: ['[::1]:25'],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
desc: 'IPv6, default port',
|
|
159
|
+
args: [{ listen: '[::1]' }],
|
|
160
|
+
expected: ['[::1]:25'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
desc: 'IPv6, custom port',
|
|
164
|
+
args: [{ listen: '[::1]' }, 250],
|
|
165
|
+
expected: ['[::1]:250'],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
desc: 'IPv4 & IPv6 fully qualified',
|
|
169
|
+
args: [{ listen: '127.0.0.1:25,[::1]:25' }],
|
|
170
|
+
expected: ['127.0.0.1:25', '[::1]:25'],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
desc: 'IPv4 & IPv6, default port',
|
|
174
|
+
args: [{ listen: '127.0.0.1:25,[::1]' }],
|
|
175
|
+
expected: ['127.0.0.1:25', '[::1]:25'],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
desc: 'IPv4 & IPv6, custom port',
|
|
179
|
+
args: [{ listen: '127.0.0.1,[::1]' }, 250],
|
|
180
|
+
expected: ['127.0.0.1:250', '[::1]:250'],
|
|
181
|
+
},
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
for (const { desc, args, expected } of cases) {
|
|
185
|
+
it(desc, () => {
|
|
186
|
+
assert.deepEqual(this.server.get_listen_addrs(...args), expected)
|
|
101
187
|
})
|
|
102
|
-
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('IPv4 & IPv6, custom port', () => {
|
|
106
|
-
const listeners = this.server.get_listen_addrs(
|
|
107
|
-
{
|
|
108
|
-
listen: '127.0.0.1,[::1]',
|
|
109
|
-
},
|
|
110
|
-
250,
|
|
111
|
-
)
|
|
112
|
-
assert.deepEqual(['127.0.0.1:250', '[::1]:250'], listeners)
|
|
113
|
-
})
|
|
188
|
+
}
|
|
114
189
|
})
|
|
115
190
|
|
|
191
|
+
// ── load_smtp_ini ─────────────────────────────────────────────────────────
|
|
116
192
|
describe('load_smtp_ini', () => {
|
|
117
|
-
beforeEach(
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
this.config = require('haraka-config')
|
|
195
|
+
this.server = require('../server')
|
|
196
|
+
})
|
|
118
197
|
|
|
119
198
|
it('saves settings to Server.cfg', () => {
|
|
120
199
|
this.server.load_smtp_ini()
|
|
121
|
-
// console.log(this.server.cfg);
|
|
122
200
|
const c = this.server.cfg.main
|
|
123
201
|
assert.notEqual(c.daemonize, undefined)
|
|
124
202
|
assert.notEqual(c.daemon_log_file, undefined)
|
|
@@ -126,387 +204,154 @@ describe('server', () => {
|
|
|
126
204
|
})
|
|
127
205
|
})
|
|
128
206
|
|
|
207
|
+
// ── get_smtp_server ───────────────────────────────────────────────────────
|
|
129
208
|
describe('get_smtp_server', () => {
|
|
130
|
-
beforeEach((
|
|
131
|
-
this.config = require('haraka-config')
|
|
132
|
-
this.config = this.config.module_config(path.resolve('test'))
|
|
133
|
-
|
|
209
|
+
beforeEach(async () => {
|
|
210
|
+
this.config = require('haraka-config').module_config(path.resolve('test'))
|
|
134
211
|
this.server = require('../server')
|
|
135
212
|
this.server.config = this.config
|
|
136
213
|
this.server.plugins.config = this.config
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
done()
|
|
141
|
-
}, 200)
|
|
214
|
+
await new Promise((resolve) => {
|
|
215
|
+
this.server.load_default_tls_config(() => setTimeout(resolve, 200))
|
|
142
216
|
})
|
|
143
217
|
})
|
|
144
218
|
|
|
145
|
-
it('gets a net server object', (
|
|
146
|
-
this.server.get_smtp_server(endpoint('0.0.0.0:2501'), 10)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
done()
|
|
156
|
-
})
|
|
157
|
-
})
|
|
219
|
+
it('gets a net server object', async () => {
|
|
220
|
+
const server = await this.server.get_smtp_server(endpoint('0.0.0.0:2501'), 10)
|
|
221
|
+
if (!server) {
|
|
222
|
+
if (process.env.CI) return
|
|
223
|
+
assert.fail('unable to bind to 0.0.0.0:2501')
|
|
224
|
+
}
|
|
225
|
+
assert.ok(server)
|
|
226
|
+
assert.equal(server.has_tls, false)
|
|
227
|
+
const count = await new Promise((res) => server.getConnections((err, n) => res(n)))
|
|
228
|
+
assert.equal(count, 0)
|
|
158
229
|
})
|
|
159
230
|
|
|
160
|
-
it('gets a TLS net server object', (
|
|
231
|
+
it('gets a TLS net server object', async () => {
|
|
161
232
|
this.server.cfg.main.smtps_port = 2502
|
|
162
|
-
this.server.get_smtp_server(endpoint('0.0.0.0:2502'), 10)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
done()
|
|
172
|
-
})
|
|
173
|
-
})
|
|
233
|
+
const server = await this.server.get_smtp_server(endpoint('0.0.0.0:2502'), 10)
|
|
234
|
+
if (!server) {
|
|
235
|
+
if (process.env.CI) return
|
|
236
|
+
assert.fail('unable to bind to 0.0.0.0:2502')
|
|
237
|
+
}
|
|
238
|
+
assert.ok(server)
|
|
239
|
+
assert.equal(server.has_tls, true)
|
|
240
|
+
const count = await new Promise((res) => server.getConnections((err, n) => res(n)))
|
|
241
|
+
assert.equal(count, 0)
|
|
174
242
|
})
|
|
175
243
|
})
|
|
176
244
|
|
|
245
|
+
// ── get_http_docroot ──────────────────────────────────────────────────────
|
|
177
246
|
describe('get_http_docroot', () => {
|
|
178
|
-
beforeEach(
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
this.config = require('haraka-config')
|
|
249
|
+
this.server = require('../server')
|
|
250
|
+
})
|
|
179
251
|
|
|
180
252
|
it('gets a fs path', () => {
|
|
181
253
|
assert.ok(this.server.get_http_docroot())
|
|
182
254
|
})
|
|
183
255
|
})
|
|
184
256
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
afterEach(_tearDownServer)
|
|
191
|
-
|
|
192
|
-
it('accepts SMTP message', () => {
|
|
193
|
-
const server = { notes: {} }
|
|
194
|
-
const cfg = {
|
|
195
|
-
connect_timeout: 2,
|
|
196
|
-
}
|
|
257
|
+
// ── SMTP sessions ─────────────────────────────────────────────────────────
|
|
258
|
+
describe('SMTP sessions', () => {
|
|
259
|
+
beforeEach(async () => setupServer('127.0.0.1:2503'))
|
|
260
|
+
afterEach(async () => tearDownServer())
|
|
197
261
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
smtp_client.get_client(
|
|
201
|
-
server,
|
|
202
|
-
(client) => {
|
|
203
|
-
client
|
|
204
|
-
.on('greeting', (command) => {
|
|
205
|
-
client.send_command('HELO', 'haraka.local')
|
|
206
|
-
})
|
|
207
|
-
.on('helo', () => {
|
|
208
|
-
client.send_command('MAIL', 'FROM:<test@haraka.local>')
|
|
209
|
-
})
|
|
210
|
-
.on('mail', () => {
|
|
211
|
-
client.send_command('RCPT', 'TO:<nobody-will-see-this@haraka.local>')
|
|
212
|
-
})
|
|
213
|
-
.on('rcpt', () => {
|
|
214
|
-
client.send_command('DATA')
|
|
215
|
-
})
|
|
216
|
-
.on('data', () => {
|
|
217
|
-
const message_stream = new message.stream({ main: { spool_after: 1024 } }, 'theMessageId')
|
|
218
|
-
|
|
219
|
-
message_stream.on('end', () => {
|
|
220
|
-
client.socket.write('.\r\n')
|
|
221
|
-
})
|
|
222
|
-
message_stream.add_line('Header: test\r\n')
|
|
223
|
-
message_stream.add_line('\r\n')
|
|
224
|
-
message_stream.add_line('I am body text\r\n')
|
|
225
|
-
message_stream.add_line_end()
|
|
226
|
-
|
|
227
|
-
client.start_data(message_stream)
|
|
228
|
-
})
|
|
229
|
-
.on('dot', () => {
|
|
230
|
-
assert.ok(1)
|
|
231
|
-
client.release()
|
|
232
|
-
})
|
|
233
|
-
.on('bad_code', (code, msg) => {
|
|
234
|
-
client.release()
|
|
235
|
-
})
|
|
236
|
-
},
|
|
237
|
-
{ port: 2500, host: 'localhost', cfg },
|
|
238
|
-
)
|
|
262
|
+
it('accepts plain SMTP message', async () => {
|
|
263
|
+
await sendMessage({ port: 2503 })
|
|
239
264
|
})
|
|
240
|
-
})
|
|
241
265
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
_setupServer('127.0.0.1:2503', done)
|
|
266
|
+
it('accepts CRAM-MD5 authenticated SMTP', async () => {
|
|
267
|
+
await sendMessage({ port: 2503, user: 'matt', pass: 'goodPass' })
|
|
245
268
|
})
|
|
246
269
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
it('accepts SMTP message', (done) => {
|
|
250
|
-
const nodemailer = require('nodemailer')
|
|
251
|
-
const transporter = nodemailer.createTransport({
|
|
252
|
-
host: '127.0.0.1',
|
|
253
|
-
port: 2503,
|
|
254
|
-
tls: {
|
|
255
|
-
// do not fail on invalid certs
|
|
256
|
-
rejectUnauthorized: false,
|
|
257
|
-
},
|
|
258
|
-
})
|
|
259
|
-
transporter.sendMail(
|
|
260
|
-
{
|
|
261
|
-
from: '"Testalicious Matt" <harakamail@gmail.com>',
|
|
262
|
-
to: 'nobody-will-see-this@haraka.local',
|
|
263
|
-
envelope: {
|
|
264
|
-
from: 'Haraka Test <test@haraka.local>',
|
|
265
|
-
to: 'Discard Queue <discard@haraka.local>',
|
|
266
|
-
},
|
|
267
|
-
subject: 'Hello ✔',
|
|
268
|
-
text: 'Hello world ?',
|
|
269
|
-
html: '<b>Hello world ?</b>',
|
|
270
|
-
},
|
|
271
|
-
(error, info) => {
|
|
272
|
-
if (error) {
|
|
273
|
-
console.log(error)
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
assert.deepEqual(info.accepted, ['discard@haraka.local'])
|
|
277
|
-
console.log(`Message sent: ${info.response}`)
|
|
278
|
-
done()
|
|
279
|
-
},
|
|
280
|
-
)
|
|
270
|
+
it('rejects invalid CRAM-MD5 credentials', async () => {
|
|
271
|
+
await assert.rejects(() => sendMessage({ port: 2503, user: 'matt', pass: 'badPass' }), /5\d\d/)
|
|
281
272
|
})
|
|
282
273
|
|
|
283
|
-
it('accepts
|
|
284
|
-
|
|
285
|
-
const transporter = nodemailer.createTransport({
|
|
286
|
-
host: '127.0.0.1',
|
|
274
|
+
it('accepts message with custom headers', async () => {
|
|
275
|
+
await sendMessage({
|
|
287
276
|
port: 2503,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
},
|
|
292
|
-
requireTLS: true,
|
|
293
|
-
tls: {
|
|
294
|
-
// do not fail on invalid certs
|
|
295
|
-
rejectUnauthorized: false,
|
|
296
|
-
},
|
|
277
|
+
from: '<sender@haraka.local>',
|
|
278
|
+
to: '<discard@haraka.local>',
|
|
279
|
+
body: 'X-Custom: test-value\r\n\r\nBody text',
|
|
297
280
|
})
|
|
298
|
-
|
|
299
|
-
transporter.sendMail(
|
|
300
|
-
{
|
|
301
|
-
from: '"Testalicious Matt" <harakamail@gmail.com>',
|
|
302
|
-
to: 'nobody-will-see-this@haraka.local',
|
|
303
|
-
envelope: {
|
|
304
|
-
from: 'Haraka Test <test@haraka.local>',
|
|
305
|
-
to: 'Discard Queue <discard@haraka.local>',
|
|
306
|
-
},
|
|
307
|
-
subject: 'Hello ✔',
|
|
308
|
-
text: 'Hello world ?',
|
|
309
|
-
html: '<b>Hello world ?</b>',
|
|
310
|
-
},
|
|
311
|
-
(error, info) => {
|
|
312
|
-
if (error) {
|
|
313
|
-
console.log(error)
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
assert.deepEqual(info.accepted, ['discard@haraka.local'])
|
|
317
|
-
console.log(`Message sent: ${info.response}`)
|
|
318
|
-
done()
|
|
319
|
-
},
|
|
320
|
-
)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('rejects invalid auth', (done) => {
|
|
324
|
-
const nodemailer = require('nodemailer')
|
|
325
|
-
const transporter = nodemailer.createTransport({
|
|
326
|
-
host: '127.0.0.1',
|
|
327
|
-
port: 2503,
|
|
328
|
-
auth: {
|
|
329
|
-
user: 'matt',
|
|
330
|
-
pass: 'badPass',
|
|
331
|
-
},
|
|
332
|
-
tls: {
|
|
333
|
-
// do not fail on invalid certs
|
|
334
|
-
rejectUnauthorized: false,
|
|
335
|
-
},
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
transporter.sendMail(
|
|
339
|
-
{
|
|
340
|
-
from: '"Testalicious Matt" <harakamail@gmail.com>',
|
|
341
|
-
to: 'nobody-will-see-this@haraka.local',
|
|
342
|
-
envelope: {
|
|
343
|
-
from: 'Haraka Test <test@haraka.local>',
|
|
344
|
-
to: 'Discard Queue <discard@haraka.local>',
|
|
345
|
-
},
|
|
346
|
-
subject: 'Hello ✔',
|
|
347
|
-
text: 'Hello world ?',
|
|
348
|
-
html: '<b>Hello world ?</b>',
|
|
349
|
-
},
|
|
350
|
-
(error, info) => {
|
|
351
|
-
if (error) {
|
|
352
|
-
assert.equal(error.code, 'EAUTH')
|
|
353
|
-
// console.log(error);
|
|
354
|
-
return done()
|
|
355
|
-
}
|
|
356
|
-
console.log(info.response)
|
|
357
|
-
done()
|
|
358
|
-
},
|
|
359
|
-
)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('DKIM validates signed message', (done) => {
|
|
363
|
-
const nodemailer = require('nodemailer')
|
|
364
|
-
const transporter = nodemailer.createTransport({
|
|
365
|
-
host: '127.0.0.1',
|
|
366
|
-
port: 2503,
|
|
367
|
-
tls: {
|
|
368
|
-
// do not fail on invalid certs
|
|
369
|
-
rejectUnauthorized: false,
|
|
370
|
-
},
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
transporter.sendMail(
|
|
374
|
-
{
|
|
375
|
-
from: '"Testalicious Matt" <harakamail@gmail.com>',
|
|
376
|
-
to: 'nobody-will-see-this@haraka.local',
|
|
377
|
-
envelope: {
|
|
378
|
-
from: 'Haraka Test <test@haraka.local>',
|
|
379
|
-
to: 'Discard Queue <discard@haraka.local>',
|
|
380
|
-
},
|
|
381
|
-
subject: 'Hello ✔',
|
|
382
|
-
text: 'Hello world ?',
|
|
383
|
-
html: '<b>Hello world ?</b>',
|
|
384
|
-
dkim: {
|
|
385
|
-
domainName: 'test.simerson.com',
|
|
386
|
-
keySelector: 'harakatest2017',
|
|
387
|
-
privateKey:
|
|
388
|
-
'-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxqoUAnQ9GB3iNnkS7coj0Iggd0nyryW062tpK95NC5UXmmAwIpUMfkYdiHY2o2duWYGF0Bp237M/QXKhJYTXfsgkwP/bq9OGWtRZxHPHhbhdjbiI\nqObi6zvYcxrI77gpWDDvruhMeS9Hwa1R99pLUWd4PsuYTzbV/jwu2pz+XZXXXNEU\nVxzDAAj0yF7mwxHMLzQfR+hdhWcrgN0stUP0o7hm7hoOP8IWgcSW3JiQYavIKoI4\nm4+I9I1LzDJN2rHVnQvmjUrqqpG7X6SyFVFtuTWGaMqf1Cj/t8eSvU9VdgLFllS8\ntThqUZHq5S5hm8M8VzLuQLG9U0dtFolcFmJkbQIDAQABAoIBAB4fUbNhjpXmihM6\nXm1htfZ7fXi45Kw76me7vJGjPklgTNjidsn3kZJf7UBwtC4ok6nMos6ABMA8fH3e\n9KIst0QI8tG0ucke5INHKWlJKNqUrtK7RTVe9M84HsStLgRzBwnRObZqkJXbXmT2\nc7RCDCOGrcvPsQNpzB6lX3FUVpk3x24RXpQV1qSgH8yuHSPc1C6rssXwPAgnESfS\nK3MHRx2CLZvTTkq/YCsT+wS/O9RWPCVOYuWaa5DDDAIp3Yw1wYq9Upoh0BdIFC3U\nWm+5Cr3o9wxcvS6+W2RA6I51eymzvCU5ZakWt/bnUDb6/ByxsWOn5rL4WfPpCwE4\nnuC72v0CgYEA9imEq6a0GoaEsMoR7cxT7uXKimQH+Jaq3CGkuh0iN32F4FXhuUKz\nLYKSLCZzpb1MiDJv6BBchV6uSQ6ATo1cZ8WzYQISikk175bf0SPom591OZElvKA2\nSOrTrXtbl33YbWZEgyEcpTgelVi5ys9rj4eKkMvM0lwRmW6gctEFXRcCgYEAzpqc\nR/wqPjgPhpF1CZtdEwOZg4kkOig8CBcuQ7o/hDG7N69A9ZbeJO8eD+gKDrHRfkYr\nTH/UdkZGjilBk/lxnpIZpyBLxQ6UdhNPuwtxXKAvuSN+aQ0pdJn8tg03OSj2OzTK\nJ4hMsO/wt1xM8EDRobLZEosMadaYZUHzx8VU5RsCgYEAvFZbuXEcT0cocpLIUOaK\nOTf7VRLfvmSYaUAcZoEv0sDpExDiWPodWO6To8/vn5lL2tCsKiOKhkhAlIjRxkgF\nsSfj7I7HXKJS7/LBX6RXrem8qMTS2JTDs9pnBk5hb3DLjDg4pxNIdWiQjbeKvw8f\nvnr3m30yQqhKlte7Tt15exUCgYBzq7RbyR6Nfy2SFdYE7usJPjawohOaS/RwQyov\n2RK+nGlJH+GqnjD5VLbsCOm4mG3F2NtdFSSKo4XVCdwhUMMAGKQsIbTKOwN7qAw3\nmIx7Y2PUr76SakAPfDc0ZenJItnZBBE6WOE3Ht8Siaa5zFCRy2QlMZxdlTv1VRt7\neUuyiQKBgQDdXJO5+3h1HPxbYZcmNm/2CJUNw2ehU8vCiBXCcWPn7JukayHx+TXy\nyj0j/b1SvmKgjB+4JWluiqIU+QBjRjvb397QY1YoCEaGZd0zdFjTZwQksQ5AFst9\nCiD9OFXe/kkmIUQQra6aw1CoppyAfvAblp8uevLWb57xU3VUB3xeGg==\n-----END RSA PRIVATE KEY-----\n',
|
|
389
|
-
},
|
|
390
|
-
},
|
|
391
|
-
(error, info) => {
|
|
392
|
-
// console.log(info);
|
|
393
|
-
if (error) {
|
|
394
|
-
console.log(error)
|
|
395
|
-
return
|
|
396
|
-
}
|
|
397
|
-
assert.deepEqual(info.accepted, ['discard@haraka.local'])
|
|
398
|
-
console.log(`Message sent: ${info.response}`)
|
|
399
|
-
done()
|
|
400
|
-
},
|
|
401
|
-
)
|
|
402
281
|
})
|
|
403
282
|
})
|
|
404
283
|
|
|
284
|
+
// ── requireAuthorized: SMTPS (implicit TLS) ───────────────────────────────
|
|
405
285
|
describe('requireAuthorized_SMTPS', () => {
|
|
406
|
-
beforeEach((
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
secure: true,
|
|
418
|
-
tls: {
|
|
419
|
-
// do not fail on invalid certs
|
|
286
|
+
beforeEach(async () => setupServer('127.0.0.1:2465'))
|
|
287
|
+
afterEach(async () => tearDownServer())
|
|
288
|
+
|
|
289
|
+
it('rejects non-validated SMTPS connection', async () => {
|
|
290
|
+
// Port 2465 is configured as SMTPS with requireAuthorized.
|
|
291
|
+
// In TLSv1.3 the handshake completes (secureConnect fires), then the server
|
|
292
|
+
// sends a post-handshake "certificate required" alert as a socket error.
|
|
293
|
+
const err = await new Promise((resolve) => {
|
|
294
|
+
const socket = tls.connect({
|
|
295
|
+
host: '127.0.0.1',
|
|
296
|
+
port: 2465,
|
|
420
297
|
rejectUnauthorized: false,
|
|
421
|
-
}
|
|
298
|
+
})
|
|
299
|
+
socket.on('error', resolve)
|
|
300
|
+
// secureConnect may fire before the post-handshake alert; keep waiting.
|
|
301
|
+
socket.on('secureConnect', () => {})
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
socket.destroy()
|
|
304
|
+
resolve(new Error('timeout'))
|
|
305
|
+
}, 3000)
|
|
422
306
|
})
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
{
|
|
428
|
-
from: '"Testalicious Matt" <harakamail@gmail.com>',
|
|
429
|
-
to: 'nobody-will-see-this@haraka.local',
|
|
430
|
-
envelope: {
|
|
431
|
-
from: 'Haraka Test <test@haraka.local>',
|
|
432
|
-
to: 'Discard Queue <discard@haraka.local>',
|
|
433
|
-
},
|
|
434
|
-
subject: 'Hello ✔',
|
|
435
|
-
text: 'Hello world ?',
|
|
436
|
-
html: '<b>Hello world ?</b>',
|
|
437
|
-
},
|
|
438
|
-
(error, info) => {
|
|
439
|
-
if (error) {
|
|
440
|
-
// console.log(error);
|
|
441
|
-
if (error.message === 'socket hang up') {
|
|
442
|
-
// node 6 & 8
|
|
443
|
-
assert.equal(error.message, 'socket hang up')
|
|
444
|
-
} else if (/alert certificate required/.test(error.message)) {
|
|
445
|
-
// node 18
|
|
446
|
-
assert.ok(/alert certificate required/.test(error.message))
|
|
447
|
-
} else {
|
|
448
|
-
// node 10+
|
|
449
|
-
assert.equal(
|
|
450
|
-
error.message,
|
|
451
|
-
'Client network socket disconnected before secure TLS connection was established',
|
|
452
|
-
)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
done()
|
|
456
|
-
},
|
|
457
|
-
)
|
|
458
|
-
}, 500)
|
|
307
|
+
assert.ok(
|
|
308
|
+
/socket hang up|disconnected before secure TLS|alert certificate required/.test(err.message),
|
|
309
|
+
`unexpected error: ${err.message}`,
|
|
310
|
+
)
|
|
459
311
|
})
|
|
460
312
|
})
|
|
461
313
|
|
|
314
|
+
// ── requireAuthorized: STARTTLS ───────────────────────────────────────────
|
|
462
315
|
describe('requireAuthorized_STARTTLS', () => {
|
|
463
|
-
beforeEach((
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
},
|
|
493
|
-
(error, info) => {
|
|
494
|
-
if (error) {
|
|
495
|
-
// console.log(error);
|
|
496
|
-
if (/alert certificate required/.test(error.message)) {
|
|
497
|
-
// node 18
|
|
498
|
-
assert.ok(/alert certificate required/.test(error.message))
|
|
499
|
-
} else {
|
|
500
|
-
assert.equal(
|
|
501
|
-
error.message,
|
|
502
|
-
'Client network socket disconnected before secure TLS connection was established',
|
|
503
|
-
)
|
|
504
|
-
}
|
|
316
|
+
beforeEach(async () => setupServer('127.0.0.1:2587'))
|
|
317
|
+
afterEach(async () => tearDownServer())
|
|
318
|
+
|
|
319
|
+
it('rejects non-validated STARTTLS connection', async () => {
|
|
320
|
+
// Port 2587 is plain SMTP; requireAuthorized enforces mutual TLS on STARTTLS upgrade.
|
|
321
|
+
// In TLSv1.3 secureConnect fires first, then the server sends a post-handshake
|
|
322
|
+
// "certificate required" alert. Use raw sockets to observe the TLS error.
|
|
323
|
+
// (smtp_client's upgrade path silently swallows the post-upgrade error.)
|
|
324
|
+
const err = await new Promise((resolve) => {
|
|
325
|
+
const sock = net.connect({ host: '127.0.0.1', port: 2587 })
|
|
326
|
+
let state = 'greeting'
|
|
327
|
+
let buf = ''
|
|
328
|
+
sock.on('data', (d) => {
|
|
329
|
+
buf += d.toString()
|
|
330
|
+
for (const line of buf.split('\r\n').slice(0, -1)) {
|
|
331
|
+
buf = buf.slice(line.length + 2)
|
|
332
|
+
if (line[3] === '-') continue // multi-line continuation
|
|
333
|
+
if (state === 'greeting') {
|
|
334
|
+
sock.write('EHLO test\r\n')
|
|
335
|
+
state = 'ehlo'
|
|
336
|
+
} else if (state === 'ehlo') {
|
|
337
|
+
sock.write('STARTTLS\r\n')
|
|
338
|
+
state = 'starttls'
|
|
339
|
+
} else if (state === 'starttls') {
|
|
340
|
+
state = 'tls'
|
|
341
|
+
const cleartext = tls.connect({ socket: sock, rejectUnauthorized: false })
|
|
342
|
+
cleartext.on('secureConnect', () => {})
|
|
343
|
+
cleartext.on('error', resolve)
|
|
344
|
+
cleartext.on('close', () => resolve(new Error('closed without error')))
|
|
505
345
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
)
|
|
509
|
-
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
sock.on('error', resolve)
|
|
349
|
+
setTimeout(() => resolve(new Error('timeout')), 3000)
|
|
350
|
+
})
|
|
351
|
+
assert.ok(
|
|
352
|
+
/alert certificate required|socket hang up|disconnected/.test(err.message),
|
|
353
|
+
`unexpected error: ${err.message}`,
|
|
354
|
+
)
|
|
510
355
|
})
|
|
511
356
|
})
|
|
512
357
|
})
|