mailertogo-spf 0.2.0 → 0.2.1

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: 581850ec95dfe6d56b2fa63dce987667bde0cb512818e8d5f4cd36d7492b68ab
4
- data.tar.gz: 4efc0919f45c40599e045fd724b32550611cee5ae142e92f029174808fd93225
3
+ metadata.gz: 316234e7966e3e895703bbabc960f8cda0b932d7348e654d7afaf1d97c3ee094
4
+ data.tar.gz: 3a20869ecc92f35b8780672af93e7d38223cbc7e6ae6ca7913b678fae541c37c
5
5
  SHA512:
6
- metadata.gz: 061357e0bb7fee158f333d4f40acd28f2dbb6306de1347450c7704e022cd1a29925c3f592d655632f5fab580b181a5ede52dc693f7d8d0bf6395a03c55d6735b
7
- data.tar.gz: d7244ab5481f550f1213317fc9b4ac8b1d3d8f887bd359c4cdf6102a208fc6a99858c89e0f2aa1810dc6a0db166f50be9df9e9e6508f2f4922ec1c61048094be
6
+ metadata.gz: d04d30b6ec8059c57d6231ffb786ffb4ce0ad8672b6faad7a29b8f215adbee0c97a3cbeb39078a24ac3988d25d273a6b05b77b02fff51a2be146110ee28927a1
7
+ data.tar.gz: 58c318462a651629b0cf7d2f11951dcb419486549a947e82fe5f6195c67f6fc694b636e66686bb87aa4b6aff2c13605f94749f3a6c754795a94f5bd15cdeadfb
data/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.1] - 2026-08-17
8
+
9
+ Two `ChainAudit` results could be read as facts when they were really "we did
10
+ not look". Both are corrected in the direction of admitting it.
11
+
12
+ ### Fixed
13
+
14
+ - `ChainAudit#total` (and `#headroom`) are **`nil` rather than `0`** for a name
15
+ that publishes no record, or when DNS did not answer. `0` is a legitimate
16
+ total — `v=spf1 -all` costs exactly that — so a caller that skipped
17
+ `#published?` could report "this record costs 0 lookups" about a domain with
18
+ no record. A record that legitimately costs nothing still reports `0`.
19
+ - Duplicate records **at the hostname itself** are reported by the new
20
+ `#apex_duplicated?` instead of being pushed onto `#duplicated_in_chain`. RFC
21
+ 7208 §4.5 makes them a permerror either way, but the apex is not *in* the
22
+ chain — it is the record being priced. Folding them together made the same
23
+ domain audit differently depending on whether the caller supplied `record:` or
24
+ let the gem resolve the apex, and a caller raising its own duplicate-record
25
+ defect from the records it resolved would have raised it twice.
26
+
27
+ ### Added
28
+
29
+ - `ChainAudit#apex_duplicated?` — `true`/`false` when the gem resolved the apex,
30
+ and `nil` ("cannot say") when the record was supplied or DNS did not answer.
31
+
32
+ ### Changed
33
+
34
+ - `#duplicated_in_chain` no longer contains the audited hostname. Callers that
35
+ want that fact ask `#apex_duplicated?`.
36
+
7
37
  ## [0.2.0] - 2026-08-17
8
38
 
9
39
  Answers a second question about a record: not only "does it authorize me, and
data/README.md CHANGED
@@ -227,6 +227,22 @@ permerrors the whole evaluation rather than quietly doing nothing:
227
227
  ```ruby
228
228
  audit.targets_without_spf # => ["nothing.example.net"] — include: of a name with no SPF (§5.2)
229
229
  audit.duplicated_in_chain # => ["two.example.net"] — two v=spf1 records in the chain (§4.5)
230
+ audit.apex_duplicated? # => false — two at the hostname itself (§4.5)
231
+ ```
232
+
233
+ `apex_duplicated?` is deliberately separate from `duplicated_in_chain`: the
234
+ hostname is not *in* the chain, it is the record being priced. It answers `nil`
235
+ — "cannot say" — when you supplied the `record:` yourself, because then we never
236
+ looked at the apex. A caller that resolved the apex already knows, and should
237
+ report from what it saw rather than ask twice.
238
+
239
+ A record that does not exist cannot be priced, so `total` and `headroom` are
240
+ `nil` rather than `0`. Check `published?` first:
241
+
242
+ ```ruby
243
+ audit = MailerToGo::SPF.chain_audit("no-spf.example.com")
244
+ audit.published? # => false
245
+ audit.total # => nil — not 0; `v=spf1 -all` legitimately costs 0
230
246
  ```
231
247
 
232
248
  Two honesty flags, because a count you could not finish must never read as "it
@@ -70,10 +70,12 @@ module MailerToGo
70
70
  @resolver = resolver
71
71
  @term_class = term_class || Term
72
72
  @spent = 0
73
- @total = 0
73
+ @total = nil
74
74
  @terms = []
75
75
  @targets_without_spf = []
76
76
  @duplicated_in_chain = []
77
+ # nil until we resolve the apex ourselves — "cannot say", not "no".
78
+ @apex_duplicated = nil
77
79
  @resolved = true
78
80
  @partial = false
79
81
  @capped = false
@@ -100,7 +102,11 @@ module MailerToGo
100
102
  seen_all ||= term.all?
101
103
  end
102
104
 
103
- @total = @spent
105
+ # nil, not 0, when there is nothing to price. 0 is a legitimate total
106
+ # `v=spf1 -all` costs exactly that — so using it for "no record" lets a
107
+ # caller that skipped #published? report "this record costs 0 lookups"
108
+ # about a domain with no record at all.
109
+ @total = published? ? @spent : nil
104
110
  self
105
111
  end
106
112
 
@@ -128,10 +134,20 @@ module MailerToGo
128
134
  # the number is a floor — for a different reason.
129
135
  def capped? = @capped
130
136
 
137
+ # Was there more than one v=spf1 record at the hostname itself? §4.5 makes
138
+ # that a permerror just as surely as duplicates inside the chain.
139
+ #
140
+ # nil means "cannot say" rather than "no": the record was handed to us, so
141
+ # we never looked at the apex (or DNS did not answer). Only a caller that
142
+ # let us resolve the apex gets a true/false here — the one that resolved it
143
+ # already knows, and should report from what it saw rather than ask us.
144
+ def apex_duplicated? = @apex_duplicated
145
+
131
146
  # How many lookups are still available before the cap, or nil when we
132
- # cannot say (a floor cannot answer "how much room is left").
147
+ # cannot say (a floor cannot answer "how much room is left", and neither
148
+ # can a record that does not exist).
133
149
  def headroom
134
- return nil if partial? || capped?
150
+ return nil if partial? || capped? || !published?
135
151
 
136
152
  [LIMIT - @total.to_i, 0].max
137
153
  end
@@ -150,9 +166,14 @@ module MailerToGo
150
166
 
151
167
  records = Array(txts).map { |t| Record.normalize_txt(t) }.select { |t| Record.spf_record?(t) }
152
168
  # §4.5 — two records at the apex permerror the evaluation just as surely
153
- # as two anywhere else in the chain, so they are noted the same way. We
154
- # price the first, because there has to be something to read.
155
- @duplicated_in_chain << @hostname if records.size > 1
169
+ # as two anywhere else in the chain. It is reported SEPARATELY from
170
+ # `duplicated_in_chain` even so, because the apex is not *in* the chain:
171
+ # it is the record being priced. Folding it in made the same domain audit
172
+ # differently depending on whether the caller resolved the apex or we
173
+ # did, and a caller that raises its own duplicate-record defect from the
174
+ # records it resolved would then raise it twice.
175
+ @apex_duplicated = records.size > 1
176
+ # We price the first, because there has to be something to read.
156
177
  @record = records.first.to_s
157
178
  end
158
179
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MailerToGo
4
4
  module SPF
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailertogo-spf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - MailerToGo