mailbounce 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e07f3db5d808f4a1d3d9f4b53fe83f3f78f619008e14de533d4101cfe1f99a6e
4
+ data.tar.gz: b2a4341195772ea43fe13a282a27dbcff914da3dbd232f7f0ad7ecbd144a1a59
5
+ SHA512:
6
+ metadata.gz: f404a22a31ce871ab3e6fb948b0dc8ed5e1dafe9a85b74822fd5e30230f7d2b8ecfedea124370a349f4a139bbc7c136ea519f5ac46ad4f6f8c84518d74436abc
7
+ data.tar.gz: fd63774ff78d90ef852e94a1994e1282beef51b23daffae422041c0a70a3d02a4e0beb9330639d821093fdcc391afc22855f0efcf5d8a743ddb8a526765c94f1
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - Delivery status report parsing (RFC 3464), every per-recipient block of a `multipart/report`
8
+ - Enhanced status codes (RFC 3463), with the class read apart from the condition
9
+ - Rejection classification into `:invalid`, `:full`, `:oversized`, `:blocked`, and `:unknown`
10
+ - The sending address as a classification input, so a rejection quoting it reads as being about the sender
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PostRider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # mailbounce
2
+
3
+ 📭 Email delivery failures for Ruby
4
+
5
+ Answers one question about a rejected message: whose fault was it?
6
+
7
+ **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`.
8
+
9
+ ## Features
10
+
11
+ - Report parsing (RFC 3464), every per-recipient block of a `multipart/report`
12
+ - Enhanced status codes (RFC 3463), read as class and condition apart
13
+ - Classification - was the rejection about the recipient, or about you
14
+ - No network, no state
15
+
16
+ > It reports what a failure *is*, never what it *costs*. Retiring an address, retrying, and how long to remember either are yours to decide.
17
+
18
+ ## Contents
19
+
20
+ - [Getting Started](#getting-started)
21
+ - [Classifying a Rejection](#classifying-a-rejection)
22
+ - [Categories](#categories)
23
+ - [Permanent Is Not the Same as Final](#permanent-is-not-the-same-as-final)
24
+ - [Reading a Report](#reading-a-report)
25
+ - [Testing](#testing)
26
+ - [History](#history)
27
+
28
+ ## Getting Started
29
+
30
+ Add this line to your application's Gemfile:
31
+
32
+ ```ruby
33
+ gem "mailbounce"
34
+ ```
35
+
36
+ Requires Ruby >= 3.3.4
37
+
38
+ ## Classifying a Rejection
39
+
40
+ ```ruby
41
+ rejection = MailBounce.classify(response: "550 5.1.1 <someone@example.com>: User unknown",
42
+ sending_ip: "203.0.113.9")
43
+
44
+ rejection.category # => :invalid
45
+ rejection.status.to_s # => "5.1.1"
46
+ rejection.permanent? # => true
47
+ rejection.about_us? # => false
48
+ ```
49
+
50
+ - **response** - the SMTP reply, or a diagnostic taken from a report
51
+ - **sending_ip** - optional, the address the mail went out from
52
+
53
+ ## Categories
54
+
55
+ | Category | What it means |
56
+ | --- | --- |
57
+ | `:invalid` | The mailbox doesn't exist - the one category that says something lasting about the address |
58
+ | `:full` | The mailbox exists and is over quota |
59
+ | `:oversized` | This message was too large; another might not be |
60
+ | `:blocked` | Policy, reputation, or the network - about the exchange, not the address |
61
+ | `:unknown` | Nothing matched |
62
+
63
+ 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.
64
+
65
+ Unmatched rejections come back `:unknown` rather than as a guess.
66
+
67
+ 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:
68
+
69
+ ```ruby
70
+ MailBounce.classify(response: "550 5.1.1 rejected because 203.0.113.9 is on a blocklist",
71
+ sending_ip: "203.0.113.9").category
72
+ # => :blocked, not :invalid
73
+ ```
74
+
75
+ 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.
76
+
77
+ ## Permanent Is Not the Same as Final
78
+
79
+ `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:
80
+
81
+ ```ruby
82
+ rejection = MailBounce.classify(response: "550 5.7.1 Service unavailable; client host blocked")
83
+
84
+ rejection.permanent? # => true - the server said so
85
+ rejection.category # => :blocked - but a listing lapses
86
+ ```
87
+
88
+ Both are reported so a caller can decide which to believe. This library doesn't decide for you.
89
+
90
+ ## Reading a Report
91
+
92
+ ```ruby
93
+ report = MailBounce.parse(raw_bounce)
94
+
95
+ report.any? # => true, when there's a machine-readable part to read
96
+ report.recipients # => [#<MailBounce::Recipient ...>]
97
+
98
+ recipient = report.for("someone@example.com")
99
+ recipient.action # => "failed"
100
+ recipient.status.to_s # => "5.1.1"
101
+ recipient.permanent? # => true
102
+ recipient.permanent_failure? # => true - failed, and permanently
103
+ recipient.diagnostic_code # => "smtp; 550 5.1.1 ... User unknown"
104
+ recipient.remote_mta # => "dns; mx.example.com"
105
+ ```
106
+
107
+ 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.
108
+
109
+ 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.
110
+
111
+ ## Testing
112
+
113
+ ```sh
114
+ bundle install
115
+ rake
116
+ ```
117
+
118
+ 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.
119
+
120
+ ## History
121
+
122
+ View the changelog
123
+
124
+ ## Contributing
125
+
126
+ Everyone is encouraged to help improve this project:
127
+
128
+ - [Report bugs](https://github.com/example/mailbounce/issues)
129
+ - Fix bugs and submit pull requests
130
+ - Write, clarify, or fix documentation
131
+ - Suggest or add new features
132
+
133
+ ## License
134
+
135
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,102 @@
1
+ require "mailbounce/status"
2
+
3
+ module MailBounce
4
+ # One per-recipient block of a delivery status report (RFC 3464 §2.3).
5
+ class Recipient
6
+ FAILED_ACTION = "failed".freeze
7
+
8
+ attr_reader :final_recipient, :original_recipient, :action, :diagnostic_code, :remote_mta
9
+
10
+ def initialize(final_recipient: nil, original_recipient: nil, failed_recipient: nil,
11
+ action: nil, status: nil, diagnostic_code: nil, remote_mta: nil)
12
+ @final_recipient = final_recipient
13
+ @original_recipient = original_recipient
14
+ @failed_recipient = failed_recipient
15
+ @action = action
16
+ @status = status
17
+ @diagnostic_code = diagnostic_code
18
+ @remote_mta = remote_mta
19
+ end
20
+
21
+ WRAPPED = /\A["'<]|[">']\z/
22
+
23
+ # RFC 3464 §2.3.2 wants Final-Recipient; also try Original-Recipient and
24
+ # X-Failed-Recipients so an address-less block is not treated as anonymous
25
+ # (anonymous answers every name).
26
+ def address
27
+ @address ||= sources.filter_map { |source| addressed(source) }.first.to_s
28
+ end
29
+
30
+ def anonymous?
31
+ address.empty?
32
+ end
33
+
34
+ def failed?
35
+ action.to_s.strip.casecmp?(FAILED_ACTION)
36
+ end
37
+
38
+ def permanent?
39
+ status&.permanent? || false
40
+ end
41
+
42
+ def permanent_failure?
43
+ failed? && permanent?
44
+ end
45
+
46
+ def status
47
+ @parsed_status ||= Status.parse(@status)
48
+ end
49
+
50
+ def addressed_to?(recipient)
51
+ named = recipient.to_s.strip
52
+
53
+ !named.empty? && named?(named)
54
+ end
55
+
56
+ private
57
+ def sources
58
+ [ @final_recipient, @original_recipient, @failed_recipient ]
59
+ end
60
+
61
+ # Type-tagged as `rfc822; someone@example.com`.
62
+ def addressed(source)
63
+ address = unwrapped(source.to_s.split(";", 2).last.to_s)
64
+
65
+ address unless address.empty?
66
+ end
67
+
68
+ # Strip outer brackets/quotes only. Quoted local-parts lose their quotes
69
+ # (safe miss — DSNs rarely carry that form).
70
+ def unwrapped(value)
71
+ address = value.strip
72
+
73
+ while address.match?(WRAPPED)
74
+ address = address.sub(/\A["'<]/, "").sub(/[">']\z/, "").strip
75
+ end
76
+
77
+ address
78
+ end
79
+
80
+ # An RFC 6533 address carries raw UTF-8, but the message it arrived in
81
+ # reads as bytes — the two only compare once their encodings agree.
82
+ def named?(named)
83
+ mine, theirs = comparable(address), comparable(named)
84
+
85
+ if mine.encoding == theirs.encoding
86
+ mine.casecmp?(theirs)
87
+ else
88
+ mine.b.casecmp?(theirs.b)
89
+ end
90
+ end
91
+
92
+ def comparable(value)
93
+ utf8 = value.dup.force_encoding(Encoding::UTF_8)
94
+
95
+ if utf8.valid_encoding?
96
+ utf8
97
+ else
98
+ value.b
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,115 @@
1
+ require "ipaddr"
2
+
3
+ require "mailbounce/status"
4
+
5
+ module MailBounce
6
+ # Classifies an SMTP rejection: recipient fault vs sender/exchange.
7
+ class Rejection
8
+ REPLY_CODE = /\A\s*(?<code>[245]\d\d)\b/
9
+
10
+ # RFC 3463 §3.2, class dropped. 1.2 is the system; permanently reported it
11
+ # is as dead as a mailbox (transient 4.1.2 is merely unreachable).
12
+ INVALID_CONDITIONS = %w[ 1.1 1.2 1.3 1.6 2.1 ].freeze
13
+
14
+ FULL_CONDITIONS = %w[ 2.2 ].freeze
15
+
16
+ OVERSIZED_CONDITIONS = %w[ 2.3 3.4 ].freeze
17
+
18
+ # RFC 3463 §3.2: sender's address, not the recipient's.
19
+ SENDER_CONDITIONS = %w[ 1.7 1.8 ].freeze
20
+
21
+ # Network, system, protocol, policy — the exchange, not the mailbox.
22
+ BLOCKED_SUBJECTS = %w[ 3 4 5 7 ].freeze
23
+
24
+ # Common MTA wording when no enhanced status is present. No bare
25
+ # "does not exist": that predicate takes any noun, so "sender address does
26
+ # not exist" would read as a dead recipient.
27
+ # No "mailbox unavailable": RFC 5321 §4.2.3 uses that phrase for both 450
28
+ # (busy / temp policy) and 550 (not found / no access / policy).
29
+ INVALID_TEXT = /user unknown|unknown user|no such (?:user|recipient)|recipient not found|invalid recipient/i
30
+
31
+ FULL_TEXT = /mailbox full|over quota/i
32
+
33
+ BLOCKED_TEXT = /blocked|blacklist|spam|denied/i
34
+
35
+ QUAD = /(?:\d{1,3}\.){3}\d{1,3}/
36
+
37
+ # Mapped form first: otherwise `\h*(?::\h*){2,}` stops at `::ffff:203`.
38
+ ADDRESS = /\h*(?::\h*)+:#{QUAD}|#{QUAD}|\h*(?::\h*){2,}/
39
+
40
+ attr_reader :response
41
+
42
+ def initialize(response:, sending_ip: nil)
43
+ @response = response.to_s
44
+ @sending_ips = Array(sending_ip).compact
45
+ end
46
+
47
+ # Prefer :unknown over a guess — a wrongful retire is worse than extra retries.
48
+ def category
49
+ @category ||=
50
+ if about_us?
51
+ :blocked
52
+ else
53
+ categorized_by_status || categorized_by_text || :unknown
54
+ end
55
+ end
56
+
57
+ # What the server claimed (5xx / class 5), not what we made of it.
58
+ def permanent?
59
+ if status
60
+ status.permanent?
61
+ else
62
+ reply_code.to_s.start_with?(Status::PERMANENT_CLASS)
63
+ end
64
+ end
65
+
66
+ def about_us?
67
+ ours.any? { |ip| quoted_addresses.include?(ip) }
68
+ end
69
+
70
+ def status
71
+ @status ||= Status.parse(@response)
72
+ end
73
+
74
+ private
75
+ # Unrecognized conditions fall through so bare 5.0.0 can use the diagnostic text.
76
+ def categorized_by_status
77
+ if status
78
+ case status.condition
79
+ when *INVALID_CONDITIONS then :invalid
80
+ when *FULL_CONDITIONS then :full
81
+ when *OVERSIZED_CONDITIONS then :oversized
82
+ when *SENDER_CONDITIONS then :blocked
83
+ else :blocked if BLOCKED_SUBJECTS.include?(status.subject)
84
+ end
85
+ end
86
+ end
87
+
88
+ def categorized_by_text
89
+ case @response
90
+ when INVALID_TEXT then :invalid
91
+ when FULL_TEXT then :full
92
+ when BLOCKED_TEXT then :blocked
93
+ end
94
+ end
95
+
96
+ def reply_code
97
+ @response[REPLY_CODE, :code]
98
+ end
99
+
100
+ def ours
101
+ @ours ||= @sending_ips.filter_map { |ip| address_for(ip) }
102
+ end
103
+
104
+ def quoted_addresses
105
+ @quoted_addresses ||= @response.scan(ADDRESS).filter_map { |token| address_for(token) }
106
+ end
107
+
108
+ # `.native` so ::ffff:203.0.113.9 and 203.0.113.9 compare equal.
109
+ def address_for(value)
110
+ IPAddr.new(value.to_s).native
111
+ rescue StandardError
112
+ nil
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,122 @@
1
+ require "mail"
2
+
3
+ require "mailbounce/recipient"
4
+
5
+ module MailBounce
6
+ # Machine-readable half of a bounce (RFC 3464). Unreadable input reports nothing.
7
+ class Report
8
+ # RFC 6533 adds the global type for internationalized addresses.
9
+ DELIVERY_STATUS_TYPES = %w[ message/delivery-status message/global-delivery-status ].freeze
10
+
11
+ # RFC 3464 §2.1.
12
+ BLANK_LINE = /\r?\n[ \t]*\r?\n/
13
+
14
+ FIELD_NAME = /\A(?<name>[A-Za-z][A-Za-z0-9\-]*)[ \t]*:/
15
+
16
+ # Value stays on its line so an empty field cannot swallow the next.
17
+ FIELDS = {
18
+ action: /^Action[ \t]*:[ \t]*(?<value>.+)$/i,
19
+ status: /^Status[ \t]*:[ \t]*(?<value>.+)$/i,
20
+ final_recipient: /^Final-Recipient[ \t]*:[ \t]*(?<value>.+)$/i,
21
+ original_recipient: /^Original-Recipient[ \t]*:[ \t]*(?<value>.+)$/i,
22
+ diagnostic_code: /^Diagnostic-Code[ \t]*:[ \t]*(?<value>.+)$/i,
23
+ remote_mta: /^Remote-MTA[ \t]*:[ \t]*(?<value>.+)$/i
24
+ }.freeze
25
+
26
+ def self.parse(raw)
27
+ new(raw)
28
+ end
29
+
30
+ def initialize(raw)
31
+ @mail = Mail.new(raw.to_s)
32
+ rescue StandardError
33
+ @mail = nil
34
+ end
35
+
36
+ def recipients
37
+ @recipients ||= reported_blocks.map { |block| recipient_from(block) }
38
+ end
39
+
40
+ # One unnamed recipient may stand in for any address; a named report only
41
+ # for the address it names.
42
+ def for(address = nil)
43
+ if named = recipients.find { |recipient| recipient.addressed_to?(address) }
44
+ named
45
+ else
46
+ sole_recipient_for(address)
47
+ end
48
+ end
49
+
50
+ def any?
51
+ recipients.any?
52
+ end
53
+
54
+ private
55
+ def sole_recipient_for(address)
56
+ if recipients.one? && (recipients.first.anonymous? || address.to_s.strip.empty?)
57
+ recipients.first
58
+ end
59
+ end
60
+
61
+ def blocks
62
+ delivery_status.to_s.split(BLANK_LINE).flat_map { |chunk| per_recipient(chunk) }
63
+ end
64
+
65
+ # Per-recipient blocks carry Action; the report preamble does not.
66
+ def reported_blocks
67
+ @reported_blocks ||= blocks.select { |block| unfolded(block).match?(FIELDS[:action]) }
68
+ end
69
+
70
+ # X-Failed-Recipients only when one block and one address — no positional guess.
71
+ def attributable_address
72
+ failed_recipients.first if reported_blocks.one? && failed_recipients.one?
73
+ end
74
+
75
+ def failed_recipients
76
+ @failed_recipients ||= failed_recipient_headers.flat_map { |header| header.split(",") }.map(&:strip).reject(&:empty?)
77
+ end
78
+
79
+ def failed_recipient_headers
80
+ Array(@mail&.[]("X-Failed-Recipients")).map(&:to_s)
81
+ rescue StandardError
82
+ []
83
+ end
84
+
85
+ # A block ends when a field it already has reappears (order is not fixed).
86
+ def per_recipient(chunk)
87
+ chunk.lines.each_with_object([]) do |line, blocks|
88
+ if blocks.last && continues?(blocks.last, line)
89
+ blocks.last << line
90
+ else
91
+ blocks << +line
92
+ end
93
+ end
94
+ end
95
+
96
+ def continues?(block, line)
97
+ if name = line[FIELD_NAME, :name]
98
+ !block.match?(/^#{Regexp.escape(name)}[ \t]*:/i)
99
+ else
100
+ true
101
+ end
102
+ end
103
+
104
+ def recipient_from(block)
105
+ fields = FIELDS.transform_values { |pattern| unfolded(block)[pattern, :value]&.strip }
106
+
107
+ Recipient.new(**fields, failed_recipient: attributable_address)
108
+ end
109
+
110
+ def unfolded(block)
111
+ block.gsub(/\r?\n[ \t]+/, " ")
112
+ end
113
+
114
+ # The mail gem parses and decodes lazily, so unreadable input raises here
115
+ # rather than at construction.
116
+ def delivery_status
117
+ @mail&.all_parts&.find { |part| DELIVERY_STATUS_TYPES.include?(part.mime_type) }&.body&.decoded
118
+ rescue StandardError
119
+ nil
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,58 @@
1
+ module MailBounce
2
+ # Enhanced status code (RFC 3463): class.subject.detail.
3
+ class Status
4
+ # Only where a status belongs — not dotted quads in diagnostics (5.1.1.10
5
+ # is an address). Misses a bare `smtp; 5.1.1 …`; that deferral is cheaper
6
+ # than retiring a mailbox over a host IP read as a status.
7
+ PATTERN = /
8
+ (?: \A[ \t]* | [245]\d\d[ \t\-]+ | ^Status[ \t]*:[ \t]* )
9
+ (?<code>[245]\.\d{1,3}\.\d{1,3}) (?!\.\d)
10
+ /xi
11
+
12
+ PERMANENT_CLASS = "5".freeze
13
+
14
+ TRANSIENT_CLASS = "4".freeze
15
+
16
+ def self.parse(text)
17
+ if code = text.to_s[PATTERN, :code]
18
+ new(code)
19
+ end
20
+ end
21
+
22
+ attr_reader :code
23
+
24
+ def initialize(code)
25
+ @code = code.to_s
26
+ end
27
+
28
+ def permanent?
29
+ status_class == PERMANENT_CLASS
30
+ end
31
+
32
+ def transient?
33
+ status_class == TRANSIENT_CLASS
34
+ end
35
+
36
+ # Class dropped: 5.1.1 and 4.1.1 share condition "1.1".
37
+ def condition
38
+ parts.drop(1).join(".")
39
+ end
40
+
41
+ def subject
42
+ parts[1].to_s
43
+ end
44
+
45
+ def to_s
46
+ code
47
+ end
48
+
49
+ private
50
+ def status_class
51
+ parts.first.to_s
52
+ end
53
+
54
+ def parts
55
+ @parts ||= code.split(".")
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module MailBounce
2
+ VERSION = "0.1.0"
3
+ end
data/lib/mailbounce.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "mailbounce/version"
2
+ require "mailbounce/status"
3
+ require "mailbounce/rejection"
4
+ require "mailbounce/recipient"
5
+ require "mailbounce/report"
6
+
7
+ # Reads delivery failures: bounce reports (RFC 3464) and SMTP rejections.
8
+ # Reports what a failure is; retiring or retrying is the caller's decision.
9
+ module MailBounce
10
+ def self.classify(response:, sending_ip: nil)
11
+ Rejection.new(response: response, sending_ip: sending_ip)
12
+ end
13
+
14
+ def self.parse(raw)
15
+ Report.parse(raw)
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
+ require_relative "lib/mailbounce/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "mailbounce"
6
+ spec.version = MailBounce::VERSION
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.required_ruby_version = ">= 3.3.4"
9
+ spec.authors = [ "Simon Lev" ]
10
+ spec.email = [ "support@postrider.dev" ]
11
+
12
+ spec.summary = "RFC 3464 delivery status notifications, parsed and classified, in pure Ruby."
13
+ spec.description = "Answers one question about a rejected message: whose fault was it?"
14
+
15
+ spec.homepage = "https://github.com/mailpiece/mailbounce"
16
+ spec.license = "MIT"
17
+
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
21
+ spec.metadata["rubygems_mfa_required"] = "true"
22
+
23
+ # Explicit list: `git ls-files` is empty before the first commit.
24
+ spec.files = Dir[
25
+ "lib/**/*.rb",
26
+ "CHANGELOG.md",
27
+ "README.md",
28
+ "LICENSE",
29
+ "mailbounce.gemspec"
30
+ ]
31
+ spec.require_paths = [ "lib" ]
32
+
33
+ spec.add_dependency "mail", ">= 2.7"
34
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailbounce
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Simon Lev
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mail
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '2.7'
26
+ description: 'Answers one question about a rejected message: whose fault was it?'
27
+ email:
28
+ - support@postrider.dev
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - LICENSE
35
+ - README.md
36
+ - lib/mailbounce.rb
37
+ - lib/mailbounce/recipient.rb
38
+ - lib/mailbounce/rejection.rb
39
+ - lib/mailbounce/report.rb
40
+ - lib/mailbounce/status.rb
41
+ - lib/mailbounce/version.rb
42
+ - mailbounce.gemspec
43
+ homepage: https://github.com/mailpiece/mailbounce
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ source_code_uri: https://github.com/mailpiece/mailbounce
48
+ changelog_uri: https://github.com/mailpiece/mailbounce/blob/main/CHANGELOG.md
49
+ bug_tracker_uri: https://github.com/mailpiece/mailbounce/issues
50
+ rubygems_mfa_required: 'true'
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.3.4
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 4.0.3
66
+ specification_version: 4
67
+ summary: RFC 3464 delivery status notifications, parsed and classified, in pure Ruby.
68
+ test_files: []