haraka-plugin-karma 2.1.3 → 2.1.5
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/{Changes.md → CHANGELOG.md} +35 -29
- package/README.md +13 -24
- package/index.js +217 -188
- package/package.json +20 -13
- package/test/karma.js +228 -124
- package/.codeclimate.yml +0 -25
- package/.eslintrc.yaml +0 -24
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
- package/.github/ISSUE_TEMPLATE/custom.md +0 -10
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/workflows/ci.yml +0 -19
- package/.github/workflows/codeql.yml +0 -14
- package/.github/workflows/publish.yml +0 -16
- package/.gitmodules +0 -3
package/index.js
CHANGED
|
@@ -2,31 +2,44 @@
|
|
|
2
2
|
// karma - reward good and penalize bad mail senders
|
|
3
3
|
|
|
4
4
|
const constants = require('haraka-constants')
|
|
5
|
-
const redis
|
|
6
|
-
const utils
|
|
5
|
+
const redis = require('redis')
|
|
6
|
+
const utils = require('haraka-utils')
|
|
7
7
|
|
|
8
8
|
const phase_prefixes = utils.to_object([
|
|
9
|
-
'connect',
|
|
9
|
+
'connect',
|
|
10
|
+
'helo',
|
|
11
|
+
'mail_from',
|
|
12
|
+
'rcpt_to',
|
|
13
|
+
'data',
|
|
10
14
|
])
|
|
11
15
|
|
|
12
16
|
exports.register = function () {
|
|
13
|
-
|
|
14
17
|
this.inherits('haraka-plugin-redis')
|
|
15
18
|
|
|
16
19
|
// set up defaults
|
|
17
|
-
this.deny_hooks = utils.to_object(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
this.deny_hooks = utils.to_object([
|
|
21
|
+
'unrecognized_command',
|
|
22
|
+
'helo',
|
|
23
|
+
'data',
|
|
24
|
+
'data_post',
|
|
25
|
+
'queue',
|
|
26
|
+
'queue_outbound',
|
|
27
|
+
])
|
|
20
28
|
this.deny_exclude_hooks = utils.to_object('rcpt_to queue queue_outbound')
|
|
21
29
|
this.deny_exclude_plugins = utils.to_object([
|
|
22
|
-
'access',
|
|
23
|
-
'
|
|
30
|
+
'access',
|
|
31
|
+
'helo.checks',
|
|
32
|
+
'data.headers',
|
|
33
|
+
'spamassassin',
|
|
34
|
+
'mail_from.is_resolvable',
|
|
35
|
+
'clamd',
|
|
36
|
+
'tls',
|
|
24
37
|
])
|
|
25
38
|
|
|
26
39
|
this.load_karma_ini()
|
|
27
40
|
|
|
28
|
-
this.register_hook('init_master',
|
|
29
|
-
this.register_hook('init_child',
|
|
41
|
+
this.register_hook('init_master', 'init_redis_plugin')
|
|
42
|
+
this.register_hook('init_child', 'init_redis_plugin')
|
|
30
43
|
|
|
31
44
|
this.register_hook('connect_init', 'results_init')
|
|
32
45
|
this.register_hook('connect_init', 'ip_history_from_redis')
|
|
@@ -35,13 +48,15 @@ exports.register = function () {
|
|
|
35
48
|
exports.load_karma_ini = function () {
|
|
36
49
|
const plugin = this
|
|
37
50
|
|
|
38
|
-
plugin.cfg = plugin.config.get(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
plugin.cfg = plugin.config.get(
|
|
52
|
+
'karma.ini',
|
|
53
|
+
{
|
|
54
|
+
booleans: ['+asn.enable'],
|
|
55
|
+
},
|
|
56
|
+
function () {
|
|
57
|
+
plugin.load_karma_ini()
|
|
58
|
+
},
|
|
59
|
+
)
|
|
45
60
|
|
|
46
61
|
plugin.merge_redis_ini()
|
|
47
62
|
|
|
@@ -73,7 +88,6 @@ exports.load_karma_ini = function () {
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
exports.results_init = async function (next, connection) {
|
|
76
|
-
|
|
77
91
|
if (this.should_we_skip(connection)) {
|
|
78
92
|
connection.logdebug(this, 'skipping')
|
|
79
93
|
return next()
|
|
@@ -81,7 +95,7 @@ exports.results_init = async function (next, connection) {
|
|
|
81
95
|
|
|
82
96
|
if (connection.results.get('karma')) {
|
|
83
97
|
connection.logerror(this, 'this should never happen')
|
|
84
|
-
return next()
|
|
98
|
+
return next() // init once per connection
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
if (this.cfg.awards) {
|
|
@@ -92,10 +106,9 @@ exports.results_init = async function (next, connection) {
|
|
|
92
106
|
const award = this.cfg.awards[key].toString()
|
|
93
107
|
todo[key] = award
|
|
94
108
|
}
|
|
95
|
-
connection.results.add(this, { score:0, todo })
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
connection.results.add(this, { score:0 })
|
|
109
|
+
connection.results.add(this, { score: 0, todo })
|
|
110
|
+
} else {
|
|
111
|
+
connection.results.add(this, { score: 0 })
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
if (!connection.server.notes.redis) {
|
|
@@ -103,7 +116,7 @@ exports.results_init = async function (next, connection) {
|
|
|
103
116
|
return next()
|
|
104
117
|
}
|
|
105
118
|
|
|
106
|
-
if (!this.result_awards) return next()
|
|
119
|
+
if (!this.result_awards) return next() // not configured
|
|
107
120
|
|
|
108
121
|
if (connection.notes.redis) {
|
|
109
122
|
connection.logdebug(this, `redis already subscribed`)
|
|
@@ -128,9 +141,8 @@ exports.preparse_result_awards = function () {
|
|
|
128
141
|
// arrange results for rapid traversal by check_result() :
|
|
129
142
|
// ex: karma.result_awards.clamd.fail = { .... }
|
|
130
143
|
for (const anum of Object.keys(cra)) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
= cra[anum].split(/(?:\s*\|\s*)/)
|
|
144
|
+
const [pi_name, prop, operator, value, award, reason, resolv] =
|
|
145
|
+
cra[anum].split(/(?:\s*\|\s*)/)
|
|
134
146
|
|
|
135
147
|
const ra = this.result_awards
|
|
136
148
|
|
|
@@ -143,7 +155,6 @@ exports.preparse_result_awards = function () {
|
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
exports.check_result = function (connection, message) {
|
|
146
|
-
|
|
147
158
|
// connection.loginfo(this, message);
|
|
148
159
|
// {"plugin":"karma","result":{"fail":"spamassassin.hits"}}
|
|
149
160
|
// {"plugin":"geoip","result":{"country":"CN"}}
|
|
@@ -152,13 +163,14 @@ exports.check_result = function (connection, message) {
|
|
|
152
163
|
if (m && m.result && m.result.asn) {
|
|
153
164
|
this.check_result_asn(m.result.asn, connection)
|
|
154
165
|
}
|
|
155
|
-
if (!this.result_awards[m.plugin]) return
|
|
166
|
+
if (!this.result_awards[m.plugin]) return // no awards for plugin
|
|
156
167
|
|
|
157
|
-
for (const r of Object.keys(m.result)) {
|
|
158
|
-
|
|
168
|
+
for (const r of Object.keys(m.result)) {
|
|
169
|
+
// each result in mess
|
|
170
|
+
if (r === 'emit') continue // r: pass, fail, skip, err, ...
|
|
159
171
|
|
|
160
172
|
const pi_prop = this.result_awards[m.plugin][r]
|
|
161
|
-
if (!pi_prop) continue
|
|
173
|
+
if (!pi_prop) continue // no award for this plugin property
|
|
162
174
|
|
|
163
175
|
const thisResult = m.result[r]
|
|
164
176
|
// ignore empty arrays, objects, and strings
|
|
@@ -169,7 +181,8 @@ exports.check_result = function (connection, message) {
|
|
|
169
181
|
if (typeof thisResult === 'string' && !thisResult) continue // empty
|
|
170
182
|
|
|
171
183
|
// do any award conditions match this result?
|
|
172
|
-
for (const thisAward of pi_prop) {
|
|
184
|
+
for (const thisAward of pi_prop) {
|
|
185
|
+
// each award...
|
|
173
186
|
// { id: '011', operator: 'equals', value: 'all_bad', award: '-2'}
|
|
174
187
|
const thisResArr = this.result_as_array(thisResult)
|
|
175
188
|
switch (thisAward.operator) {
|
|
@@ -196,14 +209,13 @@ exports.check_result = function (connection, message) {
|
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
exports.result_as_array = function (result) {
|
|
199
|
-
|
|
200
212
|
if (typeof result === 'string') return [result]
|
|
201
213
|
if (typeof result === 'number') return [result]
|
|
202
214
|
if (typeof result === 'boolean') return [result]
|
|
203
215
|
if (Array.isArray(result)) return result
|
|
204
216
|
if (typeof result === 'object') {
|
|
205
217
|
const array = []
|
|
206
|
-
Object.keys(result).forEach(tr => {
|
|
218
|
+
Object.keys(result).forEach((tr) => {
|
|
207
219
|
array.push(result[tr])
|
|
208
220
|
})
|
|
209
221
|
return array
|
|
@@ -216,41 +228,37 @@ exports.check_result_asn = function (asn, conn) {
|
|
|
216
228
|
if (!this.cfg.asn_awards) return
|
|
217
229
|
if (!this.cfg.asn_awards[asn]) return
|
|
218
230
|
|
|
219
|
-
conn.results.incr(this, {score: this.cfg.asn_awards[asn]})
|
|
220
|
-
conn.results.push(this, {fail: 'asn_awards'})
|
|
231
|
+
conn.results.incr(this, { score: this.cfg.asn_awards[asn] })
|
|
232
|
+
conn.results.push(this, { fail: 'asn_awards' })
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
exports.check_result_lt = function (thisResult, thisAward, conn) {
|
|
224
|
-
|
|
225
236
|
for (const element of thisResult) {
|
|
226
237
|
const tr = parseFloat(element)
|
|
227
238
|
if (tr >= parseFloat(thisAward.value)) continue
|
|
228
239
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
229
240
|
|
|
230
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
231
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
241
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
242
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
232
243
|
}
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
exports.check_result_gt = function (thisResult, thisAward, conn) {
|
|
236
|
-
|
|
237
247
|
for (const element of thisResult) {
|
|
238
248
|
const tr = parseFloat(element)
|
|
239
249
|
if (tr <= parseFloat(thisAward.value)) continue
|
|
240
250
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
241
251
|
|
|
242
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
243
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
252
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
253
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
244
254
|
}
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
exports.check_result_equal = function (thisResult, thisAward, conn) {
|
|
248
|
-
|
|
249
258
|
for (const element of thisResult) {
|
|
250
259
|
if (thisAward.value === 'true') {
|
|
251
260
|
if (!element) continue
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
261
|
+
} else {
|
|
254
262
|
if (element != thisAward.value) continue
|
|
255
263
|
}
|
|
256
264
|
if (!/auth/.test(thisAward.plugin)) {
|
|
@@ -258,8 +266,8 @@ exports.check_result_equal = function (thisResult, thisAward, conn) {
|
|
|
258
266
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
259
267
|
}
|
|
260
268
|
|
|
261
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
262
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
269
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
270
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
273
|
|
|
@@ -270,13 +278,12 @@ exports.check_result_match = function (thisResult, thisAward, conn) {
|
|
|
270
278
|
if (!re.test(element)) continue
|
|
271
279
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
272
280
|
|
|
273
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
274
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
281
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
282
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
275
283
|
}
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
exports.check_result_length = function (thisResult, thisAward, conn) {
|
|
279
|
-
|
|
280
287
|
for (const element of thisResult) {
|
|
281
288
|
const [operator, qty] = thisAward.value.split(/\s+/) // requires node 6+
|
|
282
289
|
|
|
@@ -297,13 +304,12 @@ exports.check_result_length = function (thisResult, thisAward, conn) {
|
|
|
297
304
|
continue
|
|
298
305
|
}
|
|
299
306
|
|
|
300
|
-
conn.results.incr(this, {score:
|
|
301
|
-
conn.results.push(this, {awards: thisAward.id
|
|
307
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
308
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
302
309
|
}
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
exports.check_result_exists = function (thisResult, thisAward, conn) {
|
|
306
|
-
|
|
307
313
|
/* eslint-disable no-unused-vars */
|
|
308
314
|
for (const r of thisResult) {
|
|
309
315
|
const [operator, qty] = thisAward.value.split(/\s+/)
|
|
@@ -317,18 +323,17 @@ exports.check_result_exists = function (thisResult, thisAward, conn) {
|
|
|
317
323
|
continue
|
|
318
324
|
}
|
|
319
325
|
|
|
320
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
321
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
326
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
327
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
322
328
|
}
|
|
323
329
|
}
|
|
324
330
|
|
|
325
331
|
exports.apply_tarpit = function (connection, hook, score, next) {
|
|
326
|
-
|
|
327
332
|
if (!this.cfg.tarpit) return next() // tarpit disabled in config
|
|
328
333
|
|
|
329
334
|
// If tarpit is enabled on the reset_transaction hook, Haraka doesn't
|
|
330
335
|
// wait. Then bad things happen, like a Haraka crash.
|
|
331
|
-
if (utils.in_array(hook, ['reset_transaction','queue'])) return next()
|
|
336
|
+
if (utils.in_array(hook, ['reset_transaction', 'queue'])) return next()
|
|
332
337
|
|
|
333
338
|
// no delay for senders with good karma
|
|
334
339
|
const k = connection.results.get('karma')
|
|
@@ -347,17 +352,18 @@ exports.apply_tarpit = function (connection, hook, score, next) {
|
|
|
347
352
|
}
|
|
348
353
|
|
|
349
354
|
exports.tarpit_delay = function (score, connection, hook, k) {
|
|
350
|
-
|
|
351
355
|
if (this.cfg.tarpit.delay && parseFloat(this.cfg.tarpit.delay)) {
|
|
352
356
|
connection.logdebug(this, 'static tarpit')
|
|
353
357
|
return parseFloat(this.cfg.tarpit.delay)
|
|
354
358
|
}
|
|
355
359
|
|
|
356
|
-
const delay = score * -1
|
|
360
|
+
const delay = score * -1 // progressive tarpit
|
|
357
361
|
|
|
358
362
|
// detect roaming users based on MSA ports that require auth
|
|
359
|
-
if (
|
|
360
|
-
utils.in_array(
|
|
363
|
+
if (
|
|
364
|
+
utils.in_array(connection.local.port, [587, 465]) &&
|
|
365
|
+
utils.in_array(hook, ['ehlo', 'connect'])
|
|
366
|
+
) {
|
|
361
367
|
return this.tarpit_delay_msa(connection, delay, k)
|
|
362
368
|
}
|
|
363
369
|
|
|
@@ -376,7 +382,7 @@ exports.tarpit_delay_msa = function (connection, delay, k) {
|
|
|
376
382
|
delay = parseFloat(delay)
|
|
377
383
|
|
|
378
384
|
// Reduce delay for good history
|
|
379
|
-
const history = (
|
|
385
|
+
const history = (k.good || 0) - (k.bad || 0)
|
|
380
386
|
if (history > 0) {
|
|
381
387
|
delay = delay - 2
|
|
382
388
|
connection.logdebug(this, `${trg} history: ${delay}`)
|
|
@@ -409,12 +415,12 @@ exports.should_we_deny = function (next, connection, hook) {
|
|
|
409
415
|
const r = connection.results.get('karma')
|
|
410
416
|
if (!r) return next()
|
|
411
417
|
|
|
412
|
-
this.check_awards(connection)
|
|
418
|
+
this.check_awards(connection) // update awards first
|
|
413
419
|
|
|
414
420
|
const score = parseFloat(r.score)
|
|
415
|
-
if (isNaN(score))
|
|
421
|
+
if (isNaN(score)) {
|
|
416
422
|
connection.logerror(this, 'score is NaN')
|
|
417
|
-
connection.results.add(this, {score: 0})
|
|
423
|
+
connection.results.add(this, { score: 0 })
|
|
418
424
|
return next()
|
|
419
425
|
}
|
|
420
426
|
|
|
@@ -450,10 +456,10 @@ exports.hook_deny = function (next, connection, params) {
|
|
|
450
456
|
|
|
451
457
|
// let pi_deny = params[0]; // (constants.deny, denysoft, ok)
|
|
452
458
|
// let pi_message = params[1];
|
|
453
|
-
const pi_name
|
|
459
|
+
const pi_name = params[2]
|
|
454
460
|
// let pi_function = params[3];
|
|
455
461
|
// let pi_params = params[4];
|
|
456
|
-
const pi_hook
|
|
462
|
+
const pi_hook = params[5]
|
|
457
463
|
|
|
458
464
|
// exceptions, whose 'DENY' should not be captured
|
|
459
465
|
if (pi_name) {
|
|
@@ -468,7 +474,7 @@ exports.hook_deny = function (next, connection, params) {
|
|
|
468
474
|
connection.results.add(this, { msg: `deny: ${pi_name}` })
|
|
469
475
|
connection.results.incr(this, { score: -2 })
|
|
470
476
|
|
|
471
|
-
next(constants.OK)
|
|
477
|
+
next(constants.OK) // resume the connection
|
|
472
478
|
}
|
|
473
479
|
|
|
474
480
|
exports.hook_connect = function (next, connection) {
|
|
@@ -526,12 +532,11 @@ exports.hook_queue_outbound = function (next, connection) {
|
|
|
526
532
|
exports.hook_reset_transaction = function (next, connection) {
|
|
527
533
|
if (this.should_we_skip(connection)) return next()
|
|
528
534
|
|
|
529
|
-
connection.results.add(this, {emit: true})
|
|
535
|
+
connection.results.add(this, { emit: true })
|
|
530
536
|
this.should_we_deny(next, connection, 'reset_transaction')
|
|
531
537
|
}
|
|
532
538
|
|
|
533
539
|
exports.hook_unrecognized_command = function (next, connection, params) {
|
|
534
|
-
|
|
535
540
|
if (this.should_we_skip(connection)) return next()
|
|
536
541
|
|
|
537
542
|
// in case karma is in config/plugins before tls
|
|
@@ -540,8 +545,8 @@ exports.hook_unrecognized_command = function (next, connection, params) {
|
|
|
540
545
|
// in case karma is in config/plugins before AUTH plugin(s)
|
|
541
546
|
if (connection.notes.authenticating) return next()
|
|
542
547
|
|
|
543
|
-
connection.results.incr(this, {score: -1})
|
|
544
|
-
connection.results.add(this, {fail: `cmd:(${params})`})
|
|
548
|
+
connection.results.incr(this, { score: -1 })
|
|
549
|
+
connection.results.add(this, { fail: `cmd:(${params})` })
|
|
545
550
|
|
|
546
551
|
return this.should_we_deny(next, connection, 'unrecognized_command')
|
|
547
552
|
}
|
|
@@ -552,72 +557,74 @@ exports.ip_history_from_redis = function (next, connection) {
|
|
|
552
557
|
if (this.should_we_skip(connection)) return next()
|
|
553
558
|
|
|
554
559
|
const expire = (this.cfg.redis.expire_days || 60) * 86400 // to days
|
|
555
|
-
const dbkey
|
|
560
|
+
const dbkey = `karma|${connection.remote.ip}`
|
|
556
561
|
|
|
557
562
|
// redis plugin is emitting errors, no need to here
|
|
558
563
|
if (!this.db) return next()
|
|
559
564
|
|
|
560
|
-
this.db
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
+
this.db
|
|
566
|
+
.hGetAll(dbkey)
|
|
567
|
+
.then((dbr) => {
|
|
568
|
+
if (dbr === null) {
|
|
569
|
+
plugin.init_ip(dbkey, connection.remote.ip, expire)
|
|
570
|
+
return next()
|
|
571
|
+
}
|
|
565
572
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
573
|
+
plugin.db
|
|
574
|
+
.multi()
|
|
575
|
+
.hIncrBy(dbkey, 'connections', 1) // increment total conn
|
|
576
|
+
.expire(dbkey, expire) // extend expiration
|
|
577
|
+
.exec()
|
|
578
|
+
.catch((err) => {
|
|
579
|
+
connection.results.add(plugin, { err })
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const results = {
|
|
583
|
+
good: dbr.good,
|
|
584
|
+
bad: dbr.bad,
|
|
585
|
+
connections: dbr.connections,
|
|
586
|
+
history: parseInt((dbr.good || 0) - (dbr.bad || 0)),
|
|
587
|
+
emit: true,
|
|
588
|
+
}
|
|
581
589
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
590
|
+
// Careful: don't become self-fulfilling prophecy.
|
|
591
|
+
if (parseInt(dbr.good) > 5 && parseInt(dbr.bad) === 0) {
|
|
592
|
+
results.pass = 'all_good'
|
|
593
|
+
}
|
|
594
|
+
if (parseInt(dbr.bad) > 5 && parseInt(dbr.good) === 0) {
|
|
595
|
+
results.fail = 'all_bad'
|
|
596
|
+
}
|
|
589
597
|
|
|
590
|
-
|
|
598
|
+
connection.results.add(plugin, results)
|
|
591
599
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
.catch(err => {
|
|
600
|
+
plugin.check_awards(connection)
|
|
601
|
+
next()
|
|
602
|
+
})
|
|
603
|
+
.catch((err) => {
|
|
596
604
|
connection.results.add(plugin, { err })
|
|
597
605
|
next()
|
|
598
606
|
})
|
|
599
607
|
}
|
|
600
608
|
|
|
601
609
|
exports.hook_mail = function (next, connection, params) {
|
|
602
|
-
|
|
603
610
|
if (this.should_we_skip(connection)) return next()
|
|
604
611
|
|
|
605
612
|
this.check_spammy_tld(params[0], connection)
|
|
606
613
|
|
|
607
614
|
// look for invalid (RFC 5321,(2)821) space in envelope from
|
|
608
615
|
const full_from = connection.current_line
|
|
609
|
-
if (full_from.toUpperCase().substring(0,11) !== 'MAIL FROM:<') {
|
|
616
|
+
if (full_from.toUpperCase().substring(0, 11) !== 'MAIL FROM:<') {
|
|
610
617
|
connection.loginfo(this, `RFC ignorant env addr format: ${full_from}`)
|
|
611
|
-
connection.results.add(this, {fail: 'rfc5321.MailFrom'})
|
|
618
|
+
connection.results.add(this, { fail: 'rfc5321.MailFrom' })
|
|
612
619
|
}
|
|
613
620
|
|
|
614
621
|
// apply TLS awards (if defined)
|
|
615
622
|
if (this.cfg.tls !== undefined) {
|
|
616
623
|
if (this.cfg.tls.set && connection.tls.enabled) {
|
|
617
|
-
connection.results.incr(this, {score: this.cfg.tls.set})
|
|
624
|
+
connection.results.incr(this, { score: this.cfg.tls.set })
|
|
618
625
|
}
|
|
619
626
|
if (this.cfg.tls.unset && !connection.tls.enabled) {
|
|
620
|
-
connection.results.incr(this, {score: this.cfg.tls.unset})
|
|
627
|
+
connection.results.incr(this, { score: this.cfg.tls.unset })
|
|
621
628
|
}
|
|
622
629
|
}
|
|
623
630
|
|
|
@@ -625,7 +632,6 @@ exports.hook_mail = function (next, connection, params) {
|
|
|
625
632
|
}
|
|
626
633
|
|
|
627
634
|
exports.hook_rcpt = function (next, connection, params) {
|
|
628
|
-
|
|
629
635
|
if (this.should_we_skip(connection)) return next()
|
|
630
636
|
|
|
631
637
|
const rcpt = params[0]
|
|
@@ -636,23 +642,22 @@ exports.hook_rcpt = function (next, connection, params) {
|
|
|
636
642
|
// odds of from_user=rcpt_user in ham: < 1%, in spam > 40%
|
|
637
643
|
// 2015-05 30-day sample: 84% spam correlation
|
|
638
644
|
if (connection?.transaction?.mail_from?.user === rcpt.user) {
|
|
639
|
-
connection.results.add(this, {fail: 'env_user_match'})
|
|
645
|
+
connection.results.add(this, { fail: 'env_user_match' })
|
|
640
646
|
}
|
|
641
647
|
|
|
642
648
|
this.check_syntax_RcptTo(connection)
|
|
643
649
|
|
|
644
|
-
connection.results.add(this, {fail: 'rcpt_to'})
|
|
650
|
+
connection.results.add(this, { fail: 'rcpt_to' })
|
|
645
651
|
|
|
646
652
|
return this.should_we_deny(next, connection, 'rcpt')
|
|
647
653
|
}
|
|
648
654
|
|
|
649
655
|
exports.hook_rcpt_ok = function (next, connection, rcpt) {
|
|
650
|
-
|
|
651
656
|
if (this.should_we_skip(connection)) return next()
|
|
652
657
|
|
|
653
658
|
const txn = connection.transaction
|
|
654
659
|
if (txn && txn.mail_from && txn.mail_from.user === rcpt.user) {
|
|
655
|
-
connection.results.add(this, {fail: 'env_user_match'})
|
|
660
|
+
connection.results.add(this, { fail: 'env_user_match' })
|
|
656
661
|
}
|
|
657
662
|
|
|
658
663
|
this.check_syntax_RcptTo(connection)
|
|
@@ -665,7 +670,7 @@ exports.hook_data_post = function (next, connection) {
|
|
|
665
670
|
|
|
666
671
|
if (this.should_we_skip(connection)) return next()
|
|
667
672
|
|
|
668
|
-
this.check_awards(connection)
|
|
673
|
+
this.check_awards(connection) // update awards
|
|
669
674
|
|
|
670
675
|
const results = connection.results.collate(this)
|
|
671
676
|
connection.logdebug(this, `adding header: ${results}`)
|
|
@@ -691,13 +696,13 @@ exports.hook_disconnect = function (next, connection) {
|
|
|
691
696
|
|
|
692
697
|
const k = connection.results.get('karma')
|
|
693
698
|
if (!k || k.score === undefined) {
|
|
694
|
-
connection.results.add(this, {err: 'karma results missing'})
|
|
699
|
+
connection.results.add(this, { err: 'karma results missing' })
|
|
695
700
|
return next()
|
|
696
701
|
}
|
|
697
702
|
|
|
698
703
|
if (!this.cfg.thresholds) {
|
|
699
704
|
this.check_awards(connection)
|
|
700
|
-
connection.results.add(this, {msg: 'no action', emit: true })
|
|
705
|
+
connection.results.add(this, { msg: 'no action', emit: true })
|
|
701
706
|
return next()
|
|
702
707
|
}
|
|
703
708
|
|
|
@@ -708,12 +713,11 @@ exports.hook_disconnect = function (next, connection) {
|
|
|
708
713
|
this.increment(connection, 'bad', 1)
|
|
709
714
|
}
|
|
710
715
|
|
|
711
|
-
connection.results.add(this, {emit: true })
|
|
716
|
+
connection.results.add(this, { emit: true })
|
|
712
717
|
next()
|
|
713
718
|
}
|
|
714
719
|
|
|
715
720
|
exports.get_award_loc_from_note = function (connection, award) {
|
|
716
|
-
|
|
717
721
|
if (connection.transaction) {
|
|
718
722
|
const obj = this.assemble_note_obj(connection.transaction, award)
|
|
719
723
|
if (obj) return obj
|
|
@@ -728,7 +732,6 @@ exports.get_award_loc_from_note = function (connection, award) {
|
|
|
728
732
|
}
|
|
729
733
|
|
|
730
734
|
exports.get_award_loc_from_results = function (connection, loc_bits) {
|
|
731
|
-
|
|
732
735
|
let pi_name = loc_bits[1]
|
|
733
736
|
let notekey = loc_bits[2]
|
|
734
737
|
|
|
@@ -755,16 +758,22 @@ exports.get_award_location = function (connection, award_key) {
|
|
|
755
758
|
const loc_bits = bits[0].split('.')
|
|
756
759
|
if (loc_bits.length === 1) return connection[bits[0]] // ex: relaying
|
|
757
760
|
|
|
758
|
-
if (loc_bits[0] === 'notes') {
|
|
761
|
+
if (loc_bits[0] === 'notes') {
|
|
762
|
+
// ex: notes.spf_mail_helo
|
|
759
763
|
return this.get_award_loc_from_note(connection, bits[0])
|
|
760
764
|
}
|
|
761
765
|
|
|
762
|
-
if (loc_bits[0] === 'results') {
|
|
766
|
+
if (loc_bits[0] === 'results') {
|
|
767
|
+
// ex: results.geoip.distance
|
|
763
768
|
return this.get_award_loc_from_results(connection, loc_bits)
|
|
764
769
|
}
|
|
765
770
|
|
|
766
771
|
// ex: transaction.results.spf
|
|
767
|
-
if (
|
|
772
|
+
if (
|
|
773
|
+
connection.transaction &&
|
|
774
|
+
loc_bits[0] === 'transaction' &&
|
|
775
|
+
loc_bits[1] === 'results'
|
|
776
|
+
) {
|
|
768
777
|
loc_bits.shift()
|
|
769
778
|
return this.get_award_loc_from_results(connection.transaction, loc_bits)
|
|
770
779
|
}
|
|
@@ -775,11 +784,13 @@ exports.get_award_location = function (connection, award_key) {
|
|
|
775
784
|
exports.get_award_condition = function (note_key, note_val) {
|
|
776
785
|
let wants
|
|
777
786
|
const keybits = note_key.split('@')
|
|
778
|
-
if (keybits[1]) {
|
|
787
|
+
if (keybits[1]) {
|
|
788
|
+
wants = keybits[1]
|
|
789
|
+
}
|
|
779
790
|
|
|
780
791
|
const valbits = note_val.split(/\s+/)
|
|
781
792
|
if (!valbits[1]) return wants
|
|
782
|
-
if (valbits[1] !== 'if') return wants
|
|
793
|
+
if (valbits[1] !== 'if') return wants // no if condition
|
|
783
794
|
|
|
784
795
|
if (valbits[2].match(/^(equals|gt|lt|match)$/)) {
|
|
785
796
|
if (valbits[3]) wants = valbits[3]
|
|
@@ -805,14 +816,16 @@ exports.check_awards = function (connection) {
|
|
|
805
816
|
// test the desired condition
|
|
806
817
|
const bits = award_terms.split(/\s+/)
|
|
807
818
|
const award = parseFloat(bits[0])
|
|
808
|
-
if (!bits[1] || bits[1] !== 'if') {
|
|
809
|
-
|
|
810
|
-
if (!
|
|
819
|
+
if (!bits[1] || bits[1] !== 'if') {
|
|
820
|
+
// no if conditions
|
|
821
|
+
if (!note) continue // failed truth test
|
|
822
|
+
if (!wants) {
|
|
823
|
+
// no wants, truth matches
|
|
811
824
|
this.apply_award(connection, key, award)
|
|
812
825
|
delete karma.todo[key]
|
|
813
826
|
continue
|
|
814
827
|
}
|
|
815
|
-
if (note !== wants) continue
|
|
828
|
+
if (note !== wants) continue // didn't match
|
|
816
829
|
}
|
|
817
830
|
|
|
818
831
|
// connection.loginfo(this, `check_awards, case matching for: ${wants}`
|
|
@@ -839,7 +852,9 @@ exports.check_awards = function (connection) {
|
|
|
839
852
|
continue
|
|
840
853
|
case 'length': {
|
|
841
854
|
const operator = bits[3]
|
|
842
|
-
if (bits[4]) {
|
|
855
|
+
if (bits[4]) {
|
|
856
|
+
wants = bits[4]
|
|
857
|
+
}
|
|
843
858
|
switch (operator) {
|
|
844
859
|
case 'gt':
|
|
845
860
|
if (note.length <= parseFloat(wants)) continue
|
|
@@ -851,14 +866,19 @@ exports.check_awards = function (connection) {
|
|
|
851
866
|
if (note.length !== parseFloat(wants)) continue
|
|
852
867
|
break
|
|
853
868
|
default:
|
|
854
|
-
connection.logerror(
|
|
869
|
+
connection.logerror(
|
|
870
|
+
this,
|
|
871
|
+
`length operator "${operator}" not supported.`,
|
|
872
|
+
)
|
|
855
873
|
continue
|
|
856
874
|
}
|
|
857
875
|
break
|
|
858
876
|
}
|
|
859
|
-
case 'in':
|
|
877
|
+
case 'in': // if in pass whitelisted
|
|
860
878
|
// let list = bits[3];
|
|
861
|
-
if (bits[4]) {
|
|
879
|
+
if (bits[4]) {
|
|
880
|
+
wants = bits[4]
|
|
881
|
+
}
|
|
862
882
|
if (!Array.isArray(note)) continue
|
|
863
883
|
if (!wants) continue
|
|
864
884
|
if (note.indexOf(wants) !== -1) break // found!
|
|
@@ -873,25 +893,31 @@ exports.check_awards = function (connection) {
|
|
|
873
893
|
|
|
874
894
|
exports.apply_award = function (connection, nl, award) {
|
|
875
895
|
if (!award) return
|
|
876
|
-
if (isNaN(award)) {
|
|
896
|
+
if (isNaN(award)) {
|
|
897
|
+
// garbage in config
|
|
877
898
|
connection.logerror(this, `non-numeric award from: ${nl}:${award}`)
|
|
878
899
|
return
|
|
879
900
|
}
|
|
880
901
|
|
|
881
|
-
const bits = nl.split('@')
|
|
902
|
+
const bits = nl.split('@')
|
|
903
|
+
nl = bits[0] // strip off @... if present
|
|
882
904
|
|
|
883
|
-
connection.results.incr(this, {score: award})
|
|
905
|
+
connection.results.incr(this, { score: award })
|
|
884
906
|
connection.logdebug(this, `applied ${nl}:${award}`)
|
|
885
907
|
|
|
886
|
-
let trimmed =
|
|
887
|
-
nl.substring(0,
|
|
888
|
-
nl.substring(
|
|
889
|
-
|
|
908
|
+
let trimmed =
|
|
909
|
+
nl.substring(0, 5) === 'notes'
|
|
910
|
+
? nl.substring(6)
|
|
911
|
+
: nl.substring(0, 7) === 'results'
|
|
912
|
+
? nl.substring(8)
|
|
913
|
+
: nl.substring(0, 19) === 'transaction.results'
|
|
914
|
+
? nl.substring(20)
|
|
915
|
+
: nl
|
|
890
916
|
|
|
891
|
-
if (trimmed.substring(0,7) === 'rcpt_to') trimmed = trimmed.substring(8)
|
|
892
|
-
if (trimmed.substring(0,7) === 'mail_from') trimmed = trimmed.substring(10)
|
|
893
|
-
if (trimmed.substring(0,7) === 'connect') trimmed = trimmed.substring(8)
|
|
894
|
-
if (trimmed.substring(0,4) === 'data') trimmed = trimmed.substring(5)
|
|
917
|
+
if (trimmed.substring(0, 7) === 'rcpt_to') trimmed = trimmed.substring(8)
|
|
918
|
+
if (trimmed.substring(0, 7) === 'mail_from') trimmed = trimmed.substring(10)
|
|
919
|
+
if (trimmed.substring(0, 7) === 'connect') trimmed = trimmed.substring(8)
|
|
920
|
+
if (trimmed.substring(0, 4) === 'data') trimmed = trimmed.substring(5)
|
|
895
921
|
|
|
896
922
|
if (award > 0) connection.results.add(this, { pass: trimmed })
|
|
897
923
|
if (award < 0) connection.results.add(this, { fail: trimmed })
|
|
@@ -899,7 +925,7 @@ exports.apply_award = function (connection, nl, award) {
|
|
|
899
925
|
|
|
900
926
|
exports.check_spammy_tld = function (mail_from, connection) {
|
|
901
927
|
if (!this.cfg.spammy_tlds) return
|
|
902
|
-
if (mail_from.isNull()) return
|
|
928
|
+
if (mail_from.isNull()) return // null sender (bounce)
|
|
903
929
|
|
|
904
930
|
const from_tld = mail_from.host.split('.').pop()
|
|
905
931
|
// connection.logdebug(this, `from_tld: ${from_tld}`);
|
|
@@ -907,17 +933,17 @@ exports.check_spammy_tld = function (mail_from, connection) {
|
|
|
907
933
|
const tld_penalty = parseFloat(this.cfg.spammy_tlds[from_tld] || 0)
|
|
908
934
|
if (tld_penalty === 0) return
|
|
909
935
|
|
|
910
|
-
connection.results.incr(this, {score: tld_penalty})
|
|
911
|
-
connection.results.add(this, {fail: 'spammy.TLD'})
|
|
936
|
+
connection.results.incr(this, { score: tld_penalty })
|
|
937
|
+
connection.results.add(this, { fail: 'spammy.TLD' })
|
|
912
938
|
}
|
|
913
939
|
|
|
914
940
|
exports.check_syntax_RcptTo = function (connection) {
|
|
915
941
|
// look for an illegal (RFC 5321,(2)821) space in envelope recipient
|
|
916
942
|
const full_rcpt = connection.current_line
|
|
917
|
-
if (full_rcpt.toUpperCase().substring(0,9) === 'RCPT TO:<') return
|
|
943
|
+
if (full_rcpt.toUpperCase().substring(0, 9) === 'RCPT TO:<') return
|
|
918
944
|
|
|
919
945
|
connection.loginfo(this, `illegal envelope address format: ${full_rcpt}`)
|
|
920
|
-
connection.results.add(this, {fail: 'rfc5321.RcptTo'})
|
|
946
|
+
connection.results.add(this, { fail: 'rfc5321.RcptTo' })
|
|
921
947
|
}
|
|
922
948
|
|
|
923
949
|
exports.assemble_note_obj = function (prefix, key) {
|
|
@@ -941,50 +967,52 @@ exports.check_asn = function (connection, asnkey) {
|
|
|
941
967
|
|
|
942
968
|
if (this.cfg.asn.report_as) report_as.name = this.cfg.asn.report_as
|
|
943
969
|
|
|
944
|
-
this.db
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const asn_score = parseInt(res.good || 0) - (res.bad || 0)
|
|
953
|
-
const asn_results = {
|
|
954
|
-
asn_score,
|
|
955
|
-
asn_connections: res.connections,
|
|
956
|
-
asn_good: res.good,
|
|
957
|
-
asn_bad: res.bad,
|
|
958
|
-
emit: true,
|
|
959
|
-
}
|
|
970
|
+
this.db
|
|
971
|
+
.hGetAll(asnkey)
|
|
972
|
+
.then((res) => {
|
|
973
|
+
if (res === null) {
|
|
974
|
+
const expire = (this.cfg.redis.expire_days || 60) * 86400 // days
|
|
975
|
+
this.init_asn(asnkey, expire)
|
|
976
|
+
return
|
|
977
|
+
}
|
|
960
978
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
979
|
+
this.db.hIncrBy(asnkey, 'connections', 1)
|
|
980
|
+
const asn_score = parseInt(res.good || 0) - (res.bad || 0)
|
|
981
|
+
const asn_results = {
|
|
982
|
+
asn_score,
|
|
983
|
+
asn_connections: res.connections,
|
|
984
|
+
asn_good: res.good,
|
|
985
|
+
asn_bad: res.bad,
|
|
986
|
+
emit: true,
|
|
964
987
|
}
|
|
965
|
-
|
|
966
|
-
|
|
988
|
+
|
|
989
|
+
if (asn_score) {
|
|
990
|
+
if (asn_score < -5) {
|
|
991
|
+
asn_results.fail = 'asn:history'
|
|
992
|
+
} else if (asn_score > 5) {
|
|
993
|
+
asn_results.pass = 'asn:history'
|
|
994
|
+
}
|
|
967
995
|
}
|
|
968
|
-
}
|
|
969
996
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
997
|
+
if (parseInt(res.bad) > 5 && parseInt(res.good) === 0) {
|
|
998
|
+
asn_results.fail = 'asn:all_bad'
|
|
999
|
+
}
|
|
1000
|
+
if (parseInt(res.good) > 5 && parseInt(res.bad) === 0) {
|
|
1001
|
+
asn_results.pass = 'asn:all_good'
|
|
1002
|
+
}
|
|
976
1003
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
.catch(err => {
|
|
1004
|
+
connection.results.add(report_as, asn_results)
|
|
1005
|
+
})
|
|
1006
|
+
.catch((err) => {
|
|
980
1007
|
connection.results.add(this, { err })
|
|
981
1008
|
})
|
|
982
1009
|
}
|
|
983
1010
|
|
|
984
1011
|
exports.init_ip = async function (dbkey, rip, expire) {
|
|
985
1012
|
if (!this.db) return
|
|
986
|
-
await this.db
|
|
987
|
-
.
|
|
1013
|
+
await this.db
|
|
1014
|
+
.multi()
|
|
1015
|
+
.hmSet(dbkey, { bad: 0, good: 0, connections: 1 })
|
|
988
1016
|
.expire(dbkey, expire)
|
|
989
1017
|
.exec()
|
|
990
1018
|
}
|
|
@@ -999,8 +1027,9 @@ exports.get_asn_key = function (connection) {
|
|
|
999
1027
|
|
|
1000
1028
|
exports.init_asn = function (asnkey, expire) {
|
|
1001
1029
|
if (!this.db) return
|
|
1002
|
-
this.db
|
|
1003
|
-
.
|
|
1004
|
-
.
|
|
1030
|
+
this.db
|
|
1031
|
+
.multi()
|
|
1032
|
+
.hmSet(asnkey, { bad: 0, good: 0, connections: 1 })
|
|
1033
|
+
.expire(asnkey, expire * 2) // keep ASN longer
|
|
1005
1034
|
.exec()
|
|
1006
1035
|
}
|