mailresolver 0.5.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 12a1881f2f511ee3179b481e2b0edfd43f79bfed4fdc9f7d96ce94a4fb511650
4
+ data.tar.gz: a1f1aadf2ecfbe335cba1c5ddd33daeba467760ad7b0d1ec070905f9622d4120
5
+ SHA512:
6
+ metadata.gz: 9ee55c2b9fefb4afd0d576e7469804e9c969c653a4b285f1beb77b80efad56dbe77f12397568dba54c17bea7e37c20fa34326c988407fd3a4e3d7d8cc659a847
7
+ data.tar.gz: 3a6ef2bbe9788425d1e65c8ac21364218eac4d67a0db34bdafc2918030bcf7f5557625410b9497a7095b272e55d5043b9f60a74e15b994409ab62a64e15f47db
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.5.1
4
+
5
+ - Re-release: 0.5.0
6
+
7
+ ## 0.5.0
8
+
9
+ - Initial public release.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simon Lev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # mailresolver
2
+
3
+ ๐Ÿ” DNS resolution for mail, in Ruby.
4
+
5
+ **mailresolver** is a small, injectable DNS client for mail libraries โ€” TXT, A/AAAA, MX, PTR, and CNAME lookups.
6
+
7
+ For mail libraries and the apps that use them: SPF/DKIM/DMARC/MTA-STS checks that need a missing record told apart from a server that couldn't answer, apps running several mail libraries that want one shared resolver instead of one apiece, test suites that want to fake DNS without touching a network.
8
+
9
+ **mailresolver** handles:
10
+
11
+ - TXT, A, AAAA, MX, PTR, and CNAME lookups, plus a merged `addresses` for A+AAAA
12
+ - a three-way error taxonomy โ€” `NotFound`, `ServerFailure`, `Timeout` โ€” instead of collapsing every failure into an empty answer
13
+ - one shared resolver across every mail library in your app, multiplexed over dnsruby's single I/O thread
14
+ - configurable retry/timeout schedule
15
+ - DNSSEC validation and answer caching off, unconditionally, for callers that have no way to act on either
16
+ - test doubles โ€” a hash-backed fake and a real loopback nameserver โ€” so a suite never needs the network
17
+
18
+ > Not a general-purpose DNS client โ€” five record types and three errors, built for what mail authentication needs.
19
+
20
+ ## Contents
21
+
22
+ - [Installation](#installation)
23
+ - [Usage](#usage)
24
+ - [The Error Taxonomy](#the-error-taxonomy)
25
+ - [Why This Distinction Exists](#why-this-distinction-exists)
26
+ - [One Resolver, Shared](#one-resolver-shared)
27
+ - [Configuration](#configuration)
28
+ - [Custom Resolvers](#custom-resolvers)
29
+ - [Test Doubles](#test-doubles)
30
+ - [Testing](#testing)
31
+ - [History](#history)
32
+ - [Contributing](#contributing)
33
+ - [Acknowledgments](#acknowledgments)
34
+ - [License](#license)
35
+
36
+ ## Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ ```ruby
41
+ gem "mailresolver"
42
+ ```
43
+
44
+ Requires Ruby >= 3.4
45
+
46
+ ## Usage
47
+
48
+ ```ruby
49
+ require "mailresolver"
50
+
51
+ resolver = MailResolver::Resolver.new
52
+
53
+ resolver.txt("example.com") # => ["v=spf1 -all"]
54
+ resolver.a("example.com") # => ["203.0.113.10"]
55
+ resolver.aaaa("example.com") # => ["2001:db8::1"]
56
+ resolver.addresses("example.com") # => ["203.0.113.10", "2001:db8::1"]
57
+ resolver.mx("example.com") # => ["mx1.example.com", "mx2.example.com"], sorted by preference
58
+ resolver.ptr("203.0.113.10") # => ["mail.example.com"]
59
+ resolver.cname("www.example.com") # => ["example.com"]
60
+ ```
61
+
62
+ `addresses` merges A and AAAA. A family the name doesn't publish contributes nothing to it rather than ending the lookup โ€” but a name that doesn't exist at all raises `NoSuchName`, and a nameserver that couldn't answer still raises, same as every other method here.
63
+
64
+ IPv6 addresses come back lower case: dnsruby renders them upper case, and RFC 5952 ยง4.3 asks for lower.
65
+
66
+ ## The Error Taxonomy
67
+
68
+ Every method raises one of three errors instead of guessing:
69
+
70
+ - `MailResolver::NotFound` โ€” the server said there is nothing there: NXDOMAIN, or NOERROR with no records of the type asked for. NXDOMAIN specifically raises the subclass `MailResolver::NoSuchName`, for the callers โ€” implicit MX, for one โ€” to whom "the name doesn't exist" means something different from "the name has no records of this type"
71
+ - `MailResolver::ServerFailure` โ€” the server didn't say: SERVFAIL, REFUSED, or a transport failure (a connect that failed, a socket dnsruby couldn't open, an address that wouldn't resolve)
72
+ - `MailResolver::Timeout` โ€” no answer arrived in time
73
+
74
+ All of them descend from `MailResolver::Error`. Malformed input is the caller's bug, not a DNS answer, and raises `ArgumentError` instead โ€” `ptr("not an ip")`, for instance.
75
+
76
+ ## Why This Distinction Exists
77
+
78
+ `NotFound` and `ServerFailure` look interchangeable from a distance โ€” both mean "I don't have an answer for you" โ€” but they mean opposite things to the caller, and mail authentication is built on the difference.
79
+
80
+ SPF (RFC 7208) counts a void lookup โ€” a `NotFound` โ€” against its ten-lookup budget and keeps evaluating. It abandons evaluation entirely on a `ServerFailure` or a `Timeout`, returning `temperror` rather than pretending the record said nothing. Collapse the two, the way `Resolv` does, and a nameserver having a bad minute reads as a domain that designates no senders โ€” SPF fails a message that should have deferred, and a forged one passes for the same reason a legitimate one would have failed.
81
+
82
+ The same shape recurs in DKIM, DMARC, and MTA-STS: a missing record is a policy decision the domain made; a server that couldn't answer is not.
83
+
84
+ ## One Resolver, Shared
85
+
86
+ Build one `MailResolver::Resolver` and hand it to every mail library your app uses:
87
+
88
+ ```ruby
89
+ resolver = MailResolver::Resolver.new
90
+
91
+ MailAuth.authenticate(raw, ip: ip, resolver: resolver)
92
+ MtaSts.lookup(domain, known: nil, resolver: resolver)
93
+ ```
94
+
95
+ dnsruby multiplexes concurrent queries over a single I/O thread, and can only do that if callers share one `Dnsruby::Resolver`. Handing each library its own `MailResolver::Resolver` โ€” rather than one instance shared across them โ€” buys nothing and costs an I/O thread apiece.
96
+
97
+ ## Configuration
98
+
99
+ ```ruby
100
+ MailResolver::Resolver.new(timeouts: [5, 3, 3])
101
+ ```
102
+
103
+ `timeouts` is one attempt per element: the first element is how long the first packet gets before the first retry, dnsruby doubles the interval for each retry after that, and the sum is the hard deadline. The default sends packets at 0, 5, and 10 seconds and gives the whole query 11 seconds before raising `Timeout`.
104
+
105
+ DNSSEC validation and dnsruby's answer cache are both off, unconditionally. DNSSEC costs a round trip that most callers of this gem have no way to act on โ€” MTA-STS, for one, trusts the certificate on the policy host rather than the DNS answer. Caching is dnsruby's own concern to have off: its cache is process-global, so a nameserver that starts failing would otherwise keep serving its last good answer to every caller in the process.
106
+
107
+ ## Custom Resolvers
108
+
109
+ Anything that responds to `txt`, `a`, `aaaa`, `addresses`, `mx`, `ptr`, and `cname` โ€” raising the three errors above โ€” is a drop-in replacement. You don't have to write one: see [Test Doubles](#test-doubles).
110
+
111
+ ## Test Doubles
112
+
113
+ `MailResolver::Testing` ships what every caller's suite would otherwise hand-roll. It isn't loaded with the gem:
114
+
115
+ ```ruby
116
+ require "mailresolver/testing"
117
+ ```
118
+
119
+ `FakeResolver` stands a hash in for a zone, no network involved:
120
+
121
+ ```ruby
122
+ resolver = MailResolver::Testing::FakeResolver.new(
123
+ "example.com" => { txt: [ "v=spf1 -all" ], a: [ "192.0.2.1" ] },
124
+ "broken.example.com" => { txt: :servfail }
125
+ )
126
+
127
+ resolver.txt("example.com") # => ["v=spf1 -all"]
128
+ resolver.mx("example.com") # raises NotFound โ€” the name is here, MX isn't
129
+ resolver.txt("absent.example.com") # raises NoSuchName โ€” the name isn't here at all
130
+ resolver.txt("broken.example.com") # raises ServerFailure
131
+ resolver.queries # => [["example.com", :txt], ...]
132
+ ```
133
+
134
+ The sentinels `:timeout`, `:servfail`, and `:nxdomain` stand where records would, for the failures a zone can't otherwise express. PTR is keyed by the address itself rather than its reverse name.
135
+
136
+ `LocalNameserver` goes a layer lower, for the cases a double can only assert by decree โ€” what dnsruby actually raises per rcode. It binds a UDP socket on loopback, answers every query with the rcode you name, and hands you a `Resolver` pointed at it:
137
+
138
+ ```ruby
139
+ MailResolver::Testing::LocalNameserver.answering(:servfail) do |resolver|
140
+ assert_raises(MailResolver::ServerFailure) { resolver.txt("example.com") }
141
+ end
142
+ ```
143
+
144
+ `:noerror`, `:servfail`, `:nxdomain`, and `:refused` are the rcodes; `nil` never replies, which is what a timeout looks like from the client. Pass `records:` to answer a `:noerror` query.
145
+
146
+ ## Testing
147
+
148
+ ```sh
149
+ bundle install
150
+ rake
151
+ ```
152
+
153
+ The suite stubs `Dnsruby::Resolver`'s query path throughout.
154
+
155
+ ## History
156
+
157
+ View the [changelog](CHANGELOG.md).
158
+
159
+ ## Contributing
160
+
161
+ Everyone is encouraged to help improve this project:
162
+
163
+ - [Report bugs](https://github.com/mailpiece/mailresolver/issues)
164
+ - Fix bugs and submit pull requests
165
+ - Write, clarify, or fix documentation
166
+ - Suggest or add new features
167
+
168
+ ## License
169
+
170
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,136 @@
1
+ require "dnsruby"
2
+ require "ipaddr"
3
+
4
+ module MailResolver
5
+ class Error < StandardError; end
6
+ # NXDOMAIN, or NOERROR with no records of the type asked for.
7
+ class NotFound < Error; end
8
+ # NXDOMAIN: the name itself doesn't exist, not just records of that type.
9
+ class NoSuchName < NotFound; end
10
+ # No answer arrived in time.
11
+ class Timeout < Error; end
12
+ # SERVFAIL, REFUSED, or transport failure โ€” distinct from NotFound's "nothing there".
13
+ class ServerFailure < Error; end
14
+
15
+ # DNS for mail. Built on dnsruby rather than Resolv, which collapses SERVFAIL/REFUSED into NXDOMAIN.
16
+ class Resolver
17
+ # One attempt per element; intervals double after the first, and the sum is the hard deadline.
18
+ # Default: attempts at 0s, 5s, and 10s, over at 11s.
19
+ TIMEOUTS = [ 5, 3, 3 ].freeze
20
+
21
+ def initialize(timeouts: TIMEOUTS)
22
+ @timeouts = timeouts
23
+ @lock = Mutex.new
24
+ end
25
+
26
+ # TXT character-strings are joined into one value (RFC 7208 ยง3.3).
27
+ def txt(name)
28
+ records("TXT", name).map { |record| record.strings.join }
29
+ end
30
+
31
+ def a(name)
32
+ records("A", name).map { |record| record.address.to_s }
33
+ end
34
+
35
+ # Dnsruby prints IPv6 in capitals; RFC 5952 ยง4.3 asks for lower case.
36
+ def aaaa(name)
37
+ records("AAAA", name).map { |record| record.address.to_s.downcase }
38
+ end
39
+
40
+ # A missing family contributes nothing; NXDOMAIN or nameserver failure still raises.
41
+ def addresses(name)
42
+ %w[ A AAAA ].flat_map { |type| family(type, name) }
43
+ end
44
+
45
+ # Equal preferences tie-break on the exchange name so callers always see the same order.
46
+ def mx(name)
47
+ records("MX", name).sort_by { |record| [ record.preference, record.exchange.to_s ] }
48
+ .map { |record| record.exchange.to_s }
49
+ end
50
+
51
+ # Dnsruby puts the question name in `name` and the host in `domainname`.
52
+ def ptr(address)
53
+ records("PTR", IPAddr.new(address.to_s).reverse).map { |record| record.domainname.to_s }
54
+ end
55
+
56
+ def cname(name)
57
+ records("CNAME", name).map { |record| record.domainname.to_s }
58
+ end
59
+
60
+ private
61
+ def family(type, name)
62
+ records(type, name).map { |record| record.address.to_s.downcase }
63
+ rescue NoSuchName
64
+ raise
65
+ rescue NotFound
66
+ []
67
+ end
68
+
69
+ # Filtered by type: a CNAME answer carries the alias beside the records it leads to.
70
+ def records(type, name)
71
+ answers = resolver.query(name.to_s, type).answer.select { |record| record.type == type }
72
+
73
+ if answers.any?
74
+ answers
75
+ else
76
+ raise NotFound, "no #{type} records for #{name}"
77
+ end
78
+ rescue Dnsruby::NXDomain
79
+ raise NoSuchName, "no such name #{name}"
80
+ rescue Dnsruby::ResolvTimeout
81
+ # Descends from Timeout::Error, not Dnsruby::ResolvError.
82
+ raise Timeout, "timed out resolving #{type} #{name}"
83
+ # Connect/send failures arrive as bare IOError/SocketError/SystemCallError from dnsruby's queue.
84
+ rescue Dnsruby::ResolvError, IOError, SocketError, SystemCallError => error
85
+ raise ServerFailure, "couldn't resolve #{type} #{name}: #{reason(error)}"
86
+ end
87
+
88
+ def reason(error)
89
+ if error.is_a?(Dnsruby::ResolvError)
90
+ rcode(error)
91
+ else
92
+ without_backtrace(error.message)
93
+ end
94
+ end
95
+
96
+ # Dnsruby rcode classes often carry no message, and `class.name` is nil on an anonymous class.
97
+ def rcode(error)
98
+ name = error.class.name&.split("::")&.last
99
+
100
+ case
101
+ when name.nil? then error.message
102
+ when error.message == error.class.name then name
103
+ else "#{name}, #{error.message}"
104
+ end
105
+ end
106
+
107
+ # Transport error messages carry dnsruby's own backtrace inline.
108
+ def without_backtrace(message)
109
+ message.sub(/\s*\[".*\z/m, "")
110
+ end
111
+
112
+ # Built once and shared so concurrent queries multiplex over one I/O thread.
113
+ # Caching and DNSSEC are off โ€” the former is process-global, the latter unused by callers.
114
+ def resolver
115
+ @lock.synchronize do
116
+ @resolver ||= Dnsruby::Resolver.new(**options).tap do |dns|
117
+ dns.dnssec = false
118
+ dns.do_caching = false
119
+ dns.query_timeout = @timeouts.sum
120
+ dns.packet_timeout = @timeouts.first
121
+ dns.retry_times = @timeouts.size
122
+ # Dnsruby fires attempt n at 2^(n-1) times this; halving the first element lands the schedule.
123
+ dns.retry_delay = @timeouts.first / 2.0
124
+ end
125
+ end
126
+ rescue ArgumentError => error
127
+ # Dnsruby raises this when no configured nameserver is usable.
128
+ raise ServerFailure, "no usable nameserver: #{error.message}"
129
+ end
130
+
131
+ # Overridden by MailResolver::Testing::LocalNameserver to point at a loopback nameserver.
132
+ def options
133
+ {}
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,170 @@
1
+ require "socket"
2
+ require "resolv"
3
+
4
+ module MailResolver
5
+ # Doubles for tests that need DNS to misbehave. Require from your test helper:
6
+ #
7
+ # require "mailresolver/testing"
8
+ module Testing
9
+ # A zone in a hash: missing names raise NoSuchName, missing types raise NotFound,
10
+ # and :timeout/:servfail/:nxdomain stand in for misbehavior. PTR is keyed by address.
11
+ #
12
+ # FakeResolver.new("example.com" => { txt: [ "v=spf1 -all" ], mx: :servfail })
13
+ class FakeResolver
14
+ attr_reader :queries
15
+
16
+ def initialize(zone = {})
17
+ @zone = zone.transform_keys { |name| normalized(name) }
18
+ @queries = []
19
+ end
20
+
21
+ def txt(name)
22
+ lookup(name, :txt)
23
+ end
24
+
25
+ def a(name)
26
+ lookup(name, :a)
27
+ end
28
+
29
+ def aaaa(name)
30
+ lookup(name, :aaaa)
31
+ end
32
+
33
+ # A missing family contributes nothing; NXDOMAIN or nameserver failure still raises.
34
+ def addresses(name)
35
+ %i[ a aaaa ].flat_map { |type| family(type, name) }
36
+ end
37
+
38
+ def mx(name)
39
+ lookup(name, :mx)
40
+ end
41
+
42
+ def ptr(address)
43
+ lookup(address, :ptr)
44
+ end
45
+
46
+ def cname(name)
47
+ lookup(name, :cname)
48
+ end
49
+
50
+ private
51
+ def family(type, name)
52
+ lookup(name, type)
53
+ rescue NoSuchName
54
+ raise
55
+ rescue NotFound
56
+ []
57
+ end
58
+
59
+ def lookup(name, type)
60
+ key = normalized(name)
61
+ @queries << [ key, type ]
62
+
63
+ case records = @zone.dig(key, type)
64
+ when :timeout then raise Timeout, "timed out resolving #{type.upcase} #{key}"
65
+ when :servfail then raise ServerFailure, "server failure resolving #{type.upcase} #{key}"
66
+ when :nxdomain then raise NoSuchName, "no such name #{key}"
67
+ when nil, [] then raise absence(key, type)
68
+ else records
69
+ end
70
+ end
71
+
72
+ # A name the zone never mentions doesn't exist; one there without this type does.
73
+ def absence(key, type)
74
+ if @zone.key?(key)
75
+ NotFound.new("no #{type.upcase} records for #{key}")
76
+ else
77
+ NoSuchName.new("no such name #{key}")
78
+ end
79
+ end
80
+
81
+ def normalized(name)
82
+ name.to_s.downcase.chomp(".")
83
+ end
84
+ end
85
+
86
+ # A loopback nameserver for one rcode (records for :noerror, [name, record]
87
+ # for an alias, nil for a timeout), with replies encoded via Resolv.
88
+ #
89
+ # LocalNameserver.answering(:servfail) do |resolver|
90
+ # assert_raises(MailResolver::ServerFailure) { resolver.txt("example.com") }
91
+ # end
92
+ class LocalNameserver
93
+ RCODES = { noerror: 0, servfail: 2, nxdomain: 3, refused: 5 }.freeze
94
+ TIMEOUTS = [ 0.5 ].freeze
95
+
96
+ def self.answering(rcode, records: [], timeouts: TIMEOUTS)
97
+ nameserver = new(rcode, records: records)
98
+ yield nameserver.resolver(timeouts: timeouts)
99
+ ensure
100
+ nameserver&.close
101
+ end
102
+
103
+ def initialize(rcode, records: [])
104
+ @rcode, @records = rcode, records
105
+ @socket = UDPSocket.new
106
+ @socket.bind("127.0.0.1", 0)
107
+ @thread = Thread.new { serve }
108
+ end
109
+
110
+ def port
111
+ @socket.addr[1]
112
+ end
113
+
114
+ def resolver(timeouts: TIMEOUTS)
115
+ LocalResolver.new(port, timeouts: timeouts)
116
+ end
117
+
118
+ def close
119
+ @thread.kill
120
+ @socket.close
121
+ end
122
+
123
+ # Overrides options so configuring a nameserver stays off the public API.
124
+ class LocalResolver < Resolver
125
+ def initialize(port, **options)
126
+ super(**options)
127
+ @port = port
128
+ end
129
+
130
+ private
131
+ def options
132
+ { nameserver: "127.0.0.1", port: @port }
133
+ end
134
+ end
135
+
136
+ private
137
+ def serve
138
+ loop do
139
+ message, sender = @socket.recvfrom(4096)
140
+ @socket.send(reply_to(message).encode, 0, sender[3], sender[1]) if @rcode
141
+ end
142
+ rescue IOError, Errno::EBADF
143
+ nil
144
+ end
145
+
146
+ def reply_to(message)
147
+ query = Resolv::DNS::Message.decode(message)
148
+ reply = Resolv::DNS::Message.new(query.id)
149
+ reply.qr = 1
150
+ reply.rd = query.rd
151
+ reply.ra = 1
152
+ reply.rcode = RCODES.fetch(@rcode)
153
+
154
+ query.each_question do |name, typeclass|
155
+ reply.add_question(name, typeclass)
156
+
157
+ @records.each do |record|
158
+ if record.is_a?(Array)
159
+ reply.add_answer(Resolv::DNS::Name.create(record.first), 60, record.last)
160
+ else
161
+ reply.add_answer(name, 60, record)
162
+ end
163
+ end
164
+ end
165
+
166
+ reply
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,3 @@
1
+ module MailResolver
2
+ VERSION = "0.5.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "mailresolver/version"
2
+ require "mailresolver/resolver"
3
+
4
+ # DNS for mail: what does DNS say, and if it didn't answer, why not?
5
+ module MailResolver
6
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "lib/mailresolver/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "mailresolver"
5
+ spec.version = MailResolver::VERSION
6
+ spec.platform = Gem::Platform::RUBY
7
+ spec.required_ruby_version = ">= 3.4"
8
+ spec.authors = [ "Simon Lev" ]
9
+
10
+ spec.summary = "DNS resolution for mail, in Ruby."
11
+ spec.description = "DNS resolution for mail, in Ruby. **mailresolver** is a small, injectable DNS client for mail libraries โ€” TXT, A/AAAA, MX, PTR, and CNAME lookups."
12
+
13
+ spec.homepage = "https://github.com/mailpiece/mailresolver"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
18
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
19
+ spec.metadata["rubygems_mfa_required"] = "true"
20
+
21
+ spec.files = Dir[
22
+ "lib/**/*.rb",
23
+ "CHANGELOG.md",
24
+ "README.md",
25
+ "LICENSE",
26
+ "mailresolver.gemspec"
27
+ ]
28
+ spec.require_paths = [ "lib" ]
29
+
30
+ # Resolv collapses SERVFAIL/REFUSED into empty answers; dnsruby does not.
31
+ spec.add_dependency "dnsruby", "~> 1.74"
32
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailresolver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - Simon Lev
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dnsruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.74'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.74'
26
+ description: DNS resolution for mail, in Ruby. **mailresolver** is a small, injectable
27
+ DNS client for mail libraries โ€” TXT, A/AAAA, MX, PTR, and CNAME lookups.
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - CHANGELOG.md
33
+ - LICENSE
34
+ - README.md
35
+ - lib/mailresolver.rb
36
+ - lib/mailresolver/resolver.rb
37
+ - lib/mailresolver/testing.rb
38
+ - lib/mailresolver/version.rb
39
+ - mailresolver.gemspec
40
+ homepage: https://github.com/mailpiece/mailresolver
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ source_code_uri: https://github.com/mailpiece/mailresolver
45
+ changelog_uri: https://github.com/mailpiece/mailresolver/blob/main/CHANGELOG.md
46
+ bug_tracker_uri: https://github.com/mailpiece/mailresolver/issues
47
+ rubygems_mfa_required: 'true'
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '3.4'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 4.0.10
63
+ specification_version: 4
64
+ summary: DNS resolution for mail, in Ruby.
65
+ test_files: []