Skip to content
This repository was archived by the owner on Jan 27, 2020. It is now read-only.

Commit 8d54e87

Browse files
Gargronabcang
authored andcommitted
Improve e-mail MX validator and add tests (mastodon#9489)
1 parent 15d2eeb commit 8d54e87

3 files changed

Lines changed: 94 additions & 6 deletions

File tree

app/models/user.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class User < ApplicationRecord
6969

7070
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
7171
validates_with BlacklistedEmailValidator, if: :email_changed?
72-
validates_with EmailMxValidator, if: :email_changed?
72+
validates_with EmailMxValidator, if: :validate_email_dns?
7373

7474
scope :recent, -> { order(id: :desc) }
7575
scope :admins, -> { where(admin: true) }
@@ -349,4 +349,8 @@ def regenerate_feed!
349349
def needs_feed_update?
350350
last_sign_in_at < ACTIVE_DURATION.ago
351351
end
352+
353+
def validate_email_dns?
354+
email_changed? && !(Rails.env.test? || Rails.env.development?)
355+
end
352356
end

app/validators/email_mx_validator.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
class EmailMxValidator < ActiveModel::Validator
66
def validate(user)
7-
return if Rails.env.test? || Rails.env.development?
87
user.errors.add(:email, I18n.t('users.invalid_email')) if invalid_mx?(user.email)
98
end
109

@@ -15,13 +14,23 @@ def invalid_mx?(value)
1514

1615
return true if domain.nil?
1716

18-
records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
19-
records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s } if records.empty?
17+
hostnames = []
18+
ips = []
2019

21-
records.empty? || on_blacklist?(records)
20+
Resolv::DNS.open do |dns|
21+
dns.timeouts = 1
22+
23+
hostnames = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
24+
25+
([domain] + hostnames).uniq.each do |hostname|
26+
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
27+
end
28+
end
29+
30+
ips.empty? || on_blacklist?(hostnames + ips)
2231
end
2332

2433
def on_blacklist?(values)
25-
EmailDomainBlock.where(domain: values).any?
34+
EmailDomainBlock.where(domain: values.uniq).any?
2635
end
2736
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
describe EmailMxValidator do
6+
describe '#validate' do
7+
let(:user) { double(email: 'foo@example.com', errors: double(add: nil)) }
8+
9+
it 'adds an error if there are no DNS records for the e-mail domain' do
10+
resolver = double
11+
12+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
13+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
14+
allow(resolver).to receive(:timeouts=).and_return(nil)
15+
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
16+
17+
subject.validate(user)
18+
expect(user.errors).to have_received(:add)
19+
end
20+
21+
it 'adds an error if a MX record exists but does not lead to an IP' do
22+
resolver = double
23+
24+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
25+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
26+
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
27+
allow(resolver).to receive(:timeouts=).and_return(nil)
28+
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
29+
30+
subject.validate(user)
31+
expect(user.errors).to have_received(:add)
32+
end
33+
34+
it 'adds an error if the A record is blacklisted' do
35+
EmailDomainBlock.create!(domain: '1.2.3.4')
36+
resolver = double
37+
38+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
39+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')])
40+
allow(resolver).to receive(:timeouts=).and_return(nil)
41+
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
42+
43+
subject.validate(user)
44+
expect(user.errors).to have_received(:add)
45+
end
46+
47+
it 'adds an error if the MX record is blacklisted' do
48+
EmailDomainBlock.create!(domain: '2.3.4.5')
49+
resolver = double
50+
51+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
52+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
53+
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
54+
allow(resolver).to receive(:timeouts=).and_return(nil)
55+
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
56+
57+
subject.validate(user)
58+
expect(user.errors).to have_received(:add)
59+
end
60+
61+
it 'adds an error if the MX hostname is blacklisted' do
62+
EmailDomainBlock.create!(domain: 'mail.example.com')
63+
resolver = double
64+
65+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
66+
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
67+
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
68+
allow(resolver).to receive(:timeouts=).and_return(nil)
69+
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
70+
71+
subject.validate(user)
72+
expect(user.errors).to have_received(:add)
73+
end
74+
end
75+
end

0 commit comments

Comments
 (0)