Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fixes and added more tests #4

Merged
merged 21 commits into from
Feb 16, 2025
Merged

Bug fixes and added more tests #4

merged 21 commits into from
Feb 16, 2025

Conversation

lnedry
Copy link
Contributor

@lnedry lnedry commented Feb 7, 2025

Changes proposed in this pull request:

  • check host_list to confirm if local domain
  • added config file for allowed Message-IDs
  • added tests for bounce_spf()
  • added Sinon for creating mocks
  • moved bad_rcpt function from hook_data to hook_rcpt_ok
  • renamed config key check.reject_all to reject.all_bounces
  • moved config key check.bad_rcpt to reject.bad_rcpt
  • refactored most of the existing tests to be more thorough
  • Message-ID header regex to check for required angle brackets
  • moved reject_all from [check] to [reject] and renamed to all_bounces

Fixes #

  • fixed bug where config key reject.empty_return_path was ignored
  • fixed timeout bug in bounce_spf()
  • updated bounce_spf to use promises

Checklist:

  • docs updated
  • tests updated
  • Changes.md updated
  • package.json.version bumped

Update empty_return_path to match default in source code.
Moved reject_all=false from [check] to [reject] and renamed to all_bounces=false
Moved reject_all from [check] to [reject] and renamed to all_bounces
Added Sinon to create mocks for testing
- Updated bounce_spf() to use Promises.
- Moved bad_rcpt function from hook_data to hook_rcpt_ok.
- Renamed config key check.reject_all to reject.all_bounces.
- Moved config key check.bad_rcpt to reject.bad_rcpt
- Fixed bug where config key reject.empty_return_path was ignored.
- Fixed timeout bug in bounce_spf()
- Modified Message-ID header regex to check for required angle brackets.
Moved config key check.bad_rcpt to reject.bad_rcpt.
- Added tests for bounce_spf()
- Refactored most of the existing tests to be more thorough.
Moved config key check.bad_rcpt to reject.bad_rcpt
index.js Outdated
// legacy settings
const c = this.cfg
if (c.check.reject_all) c.reject.all_bounces = c.check.reject_all
if (c.check.reject) c.reject.reject = c.check.reject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't look right

index.js Outdated

next(DENY, 'this bounce message does not have 1 recipient')
return next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return next()
next()

Avoid a useless use of return here.

index.js Outdated
}

transaction.results.add(this, { fail: 'empty_return_path', emit: true })
return next(DENY, 'bounce with non-empty Return-Path (RFC 3834)')
return next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return next()
next()

Again, avoid useless use of return.

index.js Outdated

next()
return next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return next()
next()

avoid useless use of return. (The calling function does nothing with the functions return. Not using return next... everywhere makes it subtly more obvious that the return next... patterns above are strictly for flow control.)

index.js Outdated
}

exports.has_null_sender = function (connection, mail_from) {
const transaction = connection?.transaction
if (!transaction) return false
const { transaction } = connection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're no longer guarding against the transaction having been destroyed. How can you be certain that when this function is called, the transaction still exists?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under what circumstances could the transaction be destroyed before one of these hooks gets called?

Copy link
Member

@msimerson msimerson Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's most common when the remote disconnects in the middle of the conversation. Any hooks that were running before the connection dropped will still be running. The transaction may get cleaned up while a plugin hook is running, especially if the hook does anything asynchronous. Then subsequent attempts to access connection.transaction will be undefined and trigger an exception, which takes down the node process.

index.js Show resolved Hide resolved
index.js Outdated
// {fail: 'Message-ID not local', emit: true });
// if (!plugin.cfg.reject.non_local_msgid) return next();
// return next(DENY, "bounce with non-local Message-ID (RFC 3834)");
return next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return next()
next()

index.js Outdated
return next(DENY, 'Invalid bounce (spoofed sender)')
}

return next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return next()
next()

Copy link
Member

@msimerson msimerson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this PR, especially the improvement of config naming.

Removed spurious line of code
- Removed description for reject_all
- Added description for all_bounces
Removed useless use of return
Removed useless use of return
Restored transaction guarding
Added test to check for missing transaction
@lnedry
Copy link
Contributor Author

lnedry commented Feb 10, 2025

Removed spurious line of code.
Changed several return next() statements to just next()
Restored transaction guarding.
Added test for transaction guarding.
Replaced description in README for reject_all with description for all_bounces

index.js Outdated
@@ -103,16 +120,19 @@ exports.single_recipient = function (next, connection) {

transaction.results.add(this, { fail: 'single_recipient', emit: true })

if (!this.cfg.reject.single_recipient) return next()
if (this.cfg.reject.single_recipient) {
next(DENY, 'this bounce message has too many recipients')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
next(DENY, 'this bounce message has too many recipients')
return next(DENY, 'this bounce message has too many recipients')

You do need the return here, for flow control. Otherwise you'll be calling next() twice.

index.js Outdated
const rcpt = element.address()
if (!this.cfg.invalid_addrs[rcpt]) continue
transaction.results.add(this, { fail: 'bad_rcpt', emit: true })
if (this.cfg.invalid_addrs.includes(rcpt.address())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the result of rcpt.address() always lower case?

index.js Outdated
const transaction = connection?.transaction
if (!transaction) return false
const { transaction } = connection
if (!transaction) return next()
Copy link
Member

@msimerson msimerson Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!transaction) return next()
if (!transaction) return false

There's no next() in context here.

@msimerson
Copy link
Member

Perhaps a good test to add for each bounce test is passing in a connection without a transaction, and assuring that the function returns (doesn't trigger an exception).

minor cose cleanup
Copy link
Member

@msimerson msimerson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the last few issues I see.

}
}

exports.non_local_msgid = function (next, connection) {
if (!this.cfg.check.non_local_msgid) return next()
if (!this.has_null_sender(connection)) return next()

const transaction = connection?.transaction
if (!transaction) return next()
const { transaction } = connection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { transaction } = connection
const { transaction } = connection
if (!transaction) return

@@ -293,24 +305,22 @@ function find_received_headers(ips, body, connection, self) {
}

exports.bounce_spf_enable = function (next, connection) {
if (!connection.transaction) return next()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restore transaction guarding here.

if (!this.cfg.check.bounce_spf) return next()
if (!this.has_null_sender(connection)) return next()

const txn = connection?.transaction
if (!txn) return next()
const { transaction } = connection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { transaction } = connection
const { transaction } = connection
if (!transaction) return next()

- added tests for transaction guarding
- moved transaction guarding from has_null_sender() to each hook
- refactored has_null_sender()
Added tests for guarding transactions
Copy link
Member

@msimerson msimerson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

A final issue is documentation. You've added the use of three new config files to the plugin:

  • host_list
  • bounce_bad_rcpt
  • bounce_allowed_msgids

None of those files are mentioned in the README, nor is the format for what you'd put in them mentioned. There are alternatives to accomplish this:

1. Move the data into bounce.ini and add inline docs there

[check]
single_recipient=true
empty_return_path=true

[reject]
single_recipient=true
empty_return_path=true

; reject all bounce messages (generally a bad idea)
all_bounces=false

; reject bounces to "bad" recipients
bad_rcpt=true

; bad_rcpts[] = [email protected]
; bad_rcpts[] = [email protected]
; bad_rcpts[] = ....


; to validate bounce message IDs, add an entry to the domains array
; for each email domain you accept email for.
[msgid]
; domains[] = example.com
; domains[] = example.net
; domains[] = example.org

2. Add a section to the README for each config file

Explain why you'd use the file and what the contents of the file should be.

Finally, the local msgid checks the domain against the contents of the host_list file. In my case, there's 4 domains listed in hosts_list, but that file hasn't been updated since 2021 and the rcpt_to.in_hosts_list plugin isn't even loaded. I think you want to say something like this:

  • The Message-ID test needs to know what domains are 'local' to your mail server. If you use the rcpt_to.host_list plugin and have populated the config/host_list file, then you're done.
  • If you don't use that plugin, make sure the host_list file is empty and populate the [msgid].domains[] array in bounce.ini (or config/bounce_allowed_msgids).

Also, if you keep the file name bounce_allowed_msgids, a name that better indicates what goes in it might be bounce_msgid_allowed_domains?

updated settings to match defaults in source
- updated documentation for all_bounces and bad_rcpt
- added documentation for non_local_msgid
minor code clean up
cleaned up test code
@msimerson msimerson merged commit 6f5d5a6 into haraka:master Feb 16, 2025
6 checks passed
This was referenced Feb 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants