Skip to content

Commit

Permalink
Merge pull request #230 from InfuseGroup/add-dns-nameserver-option
Browse files Browse the repository at this point in the history
Add `dns_nameserver` option for mx checking
  • Loading branch information
micke authored Feb 4, 2024
2 parents 5c89992 + 69e2a4f commit f2062f4
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 7 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,18 @@ To validate strictly that the domain has an MX record:
```ruby
validates :email, 'valid_email_2/email': { strict_mx: true }
```
`strict_mx` and `mx` both default to a 5 second timeout for DNS lookups. To
override this timeout, specify a `dns_timeout` option:
`strict_mx` and `mx` both default to a 5 second timeout for DNS lookups.
To override this timeout, specify a `dns_timeout` option:
```ruby
validates :email, 'valid_email_2/email': { strict_mx: true, dns_timeout: 10 }
```

Any checks that require DNS resolution will use the default `Resolv::DNS` nameservers for DNS lookups.
To override these, specify a `dns_nameserver` option:
```ruby
validates :email, 'valid_email_2/email': { mx: true, dns_nameserver: ['8.8.8.8', '8.8.4.4'] }
```

To validate that the domain is not a disposable email (checks domain and MX server):
```ruby
validates :email, 'valid_email_2/email': { disposable: true }
Expand Down
9 changes: 6 additions & 3 deletions lib/valid_email2/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ def self.prohibited_domain_characters_regex=(val)
@prohibited_domain_characters_regex = val
end

def initialize(address, dns_timeout = 5)
def initialize(address, dns_timeout = 5, dns_nameserver = nil)
@parse_error = false
@raw_address = address
@dns_timeout = dns_timeout

@resolv_config = Resolv::DNS::Config.default_config_hash
@resolv_config[:nameserver] = dns_nameserver if dns_nameserver

begin
@address = Mail::Address.new(address)
rescue Mail::Field::ParseError
Expand Down Expand Up @@ -134,7 +137,7 @@ def address_contain_emoticons?(email)
end

def mx_servers
@mx_servers ||= Resolv::DNS.open do |dns|
@mx_servers ||= Resolv::DNS.open(@resolv_config) do |dns|
dns.timeouts = @dns_timeout
dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
end
Expand All @@ -145,7 +148,7 @@ def null_mx?
end

def mx_or_a_servers
@mx_or_a_servers ||= Resolv::DNS.open do |dns|
@mx_or_a_servers ||= Resolv::DNS.open(@resolv_config) do |dns|
dns.timeouts = @dns_timeout
(mx_servers.any? && mx_servers) ||
dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
Expand Down
4 changes: 2 additions & 2 deletions lib/valid_email2/email_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
module ValidEmail2
class EmailValidator < ActiveModel::EachValidator
def default_options
{ disposable: false, mx: false, strict_mx: false, disallow_subaddressing: false, multiple: false, dns_timeout: 5 }
{ disposable: false, mx: false, strict_mx: false, disallow_subaddressing: false, multiple: false, dns_timeout: 5, dns_nameserver: nil }
end

def validate_each(record, attribute, value)
return unless value.present?
options = default_options.merge(self.options)

addresses = sanitized_values(value).map { |v| ValidEmail2::Address.new(v, options[:dns_timeout]) }
addresses = sanitized_values(value).map { |v| ValidEmail2::Address.new(v, options[:dns_timeout], options[:dns_nameserver]) }

error(record, attribute) && return unless addresses.all?(&:valid?)

Expand Down
104 changes: 104 additions & 0 deletions spec/valid_email2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ class TestUserStrictMX < TestModel
validates :email, 'valid_email_2/email': { strict_mx: true }
end

class TestUserMXDnsTimeout < TestModel
validates :email, 'valid_email_2/email': { mx: true, dns_timeout: 10 }
end

class TestUserMXDnsFailingTimeout < TestModel
validates :email, 'valid_email_2/email': { mx: true, dns_timeout: Float::MIN }
end

class TestUserMXDnsNameserver < TestModel
validates :email, 'valid_email_2/email': { mx: true, dns_nameserver: ['8.8.8.8', '8.8.4.4'] }
end

class TestUserMXDnsFailingNameserver < TestModel
validates :email, 'valid_email_2/email': { mx: true, dns_timeout: 0.1, dns_nameserver: '1.0.0.0' }
end

class TestUserDisallowDisposable < TestModel
validates :email, 'valid_email_2/email': { disposable: true }
end
Expand Down Expand Up @@ -292,6 +308,94 @@ def set_whitelist
end
end

describe "with mx validation and dns not hitting timeout" do
it "is valid if mx records are found" do
user = TestUserMXDnsTimeout.new(email: "[email protected]")
expect(user.valid?).to be_truthy
end

it "is valid if A records are found" do
user = TestUserMXDnsTimeout.new(email: "[email protected]")
expect(user.valid?).to be_truthy
end

it "is invalid if no mx records are found" do
user = TestUserMXDnsTimeout.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is invalid if a null mx is found" do
user = TestUserMXDnsTimeout.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end
end

describe "with mx validation and dns hitting timeout" do
it "is never valid even if mx records exist" do
user = TestUserMXDnsFailingTimeout.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is never valid even A records exist" do
user = TestUserMXDnsFailingTimeout.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is invalid if no mx records exist" do
user = TestUserMXDnsFailingTimeout.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is invalid if a null mx exists" do
user = TestUserMXDnsFailingTimeout.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end
end

describe "with mx validation and dns nameserver" do
it "is valid if mx records are found" do
user = TestUserMXDnsNameserver.new(email: "[email protected]")
expect(user.valid?).to be_truthy
end

it "is valid if A records are found" do
user = TestUserMXDnsNameserver.new(email: "[email protected]")
expect(user.valid?).to be_truthy
end

it "is invalid if no mx records are found" do
user = TestUserMXDnsNameserver.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is invalid if a null mx is found" do
user = TestUserMXDnsNameserver.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end
end

describe "with mx validation and failing dns nameserver" do
it "is never valid even if mx records exist" do
user = TestUserMXDnsFailingNameserver.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is never valid even A records exist" do
user = TestUserMXDnsFailingNameserver.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is invalid if no mx records exist" do
user = TestUserMXDnsFailingNameserver.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end

it "is invalid if a null mx exists" do
user = TestUserMXDnsFailingNameserver.new(email: "[email protected]")
expect(user.valid?).to be_falsey
end
end

describe "with dotted validation" do
it "is valid when address does not contain dots" do
user = TestUserDotted.new(email: "[email protected]")
Expand Down

0 comments on commit f2062f4

Please sign in to comment.