mailbounce
📠Email delivery failures for Ruby
Answers one question about a rejected message: whose fault was it?
mailbounce reads the two ways mail comes back: a rejection during the SMTP session, and a delivery status notification (RFC 3464) arriving afterwards. Pure Ruby, no dependencies beyond mail.
Features
- Report parsing (RFC 3464), every per-recipient block of a
multipart/report - Enhanced status codes (RFC 3463), read as class and condition apart
- Classification - was the rejection about the recipient, or about you
- No network, no state
It reports what a failure is, never what it costs. Retiring an address, retrying, and how long to remember either are yours to decide.
Contents
- Getting Started
- Classifying a Rejection
- Categories
- Permanent Is Not the Same as Final
- Reading a Report
- Testing
- History
Getting Started
Add this line to your application's Gemfile:
gem "mailbounce"Requires Ruby >= 3.3.4
Classifying a Rejection
rejection = MailBounce.classify(response: "550 5.1.1 <someone@example.com>: User unknown",
sending_ip: "203.0.113.9")
rejection.category # => :invalid
rejection.status.to_s # => "5.1.1"
rejection.permanent? # => true
rejection.about_us? # => false- response - the SMTP reply, or a diagnostic taken from a report
- sending_ip - optional, the address the mail went out from
Categories
| Category | What it means |
|---|---|
:invalid |
The mailbox doesn't exist - the one category that says something lasting about the address |
:full |
The mailbox exists and is over quota |
:oversized |
This message was too large; another might not be |
:blocked |
Policy, reputation, or the network - about the exchange, not the address |
:unknown |
Nothing matched |
Classification reads the enhanced status code first, then falls back to wording, since plenty of servers pair a bare 5.0.0 with a diagnostic that names the cause. The wording it knows is what Postfix, Exim, and Sendmail say by default - there's no per-provider table, since those go stale silently.
Unmatched rejections come back :unknown rather than as a guess.
Giving sending_ip is what makes the interesting case work. A server quoting the address the mail came from is describing the sender, so it classifies :blocked however firmly it talks about the recipient:
MailBounce.classify(response: "550 5.1.1 rejected because 203.0.113.9 is on a blocklist",
sending_ip: "203.0.113.9").category
# => :blocked, not :invalidWithout it, that response reads as a recipient failure - and retiring an address over someone else's opinion of your IP is the mistake this library exists to prevent.
Permanent Is Not the Same as Final
permanent? reports what the server claimed: a 5xx is permanent, per RFC 3463. category reports what the rejection was about. They disagree exactly where it matters:
rejection = MailBounce.classify(response: "550 5.7.1 Service unavailable; client host blocked")
rejection.permanent? # => true - the server said so
rejection.category # => :blocked - but a listing lapsesBoth are reported so a caller can decide which to believe. This library doesn't decide for you.
Reading a Report
report = MailBounce.parse(raw_bounce)
report.any? # => true, when there's a machine-readable part to read
report.recipients # => [#<MailBounce::Recipient ...>]
recipient = report.for("someone@example.com")
recipient.action # => "failed"
recipient.status.to_s # => "5.1.1"
recipient.permanent? # => true
recipient.permanent_failure? # => true - failed, and permanently
recipient.diagnostic_code # => "smtp; 550 5.1.1 ... User unknown"
recipient.remote_mta # => "dns; mx.example.com"Reports announce delays and successes by the same route and in the same shape, so permanent_failure? is usually the question worth asking - Action: delayed is not a bounce however permanent the covering message reads.
A report naming several recipients speaks for a given address only when it names it; one covering a single recipient needn't name anyone. Nothing raises: a report with no machine-readable part, or bytes that aren't a message at all, reports nothing.
Testing
bundle install
rakeNothing touches the network. Fixtures in test/fixtures/ are complete bounce messages covering permanent failure, delay, transient failure, several recipients, an unnamed recipient, and a report with no machine-readable part at all.
History
View the changelog
Contributing
Everyone is encouraged to help improve this project:
- Report bugs
- Fix bugs and submit pull requests
- Write, clarify, or fix documentation
- Suggest or add new features
License
MIT. See LICENSE.