Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dns_nameserver option for mx checking #230

Merged
merged 10 commits into from
Feb 4, 2024
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
Loading