Haraka 3.1.6 → 3.1.7

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/CONTRIBUTORS.md +8 -8
  3. package/Plugins.md +99 -99
  4. package/config/smtp_forward.ini +10 -0
  5. package/config/smtp_proxy.ini +10 -0
  6. package/connection.js +25 -8
  7. package/docs/plugins/queue/smtp_forward.md +19 -3
  8. package/docs/plugins/queue/smtp_proxy.md +10 -2
  9. package/haraka.js +1 -1
  10. package/outbound/hmail.js +39 -39
  11. package/outbound/index.js +4 -4
  12. package/outbound/tls.js +2 -43
  13. package/package.json +49 -48
  14. package/plugins/auth/auth_base.js +9 -3
  15. package/plugins/auth/auth_proxy.js +14 -11
  16. package/plugins/block_me.js +4 -2
  17. package/plugins/prevent_credential_leaks.js +3 -1
  18. package/plugins/process_title.js +6 -6
  19. package/plugins/queue/qmail-queue.js +15 -19
  20. package/plugins/queue/smtp_forward.js +12 -4
  21. package/plugins/queue/smtp_proxy.js +14 -3
  22. package/plugins/tls.js +13 -5
  23. package/plugins/xclient.js +3 -1
  24. package/server.js +5 -3
  25. package/smtp_client.js +20 -11
  26. package/test/config/block_me.recipient +1 -0
  27. package/test/config/block_me.senders +1 -0
  28. package/test/connection.js +24 -0
  29. package/test/outbound/bounce_net_errors.js +3 -2
  30. package/test/plugins/auth/auth_bridge.js +80 -0
  31. package/test/plugins/auth/flat_file.js +128 -0
  32. package/test/plugins/block_me.js +157 -0
  33. package/test/plugins/data.signatures.js +114 -0
  34. package/test/plugins/delay_deny.js +263 -0
  35. package/test/plugins/prevent_credential_leaks.js +178 -0
  36. package/test/plugins/process_title.js +135 -0
  37. package/test/plugins/queue/deliver.js +99 -0
  38. package/test/plugins/queue/discard.js +79 -0
  39. package/test/plugins/queue/lmtp.js +138 -0
  40. package/test/plugins/queue/qmail-queue.js +99 -0
  41. package/test/plugins/queue/quarantine.js +81 -0
  42. package/test/plugins/queue/smtp_bridge.js +154 -0
  43. package/test/plugins/queue/smtp_forward.js +42 -6
  44. package/test/plugins/queue/smtp_proxy.js +139 -0
  45. package/test/plugins/reseed_rng.js +34 -0
  46. package/test/plugins/tarpit.js +91 -0
  47. package/test/plugins/tls.js +25 -0
  48. package/test/plugins/toobusy.js +21 -0
  49. package/test/plugins/xclient.js +14 -0
  50. package/test/server.js +59 -0
  51. package/test/smtp_client.js +45 -12
  52. package/test/tls_socket.js +82 -0
  53. package/tls_socket.js +50 -0
package/outbound/hmail.js CHANGED
@@ -266,12 +266,12 @@ class HMailItem extends events.EventEmitter {
266
266
  }
267
267
 
268
268
  async found_mx(mxs) {
269
- // support draft-delany-nullmx-02
269
+ // support RFC 7505 null MX
270
270
  if (mxs.length === 1 && mxs[0].priority === 0 && mxs[0].exchange === '') {
271
271
  for (const rcpt of this.todo.rcpt_to) {
272
272
  this.extend_rcpt_with_dsn(
273
273
  rcpt,
274
- DSN.addr_bad_dest_system(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
274
+ DSN.addr_null_mx(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
275
275
  )
276
276
  }
277
277
  return this.bounce(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`)
@@ -459,7 +459,7 @@ class HMailItem extends events.EventEmitter {
459
459
  } else if (r.toUpperCase() === 'ENHANCEDSTATUSCODES') {
460
460
  smtp_properties.enh_status_codes = true
461
461
  } else if (r.toUpperCase() === 'SMTPUTF8') {
462
- smtp_properties.smtp_utf8 = true
462
+ smtp_properties.smtputf8 = true
463
463
  } else {
464
464
  // Check for SIZE parameter and limit
465
465
  let matches = r.match(/^SIZE\s+(\d+)$/)
@@ -476,9 +476,9 @@ class HMailItem extends events.EventEmitter {
476
476
  }
477
477
 
478
478
  function get_reverse_path_with_params() {
479
- const rp = self.todo.mail_from.format(!smtp_properties.smtp_utf8)
479
+ const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
480
480
  let rp_params = ''
481
- if (smtp_properties.smtp_utf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
481
+ if (smtp_properties.smtputf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
482
482
  return `FROM:${rp}${rp_params}`
483
483
  }
484
484
 
@@ -678,12 +678,12 @@ class HMailItem extends events.EventEmitter {
678
678
  processing_mail = false
679
679
  // Release back to the pool and instruct it to terminate this connection
680
680
  client_pool.release_client(socket, mx)
681
- self.todo.rcpt_to.forEach((rcpt) => {
681
+ for (const rcpt of self.todo.rcpt_to) {
682
682
  self.extend_rcpt_with_dsn(
683
683
  rcpt,
684
684
  DSN.proto_invalid_command(`Unrecognized response from upstream server: ${line}`),
685
685
  )
686
- })
686
+ }
687
687
  self.bounce(`Unrecognized response from upstream server: ${line}`, { mx })
688
688
  return
689
689
  }
@@ -720,14 +720,14 @@ class HMailItem extends events.EventEmitter {
720
720
  }
721
721
  // Error
722
722
  reason = response.join(' ')
723
- recipients.forEach((rcpt) => {
723
+ for (const rcpt of recipients) {
724
724
  rcpt.dsn_action = 'delayed'
725
725
  rcpt.dsn_smtp_code = code
726
726
  rcpt.dsn_smtp_extc = extc
727
727
  rcpt.dsn_status = extc
728
728
  rcpt.dsn_smtp_response = response.join(' ')
729
729
  rcpt.dsn_remote_mta = mx.exchange
730
- })
730
+ }
731
731
  send_command('QUIT')
732
732
  processing_mail = false
733
733
  return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
@@ -756,14 +756,14 @@ class HMailItem extends events.EventEmitter {
756
756
  }
757
757
  } else if (processing_mail) {
758
758
  reason = response.join(' ')
759
- recipients.forEach((rcpt) => {
759
+ for (const rcpt of recipients) {
760
760
  rcpt.dsn_action = 'delayed'
761
761
  rcpt.dsn_smtp_code = code
762
762
  rcpt.dsn_smtp_extc = extc
763
763
  rcpt.dsn_status = extc
764
764
  rcpt.dsn_smtp_response = response.join(' ')
765
765
  rcpt.dsn_remote_mta = mx.exchange
766
- })
766
+ }
767
767
  send_command('QUIT')
768
768
  processing_mail = false
769
769
  return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
@@ -803,14 +803,14 @@ class HMailItem extends events.EventEmitter {
803
803
  }
804
804
  }
805
805
  } else {
806
- recipients.forEach((rcpt) => {
806
+ for (const rcpt of recipients) {
807
807
  rcpt.dsn_action = 'failed'
808
808
  rcpt.dsn_smtp_code = code
809
809
  rcpt.dsn_smtp_extc = extc
810
810
  rcpt.dsn_status = extc
811
811
  rcpt.dsn_smtp_response = response.join(' ')
812
812
  rcpt.dsn_remote_mta = mx.exchange
813
- })
813
+ }
814
814
  send_command('QUIT')
815
815
  processing_mail = false
816
816
  return self.bounce(reason, { mx })
@@ -871,7 +871,7 @@ class HMailItem extends events.EventEmitter {
871
871
  case 'mail':
872
872
  last_recip = recipients[recip_index]
873
873
  recip_index++
874
- send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtp_utf8)}`)
874
+ send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
875
875
  break
876
876
  case 'rcpt':
877
877
  if (last_recip && code.match(/^250/)) {
@@ -887,7 +887,7 @@ class HMailItem extends events.EventEmitter {
887
887
  } else {
888
888
  last_recip = recipients[recip_index]
889
889
  recip_index++
890
- send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtp_utf8)}`)
890
+ send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
891
891
  }
892
892
  break
893
893
  case 'data': {
@@ -1036,7 +1036,7 @@ class HMailItem extends events.EventEmitter {
1036
1036
  msgid: `<${utils.uuid()}@${net_utils.get_primary_host_name()}>`,
1037
1037
  }
1038
1038
 
1039
- bounce_msg_.forEach((line) => {
1039
+ for (let line of bounce_msg_) {
1040
1040
  line = line.replace(/\{(\w+)\}/g, (i, word) => values[word] || '?')
1041
1041
 
1042
1042
  if (bounce_headers_done == false && line == '') {
@@ -1046,7 +1046,7 @@ class HMailItem extends events.EventEmitter {
1046
1046
  } else if (bounce_headers_done == true) {
1047
1047
  bounce_body_lines.push(line)
1048
1048
  }
1049
- })
1049
+ }
1050
1050
 
1051
1051
  const escaped_chars = {
1052
1052
  '&': 'amp',
@@ -1059,7 +1059,7 @@ class HMailItem extends events.EventEmitter {
1059
1059
  }
1060
1060
  const escape_pattern = new RegExp(`[${Object.keys(escaped_chars).join('')}]`, 'g')
1061
1061
 
1062
- bounce_msg_html_.forEach((line) => {
1062
+ for (let line of bounce_msg_html_) {
1063
1063
  line = line.replace(/\{(\w+)\}/g, (i, word) => {
1064
1064
  if (word in values) {
1065
1065
  return String(values[word]).replace(escape_pattern, (m) => `&${escaped_chars[m]};`)
@@ -1069,18 +1069,18 @@ class HMailItem extends events.EventEmitter {
1069
1069
  })
1070
1070
 
1071
1071
  bounce_html_lines.push(line)
1072
- })
1072
+ }
1073
1073
 
1074
- bounce_msg_image_.forEach((line) => {
1074
+ for (const line of bounce_msg_image_) {
1075
1075
  bounce_image_lines.push(line)
1076
- })
1076
+ }
1077
1077
 
1078
1078
  const boundary = `boundary_${utils.uuid()}`
1079
1079
  const bounce_body = []
1080
1080
 
1081
- bounce_header_lines.forEach((line) => {
1081
+ for (const line of bounce_header_lines) {
1082
1082
  bounce_body.push(`${line}${CRLF}`)
1083
- })
1083
+ }
1084
1084
  bounce_body.push(
1085
1085
  `Content-Type: multipart/report; report-type=delivery-status;${CRLF} boundary="${boundary}"${CRLF}`,
1086
1086
  )
@@ -1108,18 +1108,18 @@ class HMailItem extends events.EventEmitter {
1108
1108
  bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1109
1109
  bounce_body.push(`Content-Type: text/plain; charset=us-ascii${CRLF}`)
1110
1110
  bounce_body.push(CRLF)
1111
- bounce_body_lines.forEach((line) => {
1111
+ for (const line of bounce_body_lines) {
1112
1112
  bounce_body.push(`${line}${CRLF}`)
1113
- })
1113
+ }
1114
1114
  bounce_body.push(CRLF)
1115
1115
 
1116
1116
  if (bounce_html_lines.length > 1) {
1117
1117
  bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1118
1118
  bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`)
1119
1119
  bounce_body.push(CRLF)
1120
- bounce_html_lines.forEach((line) => {
1120
+ for (const line of bounce_html_lines) {
1121
1121
  bounce_body.push(`${line}${CRLF}`)
1122
- })
1122
+ }
1123
1123
  bounce_body.push(CRLF)
1124
1124
  bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
1125
1125
 
@@ -1128,9 +1128,9 @@ class HMailItem extends events.EventEmitter {
1128
1128
  bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1129
1129
  //bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`);
1130
1130
  //bounce_body.push(CRLF);
1131
- bounce_image_lines.forEach((line) => {
1131
+ for (const line of bounce_image_lines) {
1132
1132
  bounce_body.push(`${line}${CRLF}`)
1133
- })
1133
+ }
1134
1134
  bounce_body.push(CRLF)
1135
1135
  bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
1136
1136
  }
@@ -1146,7 +1146,7 @@ class HMailItem extends events.EventEmitter {
1146
1146
  if (this.todo.queue_time) {
1147
1147
  bounce_body.push(`Arrival-Date: ${utils.date_to_str(new Date(this.todo.queue_time))}${CRLF}`)
1148
1148
  }
1149
- this.todo.rcpt_to.forEach((rcpt_to) => {
1149
+ for (const rcpt_to of this.todo.rcpt_to) {
1150
1150
  bounce_body.push(CRLF)
1151
1151
  bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address()}${CRLF}`)
1152
1152
  let dsn_action = null
@@ -1208,16 +1208,16 @@ class HMailItem extends events.EventEmitter {
1208
1208
  if (diag_code != null) {
1209
1209
  bounce_body.push(`Diagnostic-Code: ${diag_code}${CRLF}`)
1210
1210
  }
1211
- })
1211
+ }
1212
1212
  bounce_body.push(CRLF)
1213
1213
 
1214
1214
  bounce_body.push(`--${boundary}${CRLF}`)
1215
1215
  bounce_body.push(`Content-Description: Undelivered Message Headers${CRLF}`)
1216
1216
  bounce_body.push(`Content-Type: text/rfc822-headers${CRLF}`)
1217
1217
  bounce_body.push(CRLF)
1218
- header.header_list.forEach((line) => {
1218
+ for (const line of header.header_list) {
1219
1219
  bounce_body.push(line)
1220
- })
1220
+ }
1221
1221
  bounce_body.push(CRLF)
1222
1222
 
1223
1223
  bounce_body.push(`--${boundary}--${CRLF}`)
@@ -1322,12 +1322,12 @@ class HMailItem extends events.EventEmitter {
1322
1322
  }
1323
1323
 
1324
1324
  convert_temp_failed_to_bounce(err, extra) {
1325
- this.todo.rcpt_to.forEach((rcpt_to) => {
1325
+ for (const rcpt_to of this.todo.rcpt_to) {
1326
1326
  rcpt_to.dsn_action = 'failed'
1327
1327
  if (rcpt_to.dsn_status) {
1328
1328
  rcpt_to.dsn_status = `${rcpt_to.dsn_status}`.replace(/^4/, '5')
1329
1329
  }
1330
- })
1330
+ }
1331
1331
  return this.bounce(err, extra)
1332
1332
  }
1333
1333
 
@@ -1446,9 +1446,9 @@ class HMailItem extends events.EventEmitter {
1446
1446
  })
1447
1447
  function err_handler(err, location) {
1448
1448
  logger.error(this, `Error while splitting to new recipients (${location}): ${err}`)
1449
- hmail.todo.rcpt_to.forEach((rcpt) => {
1449
+ for (const rcpt of hmail.todo.rcpt_to) {
1450
1450
  hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error splitting to new recipients: ${err}`))
1451
- })
1451
+ }
1452
1452
  hmail.bounce(`Error splitting to new recipients: ${err}`)
1453
1453
  }
1454
1454
 
@@ -1486,9 +1486,9 @@ class HMailItem extends events.EventEmitter {
1486
1486
  ws.on('error', (err) => {
1487
1487
  logger.error(this, `Unable to write queue file (${fname}): ${err}`)
1488
1488
  ws.destroy()
1489
- hmail.todo.rcpt_to.forEach((rcpt) => {
1489
+ for (const rcpt of hmail.todo.rcpt_to) {
1490
1490
  hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error re-queueing some recipients: ${err}`))
1491
- })
1491
+ }
1492
1492
  hmail.bounce(`Error re-queueing some recipients: ${err}`)
1493
1493
  })
1494
1494
 
package/outbound/index.js CHANGED
@@ -200,16 +200,16 @@ function get_deliveries(transaction) {
200
200
 
201
201
  // First get each domain
202
202
  const recips = {}
203
- transaction.rcpt_to.forEach((rcpt) => {
203
+ for (const rcpt of transaction.rcpt_to) {
204
204
  const domain = rcpt.host
205
205
  if (!recips[domain]) {
206
206
  recips[domain] = []
207
207
  }
208
208
  recips[domain].push(rcpt)
209
- })
210
- Object.keys(recips).forEach((domain) => {
209
+ }
210
+ for (const domain of Object.keys(recips)) {
211
211
  deliveries.push({ domain, rcpts: recips[domain] })
212
- })
212
+ }
213
213
  return deliveries
214
214
  }
215
215
 
package/outbound/tls.js CHANGED
@@ -8,18 +8,6 @@ const hkredis = require('haraka-plugin-redis')
8
8
  const logger = require('../logger')
9
9
  const tls_socket = require('../tls_socket')
10
10
 
11
- const inheritable_opts = [
12
- 'key',
13
- 'cert',
14
- 'ciphers',
15
- 'minVersion',
16
- 'dhparam',
17
- 'requestCert',
18
- 'honorCipherOrder',
19
- 'rejectUnauthorized',
20
- 'force_tls_hosts',
21
- ]
22
-
23
11
  class OutboundTLS {
24
12
  constructor() {
25
13
  this.config = config
@@ -34,37 +22,8 @@ class OutboundTLS {
34
22
 
35
23
  load_config() {
36
24
  const tls_cfg = tls_socket.load_tls_ini({ role: 'client' })
37
- const cfg = JSON.parse(JSON.stringify(tls_cfg.outbound || {}))
38
- cfg.redis = tls_cfg.redis // Don't clone - contains methods
39
-
40
- for (const opt of inheritable_opts) {
41
- if (cfg[opt] !== undefined) continue // option set in [outbound]
42
- if (tls_cfg.main[opt] === undefined) continue // opt unset in tls.ini[main]
43
- cfg[opt] = tls_cfg.main[opt] // use value from [main] section
44
- }
45
-
46
- if (cfg.key) {
47
- if (Array.isArray(cfg.key)) {
48
- cfg.key = cfg.key[0]
49
- }
50
- cfg.key = this.config.get(cfg.key, 'binary')
51
- }
52
-
53
- if (cfg.dhparam) {
54
- cfg.dhparam = this.config.get(cfg.dhparam, 'binary')
55
- }
56
-
57
- if (cfg.cert) {
58
- if (Array.isArray(cfg.cert)) {
59
- cfg.cert = cfg.cert[0]
60
- }
61
- cfg.cert = this.config.get(cfg.cert, 'binary')
62
- }
63
-
64
- if (!cfg.no_tls_hosts) cfg.no_tls_hosts = []
65
- if (!cfg.force_tls_hosts) cfg.force_tls_hosts = []
66
-
67
- this.cfg = cfg
25
+ this.cfg = tls_socket.load_plugin_tls_options(tls_cfg.outbound || {})
26
+ this.cfg.redis = tls_cfg.redis // outbound-only: TLS NO-GO db (don't clone has methods)
68
27
  }
69
28
 
70
29
  init(cb) {
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "server",
10
10
  "email"
11
11
  ],
12
- "version": "3.1.6",
12
+ "version": "3.1.7",
13
13
  "homepage": "http://haraka.github.io",
14
14
  "repository": {
15
15
  "type": "git",
@@ -20,58 +20,58 @@
20
20
  "node": ">=20"
21
21
  },
22
22
  "dependencies": {
23
- "address-rfc2821": "^2.2.0",
24
- "address-rfc2822": "^2.2.3",
25
- "haraka-config": "^1.5.0",
26
- "haraka-constants": "^1.0.7",
27
- "haraka-dsn": "^1.1.0",
28
- "haraka-email-message": "^1.3.3",
29
- "haraka-net-utils": "^1.8.2",
30
- "haraka-notes": "^1.1.1",
31
- "haraka-plugin-redis": "^2.0.11",
32
- "haraka-results": "^2.3.0",
33
- "haraka-tld": "^1.3.4",
34
- "haraka-utils": "^1.1.4",
23
+ "address-rfc2821": "~2.2.0",
24
+ "address-rfc2822": "~2.2.3",
25
+ "haraka-config": "~1.6.0",
26
+ "haraka-constants": "~1.0.7",
27
+ "haraka-dsn": "~1.2.0",
28
+ "haraka-email-message": "~1.3.3",
29
+ "haraka-net-utils": "~1.8.2",
30
+ "haraka-notes": "~1.1.3",
31
+ "haraka-plugin-redis": "~2.0.11",
32
+ "haraka-results": "~2.3.0",
33
+ "haraka-tld": "~1.3.4",
34
+ "haraka-utils": "~1.1.4",
35
35
  "ipaddr.js": "~2.4.0",
36
- "node-gyp": "^12.3.0",
37
- "nopt": "^9.0.0",
36
+ "node-gyp": "~12.3.0",
37
+ "nopt": "~10.0.0",
38
38
  "redis": "~5.12.1",
39
- "semver": "^7.8.0"
39
+ "semver": "~7.8.0"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@haraka/ocsp": "^1.2.0",
43
- "haraka-plugin-access": "^1.2.0",
44
- "haraka-plugin-aliases": "^1.0.3",
45
- "haraka-plugin-asn": "^2.0.6",
46
- "haraka-plugin-attachment": "^1.2.0",
47
- "haraka-plugin-bounce": "^2.1.2",
48
- "haraka-plugin-clamd": "^1.0.2",
49
- "haraka-plugin-dcc": "^1.0.3",
50
- "haraka-plugin-dkim": "^1.1.2",
51
- "haraka-plugin-dns-list": "^1.2.4",
52
- "haraka-plugin-early_talker": "^1.0.2",
53
- "haraka-plugin-fcrdns": "^1.1.2",
54
- "haraka-plugin-geoip": "^1.1.2",
55
- "haraka-plugin-greylist": "^1.1.1",
56
- "haraka-plugin-headers": "^1.1.0",
57
- "haraka-plugin-helo.checks": "^1.1.0",
58
- "haraka-plugin-karma": "^2.4.1",
59
- "haraka-plugin-known-senders": "^1.1.3",
60
- "haraka-plugin-limit": "^1.2.7",
61
- "haraka-plugin-mail_from.is_resolvable": "^1.2.0",
62
- "haraka-plugin-messagesniffer": "^1.0.1",
63
- "haraka-plugin-qmail-deliverable": "^1.3.5",
64
- "haraka-plugin-relay": "^1.0.1",
65
- "haraka-plugin-rspamd": "^1.5.0",
66
- "haraka-plugin-spamassassin": "^1.0.4",
67
- "haraka-plugin-spf": "^1.2.11",
68
- "haraka-plugin-syslog": "^1.0.7",
69
- "haraka-plugin-uribl": "^1.0.10"
42
+ "@haraka/ocsp": "~1.2.0",
43
+ "haraka-plugin-access": "~1.2.0",
44
+ "haraka-plugin-aliases": "~1.0.3",
45
+ "haraka-plugin-asn": "~2.1.0",
46
+ "haraka-plugin-attachment": "~1.2.0",
47
+ "haraka-plugin-bounce": "~2.1.2",
48
+ "haraka-plugin-clamd": "~1.0.2",
49
+ "haraka-plugin-dcc": "~1.0.3",
50
+ "haraka-plugin-dkim": "~1.1.2",
51
+ "haraka-plugin-dns-list": "~1.2.4",
52
+ "haraka-plugin-early_talker": "~1.0.2",
53
+ "haraka-plugin-fcrdns": "~1.1.2",
54
+ "haraka-plugin-geoip": "~1.1.2",
55
+ "haraka-plugin-greylist": "~1.1.1",
56
+ "haraka-plugin-headers": "~1.1.2",
57
+ "haraka-plugin-helo.checks": "~1.1.1",
58
+ "haraka-plugin-karma": "~2.4.1",
59
+ "haraka-plugin-known-senders": "~1.1.4",
60
+ "haraka-plugin-limit": "~1.2.7",
61
+ "haraka-plugin-mail_from.is_resolvable": "~1.2.0",
62
+ "haraka-plugin-messagesniffer": "~1.0.1",
63
+ "haraka-plugin-qmail-deliverable": "~1.3.5",
64
+ "haraka-plugin-relay": "~1.0.2",
65
+ "haraka-plugin-rspamd": "~1.5.0",
66
+ "haraka-plugin-spamassassin": "~1.0.4",
67
+ "haraka-plugin-spf": "~1.2.11",
68
+ "haraka-plugin-syslog": "~1.1.0",
69
+ "haraka-plugin-uribl": "~1.0.10"
70
70
  },
71
71
  "devDependencies": {
72
- "@haraka/eslint-config": "^2.0.4",
73
- "haraka-test-fixtures": "^1.4.3",
74
- "mock-require": "^3.0.3"
72
+ "@haraka/eslint-config": "~2.0.4",
73
+ "haraka-test-fixtures": "~1.6.0",
74
+ "mock-require": "~3.0.3"
75
75
  },
76
76
  "bugs": {
77
77
  "mail": "haraka.mail@gmail.com",
@@ -90,7 +90,8 @@
90
90
  "test": "sh ./run_tests",
91
91
  "versions": "npx npm-dep-mgr check",
92
92
  "versions:fix": "npx npm-dep-mgr update",
93
- "test:coverage": "npx c8 --reporter=text --reporter=text-summary npm test"
93
+ "test:coverage": "node --test --test-concurrency=1 --experimental-test-coverage",
94
+ "test:coverage:lcov": "mkdir -p coverage && node --test --test-concurrency=1 --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info"
94
95
  },
95
96
  "prettier": {
96
97
  "singleQuote": true,
@@ -99,6 +99,12 @@ exports.check_user = function (next, connection, credentials, method) {
99
99
  (typeof opts === 'object' ? opts.message : opts) ||
100
100
  (valid ? '2.7.0 Authentication successful' : '5.7.8 Authentication failed')
101
101
 
102
+ // The AUTH username is attacker-controlled (base64-decoded). Strip
103
+ // control chars before it is stored in notes or emitted into the
104
+ // Authentication-Results header (header injection).
105
+ // eslint-disable-next-line no-control-regex
106
+ const safe_user = String(credentials[0] ?? '').replace(/[\x00-\x1f\x7f]/g, '')
107
+
102
108
  if (valid) {
103
109
  connection.relaying = true
104
110
  connection.results.add({ name: 'relay' }, { pass: plugin.name })
@@ -108,14 +114,14 @@ exports.check_user = function (next, connection, credentials, method) {
108
114
  {
109
115
  pass: plugin.name,
110
116
  method,
111
- user: credentials[0],
117
+ user: safe_user,
112
118
  },
113
119
  )
114
120
 
115
121
  connection.respond(status_code, status_message, () => {
116
122
  connection.authheader = '(authenticated bits=0)\n'
117
123
  connection.auth_results(`auth=pass (${method.toLowerCase()})`)
118
- connection.notes.auth_user = credentials[0]
124
+ connection.notes.auth_user = safe_user
119
125
  if (!plugin.blankout_password) connection.notes.auth_passwd = credentials[1]
120
126
  next(OK)
121
127
  })
@@ -133,7 +139,7 @@ exports.check_user = function (next, connection, credentials, method) {
133
139
  }
134
140
  connection.lognotice(plugin, `delaying for ${delay} seconds`)
135
141
  // here we include the username, as shown in RFC 5451 example
136
- connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${credentials[0]}`)
142
+ connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${safe_user}`)
137
143
  setTimeout(() => {
138
144
  connection.respond(status_code, status_message, () => {
139
145
  connection.reset_transaction(() => next(OK))
@@ -93,7 +93,9 @@ exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) {
93
93
  if (cmd === 'dot') {
94
94
  line = '.'
95
95
  }
96
- connection.logprotocol(self, `C: ${line}`)
96
+ // Don't leak proxied SASL credentials (AUTH PLAIN <base64>) to logs
97
+ const safe = line.replace(/^(AUTH\s+\S+\s+).+$/i, '$1[redacted]')
98
+ connection.logprotocol(self, `C: ${safe}`)
97
99
  command = cmd.toLowerCase()
98
100
  this.write(`${line}\r\n`)
99
101
  // Clear response buffer from previous command
@@ -128,18 +130,19 @@ exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) {
128
130
  for (const i in response) {
129
131
  if (/^STARTTLS/.test(response[i])) {
130
132
  if (secure) continue // silly remote, we've already upgraded
131
- // Use TLS opportunistically if we found the key and certificate
133
+ // Opportunistic TLS: a client does not need its own
134
+ // certificate to negotiate TLS, so always STARTTLS when
135
+ // the backend offers it. The local key/cert are only
136
+ // attached if configured (mutual TLS), not required.
132
137
  key = self.config.get(self.tls_cfg.main.key || 'tls_key.pem', 'binary')
133
138
  cert = self.config.get(self.tls_cfg.main.cert || 'tls_cert.pem', 'binary')
134
- if (key && cert) {
135
- this.on('secure', () => {
136
- if (secure) return
137
- secure = true
138
- socket.send_command('EHLO', connection.local.host)
139
- })
140
- socket.send_command('STARTTLS')
141
- return
142
- }
139
+ this.on('secure', () => {
140
+ if (secure) return
141
+ secure = true
142
+ socket.send_command('EHLO', connection.local.host)
143
+ })
144
+ socket.send_command('STARTTLS')
145
+ return
143
146
  } else if (/^AUTH /.test(response[i])) {
144
147
  // Parse supported AUTH methods
145
148
  const parse = /^AUTH (.+)$/.exec(response[i])
@@ -4,6 +4,7 @@
4
4
  // mail_from.blocklist plugin for this to work fully.
5
5
 
6
6
  const fs = require('node:fs')
7
+ const path = require('node:path')
7
8
  const utils = require('haraka-utils')
8
9
 
9
10
  exports.hook_data = (next, connection) => {
@@ -45,8 +46,9 @@ exports.hook_data_post = function (next, connection) {
45
46
 
46
47
  connection.transaction.notes.block_me = 1
47
48
 
48
- // add to mail_from.blocklist
49
- fs.open('./config/mail_from.blocklist', 'a', (err, fd) => {
49
+ // add to mail_from.blocklist, in the same config dir the plugin reads from
50
+ const blocklist = path.join(this.config.root_path, 'mail_from.blocklist')
51
+ fs.open(blocklist, 'a', (err, fd) => {
50
52
  if (err) {
51
53
  connection.logerror(this, `Unable to append to mail_from.blocklist: ${err}`)
52
54
  return
@@ -22,9 +22,11 @@ exports.hook_data_post = (next, connection) => {
22
22
  let user = connection.notes.auth_user
23
23
  let domain
24
24
  const idx = user.indexOf('@')
25
- if (idx) {
25
+ if (idx > 0) {
26
26
  // If the username is qualified (e.g. user@domain.com)
27
27
  // then we make the @domain.com part optional in the regexp.
28
+ // (idx === -1 is "no @"; idx === 0 is a leading @ — neither is
29
+ // a qualified user@domain, so don't split.)
28
30
  domain = user.substring(idx)
29
31
  user = user.substring(0, idx)
30
32
  }
@@ -64,17 +64,17 @@ exports.hook_init_master = function (next, server) {
64
64
  server.notes.pt_connections++
65
65
  server.notes.pt_concurrent_cluster[msg.wid]++
66
66
  count = 0
67
- Object.keys(server.notes.pt_concurrent_cluster).forEach((id) => {
67
+ for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
68
68
  count += server.notes.pt_concurrent_cluster[id]
69
- })
69
+ }
70
70
  server.notes.pt_concurrent = count
71
71
  break
72
72
  case 'process_title.disconnect':
73
73
  server.notes.pt_concurrent_cluster[msg.wid]--
74
74
  count = 0
75
- Object.keys(server.notes.pt_concurrent_cluster).forEach((id) => {
75
+ for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
76
76
  count += server.notes.pt_concurrent_cluster[id]
77
- })
77
+ }
78
78
  server.notes.pt_concurrent = count
79
79
  break
80
80
  case 'process_title.recipient':
@@ -109,9 +109,9 @@ exports.hook_init_master = function (next, server) {
109
109
  delete server.notes.pt_concurrent_cluster[worker.id]
110
110
  // Update concurrency
111
111
  let count = 0
112
- Object.keys(server.notes.pt_concurrent_cluster).forEach((id) => {
112
+ for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
113
113
  count += server.notes.pt_concurrent_cluster[id]
114
- })
114
+ }
115
115
  server.notes.pt_concurrent = count
116
116
  server.notes.pt_child_exits++
117
117
  })