haraka-plugin-karma 2.3.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 +6 -5
- package/config/karma.ini +6 -5
- package/index.js +28 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
4
4
|
|
|
5
5
|
### Unreleased
|
|
6
6
|
|
|
7
|
+
### [2.4.0] - 2026-05-06
|
|
8
|
+
|
|
9
|
+
- extend spammy_tlds to support multi-label suffixes
|
|
10
|
+
- allow rspamd to deny(soft) image-only spam from specific ASNs
|
|
11
|
+
|
|
7
12
|
### [2.3.0] - 2026-05-05
|
|
8
13
|
|
|
9
14
|
- allow rspamd to greylist image-only spam
|
|
@@ -11,12 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
11
16
|
|
|
12
17
|
### [2.2.0] - 2026-03-31
|
|
13
18
|
|
|
14
|
-
#### Added
|
|
15
|
-
|
|
16
19
|
- allow rspamd to greylist ASNs when SA & rspamd agree
|
|
17
|
-
|
|
18
|
-
#### Changed
|
|
19
|
-
|
|
20
20
|
- replace to_object with arrays
|
|
21
21
|
- deps(all): updated to latest
|
|
22
22
|
- style: ES2024 updates throughout
|
|
@@ -171,3 +171,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
171
171
|
[2.1.8]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.1.8
|
|
172
172
|
[2.2.0]: https://github.com/haraka/haraka-plugin-karma/releases/tag/v2.2.0
|
|
173
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,20 +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
|
-
; allow rspamd to greylist (DENYSOFT) instead of karma intercepting
|
|
68
|
-
;
|
|
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.
|
|
69
69
|
;
|
|
70
70
|
; asn[] = 55286 ; ASN numbers eligible for greylisting
|
|
71
71
|
; asn[] = 33182
|
|
72
|
-
; asn[] =
|
|
72
|
+
; asn[] = 15169
|
|
73
73
|
;
|
|
74
74
|
; spamassassin_score = SpamAssassin hits score must exceed this value
|
|
75
75
|
; spamassassin_score = 5
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
[spammy_tlds]
|
|
79
|
-
; 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
|
|
80
81
|
; caution, awarding karma > msg_negative_limit may blacklist that TLD
|
|
81
82
|
bid=-3
|
|
82
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,29 +385,24 @@ 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
396
|
connection.transaction?.results?.get('spamassassin')?.hits ?? 0,
|
|
406
397
|
)
|
|
407
|
-
const saThreshold = parseFloat(this.cfg.
|
|
408
|
-
if (saScore >= saThreshold) return true
|
|
409
|
-
|
|
410
|
-
if (connection.transaction.header.get('X-Google-Group-Id')) return true
|
|
398
|
+
const saThreshold = parseFloat(this.cfg.rspamd_deny?.spamassassin_score ?? 5)
|
|
399
|
+
if (saScore >= saThreshold) return true // SA & rspamd agree
|
|
411
400
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
)
|
|
418
|
-
return true
|
|
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
|
|
419
406
|
|
|
420
407
|
return false
|
|
421
408
|
}
|
|
@@ -445,10 +432,7 @@ exports.should_we_deny = function (next, connection, hook) {
|
|
|
445
432
|
return this.apply_tarpit(connection, hook, score, next)
|
|
446
433
|
}
|
|
447
434
|
|
|
448
|
-
let rejectMsg =
|
|
449
|
-
if (this.cfg.deny && this.cfg.deny.message) {
|
|
450
|
-
rejectMsg = this.cfg.deny.message
|
|
451
|
-
}
|
|
435
|
+
let rejectMsg = this.cfg.deny?.message ?? `very bad karma score: ${score}`
|
|
452
436
|
|
|
453
437
|
if (/\{/.test(rejectMsg)) {
|
|
454
438
|
rejectMsg = rejectMsg.replace(/\{score\}/, score)
|
|
@@ -463,18 +447,13 @@ exports.should_we_deny = function (next, connection, hook) {
|
|
|
463
447
|
exports.hook_deny = function (next, connection, params) {
|
|
464
448
|
if (this.should_we_skip(connection)) return next()
|
|
465
449
|
|
|
466
|
-
const [
|
|
450
|
+
const [_pi_rc, , pi_name, , , pi_hook] = params
|
|
467
451
|
|
|
468
452
|
// exceptions, whose 'DENY' should not be captured
|
|
469
453
|
if (pi_name) {
|
|
470
454
|
if (pi_name === 'karma') return next()
|
|
471
455
|
if (this.deny_exclude_plugins.includes(pi_name)) return next()
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
pi_rc === constants.DENYSOFT &&
|
|
475
|
-
pi_name === 'rspamd' &&
|
|
476
|
-
this.should_rspamd_greylist(connection)
|
|
477
|
-
)
|
|
456
|
+
if (pi_name === 'rspamd' && this.should_rspamd_deny(connection))
|
|
478
457
|
return next()
|
|
479
458
|
}
|
|
480
459
|
if (pi_hook && this.deny_exclude_hooks.includes(pi_hook)) return next()
|
|
@@ -934,13 +913,17 @@ exports.check_spammy_tld = function (mail_from, connection) {
|
|
|
934
913
|
if (!this.cfg.spammy_tlds) return
|
|
935
914
|
if (mail_from.isNull()) return // null sender (bounce)
|
|
936
915
|
|
|
937
|
-
const
|
|
916
|
+
const labels = mail_from.host.split('.')
|
|
938
917
|
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|
941
922
|
|
|
942
|
-
|
|
943
|
-
|
|
923
|
+
connection.results.incr(this, { score: tld_penalty })
|
|
924
|
+
connection.results.add(this, { fail: 'spammy.TLD' })
|
|
925
|
+
return
|
|
926
|
+
}
|
|
944
927
|
}
|
|
945
928
|
|
|
946
929
|
exports.check_syntax_RcptTo = function (connection) {
|