This setup allows you to check incoming email submissions to any signup form. It uses a “trust & verify” approach— creating two separate tables: allowlisted domains and denylisted domains.
If a new user gives an email address that is on the allowlisted domain, they can continue with a certain set of activities automatically. These might include things like gmail.com, yahoo.com, etc.
If a new user gives an email address that is on neither the allowlisted domain nor the denylisted domains, they go into a limbo and have a limited set of things they can do — you determine what that is for these “greylisted” new signups. Typically, this means they have limited access until they confirm the email address using a confirmation link.
If a user gives an email that is on the denylisted list, on the other hand, they are stopped immediately. No confirmation email is sent, and the user record cannot not be created in the database.
For the greylisted domain names, which come in from the signup form, an automatic process runs every 6 hours to check to see if the domain name looks like it is valid. This process automatically allowlists if there is an A-record for the associated domain, but it does not denylist the domains — you must do that manually. Once you allowlist, the User’s domain_verified_at flag is set, and they can continue with the operations restricted to only domain verified users. If you denylist the domain, this code takes the aggressive strategy of deleting the user record. (If the domain was already on the denylist when the user signed up, they would not have have been able to signup).
This process is specifically about domain verification — just confirming that the domain supplied is valid to receive email messages at all. The only way to confirm that a full email is valid to actually send the confirmation email and require the user click on a confirmation link to confirm their account.
Domain verification is designed to stop you from sending bad emails to fraud domains — something that can and will hurt your email sending reputation. This process should be in addition to making the user actually confirm the email using a confirmation link. Once a domain is denylisted, no confirmation email will be sent to protect your email sending reputation (users will not be able to use that domain on the site).
1/ Add dnsruby Gem
bundle add dnsruby
2/ Add this patch to your String object
Create a file at lib/string_patch.rb
class String
def obscure_email
first_part = self.split("@")[0]
first_part[0] + (first_part.length - 2).times.collect{"*"}.join + first_part[-1] + "@" + self.split("@")[1]
end
end
require it in config/application.rb
:
require "./lib/string_patch.rb"
3/ The DomainCheck Service Object
create app/services/domain_check.rb
class DomainCheck
def self.check(domain)
res = Dnsruby::Resolver.new
begin
ret = res.query(domain, Dnsruby::Types.A)
rescue Dnsruby::NXDomain => e
return false
rescue Dnsruby::ResolvTimeout => e
return false
rescue Dnsruby::ServFail => e
return false
end
return !!ret.answer[0]
end
end
4/ Create Tables for Allowlisted and Denylisted Domains
rails generate model AllowListedDomain domain:string last_checked_at:datetime
rails generate model DenyListedDomain domain:string
class CreateAllowListedDomains < ActiveRecord::Migration[7.0]
def change
create_table :allow_listed_domains do |t|
t.string :domain
t.datetime :last_check_at
t.timestamps
end
end
end
class CreateDenylistedDomains < ActiveRecord::Migration[7.0]
def change
create_table :denylisted_domains do |t|
t.string :domain
t.timestamps
end
end
end
5/ Add hooks to the AllowlistedDomain
and DenylistedDomain
model objects
app/models/allowlisted_domain.rb
and
class AllowedDomain < ApplicationRecord
before_destroy :reset_any_users
def reset_any_users
User.where("email ILIKE :search", search: "%@#{self.domain}" ).update_all(domain_verified_at: nil)
end
def to_label
domain
end
end
app/models/denylisted_domain.rb
class DenylistedDomain < ApplicationRecord
after_create :kill_from_allowlist
after_create :kill_any_users
def kill_from_allowlist
AllowlistedDomain.where(domain: self.domain).destroy_all
end
def kill_any_users
User.where("email ILIKE :search", search: "%@#{self.domain}" ).destroy_all
end
def to_label
domain
end
end
6/ create file app/jobs/domain_verify_users_job.rb
require 'sidekiq-scheduler'
# require 'get_process_mem'
require 'dnsruby'
class DomainVerifyUsersJob < ApplicationJob
queue_as :default
def perform(*args)
all_users = User.not_domain_verified
count = all_users.count
@email_result = ""
if count > 0
@email_result = "**** VERIFYING DOMAINS for #{count} unverified users.... "
end
all_users.each do |user|
domain = user.email.split("@")[1]
if !AllowListedDomain.find_by(domain: domain).nil?
mark_allowlisted(user, domain)
elsif !AllowListedDomain.find_by(domain: domain).nil?
mark_allowlisted(user, domain)
else
if DomainCheck.check(domain)
AllowListedDomain.create(domain: domain, last_checked_at: Time.current)
@email_result << "\n Checking domain #{domain}... GOOD, adding to allowlist"
mark_allowlisted(user, domain)
else
@email_result << "\n Checking domain #{domain}...\n BAD DOMAIN: \n denylisting #{domain} and deleting user #{user.id} #{user.email}"
DenyListedDomain.create(domain: domain)
user.destroy
end
end
end
if count > 0
AdminMailer.with(result_data: @email_result, subject: "VDQ - DomainVerifyUsersJob").notice_email.deliver_now
end
end
def mark_allowlisted(user, domain)
@email_result << "\n Setting user #{user.email.obscure_email} to verified with domain #{domain}"
user.domain_verified_at = Time.current
user.save
end
def mark_denylisted(user, domain)
@email_result << "\n Destroying user #{user.email} with denylisted domain #{domain}"
user.destroy
end
end
7/ Add domain_verified_at to your User model
class AddDomainVerifiedAtToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :domain_verified_at, :datetime
end
end
Add this scope to your user model:
scope :not_domain_verified, -> {where(domain_verified_at: nil)}
8/ Add logic to the controller that handles the signup when a user first enters an email address
Notice that if the domain is already denylisted, the user is not created at all.
If the domain is not denylisted, the user is created but the domain_verified_at will remain null.
If the domain is allowlisted, the user is created and the domain_verified_at will be set to the current time.
domain = user_params[:email].split("@")[1]
if DenylistedDomain.find_by(domain: domain)
flash[:alert] = "Sorry, the domain #{domain} can't be used with this website. Please try a different email."
else
allowlisted = ! AllowlistedDomain.find_by(domain: domain).nil?
user = User.create!({email: user_params[:email], domain_verified_at: (allowlisted ? DateTime.current : nil) })
user.update(user_params)
flash[:notice] = "Welcome to ____. Please check your email for confirmation instructions. "
if !user.domain_verified_at
flash[:notice] << "The domain @#{domain} has been flagged for review. Some emails may be blocked until this domain is verified. "
end
end
9/ You will need an Admin Mailer to send yourself an email
app/mailers/admin_mailer.rb
Note: instead of using Devise::Mailer as the super class you may also use a subclass of Devise::Mailer, such as AbcAppMailer
. This will send you, the administrator, a daily report of the allowlisting & denylisting activity. Be sure to set your email using @@ADMIN_EMAIL
below.
Note that because of how the job works (above), you will only get a message of any newly incoming domain was allowlisted or denylisted.
class AdminMailer < Devise::Mailer
helper :application # helpers defined within `application_helper`
include Devise::Controllers::UrlHelpers # eg. `confirmation_url`
default from: 'ADMIN <admin@domain.com>'
default reply_to: 'admin@domain.com'
layout 'admin_mailer'
@@ADMIN_EMAIL = "your-admin-email@domain.com"
def notice_email # note the params are basically proxied to MailConstructor
@content = params[:result_data]
@subject = params[:subject]
mail(to: @@ADMIN_EMAIL , subject: "#{@subject} [#{Rails.env}]")
end
end
10/ Add To config/sidekiq.yml
Add this recurring job to the Sikekiq scheduler. This will run the DomainVerifyUsersJob
every 6 hours.
:schedule:
DomainVerifyUsersJob:
cron: '1 */6 * * *' # “At minute 1 past every 6th hour.”
class: DomainVerifyUsersJob
desc: "Domain Verify Users Job"
Use this sample denylisted domains file to get started:
You will need to load this CSV into the denylisted_domains
table created above using a database tool.
11/ Build an Interface that Allows you to Manually Approve New Domains
To build an interface that allows you to go through the Users and allowlist or denylist the one that the job wouldn’t do, try out my gem Hot Glue.
Using Hot Glue, create admin interfaces to view & edit your AllowlistDomains
and DenylistDomains
, like so:
rails generate hot_glue:scaffold AllowlistDomain --gd --namespace=admin
rails generate hot_glue:scaffold DenylistDomain --gd --namespace=admin
The manual task for your admins to periodically is go through the Users where domain_verified_at
is null
and make a determination if the email domain looks valid. Add the domain name to either the allowlist or the denylist, then wait up to 6 hours for the job to run. Your user will get automatically domain verified or the user record will be destroyed accordingly.
Remember, this process should be in addition to making the user actually verify their email using a confirmation link. It protects you against obviously bad email inputs that can hurt your email sending score by letting you avoid sending to bad domains almost entirely or at little as possible.