forked from fedi/mastodon
Improve e-mail MX validator and add tests (#9489)
This commit is contained in:
parent
3f12c07ff5
commit
dbb1ee269f
|
@ -73,7 +73,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||||
validates_with BlacklistedEmailValidator, if: :email_changed?
|
validates_with BlacklistedEmailValidator, if: :email_changed?
|
||||||
validates_with EmailMxValidator, if: :email_changed?
|
validates_with EmailMxValidator, if: :validate_email_dns?
|
||||||
|
|
||||||
scope :recent, -> { order(id: :desc) }
|
scope :recent, -> { order(id: :desc) }
|
||||||
scope :admins, -> { where(admin: true) }
|
scope :admins, -> { where(admin: true) }
|
||||||
|
@ -360,4 +360,8 @@ class User < ApplicationRecord
|
||||||
def needs_feed_update?
|
def needs_feed_update?
|
||||||
last_sign_in_at < ACTIVE_DURATION.ago
|
last_sign_in_at < ACTIVE_DURATION.ago
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_email_dns?
|
||||||
|
email_changed? && !(Rails.env.test? || Rails.env.development?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,6 @@ require 'resolv'
|
||||||
|
|
||||||
class EmailMxValidator < ActiveModel::Validator
|
class EmailMxValidator < ActiveModel::Validator
|
||||||
def validate(user)
|
def validate(user)
|
||||||
return if Rails.env.test? || Rails.env.development?
|
|
||||||
user.errors.add(:email, I18n.t('users.invalid_email')) if invalid_mx?(user.email)
|
user.errors.add(:email, I18n.t('users.invalid_email')) if invalid_mx?(user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -15,13 +14,23 @@ class EmailMxValidator < ActiveModel::Validator
|
||||||
|
|
||||||
return true if domain.nil?
|
return true if domain.nil?
|
||||||
|
|
||||||
records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
|
hostnames = []
|
||||||
records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s } if records.empty?
|
ips = []
|
||||||
|
|
||||||
records.empty? || on_blacklist?(records)
|
Resolv::DNS.open do |dns|
|
||||||
|
dns.timeouts = 1
|
||||||
|
|
||||||
|
hostnames = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
|
||||||
|
|
||||||
|
([domain] + hostnames).uniq.each do |hostname|
|
||||||
|
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ips.empty? || on_blacklist?(hostnames + ips)
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_blacklist?(values)
|
def on_blacklist?(values)
|
||||||
EmailDomainBlock.where(domain: values).any?
|
EmailDomainBlock.where(domain: values.uniq).any?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
75
spec/validators/email_mx_validator_spec.rb
Normal file
75
spec/validators/email_mx_validator_spec.rb
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe EmailMxValidator do
|
||||||
|
describe '#validate' do
|
||||||
|
let(:user) { double(email: 'foo@example.com', errors: double(add: nil)) }
|
||||||
|
|
||||||
|
it 'adds an error if there are no DNS records for the e-mail domain' do
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error if a MX record exists but does not lead to an IP' do
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error if the A record is blacklisted' do
|
||||||
|
EmailDomainBlock.create!(domain: '1.2.3.4')
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '1.2.3.4')])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error if the MX record is blacklisted' do
|
||||||
|
EmailDomainBlock.create!(domain: '2.3.4.5')
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds an error if the MX hostname is blacklisted' do
|
||||||
|
EmailDomainBlock.create!(domain: 'mail.example.com')
|
||||||
|
resolver = double
|
||||||
|
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::MX).and_return([double(exchange: 'mail.example.com')])
|
||||||
|
allow(resolver).to receive(:getresources).with('example.com', Resolv::DNS::Resource::IN::A).and_return([])
|
||||||
|
allow(resolver).to receive(:getresources).with('mail.example.com', Resolv::DNS::Resource::IN::A).and_return([double(address: '2.3.4.5')])
|
||||||
|
allow(resolver).to receive(:timeouts=).and_return(nil)
|
||||||
|
allow(Resolv::DNS).to receive(:open).and_yield(resolver)
|
||||||
|
|
||||||
|
subject.validate(user)
|
||||||
|
expect(user.errors).to have_received(:add)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue