haraka-plugin-spamassassin 1.0.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 ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ The format is based on [Keep a Changelog](https://keepachangelog.com/).
4
+
5
+ ### Unreleased
6
+
7
+ ### [1.0.0] - 2024-05-07
8
+
9
+ - repackaged from haraka/Haraka
10
+
11
+ [1.0.0]: https://github.com/haraka/haraka-plugin-template/releases/tag/1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Haraka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ [![CI Test Status][ci-img]][ci-url]
2
+ [![Code Climate][clim-img]][clim-url]
3
+
4
+ [![NPM][npm-img]][npm-url]
5
+
6
+ # haraka-plugin-spamassassin
7
+
8
+ This plugin implements the spamd protocol and will send messages to spamd for scoring.
9
+
10
+ ## Configuration
11
+
12
+ ```
13
+ cp node_modules/haraka-plugin-spamassassin/config/spamassassin.ini config/spamassassin.ini
14
+ $EDITOR config/spamassassin.ini
15
+ ```
16
+
17
+ spamassassin.ini
18
+
19
+ - spamd_socket = \[host:port | /path/to/socket\] _optional_
20
+
21
+ Default: localhost:783
22
+
23
+ Host or path to socket where spamd is running.
24
+
25
+ - spamd_user = \[user\] _optional_
26
+
27
+ Default: default
28
+
29
+ Username to pass to spamd. This is useful when you are running
30
+ spamd with virtual users.
31
+
32
+ You can also pass this value in dynamically by setting:
33
+
34
+ 1. `connection.transaction.notes.spamd_user` in another plugin.
35
+
36
+ 2. The special username: _first-recipient_. The first envelope recipient
37
+ will be used as the username.
38
+
39
+ 3. the special username _all-recipients_ may eventually be supported. See
40
+ the get_spamd_username function in the plugin.
41
+
42
+ - max_size = N _optional_
43
+
44
+ Default: 500000
45
+
46
+ Maximum size of messages (in bytes) to send to spamd.
47
+ Messages over this size will be skipped.
48
+
49
+ - reject_threshold = N _optional_
50
+
51
+ Default: none (do not reject any mail)
52
+
53
+ SpamAssassin score at which the mail should be rejected.
54
+
55
+ - relay_reject_threshold = N _optional_
56
+
57
+ Default: none
58
+
59
+ As above, except this threshold only applies to connections
60
+ that are relays (e.g. AUTH) where connection.relaying = true.
61
+ This is used to set a _lower_ thresold at which to reject mail
62
+ from these hosts to prevent sending outbound spam.
63
+
64
+ If this is not set, then the `reject_thresold` value is used.
65
+
66
+ - munge_subject_threshold = N _optional_
67
+
68
+ Default: none (do not munge the subject)
69
+
70
+ Score at which the subject should be munged (prefixed).
71
+
72
+ - subject_prefix = \[prefix\] _optional_
73
+
74
+ Default: **_ SPAM _**
75
+
76
+ Prefix to use when munging the subject.
77
+
78
+ - old_headers_action = \[rename | drop | keep\] _optional_
79
+
80
+ Default: rename
81
+
82
+ If old X-Spam-\* headers are in the email, what do we do with them?
83
+
84
+ `rename` them to X-Old-Spam-\*.
85
+
86
+ `drop` will delete them.
87
+
88
+ `keep` will keep them (new X-Spam-\* headers appear lower down in
89
+ the headers then).
90
+
91
+ - connect_timeout = N _optional_
92
+
93
+ Default: 30
94
+
95
+ Time in seconds to wait for a connection to spamd
96
+
97
+ - results_timeout = N _optional_
98
+
99
+ Default: 300
100
+
101
+ Time in seconds to wait for results from spamd
102
+
103
+ ### [check]
104
+
105
+ The optional check section can allow skipping SpamAssassin check for remote connection
106
+ meeting following criteria.
107
+
108
+ - authenticated
109
+
110
+ Default: true
111
+
112
+ If true, messages from authenticated users will be scored.
113
+
114
+ - private_ip
115
+
116
+ Default: true
117
+
118
+ If true, messages from private IPs will be scored.
119
+
120
+ - local_ip
121
+
122
+ Default: true
123
+
124
+ If true, messages from localhost will be scored.
125
+
126
+ - relay
127
+
128
+ Default: true
129
+
130
+ If true, messages that are to be relayed will be scored.
131
+
132
+ ### [defer]
133
+
134
+ The optional defer section can allow returning a DENYSOFT status back to the
135
+ client. Setting these to true will force the client to retry later in cases where
136
+ spamassassin is not responding properly. If set to false, then the errors
137
+ will be ignored and message processing will continue.
138
+
139
+ - error
140
+
141
+ Default: false
142
+
143
+ If true, return DENYSOFT on socket errors
144
+
145
+ - connect_timeout
146
+
147
+ Default: false
148
+
149
+ If true, return DENYSOFT on socket connection timeouts
150
+
151
+ - scan_timeout
152
+
153
+ Default: false
154
+
155
+ If true, return DENYSOFT on scan timeouts
156
+
157
+ ## Extras
158
+
159
+ A SpamAssassin plugin can be found in the `contrib` directory.
160
+ The `Haraka.\[pm|cf\]` files should be placed in the SpamAssassin local
161
+ site rules directory (/etc/mail/spamassassin on Linux), spamd should be
162
+ restarted and the plugin will make spamd output the Haraka UUID as part
163
+ of its log output to aid debugging when searching the mail logs.
164
+
165
+ ## Changes
166
+
167
+ This plugin now passes the X-Spam-\* headers generated by SA through
168
+ unaltered. You can control the presence and appearance of X-Spam-\*
169
+ headers by editing your SpamAssassin config.
170
+
171
+ The default headers added by SpamAssassin are:
172
+
173
+ add_header all Checker-Version SpamAssassin _VERSION_ (_SUBVERSION_) on _HOSTNAME_
174
+ add_header spam Flag _YESNOCAPS_
175
+ add_header all Level _STARS(\*)_
176
+ add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_"
177
+
178
+ Other headers options you might find interesting or useful are:
179
+
180
+ add_header all DCC _DCCB_: _DCCR_
181
+ add_header all Tests _TESTS_
182
+
183
+ ## USAGE
184
+
185
+ <!-- leave these buried at the bottom of the document -->
186
+
187
+ [ci-img]: https://github.com/haraka/haraka-plugin-spamassassin/actions/workflows/ci.yml/badge.svg
188
+ [ci-url]: https://github.com/haraka/haraka-plugin-spamassassin/actions/workflows/ci.yml
189
+ [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-spamassassin/badges/gpa.svg
190
+ [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-spamassassin
191
+ [npm-img]: https://nodei.co/npm/haraka-plugin-spamassassin.png
192
+ [npm-url]: https://www.npmjs.com/package/haraka-plugin-spamassassin
@@ -0,0 +1,56 @@
1
+ ; How does Haraka connect to the SpamAssassin spamd daemon?
2
+ ; TCP/IP: 127.0.0.1:783
3
+ ; socket: /var/run/spamd/spamd.sock
4
+ spamd_socket=127.0.0.1:783
5
+
6
+ ; the username we tell spamd the message is to (default: default)
7
+ ;spamd_user=first-recipient (see docs)
8
+ ;spamd_user=
9
+
10
+ ; messages larger than this are not scored by SA
11
+ max_size=500000
12
+
13
+ ; Munge the subject of messages with a score higher than..
14
+ ; munge_subject_threshold=5
15
+ subject_prefix=*** SPAM ***
16
+
17
+ ; what to do with incoming messages with X-Spam-* headers
18
+ ; options are: rename, drop, keep
19
+ old_headers_action=rename
20
+
21
+ ; use the SpamAssassin 3.0+ syntax in X-Spam-Status header
22
+ ; modern: No, score=0.8 required=8.0 tests=...
23
+ ; legacy: No, hits=0.8 required=8.0 tests=...
24
+ modern_status_syntax=1
25
+
26
+ ; Reject all messages with more than this many hits
27
+ ; reject_threshold=10
28
+
29
+ ; when a connection has relay privileges, the rejection limit
30
+ ; relay_reject_threshold=7
31
+
32
+ ; How long should we wait for SpamAssassin to answer the socket
33
+ ; in seconds (default: 30)
34
+ ;connect_timeout=
35
+
36
+ ; How long should we wait for a result from SpamAssassin
37
+ ; in seconds (default: 300)
38
+ ;results_timeout=
39
+
40
+ ; Merge SpamAssassin's headers into the message
41
+ ;add_headers=true
42
+
43
+ ; the header that is sent to spamc
44
+ ;spamc_auth_header = X-Haraka-Relay
45
+
46
+ [check]
47
+ ;authenticated=true
48
+ ;private_ip=true
49
+ ;local_ip=true
50
+ ;relay=true
51
+
52
+ [defer]
53
+ ; Set to true to return DENYSOFT on errors, connection timeouts, or scanning timeouts
54
+ ;error=false
55
+ ;connect_timeout=false
56
+ ;scan_timeout=false
package/index.js ADDED
@@ -0,0 +1,405 @@
1
+ 'use strict'
2
+ // Call spamassassin via spamd
3
+
4
+ const net = require('node:net')
5
+
6
+ const utils = require('haraka-utils')
7
+ const net_utils = require('haraka-net-utils')
8
+
9
+ exports.register = function () {
10
+ this.load_spamassassin_ini()
11
+ }
12
+
13
+ exports.load_spamassassin_ini = function () {
14
+ this.cfg = this.config.get(
15
+ 'spamassassin.ini',
16
+ {
17
+ booleans: [
18
+ '+add_headers',
19
+ '+check.authenticated',
20
+ '+check.private_ip',
21
+ '+check.local_ip',
22
+ '+check.relay',
23
+
24
+ '-defer.error',
25
+ '-defer.connect_timeout',
26
+ '-defer.scan_timeout',
27
+ ],
28
+ },
29
+ () => {
30
+ this.load_spamassassin_ini()
31
+ },
32
+ )
33
+
34
+ const defaults = {
35
+ spamd_socket: 'localhost:783',
36
+ max_size: 500000,
37
+ old_headers_action: 'rename',
38
+ subject_prefix: '*** SPAM ***',
39
+ spamc_auth_header: 'X-Haraka-Relay',
40
+ }
41
+
42
+ for (const key in defaults) {
43
+ if (this.cfg.main[key]) continue
44
+ this.cfg.main[key] = defaults[key]
45
+ }
46
+
47
+ for (const item of [
48
+ 'reject_threshold',
49
+ 'relay_reject_threshold',
50
+ 'munge_subject_threshold',
51
+ 'max_size',
52
+ ]) {
53
+ if (!this.cfg.main[item]) continue
54
+ this.cfg.main[item] = Number(this.cfg.main[item])
55
+ }
56
+ }
57
+
58
+ exports.hook_data_post = function (next, connection) {
59
+ if (this.should_skip(connection)) return next()
60
+
61
+ const txn = connection.transaction
62
+ txn.remove_header(this.cfg.main.spamc_auth_header) // just to be safe
63
+
64
+ const username = this.get_spamd_username(connection)
65
+ const headers = this.get_spamd_headers(connection, username)
66
+ const socket = this.get_spamd_socket(next, connection, headers)
67
+
68
+ const spamd_response = { headers: {} }
69
+ let state = 'line0'
70
+ let last_header
71
+ const start = Date.now()
72
+
73
+ socket.on('line', (line) => {
74
+ connection.logprotocol(this, `Spamd C: ${line} state=${state}`)
75
+ line = line.replace(/\r?\n/, '')
76
+ if (state === 'line0') {
77
+ spamd_response.line0 = line
78
+ state = 'response'
79
+ } else if (state === 'response') {
80
+ if (line.match(/\S/)) {
81
+ const matches = line.match(
82
+ /Spam: (True|False) ; (-?\d+\.\d) \/ (-?\d+\.\d)/,
83
+ )
84
+ if (matches) {
85
+ spamd_response.flag = matches[1]
86
+ spamd_response.score = matches[2]
87
+ spamd_response.hits = matches[2] // backwards compat
88
+ spamd_response.reqd = matches[3]
89
+ spamd_response.flag = spamd_response.flag === 'True' ? 'Yes' : 'No'
90
+ }
91
+ } else {
92
+ state = 'headers'
93
+ }
94
+ } else if (state === 'headers') {
95
+ const m = line.match(/^X-Spam-([\x21-\x39\x3B-\x7E]+):\s*(.*)/)
96
+ if (m) {
97
+ connection.logdebug(this, `header: ${line}`)
98
+ last_header = m[1]
99
+ spamd_response.headers[m[1]] = m[2]
100
+ return
101
+ }
102
+ let fold
103
+ if (last_header && (fold = line.match(/^(\s+.*)/))) {
104
+ spamd_response.headers[last_header] += `\r\n${fold[1]}`
105
+ return
106
+ }
107
+ last_header = ''
108
+ }
109
+ })
110
+
111
+ socket.once('end', () => {
112
+ if (!connection.transaction) return next() // client gone
113
+
114
+ if (spamd_response.headers?.Tests) {
115
+ spamd_response.tests = spamd_response.headers.Tests.replace(/\s/g, '')
116
+ }
117
+ if (spamd_response.tests === undefined) {
118
+ // strip the 'tests' from the X-Spam-Status header
119
+ if (spamd_response.headers?.Status) {
120
+ // SpamAssassin appears to have a bug that causes a space not to
121
+ // be added before autolearn= when the header line has been folded.
122
+ // So we modify the regexp here not to match autolearn onwards.
123
+ const tests = /tests=((?:(?!autolearn)[^ ])+)/.exec(
124
+ spamd_response.headers.Status.replace(/\r?\n\t/g, ''),
125
+ )
126
+ if (tests) spamd_response.tests = tests[1]
127
+ }
128
+ }
129
+
130
+ // do stuff with the results...
131
+ txn.notes.spamassassin = spamd_response
132
+ connection.results.add(this, {
133
+ time: (Date.now() - start) / 1000,
134
+ hits: spamd_response.hits,
135
+ flag: spamd_response.flag,
136
+ })
137
+
138
+ this.fixup_old_headers(txn)
139
+ this.do_header_updates(connection, spamd_response)
140
+ this.log_results(connection, spamd_response)
141
+
142
+ const exceeds_err = this.score_too_high(connection, spamd_response)
143
+ if (exceeds_err) return next(DENY, exceeds_err)
144
+
145
+ this.munge_subject(connection, spamd_response.score)
146
+
147
+ next()
148
+ })
149
+ }
150
+
151
+ exports.fixup_old_headers = function (txn) {
152
+ const action = this.cfg.main.old_headers_action
153
+ const { headers } = txn.notes.spamassassin
154
+
155
+ let key
156
+ switch (action) {
157
+ case 'keep':
158
+ break
159
+ case 'drop':
160
+ for (key in headers) {
161
+ if (!key) continue
162
+ txn.remove_header(`X-Spam-${key}`)
163
+ }
164
+ break
165
+ // case 'rename':
166
+ default:
167
+ for (key in headers) {
168
+ if (!key) continue
169
+ key = `X-Spam-${key}`
170
+ const old_val = txn.header.get(key)
171
+ txn.remove_header(key)
172
+ if (old_val) {
173
+ // plugin.logdebug(plugin, `header: ${key}, ${old_val}`);
174
+ txn.add_header(key.replace(/^X-/, 'X-Old-'), old_val)
175
+ }
176
+ }
177
+ break
178
+ }
179
+ }
180
+
181
+ exports.munge_subject = function (conn, score) {
182
+ const munge = this.cfg.main.munge_subject_threshold
183
+ if (!munge) return
184
+ if (parseFloat(score) < parseFloat(munge)) return
185
+
186
+ const subj = conn.transaction.header.get('Subject')
187
+ const subject_re = new RegExp(
188
+ `^${utils.regexp_escape(this.cfg.main.subject_prefix)}`,
189
+ )
190
+ if (subject_re.test(subj)) return // prevent double munge
191
+
192
+ conn.transaction.remove_header('Subject')
193
+ conn.transaction.add_header(
194
+ 'Subject',
195
+ `${this.cfg.main.subject_prefix} ${subj}`,
196
+ )
197
+ }
198
+
199
+ exports.do_header_updates = function (conn, spamd_response) {
200
+ if (spamd_response.flag === 'Yes') {
201
+ // X-Spam-Flag is added by SpamAssassin
202
+ conn.transaction.remove_header('precedence')
203
+ conn.transaction.add_header('Precedence', 'junk')
204
+ }
205
+
206
+ const modern = this.cfg.main.modern_status_syntax
207
+ if (!this.cfg.main.add_headers) return
208
+
209
+ for (const key in spamd_response.headers) {
210
+ if (!key || key === '' || key === undefined) continue
211
+ let val = spamd_response.headers[key]
212
+ if (val === undefined) {
213
+ val = ''
214
+ }
215
+
216
+ if (key === 'Status' && !modern) {
217
+ const legacy = spamd_response.headers[key].replace(/ score=/, ' hits=')
218
+ conn.transaction.add_header('X-Spam-Status', legacy)
219
+ continue
220
+ }
221
+ if (val === '') continue
222
+ conn.transaction.add_header(`X-Spam-${key}`, val)
223
+ }
224
+ }
225
+
226
+ exports.score_too_high = function (conn, spamd_response) {
227
+ const { score } = spamd_response
228
+ if (conn.relaying) {
229
+ const rmax = this.cfg.main.relay_reject_threshold
230
+ if (rmax && score >= rmax) {
231
+ return 'spam score exceeded relay threshold'
232
+ }
233
+ }
234
+
235
+ const max = this.cfg.main.reject_threshold
236
+ if (max && score >= max) return 'spam score exceeded threshold'
237
+
238
+ return ''
239
+ }
240
+
241
+ exports.get_spamd_username = function (conn) {
242
+ let user = conn.transaction.notes.spamd_user // 1st priority
243
+ if (user && user !== undefined) return user
244
+
245
+ if (!this.cfg.main.spamd_user) return 'default' // when not defined
246
+ user = this.cfg.main.spamd_user
247
+
248
+ // Enable per-user SA prefs
249
+ if (user === 'first-recipient') {
250
+ // special cases
251
+ return conn.transaction.rcpt_to[0].address()
252
+ }
253
+ if (user === 'all-recipients') {
254
+ throw 'Unimplemented'
255
+ // TODO: pass the message through SA for each recipient. Then apply
256
+ // the least strict result to the connection. That is useful when
257
+ // one user blacklists a sender that another user wants to get mail
258
+ // from. If this is something you care about, this is the spot.
259
+ }
260
+ return user
261
+ }
262
+
263
+ exports.get_spamd_headers = function (conn, username) {
264
+ // http://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
265
+ const headers = [
266
+ 'HEADERS SPAMC/1.4',
267
+ `User: ${username}`,
268
+ '',
269
+ `X-Envelope-From: ${conn.transaction.mail_from.address()}`,
270
+ `X-Haraka-UUID: ${conn.transaction.uuid}`,
271
+ ]
272
+ if (conn.relaying) {
273
+ headers.push(`${this.cfg.main.spamc_auth_header}: true`)
274
+ }
275
+
276
+ return headers
277
+ }
278
+
279
+ exports.get_spamd_socket = function (next, conn, headers) {
280
+ const plugin = this
281
+ const txn = conn.transaction
282
+
283
+ const socket = new net.Socket()
284
+ socket.is_connected = false
285
+ net_utils.add_line_processor(socket)
286
+ const results_timeout = parseInt(plugin.cfg.main.results_timeout) || 300
287
+
288
+ socket.on('connect', function () {
289
+ // Abort if the transaction is gone
290
+ if (!txn) {
291
+ plugin.logwarn(conn, 'Transaction gone, cancelling SPAMD connection')
292
+ socket.end()
293
+ return
294
+ }
295
+
296
+ this.is_connected = true
297
+ // Reset timeout
298
+ this.setTimeout(results_timeout * 1000)
299
+ socket.write(`${headers.join('\r\n')}\r\n`)
300
+ conn.transaction.message_stream.pipe(socket)
301
+ })
302
+
303
+ socket.on('error', (err) => {
304
+ socket.destroy()
305
+ if (txn) txn.results.add(plugin, { err: `socket error: ${err.message}` })
306
+ if (plugin.cfg.defer.error) return next(DENYSOFT, 'spamd scan error')
307
+ return next()
308
+ })
309
+
310
+ socket.on('timeout', function () {
311
+ socket.destroy()
312
+ if (!this.is_connected) {
313
+ if (txn) txn.results.add(plugin, { err: `socket connect timeout` })
314
+ if (plugin.cfg.defer.connect_timeout)
315
+ return next(DENYSOFT, 'spamd connect timeout')
316
+ } else {
317
+ if (txn) txn.results.add(plugin, { err: `timeout waiting for results` })
318
+ if (plugin.cfg.defer.scan_timeout)
319
+ return next(DENYSOFT, 'spamd scan timeout')
320
+ }
321
+ return next()
322
+ })
323
+
324
+ const connect_timeout = parseInt(plugin.cfg.main.connect_timeout) || 30
325
+ socket.setTimeout(connect_timeout * 1000)
326
+
327
+ if (plugin.cfg.main.spamd_socket.match(/\//)) {
328
+ // assume unix socket
329
+ socket.connect(plugin.cfg.main.spamd_socket)
330
+ } else {
331
+ const hostport = plugin.cfg.main.spamd_socket.split(/:/)
332
+ socket.connect(hostport[1] || 783, hostport[0])
333
+ }
334
+
335
+ return socket
336
+ }
337
+
338
+ exports.log_results = function (conn, spamd_response) {
339
+ const cfg = this.cfg.main
340
+ const reject_threshold = conn.relaying
341
+ ? cfg.relay_reject_threshold || cfg.reject_threshold
342
+ : cfg.reject_threshold
343
+
344
+ const human_text =
345
+ `status=${spamd_response.flag}` +
346
+ `, score=${spamd_response.score}` +
347
+ `, required=${spamd_response.reqd}` +
348
+ `, reject=${reject_threshold}` +
349
+ `, tests="${spamd_response.tests}"`
350
+
351
+ conn.transaction.results.add(this, {
352
+ human: human_text,
353
+ status: spamd_response.flag,
354
+ score: parseFloat(spamd_response.score),
355
+ required: parseFloat(spamd_response.reqd),
356
+ reject: reject_threshold,
357
+ tests: spamd_response.tests,
358
+ emit: true,
359
+ })
360
+ }
361
+
362
+ exports.should_skip = function (connection = {}) {
363
+ const { transaction } = connection
364
+ if (!transaction) return true
365
+
366
+ // a message might be skipped for multiple reasons, store each in results
367
+ let result = false // default
368
+
369
+ const max = this.cfg.main.max_size
370
+ if (max) {
371
+ const size = connection.transaction.data_bytes
372
+ if (size > max) {
373
+ connection.transaction.results.add(this, {
374
+ skip: `size ${utils.prettySize(size)} exceeds max: ${utils.prettySize(max)}`,
375
+ })
376
+ result = true
377
+ }
378
+ }
379
+
380
+ if (this.cfg.check.authenticated == false && connection.notes.auth_user) {
381
+ connection.transaction.results.add(this, { skip: 'authed' })
382
+ result = true
383
+ }
384
+
385
+ if (this.cfg.check.relay == false && connection.relaying) {
386
+ connection.transaction.results.add(this, { skip: 'relay' })
387
+ result = true
388
+ }
389
+
390
+ if (this.cfg.check.local_ip == false && connection.remote.is_local) {
391
+ connection.transaction.results.add(this, { skip: 'local_ip' })
392
+ result = true
393
+ }
394
+
395
+ if (this.cfg.check.private_ip == false && connection.remote.is_private) {
396
+ if (this.cfg.check.local_ip == true && connection.remote.is_local) {
397
+ // local IPs are included in private IPs
398
+ } else {
399
+ connection.transaction.results.add(this, { skip: 'private_ip' })
400
+ result = true
401
+ }
402
+ }
403
+
404
+ return result
405
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "haraka-plugin-spamassassin",
3
+ "version": "1.0.0",
4
+ "description": "Haraka plugin that...CHANGE THIS",
5
+ "main": "index.js",
6
+ "files": [
7
+ "CHANGELOG.md",
8
+ "config"
9
+ ],
10
+ "scripts": {
11
+ "format": "npm run prettier:fix && npm run lint:fix",
12
+ "lint": "npx eslint@^8 *.js test",
13
+ "lint:fix": "npx eslint@^8 *.js test --fix",
14
+ "prettier": "npx prettier . --check",
15
+ "prettier:fix": "npx prettier . --write --log-level=warn",
16
+ "test": "node --test",
17
+ "versions": "npx dependency-version-checker check",
18
+ "versions:fix": "npx dependency-version-checker update"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/haraka/haraka-plugin-spamassassin.git"
23
+ },
24
+ "keywords": [
25
+ "haraka",
26
+ "plugin",
27
+ "spamassassin"
28
+ ],
29
+ "author": "Haraka Team <haraka.team@gmail.com>",
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/haraka/haraka-plugin-spamassassin/issues"
33
+ },
34
+ "homepage": "https://github.com/haraka/haraka-plugin-spamassassin#readme",
35
+ "dependencies": {
36
+ "haraka-net-utils": "^1.7.0",
37
+ "haraka-utils": "^1.1.3"
38
+ },
39
+ "devDependencies": {
40
+ "@haraka/eslint-config": "1.1.3",
41
+ "address-rfc2821": "^2.1.2",
42
+ "haraka-test-fixtures": "1.3.5"
43
+ }
44
+ }