Haraka 3.1.2 → 3.1.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/.prettierignore +2 -0
- package/CONTRIBUTORS.md +24 -2
- package/Changes.md +48 -0
- package/Plugins.md +81 -64
- package/README.md +1 -1
- package/bin/haraka +9 -7
- package/config/connection.ini +10 -0
- package/config/smtp.ini +0 -9
- package/connection.js +15 -19
- package/docs/CoreConfig.md +2 -3
- package/docs/Plugins.md +1 -1
- package/docs/plugins/aliases.md +0 -2
- package/docs/plugins/queue/qmail-queue.md +0 -1
- package/docs/tutorials/Migrating_from_v1_to_v2.md +1 -1
- package/logger.js +2 -2
- package/outbound/client_pool.js +1 -1
- package/outbound/hmail.js +76 -83
- package/outbound/index.js +36 -34
- package/outbound/queue.js +231 -176
- package/package.json +29 -31
- package/plugins/prevent_credential_leaks.js +2 -2
- package/plugins/process_title.js +1 -1
- package/plugins/queue/smtp_forward.js +1 -1
- 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 +20 -2
- package/server.js +15 -10
- package/smtp_client.js +2 -9
- package/test/config/tls/haraka.local.pem +47 -47
- package/test/connection.js +286 -147
- package/test/fixtures/line_socket.js +1 -0
- package/test/fixtures/util_hmailitem.js +1 -1
- package/test/outbound/bounce_net_errors.js +176 -0
- package/test/outbound/bounce_rfc3464.js +303 -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/queue/smtp_forward.js +1 -1
- package/test/plugins/record_envelope_addresses.js +93 -0
- package/test/plugins/tls.js +2 -2
- package/test/plugins/xclient.js +137 -0
- package/test/rfc1869.js +43 -0
- package/test/smtp_client.js +6 -6
- package/test/transaction.js +486 -201
- package/tls_socket.js +3 -3
- package/transaction.js +33 -10
- package/config/me +0 -1
- package/config/rabbitmq.ini +0 -10
- package/config/rabbitmq_amqplib.ini +0 -19
- package/config/tls_cert.pem +0 -23
- package/config/tls_key.pem +0 -28
- 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/queue/multibyte +0 -0
- package/test/queue/plain +0 -0
- package/test/test-queue/delete-me +0 -0
- package/test/tls_socket.js +0 -273
package/test/outbound/qfile.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
1
4
|
const assert = require('node:assert')
|
|
2
5
|
const os = require('node:os')
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
describe('qfile', () => {
|
|
6
|
-
beforeEach((done) => {
|
|
7
|
-
this.qfile = require('../../outbound/qfile')
|
|
8
|
-
done()
|
|
9
|
-
})
|
|
7
|
+
const qfile = require('../../outbound/qfile')
|
|
10
8
|
|
|
9
|
+
describe('outbound/qfile', () => {
|
|
10
|
+
describe('name', () => {
|
|
11
11
|
it('name() basic functions', () => {
|
|
12
|
-
const name =
|
|
12
|
+
const name = qfile.name()
|
|
13
13
|
const split = name.split('_')
|
|
14
14
|
assert.equal(split.length, 7)
|
|
15
15
|
assert.equal(split[2], 0)
|
|
@@ -25,7 +25,7 @@ describe('qfile', () => {
|
|
|
25
25
|
uid: 'XXYYZZ',
|
|
26
26
|
host: os.hostname(),
|
|
27
27
|
}
|
|
28
|
-
const name =
|
|
28
|
+
const name = qfile.name(overrides)
|
|
29
29
|
const split = name.split('_')
|
|
30
30
|
assert.equal(split.length, 7)
|
|
31
31
|
assert.equal(split[0], overrides.arrival)
|
|
@@ -38,9 +38,9 @@ describe('qfile', () => {
|
|
|
38
38
|
|
|
39
39
|
it('rnd_unique() is unique-ish', () => {
|
|
40
40
|
const repeats = 1000
|
|
41
|
-
const u =
|
|
41
|
+
const u = qfile.rnd_unique()
|
|
42
42
|
for (let i = 0; i < repeats; i++) {
|
|
43
|
-
assert.notEqual(u,
|
|
43
|
+
assert.notEqual(u, qfile.rnd_unique())
|
|
44
44
|
}
|
|
45
45
|
})
|
|
46
46
|
})
|
|
@@ -49,7 +49,7 @@ describe('qfile', () => {
|
|
|
49
49
|
it('parts() updates previous queue filenames', () => {
|
|
50
50
|
// $nextattempt_$attempts_$pid_$uniq.$host
|
|
51
51
|
const name = '1111_0_2222_3333.foo.example.com'
|
|
52
|
-
const parts =
|
|
52
|
+
const parts = qfile.parts(name)
|
|
53
53
|
assert.equal(parts.next_attempt, 1111)
|
|
54
54
|
assert.equal(parts.attempts, 0)
|
|
55
55
|
assert.equal(parts.pid, 2222)
|
|
@@ -65,8 +65,8 @@ describe('qfile', () => {
|
|
|
65
65
|
uid: 'XXYYZZ',
|
|
66
66
|
host: os.hostname(),
|
|
67
67
|
}
|
|
68
|
-
const name =
|
|
69
|
-
const parts =
|
|
68
|
+
const name = qfile.name(overrides)
|
|
69
|
+
const parts = qfile.parts(name)
|
|
70
70
|
assert.equal(parts.arrival, overrides.arrival)
|
|
71
71
|
assert.equal(parts.next_attempt, overrides.next_attempt)
|
|
72
72
|
assert.equal(parts.attempts, overrides.attempts)
|
|
@@ -75,8 +75,8 @@ describe('qfile', () => {
|
|
|
75
75
|
assert.equal(parts.host, overrides.host)
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it('handles 4', () => {
|
|
79
|
-
const r =
|
|
78
|
+
it('handles 4-part legacy filename', () => {
|
|
79
|
+
const r = qfile.parts('1484878079415_0_12345_8888.mta1.example.com')
|
|
80
80
|
delete r.arrival
|
|
81
81
|
delete r.uid
|
|
82
82
|
delete r.counter
|
|
@@ -89,8 +89,8 @@ describe('qfile', () => {
|
|
|
89
89
|
})
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it('handles 7', () => {
|
|
93
|
-
const r =
|
|
92
|
+
it('handles 7-part standard filename', () => {
|
|
93
|
+
const r = qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz_1_haraka')
|
|
94
94
|
delete r.age
|
|
95
95
|
assert.deepEqual(r, {
|
|
96
96
|
arrival: 1516650518128,
|
|
@@ -103,22 +103,22 @@ describe('qfile', () => {
|
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
-
it('punts on 5', () => {
|
|
107
|
-
assert.deepEqual(
|
|
106
|
+
it('punts on 5-part filename', () => {
|
|
107
|
+
assert.deepEqual(qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz'), null)
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
describe('hostname', () => {
|
|
112
|
-
it('
|
|
113
|
-
assert.deepEqual(
|
|
112
|
+
it('defaults to os.hostname()', () => {
|
|
113
|
+
assert.deepEqual(qfile.hostname(), os.hostname())
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
it('
|
|
117
|
-
assert.deepEqual(
|
|
116
|
+
it('replaces backslash char', () => {
|
|
117
|
+
assert.deepEqual(qfile.hostname('mt\\a1.exam\\ple.com'), 'mt\\057a1.exam\\057ple.com')
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
-
it('
|
|
121
|
-
assert.deepEqual(
|
|
120
|
+
it('replaces underscore char', () => {
|
|
121
|
+
assert.deepEqual(qfile.hostname('mt_a1.exam_ple.com'), 'mt\\137a1.exam\\137ple.com')
|
|
122
122
|
})
|
|
123
123
|
})
|
|
124
124
|
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
4
|
+
const assert = require('node:assert')
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const os = require('node:os')
|
|
8
|
+
|
|
9
|
+
const queue = require('../../outbound/queue')
|
|
10
|
+
const qfile = require('../../outbound/qfile')
|
|
11
|
+
|
|
12
|
+
const sourceQueueDir = path.join('test', 'queue')
|
|
13
|
+
const testQueueDir = path.join('test', 'test-queue')
|
|
14
|
+
const fixtureFiles = [
|
|
15
|
+
'1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
|
|
16
|
+
'1508269674999_1508269674999_0_34002_socVUF_1_haraka',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const clearTestQueue = () => {
|
|
20
|
+
fs.mkdirSync(testQueueDir, { recursive: true })
|
|
21
|
+
for (const file of fs.readdirSync(testQueueDir)) {
|
|
22
|
+
fs.unlinkSync(path.join(testQueueDir, file))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const populateTestQueue = () => {
|
|
27
|
+
clearTestQueue()
|
|
28
|
+
for (const file of fixtureFiles) {
|
|
29
|
+
fs.copyFileSync(path.join(sourceQueueDir, file), path.join(testQueueDir, file))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('outbound/queue', () => {
|
|
34
|
+
describe('read_parts', () => {
|
|
35
|
+
it('parses valid queue filenames', () => {
|
|
36
|
+
const filename = '1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka'
|
|
37
|
+
const parts = queue.read_parts(filename)
|
|
38
|
+
assert.ok(parts)
|
|
39
|
+
assert.equal(parts.arrival, 1507509981169)
|
|
40
|
+
assert.equal(parts.next_attempt, 1507509981169)
|
|
41
|
+
assert.equal(parts.attempts, 0)
|
|
42
|
+
assert.equal(parts.pid, 61403)
|
|
43
|
+
assert.equal(parts.uid, 'e0Y0Ym')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('rejects dot files', () => {
|
|
47
|
+
assert.strictEqual(queue.read_parts('__tmp__.filename'), false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('rejects error files', () => {
|
|
51
|
+
assert.strictEqual(queue.read_parts('error.something'), false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('rejects invalid queue files', () => {
|
|
55
|
+
assert.strictEqual(queue.read_parts('invalid-file'), false)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('load_queue_files', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
populateTestQueue()
|
|
62
|
+
})
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
clearTestQueue()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('processes valid queue files', async () => {
|
|
68
|
+
const seen = []
|
|
69
|
+
|
|
70
|
+
const files = await queue.load_queue_files(
|
|
71
|
+
null,
|
|
72
|
+
['1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka'],
|
|
73
|
+
(file) => {
|
|
74
|
+
seen.push(file)
|
|
75
|
+
return file
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
assert.equal(seen.length, 1)
|
|
79
|
+
assert.equal(files[0], '1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('skips invalid files', async () => {
|
|
83
|
+
const seen = []
|
|
84
|
+
|
|
85
|
+
await queue.load_queue_files(
|
|
86
|
+
null,
|
|
87
|
+
['1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka', 'invalid-file', 'zero-length'],
|
|
88
|
+
(file) => {
|
|
89
|
+
seen.push(file)
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
assert.equal(seen.length, 1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('filters files by pid', async () => {
|
|
96
|
+
let renameAttempts = 0
|
|
97
|
+
|
|
98
|
+
const originalRename = queue.rename_to_actual_pid
|
|
99
|
+
queue.rename_to_actual_pid = (_file, _parts) => {
|
|
100
|
+
renameAttempts++
|
|
101
|
+
throw new Error('test skip')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await queue.load_queue_files(
|
|
105
|
+
61403,
|
|
106
|
+
[
|
|
107
|
+
'1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka',
|
|
108
|
+
'1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
|
|
109
|
+
],
|
|
110
|
+
(_file) => {},
|
|
111
|
+
)
|
|
112
|
+
queue.rename_to_actual_pid = originalRename
|
|
113
|
+
assert.equal(renameAttempts, 1)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('ensure_queue_dir', () => {
|
|
118
|
+
it('creates queue dir', async () => {
|
|
119
|
+
const tmpDir = path.join(os.tmpdir(), `haraka-test-queue-${Date.now()}`)
|
|
120
|
+
|
|
121
|
+
const originalQueueDir = queue.queue_dir
|
|
122
|
+
queue.queue_dir = tmpDir
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await queue.ensure_queue_dir()
|
|
126
|
+
assert.ok(fs.existsSync(tmpDir))
|
|
127
|
+
const stat = await fs.promises.stat(tmpDir)
|
|
128
|
+
assert.ok(stat.isDirectory())
|
|
129
|
+
} catch (err) {
|
|
130
|
+
assert.fail(`ensure_queue_dir threw an error: ${err.message}`)
|
|
131
|
+
} finally {
|
|
132
|
+
queue.queue_dir = originalQueueDir
|
|
133
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true })
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('returns early if queue dir already exists', async () => {
|
|
138
|
+
const tmpDir = path.join(os.tmpdir(), `haraka-test-queue-exists-${Date.now()}`)
|
|
139
|
+
fs.mkdirSync(tmpDir)
|
|
140
|
+
|
|
141
|
+
const originalQueueDir = queue.queue_dir
|
|
142
|
+
queue.queue_dir = tmpDir
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await queue.ensure_queue_dir()
|
|
146
|
+
assert.ok(fs.existsSync(tmpDir))
|
|
147
|
+
} catch (err) {
|
|
148
|
+
assert.fail(`ensure_queue_dir threw an error: ${err.message}`)
|
|
149
|
+
} finally {
|
|
150
|
+
queue.queue_dir = originalQueueDir
|
|
151
|
+
fs.rmSync(tmpDir, { recursive: true })
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('_load_cur_queue', () => {
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
populateTestQueue()
|
|
159
|
+
})
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
clearTestQueue()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('reads queue directory and processes files', async () => {
|
|
165
|
+
const processedFiles = []
|
|
166
|
+
await queue._load_cur_queue(null, (file) => {
|
|
167
|
+
processedFiles.push(file)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
assert.ok(processedFiles.length >= 0)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('list_queue', () => {
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
populateTestQueue()
|
|
177
|
+
})
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
clearTestQueue()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('returns todo objects from real queue files', async () => {
|
|
183
|
+
const qlist = await queue.list_queue()
|
|
184
|
+
assert.ok(Array.isArray(qlist))
|
|
185
|
+
assert.ok(qlist.length > 0)
|
|
186
|
+
assert.ok(qlist[0].mail_from)
|
|
187
|
+
assert.ok(Array.isArray(qlist[0].rcpt_to))
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('stat_queue', () => {
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
populateTestQueue()
|
|
194
|
+
})
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
clearTestQueue()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('returns queue stats', async () => {
|
|
200
|
+
const stats = await queue.stat_queue()
|
|
201
|
+
assert.ok(stats)
|
|
202
|
+
assert.ok('queue_dir' in stats)
|
|
203
|
+
assert.ok(stats.queue_count >= 1)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('load_pid_queue', () => {
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
populateTestQueue()
|
|
210
|
+
})
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
clearTestQueue()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('delegates pid loading to init_queue', async () => {
|
|
216
|
+
const parts = qfile.parts(fixtureFiles[0])
|
|
217
|
+
const observed = []
|
|
218
|
+
const originalLoadQueue = queue.init_queue
|
|
219
|
+
|
|
220
|
+
queue.init_queue = (pid) => {
|
|
221
|
+
observed.push(pid)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
assert.ok(fs.existsSync(path.join(testQueueDir, fixtureFiles[0])))
|
|
226
|
+
await queue.load_pid_queue(parts.pid)
|
|
227
|
+
assert.deepEqual(observed, [parts.pid])
|
|
228
|
+
} finally {
|
|
229
|
+
queue.init_queue = originalLoadQueue
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
|
|
5
|
+
const { Address } = require('address-rfc2821')
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
const _set_up = (done) => {
|
|
9
|
+
this.plugin = new fixtures.plugin('record_envelope_addresses')
|
|
10
|
+
this.connection = fixtures.connection.createConnection()
|
|
11
|
+
this.connection.init_transaction()
|
|
12
|
+
done()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('record_envelope_addresses', () => {
|
|
16
|
+
beforeEach(_set_up)
|
|
17
|
+
|
|
18
|
+
describe('hook_mail', () => {
|
|
19
|
+
it('adds X-Envelope-From header from MAIL FROM address', (done) => {
|
|
20
|
+
const addr = new Address('<sender@example.com>')
|
|
21
|
+
this.plugin.hook_mail(
|
|
22
|
+
() => {
|
|
23
|
+
const vals = this.connection.transaction.header.get_all('X-Envelope-From')
|
|
24
|
+
assert.equal(vals.length, 1, 'header was added')
|
|
25
|
+
assert.equal(vals[0], 'sender@example.com')
|
|
26
|
+
done()
|
|
27
|
+
},
|
|
28
|
+
this.connection,
|
|
29
|
+
[addr],
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('does not throw when connection has no transaction', (done) => {
|
|
34
|
+
this.connection.transaction = null
|
|
35
|
+
const addr = new Address('<sender@example.com>')
|
|
36
|
+
this.plugin.hook_mail(
|
|
37
|
+
() => {
|
|
38
|
+
assert.ok(true, 'next was called without error')
|
|
39
|
+
done()
|
|
40
|
+
},
|
|
41
|
+
this.connection,
|
|
42
|
+
[addr],
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('hook_rcpt', () => {
|
|
48
|
+
it('adds X-Envelope-To header from RCPT TO address', (done) => {
|
|
49
|
+
const addr = new Address('<rcpt@example.com>')
|
|
50
|
+
this.plugin.hook_rcpt(
|
|
51
|
+
() => {
|
|
52
|
+
const vals = this.connection.transaction.header.get_all('X-Envelope-To')
|
|
53
|
+
assert.equal(vals.length, 1, 'header was added')
|
|
54
|
+
assert.equal(vals[0], 'rcpt@example.com')
|
|
55
|
+
done()
|
|
56
|
+
},
|
|
57
|
+
this.connection,
|
|
58
|
+
[addr],
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('adds X-Envelope-To header for each recipient', (done) => {
|
|
63
|
+
const addr1 = new Address('<one@example.com>')
|
|
64
|
+
const addr2 = new Address('<two@example.com>')
|
|
65
|
+
let calls = 0
|
|
66
|
+
const next = () => {
|
|
67
|
+
calls++
|
|
68
|
+
if (calls === 2) {
|
|
69
|
+
const vals = this.connection.transaction.header.get_all('X-Envelope-To')
|
|
70
|
+
assert.equal(vals.length, 2, 'two headers added')
|
|
71
|
+
assert.equal(vals[0], 'one@example.com')
|
|
72
|
+
assert.equal(vals[1], 'two@example.com')
|
|
73
|
+
done()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.plugin.hook_rcpt(next, this.connection, [addr1])
|
|
77
|
+
this.plugin.hook_rcpt(next, this.connection, [addr2])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('does not throw when connection has no transaction', (done) => {
|
|
81
|
+
this.connection.transaction = null
|
|
82
|
+
const addr = new Address('<rcpt@example.com>')
|
|
83
|
+
this.plugin.hook_rcpt(
|
|
84
|
+
() => {
|
|
85
|
+
assert.ok(true, 'next was called without error')
|
|
86
|
+
done()
|
|
87
|
+
},
|
|
88
|
+
this.connection,
|
|
89
|
+
[addr],
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
package/test/plugins/tls.js
CHANGED
|
@@ -39,9 +39,9 @@ describe('tls', () => {
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
describe('register', () => {
|
|
42
|
-
it('with certs, should
|
|
42
|
+
it('with certs, should register hooks', () => {
|
|
43
43
|
this.plugin.register()
|
|
44
|
-
assert.ok(this.plugin.
|
|
44
|
+
assert.ok(Object.keys(this.plugin.hooks).length)
|
|
45
45
|
})
|
|
46
46
|
})
|
|
47
47
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
|
|
5
|
+
const fixtures = require('haraka-test-fixtures')
|
|
6
|
+
|
|
7
|
+
const _set_up = (done) => {
|
|
8
|
+
this.plugin = new fixtures.plugin('xclient')
|
|
9
|
+
this.connection = fixtures.connection.createConnection()
|
|
10
|
+
this.connection.capabilities = []
|
|
11
|
+
done()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('xclient', () => {
|
|
15
|
+
beforeEach(_set_up)
|
|
16
|
+
|
|
17
|
+
describe('hook_capabilities', () => {
|
|
18
|
+
it('adds XCLIENT capability for allowed IP (127.0.0.1)', (done) => {
|
|
19
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
20
|
+
this.plugin.hook_capabilities(() => {
|
|
21
|
+
assert.ok(
|
|
22
|
+
this.connection.capabilities.some((c) => c.startsWith('XCLIENT')),
|
|
23
|
+
'XCLIENT capability added',
|
|
24
|
+
)
|
|
25
|
+
done()
|
|
26
|
+
}, this.connection)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('adds XCLIENT capability for allowed IP (::1)', (done) => {
|
|
30
|
+
this.connection.remote.ip = '::1'
|
|
31
|
+
this.plugin.hook_capabilities(() => {
|
|
32
|
+
assert.ok(
|
|
33
|
+
this.connection.capabilities.some((c) => c.startsWith('XCLIENT')),
|
|
34
|
+
'XCLIENT capability added for IPv6 loopback',
|
|
35
|
+
)
|
|
36
|
+
done()
|
|
37
|
+
}, this.connection)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('does not add XCLIENT capability for disallowed IP', (done) => {
|
|
41
|
+
this.connection.remote.ip = '10.0.0.1'
|
|
42
|
+
this.plugin.hook_capabilities(() => {
|
|
43
|
+
assert.ok(
|
|
44
|
+
!this.connection.capabilities.some((c) => c.startsWith('XCLIENT')),
|
|
45
|
+
'XCLIENT capability not added',
|
|
46
|
+
)
|
|
47
|
+
done()
|
|
48
|
+
}, this.connection)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('hook_unrecognized_command', () => {
|
|
53
|
+
it('ignores non-XCLIENT commands', (done) => {
|
|
54
|
+
this.plugin.hook_unrecognized_command(
|
|
55
|
+
(code) => {
|
|
56
|
+
assert.equal(code, undefined, 'next called with no args')
|
|
57
|
+
done()
|
|
58
|
+
},
|
|
59
|
+
this.connection,
|
|
60
|
+
['EHLO', 'example.com'],
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('denies XCLIENT when transaction is in progress', (done) => {
|
|
65
|
+
this.connection.init_transaction()
|
|
66
|
+
this.plugin.hook_unrecognized_command(
|
|
67
|
+
(code) => {
|
|
68
|
+
assert.equal(code, DENY, 'denied with transaction in progress')
|
|
69
|
+
done()
|
|
70
|
+
},
|
|
71
|
+
this.connection,
|
|
72
|
+
['XCLIENT', 'ADDR=127.0.0.1'],
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('denies XCLIENT from disallowed IP', (done) => {
|
|
77
|
+
this.connection.remote.ip = '10.0.0.1'
|
|
78
|
+
this.plugin.hook_unrecognized_command(
|
|
79
|
+
(code) => {
|
|
80
|
+
assert.equal(code, DENY, 'denied from non-allowed IP')
|
|
81
|
+
done()
|
|
82
|
+
},
|
|
83
|
+
this.connection,
|
|
84
|
+
['XCLIENT', 'ADDR=127.0.0.2'],
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('denies XCLIENT with no valid IP address', (done) => {
|
|
89
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
90
|
+
this.plugin.hook_unrecognized_command(
|
|
91
|
+
(code) => {
|
|
92
|
+
assert.equal(code, DENY, 'denied when no valid ADDR')
|
|
93
|
+
done()
|
|
94
|
+
},
|
|
95
|
+
this.connection,
|
|
96
|
+
['XCLIENT', 'NAME=example.com'],
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('accepts XCLIENT with valid IPv4 ADDR from allowed host', (done) => {
|
|
101
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
102
|
+
this.plugin.hook_unrecognized_command(
|
|
103
|
+
(code) => {
|
|
104
|
+
// NEXT_HOOK or undefined (next called) means accepted
|
|
105
|
+
assert.ok(code === NEXT_HOOK || code === undefined, 'accepted valid XCLIENT')
|
|
106
|
+
done()
|
|
107
|
+
},
|
|
108
|
+
this.connection,
|
|
109
|
+
['XCLIENT', 'ADDR=1.2.3.4'],
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('accepts XCLIENT with valid IPv6 ADDR from allowed host', (done) => {
|
|
114
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
115
|
+
this.plugin.hook_unrecognized_command(
|
|
116
|
+
(code) => {
|
|
117
|
+
assert.ok(code === NEXT_HOOK || code === undefined, 'accepted valid IPv6 XCLIENT')
|
|
118
|
+
done()
|
|
119
|
+
},
|
|
120
|
+
this.connection,
|
|
121
|
+
['XCLIENT', 'ADDR=IPV6:2001:db8::1'],
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('accepts XCLIENT with ADDR and NAME, skipping rdns lookup', (done) => {
|
|
126
|
+
this.connection.remote.ip = '127.0.0.1'
|
|
127
|
+
this.plugin.hook_unrecognized_command(
|
|
128
|
+
(code) => {
|
|
129
|
+
assert.equal(code, NEXT_HOOK, 'jumps to connect hook when NAME provided')
|
|
130
|
+
done()
|
|
131
|
+
},
|
|
132
|
+
this.connection,
|
|
133
|
+
['XCLIENT', 'ADDR=1.2.3.4 NAME=example.com'],
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
package/test/rfc1869.js
CHANGED
|
@@ -63,4 +63,47 @@ describe('rfc1869', () => {
|
|
|
63
63
|
it('RCPT TO:<postmaster>', () => {
|
|
64
64
|
_check('RCPT TO:<postmaster>', ['<postmaster>'])
|
|
65
65
|
})
|
|
66
|
+
|
|
67
|
+
describe('error cases', () => {
|
|
68
|
+
const throwCases = [
|
|
69
|
+
{
|
|
70
|
+
desc: 'MAIL FROM with space inside angle-bracket address',
|
|
71
|
+
args: ['mail', 'FROM:<user@dom ain>'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
desc: 'RCPT TO with syntax error in address (space in address)',
|
|
75
|
+
args: ['rcpt', 'TO: user @domain bad'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
desc: 'RCPT TO unknown address (no @ and not postmaster/abuse)',
|
|
79
|
+
args: ['rcpt', 'TO:unknown'],
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
for (const { desc, args } of throwCases) {
|
|
84
|
+
it(`throws: ${desc}`, () => {
|
|
85
|
+
assert.throws(() => parse(...args), Error)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('strict mode', () => {
|
|
91
|
+
it('strict MAIL FROM:<user@domain> accepts angle-bracket address', () => {
|
|
92
|
+
const result = parse('mail', 'FROM:<user@domain.com>', true)
|
|
93
|
+
assert.equal(result[0], '<user@domain.com>')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('strict MAIL FROM without angle brackets throws', () => {
|
|
97
|
+
assert.throws(() => parse('mail', 'FROM:user@domain.com', true), Error)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('strict RCPT TO:<user@domain> accepts angle-bracket address', () => {
|
|
101
|
+
const result = parse('rcpt', 'TO:<user@domain.com>', true)
|
|
102
|
+
assert.equal(result[0], '<user@domain.com>')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('strict RCPT TO without angle brackets throws', () => {
|
|
106
|
+
assert.throws(() => parse('rcpt', 'TO:user@domain.com', true), Error)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
66
109
|
})
|
package/test/smtp_client.js
CHANGED
|
@@ -140,15 +140,15 @@ describe('smtp_client', () => {
|
|
|
140
140
|
this.client.on('data', () => {
|
|
141
141
|
assert.equal(this.client.response[0], 'go ahead')
|
|
142
142
|
this.client.start_data(message_stream)
|
|
143
|
-
message_stream.on('end', () => {
|
|
144
|
-
this.client.socket.write('.\r\n')
|
|
145
|
-
})
|
|
146
143
|
message_stream.add_line('Header: test\r\n')
|
|
147
144
|
message_stream.add_line('\r\n')
|
|
148
145
|
message_stream.add_line('hi\r\n')
|
|
149
146
|
message_stream.add_line_end()
|
|
150
147
|
})
|
|
151
148
|
|
|
149
|
+
data.push('Header: test')
|
|
150
|
+
data.push('')
|
|
151
|
+
data.push('hi')
|
|
152
152
|
data.push('.')
|
|
153
153
|
data.push('250 message queued')
|
|
154
154
|
|
|
@@ -249,15 +249,15 @@ describe('smtp_client', () => {
|
|
|
249
249
|
this.client.on('data', () => {
|
|
250
250
|
assert.equal(this.client.response[0], 'go ahead')
|
|
251
251
|
this.client.start_data(message_stream)
|
|
252
|
-
message_stream.on('end', () => {
|
|
253
|
-
this.client.socket.write('.\r\n')
|
|
254
|
-
})
|
|
255
252
|
message_stream.add_line('Header: test\r\n')
|
|
256
253
|
message_stream.add_line('\r\n')
|
|
257
254
|
message_stream.add_line('hi\r\n')
|
|
258
255
|
message_stream.add_line_end()
|
|
259
256
|
})
|
|
260
257
|
|
|
258
|
+
data.push('Header: test')
|
|
259
|
+
data.push('')
|
|
260
|
+
data.push('hi')
|
|
261
261
|
data.push('.')
|
|
262
262
|
data.push('250 message queued')
|
|
263
263
|
|