Skip to content

Commit

Permalink
Merge pull request #2063 from internetee/2059-email-verification-rework
Browse files Browse the repository at this point in the history
Refactor email/contact verification
  • Loading branch information
vohmar authored Oct 8, 2021
2 parents b0fb2f4 + 763c91a commit fb37282
Show file tree
Hide file tree
Showing 30 changed files with 537 additions and 358 deletions.
10 changes: 0 additions & 10 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,4 @@ def legal_document_types
def body_css_class
[controller_path.split('/').map!(&:dasherize), action_name.dasherize, 'page'].join('-')
end

def verified_email_span(verification)
tag.span(verification.email, class: verified_email_class(verification))
end

def verified_email_class(verification)
return 'text-danger' if verification.failed?
return 'text-primary' if verification.not_verified?
return 'text-success' if verification.verified?
end
end
64 changes: 64 additions & 0 deletions app/interactions/actions/email_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module Actions
class EmailCheck
attr_reader :email, :validation_eventable, :check_level

def initialize(email:, validation_eventable:, check_level: nil)
@email = email
@validation_eventable = validation_eventable
@check_level = check_level || :regex
end

def call
result = check_email(email)
save_result(result)
result.success ? log_success : log_failure(result)
result.success
end

private

def check_email(parsed_email)
Truemail.validate(parsed_email, with: calculate_check_level).result
end

def calculate_check_level
Rails.env.test? && check_level == 'smtp' ? :mx : check_level.to_sym
end

def save_result(result)
validation_eventable.validation_events.create(validation_event_attrs(result))
rescue ActiveRecord::RecordNotSaved
logger.info "Cannot save validation result for #{log_object_id}"
true
end

def validation_event_attrs(result)
{
event_data: event_data(result),
event_type: ValidationEvent::EventType::TYPES[:email_validation],
success: result.success,
}
end

def logger
@logger ||= Rails.logger
end

def event_data(result)
result.to_h.merge(check_level: check_level)
end

def log_failure(result)
logger.info "Failed to validate email #{email} for the #{log_object_id}."
logger.info "Validation level #{check_level}, the result was #{result}"
end

def log_success
logger.info "Successfully validated email #{email} for the #{log_object_id}."
end

def log_object_id
"#{validation_eventable.class}: #{validation_eventable.id}"
end
end
end
11 changes: 2 additions & 9 deletions app/interactions/domains/force_delete_lift/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ class Base < ActiveInteraction::Base
description: 'Domain to check if ForceDelete needs to be listed'

def execute
prepare_email_verifications(domain)

lift_force_delete(domain) if force_delete_condition(domain)
end

Expand All @@ -29,13 +27,8 @@ def template_of_invalid_email?(domain)
end

def contact_emails_valid?(domain)
domain.contacts.all? { |contact| contact.email_verification.verified? } &&
domain.registrant.email_verification.verified?
end

def prepare_email_verifications(domain)
domain.registrant.email_verification.verify
domain.contacts.each { |contact| contact.email_verification.verify }
domain.contacts.all(&:need_to_lift_force_delete?) &&
domain.registrant.need_to_lift_force_delete?
end

def bounces_absent?(domain)
Expand Down
48 changes: 19 additions & 29 deletions app/jobs/verify_emails_job.rb
Original file line number Diff line number Diff line change
@@ -1,47 +1,37 @@
class VerifyEmailsJob < ApplicationJob
discard_on StandardError

def perform(verification_id)
email_address_verification = EmailAddressVerification.find(verification_id)
return unless need_to_verify?(email_address_verification)

process(email_address_verification)
def perform(contact_id:, check_level: 'regex')
contact = Contact.find_by(id: contact_id)
contact_not_found(contact_id) unless contact
validate_check_level(check_level)

action = Actions::EmailCheck.new(email: contact.email,
validation_eventable: contact,
check_level: check_level)
action.call
rescue StandardError => e
log_error(verification: email_address_verification, error: e)
logger.error e.message
raise e
end

private

def need_to_verify?(email_address_verification)
return false if email_address_verification.blank?
return false if email_address_verification.recently_verified?

true
def contact_not_found(contact_id)
raise StandardError, "Contact with contact_id #{contact_id} not found"
end

def process(email_address_verification)
email_address_verification.verify
log_success(email_address_verification)
end
def validate_check_level(check_level)
return if valid_check_levels.include? check_level

def logger
@logger ||= Logger.new(Rails.root.join('log/email_verification.log'))
raise StandardError, "Check level #{check_level} is invalid"
end

def log_success(verification)
email = verification.try(:email) || verification
message = "Email address #{email} verification done"
logger.info message
def logger
@logger ||= Rails.logger
end

def log_error(verification:, error:)
email = verification.try(:email) || verification
message = <<~TEXT.squish
There was an error verifying email #{email}.
The error message was the following: #{error}
This job will retry.
TEXT
logger.error message
def valid_check_levels
ValidationEvent::VALID_CHECK_LEVELS
end
end
96 changes: 32 additions & 64 deletions app/models/concerns/email_verifable.rb
Original file line number Diff line number Diff line change
@@ -1,91 +1,59 @@
module EmailVerifable
extend ActiveSupport::Concern

def email_verification
EmailAddressVerification.find_or_create_by(email: unicode_email, domain: domain(email))
end

def billing_email_verification
return unless attribute_names.include?('billing_email')

EmailAddressVerification.find_or_create_by(email: unicode_billing_email,
domain: domain(billing_email))
included do
scope :recently_not_validated, -> { where.not(id: ValidationEvent.validated_ids_by(name)) }
end

def email_verification_failed?
email_verification&.failed?
need_to_start_force_delete?
end

class_methods do
def domain(email)
Mail::Address.new(email).domain&.downcase || 'not_found'
rescue Mail::Field::IncompleteParseError
'not_found'
end

def local(email)
Mail::Address.new(email).local&.downcase || email
rescue Mail::Field::IncompleteParseError
email
end

def punycode_to_unicode(email)
return email if domain(email) == 'not_found'

local = local(email)
domain = SimpleIDN.to_unicode(domain(email))
"#{local}@#{domain}"&.downcase
end

def unicode_to_punycode(email)
return email if domain(email) == 'not_found'

local = local(email)
domain = SimpleIDN.to_ascii(domain(email))
"#{local}@#{domain}"&.downcase
def need_to_start_force_delete?
ValidationEvent::INVALID_EVENTS_COUNT_BY_LEVEL.any? do |level, count|
validation_events.recent.order(id: :desc).limit(count).all? do |event|
event.check_level == level.to_s && event.failed?
end
end
end

def unicode_billing_email
self.class.punycode_to_unicode(billing_email)
end

def unicode_email
self.class.punycode_to_unicode(email)
end

def domain(email)
SimpleIDN.to_unicode(self.class.domain(email))
end

def punycode_to_unicode(email)
self.class.punycode_to_unicode(email)
def need_to_lift_force_delete?
validation_events.recent.failed.empty? ||
ValidationEvent::REDEEM_EVENTS_COUNT_BY_LEVEL.any? do |level, count|
validation_events.recent.order(id: :desc).limit(count).all? do |event|
event.check_level == level.to_s && event.successful?
end
end
end

def correct_email_format
return if email.blank?

result = email_verification.verify
process_result(result: result, field: :email)
result = verify(email: email)
process_error(:email) unless result
end

def correct_billing_email_format
return if email.blank?

result = billing_email_verification.verify
process_result(result: result, field: :billing_email)
result = verify(email: billing_email)
process_error(:billing_email) unless result
end

def verify_email(check_level: 'regex')
verify(email: email, check_level: check_level)
end

def verify(email:, check_level: 'regex')
action = Actions::EmailCheck.new(email: email,
validation_eventable: self,
check_level: check_level)
action.call
end

# rubocop:disable Metrics/LineLength
def process_result(result:, field:)
case result[:errors].keys.first
when :smtp
errors.add(field, I18n.t('activerecord.errors.models.contact.attributes.email.email_smtp_check_error'))
when :mx
errors.add(field, I18n.t('activerecord.errors.models.contact.attributes.email.email_mx_check_error'))
when :regex
errors.add(field, I18n.t('activerecord.errors.models.contact.attributes.email.email_regex_check_error'))
end
def process_error(field)
errors.add(field, I18n.t('activerecord.errors.models.contact.attributes.email.email_regex_check_error'))
end
# rubocop:enable Metrics/LineLength
end
1 change: 1 addition & 0 deletions app/models/contact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Contact < ApplicationRecord
has_many :domain_contacts
has_many :domains, through: :domain_contacts
has_many :legal_documents, as: :documentable
has_many :validation_events, as: :validation_eventable
has_many :registrant_domains, class_name: 'Domain', foreign_key: 'registrant_id'
has_many :actions, dependent: :destroy

Expand Down
67 changes: 0 additions & 67 deletions app/models/email_address_verification.rb

This file was deleted.

1 change: 1 addition & 0 deletions app/models/registrar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Registrar < ApplicationRecord
has_many :nameservers, through: :domains
has_many :whois_records
has_many :white_ips, dependent: :destroy
has_many :validation_events, as: :validation_eventable

delegate :balance, to: :cash_account, allow_nil: true

Expand Down
Loading

0 comments on commit fb37282

Please sign in to comment.