dmarcator 0.6.1 → 0.7.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: dfd0c6f8b9f84d311a49c5ad8a6b0e43618bd151ebd9cb7532a76486f4ea600a
4
+ data.tar.gz: 1b0eb0947c60844e369594d4c28e68510a91246faceb4a099f2c08dd7391a3fa
5
5
  SHA512:
6
- metadata.gz: 71c83a39fc17926997e43465ef07210a7a062a67b922462623e4d6c0daa617acefc4ec2546194ffa2f6755bf62321504134140b5e35813ac9e34b8ed43abbf89
7
- data.tar.gz: ebdc8623cdba9d7632188a52fbdc203d7c1f85efed580f72ce5ceebeafd8a7c5eae4f5f0797d8960b1a6fb5e1b2af9aeb9d5484ffc74a5f8adc4942af4de286a
6
+ metadata.gz: a151f9aabc45f12dc8107a0d4fb8c61bcb4f66880698045ba6df8dc97664f4840db0dc98af62fce3f2c77243f9250ee0df6d886908816a71b853d55f94d4ca8a
7
+ data.tar.gz: 7d7d50f10d95e4d762e8a8f3f1deb9fab301f690b942e65f77129f0ee633afa108281a86947ac6fe81a59e8062fe62f51e9b492ab0f2e08940b6483ad27f8248
data/ChangeLog.md CHANGED
@@ -1,3 +1,13 @@
1
+ ### 0.7.0 / 2026-10-29
2
+
3
+ * 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.
4
+ * DoH responses must use the `application/dns-message` content type (RFC 8484) and are capped at 65,535 bytes.
5
+ * `DMARCator::Uri` no longer forwards calls to private methods of the wrapped `URI::MailTo`.
6
+ * Remove the unused `XmlHelper#get_int`/`#get_timestamp` helpers and the deprecated `Record.from_txt`.
7
+ * Memoize a missing report version instead of recomputing it, and scope spec constants under `DMARCator`.
8
+ * Add `changelog_uri` and `documentation_uri` gem metadata, and document that domains must be passed as A-labels.
9
+ * 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.
10
+
1
11
  ### 0.6.1 / 2026-10-29
2
12
 
3
13
  * 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
@@ -93,6 +93,11 @@ record = DMARCator::Record.query("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
+
96
101
  ## Strict parsing
97
102
 
98
103
  By default, unknown tags and tags with invalid values are ignored. Pass
@@ -149,9 +154,11 @@ hostname.
149
154
 
150
155
  Encrypted transports fail closed: a transport failure raises
151
156
  `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.
157
+ DMARC record. This applies to every transport, including plaintext —
158
+ `Resolv` returns an empty result for NXDOMAIN and raises for timeouts and
159
+ other failures, so a failed lookup is never mistaken for an absent record.
160
+ Plaintext remains the default because forcing a public resolver breaks private
161
+ and split-horizon zones, and some networks block port 853 or public resolvers.
155
162
 
156
163
  A resolver can also be built explicitly:
157
164
 
@@ -181,7 +188,8 @@ DMARC records and XML reports are treated as untrusted input:
181
188
  TLS certificates and fail closed: a transport failure raises
182
189
  `DMARCator::ResolutionError` rather than being reported as a domain without a
183
190
  DMARC record, so a blocked or downgraded lookup can never silently enforce no
184
- policy.
191
+ policy. Plaintext lookups fail closed the same way. DoH responses must be
192
+ `application/dns-message` and are capped at 65,535 bytes.
185
193
 
186
194
  ## Requirements
187
195
 
@@ -104,11 +104,11 @@ module DMARCator
104
104
  # plaintext resolvers receive {DNS_TIMEOUTS}.
105
105
  #
106
106
  # @return [String, nil]
107
- # The domain's DMARC record or `nil` if none exists.
107
+ # The domain's DMARC record, or `nil` if the domain simply has none.
108
108
  #
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.
109
+ # @raise [ResolutionError]
110
+ # The lookup failed. A failure is never reported as a missing record, so a
111
+ # blocked or degraded resolver cannot silently enforce no policy.
112
112
  #
113
113
  # @api semipublic
114
114
  #
@@ -121,10 +121,13 @@ module DMARCator
121
121
 
122
122
  host = "_dmarc.#{domain}"
123
123
 
124
+ # Resolv returns an empty result for NXDOMAIN and raises for timeouts and
125
+ # other failures, so any exception here is a transport failure, not an
126
+ # absent record.
124
127
  begin
125
128
  records = resolver.getresources(host, Resolv::DNS::Resource::IN::TXT)
126
- rescue Resolv::ResolvError
127
- return nil
129
+ rescue Resolv::ResolvError => error
130
+ raise ResolutionError, "DNS lookup failed for #{host}: #{error.message}"
128
131
  end
129
132
 
130
133
  # Per RFC 7489 §6.6.3, use the TXT record beginning with "v=DMARC1".
@@ -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
 
@@ -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
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.7.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.7.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: