mailbounce 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e07f3db5d808f4a1d3d9f4b53fe83f3f78f619008e14de533d4101cfe1f99a6e
4
- data.tar.gz: b2a4341195772ea43fe13a282a27dbcff914da3dbd232f7f0ad7ecbd144a1a59
3
+ metadata.gz: 4f99ac991751ae3b826fb20a5e9e9f13c0871ef50b8957abab82daddccad242a
4
+ data.tar.gz: 5568d4cfaf4644bf4c984116276e0142ec1d4842bf79c6fed565f5dd965c2484
5
5
  SHA512:
6
- metadata.gz: f404a22a31ce871ab3e6fb948b0dc8ed5e1dafe9a85b74822fd5e30230f7d2b8ecfedea124370a349f4a139bbc7c136ea519f5ac46ad4f6f8c84518d74436abc
7
- data.tar.gz: fd63774ff78d90ef852e94a1994e1282beef51b23daffae422041c0a70a3d02a4e0beb9330639d821093fdcc391afc22855f0efcf5d8a743ddb8a526765c94f1
6
+ metadata.gz: 19e7c1b2748c550f40c283841e869de952724c29defc24404a2c7d0f251f515150e59383f304261c2b71ffb4d6e66d1f587952dd2e35ac86bc4c4edc9f11b6d5
7
+ data.tar.gz: febb828d483008e5abcd932ed760f86db3f8cde575f32ef47ae901a89d62b66a67c8349515aadababf0950bd85c9ca256f52c6dbfc57bb293d2f88677057f0ab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ - Split a report into per-recipient blocks
6
+ - A report is now read only up to 1 MB, which `parse` takes as `max_size`
7
+
8
+ ## 0.1.1
9
+
10
+ Documentation
11
+
3
12
  ## 0.1.0
4
13
 
5
14
  Initial release.
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # mailbounce
2
2
 
3
- 📭 Email delivery failures for Ruby
3
+ 📭 Email delivery failures for Ruby.
4
4
 
5
5
  Answers one question about a rejected message: whose fault was it?
6
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`.
7
+ **mailbounce** reads the two ways mail comes back: a rejection during the SMTP session, and a delivery status notification (RFC 3464) arriving afterwards.
8
8
 
9
9
  ## Features
10
10
 
@@ -24,6 +24,8 @@ Answers one question about a rejected message: whose fault was it?
24
24
  - [Reading a Report](#reading-a-report)
25
25
  - [Testing](#testing)
26
26
  - [History](#history)
27
+ - [Contributing](#contributing)
28
+ - [License](#license)
27
29
 
28
30
  ## Getting Started
29
31
 
@@ -52,13 +54,13 @@ rejection.about_us? # => false
52
54
 
53
55
  ## Categories
54
56
 
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 |
57
+ | Category | What it means |
58
+ | ------------ | ------------------------------------------------------------------------------------------ |
59
+ | `:invalid` | The mailbox doesn't exist - the one category that says something lasting about the address |
60
+ | `:full` | The mailbox exists and is over quota |
61
+ | `:oversized` | This message was too large; another might not be |
62
+ | `:blocked` | Policy, reputation, or the network - about the exchange, not the address |
63
+ | `:unknown` | Nothing matched |
62
64
 
63
65
  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
66
 
@@ -108,6 +110,12 @@ Reports announce delays and successes by the same route and in the same shape, s
108
110
 
109
111
  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
112
 
113
+ Bounce are cappted to 1 MB. Past that a report is refused like any other it can't read. Lower it where you'd rather hold less:
114
+
115
+ ```ruby
116
+ MailBounce.parse(raw_bounce, max_size: 64 * 1024)
117
+ ```
118
+
111
119
  ## Testing
112
120
 
113
121
  ```sh
@@ -115,21 +123,19 @@ bundle install
115
123
  rake
116
124
  ```
117
125
 
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
126
  ## History
121
127
 
122
- View the changelog
128
+ View the [changelog](CHANGELOG.md).
123
129
 
124
130
  ## Contributing
125
131
 
126
132
  Everyone is encouraged to help improve this project:
127
133
 
128
- - [Report bugs](https://github.com/example/mailbounce/issues)
134
+ - [Report bugs](https://github.com/mailpiece/mailbounce/issues)
129
135
  - Fix bugs and submit pull requests
130
136
  - Write, clarify, or fix documentation
131
137
  - Suggest or add new features
132
138
 
133
139
  ## License
134
140
 
135
- MIT. See [LICENSE](LICENSE).
141
+ MIT. See [LICENSE](LICENSE).
@@ -1,4 +1,5 @@
1
1
  require "mail"
2
+ require "set"
2
3
 
3
4
  require "mailbounce/recipient"
4
5
 
@@ -13,6 +14,10 @@ module MailBounce
13
14
 
14
15
  FIELD_NAME = /\A(?<name>[A-Za-z][A-Za-z0-9\-]*)[ \t]*:/
15
16
 
17
+ # A bounce is a notice, not a mailbox — Postfix caps its own at 50 KB.
18
+ # Anything past this is not a report anyone meant to send.
19
+ MAX_SIZE = 1024 * 1024
20
+
16
21
  # Value stays on its line so an empty field cannot swallow the next.
17
22
  FIELDS = {
18
23
  action: /^Action[ \t]*:[ \t]*(?<value>.+)$/i,
@@ -23,12 +28,14 @@ module MailBounce
23
28
  remote_mta: /^Remote-MTA[ \t]*:[ \t]*(?<value>.+)$/i
24
29
  }.freeze
25
30
 
26
- def self.parse(raw)
27
- new(raw)
31
+ def self.parse(raw, max_size: MAX_SIZE)
32
+ new(raw, max_size: max_size)
28
33
  end
29
34
 
30
- def initialize(raw)
31
- @mail = Mail.new(raw.to_s)
35
+ def initialize(raw, max_size: MAX_SIZE)
36
+ report = raw.to_s
37
+
38
+ @mail = Mail.new(report) if report.bytesize <= max_size
32
39
  rescue StandardError
33
40
  @mail = nil
34
41
  end
@@ -83,22 +90,30 @@ module MailBounce
83
90
  end
84
91
 
85
92
  # A block ends when a field it already has reappears (order is not fixed).
93
+ # The names are kept rather than matched for again: a block grows, and
94
+ # reading all of it per line costs the square of its length.
86
95
  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
96
+ blocks, named = [], Set.new
97
+
98
+ chunk.lines.each do |line|
99
+ name = field_name(line)
100
+
101
+ if blocks.empty? || named.include?(name)
91
102
  blocks << +line
103
+ named = Set.new
104
+ else
105
+ blocks.last << line
92
106
  end
107
+
108
+ named << name if name
93
109
  end
110
+
111
+ blocks
94
112
  end
95
113
 
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
114
+ # Field names are case-insensitive; a continuation line has none.
115
+ def field_name(line)
116
+ line[FIELD_NAME, :name]&.downcase
102
117
  end
103
118
 
104
119
  def recipient_from(block)
@@ -1,3 +1,3 @@
1
1
  module MailBounce
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
data/lib/mailbounce.rb CHANGED
@@ -11,7 +11,7 @@ module MailBounce
11
11
  Rejection.new(response: response, sending_ip: sending_ip)
12
12
  end
13
13
 
14
- def self.parse(raw)
15
- Report.parse(raw)
14
+ def self.parse(raw, max_size: Report::MAX_SIZE)
15
+ Report.parse(raw, max_size: max_size)
16
16
  end
17
17
  end
data/mailbounce.gemspec CHANGED
@@ -7,10 +7,9 @@ Gem::Specification.new do |spec|
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.required_ruby_version = ">= 3.3.4"
9
9
  spec.authors = [ "Simon Lev" ]
10
- spec.email = [ "support@postrider.dev" ]
11
10
 
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?"
11
+ spec.summary = "Email delivery failures for Ruby."
12
+ spec.description = "Answers one question about a rejected message: whose fault was it? Parses bounces, NDRs, and DSN reports to classify delivery failures."
14
13
 
15
14
  spec.homepage = "https://github.com/mailpiece/mailbounce"
16
15
  spec.license = "MIT"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailbounce
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Lev
@@ -23,9 +23,8 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.7'
26
- description: 'Answers one question about a rejected message: whose fault was it?'
27
- email:
28
- - support@postrider.dev
26
+ description: 'Answers one question about a rejected message: whose fault was it? Parses
27
+ bounces, NDRs, and DSN reports to classify delivery failures.'
29
28
  executables: []
30
29
  extensions: []
31
30
  extra_rdoc_files: []
@@ -64,5 +63,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
63
  requirements: []
65
64
  rubygems_version: 4.0.3
66
65
  specification_version: 4
67
- summary: RFC 3464 delivery status notifications, parsed and classified, in pure Ruby.
66
+ summary: Email delivery failures for Ruby.
68
67
  test_files: []