dmarcator 0.6.1 → 0.8.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: ab57f0c6599df86ed3331199b84f0abdd9aae8876405e4205f2070b9e457b5ca
4
- data.tar.gz: 5466098000ff91848f6cb8208606ed8b3312eb9b476763074d917b3f7f377aca
3
+ metadata.gz: f09058293dac6f0e31294f06182d69502ee06bc152aa3eb2ef06c07515f4363b
4
+ data.tar.gz: 6e3aee91c8c8d098c0f066a6b301ef9bc36f42018078769f15e65d695d35ba8a
5
5
  SHA512:
6
- metadata.gz: 71c83a39fc17926997e43465ef07210a7a062a67b922462623e4d6c0daa617acefc4ec2546194ffa2f6755bf62321504134140b5e35813ac9e34b8ed43abbf89
7
- data.tar.gz: ebdc8623cdba9d7632188a52fbdc203d7c1f85efed580f72ce5ceebeafd8a7c5eae4f5f0797d8960b1a6fb5e1b2af9aeb9d5484ffc74a5f8adc4942af4de286a
6
+ metadata.gz: dd47e82f78219d828832c95912a84581ad589efaec940bc25749a0a38b49692805f9afd8030e0f0bd01b2b611dd05a36fec474a8beb9866be340ba9a2e327f39
7
+ data.tar.gz: 973b12e73708b1540e912063db142ca91e07c5e29cca86e32481222e25334448b8efe754113bfeec6fae591c6411f8a82d796429c7ac36c26e4c22c8c3b714fa
data/ChangeLog.md CHANGED
@@ -1,3 +1,20 @@
1
+ ### 0.8.0 / 2026-10-29
2
+
3
+ * 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.
4
+ * Rename the lookup methods after what they return: `DMARCator.dmarc` (was `DMARCator.query`) and `DMARCator::Record.lookup` (was `DMARCator::Record.query`). The old names remain as deprecated aliases and emit no runtime warnings.
5
+ * Apply `DMARCATOR_DNS_TIMEOUTS` when the library builds a plaintext resolver, instead of on every lookup. An injected resolver now keeps its own configuration.
6
+ * Document the lookup APIs, bulk-scan guidance and deprecations in the README.
7
+
8
+ ### 0.7.0 / 2026-10-29
9
+
10
+ * Plaintext DNS lookups now fail closed: a lookup failure raises `DMARCator::ResolutionError` instead of being reported as a domain without a DMARC record. Previously a timeout or resolver failure looked like no policy, which failed open. This is a behavior change and applies to every transport.
11
+ * DoH responses must use the `application/dns-message` content type (RFC 8484) and are capped at 65,535 bytes.
12
+ * `DMARCator::Uri` no longer forwards calls to private methods of the wrapped `URI::MailTo`.
13
+ * Remove the unused `XmlHelper#get_int`/`#get_timestamp` helpers and the deprecated `Record.from_txt`.
14
+ * Memoize a missing report version instead of recomputing it, and scope spec constants under `DMARCator`.
15
+ * Add `changelog_uri` and `documentation_uri` gem metadata, and document that domains must be passed as A-labels.
16
+ * Add a CI workflow that runs the specs on the minimum supported Ruby and the current release, loads the gem with warnings enabled, fails on YARD warnings, and builds the gem.
17
+
1
18
  ### 0.6.1 / 2026-10-29
2
19
 
3
20
  * Remove a redundant `attr_reader :sp` that `Record#sp` already overrode, fixing a `method redefined; discarding old sp` warning emitted under `-w` (as CI enables).
data/README.md CHANGED
@@ -89,10 +89,82 @@ record.sp
89
89
  Query the DMARC record for a domain:
90
90
 
91
91
  ```ruby
92
- record = DMARCator::Record.query("twitter.com")
92
+ record = DMARCator::Record.lookup("twitter.com")
93
93
  # => #<DMARCator::Record ... @p=:reject, @rua=[...], ...>
94
94
  ```
95
95
 
96
+ Domains must be ASCII (A-labels): pass punycode such as
97
+ `xn--mnchen-3ya.de` rather than `münchen.de`. A lookup failure raises
98
+ `DMARCator::ResolutionError`; `nil` only means the domain genuinely has no
99
+ DMARC record.
100
+
101
+ ## Lookup APIs
102
+
103
+ The lookup methods are named after what they return:
104
+
105
+ | Method | Returns | Cost |
106
+ |---|---|---|
107
+ | `DMARCator.txt(name)` | `Array<String>` — every TXT record at `name` | one lookup |
108
+ | `DMARCator.dmarc(domain)` | `String` — the DMARC record text, or `nil` | one lookup |
109
+ | `DMARCator::Record.lookup(domain)` | `Record` — parsed | one lookup + parse (~0.4 ms) |
110
+ | `DMARCator::Record.parse(text)` | `Record` — parsed from a string | parse only |
111
+
112
+ `DMARCator.txt` is the primitive: unfiltered, in response order, multi-chunk
113
+ records joined. It works for any TXT name, including `_dmarc.` and
114
+ `_domainkey.` labels, and leaves interpretation to the caller:
115
+
116
+ ```ruby
117
+ DMARCator.txt("_dmarc.example.com")
118
+ # => ["v=DMARC1; p=reject; rua=mailto:dmarc@example.com"]
119
+
120
+ DMARCator.txt("example.com")
121
+ # => ["v=spf1 include:_spf.example.com -all", "google-site-verification=abc"]
122
+
123
+ DMARCator.txt("s1._domainkey.example.com")
124
+ # => []
125
+ ```
126
+
127
+ Because it returns every record, it is also how you detect misconfiguration
128
+ that `DMARCator.dmarc` deliberately hides per RFC 7489 §6.6.3:
129
+
130
+ ```ruby
131
+ records = DMARCator.txt("_dmarc.#{domain}").select { |t| t.start_with?("v=DMARC1") }
132
+ case records.length
133
+ when 0 then :none
134
+ when 1 then :ok
135
+ else :duplicate # more than one DMARC record breaks enforcement
136
+ end
137
+ ```
138
+
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):
142
+
143
+ ```ruby
144
+ resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
145
+ DMARCator.txt("_dmarc.example.com", resolver: resolver)
146
+ DMARCator.dmarc("example.com", resolver: resolver)
147
+ DMARCator::Record.lookup("example.com", resolver: resolver)
148
+ ```
149
+
150
+ ### Bulk lookups
151
+
152
+ `DMARCator.dmarc` is the cheapest DMARC path — one lookup, no parsing. For a
153
+ scan over many domains:
154
+
155
+ ```ruby
156
+ resolver = DMARCator.default_resolver # build once, not per domain
157
+ domains.each do |domain|
158
+ record = DMARCator.dmarc(domain, resolver: resolver)
159
+ # parse only when you need the fields
160
+ end
161
+ ```
162
+
163
+ Notes for large scans: `Record.lookup` adds a parse per domain (~0.4 ms, far
164
+ more than the lookup on a fast resolver); `Resolv::DNS` does not lock per query,
165
+ so use one resolver per thread when parallelising; and DoT/DoH open a new TLS
166
+ connection per lookup, so plaintext is the better choice for bulk work.
167
+
96
168
  ## Strict parsing
97
169
 
98
170
  By default, unknown tags and tags with invalid values are ignored. Pass
@@ -149,15 +221,17 @@ hostname.
149
221
 
150
222
  Encrypted transports fail closed: a transport failure raises
151
223
  `DMARCator::ResolutionError` instead of being reported as a domain without a
152
- DMARC record. Plaintext is the default because forcing a public resolver breaks
153
- private and split-horizon zones, and some networks block port 853 or public
154
- resolvers.
224
+ DMARC record. This applies to every transport, including plaintext —
225
+ `Resolv` returns an empty result for NXDOMAIN and raises for timeouts and
226
+ other failures, so a failed lookup is never mistaken for an absent record.
227
+ Plaintext remains the default because forcing a public resolver breaks private
228
+ and split-horizon zones, and some networks block port 853 or public resolvers.
155
229
 
156
230
  A resolver can also be built explicitly:
157
231
 
158
232
  ```ruby
159
233
  resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
160
- record = DMARCator::Record.query("example.com", resolver)
234
+ record = DMARCator::Record.lookup("example.com", resolver: resolver)
161
235
  ```
162
236
 
163
237
  ## Security
@@ -181,7 +255,8 @@ DMARC records and XML reports are treated as untrusted input:
181
255
  TLS certificates and fail closed: a transport failure raises
182
256
  `DMARCator::ResolutionError` rather than being reported as a domain without a
183
257
  DMARC record, so a blocked or downgraded lookup can never silently enforce no
184
- policy.
258
+ policy. Plaintext lookups fail closed the same way. DoH responses must be
259
+ `application/dns-message` and are capped at 65,535 bytes.
185
260
 
186
261
  ## Requirements
187
262
 
@@ -208,8 +283,12 @@ was built, so a gem can always be traced back to its source:
208
283
  # 2. Commit the release.
209
284
  git commit -am "Bump version to X.Y.Z"
210
285
 
211
- # 3. Tag that commit before building anything.
212
- git tag -a vX.Y.Z -m "dmarcator X.Y.Z"
286
+ # 3. Tag that commit before building anything, with release notes in the
287
+ # tag message. Repeating -m adds blank-line-separated paragraphs.
288
+ git tag -a vX.Y.Z \
289
+ -m "dmarcator X.Y.Z" \
290
+ -m "Summary of the release, including behaviour changes and upgrade notes." \
291
+ -m "See ChangeLog.md for the itemized list."
213
292
 
214
293
  # 4. Push the commit and its tags.
215
294
  git push origin main --follow-tags
@@ -219,6 +298,10 @@ gem build dmarc.gemspec
219
298
  gem push dmarcator-X.Y.Z.gem
220
299
  ```
221
300
 
301
+ The tag message is the release note: it is what `git show vX.Y.Z` prints, so
302
+ put the highlight summary and any upgrade guidance there. Write it before step
303
+ 4, because a pushed tag message cannot be changed without force-pushing the tag.
304
+
222
305
  Tag before publishing, never after: a tag without a published gem, or a gem
223
306
  without a tag, makes the two hard to reconcile later.
224
307
 
@@ -17,8 +17,18 @@ module DMARCator
17
17
  #
18
18
  # DNS query timeouts in seconds, tried in order.
19
19
  #
20
+ # Applied when the library builds a plaintext resolver. Timeouts on an
21
+ # injected resolver are left alone.
22
+ #
20
23
  DNS_TIMEOUTS = [2, 4, 8].freeze
21
24
 
25
+ #
26
+ # The maximum length of a textual DNS name, in octets.
27
+ #
28
+ # @since 0.8.0
29
+ #
30
+ MAX_TXT_NAME_LENGTH = 253
31
+
22
32
  #
23
33
  # Determines the version of the loaded `resolv` gem, if available.
24
34
  #
@@ -57,6 +67,10 @@ module DMARCator
57
67
  #
58
68
  # Validates a domain before using it in a DNS query.
59
69
  #
70
+ # Rejects underscores and reserves room for the `_dmarc.` prefix, so this is
71
+ # the validator for registrable domains rather than arbitrary DNS names.
72
+ # Use {validate_txt_name} for service labels.
73
+ #
60
74
  # @param [String] domain
61
75
  # The domain to validate.
62
76
  #
@@ -79,6 +93,37 @@ module DMARCator
79
93
  domain
80
94
  end
81
95
 
96
+ #
97
+ # Validates an arbitrary DNS name used for a TXT lookup.
98
+ #
99
+ # Unlike {validate_domain}, service labels such as `_dmarc` and `_domainkey`
100
+ # are allowed, so names may contain underscores. Length and label limits are
101
+ # still enforced so hostile input cannot reach the resolver unbounded.
102
+ #
103
+ # @param [String] name
104
+ # The DNS name to validate.
105
+ #
106
+ # @return [String]
107
+ # The validated name.
108
+ #
109
+ # @raise [ArgumentError]
110
+ # The name is not a syntactically valid DNS name.
111
+ #
112
+ # @since 0.8.0
113
+ #
114
+ def self.validate_txt_name(name)
115
+ unless name.is_a?(String) && !name.empty? &&
116
+ name.length <= MAX_TXT_NAME_LENGTH &&
117
+ !name.start_with?('.') &&
118
+ !name.include?('..') &&
119
+ name.match?(/\A[A-Za-z0-9._-]+\z/) &&
120
+ name.split('.').all? { |label| label.length <= 63 && !label.start_with?('-') && !label.end_with?('-') }
121
+ raise ArgumentError, "invalid TXT name: #{name.inspect}"
122
+ end
123
+
124
+ name
125
+ end
126
+
82
127
  #
83
128
  # Builds the resolver configured by the environment.
84
129
  #
@@ -94,47 +139,116 @@ module DMARCator
94
139
  end
95
140
 
96
141
  #
97
- # Queries a domain for the DMARC record.
142
+ # Looks up every TXT record published at a DNS name.
143
+ #
144
+ # Records are returned exactly as published: unfiltered, in response order,
145
+ # and with multi-chunk records joined. Interpreting them (SPF, DKIM, DMARC,
146
+ # domain verification) is left to the caller. Use this when the individual
147
+ # records matter, for example to detect that a domain publishes more than
148
+ # one DMARC record.
149
+ #
150
+ # @param [String] name
151
+ # The DNS name to look up, such as `example.com`, `_dmarc.example.com`, or
152
+ # `s1._domainkey.example.com`.
153
+ #
154
+ # @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
155
+ # The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
156
+ # An injected resolver keeps its own configuration.
157
+ #
158
+ # @return [Array<String>]
159
+ # The TXT records, or an empty array when the name has none. NXDOMAIN and
160
+ # NODATA are indistinguishable here.
161
+ #
162
+ # @raise [ResolutionError]
163
+ # The lookup failed. A failure is never reported as an empty result, so a
164
+ # blocked or degraded resolver cannot be mistaken for an absent record.
165
+ #
166
+ # @raise [ArgumentError]
167
+ # The name is not a syntactically valid DNS name.
168
+ #
169
+ # @note Names must be ASCII: pass punycode (A-labels) rather than Unicode.
170
+ #
171
+ # @since 0.8.0
172
+ #
173
+ def self.txt(name, resolver: default_resolver)
174
+ lookup_txt(validate_txt_name(name), resolver: resolver)
175
+ end
176
+
177
+ #
178
+ # Looks up the DMARC record text published for a domain.
179
+ #
180
+ # This is the lowest-cost DMARC path: one TXT lookup and no parsing. Use it
181
+ # for bulk scans, and call {Record.parse} only on records you care about.
98
182
  #
99
183
  # @param [String] domain
100
- # The domain to query DMARC for.
184
+ # The domain to look up DMARC for.
101
185
  #
102
186
  # @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
103
- # The resolver to use. When omitted, one is built from `DMARCATOR_DNS`;
104
- # plaintext resolvers receive {DNS_TIMEOUTS}.
187
+ # The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
105
188
  #
106
189
  # @return [String, nil]
107
- # The domain's DMARC record or `nil` if none exists.
190
+ # The DMARC record text, or `nil` when the domain publishes none or
191
+ # publishes more than one (per RFC 7489 §6.6.3). Use {txt} to see every
192
+ # record, including duplicates.
108
193
  #
109
- # @raise [Resolver::DoH, Resolver::DoT] resolution errors
110
- # Encrypted transports raise {ResolutionError} when the lookup fails, so
111
- # a transport problem is never reported as a missing record.
194
+ # @raise [ResolutionError]
195
+ # The lookup failed.
112
196
  #
113
- # @api semipublic
197
+ # @raise [ArgumentError]
198
+ # The domain is not a syntactically valid DNS name.
114
199
  #
115
- # @since 0.3.0
200
+ # @since 0.8.0
116
201
  #
117
- def self.query(domain,resolver=default_resolver)
118
- validate_domain(domain)
202
+ def self.dmarc(domain, resolver: default_resolver)
203
+ name = "_dmarc.#{validate_domain(domain)}"
119
204
 
120
- resolver.timeouts = DNS_TIMEOUTS if resolver.respond_to?(:timeouts=)
205
+ # Per RFC 7489 §6.6.3, use the TXT record beginning with "v=DMARC1".
206
+ # If multiple DMARC records exist, treat the domain as having none.
207
+ dmarc_records = lookup_txt(name, resolver: resolver).select do |text|
208
+ text.start_with?('v=DMARC1')
209
+ end
121
210
 
122
- host = "_dmarc.#{domain}"
211
+ dmarc_records.first if dmarc_records.one?
212
+ end
123
213
 
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
+ #
225
+ # Looks up the TXT records at an already-validated name.
226
+ #
227
+ # Shared by {txt} and {dmarc} so both use one transport path.
228
+ #
229
+ # @param [String] name
230
+ #
231
+ # @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
232
+ #
233
+ # @return [Array<String>]
234
+ #
235
+ # @raise [ResolutionError]
236
+ #
237
+ # @api private
238
+ #
239
+ def self.lookup_txt(name, resolver:)
240
+ # Resolv returns an empty result for NXDOMAIN and raises for timeouts and
241
+ # other failures, so any exception here is a transport failure, not an
242
+ # absent record.
124
243
  begin
125
- records = resolver.getresources(host, Resolv::DNS::Resource::IN::TXT)
126
- rescue Resolv::ResolvError
127
- return nil
244
+ records = resolver.getresources(name, Resolv::DNS::Resource::IN::TXT)
245
+ rescue Resolv::ResolvError => error
246
+ raise ResolutionError, "DNS TXT lookup failed for #{name}: #{error.message}"
128
247
  end
129
248
 
130
- # Per RFC 7489 §6.6.3, use the TXT record beginning with "v=DMARC1".
131
- # If multiple DMARC records exist, treat the domain as having none.
132
- dmarc_records = records.map { |record| record.strings.join }
133
- .select { |text| text.start_with?('v=DMARC1') }
134
-
135
- return dmarc_records.first if dmarc_records.one?
136
- nil
249
+ records.map { |record| record.strings.join }
137
250
  end
251
+ private_class_method :lookup_txt
138
252
  end
139
253
 
140
254
  if DMARCator.vulnerable_resolv?
@@ -288,13 +288,6 @@ module DMARCator
288
288
  raise InvalidRecord.new(truncate_error_message(error.message), error.cause)
289
289
  end
290
290
 
291
- #
292
- # @deprecated use {parse} instead.
293
- #
294
- def self.from_txt(rec)
295
- parse(rec)
296
- end
297
-
298
291
  # Maximum length of error messages echoed from parsing untrusted input.
299
292
  MAX_ERROR_MESSAGE_LENGTH = 500
300
293
 
@@ -319,31 +312,46 @@ module DMARCator
319
312
  end
320
313
 
321
314
  #
322
- # Queries and parses the DMARC record for a domain.
315
+ # Looks up and parses the DMARC record for a domain.
316
+ #
317
+ # Prefer {DMARCator.dmarc} for bulk work: this method adds a parse
318
+ # (roughly 0.4 ms) on top of the lookup.
323
319
  #
324
320
  # @param [String] domain
325
- # The domain to query DMARC for.
321
+ # The domain to look up DMARC for.
326
322
  #
327
- # @param [Resolv::DNS] resolver
328
- # The resolver to use.
323
+ # @param [Resolv::DNS, DMARCator::Resolver::DoH, DMARCator::Resolver::DoT] resolver
324
+ # The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
329
325
  #
330
326
  # @return [Record, nil]
331
- # The parsed DMARC record. If no DMARC record was found, `nil` will be
332
- # returned.
327
+ # The parsed DMARC record, or `nil` when the domain publishes none.
333
328
  #
334
329
  # @raise [InvalidRecord]
335
330
  # The DMARC record could not be parsed.
336
331
  #
337
- # @since 0.3.0
332
+ # @raise [DMARCator::ResolutionError]
333
+ # The lookup failed.
334
+ #
335
+ # @since 0.8.0
338
336
  #
339
337
  # @api public
340
338
  #
341
- def self.query(domain,resolver=DMARCator.default_resolver)
342
- if (dmarc = DMARCator.query(domain,resolver))
339
+ def self.lookup(domain, resolver: DMARCator.default_resolver)
340
+ if (dmarc = DMARCator.dmarc(domain, resolver: resolver))
343
341
  parse(dmarc)
344
342
  end
345
343
  end
346
344
 
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
+
347
355
  #
348
356
  # Converts the record to a Hash.
349
357
  #
@@ -78,7 +78,9 @@ module DMARCator
78
78
  # @return [BigDecimal, nil]
79
79
  #
80
80
  def version
81
- @version ||= begin
81
+ return @version if defined?(@version)
82
+
83
+ @version = begin
82
84
  value = get_text('feedback/version')
83
85
 
84
86
  if value && !value.empty?
@@ -25,6 +25,12 @@ module DMARCator
25
25
  # Default connect/read timeout in seconds.
26
26
  DEFAULT_TIMEOUT = 5
27
27
 
28
+ # Maximum accepted response body in bytes (the DNS message limit).
29
+ MAX_RESPONSE_BYTES = 65_535
30
+
31
+ # Media type RFC 8484 requires for DNS messages.
32
+ CONTENT_TYPE = 'application/dns-message'
33
+
28
34
  # @return [URI::HTTPS] the DoH endpoint.
29
35
  attr_reader :url
30
36
 
@@ -92,8 +98,8 @@ module DMARCator
92
98
  http.ipaddr = @ipaddr if @ipaddr
93
99
 
94
100
  request = Net::HTTP::Post.new(@url.request_uri)
95
- request['content-type'] = 'application/dns-message'
96
- request['accept'] = 'application/dns-message'
101
+ request['content-type'] = CONTENT_TYPE
102
+ request['accept'] = CONTENT_TYPE
97
103
  request.body = query.encode
98
104
 
99
105
  response = http.start { |connection| connection.request(request) }
@@ -104,7 +110,20 @@ module DMARCator
104
110
  raise ResolutionError, "DoH endpoint returned HTTP #{status}"
105
111
  end
106
112
 
107
- response.body.to_s.b
113
+ content_type = response['content-type'].to_s
114
+
115
+ unless content_type.start_with?(CONTENT_TYPE)
116
+ raise ResolutionError, "DoH endpoint returned unexpected content-type: #{content_type.inspect}"
117
+ end
118
+
119
+ body = response.body.to_s.b
120
+
121
+ if body.bytesize > MAX_RESPONSE_BYTES
122
+ raise ResolutionError,
123
+ "DoH response exceeds maximum size of #{MAX_RESPONSE_BYTES} bytes"
124
+ end
125
+
126
+ body
108
127
  rescue ResolutionError
109
128
  raise
110
129
  rescue StandardError => error
@@ -61,7 +61,9 @@ module DMARCator
61
61
 
62
62
  case name
63
63
  when 'plaintext'
64
- Resolv::DNS.new
64
+ resolver = Resolv::DNS.new
65
+ resolver.timeouts = DNS_TIMEOUTS
66
+ resolver
65
67
  when 'doh'
66
68
  DoH.from_env(env)
67
69
  when 'dot'
data/lib/dmarcator/uri.rb CHANGED
@@ -118,19 +118,24 @@ module DMARCator
118
118
  private
119
119
 
120
120
  #
121
- # Pass all missing methods to {#uri}.
121
+ # Pass public methods through to {#uri}.
122
+ #
123
+ # Private methods are not forwarded: delegation is limited to the wrapped
124
+ # URI's public interface.
122
125
  #
123
126
  def method_missing(name,*arguments,&block)
124
- @uri.send(name,*arguments,&block)
127
+ return super unless @uri.respond_to?(name)
128
+
129
+ @uri.public_send(name,*arguments,&block)
125
130
  end
126
131
 
127
132
  #
128
- # Indicates whether a missing method can be handled by {#uri}.
133
+ # Indicates whether a missing public method can be handled by {#uri}.
129
134
  #
130
135
  # @return [Boolean]
131
136
  #
132
137
  def respond_to_missing?(name, include_private = false)
133
- @uri.respond_to?(name, include_private) || super
138
+ @uri.respond_to?(name) || super
134
139
  end
135
140
 
136
141
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DMARCator
4
4
  # dmarcator version
5
- VERSION = '0.6.1'
5
+ VERSION = '0.8.0'
6
6
  end
@@ -20,30 +20,6 @@ module DMARCator
20
20
  @node.at_xpath(xpath)&.text
21
21
  end
22
22
 
23
- #
24
- # Returns a Unix timestamp as a `Time`.
25
- #
26
- # @param [String] xpath
27
- # The XPath expression to match.
28
- #
29
- # @return [Time, nil]
30
- #
31
- def get_timestamp(xpath)
32
- parse_timestamp(get_text(xpath))
33
- end
34
-
35
- #
36
- # Returns an integer value.
37
- #
38
- # @param [String] xpath
39
- # The XPath expression to match.
40
- #
41
- # @return [Integer, nil]
42
- #
43
- def get_int(xpath)
44
- parse_integer(get_text(xpath))
45
- end
46
-
47
23
  #
48
24
  # Parses a Unix timestamp from raw element text.
49
25
  #
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.6.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -92,6 +92,8 @@ licenses:
92
92
  metadata:
93
93
  source_code_uri: https://github.com/bermannoah/dmarcator
94
94
  bug_tracker_uri: https://github.com/bermannoah/dmarcator/issues
95
+ changelog_uri: https://github.com/bermannoah/dmarcator/blob/main/ChangeLog.md
96
+ documentation_uri: https://rubydoc.info/gems/dmarcator
95
97
  rubygems_mfa_required: 'true'
96
98
  rdoc_options: []
97
99
  require_paths: