haraka-plugin-karma 2.1.2 → 2.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/{Changes.md → CHANGELOG.md} +33 -27
- package/README.md +13 -21
- package/index.js +230 -202
- package/package.json +21 -14
- package/test/karma.js +227 -123
- 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 -50
- 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,8 +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 (
|
|
173
|
-
|
|
184
|
+
for (const thisAward of pi_prop) {
|
|
185
|
+
// each award...
|
|
174
186
|
// { id: '011', operator: 'equals', value: 'all_bad', award: '-2'}
|
|
175
187
|
const thisResArr = this.result_as_array(thisResult)
|
|
176
188
|
switch (thisAward.operator) {
|
|
@@ -197,14 +209,13 @@ exports.check_result = function (connection, message) {
|
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
exports.result_as_array = function (result) {
|
|
200
|
-
|
|
201
212
|
if (typeof result === 'string') return [result]
|
|
202
213
|
if (typeof result === 'number') return [result]
|
|
203
214
|
if (typeof result === 'boolean') return [result]
|
|
204
215
|
if (Array.isArray(result)) return result
|
|
205
216
|
if (typeof result === 'object') {
|
|
206
217
|
const array = []
|
|
207
|
-
Object.keys(result).forEach(tr => {
|
|
218
|
+
Object.keys(result).forEach((tr) => {
|
|
208
219
|
array.push(result[tr])
|
|
209
220
|
})
|
|
210
221
|
return array
|
|
@@ -217,94 +228,88 @@ exports.check_result_asn = function (asn, conn) {
|
|
|
217
228
|
if (!this.cfg.asn_awards) return
|
|
218
229
|
if (!this.cfg.asn_awards[asn]) return
|
|
219
230
|
|
|
220
|
-
conn.results.incr(this, {score: this.cfg.asn_awards[asn]})
|
|
221
|
-
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' })
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
exports.check_result_lt = function (thisResult, thisAward, conn) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const tr = parseFloat(thisResult[j])
|
|
236
|
+
for (const element of thisResult) {
|
|
237
|
+
const tr = parseFloat(element)
|
|
228
238
|
if (tr >= parseFloat(thisAward.value)) continue
|
|
229
239
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
230
240
|
|
|
231
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
232
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
241
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
242
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
233
243
|
}
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
exports.check_result_gt = function (thisResult, thisAward, conn) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const tr = parseFloat(thisResult[j])
|
|
247
|
+
for (const element of thisResult) {
|
|
248
|
+
const tr = parseFloat(element)
|
|
240
249
|
if (tr <= parseFloat(thisAward.value)) continue
|
|
241
250
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
242
251
|
|
|
243
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
244
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
252
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
253
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
245
254
|
}
|
|
246
255
|
}
|
|
247
256
|
|
|
248
257
|
exports.check_result_equal = function (thisResult, thisAward, conn) {
|
|
249
|
-
|
|
250
|
-
for (let j=0; j < thisResult.length; j++) {
|
|
258
|
+
for (const element of thisResult) {
|
|
251
259
|
if (thisAward.value === 'true') {
|
|
252
|
-
if (!
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (thisResult[j] != thisAward.value) continue
|
|
260
|
+
if (!element) continue
|
|
261
|
+
} else {
|
|
262
|
+
if (element != thisAward.value) continue
|
|
256
263
|
}
|
|
257
264
|
if (!/auth/.test(thisAward.plugin)) {
|
|
258
265
|
// only auth attempts are scored > 1x
|
|
259
266
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
260
267
|
}
|
|
261
268
|
|
|
262
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
263
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
269
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
270
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
264
271
|
}
|
|
265
272
|
}
|
|
266
273
|
|
|
267
274
|
exports.check_result_match = function (thisResult, thisAward, conn) {
|
|
268
275
|
const re = new RegExp(thisAward.value, 'i')
|
|
269
276
|
|
|
270
|
-
for (
|
|
271
|
-
if (!re.test(
|
|
277
|
+
for (const element of thisResult) {
|
|
278
|
+
if (!re.test(element)) continue
|
|
272
279
|
if (conn.results.has('karma', 'awards', thisAward.id)) continue
|
|
273
280
|
|
|
274
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
275
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
281
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
282
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
276
283
|
}
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
exports.check_result_length = function (thisResult, thisAward, conn) {
|
|
280
|
-
|
|
281
|
-
for (let j=0; j < thisResult.length; j++) {
|
|
287
|
+
for (const element of thisResult) {
|
|
282
288
|
const [operator, qty] = thisAward.value.split(/\s+/) // requires node 6+
|
|
283
289
|
|
|
284
290
|
switch (operator) {
|
|
285
291
|
case 'eq':
|
|
286
292
|
case 'equal':
|
|
287
293
|
case 'equals':
|
|
288
|
-
if (parseInt(
|
|
294
|
+
if (parseInt(element, 10) != parseInt(qty, 10)) continue
|
|
289
295
|
break
|
|
290
296
|
case 'gt':
|
|
291
|
-
if (parseInt(
|
|
297
|
+
if (parseInt(element, 10) <= parseInt(qty, 10)) continue
|
|
292
298
|
break
|
|
293
299
|
case 'lt':
|
|
294
|
-
if (parseInt(
|
|
300
|
+
if (parseInt(element, 10) >= parseInt(qty, 10)) continue
|
|
295
301
|
break
|
|
296
302
|
default:
|
|
297
303
|
conn.results.add(this, { err: `invalid operator: ${operator}` })
|
|
298
304
|
continue
|
|
299
305
|
}
|
|
300
306
|
|
|
301
|
-
conn.results.incr(this, {score:
|
|
302
|
-
conn.results.push(this, {awards: thisAward.id
|
|
307
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
308
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
exports.check_result_exists = function (thisResult, thisAward, conn) {
|
|
307
|
-
|
|
308
313
|
/* eslint-disable no-unused-vars */
|
|
309
314
|
for (const r of thisResult) {
|
|
310
315
|
const [operator, qty] = thisAward.value.split(/\s+/)
|
|
@@ -318,18 +323,17 @@ exports.check_result_exists = function (thisResult, thisAward, conn) {
|
|
|
318
323
|
continue
|
|
319
324
|
}
|
|
320
325
|
|
|
321
|
-
conn.results.incr(this, {score: thisAward.award})
|
|
322
|
-
conn.results.push(this, {awards: thisAward.id})
|
|
326
|
+
conn.results.incr(this, { score: thisAward.award })
|
|
327
|
+
conn.results.push(this, { awards: thisAward.id })
|
|
323
328
|
}
|
|
324
329
|
}
|
|
325
330
|
|
|
326
331
|
exports.apply_tarpit = function (connection, hook, score, next) {
|
|
327
|
-
|
|
328
332
|
if (!this.cfg.tarpit) return next() // tarpit disabled in config
|
|
329
333
|
|
|
330
334
|
// If tarpit is enabled on the reset_transaction hook, Haraka doesn't
|
|
331
335
|
// wait. Then bad things happen, like a Haraka crash.
|
|
332
|
-
if (utils.in_array(hook, ['reset_transaction','queue'])) return next()
|
|
336
|
+
if (utils.in_array(hook, ['reset_transaction', 'queue'])) return next()
|
|
333
337
|
|
|
334
338
|
// no delay for senders with good karma
|
|
335
339
|
const k = connection.results.get('karma')
|
|
@@ -348,17 +352,18 @@ exports.apply_tarpit = function (connection, hook, score, next) {
|
|
|
348
352
|
}
|
|
349
353
|
|
|
350
354
|
exports.tarpit_delay = function (score, connection, hook, k) {
|
|
351
|
-
|
|
352
355
|
if (this.cfg.tarpit.delay && parseFloat(this.cfg.tarpit.delay)) {
|
|
353
356
|
connection.logdebug(this, 'static tarpit')
|
|
354
357
|
return parseFloat(this.cfg.tarpit.delay)
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
const delay = score * -1
|
|
360
|
+
const delay = score * -1 // progressive tarpit
|
|
358
361
|
|
|
359
362
|
// detect roaming users based on MSA ports that require auth
|
|
360
|
-
if (
|
|
361
|
-
utils.in_array(
|
|
363
|
+
if (
|
|
364
|
+
utils.in_array(connection.local.port, [587, 465]) &&
|
|
365
|
+
utils.in_array(hook, ['ehlo', 'connect'])
|
|
366
|
+
) {
|
|
362
367
|
return this.tarpit_delay_msa(connection, delay, k)
|
|
363
368
|
}
|
|
364
369
|
|
|
@@ -377,7 +382,7 @@ exports.tarpit_delay_msa = function (connection, delay, k) {
|
|
|
377
382
|
delay = parseFloat(delay)
|
|
378
383
|
|
|
379
384
|
// Reduce delay for good history
|
|
380
|
-
const history = (
|
|
385
|
+
const history = (k.good || 0) - (k.bad || 0)
|
|
381
386
|
if (history > 0) {
|
|
382
387
|
delay = delay - 2
|
|
383
388
|
connection.logdebug(this, `${trg} history: ${delay}`)
|
|
@@ -410,12 +415,12 @@ exports.should_we_deny = function (next, connection, hook) {
|
|
|
410
415
|
const r = connection.results.get('karma')
|
|
411
416
|
if (!r) return next()
|
|
412
417
|
|
|
413
|
-
this.check_awards(connection)
|
|
418
|
+
this.check_awards(connection) // update awards first
|
|
414
419
|
|
|
415
420
|
const score = parseFloat(r.score)
|
|
416
|
-
if (isNaN(score))
|
|
421
|
+
if (isNaN(score)) {
|
|
417
422
|
connection.logerror(this, 'score is NaN')
|
|
418
|
-
connection.results.add(this, {score: 0})
|
|
423
|
+
connection.results.add(this, { score: 0 })
|
|
419
424
|
return next()
|
|
420
425
|
}
|
|
421
426
|
|
|
@@ -451,10 +456,10 @@ exports.hook_deny = function (next, connection, params) {
|
|
|
451
456
|
|
|
452
457
|
// let pi_deny = params[0]; // (constants.deny, denysoft, ok)
|
|
453
458
|
// let pi_message = params[1];
|
|
454
|
-
const pi_name
|
|
459
|
+
const pi_name = params[2]
|
|
455
460
|
// let pi_function = params[3];
|
|
456
461
|
// let pi_params = params[4];
|
|
457
|
-
const pi_hook
|
|
462
|
+
const pi_hook = params[5]
|
|
458
463
|
|
|
459
464
|
// exceptions, whose 'DENY' should not be captured
|
|
460
465
|
if (pi_name) {
|
|
@@ -469,7 +474,7 @@ exports.hook_deny = function (next, connection, params) {
|
|
|
469
474
|
connection.results.add(this, { msg: `deny: ${pi_name}` })
|
|
470
475
|
connection.results.incr(this, { score: -2 })
|
|
471
476
|
|
|
472
|
-
next(constants.OK)
|
|
477
|
+
next(constants.OK) // resume the connection
|
|
473
478
|
}
|
|
474
479
|
|
|
475
480
|
exports.hook_connect = function (next, connection) {
|
|
@@ -527,12 +532,11 @@ exports.hook_queue_outbound = function (next, connection) {
|
|
|
527
532
|
exports.hook_reset_transaction = function (next, connection) {
|
|
528
533
|
if (this.should_we_skip(connection)) return next()
|
|
529
534
|
|
|
530
|
-
connection.results.add(this, {emit: true})
|
|
535
|
+
connection.results.add(this, { emit: true })
|
|
531
536
|
this.should_we_deny(next, connection, 'reset_transaction')
|
|
532
537
|
}
|
|
533
538
|
|
|
534
539
|
exports.hook_unrecognized_command = function (next, connection, params) {
|
|
535
|
-
|
|
536
540
|
if (this.should_we_skip(connection)) return next()
|
|
537
541
|
|
|
538
542
|
// in case karma is in config/plugins before tls
|
|
@@ -541,8 +545,8 @@ exports.hook_unrecognized_command = function (next, connection, params) {
|
|
|
541
545
|
// in case karma is in config/plugins before AUTH plugin(s)
|
|
542
546
|
if (connection.notes.authenticating) return next()
|
|
543
547
|
|
|
544
|
-
connection.results.incr(this, {score: -1})
|
|
545
|
-
connection.results.add(this, {fail: `cmd:(${params})`})
|
|
548
|
+
connection.results.incr(this, { score: -1 })
|
|
549
|
+
connection.results.add(this, { fail: `cmd:(${params})` })
|
|
546
550
|
|
|
547
551
|
return this.should_we_deny(next, connection, 'unrecognized_command')
|
|
548
552
|
}
|
|
@@ -553,72 +557,74 @@ exports.ip_history_from_redis = function (next, connection) {
|
|
|
553
557
|
if (this.should_we_skip(connection)) return next()
|
|
554
558
|
|
|
555
559
|
const expire = (this.cfg.redis.expire_days || 60) * 86400 // to days
|
|
556
|
-
const dbkey
|
|
560
|
+
const dbkey = `karma|${connection.remote.ip}`
|
|
557
561
|
|
|
558
562
|
// redis plugin is emitting errors, no need to here
|
|
559
563
|
if (!this.db) return next()
|
|
560
564
|
|
|
561
|
-
this.db
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
}
|
|
566
572
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
+
}
|
|
582
589
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
+
}
|
|
590
597
|
|
|
591
|
-
|
|
598
|
+
connection.results.add(plugin, results)
|
|
592
599
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
.catch(err => {
|
|
600
|
+
plugin.check_awards(connection)
|
|
601
|
+
next()
|
|
602
|
+
})
|
|
603
|
+
.catch((err) => {
|
|
597
604
|
connection.results.add(plugin, { err })
|
|
598
605
|
next()
|
|
599
606
|
})
|
|
600
607
|
}
|
|
601
608
|
|
|
602
609
|
exports.hook_mail = function (next, connection, params) {
|
|
603
|
-
|
|
604
610
|
if (this.should_we_skip(connection)) return next()
|
|
605
611
|
|
|
606
612
|
this.check_spammy_tld(params[0], connection)
|
|
607
613
|
|
|
608
614
|
// look for invalid (RFC 5321,(2)821) space in envelope from
|
|
609
615
|
const full_from = connection.current_line
|
|
610
|
-
if (full_from.toUpperCase().substring(0,11) !== 'MAIL FROM:<') {
|
|
616
|
+
if (full_from.toUpperCase().substring(0, 11) !== 'MAIL FROM:<') {
|
|
611
617
|
connection.loginfo(this, `RFC ignorant env addr format: ${full_from}`)
|
|
612
|
-
connection.results.add(this, {fail: 'rfc5321.MailFrom'})
|
|
618
|
+
connection.results.add(this, { fail: 'rfc5321.MailFrom' })
|
|
613
619
|
}
|
|
614
620
|
|
|
615
621
|
// apply TLS awards (if defined)
|
|
616
622
|
if (this.cfg.tls !== undefined) {
|
|
617
623
|
if (this.cfg.tls.set && connection.tls.enabled) {
|
|
618
|
-
connection.results.incr(this, {score: this.cfg.tls.set})
|
|
624
|
+
connection.results.incr(this, { score: this.cfg.tls.set })
|
|
619
625
|
}
|
|
620
626
|
if (this.cfg.tls.unset && !connection.tls.enabled) {
|
|
621
|
-
connection.results.incr(this, {score: this.cfg.tls.unset})
|
|
627
|
+
connection.results.incr(this, { score: this.cfg.tls.unset })
|
|
622
628
|
}
|
|
623
629
|
}
|
|
624
630
|
|
|
@@ -626,7 +632,6 @@ exports.hook_mail = function (next, connection, params) {
|
|
|
626
632
|
}
|
|
627
633
|
|
|
628
634
|
exports.hook_rcpt = function (next, connection, params) {
|
|
629
|
-
|
|
630
635
|
if (this.should_we_skip(connection)) return next()
|
|
631
636
|
|
|
632
637
|
const rcpt = params[0]
|
|
@@ -637,23 +642,22 @@ exports.hook_rcpt = function (next, connection, params) {
|
|
|
637
642
|
// odds of from_user=rcpt_user in ham: < 1%, in spam > 40%
|
|
638
643
|
// 2015-05 30-day sample: 84% spam correlation
|
|
639
644
|
if (connection?.transaction?.mail_from?.user === rcpt.user) {
|
|
640
|
-
connection.results.add(this, {fail: 'env_user_match'})
|
|
645
|
+
connection.results.add(this, { fail: 'env_user_match' })
|
|
641
646
|
}
|
|
642
647
|
|
|
643
648
|
this.check_syntax_RcptTo(connection)
|
|
644
649
|
|
|
645
|
-
connection.results.add(this, {fail: 'rcpt_to'})
|
|
650
|
+
connection.results.add(this, { fail: 'rcpt_to' })
|
|
646
651
|
|
|
647
652
|
return this.should_we_deny(next, connection, 'rcpt')
|
|
648
653
|
}
|
|
649
654
|
|
|
650
655
|
exports.hook_rcpt_ok = function (next, connection, rcpt) {
|
|
651
|
-
|
|
652
656
|
if (this.should_we_skip(connection)) return next()
|
|
653
657
|
|
|
654
658
|
const txn = connection.transaction
|
|
655
659
|
if (txn && txn.mail_from && txn.mail_from.user === rcpt.user) {
|
|
656
|
-
connection.results.add(this, {fail: 'env_user_match'})
|
|
660
|
+
connection.results.add(this, { fail: 'env_user_match' })
|
|
657
661
|
}
|
|
658
662
|
|
|
659
663
|
this.check_syntax_RcptTo(connection)
|
|
@@ -666,7 +670,7 @@ exports.hook_data_post = function (next, connection) {
|
|
|
666
670
|
|
|
667
671
|
if (this.should_we_skip(connection)) return next()
|
|
668
672
|
|
|
669
|
-
this.check_awards(connection)
|
|
673
|
+
this.check_awards(connection) // update awards
|
|
670
674
|
|
|
671
675
|
const results = connection.results.collate(this)
|
|
672
676
|
connection.logdebug(this, `adding header: ${results}`)
|
|
@@ -692,13 +696,13 @@ exports.hook_disconnect = function (next, connection) {
|
|
|
692
696
|
|
|
693
697
|
const k = connection.results.get('karma')
|
|
694
698
|
if (!k || k.score === undefined) {
|
|
695
|
-
connection.results.add(this, {err: 'karma results missing'})
|
|
699
|
+
connection.results.add(this, { err: 'karma results missing' })
|
|
696
700
|
return next()
|
|
697
701
|
}
|
|
698
702
|
|
|
699
703
|
if (!this.cfg.thresholds) {
|
|
700
704
|
this.check_awards(connection)
|
|
701
|
-
connection.results.add(this, {msg: 'no action', emit: true })
|
|
705
|
+
connection.results.add(this, { msg: 'no action', emit: true })
|
|
702
706
|
return next()
|
|
703
707
|
}
|
|
704
708
|
|
|
@@ -709,12 +713,11 @@ exports.hook_disconnect = function (next, connection) {
|
|
|
709
713
|
this.increment(connection, 'bad', 1)
|
|
710
714
|
}
|
|
711
715
|
|
|
712
|
-
connection.results.add(this, {emit: true })
|
|
716
|
+
connection.results.add(this, { emit: true })
|
|
713
717
|
next()
|
|
714
718
|
}
|
|
715
719
|
|
|
716
720
|
exports.get_award_loc_from_note = function (connection, award) {
|
|
717
|
-
|
|
718
721
|
if (connection.transaction) {
|
|
719
722
|
const obj = this.assemble_note_obj(connection.transaction, award)
|
|
720
723
|
if (obj) return obj
|
|
@@ -729,7 +732,6 @@ exports.get_award_loc_from_note = function (connection, award) {
|
|
|
729
732
|
}
|
|
730
733
|
|
|
731
734
|
exports.get_award_loc_from_results = function (connection, loc_bits) {
|
|
732
|
-
|
|
733
735
|
let pi_name = loc_bits[1]
|
|
734
736
|
let notekey = loc_bits[2]
|
|
735
737
|
|
|
@@ -756,16 +758,22 @@ exports.get_award_location = function (connection, award_key) {
|
|
|
756
758
|
const loc_bits = bits[0].split('.')
|
|
757
759
|
if (loc_bits.length === 1) return connection[bits[0]] // ex: relaying
|
|
758
760
|
|
|
759
|
-
if (loc_bits[0] === 'notes') {
|
|
761
|
+
if (loc_bits[0] === 'notes') {
|
|
762
|
+
// ex: notes.spf_mail_helo
|
|
760
763
|
return this.get_award_loc_from_note(connection, bits[0])
|
|
761
764
|
}
|
|
762
765
|
|
|
763
|
-
if (loc_bits[0] === 'results') {
|
|
766
|
+
if (loc_bits[0] === 'results') {
|
|
767
|
+
// ex: results.geoip.distance
|
|
764
768
|
return this.get_award_loc_from_results(connection, loc_bits)
|
|
765
769
|
}
|
|
766
770
|
|
|
767
771
|
// ex: transaction.results.spf
|
|
768
|
-
if (
|
|
772
|
+
if (
|
|
773
|
+
connection.transaction &&
|
|
774
|
+
loc_bits[0] === 'transaction' &&
|
|
775
|
+
loc_bits[1] === 'results'
|
|
776
|
+
) {
|
|
769
777
|
loc_bits.shift()
|
|
770
778
|
return this.get_award_loc_from_results(connection.transaction, loc_bits)
|
|
771
779
|
}
|
|
@@ -776,11 +784,13 @@ exports.get_award_location = function (connection, award_key) {
|
|
|
776
784
|
exports.get_award_condition = function (note_key, note_val) {
|
|
777
785
|
let wants
|
|
778
786
|
const keybits = note_key.split('@')
|
|
779
|
-
if (keybits[1]) {
|
|
787
|
+
if (keybits[1]) {
|
|
788
|
+
wants = keybits[1]
|
|
789
|
+
}
|
|
780
790
|
|
|
781
791
|
const valbits = note_val.split(/\s+/)
|
|
782
792
|
if (!valbits[1]) return wants
|
|
783
|
-
if (valbits[1] !== 'if') return wants
|
|
793
|
+
if (valbits[1] !== 'if') return wants // no if condition
|
|
784
794
|
|
|
785
795
|
if (valbits[2].match(/^(equals|gt|lt|match)$/)) {
|
|
786
796
|
if (valbits[3]) wants = valbits[3]
|
|
@@ -806,14 +816,16 @@ exports.check_awards = function (connection) {
|
|
|
806
816
|
// test the desired condition
|
|
807
817
|
const bits = award_terms.split(/\s+/)
|
|
808
818
|
const award = parseFloat(bits[0])
|
|
809
|
-
if (!bits[1] || bits[1] !== 'if') {
|
|
810
|
-
|
|
811
|
-
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
|
|
812
824
|
this.apply_award(connection, key, award)
|
|
813
825
|
delete karma.todo[key]
|
|
814
826
|
continue
|
|
815
827
|
}
|
|
816
|
-
if (note !== wants) continue
|
|
828
|
+
if (note !== wants) continue // didn't match
|
|
817
829
|
}
|
|
818
830
|
|
|
819
831
|
// connection.loginfo(this, `check_awards, case matching for: ${wants}`
|
|
@@ -840,7 +852,9 @@ exports.check_awards = function (connection) {
|
|
|
840
852
|
continue
|
|
841
853
|
case 'length': {
|
|
842
854
|
const operator = bits[3]
|
|
843
|
-
if (bits[4]) {
|
|
855
|
+
if (bits[4]) {
|
|
856
|
+
wants = bits[4]
|
|
857
|
+
}
|
|
844
858
|
switch (operator) {
|
|
845
859
|
case 'gt':
|
|
846
860
|
if (note.length <= parseFloat(wants)) continue
|
|
@@ -852,14 +866,19 @@ exports.check_awards = function (connection) {
|
|
|
852
866
|
if (note.length !== parseFloat(wants)) continue
|
|
853
867
|
break
|
|
854
868
|
default:
|
|
855
|
-
connection.logerror(
|
|
869
|
+
connection.logerror(
|
|
870
|
+
this,
|
|
871
|
+
`length operator "${operator}" not supported.`,
|
|
872
|
+
)
|
|
856
873
|
continue
|
|
857
874
|
}
|
|
858
875
|
break
|
|
859
876
|
}
|
|
860
|
-
case 'in':
|
|
877
|
+
case 'in': // if in pass whitelisted
|
|
861
878
|
// let list = bits[3];
|
|
862
|
-
if (bits[4]) {
|
|
879
|
+
if (bits[4]) {
|
|
880
|
+
wants = bits[4]
|
|
881
|
+
}
|
|
863
882
|
if (!Array.isArray(note)) continue
|
|
864
883
|
if (!wants) continue
|
|
865
884
|
if (note.indexOf(wants) !== -1) break // found!
|
|
@@ -874,25 +893,31 @@ exports.check_awards = function (connection) {
|
|
|
874
893
|
|
|
875
894
|
exports.apply_award = function (connection, nl, award) {
|
|
876
895
|
if (!award) return
|
|
877
|
-
if (isNaN(award)) {
|
|
896
|
+
if (isNaN(award)) {
|
|
897
|
+
// garbage in config
|
|
878
898
|
connection.logerror(this, `non-numeric award from: ${nl}:${award}`)
|
|
879
899
|
return
|
|
880
900
|
}
|
|
881
901
|
|
|
882
|
-
const bits = nl.split('@')
|
|
902
|
+
const bits = nl.split('@')
|
|
903
|
+
nl = bits[0] // strip off @... if present
|
|
883
904
|
|
|
884
|
-
connection.results.incr(this, {score: award})
|
|
905
|
+
connection.results.incr(this, { score: award })
|
|
885
906
|
connection.logdebug(this, `applied ${nl}:${award}`)
|
|
886
907
|
|
|
887
|
-
let trimmed =
|
|
888
|
-
nl.substring(0,
|
|
889
|
-
nl.substring(
|
|
890
|
-
|
|
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
|
|
891
916
|
|
|
892
|
-
if (trimmed.substring(0,7) === 'rcpt_to') trimmed = trimmed.substring(8)
|
|
893
|
-
if (trimmed.substring(0,7) === 'mail_from') trimmed = trimmed.substring(10)
|
|
894
|
-
if (trimmed.substring(0,7) === 'connect') trimmed = trimmed.substring(8)
|
|
895
|
-
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)
|
|
896
921
|
|
|
897
922
|
if (award > 0) connection.results.add(this, { pass: trimmed })
|
|
898
923
|
if (award < 0) connection.results.add(this, { fail: trimmed })
|
|
@@ -900,7 +925,7 @@ exports.apply_award = function (connection, nl, award) {
|
|
|
900
925
|
|
|
901
926
|
exports.check_spammy_tld = function (mail_from, connection) {
|
|
902
927
|
if (!this.cfg.spammy_tlds) return
|
|
903
|
-
if (mail_from.isNull()) return
|
|
928
|
+
if (mail_from.isNull()) return // null sender (bounce)
|
|
904
929
|
|
|
905
930
|
const from_tld = mail_from.host.split('.').pop()
|
|
906
931
|
// connection.logdebug(this, `from_tld: ${from_tld}`);
|
|
@@ -908,17 +933,17 @@ exports.check_spammy_tld = function (mail_from, connection) {
|
|
|
908
933
|
const tld_penalty = parseFloat(this.cfg.spammy_tlds[from_tld] || 0)
|
|
909
934
|
if (tld_penalty === 0) return
|
|
910
935
|
|
|
911
|
-
connection.results.incr(this, {score: tld_penalty})
|
|
912
|
-
connection.results.add(this, {fail: 'spammy.TLD'})
|
|
936
|
+
connection.results.incr(this, { score: tld_penalty })
|
|
937
|
+
connection.results.add(this, { fail: 'spammy.TLD' })
|
|
913
938
|
}
|
|
914
939
|
|
|
915
940
|
exports.check_syntax_RcptTo = function (connection) {
|
|
916
941
|
// look for an illegal (RFC 5321,(2)821) space in envelope recipient
|
|
917
942
|
const full_rcpt = connection.current_line
|
|
918
|
-
if (full_rcpt.toUpperCase().substring(0,9) === 'RCPT TO:<') return
|
|
943
|
+
if (full_rcpt.toUpperCase().substring(0, 9) === 'RCPT TO:<') return
|
|
919
944
|
|
|
920
945
|
connection.loginfo(this, `illegal envelope address format: ${full_rcpt}`)
|
|
921
|
-
connection.results.add(this, {fail: 'rfc5321.RcptTo'})
|
|
946
|
+
connection.results.add(this, { fail: 'rfc5321.RcptTo' })
|
|
922
947
|
}
|
|
923
948
|
|
|
924
949
|
exports.assemble_note_obj = function (prefix, key) {
|
|
@@ -942,50 +967,52 @@ exports.check_asn = function (connection, asnkey) {
|
|
|
942
967
|
|
|
943
968
|
if (this.cfg.asn.report_as) report_as.name = this.cfg.asn.report_as
|
|
944
969
|
|
|
945
|
-
this.db
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const asn_score = parseInt(res.good || 0) - (res.bad || 0)
|
|
954
|
-
const asn_results = {
|
|
955
|
-
asn_score,
|
|
956
|
-
asn_connections: res.connections,
|
|
957
|
-
asn_good: res.good,
|
|
958
|
-
asn_bad: res.bad,
|
|
959
|
-
emit: true,
|
|
960
|
-
}
|
|
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
|
+
}
|
|
961
978
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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,
|
|
965
987
|
}
|
|
966
|
-
|
|
967
|
-
|
|
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
|
+
}
|
|
968
995
|
}
|
|
969
|
-
}
|
|
970
996
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
+
}
|
|
977
1003
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
.catch(err => {
|
|
1004
|
+
connection.results.add(report_as, asn_results)
|
|
1005
|
+
})
|
|
1006
|
+
.catch((err) => {
|
|
981
1007
|
connection.results.add(this, { err })
|
|
982
1008
|
})
|
|
983
1009
|
}
|
|
984
1010
|
|
|
985
1011
|
exports.init_ip = async function (dbkey, rip, expire) {
|
|
986
1012
|
if (!this.db) return
|
|
987
|
-
await this.db
|
|
988
|
-
.
|
|
1013
|
+
await this.db
|
|
1014
|
+
.multi()
|
|
1015
|
+
.hmSet(dbkey, { bad: 0, good: 0, connections: 1 })
|
|
989
1016
|
.expire(dbkey, expire)
|
|
990
1017
|
.exec()
|
|
991
1018
|
}
|
|
@@ -1000,8 +1027,9 @@ exports.get_asn_key = function (connection) {
|
|
|
1000
1027
|
|
|
1001
1028
|
exports.init_asn = function (asnkey, expire) {
|
|
1002
1029
|
if (!this.db) return
|
|
1003
|
-
this.db
|
|
1004
|
-
.
|
|
1005
|
-
.
|
|
1030
|
+
this.db
|
|
1031
|
+
.multi()
|
|
1032
|
+
.hmSet(asnkey, { bad: 0, good: 0, connections: 1 })
|
|
1033
|
+
.expire(asnkey, expire * 2) // keep ASN longer
|
|
1006
1034
|
.exec()
|
|
1007
1035
|
}
|