Haraka 3.1.6 → 3.1.7

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/CONTRIBUTORS.md +8 -8
  3. package/Plugins.md +99 -99
  4. package/config/smtp_forward.ini +10 -0
  5. package/config/smtp_proxy.ini +10 -0
  6. package/connection.js +25 -8
  7. package/docs/plugins/queue/smtp_forward.md +19 -3
  8. package/docs/plugins/queue/smtp_proxy.md +10 -2
  9. package/haraka.js +1 -1
  10. package/outbound/hmail.js +39 -39
  11. package/outbound/index.js +4 -4
  12. package/outbound/tls.js +2 -43
  13. package/package.json +49 -48
  14. package/plugins/auth/auth_base.js +9 -3
  15. package/plugins/auth/auth_proxy.js +14 -11
  16. package/plugins/block_me.js +4 -2
  17. package/plugins/prevent_credential_leaks.js +3 -1
  18. package/plugins/process_title.js +6 -6
  19. package/plugins/queue/qmail-queue.js +15 -19
  20. package/plugins/queue/smtp_forward.js +12 -4
  21. package/plugins/queue/smtp_proxy.js +14 -3
  22. package/plugins/tls.js +13 -5
  23. package/plugins/xclient.js +3 -1
  24. package/server.js +5 -3
  25. package/smtp_client.js +20 -11
  26. package/test/config/block_me.recipient +1 -0
  27. package/test/config/block_me.senders +1 -0
  28. package/test/connection.js +24 -0
  29. package/test/outbound/bounce_net_errors.js +3 -2
  30. package/test/plugins/auth/auth_bridge.js +80 -0
  31. package/test/plugins/auth/flat_file.js +128 -0
  32. package/test/plugins/block_me.js +157 -0
  33. package/test/plugins/data.signatures.js +114 -0
  34. package/test/plugins/delay_deny.js +263 -0
  35. package/test/plugins/prevent_credential_leaks.js +178 -0
  36. package/test/plugins/process_title.js +135 -0
  37. package/test/plugins/queue/deliver.js +99 -0
  38. package/test/plugins/queue/discard.js +79 -0
  39. package/test/plugins/queue/lmtp.js +138 -0
  40. package/test/plugins/queue/qmail-queue.js +99 -0
  41. package/test/plugins/queue/quarantine.js +81 -0
  42. package/test/plugins/queue/smtp_bridge.js +154 -0
  43. package/test/plugins/queue/smtp_forward.js +42 -6
  44. package/test/plugins/queue/smtp_proxy.js +139 -0
  45. package/test/plugins/reseed_rng.js +34 -0
  46. package/test/plugins/tarpit.js +91 -0
  47. package/test/plugins/tls.js +25 -0
  48. package/test/plugins/toobusy.js +21 -0
  49. package/test/plugins/xclient.js +14 -0
  50. package/test/server.js +59 -0
  51. package/test/smtp_client.js +45 -12
  52. package/test/tls_socket.js +82 -0
  53. package/tls_socket.js +50 -0
@@ -0,0 +1,178 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it, beforeEach } = require('node:test')
5
+
6
+ const fixtures = require('haraka-test-fixtures')
7
+ require('haraka-constants').import(global)
8
+
9
+ function makeConnection({ authUser, authPasswd, bodytext = '', children = [] } = {}) {
10
+ const conn = fixtures.connection.createConnection()
11
+ conn.init_transaction()
12
+ if (authUser) conn.notes.auth_user = authUser
13
+ if (authPasswd) conn.notes.auth_passwd = authPasswd
14
+ conn.transaction.body = { bodytext, children }
15
+ return conn
16
+ }
17
+
18
+ describe('prevent_credential_leaks', () => {
19
+ let plugin
20
+
21
+ beforeEach(() => {
22
+ plugin = new fixtures.plugin('prevent_credential_leaks')
23
+ })
24
+
25
+ describe('hook_data', () => {
26
+ it('does not enable parse_body when no auth credentials', (t, done) => {
27
+ const conn = makeConnection()
28
+ conn.transaction.parse_body = false
29
+ plugin.hook_data((rc) => {
30
+ assert.equal(rc, undefined)
31
+ assert.equal(conn.transaction.parse_body, false)
32
+ done()
33
+ }, conn)
34
+ })
35
+
36
+ it('enables parse_body when both auth_user and auth_passwd are present', (t, done) => {
37
+ const conn = makeConnection({ authUser: 'user@example.com', authPasswd: 'secret' })
38
+ conn.transaction.parse_body = false
39
+ plugin.hook_data((rc) => {
40
+ assert.equal(rc, undefined)
41
+ assert.equal(conn.transaction.parse_body, true)
42
+ done()
43
+ }, conn)
44
+ })
45
+
46
+ it('does not enable parse_body when only auth_user is set', (t, done) => {
47
+ const conn = makeConnection({ authUser: 'user@example.com' })
48
+ conn.transaction.parse_body = false
49
+ plugin.hook_data((rc) => {
50
+ assert.equal(rc, undefined)
51
+ assert.equal(conn.transaction.parse_body, false)
52
+ done()
53
+ }, conn)
54
+ })
55
+
56
+ it('handles missing connection gracefully', (t, done) => {
57
+ // Simulate a null-ish connection by calling with empty notes
58
+ const conn = fixtures.connection.createConnection()
59
+ conn.init_transaction()
60
+ conn.notes = {}
61
+ plugin.hook_data((rc) => {
62
+ assert.equal(rc, undefined)
63
+ done()
64
+ }, conn)
65
+ })
66
+ })
67
+
68
+ describe('hook_data_post', () => {
69
+ it('calls next when no auth credentials are set', (t, done) => {
70
+ const conn = makeConnection({ bodytext: 'user@example.com secret123' })
71
+ plugin.hook_data_post((rc) => {
72
+ assert.equal(rc, undefined)
73
+ done()
74
+ }, conn)
75
+ })
76
+
77
+ it('calls next when only auth_user is set (no password)', (t, done) => {
78
+ const conn = makeConnection({ authUser: 'user@example.com', bodytext: 'user@example.com' })
79
+ plugin.hook_data_post((rc) => {
80
+ assert.equal(rc, undefined)
81
+ done()
82
+ }, conn)
83
+ })
84
+
85
+ it('calls next when body contains neither username nor password', (t, done) => {
86
+ const conn = makeConnection({
87
+ authUser: 'alice@example.com',
88
+ authPasswd: 'mypassword',
89
+ bodytext: 'Hello, this is a clean email with no credentials.',
90
+ })
91
+ plugin.hook_data_post((rc) => {
92
+ assert.equal(rc, undefined)
93
+ done()
94
+ }, conn)
95
+ })
96
+
97
+ it('calls next when body contains username but not password', (t, done) => {
98
+ const conn = makeConnection({
99
+ authUser: 'alice@example.com',
100
+ authPasswd: 'mypassword',
101
+ bodytext: 'Contact alice@example.com for more info.',
102
+ })
103
+ plugin.hook_data_post((rc) => {
104
+ assert.equal(rc, undefined)
105
+ done()
106
+ }, conn)
107
+ })
108
+
109
+ it('denies when body contains both username and password', (t, done) => {
110
+ const conn = makeConnection({
111
+ authUser: 'alice@example.com',
112
+ authPasswd: 'mypassword',
113
+ bodytext: 'Please send your login: alice and password: mypassword to activate.',
114
+ })
115
+ plugin.hook_data_post((rc, msg) => {
116
+ assert.equal(rc, DENY)
117
+ assert.ok(msg.includes('Credential leak'))
118
+ done()
119
+ }, conn)
120
+ })
121
+
122
+ it('denies when credentials appear in a child body part', (t, done) => {
123
+ const conn = fixtures.connection.createConnection()
124
+ conn.init_transaction()
125
+ conn.notes.auth_user = 'bob@example.com'
126
+ conn.notes.auth_passwd = 's3cr3t'
127
+ conn.transaction.body = {
128
+ bodytext: 'clean parent text',
129
+ children: [{ bodytext: 'bob login with s3cr3t password', children: [] }],
130
+ }
131
+ plugin.hook_data_post((rc) => {
132
+ assert.equal(rc, DENY)
133
+ done()
134
+ }, conn)
135
+ })
136
+
137
+ it('handles qualified username (user@domain) by making domain optional', (t, done) => {
138
+ const conn = makeConnection({
139
+ authUser: 'carol@corp.example.com',
140
+ authPasswd: 'pass123',
141
+ bodytext: 'carol pass123 credentials',
142
+ })
143
+ plugin.hook_data_post((rc) => {
144
+ assert.equal(rc, DENY)
145
+ done()
146
+ }, conn)
147
+ })
148
+
149
+ it('unqualified username (no @) is not split into a partial match', (t, done) => {
150
+ // Bug: `if (idx)` with idx === -1 treated 'admin' as qualified,
151
+ // splitting it to user='admi' which then matches 'admiral'.
152
+ const conn = makeConnection({
153
+ authUser: 'admin',
154
+ authPasswd: 'pw',
155
+ bodytext: 'the admiral said pw today',
156
+ })
157
+ plugin.hook_data_post((rc) => {
158
+ assert.equal(rc, undefined)
159
+ done()
160
+ }, conn)
161
+ })
162
+
163
+ it('calls next when credentials appear in neither top nor child', (t, done) => {
164
+ const conn = fixtures.connection.createConnection()
165
+ conn.init_transaction()
166
+ conn.notes.auth_user = 'dave@example.com'
167
+ conn.notes.auth_passwd = 'xyzzy'
168
+ conn.transaction.body = {
169
+ bodytext: 'Hello world',
170
+ children: [{ bodytext: 'No credentials here at all', children: [] }],
171
+ }
172
+ plugin.hook_data_post((rc) => {
173
+ assert.equal(rc, undefined)
174
+ done()
175
+ }, conn)
176
+ })
177
+ })
178
+ })
@@ -0,0 +1,135 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it, beforeEach } = require('node:test')
5
+
6
+ const fixtures = require('haraka-test-fixtures')
7
+ const Notes = require('haraka-notes')
8
+
9
+ function makeServer(extra = {}) {
10
+ return {
11
+ notes: new Notes({
12
+ pt_connections: 0,
13
+ pt_concurrent: 0,
14
+ pt_cps_diff: 0,
15
+ pt_cps_max: 0,
16
+ pt_recipients: 0,
17
+ pt_rps_diff: 0,
18
+ pt_rps_max: 0,
19
+ pt_messages: 0,
20
+ pt_mps_diff: 0,
21
+ pt_mps_max: 0,
22
+ ...extra,
23
+ }),
24
+ cluster: null,
25
+ address: () => ({ address: '127.0.0.1', port: 25 }),
26
+ }
27
+ }
28
+
29
+ describe('process_title', () => {
30
+ let plugin
31
+
32
+ beforeEach(() => {
33
+ plugin = new fixtures.plugin('process_title')
34
+ })
35
+
36
+ describe('hook_connect_init', () => {
37
+ it('increments connection and concurrent counts', (t, done) => {
38
+ const server = makeServer()
39
+ const conn = fixtures.connection.createConnection({}, server)
40
+ plugin.hook_connect_init((rc) => {
41
+ assert.equal(rc, undefined)
42
+ assert.equal(server.notes.pt_connections, 1)
43
+ assert.equal(server.notes.pt_concurrent, 1)
44
+ assert.equal(conn.notes.pt_connect_run, true)
45
+ done()
46
+ }, conn)
47
+ })
48
+ })
49
+
50
+ describe('hook_disconnect', () => {
51
+ it('decrements concurrent count when connect_init ran', (t, done) => {
52
+ const server = makeServer({ pt_connections: 1, pt_concurrent: 1 })
53
+ const conn = fixtures.connection.createConnection({}, server)
54
+ conn.notes.pt_connect_run = true
55
+ plugin.hook_disconnect((rc) => {
56
+ assert.equal(rc, undefined)
57
+ assert.equal(server.notes.pt_concurrent, 0)
58
+ assert.equal(server.notes.pt_connections, 1) // not re-incremented
59
+ done()
60
+ }, conn)
61
+ })
62
+
63
+ it('increments connection count when connect_init did not run', (t, done) => {
64
+ const server = makeServer({ pt_connections: 0, pt_concurrent: 0 })
65
+ const conn = fixtures.connection.createConnection({}, server)
66
+ // pt_connect_run is NOT set: disconnect does connect bookkeeping then decrements
67
+ plugin.hook_disconnect((rc) => {
68
+ assert.equal(rc, undefined)
69
+ assert.equal(server.notes.pt_connections, 1) // incremented by disconnect
70
+ assert.equal(server.notes.pt_concurrent, 0) // +1 then -1
71
+ done()
72
+ }, conn)
73
+ })
74
+ })
75
+
76
+ describe('hook_rcpt', () => {
77
+ it('increments recipient count', (t, done) => {
78
+ const server = makeServer()
79
+ const conn = fixtures.connection.createConnection({}, server)
80
+ plugin.hook_rcpt((rc) => {
81
+ assert.equal(rc, undefined)
82
+ assert.equal(server.notes.pt_recipients, 1)
83
+ done()
84
+ }, conn)
85
+ })
86
+ })
87
+
88
+ describe('hook_data', () => {
89
+ it('increments message count', (t, done) => {
90
+ const server = makeServer()
91
+ const conn = fixtures.connection.createConnection({}, server)
92
+ plugin.hook_data((rc) => {
93
+ assert.equal(rc, undefined)
94
+ assert.equal(server.notes.pt_messages, 1)
95
+ done()
96
+ }, conn)
97
+ })
98
+ })
99
+
100
+ describe('hook_init_child', () => {
101
+ it('initializes server notes and calls next', (t, done) => {
102
+ const server = { notes: new Notes(), cluster: null }
103
+ plugin.hook_init_child((rc) => {
104
+ clearInterval(plugin._interval)
105
+ assert.equal(rc, undefined)
106
+ assert.equal(server.notes.pt_connections, 0)
107
+ assert.equal(server.notes.pt_messages, 0)
108
+ assert.equal(server.notes.pt_recipients, 0)
109
+ done()
110
+ }, server)
111
+ })
112
+ })
113
+
114
+ describe('hook_init_master', () => {
115
+ it('initializes server notes and calls next (no cluster)', (t, done) => {
116
+ const server = { notes: new Notes(), cluster: null }
117
+ plugin.hook_init_master((rc) => {
118
+ clearInterval(plugin._interval)
119
+ assert.equal(rc, undefined)
120
+ assert.equal(server.notes.pt_connections, 0)
121
+ assert.equal(server.notes.pt_child_exits, 0)
122
+ done()
123
+ }, server)
124
+ })
125
+ })
126
+
127
+ describe('shutdown', () => {
128
+ it('clears the interval', () => {
129
+ plugin._interval = setInterval(() => {}, 9999)
130
+ plugin.shutdown()
131
+ // If the interval was cleared, no error thrown
132
+ assert.ok(true)
133
+ })
134
+ })
135
+ })
@@ -0,0 +1,99 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+ const path = require('node:path')
5
+ const Module = require('node:module')
6
+ const { describe, it, beforeEach, before, after } = require('node:test')
7
+
8
+ const fixtures = require('haraka-test-fixtures')
9
+
10
+ // deliver.js does `require('./outbound')` at the top level. In a running
11
+ // Haraka that resolves to the core outbound module (Haraka/outbound/index.js),
12
+ // which we don't want to load here: it would pull in the real delivery
13
+ // machinery. So we intercept Module._resolveFilename to map './outbound' to a
14
+ // stable path and pre-populate the require cache with a mock at that path
15
+ // before loading the plugin.
16
+ const outboundPath = path.resolve('outbound/index.js')
17
+ let mockOutbound
18
+ let origResolve
19
+
20
+ before(() => {
21
+ require('haraka-constants').import(global)
22
+
23
+ mockOutbound = { send_trans_email: () => {} }
24
+ require.cache[outboundPath] = {
25
+ id: outboundPath,
26
+ filename: outboundPath,
27
+ loaded: true,
28
+ exports: mockOutbound,
29
+ }
30
+
31
+ origResolve = Module._resolveFilename
32
+ Module._resolveFilename = function (request, parent, isMain, options) {
33
+ if (request === './outbound') return outboundPath
34
+ return origResolve.call(this, request, parent, isMain, options)
35
+ }
36
+ })
37
+
38
+ after(() => {
39
+ Module._resolveFilename = origResolve
40
+ delete require.cache[outboundPath]
41
+ })
42
+
43
+ function makeConnection(opts = {}) {
44
+ const conn = fixtures.connection.createConnection()
45
+ conn.init_transaction()
46
+ if (opts.relaying !== undefined) conn.relaying = opts.relaying
47
+ return conn
48
+ }
49
+
50
+ describe('queue/deliver', () => {
51
+ describe('hook_queue_outbound', () => {
52
+ let plugin, conn
53
+
54
+ beforeEach(() => {
55
+ plugin = new fixtures.plugin('queue/deliver')
56
+ mockOutbound.send_trans_email = () => {}
57
+ })
58
+
59
+ it('calls next() when connection is not relaying', (t, done) => {
60
+ conn = makeConnection({ relaying: false })
61
+ plugin.hook_queue_outbound((rc) => {
62
+ assert.equal(rc, undefined)
63
+ done()
64
+ }, conn)
65
+ })
66
+
67
+ it('calls next() when connection is undefined', (t, done) => {
68
+ plugin.hook_queue_outbound((rc) => {
69
+ assert.equal(rc, undefined)
70
+ done()
71
+ }, undefined)
72
+ })
73
+
74
+ it('calls outbound.send_trans_email when relaying is true', (t, done) => {
75
+ conn = makeConnection({ relaying: true })
76
+ mockOutbound.send_trans_email = (txn, next) => {
77
+ assert.equal(txn, conn.transaction)
78
+ next(OK)
79
+ }
80
+ plugin.hook_queue_outbound((rc) => {
81
+ assert.equal(rc, OK)
82
+ done()
83
+ }, conn)
84
+ })
85
+
86
+ it('passes transaction to outbound.send_trans_email', (t, done) => {
87
+ conn = makeConnection({ relaying: true })
88
+ let capturedTxn
89
+ mockOutbound.send_trans_email = (txn, next) => {
90
+ capturedTxn = txn
91
+ next(OK)
92
+ }
93
+ plugin.hook_queue_outbound(() => {
94
+ assert.equal(capturedTxn, conn.transaction)
95
+ done()
96
+ }, conn)
97
+ })
98
+ })
99
+ })
@@ -0,0 +1,79 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+ const { describe, it, beforeEach, before } = require('node:test')
5
+
6
+ const fixtures = require('haraka-test-fixtures')
7
+
8
+ before(() => {
9
+ require('haraka-constants').import(global)
10
+ })
11
+
12
+ describe('queue/discard', () => {
13
+ describe('discard hook', () => {
14
+ let plugin, conn
15
+
16
+ beforeEach(() => {
17
+ plugin = new fixtures.plugin('queue/discard')
18
+ conn = fixtures.connection.createConnection()
19
+ conn.init_transaction()
20
+ delete process.env.YES_REALLY_DO_DISCARD
21
+ })
22
+
23
+ it('calls next() when queue.wants is set to another plugin', (t, done) => {
24
+ conn.transaction.notes.set('queue.wants', 'smtp_forward')
25
+ plugin.discard((rc) => {
26
+ assert.equal(rc, undefined)
27
+ done()
28
+ }, conn)
29
+ })
30
+
31
+ it('calls next(OK) when connection.notes.discard is set', (t, done) => {
32
+ conn.notes.discard = true
33
+ plugin.discard((rc) => {
34
+ assert.equal(rc, OK)
35
+ done()
36
+ }, conn)
37
+ })
38
+
39
+ it('calls next(OK) when txn.notes.discard is set', (t, done) => {
40
+ conn.transaction.notes.discard = true
41
+ plugin.discard((rc) => {
42
+ assert.equal(rc, OK)
43
+ done()
44
+ }, conn)
45
+ })
46
+
47
+ it('calls next(OK) when queue.wants is discard', (t, done) => {
48
+ conn.transaction.notes.set('queue.wants', 'discard')
49
+ plugin.discard((rc) => {
50
+ assert.equal(rc, OK)
51
+ done()
52
+ }, conn)
53
+ })
54
+
55
+ it('calls next(OK) when YES_REALLY_DO_DISCARD env var is set', (t, done) => {
56
+ process.env.YES_REALLY_DO_DISCARD = '1'
57
+ plugin.discard((rc) => {
58
+ assert.equal(rc, OK)
59
+ done()
60
+ }, conn)
61
+ })
62
+
63
+ it('calls next() (pass-through) when no discard conditions are met', (t, done) => {
64
+ plugin.discard((rc) => {
65
+ assert.equal(rc, undefined)
66
+ done()
67
+ }, conn)
68
+ })
69
+
70
+ it('queue.wants=discard takes priority over other queue wants', (t, done) => {
71
+ // Once queue.wants is set, it's compared against 'discard'; since it equals 'discard', discard runs
72
+ conn.transaction.notes.set('queue.wants', 'discard')
73
+ plugin.discard((rc) => {
74
+ assert.equal(rc, OK)
75
+ done()
76
+ }, conn)
77
+ })
78
+ })
79
+ })
@@ -0,0 +1,138 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+ const { describe, it, beforeEach, before } = require('node:test')
5
+
6
+ const fixtures = require('haraka-test-fixtures')
7
+
8
+ before(() => {
9
+ require('haraka-constants').import(global)
10
+ })
11
+
12
+ describe('queue/lmtp', () => {
13
+ describe('hook_get_mx', () => {
14
+ let plugin
15
+
16
+ beforeEach(() => {
17
+ plugin = new fixtures.plugin('queue/lmtp')
18
+ plugin.load_lmtp_ini()
19
+ })
20
+
21
+ it('calls next() when hmail does not have using_lmtp note', (t, done) => {
22
+ const hmail = { todo: { notes: {} } }
23
+ plugin.hook_get_mx(
24
+ (rc, mx) => {
25
+ assert.equal(rc, undefined)
26
+ assert.equal(mx, undefined)
27
+ done()
28
+ },
29
+ hmail,
30
+ 'example.com',
31
+ )
32
+ })
33
+
34
+ it('returns OK with default host and port when using_lmtp is set', (t, done) => {
35
+ const hmail = { todo: { notes: { using_lmtp: true } } }
36
+ plugin.cfg = { main: {} }
37
+ plugin.hook_get_mx(
38
+ (rc, mx) => {
39
+ assert.equal(rc, OK)
40
+ assert.equal(mx.using_lmtp, true)
41
+ assert.equal(mx.exchange, '127.0.0.1')
42
+ assert.equal(mx.port, 24)
43
+ assert.equal(mx.priority, 0)
44
+ done()
45
+ },
46
+ hmail,
47
+ 'example.com',
48
+ )
49
+ })
50
+
51
+ it('uses domain-specific section when available', (t, done) => {
52
+ const hmail = { todo: { notes: { using_lmtp: true } } }
53
+ plugin.cfg = {
54
+ main: { host: '127.0.0.1' },
55
+ 'example.com': { host: 'lmtp.example.com', port: 2400 },
56
+ }
57
+ plugin.hook_get_mx(
58
+ (rc, mx) => {
59
+ assert.equal(rc, OK)
60
+ assert.equal(mx.exchange, 'lmtp.example.com')
61
+ assert.equal(mx.port, 2400)
62
+ done()
63
+ },
64
+ hmail,
65
+ 'example.com',
66
+ )
67
+ })
68
+
69
+ it('falls back to main section when no domain-specific section', (t, done) => {
70
+ const hmail = { todo: { notes: { using_lmtp: true } } }
71
+ plugin.cfg = { main: { host: 'lmtp.default.com', port: 24 } }
72
+ plugin.hook_get_mx(
73
+ (rc, mx) => {
74
+ assert.equal(rc, OK)
75
+ assert.equal(mx.exchange, 'lmtp.default.com')
76
+ done()
77
+ },
78
+ hmail,
79
+ 'other.com',
80
+ )
81
+ })
82
+
83
+ it('includes path in mx when configured', (t, done) => {
84
+ const hmail = { todo: { notes: { using_lmtp: true } } }
85
+ plugin.cfg = { main: { host: '127.0.0.1', path: '/var/run/lmtp.sock' } }
86
+ plugin.hook_get_mx(
87
+ (rc, mx) => {
88
+ assert.equal(rc, OK)
89
+ assert.equal(mx.path, '/var/run/lmtp.sock')
90
+ done()
91
+ },
92
+ hmail,
93
+ 'example.com',
94
+ )
95
+ })
96
+
97
+ it('does not include path in mx when not configured', (t, done) => {
98
+ const hmail = { todo: { notes: { using_lmtp: true } } }
99
+ plugin.cfg = { main: {} }
100
+ plugin.hook_get_mx(
101
+ (rc, mx) => {
102
+ assert.equal(rc, OK)
103
+ assert.equal(mx.path, undefined)
104
+ done()
105
+ },
106
+ hmail,
107
+ 'example.com',
108
+ )
109
+ })
110
+ })
111
+
112
+ describe('hook_queue', () => {
113
+ let plugin, conn
114
+
115
+ beforeEach(() => {
116
+ plugin = new fixtures.plugin('queue/lmtp')
117
+ plugin.load_lmtp_ini()
118
+ conn = fixtures.connection.createConnection()
119
+ conn.init_transaction()
120
+ })
121
+
122
+ it('calls next() when there is no transaction', (t, done) => {
123
+ const connNoTxn = fixtures.connection.createConnection()
124
+ plugin.hook_queue((rc) => {
125
+ assert.equal(rc, undefined)
126
+ done()
127
+ }, connNoTxn)
128
+ })
129
+
130
+ it('calls next() when queue.wants is set to another plugin', (t, done) => {
131
+ conn.transaction.notes.set('queue.wants', 'smtp_forward')
132
+ plugin.hook_queue((rc) => {
133
+ assert.equal(rc, undefined)
134
+ done()
135
+ }, conn)
136
+ })
137
+ })
138
+ })