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 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.2.0] - 2026-03-31
7
+ ### [2.4.0] - 2026-05-06
8
8
 
9
- #### Added
9
+ - extend spammy_tlds to support multi-label suffixes
10
+ - allow rspamd to deny(soft) image-only spam from specific ASNs
10
11
 
11
- - allow rspamd to greylist ASNs when SA & rspamd agree
12
+ ### [2.3.0] - 2026-05-05
12
13
 
13
- #### Changed
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
- [greylist]
67
- ; When a connection comes from one of the listed ASNs and both SpamAssassin
68
- ; and rspamd scores exceed their respective thresholds, allow rspamd to
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[] = 46717
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
- this.deny_exclude_hooks = this.stringToArray(e.hooks)
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.greylist_asns = cfg.greylist?.asn ?? []
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.should_rspamd_greylist = function (connection) {
388
+ exports.should_rspamd_deny = function (connection) {
397
389
  // check connection's ASN is in the configured list
398
- let asnr = connection.results.get('asn')
399
- if (!asnr?.asn) asnr = connection.results.get('geoip')
400
- if (!asnr?.asn) return false
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.greylist?.spamassassin_score)
408
- if (isNaN(saScore) || isNaN(saThreshold) || saScore <= saThreshold)
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
- // check rspamd score exceeds configured threshold
412
- const rspamdScore = parseFloat(
413
- connection.transaction?.results?.get('rspamd')?.score,
414
- )
415
- const rspamdThreshold = parseFloat(this.cfg.greylist?.rspamd_score)
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 true
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 = 'very bad karma score: {score}'
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 [pi_rc, , pi_name, , , pi_hook] = params
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
- // allow rspamd to greylist when configured ASN/score conditions are met
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 from_tld = mail_from.host.split('.').pop()
916
+ const labels = mail_from.host.split('.')
941
917
 
942
- const tld_penalty = parseFloat(this.cfg.spammy_tlds[from_tld] || 0)
943
- if (tld_penalty === 0) return
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
- connection.results.incr(this, { score: tld_penalty })
946
- connection.results.add(this, { fail: 'spammy.TLD' })
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haraka-plugin-karma",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "A heuristics scoring and reputation engine for SMTP connections",
5
5
  "main": "index.js",
6
6
  "files": [