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/connection.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
1
4
|
const assert = require('node:assert/strict')
|
|
2
5
|
|
|
3
6
|
const constants = require('haraka-constants')
|
|
@@ -6,33 +9,43 @@ const DSN = require('haraka-dsn')
|
|
|
6
9
|
const connection = require('../connection')
|
|
7
10
|
const Server = require('../server')
|
|
8
11
|
|
|
9
|
-
//
|
|
12
|
+
// Expose SMTP result constants as globals (DENY, DENYSOFT, etc.)
|
|
10
13
|
constants.import(global)
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function makeClient(opts = {}) {
|
|
18
|
+
return {
|
|
19
|
+
remotePort: opts.remotePort ?? null,
|
|
20
|
+
remoteAddress: opts.remoteAddress ?? null,
|
|
21
|
+
localPort: opts.localPort ?? null,
|
|
22
|
+
localAddress: opts.localAddress ?? null,
|
|
23
|
+
destroy: () => {},
|
|
24
|
+
pause: () => {},
|
|
25
|
+
resume: () => {},
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeServer(ip = null) {
|
|
30
|
+
return {
|
|
31
|
+
ip_address: ip,
|
|
23
32
|
address() {
|
|
24
33
|
return this.ip_address
|
|
25
34
|
},
|
|
26
35
|
}
|
|
27
|
-
this.connection = connection.createConnection(client, server, Server.cfg)
|
|
28
|
-
done()
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
const setUp = () => {
|
|
39
|
+
this.connection = connection.createConnection(makeClient(), makeServer(), Server.cfg)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
31
44
|
describe('connection', () => {
|
|
32
|
-
describe('
|
|
33
|
-
beforeEach(
|
|
45
|
+
describe('initial properties', () => {
|
|
46
|
+
beforeEach(setUp)
|
|
34
47
|
|
|
35
|
-
it('
|
|
48
|
+
it('remote object defaults', () => {
|
|
36
49
|
assert.deepEqual(this.connection.remote, {
|
|
37
50
|
ip: null,
|
|
38
51
|
port: null,
|
|
@@ -44,13 +57,13 @@ describe('connection', () => {
|
|
|
44
57
|
})
|
|
45
58
|
})
|
|
46
59
|
|
|
47
|
-
it('
|
|
60
|
+
it('local object defaults', () => {
|
|
48
61
|
assert.equal(this.connection.local.ip, null)
|
|
49
62
|
assert.equal(this.connection.local.port, null)
|
|
50
|
-
assert.ok(this.connection.local.host,
|
|
63
|
+
assert.ok(this.connection.local.host, 'local.host is set')
|
|
51
64
|
})
|
|
52
65
|
|
|
53
|
-
it('
|
|
66
|
+
it('tls object defaults', () => {
|
|
54
67
|
assert.deepEqual(this.connection.tls, {
|
|
55
68
|
enabled: false,
|
|
56
69
|
advertised: false,
|
|
@@ -59,147 +72,103 @@ describe('connection', () => {
|
|
|
59
72
|
})
|
|
60
73
|
})
|
|
61
74
|
|
|
62
|
-
it('
|
|
63
|
-
assert.
|
|
75
|
+
it('hello object defaults', () => {
|
|
76
|
+
assert.equal(this.connection.hello.host, null)
|
|
77
|
+
assert.equal(this.connection.hello.verb, null)
|
|
64
78
|
})
|
|
65
79
|
|
|
66
|
-
it('
|
|
67
|
-
assert.equal(
|
|
80
|
+
it('proxy object defaults', () => {
|
|
81
|
+
assert.equal(this.connection.proxy.allowed, false)
|
|
82
|
+
assert.equal(this.connection.proxy.ip, null)
|
|
83
|
+
assert.equal(this.connection.proxy.type, null)
|
|
84
|
+
assert.equal(this.connection.proxy.timer, null)
|
|
68
85
|
})
|
|
69
86
|
|
|
70
|
-
it('
|
|
71
|
-
assert.
|
|
72
|
-
assert.equal(
|
|
87
|
+
it('notes object exists', () => {
|
|
88
|
+
assert.ok(this.connection.notes, 'notes is set')
|
|
89
|
+
assert.equal(typeof this.connection.notes, 'object')
|
|
73
90
|
})
|
|
74
91
|
|
|
75
|
-
it('
|
|
76
|
-
assert.equal(
|
|
77
|
-
assert.equal('Message denied temporarily', this.connection.queue_msg(DENYSOFTDISCONNECT))
|
|
92
|
+
it('transaction is null', () => {
|
|
93
|
+
assert.equal(this.connection.transaction, null)
|
|
78
94
|
})
|
|
79
95
|
|
|
80
|
-
it('
|
|
81
|
-
assert.equal(
|
|
96
|
+
it('capabilities is null', () => {
|
|
97
|
+
assert.equal(this.connection.capabilities, null)
|
|
82
98
|
})
|
|
83
99
|
|
|
84
|
-
it('
|
|
85
|
-
this.connection.
|
|
86
|
-
this.connection.
|
|
87
|
-
this.connection.set('tls', 'enabled', true)
|
|
88
|
-
|
|
89
|
-
assert.equal('172.16.15.1', this.connection.remote.ip)
|
|
90
|
-
assert.equal(null, this.connection.remote.port)
|
|
91
|
-
assert.equal('EHLO', this.connection.hello.verb)
|
|
92
|
-
assert.equal(null, this.connection.hello.host)
|
|
93
|
-
assert.equal(true, this.connection.tls.enabled)
|
|
100
|
+
it('remote.is_private and remote.is_local default to false', () => {
|
|
101
|
+
assert.equal(this.connection.remote.is_private, false)
|
|
102
|
+
assert.equal(this.connection.remote.is_local, false)
|
|
94
103
|
})
|
|
104
|
+
})
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
describe('private IP connection', () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
this.connection = connection.createConnection(
|
|
109
|
+
makeClient({
|
|
110
|
+
remotePort: 2525,
|
|
111
|
+
remoteAddress: '172.16.15.1',
|
|
112
|
+
localPort: 25,
|
|
113
|
+
localAddress: '172.16.15.254',
|
|
114
|
+
}),
|
|
115
|
+
makeServer('172.16.15.254'),
|
|
116
|
+
Server.cfg,
|
|
117
|
+
)
|
|
99
118
|
})
|
|
100
119
|
|
|
101
|
-
it('
|
|
102
|
-
assert.equal(
|
|
103
|
-
assert.equal(null, this.connection.proxy.ip)
|
|
104
|
-
assert.equal(null, this.connection.proxy.type)
|
|
105
|
-
assert.equal(null, this.connection.proxy.timer)
|
|
120
|
+
it('remote.is_private is true', () => {
|
|
121
|
+
assert.equal(this.connection.remote.is_private, true)
|
|
106
122
|
})
|
|
107
123
|
|
|
108
|
-
it('
|
|
109
|
-
this.connection.
|
|
110
|
-
|
|
111
|
-
this.connection.set(
|
|
112
|
-
'proxy',
|
|
113
|
-
'timer',
|
|
114
|
-
setTimeout(() => {}, 1000),
|
|
115
|
-
)
|
|
116
|
-
this.connection.set('proxy', 'allowed', true)
|
|
124
|
+
it('remote.is_local is false', () => {
|
|
125
|
+
assert.equal(this.connection.remote.is_local, false)
|
|
126
|
+
})
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
assert.equal(
|
|
120
|
-
assert.ok(this.connection.proxy.timer)
|
|
121
|
-
assert.equal(this.connection.proxy.type, 'haproxy')
|
|
128
|
+
it('remote.port is set', () => {
|
|
129
|
+
assert.equal(this.connection.remote.port, 2525)
|
|
122
130
|
})
|
|
123
131
|
})
|
|
124
132
|
|
|
125
|
-
describe('
|
|
126
|
-
beforeEach((
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
localAddress: '172.16.15.254',
|
|
133
|
-
destroy: () => {
|
|
134
|
-
true
|
|
135
|
-
},
|
|
136
|
-
}
|
|
137
|
-
const server = {
|
|
138
|
-
ip_address: '172.16.15.254',
|
|
139
|
-
address() {
|
|
140
|
-
return this.ip_address
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
this.connection = connection.createConnection(client, server, Server.cfg)
|
|
144
|
-
done()
|
|
133
|
+
describe('loopback connection', () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
this.connection = connection.createConnection(
|
|
136
|
+
makeClient({ remotePort: 2525, remoteAddress: '127.0.0.2', localPort: 25, localAddress: '172.0.0.1' }),
|
|
137
|
+
makeServer('127.0.0.1'),
|
|
138
|
+
Server.cfg,
|
|
139
|
+
)
|
|
145
140
|
})
|
|
146
141
|
|
|
147
|
-
it('
|
|
148
|
-
assert.equal(
|
|
149
|
-
assert.equal(false, this.connection.remote.is_local)
|
|
150
|
-
assert.equal(2525, this.connection.remote.port)
|
|
142
|
+
it('remote.is_private is true', () => {
|
|
143
|
+
assert.equal(this.connection.remote.is_private, true)
|
|
151
144
|
})
|
|
152
|
-
})
|
|
153
145
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const client = {
|
|
157
|
-
remotePort: 2525,
|
|
158
|
-
remoteAddress: '127.0.0.2',
|
|
159
|
-
localPort: 25,
|
|
160
|
-
localAddress: '172.0.0.1',
|
|
161
|
-
destroy: () => {
|
|
162
|
-
true
|
|
163
|
-
},
|
|
164
|
-
}
|
|
165
|
-
const server = {
|
|
166
|
-
ip_address: '127.0.0.1',
|
|
167
|
-
address() {
|
|
168
|
-
return this.ip_address
|
|
169
|
-
},
|
|
170
|
-
}
|
|
171
|
-
this.connection = connection.createConnection(client, server, Server.cfg)
|
|
172
|
-
done()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('sets remote.is_private and remote.is_local', () => {
|
|
176
|
-
assert.equal(true, this.connection.remote.is_private)
|
|
177
|
-
assert.equal(true, this.connection.remote.is_local)
|
|
178
|
-
assert.equal(2525, this.connection.remote.port)
|
|
146
|
+
it('remote.is_local is true', () => {
|
|
147
|
+
assert.equal(this.connection.remote.is_local, true)
|
|
179
148
|
})
|
|
180
149
|
})
|
|
181
150
|
|
|
182
151
|
describe('get_remote', () => {
|
|
183
|
-
beforeEach(
|
|
152
|
+
beforeEach(setUp)
|
|
184
153
|
|
|
185
|
-
it('
|
|
154
|
+
it('formats host and IP', () => {
|
|
186
155
|
this.connection.remote.host = 'a.host.tld'
|
|
187
156
|
this.connection.remote.ip = '172.16.199.198'
|
|
188
157
|
assert.equal(this.connection.get_remote('host'), 'a.host.tld [172.16.199.198]')
|
|
189
158
|
})
|
|
190
159
|
|
|
191
|
-
it('no
|
|
160
|
+
it('falls back to bracketed IP when no host', () => {
|
|
192
161
|
this.connection.remote.ip = '172.16.199.198'
|
|
193
162
|
assert.equal(this.connection.get_remote('host'), '[172.16.199.198]')
|
|
194
163
|
})
|
|
195
164
|
|
|
196
|
-
it('DNSERROR', () => {
|
|
165
|
+
it('DNSERROR suppresses hostname', () => {
|
|
197
166
|
this.connection.remote.host = 'DNSERROR'
|
|
198
167
|
this.connection.remote.ip = '172.16.199.198'
|
|
199
168
|
assert.equal(this.connection.get_remote('host'), '[172.16.199.198]')
|
|
200
169
|
})
|
|
201
170
|
|
|
202
|
-
it('NXDOMAIN', () => {
|
|
171
|
+
it('NXDOMAIN suppresses hostname', () => {
|
|
203
172
|
this.connection.remote.host = 'NXDOMAIN'
|
|
204
173
|
this.connection.remote.ip = '172.16.199.198'
|
|
205
174
|
assert.equal(this.connection.get_remote('host'), '[172.16.199.198]')
|
|
@@ -207,100 +176,270 @@ describe('connection', () => {
|
|
|
207
176
|
})
|
|
208
177
|
|
|
209
178
|
describe('local.info', () => {
|
|
210
|
-
beforeEach(
|
|
179
|
+
beforeEach(setUp)
|
|
180
|
+
|
|
181
|
+
it('contains Haraka/version', () => {
|
|
182
|
+
assert.match(this.connection.local.info, /Haraka\/\d+\.\d+/)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
211
185
|
|
|
212
|
-
|
|
213
|
-
|
|
186
|
+
describe('get_capabilities', () => {
|
|
187
|
+
beforeEach(setUp)
|
|
188
|
+
|
|
189
|
+
it('returns empty array by default', () => {
|
|
190
|
+
assert.deepEqual(this.connection.get_capabilities(), [])
|
|
214
191
|
})
|
|
215
192
|
})
|
|
216
193
|
|
|
217
194
|
describe('relaying', () => {
|
|
218
|
-
beforeEach(
|
|
195
|
+
beforeEach(setUp)
|
|
219
196
|
|
|
220
|
-
it('
|
|
197
|
+
it('defaults to false', () => {
|
|
221
198
|
assert.equal(this.connection.relaying, false)
|
|
199
|
+
})
|
|
222
200
|
|
|
201
|
+
it('set() and get() round-trip on connection', () => {
|
|
223
202
|
this.connection.set('relaying', 'crocodiles')
|
|
224
203
|
assert.equal(this.connection.get('relaying'), 'crocodiles')
|
|
225
204
|
assert.equal(this.connection.relaying, 'crocodiles')
|
|
226
205
|
assert.equal(this.connection._relaying, 'crocodiles')
|
|
206
|
+
})
|
|
227
207
|
|
|
208
|
+
it('direct assignment round-trips', () => {
|
|
228
209
|
this.connection.relaying = 'alligators'
|
|
229
210
|
assert.equal(this.connection.get('relaying'), 'alligators')
|
|
230
|
-
assert.equal(this.connection.relaying, 'alligators')
|
|
231
211
|
assert.equal(this.connection._relaying, 'alligators')
|
|
232
212
|
})
|
|
233
213
|
|
|
234
|
-
it('
|
|
235
|
-
assert.equal(this.connection.relaying, false)
|
|
236
|
-
|
|
214
|
+
it('set() with a transaction updates txn, not connection', () => {
|
|
237
215
|
this.connection.transaction = {}
|
|
238
|
-
this.connection.set('relaying', 'txn-only')
|
|
239
|
-
|
|
216
|
+
this.connection.set('relaying', 'txn-only')
|
|
240
217
|
assert.equal(this.connection.get('relaying'), 'txn-only')
|
|
241
218
|
assert.equal(this.connection._relaying, false)
|
|
242
219
|
assert.equal(this.connection.transaction._relaying, 'txn-only')
|
|
243
220
|
})
|
|
244
221
|
})
|
|
245
222
|
|
|
246
|
-
describe('
|
|
247
|
-
beforeEach(
|
|
223
|
+
describe('get / set', () => {
|
|
224
|
+
beforeEach(setUp)
|
|
248
225
|
|
|
249
|
-
it('sets single
|
|
226
|
+
it('sets and gets a single-level property', () => {
|
|
250
227
|
this.connection.set('encoding', true)
|
|
251
228
|
assert.ok(this.connection.encoding)
|
|
252
229
|
assert.ok(this.connection.get('encoding'))
|
|
253
230
|
})
|
|
254
231
|
|
|
255
|
-
it('sets two
|
|
232
|
+
it('sets and gets a two-level property', () => {
|
|
256
233
|
this.connection.set('local.host', 'test')
|
|
257
234
|
assert.equal(this.connection.local.host, 'test')
|
|
258
235
|
assert.equal(this.connection.get('local.host'), 'test')
|
|
259
236
|
})
|
|
260
237
|
|
|
261
|
-
it('sets three
|
|
238
|
+
it('sets and gets a three-level property', () => {
|
|
262
239
|
this.connection.set('some.fine.example', true)
|
|
263
240
|
assert.ok(this.connection.some.fine.example)
|
|
264
241
|
assert.ok(this.connection.get('some.fine.example'))
|
|
265
242
|
})
|
|
243
|
+
|
|
244
|
+
it('sets hello.verb via set()', () => {
|
|
245
|
+
this.connection.set('hello', 'verb', 'EHLO')
|
|
246
|
+
assert.equal(this.connection.hello.verb, 'EHLO')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('sets proxy fields via set()', () => {
|
|
250
|
+
this.connection.set('proxy', 'ip', '172.16.15.1')
|
|
251
|
+
this.connection.set('proxy', 'type', 'haproxy')
|
|
252
|
+
this.connection.set('proxy', 'allowed', true)
|
|
253
|
+
assert.equal(this.connection.proxy.ip, '172.16.15.1')
|
|
254
|
+
assert.equal(this.connection.proxy.type, 'haproxy')
|
|
255
|
+
assert.equal(this.connection.proxy.allowed, true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('has normalised connection properties after set()', () => {
|
|
259
|
+
this.connection.set('remote', 'ip', '172.16.15.1')
|
|
260
|
+
this.connection.set('hello', 'verb', 'EHLO')
|
|
261
|
+
this.connection.set('tls', 'enabled', true)
|
|
262
|
+
assert.equal(this.connection.remote.ip, '172.16.15.1')
|
|
263
|
+
assert.equal(this.connection.remote.port, null)
|
|
264
|
+
assert.equal(this.connection.hello.verb, 'EHLO')
|
|
265
|
+
assert.equal(this.connection.hello.host, null)
|
|
266
|
+
assert.equal(this.connection.tls.enabled, true)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('queue_msg', () => {
|
|
271
|
+
beforeEach(setUp)
|
|
272
|
+
|
|
273
|
+
it('returns supplied message when given', () => {
|
|
274
|
+
assert.equal(this.connection.queue_msg(1, 'test message'), 'test message')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('returns default DENY message', () => {
|
|
278
|
+
assert.equal(this.connection.queue_msg(DENY), 'Message denied')
|
|
279
|
+
assert.equal(this.connection.queue_msg(DENYDISCONNECT), 'Message denied')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('returns default DENYSOFT message', () => {
|
|
283
|
+
assert.equal(this.connection.queue_msg(DENYSOFT), 'Message denied temporarily')
|
|
284
|
+
assert.equal(this.connection.queue_msg(DENYSOFTDISCONNECT), 'Message denied temporarily')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('returns empty string for unrecognised code', () => {
|
|
288
|
+
assert.equal(this.connection.queue_msg('hello'), '')
|
|
289
|
+
})
|
|
266
290
|
})
|
|
267
291
|
|
|
268
292
|
describe('respond', () => {
|
|
269
|
-
beforeEach(
|
|
293
|
+
beforeEach(setUp)
|
|
270
294
|
|
|
271
|
-
it('
|
|
295
|
+
it('returns undefined when disconnected', () => {
|
|
272
296
|
this.connection.state = constants.connection.state.DISCONNECTED
|
|
273
297
|
assert.equal(this.connection.respond(200, 'your lucky day'), undefined)
|
|
274
298
|
assert.equal(this.connection.respond(550, 'you are jacked'), undefined)
|
|
275
299
|
})
|
|
276
300
|
|
|
277
|
-
it('
|
|
301
|
+
it('formats a simple 200 response', () => {
|
|
278
302
|
assert.equal(this.connection.respond(200, 'you may pass Go'), '200 you may pass Go\r\n')
|
|
279
303
|
})
|
|
280
304
|
|
|
281
|
-
it('DSN 200', () => {
|
|
305
|
+
it('formats a DSN 200 response', () => {
|
|
282
306
|
assert.equal(
|
|
283
307
|
this.connection.respond(200, DSN.create(200, 'you may pass Go')),
|
|
284
308
|
'200 2.0.0 you may pass Go\r\n',
|
|
285
309
|
)
|
|
286
310
|
})
|
|
287
311
|
|
|
288
|
-
it('DSN
|
|
289
|
-
// note, the DSN code overrides the response code
|
|
312
|
+
it('DSN overrides response code', () => {
|
|
290
313
|
assert.equal(
|
|
291
|
-
this.connection.respond(450, DSN.create(550, 'This domain is not in use
|
|
292
|
-
'550 5.0.0 This domain is not in use
|
|
314
|
+
this.connection.respond(450, DSN.create(550, 'This domain is not in use')),
|
|
315
|
+
'550 5.0.0 This domain is not in use\r\n',
|
|
293
316
|
)
|
|
294
317
|
})
|
|
295
318
|
|
|
296
|
-
it('DSN
|
|
319
|
+
it('DSN addr_bad_dest_system (5.1.2)', () => {
|
|
297
320
|
assert.equal(
|
|
298
|
-
this.connection.respond(
|
|
299
|
-
|
|
300
|
-
DSN.addr_bad_dest_system('This domain is not in use and does not accept mail', 550),
|
|
301
|
-
),
|
|
302
|
-
'550 5.1.2 This domain is not in use and does not accept mail\r\n',
|
|
321
|
+
this.connection.respond(550, DSN.addr_bad_dest_system('Domain not in use', 550)),
|
|
322
|
+
'550 5.1.2 Domain not in use\r\n',
|
|
303
323
|
)
|
|
304
324
|
})
|
|
325
|
+
|
|
326
|
+
it('formats multi-line response from array', () => {
|
|
327
|
+
const resp = this.connection.respond(250, ['Hello', 'World'])
|
|
328
|
+
assert.ok(resp.includes('250-Hello\r\n'), 'first line uses dash')
|
|
329
|
+
assert.ok(resp.includes('250 World\r\n'), 'last line uses space')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('formats multi-line response from newline-separated string', () => {
|
|
333
|
+
const resp = this.connection.respond(250, 'Hello\nWorld')
|
|
334
|
+
assert.ok(resp.includes('250-Hello\r\n'), 'first line uses dash')
|
|
335
|
+
assert.ok(resp.includes('250 World\r\n'), 'last line uses space')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('last_response is updated when client has a write method', () => {
|
|
339
|
+
// When client.write is defined, respond() writes to the socket and
|
|
340
|
+
// stores the formatted buffer in last_response.
|
|
341
|
+
let written = ''
|
|
342
|
+
this.connection.client.write = (buf) => {
|
|
343
|
+
written += buf
|
|
344
|
+
}
|
|
345
|
+
this.connection.respond(250, 'OK')
|
|
346
|
+
assert.ok(written.includes('250 OK'), 'data written to socket')
|
|
347
|
+
assert.ok(this.connection.last_response.includes('250 OK'), 'last_response updated')
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('pause and resume', () => {
|
|
352
|
+
beforeEach(setUp)
|
|
353
|
+
|
|
354
|
+
it('restores previous state when still paused at resume', () => {
|
|
355
|
+
this.connection.state = constants.connection.state.PAUSE_SMTP
|
|
356
|
+
this.connection.pause()
|
|
357
|
+
this.connection.resume()
|
|
358
|
+
assert.equal(this.connection.state, constants.connection.state.PAUSE_SMTP)
|
|
359
|
+
assert.equal(this.connection.prev_state, null)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('does not overwrite state changed while paused', () => {
|
|
363
|
+
this.connection.state = constants.connection.state.PAUSE_SMTP
|
|
364
|
+
this.connection.pause()
|
|
365
|
+
this.connection.state = constants.connection.state.CMD
|
|
366
|
+
this.connection.resume()
|
|
367
|
+
assert.equal(this.connection.state, constants.connection.state.CMD)
|
|
368
|
+
assert.equal(this.connection.prev_state, null)
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe('loop_respond', () => {
|
|
373
|
+
beforeEach(setUp)
|
|
374
|
+
|
|
375
|
+
it('sets state to LOOP', () => {
|
|
376
|
+
this.connection.loop_respond(554, 'Denied')
|
|
377
|
+
assert.equal(this.connection.state, constants.connection.state.LOOP)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('records loop_code and loop_msg', () => {
|
|
381
|
+
this.connection.loop_respond(554, 'Denied')
|
|
382
|
+
assert.equal(this.connection.loop_code, 554)
|
|
383
|
+
assert.equal(this.connection.loop_msg, 'Denied')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('does nothing when already disconnecting', () => {
|
|
387
|
+
this.connection.state = constants.connection.state.DISCONNECTING
|
|
388
|
+
this.connection.loop_respond(554, 'Denied')
|
|
389
|
+
assert.equal(this.connection.state, constants.connection.state.DISCONNECTING)
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
describe('tran_uuid', () => {
|
|
394
|
+
beforeEach(setUp)
|
|
395
|
+
|
|
396
|
+
it('increments tran_count on each call', () => {
|
|
397
|
+
assert.equal(this.connection.tran_count, 0)
|
|
398
|
+
const u1 = this.connection.tran_uuid()
|
|
399
|
+
assert.equal(this.connection.tran_count, 1)
|
|
400
|
+
const u2 = this.connection.tran_uuid()
|
|
401
|
+
assert.equal(this.connection.tran_count, 2)
|
|
402
|
+
assert.notEqual(u1, u2)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('formats as <connection-uuid>.<count>', () => {
|
|
406
|
+
const u = this.connection.tran_uuid()
|
|
407
|
+
assert.match(u, new RegExp(`^${this.connection.uuid}\\.1$`))
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
describe('issue #3374 — double QUIT prevention', () => {
|
|
412
|
+
beforeEach(setUp)
|
|
413
|
+
|
|
414
|
+
it('quit hook fires only once when two QUITs arrive in LOOP state', async () => {
|
|
415
|
+
const conn = this.connection
|
|
416
|
+
conn.loop_respond(554, 'Denied')
|
|
417
|
+
assert.equal(conn.state, constants.connection.state.LOOP)
|
|
418
|
+
|
|
419
|
+
let quit_hook_calls = 0
|
|
420
|
+
const plugins = require('../plugins')
|
|
421
|
+
const original_run_hooks = plugins.run_hooks
|
|
422
|
+
plugins.run_hooks = (hook, c, params) => {
|
|
423
|
+
if (hook === 'quit') {
|
|
424
|
+
quit_hook_calls++
|
|
425
|
+
if (quit_hook_calls === 1) {
|
|
426
|
+
setTimeout(() => c.quit_respond(constants.ok), 50)
|
|
427
|
+
}
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
original_run_hooks(hook, c, params)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
conn.process_line(Buffer.from('QUIT\r\n'))
|
|
434
|
+
conn.process_line(Buffer.from('QUIT\r\n'))
|
|
435
|
+
|
|
436
|
+
await new Promise((resolve) => {
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
plugins.run_hooks = original_run_hooks
|
|
439
|
+
assert.equal(quit_hook_calls, 1, 'quit hook called exactly once')
|
|
440
|
+
resolve()
|
|
441
|
+
}, 100)
|
|
442
|
+
})
|
|
443
|
+
})
|
|
305
444
|
})
|
|
306
445
|
})
|
|
@@ -67,7 +67,7 @@ exports.createHMailItem = (outbound_context, options, callback) => {
|
|
|
67
67
|
let line = match[1]
|
|
68
68
|
line = line.replace(/\r?\n?$/, '\r\n') // make sure it ends in \r\n
|
|
69
69
|
conn.transaction.add_data(Buffer.from(line))
|
|
70
|
-
contents = contents.
|
|
70
|
+
contents = contents.substring(match[1].length)
|
|
71
71
|
if (contents.length === 0) {
|
|
72
72
|
break
|
|
73
73
|
}
|