haraka-plugin-spamassassin 1.1.0 → 1.1.1

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,6 +4,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
4
4
 
5
5
  ### Unreleased
6
6
 
7
+ ### [1.1.1] - 2026-06-20
8
+
9
+ - fix(spamd_socket): support `[ipv6]:port` via net_utils.endpoint #7
10
+ - deps(dev): bump haraka-test-fixtures to ^1.7.0
11
+ - refactor: rename hook_data_post to spamassassin_data_post
12
+ - split into reusable `parse_spamassassin` + `handle_spamassassin`
13
+ related to haraka/Haraka#3604
14
+
7
15
  ### [1.1.0] - 2026-05-17
8
16
 
9
17
  - changed: dep address-rfc2821 -> @haraka/email-address
@@ -40,3 +48,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
40
48
  [1.0.3]: https://github.com/haraka/haraka-plugin-spamassassin/releases/tag/v1.0.3
41
49
  [1.0.4]: https://github.com/haraka/haraka-plugin-spamassassin/releases/tag/v1.0.4
42
50
  [1.1.0]: https://github.com/haraka/haraka-plugin-spamassassin/releases/tag/v1.1.0
51
+ [1.1.1]: https://github.com/haraka/haraka-plugin-spamassassin/releases/tag/v1.1.1
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![CI Test Status][ci-img]][ci-url]
2
+ [![Code Coverage][cov-img]][cov-url]
2
3
  [![Code Climate][clim-img]][clim-url]
3
4
 
4
5
  # haraka-plugin-spamassassin
@@ -183,5 +184,7 @@ Other headers options you might find interesting or useful are:
183
184
 
184
185
  [ci-img]: https://github.com/haraka/haraka-plugin-spamassassin/actions/workflows/ci.yml/badge.svg
185
186
  [ci-url]: https://github.com/haraka/haraka-plugin-spamassassin/actions/workflows/ci.yml
186
- [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-spamassassin/badges/gpa.svg
187
- [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-spamassassin
187
+ [cov-img]: https://codecov.io/github/haraka/haraka-plugin-spamassassin/coverage.svg
188
+ [cov-url]: https://codecov.io/github/haraka/haraka-plugin-spamassassin
189
+ [clim-img]: https://qlty.sh/gh/haraka/projects/haraka-plugin-spamassassin/maintainability.svg
190
+ [clim-url]: https://qlty.sh/gh/haraka/projects/haraka-plugin-spamassassin
package/index.js CHANGED
@@ -8,6 +8,10 @@ const net_utils = require('haraka-net-utils')
8
8
 
9
9
  exports.register = function () {
10
10
  this.load_spamassassin_ini()
11
+ // explicit hook (not magic hook_data_post) so the plugin can be inherited;
12
+ // don't rename. guarded so inheritors don't re-register. haraka/Haraka#3604
13
+ if (this.name === 'spamassassin')
14
+ this.register_hook('data_post', 'spamassassin_data_post')
11
15
  }
12
16
 
13
17
  exports.load_spamassassin_ini = function () {
@@ -55,7 +59,7 @@ exports.load_spamassassin_ini = function () {
55
59
  }
56
60
  }
57
61
 
58
- exports.hook_data_post = function (next, connection) {
62
+ exports.spamassassin_data_post = function (next, connection) {
59
63
  if (this.should_skip(connection)) return next()
60
64
 
61
65
  const txn = connection.transaction
@@ -65,14 +69,30 @@ exports.hook_data_post = function (next, connection) {
65
69
  const headers = this.get_spamd_headers(connection, username)
66
70
  const socket = this.get_spamd_socket(next, connection, headers)
67
71
 
68
- const spamd_response = { headers: {} }
69
- let state = 'line0'
70
- let last_header
72
+ const lines = []
71
73
  const start = Date.now()
72
74
 
73
75
  socket.on('line', (line) => {
74
- connection.logprotocol(this, `Spamd C: ${line} state=${state}`)
75
- line = line.replace(/\r?\n/, '')
76
+ connection.logprotocol(this, `Spamd C: ${line}`)
77
+ lines.push(line.replace(/\r?\n/, ''))
78
+ })
79
+
80
+ socket.once('end', () => {
81
+ if (!connection.transaction) return next() // client gone
82
+ const spamd_response = this.parse_spamassassin(lines.join('\n'))
83
+ socket.nextOnce(
84
+ ...this.handle_spamassassin(connection, spamd_response, start),
85
+ )
86
+ })
87
+ }
88
+
89
+ // parse a raw spamd response into a result object. reusable by inheriting
90
+ // plugins (e.g. @haraka/plugin-ecsd). See haraka/Haraka#3604.
91
+ exports.parse_spamassassin = function (raw) {
92
+ const spamd_response = { headers: {} }
93
+ let state = 'line0'
94
+ let last_header
95
+ for (const line of String(raw).split(/\r?\n/)) {
76
96
  if (state === 'line0') {
77
97
  spamd_response.line0 = line
78
98
  state = 'response'
@@ -92,57 +112,59 @@ exports.hook_data_post = function (next, connection) {
92
112
  } else if (state === 'headers') {
93
113
  const m = line.match(/^X-Spam-([\x21-\x39\x3B-\x7E]+):\s*(.*)/)
94
114
  if (m) {
95
- connection.logdebug(this, `header: ${line}`)
96
115
  last_header = m[1]
97
116
  spamd_response.headers[m[1]] = m[2]
98
- return
117
+ continue
99
118
  }
100
119
  let fold
101
120
  if (last_header && (fold = line.match(/^(\s+.*)/))) {
102
121
  spamd_response.headers[last_header] += `\r\n${fold[1]}`
103
- return
122
+ continue
104
123
  }
105
124
  last_header = ''
106
125
  }
107
- })
108
-
109
- socket.once('end', () => {
110
- if (!connection.transaction) return next() // client gone
126
+ }
127
+ extract_tests(spamd_response)
128
+ return spamd_response
129
+ }
111
130
 
112
- if (spamd_response.headers?.Tests) {
113
- spamd_response.tests = spamd_response.headers.Tests.replace(/\s/g, '')
114
- }
115
- if (spamd_response.tests === undefined) {
116
- // strip the 'tests' from the X-Spam-Status header
117
- if (spamd_response.headers?.Status) {
118
- // SpamAssassin appears to have a bug that causes a space not to
119
- // be added before autolearn= when the header line has been folded.
120
- // So we modify the regexp here not to match autolearn onwards.
121
- const tests = /tests=((?:(?!autolearn)[^ ])+)/.exec(
122
- spamd_response.headers.Status.replace(/\r?\n\t/g, ''),
123
- )
124
- if (tests) spamd_response.tests = tests[1]
125
- }
126
- }
131
+ function extract_tests(spamd_response) {
132
+ if (spamd_response.headers?.Tests) {
133
+ spamd_response.tests = spamd_response.headers.Tests.replace(/\s/g, '')
134
+ return
135
+ }
136
+ // SpamAssassin omits a space before autolearn= on folded header lines, so
137
+ // don't match autolearn onwards.
138
+ if (spamd_response.headers?.Status) {
139
+ const tests = /tests=((?:(?!autolearn)[^ ])+)/.exec(
140
+ spamd_response.headers.Status.replace(/\r?\n\t/g, ''),
141
+ )
142
+ if (tests) spamd_response.tests = tests[1]
143
+ }
144
+ }
127
145
 
128
- // do stuff with the results...
129
- txn.notes.spamassassin = spamd_response
130
- connection.results.add(this, {
131
- time: (Date.now() - start) / 1000,
132
- flag: spamd_response.flag,
133
- })
146
+ // handle a parsed spamd response (annotate + headers + reject decision).
147
+ // I/O-free so inheriting plugins can reuse it; returns next() args ([] = CONT).
148
+ // See haraka/Haraka#3604.
149
+ exports.handle_spamassassin = function (connection, spamd_response, start) {
150
+ const txn = connection.transaction
151
+ if (!txn) return []
134
152
 
135
- this.fixup_old_headers(txn)
136
- this.do_header_updates(connection, spamd_response)
137
- this.log_results(connection, spamd_response)
153
+ txn.notes.spamassassin = spamd_response
154
+ connection.results.add(this, {
155
+ time: start === undefined ? undefined : (Date.now() - start) / 1000,
156
+ flag: spamd_response.flag,
157
+ })
138
158
 
139
- const exceeds_err = this.score_too_high(connection, spamd_response)
140
- if (exceeds_err) return socket.nextOnce(DENY, exceeds_err)
159
+ this.fixup_old_headers(txn)
160
+ this.do_header_updates(connection, spamd_response)
161
+ this.log_results(connection, spamd_response)
141
162
 
142
- this.munge_subject(connection, spamd_response.score)
163
+ const exceeds_err = this.score_too_high(connection, spamd_response)
164
+ if (exceeds_err) return [DENY, exceeds_err]
143
165
 
144
- socket.nextOnce()
145
- })
166
+ this.munge_subject(connection, spamd_response.score)
167
+ return []
146
168
  }
147
169
 
148
170
  exports.fixup_old_headers = function (txn) {
@@ -333,12 +355,12 @@ exports.get_spamd_socket = function (next, conn, headers) {
333
355
  const connect_timeout = parseInt(plugin.cfg.main.connect_timeout) || 30
334
356
  socket.setTimeout(connect_timeout * 1000)
335
357
 
336
- if (plugin.cfg.main.spamd_socket.match(/\//)) {
337
- // assume unix socket
338
- socket.connect(plugin.cfg.main.spamd_socket)
358
+ const ep = net_utils.endpoint(plugin.cfg.main.spamd_socket, 783)
359
+ if (ep instanceof Error) throw ep
360
+ if (ep.path) {
361
+ socket.connect(ep.path)
339
362
  } else {
340
- const hostport = plugin.cfg.main.spamd_socket.split(/:/)
341
- socket.connect(hostport[1] || 783, hostport[0])
363
+ socket.connect(ep.port, ep.host)
342
364
  }
343
365
 
344
366
  return socket
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haraka-plugin-spamassassin",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Haraka plugin that scans messages with SpamAssassin",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -8,16 +8,18 @@
8
8
  "config"
9
9
  ],
10
10
  "scripts": {
11
+ "prepare": "git rev-parse --git-dir >/dev/null 2>&1 && git config core.hooksPath .githooks || true",
11
12
  "format": "npm run prettier:fix && npm run lint:fix",
12
13
  "lint": "npx eslint *.js test",
13
14
  "lint:fix": "npx eslint *.js test --fix",
14
15
  "prettier": "npx prettier . --check",
15
16
  "prettier:fix": "npx prettier . --write --log-level=warn",
17
+ "qlty": "qlty smells --all",
16
18
  "test": "node --test",
17
- "versions": "npx npm-dep-mgr check",
18
- "versions:fix": "npx npm-dep-mgr update",
19
19
  "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=package.json --test-coverage-exclude=test/*.js",
20
- "test:coverage:lcov": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info test/*.js"
20
+ "test:coverage:lcov": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info test/*.js",
21
+ "versions": "npx npm-dep-mgr check",
22
+ "versions:fix": "npx npm-dep-mgr update"
21
23
  },
22
24
  "repository": {
23
25
  "type": "git",
@@ -35,13 +37,13 @@
35
37
  },
36
38
  "homepage": "https://github.com/haraka/haraka-plugin-spamassassin#readme",
37
39
  "dependencies": {
38
- "haraka-net-utils": "^1.8.2",
39
- "haraka-utils": "^1.1.4"
40
+ "haraka-net-utils": "^1.9.2",
41
+ "haraka-utils": "^2.2.1"
40
42
  },
41
43
  "devDependencies": {
42
- "@haraka/eslint-config": "^2.0.4",
43
- "@haraka/email-address": "^3.1.4",
44
- "haraka-test-fixtures": "^1.6.0"
44
+ "@haraka/email-address": "^3.1.6",
45
+ "@haraka/eslint-config": "^3.0.0",
46
+ "haraka-test-fixtures": "^1.7.2"
45
47
  },
46
48
  "prettier": {
47
49
  "singleQuote": true,