Add MX record validation to e-mail subscriptions, refactor validator (#38502)
This commit is contained in:
parent
eda0f62f89
commit
0742a01571
@ -20,6 +20,7 @@ class EmailSubscription < ApplicationRecord
|
|||||||
normalizes :email, with: ->(str) { str.squish.downcase }
|
normalizes :email, with: ->(str) { str.squish.downcase }
|
||||||
|
|
||||||
validates :email, presence: true, email_address: true, uniqueness: { scope: :account_id }
|
validates :email, presence: true, email_address: true, uniqueness: { scope: :account_id }
|
||||||
|
validates :email, email_mx: true, if: -> { email_changed? && !Rails.env.local? }
|
||||||
|
|
||||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||||
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
scope :unconfirmed, -> { where(confirmed_at: nil) }
|
||||||
|
|||||||
@ -93,9 +93,9 @@ class User < ApplicationRecord
|
|||||||
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
|
||||||
|
|
||||||
validates :email, presence: true, email_address: true
|
validates :email, presence: true, email_address: true
|
||||||
|
validates :email, email_mx: { attempt_ip: :sign_up_ip }, if: :validate_email_dns?
|
||||||
|
|
||||||
validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
|
||||||
validates_with EmailMxValidator, if: :validate_email_dns?
|
|
||||||
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
|
||||||
|
|
||||||
# Honeypot/anti-spam fields
|
# Honeypot/anti-spam fields
|
||||||
|
|||||||
@ -2,21 +2,21 @@
|
|||||||
|
|
||||||
require 'resolv'
|
require 'resolv'
|
||||||
|
|
||||||
class EmailMxValidator < ActiveModel::Validator
|
class EmailMxValidator < ActiveModel::EachValidator
|
||||||
def validate(user)
|
def validate_each(record, attribute, value)
|
||||||
return if user.email.blank?
|
return if value.blank?
|
||||||
|
|
||||||
domain = get_domain(user.email)
|
domain = get_domain(value)
|
||||||
|
|
||||||
if domain.blank? || domain.include?('..')
|
if domain.blank? || domain.include?('..')
|
||||||
user.errors.add(:email, :invalid)
|
record.errors.add(attribute, :invalid)
|
||||||
elsif !on_allowlist?(domain)
|
elsif !on_allowlist?(domain)
|
||||||
resolved_ips, resolved_domains = resolve_mx(domain)
|
resolved_ips, resolved_domains = resolve_mx(domain)
|
||||||
|
|
||||||
if resolved_ips.empty?
|
if resolved_ips.empty?
|
||||||
user.errors.add(:email, :unreachable)
|
record.errors.add(attribute, :unreachable)
|
||||||
elsif email_domain_blocked?(resolved_domains, user.sign_up_ip)
|
elsif email_domain_blocked?([domain, *resolved_domains], options[:attempt_ip].is_a?(Symbol) ? record.public_send(options[:attempt_ip]) : nil)
|
||||||
user.errors.add(:email, :blocked)
|
record.errors.add(attribute, :blocked)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -3,109 +3,101 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe EmailMxValidator do
|
RSpec.describe EmailMxValidator do
|
||||||
let(:user) { Fabricate.build :user, email: }
|
subject { record_class.new }
|
||||||
let(:email) { 'foo@example.com' }
|
|
||||||
let(:resolv_dns_double) { instance_double(Resolv::DNS) }
|
|
||||||
|
|
||||||
context 'with an e-mail domain that is explicitly allowed' do
|
context 'with no options' do
|
||||||
around do |example|
|
let(:record_class) do
|
||||||
original = Rails.configuration.x.email_domains_allowlist
|
Class.new do
|
||||||
Rails.configuration.x.email_domains_allowlist = 'example.com'
|
include ActiveModel::Validations
|
||||||
example.run
|
|
||||||
Rails.configuration.x.email_domains_allowlist = original
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are not DNS records' do
|
def self.name = 'Record'
|
||||||
before { configure_resolver('example.com') }
|
|
||||||
|
|
||||||
it 'does not add errors to record' do
|
attr_accessor :email
|
||||||
subject.validate(user)
|
|
||||||
expect(user.errors).to be_empty
|
validates :email, email_mx: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are DNS records for the domain' do
|
let(:user) { Fabricate.build :user, email: }
|
||||||
before { configure_resolver('example.com', a: resolv_double_a('192.0.2.42')) }
|
let(:email) { 'foo@example.com' }
|
||||||
|
let(:resolv_dns_double) { instance_double(Resolv::DNS) }
|
||||||
|
|
||||||
it 'does not add errors to record' do
|
context 'with an e-mail domain that is explicitly allowed' do
|
||||||
subject.validate(user)
|
around do |example|
|
||||||
expect(user.errors).to be_empty
|
original = Rails.configuration.x.email_domains_allowlist
|
||||||
end
|
Rails.configuration.x.email_domains_allowlist = 'example.com'
|
||||||
end
|
example.run
|
||||||
|
Rails.configuration.x.email_domains_allowlist = original
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the TagManager fails to normalize the domain' do
|
context 'when there are not DNS records' do
|
||||||
before do
|
before { configure_resolver('example.com') }
|
||||||
allow(TagManager).to receive(:instance).and_return(tag_manage_double)
|
|
||||||
allow(tag_manage_double).to receive(:normalize_domain).with('example.com').and_raise(Addressable::URI::InvalidURIError)
|
it { is_expected.to allow_value(email).for(:email) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:tag_manage_double) { instance_double(TagManager) }
|
context 'when there are DNS records for the domain' do
|
||||||
|
before { configure_resolver('example.com', a: resolv_double_a('192.0.2.42')) }
|
||||||
|
|
||||||
it 'adds errors to record' do
|
it { is_expected.to allow_value(email).for(:email) }
|
||||||
subject.validate(user)
|
|
||||||
expect(user.errors).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the email portion is blank' do
|
|
||||||
let(:email) { 'foo@' }
|
|
||||||
|
|
||||||
it 'adds errors to record' do
|
|
||||||
subject.validate(user)
|
|
||||||
expect(user.errors).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the email domain contains empty labels' do
|
|
||||||
let(:email) { 'foo@example..com' }
|
|
||||||
|
|
||||||
before { configure_resolver('example..com', a: resolv_double_a('192.0.2.42')) }
|
|
||||||
|
|
||||||
it 'adds errors to record' do
|
|
||||||
subject.validate(user)
|
|
||||||
expect(user.errors).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are no DNS records for the email domain' do
|
|
||||||
before { configure_resolver('example.com') }
|
|
||||||
|
|
||||||
it 'adds errors to record' do
|
|
||||||
subject.validate(user)
|
|
||||||
expect(user.errors).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when MX record does not lead to an IP' do
|
|
||||||
before do
|
|
||||||
configure_resolver('example.com', mx: resolv_double_mx('mail.example.com'))
|
|
||||||
configure_resolver('mail.example.com')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'adds errors to record' do
|
context 'when the TagManager fails to normalize the domain' do
|
||||||
subject.validate(user)
|
before do
|
||||||
expect(user.errors).to be_present
|
allow(TagManager).to receive(:instance).and_return(tag_manage_double)
|
||||||
end
|
allow(tag_manage_double).to receive(:normalize_domain).with('example.com').and_raise(Addressable::URI::InvalidURIError)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the MX record has an email domain block' do
|
let(:tag_manage_double) { instance_double(TagManager) }
|
||||||
before do
|
|
||||||
Fabricate :email_domain_block, domain: 'mail.example.com'
|
it { is_expected.to_not allow_value(email).for(:email) }
|
||||||
configure_resolver(
|
|
||||||
'example.com',
|
|
||||||
mx: resolv_double_mx('mail.example.com')
|
|
||||||
)
|
|
||||||
configure_resolver(
|
|
||||||
'mail.example.com',
|
|
||||||
a: instance_double(Resolv::DNS::Resource::IN::A, address: '2.3.4.5'),
|
|
||||||
aaaa: instance_double(Resolv::DNS::Resource::IN::AAAA, address: 'fd00::2')
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'adds errors to record' do
|
context 'when the email portion is blank' do
|
||||||
subject.validate(user)
|
let(:email) { 'foo@' }
|
||||||
expect(user.errors).to be_present
|
|
||||||
|
it { is_expected.to_not allow_value(email).for(:email) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the email domain contains empty labels' do
|
||||||
|
let(:email) { 'foo@example..com' }
|
||||||
|
|
||||||
|
before { configure_resolver('example..com', a: resolv_double_a('192.0.2.42')) }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_value(email).for(:email) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are no DNS records for the email domain' do
|
||||||
|
before { configure_resolver('example.com') }
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_value(email).for(:email) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when MX record does not lead to an IP' do
|
||||||
|
before do
|
||||||
|
configure_resolver('example.com', mx: resolv_double_mx('mail.example.com'))
|
||||||
|
configure_resolver('mail.example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_value(email).for(:email) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the MX record has an email domain block' do
|
||||||
|
before do
|
||||||
|
Fabricate :email_domain_block, domain: 'mail.example.com'
|
||||||
|
configure_resolver(
|
||||||
|
'example.com',
|
||||||
|
mx: resolv_double_mx('mail.example.com')
|
||||||
|
)
|
||||||
|
configure_resolver(
|
||||||
|
'mail.example.com',
|
||||||
|
a: instance_double(Resolv::DNS::Resource::IN::A, address: '2.3.4.5'),
|
||||||
|
aaaa: instance_double(Resolv::DNS::Resource::IN::AAAA, address: 'fd00::2')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to_not allow_value(email).for(:email) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user