haraka-plugin-karma 2.2.0 → 2.4.0
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/CHANGELOG.md +11 -4
- package/config/karma.ini +6 -10
- package/index.js +30 -50
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
4
4
|
|
|
5
5
|
### Unreleased
|
|
6
6
|
|
|
7
|
-
### [2.
|
|
7
|
+
### [2.4.0] - 2026-05-06
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- extend spammy_tlds to support multi-label suffixes
|
|
10
|
+
- allow rspamd to deny(soft) image-only spam from specific ASNs
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
### [2.3.0] - 2026-05-05
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
- allow rspamd to greylist image-only spam
|
|
15
|
+
- allow rspamd to greylist Google Groups messages
|
|
14
16
|
|
|
17
|
+
### [2.2.0] - 2026-03-31
|
|
18
|
+
|
|
19
|
+
- allow rspamd to greylist ASNs when SA & rspamd agree
|
|
15
20
|
- replace to_object with arrays
|
|
16
21
|
- deps(all): updated to latest
|
|
17
22
|
- style: ES2024 updates throughout
|
|
@@ -165,3 +170,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
165
170
|
[2.1.7]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.7
|
|
166
171
|
[2.1.8]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.8
|
|
167
172
|
[2.2.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.2.0
|
|
173
|
+
[2.3.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.3.0
|
|
174
|
+
[2.4.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.4.0
|
package/config/karma.ini
CHANGED
|
@@ -63,25 +63,21 @@ plugins=send_email, tls, access, helo.checks, headers, rspamd, spamassassin, cla
|
|
|
63
63
|
hooks=rcpt, queue, queue_outbound
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
[
|
|
67
|
-
;
|
|
68
|
-
;
|
|
69
|
-
; greylist (DENYSOFT) instead of karma intercepting the denial.
|
|
70
|
-
; This is only relevant when rspamd is NOT in [deny_excludes] plugins.
|
|
66
|
+
[rspamd_deny]
|
|
67
|
+
; allow rspamd to greylist (DENYSOFT) or reject (DENY) instead of karma intercepting
|
|
68
|
+
; the denial. Only relevant when rspamd is NOT in [deny_excludes] plugins.
|
|
71
69
|
;
|
|
72
70
|
; asn[] = 55286 ; ASN numbers eligible for greylisting
|
|
73
71
|
; asn[] = 33182
|
|
74
|
-
; asn[] =
|
|
72
|
+
; asn[] = 15169
|
|
75
73
|
;
|
|
76
74
|
; spamassassin_score = SpamAssassin hits score must exceed this value
|
|
77
75
|
; spamassassin_score = 5
|
|
78
|
-
;
|
|
79
|
-
; rspamd_score = rspamd score must exceed this value
|
|
80
|
-
; rspamd_score = 6
|
|
81
76
|
|
|
82
77
|
|
|
83
78
|
[spammy_tlds]
|
|
84
|
-
; award negative karma to spammy TLDs
|
|
79
|
+
; award negative karma to spammy TLDs or second-level domains
|
|
80
|
+
; multi-label suffixes are supported, e.g.: sa.com=-3
|
|
85
81
|
; caution, awarding karma > msg_negative_limit may blacklist that TLD
|
|
86
82
|
bid=-3
|
|
87
83
|
biz=-2
|
package/index.js
CHANGED
|
@@ -50,23 +50,15 @@ exports.load_karma_ini = function () {
|
|
|
50
50
|
this.merge_redis_ini()
|
|
51
51
|
|
|
52
52
|
const cfg = this.cfg
|
|
53
|
-
if (cfg.deny?.hooks)
|
|
54
|
-
this.deny_hooks = this.stringToArray(cfg.deny.hooks)
|
|
55
|
-
}
|
|
53
|
+
if (cfg.deny?.hooks) this.deny_hooks = this.stringToArray(cfg.deny.hooks)
|
|
56
54
|
|
|
57
55
|
const e = cfg.deny_excludes
|
|
58
|
-
if (e?.hooks)
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
if (e?.plugins) {
|
|
62
|
-
this.deny_exclude_plugins = this.stringToArray(e.plugins)
|
|
63
|
-
}
|
|
56
|
+
if (e?.hooks) this.deny_exclude_hooks = this.stringToArray(e.hooks)
|
|
57
|
+
if (e?.plugins) this.deny_exclude_plugins = this.stringToArray(e.plugins)
|
|
64
58
|
|
|
65
|
-
if (cfg.result_awards)
|
|
66
|
-
this.preparse_result_awards()
|
|
67
|
-
}
|
|
59
|
+
if (cfg.result_awards) this.preparse_result_awards()
|
|
68
60
|
|
|
69
|
-
this.
|
|
61
|
+
this.rspamdDenyAsns = cfg.rspamd_deny?.asn ?? cfg.greylist?.asn ?? []
|
|
70
62
|
|
|
71
63
|
if (!cfg.redis) cfg.redis = {}
|
|
72
64
|
if (!cfg.redis.host && cfg.redis.server_ip) {
|
|
@@ -393,34 +385,26 @@ exports.should_we_skip = function (connection) {
|
|
|
393
385
|
return false
|
|
394
386
|
}
|
|
395
387
|
|
|
396
|
-
exports.
|
|
388
|
+
exports.should_rspamd_deny = function (connection) {
|
|
397
389
|
// check connection's ASN is in the configured list
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (!
|
|
401
|
-
if (!this.greylist_asns.includes(String(asnr.asn))) return false
|
|
390
|
+
const cr = connection.results
|
|
391
|
+
const asn = cr.get('asn')?.asn ?? cr.get('geoip')?.asn
|
|
392
|
+
if (!this.rspamdDenyAsns.includes(String(asn))) return false
|
|
402
393
|
|
|
403
394
|
// check SpamAssassin score exceeds configured threshold
|
|
404
395
|
const saScore = parseFloat(
|
|
405
|
-
connection.transaction?.results?.get('spamassassin')?.hits,
|
|
396
|
+
connection.transaction?.results?.get('spamassassin')?.hits ?? 0,
|
|
406
397
|
)
|
|
407
|
-
const saThreshold = parseFloat(this.cfg.
|
|
408
|
-
if (
|
|
409
|
-
return false
|
|
398
|
+
const saThreshold = parseFloat(this.cfg.rspamd_deny?.spamassassin_score ?? 5)
|
|
399
|
+
if (saScore >= saThreshold) return true // SA & rspamd agree
|
|
410
400
|
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
isNaN(rspamdScore) ||
|
|
418
|
-
isNaN(rspamdThreshold) ||
|
|
419
|
-
rspamdScore <= rspamdThreshold
|
|
420
|
-
)
|
|
421
|
-
return false
|
|
401
|
+
const gGroups = connection.transaction.header.get('X-Google-Group-Id')
|
|
402
|
+
const imgOnly = connection.transaction.header
|
|
403
|
+
.get('content-type')
|
|
404
|
+
?.startsWith('multipart/related')
|
|
405
|
+
if (gGroups && imgOnly) return true
|
|
422
406
|
|
|
423
|
-
return
|
|
407
|
+
return false
|
|
424
408
|
}
|
|
425
409
|
|
|
426
410
|
exports.should_we_deny = function (next, connection, hook) {
|
|
@@ -448,10 +432,7 @@ exports.should_we_deny = function (next, connection, hook) {
|
|
|
448
432
|
return this.apply_tarpit(connection, hook, score, next)
|
|
449
433
|
}
|
|
450
434
|
|
|
451
|
-
let rejectMsg =
|
|
452
|
-
if (this.cfg.deny && this.cfg.deny.message) {
|
|
453
|
-
rejectMsg = this.cfg.deny.message
|
|
454
|
-
}
|
|
435
|
+
let rejectMsg = this.cfg.deny?.message ?? `very bad karma score: ${score}`
|
|
455
436
|
|
|
456
437
|
if (/\{/.test(rejectMsg)) {
|
|
457
438
|
rejectMsg = rejectMsg.replace(/\{score\}/, score)
|
|
@@ -466,18 +447,13 @@ exports.should_we_deny = function (next, connection, hook) {
|
|
|
466
447
|
exports.hook_deny = function (next, connection, params) {
|
|
467
448
|
if (this.should_we_skip(connection)) return next()
|
|
468
449
|
|
|
469
|
-
const [
|
|
450
|
+
const [_pi_rc, , pi_name, , , pi_hook] = params
|
|
470
451
|
|
|
471
452
|
// exceptions, whose 'DENY' should not be captured
|
|
472
453
|
if (pi_name) {
|
|
473
454
|
if (pi_name === 'karma') return next()
|
|
474
455
|
if (this.deny_exclude_plugins.includes(pi_name)) return next()
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
pi_rc === constants.DENYSOFT &&
|
|
478
|
-
pi_name === 'rspamd' &&
|
|
479
|
-
this.should_rspamd_greylist(connection)
|
|
480
|
-
)
|
|
456
|
+
if (pi_name === 'rspamd' && this.should_rspamd_deny(connection))
|
|
481
457
|
return next()
|
|
482
458
|
}
|
|
483
459
|
if (pi_hook && this.deny_exclude_hooks.includes(pi_hook)) return next()
|
|
@@ -937,13 +913,17 @@ exports.check_spammy_tld = function (mail_from, connection) {
|
|
|
937
913
|
if (!this.cfg.spammy_tlds) return
|
|
938
914
|
if (mail_from.isNull()) return // null sender (bounce)
|
|
939
915
|
|
|
940
|
-
const
|
|
916
|
+
const labels = mail_from.host.split('.')
|
|
941
917
|
|
|
942
|
-
|
|
943
|
-
|
|
918
|
+
for (let i = 0; i < labels.length; i++) {
|
|
919
|
+
const suffix = labels.slice(i).join('.')
|
|
920
|
+
const tld_penalty = parseFloat(this.cfg.spammy_tlds[suffix] || 0)
|
|
921
|
+
if (tld_penalty === 0) continue
|
|
944
922
|
|
|
945
|
-
|
|
946
|
-
|
|
923
|
+
connection.results.incr(this, { score: tld_penalty })
|
|
924
|
+
connection.results.add(this, { fail: 'spammy.TLD' })
|
|
925
|
+
return
|
|
926
|
+
}
|
|
947
927
|
}
|
|
948
928
|
|
|
949
929
|
exports.check_syntax_RcptTo = function (connection) {
|