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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Testing bounce email contents related to errors occurring during SMTP dialog.
|
|
4
|
+
// Strategy: create an HMailItem via fixtures, invoke an outbound method, then
|
|
5
|
+
// verify that the correct bounce/temp_fail handler is called.
|
|
6
|
+
|
|
7
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
8
|
+
const assert = require('node:assert')
|
|
9
|
+
const dns = require('node:dns')
|
|
10
|
+
const fs = require('node:fs')
|
|
11
|
+
const path = require('node:path')
|
|
12
|
+
|
|
13
|
+
const constants = require('haraka-constants')
|
|
14
|
+
|
|
15
|
+
// Load outbound/index FIRST to avoid the circular-dependency boot-order issue:
|
|
16
|
+
// hmail.js → require('./index') while index.js is still loading causes queue.js
|
|
17
|
+
// to capture a stale (empty) module.exports for hmail.js.
|
|
18
|
+
const outbound = require('../../outbound')
|
|
19
|
+
const HMailItem = outbound.HMailItem
|
|
20
|
+
const TODOItem = require('../../outbound/todo')
|
|
21
|
+
|
|
22
|
+
const util_hmailitem = require('../fixtures/util_hmailitem')
|
|
23
|
+
|
|
24
|
+
const outbound_context = { TODOItem, exports: outbound }
|
|
25
|
+
const queue_dir = path.resolve(__dirname, '../test-queue')
|
|
26
|
+
|
|
27
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const ensureQueueDir = () =>
|
|
30
|
+
new Promise((resolve, reject) => {
|
|
31
|
+
fs.exists(queue_dir, (exists) => {
|
|
32
|
+
if (exists) return resolve()
|
|
33
|
+
fs.mkdir(queue_dir, (err) => (err ? reject(err) : resolve()))
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const cleanQueueDir = () =>
|
|
38
|
+
new Promise((resolve, reject) => {
|
|
39
|
+
fs.exists(queue_dir, (exists) => {
|
|
40
|
+
if (!exists) return resolve()
|
|
41
|
+
try {
|
|
42
|
+
for (const file of fs.readdirSync(queue_dir)) {
|
|
43
|
+
const full = path.resolve(queue_dir, file)
|
|
44
|
+
if (fs.lstatSync(full).isDirectory()) return reject(new Error(`unexpected subdirectory: ${full}`))
|
|
45
|
+
fs.unlinkSync(full)
|
|
46
|
+
}
|
|
47
|
+
resolve()
|
|
48
|
+
} catch (err) {
|
|
49
|
+
reject(err)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/** Creates a mock HMailItem, resolving with it or rejecting on error. */
|
|
55
|
+
const mockHMailItem = (ctx, opts = {}) =>
|
|
56
|
+
new Promise((resolve, reject) => {
|
|
57
|
+
util_hmailitem.newMockHMailItem(ctx, reject, opts, resolve)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe('outbound_bounce_net_errors', () => {
|
|
63
|
+
beforeEach(ensureQueueDir)
|
|
64
|
+
afterEach(cleanQueueDir)
|
|
65
|
+
|
|
66
|
+
it('get_mx=DENY triggers bounce with dsn_status 5.1.2', async () => {
|
|
67
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
const orig = HMailItem.prototype.bounce
|
|
70
|
+
HMailItem.prototype.bounce = function (err, opts) {
|
|
71
|
+
try {
|
|
72
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
|
|
73
|
+
resolve()
|
|
74
|
+
} catch (e) {
|
|
75
|
+
reject(e)
|
|
76
|
+
} finally {
|
|
77
|
+
HMailItem.prototype.bounce = orig
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
mock_hmail.domain = mock_hmail.todo.domain
|
|
81
|
+
HMailItem.prototype.get_mx_respond.apply(mock_hmail, [constants.deny, {}])
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('get_mx=DENYSOFT triggers temp_fail with dsn_status 4.1.2', async () => {
|
|
86
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
const orig = HMailItem.prototype.temp_fail
|
|
89
|
+
HMailItem.prototype.temp_fail = function (err, opts) {
|
|
90
|
+
try {
|
|
91
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '4.1.2', 'dsn status')
|
|
92
|
+
resolve()
|
|
93
|
+
} catch (e) {
|
|
94
|
+
reject(e)
|
|
95
|
+
} finally {
|
|
96
|
+
HMailItem.prototype.temp_fail = orig
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
mock_hmail.domain = mock_hmail.todo.domain
|
|
100
|
+
HMailItem.prototype.get_mx_respond.apply(mock_hmail, [constants.denysoft, {}])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('get_mx_error({code:NXDOMAIN}) triggers bounce with dsn_status 5.1.2', async () => {
|
|
105
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
106
|
+
await new Promise((resolve, reject) => {
|
|
107
|
+
const orig = HMailItem.prototype.bounce
|
|
108
|
+
HMailItem.prototype.bounce = function (err, opts) {
|
|
109
|
+
try {
|
|
110
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
|
|
111
|
+
resolve()
|
|
112
|
+
} catch (e) {
|
|
113
|
+
reject(e)
|
|
114
|
+
} finally {
|
|
115
|
+
HMailItem.prototype.bounce = orig
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
HMailItem.prototype.get_mx_error.apply(mock_hmail, [{ code: dns.NXDOMAIN }])
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("get_mx_error({code:'SOME-OTHER-ERR'}) triggers temp_fail with dsn_status 4.1.0", async () => {
|
|
123
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
const orig = HMailItem.prototype.temp_fail
|
|
126
|
+
HMailItem.prototype.temp_fail = function (err, opts) {
|
|
127
|
+
try {
|
|
128
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '4.1.0', 'dsn status')
|
|
129
|
+
resolve()
|
|
130
|
+
} catch (e) {
|
|
131
|
+
reject(e)
|
|
132
|
+
} finally {
|
|
133
|
+
HMailItem.prototype.temp_fail = orig
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
HMailItem.prototype.get_mx_error.apply(mock_hmail, [{ code: 'SOME-OTHER-ERR' }, {}])
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("found_mx(null, [{priority:0,exchange:''}]) triggers bounce with dsn_status 5.1.2", async () => {
|
|
141
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
const orig = HMailItem.prototype.bounce
|
|
144
|
+
HMailItem.prototype.bounce = function (err, opts) {
|
|
145
|
+
try {
|
|
146
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
|
|
147
|
+
resolve()
|
|
148
|
+
} catch (e) {
|
|
149
|
+
reject(e)
|
|
150
|
+
} finally {
|
|
151
|
+
HMailItem.prototype.bounce = orig
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
HMailItem.prototype.found_mx.apply(mock_hmail, [[{ priority: 0, exchange: '' }]])
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('try_deliver with empty mxlist triggers temp_fail with dsn_status 5.1.2', async () => {
|
|
159
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
160
|
+
mock_hmail.mxlist = []
|
|
161
|
+
await new Promise((resolve, reject) => {
|
|
162
|
+
const orig = HMailItem.prototype.temp_fail
|
|
163
|
+
HMailItem.prototype.temp_fail = function (err, opts) {
|
|
164
|
+
try {
|
|
165
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
|
|
166
|
+
resolve()
|
|
167
|
+
} catch (e) {
|
|
168
|
+
reject(e)
|
|
169
|
+
} finally {
|
|
170
|
+
HMailItem.prototype.temp_fail = orig
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
HMailItem.prototype.try_deliver.apply(mock_hmail, [])
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Testing bounce email contents related to errors occurring during SMTP dialog.
|
|
4
|
+
// These tests simulate a remote SMTP server responding with various error codes
|
|
5
|
+
// and verify that Haraka generates correctly formatted RFC 3464 DSN bounce messages.
|
|
6
|
+
|
|
7
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
8
|
+
const assert = require('node:assert')
|
|
9
|
+
const fs = require('node:fs')
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
|
|
12
|
+
// Load outbound/index FIRST to avoid the circular-dependency boot-order issue.
|
|
13
|
+
const outbound = require('../../outbound')
|
|
14
|
+
const HMailItem = outbound.HMailItem
|
|
15
|
+
const TODOItem = require('../../outbound/todo')
|
|
16
|
+
const obc = require('../../outbound/config')
|
|
17
|
+
|
|
18
|
+
const util_hmailitem = require('../fixtures/util_hmailitem')
|
|
19
|
+
const mock_sock = require('../fixtures/line_socket')
|
|
20
|
+
|
|
21
|
+
obc.cfg.pool_concurrency_max = 0
|
|
22
|
+
|
|
23
|
+
const outbound_context = { TODOItem, exports: outbound }
|
|
24
|
+
const queue_dir = path.resolve(__dirname, '../test-queue')
|
|
25
|
+
|
|
26
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const ensureQueueDir = () =>
|
|
29
|
+
new Promise((resolve, reject) => {
|
|
30
|
+
fs.exists(queue_dir, (exists) => {
|
|
31
|
+
if (exists) return resolve()
|
|
32
|
+
fs.mkdir(queue_dir, (err) => (err ? reject(err) : resolve()))
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const cleanQueueDir = () =>
|
|
37
|
+
new Promise((resolve, reject) => {
|
|
38
|
+
fs.exists(queue_dir, (exists) => {
|
|
39
|
+
if (!exists) return resolve()
|
|
40
|
+
try {
|
|
41
|
+
for (const file of fs.readdirSync(queue_dir)) {
|
|
42
|
+
const full = path.resolve(queue_dir, file)
|
|
43
|
+
if (fs.lstatSync(full).isDirectory()) return reject(new Error(`unexpected subdirectory: ${full}`))
|
|
44
|
+
fs.unlinkSync(full)
|
|
45
|
+
}
|
|
46
|
+
resolve()
|
|
47
|
+
} catch (err) {
|
|
48
|
+
reject(err)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const mockHMailItem = (ctx, opts = {}) =>
|
|
54
|
+
new Promise((resolve, reject) => {
|
|
55
|
+
util_hmailitem.newMockHMailItem(ctx, reject, opts, resolve)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe('outbound_bounce_rfc3464', () => {
|
|
61
|
+
beforeEach(ensureQueueDir)
|
|
62
|
+
afterEach(cleanQueueDir)
|
|
63
|
+
|
|
64
|
+
it('MAIL FROM 500 triggers RFC3464 bounce with status 5.0.0', async () => {
|
|
65
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
66
|
+
const mock_socket = mock_sock.connect('testhost', 'testport')
|
|
67
|
+
mock_socket.writable = true
|
|
68
|
+
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
const orig = outbound_context.exports.send_email
|
|
71
|
+
outbound_context.exports.send_email = (from, to, contents, cb, opts) => {
|
|
72
|
+
try {
|
|
73
|
+
assert.match(contents, /^Content-type: message\/delivery-status/m)
|
|
74
|
+
assert.match(contents, /^Final-Recipient: rfc822;recipient@domain/m)
|
|
75
|
+
assert.match(contents, /^Action: failed/m)
|
|
76
|
+
assert.match(contents, /^Status: 5\.0\.0/m)
|
|
77
|
+
assert.match(contents, /Absolutely not acceptable\. Basic Test Only\./)
|
|
78
|
+
resolve()
|
|
79
|
+
} catch (e) {
|
|
80
|
+
reject(e)
|
|
81
|
+
} finally {
|
|
82
|
+
outbound_context.exports.send_email = orig
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
util_hmailitem.playTestSmtpConversation(
|
|
86
|
+
mock_hmail,
|
|
87
|
+
mock_socket,
|
|
88
|
+
reject,
|
|
89
|
+
[
|
|
90
|
+
{ from: 'remote', line: '220 testing-smtp' },
|
|
91
|
+
{ from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
|
|
92
|
+
{ from: 'remote', line: '220-testing-smtp' },
|
|
93
|
+
{ from: 'remote', line: '220 8BITMIME' },
|
|
94
|
+
{ from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
|
|
95
|
+
{ from: 'remote', line: '500 5.0.0 Absolutely not acceptable. Basic Test Only.' },
|
|
96
|
+
{ from: 'haraka', test: 'QUIT', end_test: true },
|
|
97
|
+
],
|
|
98
|
+
() => {},
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('early 3XX response triggers temp_fail with status 3.0.0', async () => {
|
|
104
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
105
|
+
const mock_socket = mock_sock.connect('testhost', 'testport')
|
|
106
|
+
mock_socket.writable = true
|
|
107
|
+
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
const orig = HMailItem.prototype.temp_fail
|
|
110
|
+
HMailItem.prototype.temp_fail = function (err, opts) {
|
|
111
|
+
try {
|
|
112
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '3.0.0')
|
|
113
|
+
assert.equal(this.todo.rcpt_to[0].dsn_action, 'delayed')
|
|
114
|
+
assert.match(this.todo.rcpt_to[0].dsn_smtp_response, /No time for you right now/)
|
|
115
|
+
resolve()
|
|
116
|
+
} catch (e) {
|
|
117
|
+
reject(e)
|
|
118
|
+
} finally {
|
|
119
|
+
HMailItem.prototype.temp_fail = orig
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
util_hmailitem.playTestSmtpConversation(
|
|
123
|
+
mock_hmail,
|
|
124
|
+
mock_socket,
|
|
125
|
+
reject,
|
|
126
|
+
[
|
|
127
|
+
{ from: 'remote', line: '220 testing-smtp' },
|
|
128
|
+
{ from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
|
|
129
|
+
{ from: 'remote', line: '220-testing-smtp' },
|
|
130
|
+
{ from: 'remote', line: '220 8BITMIME' },
|
|
131
|
+
{ from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
|
|
132
|
+
{ from: 'remote', line: '300 3.0.0 No time for you right now' },
|
|
133
|
+
{ from: 'haraka', test: 'QUIT', end_test: true },
|
|
134
|
+
],
|
|
135
|
+
() => {},
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('RCPT-TO 4XX triggers temp_fail with status 4.0.0', async () => {
|
|
141
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
142
|
+
const mock_socket = mock_sock.connect('testhost', 'testport')
|
|
143
|
+
mock_socket.writable = true
|
|
144
|
+
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
const orig = HMailItem.prototype.temp_fail
|
|
147
|
+
HMailItem.prototype.temp_fail = function (err, opts) {
|
|
148
|
+
try {
|
|
149
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '4.0.0')
|
|
150
|
+
assert.equal(this.todo.rcpt_to[0].dsn_action, 'delayed')
|
|
151
|
+
assert.match(this.todo.rcpt_to[0].dsn_smtp_response, /Currently not available\. Try again later\./)
|
|
152
|
+
resolve()
|
|
153
|
+
} catch (e) {
|
|
154
|
+
reject(e)
|
|
155
|
+
} finally {
|
|
156
|
+
HMailItem.prototype.temp_fail = orig
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
util_hmailitem.playTestSmtpConversation(
|
|
160
|
+
mock_hmail,
|
|
161
|
+
mock_socket,
|
|
162
|
+
reject,
|
|
163
|
+
[
|
|
164
|
+
{ from: 'remote', line: '220 testing-smtp' },
|
|
165
|
+
{ from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
|
|
166
|
+
{ from: 'remote', line: '220-testing-smtp' },
|
|
167
|
+
{ from: 'remote', line: '220 8BITMIME' },
|
|
168
|
+
{ from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
|
|
169
|
+
{ from: 'remote', line: '250 2.1.0 Ok' },
|
|
170
|
+
{ from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
|
|
171
|
+
{ from: 'remote', line: '400 4.0.0 Currently not available. Try again later.' },
|
|
172
|
+
{ from: 'haraka', test: 'QUIT', end_test: true },
|
|
173
|
+
],
|
|
174
|
+
() => {},
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('DATA 4XX triggers temp_fail with status 4.6.0', async () => {
|
|
180
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
181
|
+
const mock_socket = mock_sock.connect('testhost', 'testport')
|
|
182
|
+
mock_socket.writable = true
|
|
183
|
+
|
|
184
|
+
await new Promise((resolve, reject) => {
|
|
185
|
+
const orig = HMailItem.prototype.temp_fail
|
|
186
|
+
HMailItem.prototype.temp_fail = function (err, opts) {
|
|
187
|
+
try {
|
|
188
|
+
assert.equal(this.todo.rcpt_to[0].dsn_status, '4.6.0')
|
|
189
|
+
assert.equal(this.todo.rcpt_to[0].dsn_action, 'delayed')
|
|
190
|
+
assert.match(this.todo.rcpt_to[0].dsn_smtp_response, /Currently I do not like ascii art cats\./)
|
|
191
|
+
resolve()
|
|
192
|
+
} catch (e) {
|
|
193
|
+
reject(e)
|
|
194
|
+
} finally {
|
|
195
|
+
HMailItem.prototype.temp_fail = orig
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
util_hmailitem.playTestSmtpConversation(
|
|
199
|
+
mock_hmail,
|
|
200
|
+
mock_socket,
|
|
201
|
+
reject,
|
|
202
|
+
[
|
|
203
|
+
{ from: 'remote', line: '220 testing-smtp' },
|
|
204
|
+
{ from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
|
|
205
|
+
{ from: 'remote', line: '220-testing-smtp' },
|
|
206
|
+
{ from: 'remote', line: '220 8BITMIME' },
|
|
207
|
+
{ from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
|
|
208
|
+
{ from: 'remote', line: '250 2.1.0 Ok' },
|
|
209
|
+
{ from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
|
|
210
|
+
{ from: 'remote', line: '250 2.1.5 Ok' },
|
|
211
|
+
{ from: 'haraka', test: 'DATA' },
|
|
212
|
+
{ from: 'remote', line: '450 4.6.0 Currently I do not like ascii art cats.' },
|
|
213
|
+
{ from: 'haraka', test: 'QUIT', end_test: true },
|
|
214
|
+
],
|
|
215
|
+
() => {},
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('RCPT-TO 5XX triggers RFC3464 bounce with status 5.1.1', async () => {
|
|
221
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
222
|
+
const mock_socket = mock_sock.connect('testhost', 'testport')
|
|
223
|
+
mock_socket.writable = true
|
|
224
|
+
|
|
225
|
+
await new Promise((resolve, reject) => {
|
|
226
|
+
const orig = outbound_context.exports.send_email
|
|
227
|
+
outbound_context.exports.send_email = (from, to, contents, cb, opts) => {
|
|
228
|
+
try {
|
|
229
|
+
assert.match(contents, /^Content-type: message\/delivery-status/m)
|
|
230
|
+
assert.match(contents, /^Final-Recipient: rfc822;recipient@domain/m)
|
|
231
|
+
assert.match(contents, /^Action: failed/m)
|
|
232
|
+
assert.match(contents, /^Status: 5\.1\.1/m)
|
|
233
|
+
assert.match(contents, /Not available and will not come back/)
|
|
234
|
+
resolve()
|
|
235
|
+
} catch (e) {
|
|
236
|
+
reject(e)
|
|
237
|
+
} finally {
|
|
238
|
+
outbound_context.exports.send_email = orig
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
util_hmailitem.playTestSmtpConversation(
|
|
242
|
+
mock_hmail,
|
|
243
|
+
mock_socket,
|
|
244
|
+
reject,
|
|
245
|
+
[
|
|
246
|
+
{ from: 'remote', line: '220 testing-smtp' },
|
|
247
|
+
{ from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
|
|
248
|
+
{ from: 'remote', line: '220-testing-smtp' },
|
|
249
|
+
{ from: 'remote', line: '220 8BITMIME' },
|
|
250
|
+
{ from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
|
|
251
|
+
{ from: 'remote', line: '250 2.1.0 Ok' },
|
|
252
|
+
{ from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
|
|
253
|
+
{ from: 'remote', line: '550 5.1.1 Not available and will not come back' },
|
|
254
|
+
{ from: 'haraka', test: 'QUIT', end_test: true },
|
|
255
|
+
],
|
|
256
|
+
() => {},
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('DATA 5XX triggers RFC3464 bounce with status 5.6.0', async () => {
|
|
262
|
+
const mock_hmail = await mockHMailItem(outbound_context)
|
|
263
|
+
const mock_socket = mock_sock.connect('testhost', 'testport')
|
|
264
|
+
mock_socket.writable = true
|
|
265
|
+
|
|
266
|
+
await new Promise((resolve, reject) => {
|
|
267
|
+
const orig = outbound_context.exports.send_email
|
|
268
|
+
outbound_context.exports.send_email = (from, to, contents, cb, opts) => {
|
|
269
|
+
try {
|
|
270
|
+
assert.match(contents, /^Content-type: message\/delivery-status/m)
|
|
271
|
+
assert.match(contents, /^Final-Recipient: rfc822;recipient@domain/m)
|
|
272
|
+
assert.match(contents, /^Action: failed/m)
|
|
273
|
+
assert.match(contents, /^Status: 5\.6\.0/m)
|
|
274
|
+
assert.match(contents, /I never did and will like ascii art cats/)
|
|
275
|
+
resolve()
|
|
276
|
+
} catch (e) {
|
|
277
|
+
reject(e)
|
|
278
|
+
} finally {
|
|
279
|
+
outbound_context.exports.send_email = orig
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
util_hmailitem.playTestSmtpConversation(
|
|
283
|
+
mock_hmail,
|
|
284
|
+
mock_socket,
|
|
285
|
+
reject,
|
|
286
|
+
[
|
|
287
|
+
{ from: 'remote', line: '220 testing-smtp' },
|
|
288
|
+
{ from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
|
|
289
|
+
{ from: 'remote', line: '220-testing-smtp' },
|
|
290
|
+
{ from: 'remote', line: '220 8BITMIME' },
|
|
291
|
+
{ from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
|
|
292
|
+
{ from: 'remote', line: '250 2.1.0 Ok' },
|
|
293
|
+
{ from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
|
|
294
|
+
{ from: 'remote', line: '250 2.1.5 Ok' },
|
|
295
|
+
{ from: 'haraka', test: 'DATA' },
|
|
296
|
+
{ from: 'remote', line: '550 5.6.0 I never did and will like ascii art cats.' },
|
|
297
|
+
{ from: 'haraka', test: 'QUIT', end_test: true },
|
|
298
|
+
],
|
|
299
|
+
() => {},
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
})
|