📠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.
- 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.
- Getting Started
- Classifying a Rejection
- Categories
- Permanent Is Not the Same as Final
- Reading a Report
- Testing
- History
- Contributing
- License
Add this line to your application's Gemfile:
gem "mailbounce"Requires Ruby >= 3.3.4
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
| 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? 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.
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.
bundle install
rakeView the changelog.
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
MIT. See LICENSE.