dmarcator 0.8.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f09058293dac6f0e31294f06182d69502ee06bc152aa3eb2ef06c07515f4363b
4
- data.tar.gz: 6e3aee91c8c8d098c0f066a6b301ef9bc36f42018078769f15e65d695d35ba8a
3
+ metadata.gz: a1bd93bc4b534e2b9613241e920dd4a71c41e564c92c10ab8abaf6be7294feb8
4
+ data.tar.gz: fb95401f394f53189a0db431f7d31bc4ffe8c25c87ce87f825f18a99962dadba
5
5
  SHA512:
6
- metadata.gz: dd47e82f78219d828832c95912a84581ad589efaec940bc25749a0a38b49692805f9afd8030e0f0bd01b2b611dd05a36fec474a8beb9866be340ba9a2e327f39
7
- data.tar.gz: 973b12e73708b1540e912063db142ca91e07c5e29cca86e32481222e25334448b8efe754113bfeec6fae591c6411f8a82d796429c7ac36c26e4c22c8c3b714fa
6
+ metadata.gz: c94318c4ac8a5f7089276f5d6bf91ae90ab8cf576f6367acf418e86adba5fe4bb5e28f7df4a478f8783d134b82bd9390555b3af48c713ca869fd276be4cfe08b
7
+ data.tar.gz: 6b10b30dfc64acd1cdef7427c4a475d7d9fa90b4c9d387cbc1e2293e8b693e66d3fa4bc23d80d254d92a5a4137ab65b1fe98adfc0e28e573bafc8342274e7995
data/ChangeLog.md CHANGED
@@ -1,3 +1,25 @@
1
+ ### 0.10.0 / 2026-10-29
2
+
3
+ Breaking changes, taken before 1.0 so the release does not carry them:
4
+
5
+ * Add `DMARCator::Error` as the base class for every error the library raises. `InvalidRecord`, `InvalidReport` and `ResolutionError` inherit from it, so one `rescue DMARCator::Error` catches all three.
6
+ * `InvalidRecord` no longer inherits from `Parslet::ParseFailed`, removing the parser implementation from the public exception contract. Parse failures still expose the underlying parser error as their `cause`.
7
+ * Remove the deprecated `DMARCator.query` and `DMARCator::Record.query` aliases. Use `DMARCator.dmarc` and `DMARCator::Record.lookup`.
8
+ * Rename `DMARCator::Report::Record` to `DMARCator::Report::Row`, so aggregate rows are not confused with `DMARCator::Record`, the parsed DMARC TXT record.
9
+
10
+ Migration:
11
+
12
+ | Before | After |
13
+ |---|---|
14
+ | `DMARCator.query(domain)` | `DMARCator.dmarc(domain)` |
15
+ | `DMARCator::Record.query(domain)` | `DMARCator::Record.lookup(domain)` |
16
+ | `DMARCator::Report::Record` | `DMARCator::Report::Row` |
17
+ | `rescue Parslet::ParseFailed` | `rescue DMARCator::Error` |
18
+
19
+ ### 0.9.0 / 2026-10-29
20
+
21
+ * Add `DMARCator.txt_records`, which returns the TXT resource records instead of strings, so callers that cache lookups can read each record's `ttl`. `DMARCator.txt` is now defined in terms of it and behaves the same.
22
+
1
23
  ### 0.8.0 / 2026-10-29
2
24
 
3
25
  * Add `DMARCator.txt`, which returns every TXT record at a name — unfiltered, in response order, with multi-chunk records joined. It accepts service labels such as `_dmarc` and `_domainkey` through a new relaxed `validate_txt_name`, and lets callers detect duplicate DMARC records that `dmarc` deliberately hides.
data/README.md CHANGED
@@ -104,6 +104,7 @@ The lookup methods are named after what they return:
104
104
 
105
105
  | Method | Returns | Cost |
106
106
  |---|---|---|
107
+ | `DMARCator.txt_records(name)` | `Array<Resolv::DNS::Resource::IN::TXT>` — the records themselves | one lookup |
107
108
  | `DMARCator.txt(name)` | `Array<String>` — every TXT record at `name` | one lookup |
108
109
  | `DMARCator.dmarc(domain)` | `String` — the DMARC record text, or `nil` | one lookup |
109
110
  | `DMARCator::Record.lookup(domain)` | `Record` — parsed | one lookup + parse (~0.4 ms) |
@@ -136,13 +137,24 @@ else :duplicate # more than one DMARC record breaks enforcement
136
137
  end
137
138
  ```
138
139
 
139
- All three methods accept an injected resolver, and an injected resolver keeps
140
- its own configuration (timeouts are only applied to resolvers the library
141
- builds):
140
+ Use `DMARCator.txt_records` when you need the DNS TTL, for example to cache
141
+ lookups in a database:
142
+
143
+ ```ruby
144
+ records = DMARCator.txt_records("_dmarc.#{domain}")
145
+ records.each do |record|
146
+ record.strings.join # the TXT value
147
+ record.ttl # seconds the answer may be cached
148
+ end
149
+ ```
150
+
151
+ All of these accept an injected resolver, and an injected resolver keeps its
152
+ own configuration (timeouts are only applied to resolvers the library builds):
142
153
 
143
154
  ```ruby
144
155
  resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
145
156
  DMARCator.txt("_dmarc.example.com", resolver: resolver)
157
+ DMARCator.txt_records("_dmarc.example.com", resolver: resolver)
146
158
  DMARCator.dmarc("example.com", resolver: resolver)
147
159
  DMARCator::Record.lookup("example.com", resolver: resolver)
148
160
  ```
@@ -165,6 +177,22 @@ more than the lookup on a fast resolver); `Resolv::DNS` does not lock per query,
165
177
  so use one resolver per thread when parallelising; and DoT/DoH open a new TLS
166
178
  connection per lookup, so plaintext is the better choice for bulk work.
167
179
 
180
+ ### SPF and DKIM
181
+
182
+ The TXT primitives also cover the other two records a checker needs. There is
183
+ no record-type-specific API, because the semantics belong to the caller:
184
+
185
+ ```ruby
186
+ DMARCator.txt("example.com") # SPF (RFC 7208)
187
+ DMARCator.txt("s1._domainkey.example.com") # DKIM (RFC 6376)
188
+ ```
189
+
190
+ Both need the raw list rather than a single value: more than one SPF record is
191
+ a `permerror`, and DKIM keys can be duplicated, rotated or revoked (`p=`). A
192
+ helper returning "the" record would hide exactly those conditions. DKIM is also
193
+ per-selector and selectors cannot be enumerated, so any convenience method would
194
+ have to take a selector and would only build the name above.
195
+
168
196
  ## Strict parsing
169
197
 
170
198
  By default, unknown tags and tags with invalid values are ignored. Pass
@@ -1,30 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'parslet'
4
-
5
3
  module DMARCator
4
+ #
5
+ # Base class for every error raised by this library.
6
+ #
7
+ # Rescuing this catches parse failures, report failures and DNS lookup
8
+ # failures alike.
9
+ #
10
+ # @since 0.10.0
11
+ #
12
+ class Error < StandardError
13
+ end
14
+
6
15
  #
7
16
  # Raised when a DMARC record cannot be parsed.
8
17
  #
9
18
  # @since 0.2.0
10
19
  #
11
- class InvalidRecord < Parslet::ParseFailed
20
+ class InvalidRecord < Error
12
21
  end
13
22
 
14
23
  #
15
24
  # Raised when a DMARC XML report cannot be parsed.
16
25
  #
17
- class InvalidReport < StandardError
26
+ # @since 0.3.0
27
+ #
28
+ class InvalidReport < Error
18
29
  end
19
30
 
20
31
  #
21
- # Raised when an encrypted DNS transport fails to return a usable answer.
32
+ # Raised when a DNS transport fails to return a usable answer.
22
33
  #
23
34
  # This is deliberately distinct from a missing record: a failed lookup must
24
35
  # not be mistaken for a domain without a DMARC policy.
25
36
  #
26
37
  # @since 0.6.0
27
38
  #
28
- class ResolutionError < StandardError
39
+ class ResolutionError < Error
29
40
  end
30
41
  end
@@ -171,6 +171,39 @@ module DMARCator
171
171
  # @since 0.8.0
172
172
  #
173
173
  def self.txt(name, resolver: default_resolver)
174
+ txt_records(name, resolver: resolver).map { |record| record.strings.join }
175
+ end
176
+
177
+ #
178
+ # Looks up every TXT record at a DNS name, returning the resource records
179
+ # themselves.
180
+ #
181
+ # Like {txt}, but without flattening to strings. The records expose
182
+ # `strings` and `ttl`, which matters when caching results: use
183
+ # `record.ttl` to decide how long an answer may be reused.
184
+ #
185
+ # @param [String] name
186
+ # The DNS name to look up.
187
+ #
188
+ # @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
189
+ # The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
190
+ # An injected resolver keeps its own configuration.
191
+ #
192
+ # @return [Array<Resolv::DNS::Resource::IN::TXT>]
193
+ # The records, or an empty array when the name has none. NXDOMAIN and
194
+ # NODATA are indistinguishable here.
195
+ #
196
+ # @raise [ResolutionError]
197
+ # The lookup failed. A failure is never reported as an empty result.
198
+ #
199
+ # @raise [ArgumentError]
200
+ # The name is not a syntactically valid DNS name.
201
+ #
202
+ # @note Names must be ASCII: pass punycode (A-labels) rather than Unicode.
203
+ #
204
+ # @since 0.9.0
205
+ #
206
+ def self.txt_records(name, resolver: default_resolver)
174
207
  lookup_txt(validate_txt_name(name), resolver: resolver)
175
208
  end
176
209
 
@@ -204,33 +237,25 @@ module DMARCator
204
237
 
205
238
  # Per RFC 7489 §6.6.3, use the TXT record beginning with "v=DMARC1".
206
239
  # If multiple DMARC records exist, treat the domain as having none.
207
- dmarc_records = lookup_txt(name, resolver: resolver).select do |text|
240
+ dmarc_records = lookup_txt(name, resolver: resolver).map do |record|
241
+ record.strings.join
242
+ end.select do |text|
208
243
  text.start_with?('v=DMARC1')
209
244
  end
210
245
 
211
246
  dmarc_records.first if dmarc_records.one?
212
247
  end
213
248
 
214
- #
215
- # @deprecated Use {dmarc}, which names what it returns. Kept for
216
- # compatibility; it will be removed in a future release.
217
- #
218
- # @see dmarc
219
- #
220
- def self.query(domain,resolver=default_resolver)
221
- dmarc(domain, resolver: resolver)
222
- end
223
-
224
249
  #
225
250
  # Looks up the TXT records at an already-validated name.
226
251
  #
227
- # Shared by {txt} and {dmarc} so both use one transport path.
252
+ # Shared by {txt}, {txt_records} and {dmarc} so they use one transport path.
228
253
  #
229
254
  # @param [String] name
230
255
  #
231
256
  # @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
232
257
  #
233
- # @return [Array<String>]
258
+ # @return [Array<Resolv::DNS::Resource::IN::TXT>]
234
259
  #
235
260
  # @raise [ResolutionError]
236
261
  #
@@ -246,7 +271,7 @@ module DMARCator
246
271
  raise ResolutionError, "DNS TXT lookup failed for #{name}: #{error.message}"
247
272
  end
248
273
 
249
- records.map { |record| record.strings.join }
274
+ records
250
275
  end
251
276
  private_class_method :lookup_txt
252
277
  end
@@ -285,7 +285,8 @@ module DMARCator
285
285
 
286
286
  new(attributes)
287
287
  rescue Parslet::ParseFailed => error
288
- raise InvalidRecord.new(truncate_error_message(error.message), error.cause)
288
+ # Raising inside the rescue sets this error as the cause automatically.
289
+ raise InvalidRecord, truncate_error_message(error.message)
289
290
  end
290
291
 
291
292
  # Maximum length of error messages echoed from parsing untrusted input.
@@ -342,16 +343,6 @@ module DMARCator
342
343
  end
343
344
  end
344
345
 
345
- #
346
- # @deprecated Use {lookup}, which names what it returns. Kept for
347
- # compatibility; it will be removed in a future release.
348
- #
349
- # @see lookup
350
- #
351
- def self.query(domain,resolver=DMARCator.default_resolver)
352
- lookup(domain, resolver: resolver)
353
- end
354
-
355
346
  #
356
347
  # Converts the record to a Hash.
357
348
  #
@@ -9,7 +9,10 @@ module DMARCator
9
9
  #
10
10
  # Represents a single `record` element of a DMARC XML report.
11
11
  #
12
- class Record
12
+ # Named `Row` so it is not confused with {DMARCator::Record}, the parsed
13
+ # DMARC TXT record.
14
+ #
15
+ class Row
13
16
  include XmlHelper
14
17
 
15
18
  # Attribute names exposed by this class.
@@ -8,7 +8,7 @@ require_relative 'xml_helper'
8
8
  require_relative 'exceptions'
9
9
  require_relative 'report/metadata'
10
10
  require_relative 'report/policy'
11
- require_relative 'report/record'
11
+ require_relative 'report/row'
12
12
 
13
13
  module DMARCator
14
14
  #
@@ -114,11 +114,11 @@ module DMARCator
114
114
  #
115
115
  # The report records.
116
116
  #
117
- # @return [Array<Report::Record>]
117
+ # @return [Array<Report::Row>]
118
118
  #
119
119
  def records
120
120
  @records ||= @node.xpath('feedback/record').map do |node|
121
- Record.new(node)
121
+ Row.new(node)
122
122
  end
123
123
  end
124
124
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DMARCator
4
4
  # dmarcator version
5
- VERSION = '0.8.0'
5
+ VERSION = '0.10.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dmarcator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -78,7 +78,7 @@ files:
78
78
  - lib/dmarcator/report/metadata.rb
79
79
  - lib/dmarcator/report/policy.rb
80
80
  - lib/dmarcator/report/reason.rb
81
- - lib/dmarcator/report/record.rb
81
+ - lib/dmarcator/report/row.rb
82
82
  - lib/dmarcator/resolver.rb
83
83
  - lib/dmarcator/resolver/doh.rb
84
84
  - lib/dmarcator/resolver/dot.rb