Project

mailbounce

0.0
The project is in a healthy, maintained state
Answers one question about a rejected message: whose fault was it?
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 2.7
 Project Readme

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 :invalid

Without 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 lapses

Both 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
rake

Nothing 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.