• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

msimerson / Haraka / 26729698662

01 Jun 2026 12:59AM UTC coverage: 67.641% (-1.0%) from 68.622%
26729698662

Pull #32

github

web-flow
Merge d4ff4e5f9 into 56e744da7
Pull Request #32: chore(deps): bump the production-dependencies group across 1 directory with 4 updates

1424 of 1952 branches covered (72.95%)

6482 of 9583 relevant lines covered (67.64%)

22.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.88
/plugins/auth/auth_base.js
1
// Base authentication plugin.
6✔
2
// This cannot be used on its own. You need to inherit from it.
6✔
3
// See plugins/auth/flat_file.js for an example.
6✔
4

6✔
5
// Note: You can disable setting `connection.notes.auth_passwd` by `plugin.blankout_password = true`
6✔
6

6✔
7
const crypto = require('node:crypto')
6✔
8

6✔
9
const tlds = require('haraka-tld')
6✔
10
const utils = require('haraka-utils')
6✔
11

6✔
12
const AUTH_COMMAND = 'AUTH'
6✔
13
const AUTH_METHOD_CRAM_MD5 = 'CRAM-MD5'
6✔
14
const AUTH_METHOD_PLAIN = 'PLAIN'
6✔
15
const AUTH_METHOD_LOGIN = 'LOGIN'
6✔
16
const LOGIN_STRING1 = 'VXNlcm5hbWU6' //Username: base64 coded
6✔
17
const LOGIN_STRING2 = 'UGFzc3dvcmQ6' //Password: base64 coded
6✔
18

6✔
19
exports.hook_capabilities = (next, connection) => {
6✔
20
    // Don't offer AUTH capabilities unless session is encrypted
2✔
21
    if (!connection.tls.enabled) return next()
2✔
22

2✔
23
    const methods = ['PLAIN', 'LOGIN', 'CRAM-MD5']
1✔
24
    connection.capabilities.push(`AUTH ${methods.join(' ')}`)
1✔
25
    connection.notes.allowed_auth_methods = methods
1✔
26
    next()
1✔
27
}
1✔
28

1✔
29
// Override this at a minimum. Run cb(passwd) to provide a password.
6✔
30
exports.get_plain_passwd = (user, connection, cb) => cb()
6✔
31

×
32
exports.hook_unrecognized_command = function (next, connection, params) {
6✔
33
    if (params[0].toUpperCase() === AUTH_COMMAND && params[1]) {
13✔
34
        return this.select_auth_method(next, connection, params.slice(1).join(' '))
13✔
35
    }
8✔
36
    if (!connection.notes.authenticating) return next()
13✔
37

5!
38
    const am = connection.notes.auth_method
13!
39
    if (am === AUTH_METHOD_CRAM_MD5 && connection.notes.auth_ticket) {
13!
40
        return this.auth_cram_md5(next, connection, params)
13!
41
    }
×
42
    if (am === AUTH_METHOD_LOGIN) {
13!
43
        return this.auth_login(next, connection, params)
13✔
44
    }
4✔
45
    if (am === AUTH_METHOD_PLAIN) {
13✔
46
        return this.auth_plain(next, connection, params)
1✔
47
    }
1✔
48
    next()
1✔
49
}
1✔
50

1✔
51
exports.check_plain_passwd = function (connection, user, passwd, cb) {
6!
52
    function callback(plain_pw) {
15✔
53
        cb(plain_pw === null ? false : plain_pw === passwd)
15✔
54
    }
13✔
55
    if (this.get_plain_passwd.length == 2) {
15✔
56
        this.get_plain_passwd(user, callback)
15✔
57
    } else if (this.get_plain_passwd.length == 3) {
15✔
58
        this.get_plain_passwd(user, connection, callback)
5✔
59
    } else {
5✔
60
        throw 'Invalid number of arguments for get_plain_passwd'
5!
61
    }
×
62
}
×
63

×
64
exports.check_cram_md5_passwd = function (connection, user, passwd, cb) {
6✔
65
    function callback(plain_pw) {
✔
66
        if (plain_pw == null) return cb(false)
2!
67

×
68
        const hmac = crypto.createHmac('md5', plain_pw.toString())
2✔
69
        hmac.update(connection.notes.auth_ticket)
2✔
70

2✔
71
        if (hmac.digest('hex') === passwd) return cb(true)
2✔
72

1✔
73
        cb(false)
1✔
74
    }
1✔
75
    if (this.get_plain_passwd.length == 2) {
×
76
        this.get_plain_passwd(user, callback)
×
77
    } else if (this.get_plain_passwd.length == 3) {
×
78
        this.get_plain_passwd(user, connection, callback)
×
79
    } else {
×
80
        throw 'Invalid number of arguments for get_plain_passwd'
×
81
    }
×
82
}
×
83

×
84
exports.check_user = function (next, connection, credentials, method) {
6✔
85
    const plugin = this
19✔
86
    connection.notes.authenticating = false
19✔
87
    if (!(credentials[0] && credentials[1])) {
19✔
88
        connection.respond(504, 'Invalid AUTH string', () => {
19!
89
            connection.reset_transaction(() => next(OK))
1✔
90
        })
1✔
91
        return
1✔
92
    }
1✔
93

1✔
94
    // valid: (true|false)
19✔
95
    // opts: ({ message, code }|String)
18✔
96
    function passwd_ok(valid, opts) {
18✔
97
        const status_code = (typeof opts === 'object' && opts.code) || (valid ? 235 : 535)
18!
98
        const status_message =
18✔
99
            (typeof opts === 'object' ? opts.message : opts) ||
18!
100
            (valid ? '2.7.0 Authentication successful' : '5.7.8 Authentication failed')
18✔
101

3✔
102
        if (valid) {
18✔
103
            connection.relaying = true
18✔
104
            connection.results.add({ name: 'relay' }, { pass: plugin.name })
13✔
105

13✔
106
            connection.results.add(
13✔
107
                { name: 'auth' },
13✔
108
                {
13✔
109
                    pass: plugin.name,
13✔
110
                    method,
13✔
111
                    user: credentials[0],
13✔
112
                },
13✔
113
            )
13✔
114

13✔
115
            connection.respond(status_code, status_message, () => {
13✔
116
                connection.authheader = '(authenticated bits=0)\n'
13✔
117
                connection.auth_results(`auth=pass (${method.toLowerCase()})`)
13✔
118
                connection.notes.auth_user = credentials[0]
13✔
119
                if (!plugin.blankout_password) connection.notes.auth_passwd = credentials[1]
13✔
120
                next(OK)
13✔
121
            })
13✔
122
            return
13✔
123
        }
13✔
124

13✔
125
        if (!connection.notes.auth_fails) connection.notes.auth_fails = 0
18✔
126

5✔
127
        connection.notes.auth_fails++
5✔
128
        connection.results.add({ name: 'auth' }, { fail: `${plugin.name}/${method}` })
5✔
129

5✔
130
        let delay = Math.pow(2, connection.notes.auth_fails - 1)
5✔
131
        if (plugin.timeout && delay >= plugin.timeout) {
5✔
132
            delay = plugin.timeout - 1
18!
133
        }
×
134
        connection.lognotice(plugin, `delaying for ${delay} seconds`)
18✔
135
        // here we include the username, as shown in RFC 5451 example
5✔
136
        connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${credentials[0]}`)
5✔
137
        setTimeout(() => {
5✔
138
            connection.respond(status_code, status_message, () => {
5✔
139
                connection.reset_transaction(() => next(OK))
5✔
140
            })
5✔
141
        }, delay * 1000)
5✔
142
    }
5✔
143

5✔
144
    if (method === AUTH_METHOD_PLAIN || method === AUTH_METHOD_LOGIN) {
19✔
145
        plugin.check_plain_passwd(connection, credentials[0], credentials[1], passwd_ok)
19!
146
    } else if (method === AUTH_METHOD_CRAM_MD5) {
19!
147
        plugin.check_cram_md5_passwd(connection, credentials[0], credentials[1], passwd_ok)
×
148
    }
×
149
}
×
150

×
151
exports.select_auth_method = function (next, connection, method) {
6✔
152
    const split = method.split(/\s+/)
11✔
153
    method = split.shift().toUpperCase()
11✔
154
    if (!connection.notes.allowed_auth_methods) return next()
11✔
155
    if (!connection.notes.allowed_auth_methods.includes(method)) return next()
11!
156

10!
157
    if (connection.notes.authenticating) return next(DENYDISCONNECT, 'bad protocol')
11!
158

1✔
159
    connection.notes.authenticating = true
11✔
160
    connection.notes.auth_method = method
7✔
161

7✔
162
    if (method === AUTH_METHOD_PLAIN) return this.auth_plain(next, connection, split)
11!
163
    if (method === AUTH_METHOD_LOGIN) return this.auth_login(next, connection, split)
11!
164
    if (method === AUTH_METHOD_CRAM_MD5) return this.auth_cram_md5(next, connection)
11!
165
}
×
166

×
167
exports.auth_plain = function (next, connection, params) {
6✔
168
    // one parameter given on line, either:
7✔
169
    //    AUTH PLAIN <param> or
7✔
170
    //    AUTH PLAIN\n
7✔
171
    //...
7✔
172
    //    <param>
7✔
173
    if (params[0]) {
7✔
174
        const credentials = utils.unbase64(params[0]).split(/\0/)
7✔
175
        credentials.shift() // Discard authid
5✔
176
        this.check_user(next, connection, credentials, AUTH_METHOD_PLAIN)
5✔
177
        return
5✔
178
    }
5✔
179

5✔
180
    if (connection.notes.auth_plain_asked_login) {
7✔
181
        return next(DENYDISCONNECT, 'bad protocol')
7!
182
    }
×
183

×
184
    connection.respond(334, ' ', () => {
7✔
185
        connection.notes.auth_plain_asked_login = true
2✔
186
        next(OK)
2✔
187
    })
2✔
188
}
2✔
189

2✔
190
exports.auth_login = function (next, connection, params) {
6✔
191
    if (
9✔
192
        (!connection.notes.auth_login_asked_login && params[0]) ||
9✔
193
        (connection.notes.auth_login_asked_login && !connection.notes.auth_login_userlogin)
9✔
194
    ) {
4✔
195
        if (!params[0]) return next(DENYDISCONNECT, 'bad protocol')
9!
196

×
197
        const login = utils.unbase64(params[0])
4✔
198
        connection.respond(334, LOGIN_STRING2, () => {
4✔
199
            connection.notes.auth_login_userlogin = login
4✔
200
            connection.notes.auth_login_asked_login = true
4✔
201
            next(OK)
4✔
202
        })
4✔
203
        return
4✔
204
    }
4✔
205

4✔
206
    if (connection.notes.auth_login_userlogin) {
9✔
207
        const credentials = [connection.notes.auth_login_userlogin, utils.unbase64(params[0])]
9✔
208

3✔
209
        connection.notes.auth_login_userlogin = null
3✔
210
        connection.notes.auth_login_asked_login = false
3✔
211

3✔
212
        return this.check_user(next, connection, credentials, AUTH_METHOD_LOGIN)
3✔
213
    }
3✔
214

3✔
215
    connection.respond(334, LOGIN_STRING1, () => {
9✔
216
        connection.notes.auth_login_asked_login = true
2✔
217
        next(OK)
2✔
218
    })
2✔
219
}
2✔
220

2✔
221
exports.auth_cram_md5 = function (next, connection, params) {
6✔
222
    if (params) {
×
223
        const credentials = utils.unbase64(params[0]).split(' ')
✔
224
        return this.check_user(next, connection, credentials, AUTH_METHOD_CRAM_MD5)
×
225
    }
×
226

×
227
    const ticket = `<${this.hexi(Math.floor(Math.random() * 1000000))}.${this.hexi(Date.now())}@${connection.local.host}>`
×
228

×
229
    connection.loginfo(this, `ticket: ${ticket}`)
×
230
    connection.respond(334, utils.base64(ticket), () => {
×
231
        connection.notes.auth_ticket = ticket
✔
232
        next(OK)
2✔
233
    })
2✔
234
}
2✔
235

2✔
236
exports.hexi = (number) => String(Math.abs(parseInt(number)).toString(16))
6✔
237

2✔
238
exports.constrain_sender = function (next, connection, params) {
6✔
239
    if (this?.cfg?.main?.constrain_sender === false) return next()
3!
240

3!
241
    const au = connection.results.get('auth')?.user
3!
242
    if (!au) return next()
3✔
243

3✔
244
    const ad = /@/.test(au) ? au.split('@').pop() : null
3!
245
    const ed = params[0].host
3✔
246

3✔
247
    if (!ad || !ed) return next()
3!
248

3✔
249
    const auth_od = tlds.get_organizational_domain(ad)
3!
250
    const envelope_od = tlds.get_organizational_domain(ed)
2✔
251

2✔
252
    if (auth_od === envelope_od) return next()
2✔
253

2✔
254
    next(DENY, `Envelope domain '${envelope_od}' doesn't match AUTH domain '${auth_od}'`)
1✔
255
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc