-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2063 from internetee/2059-email-verification-rework
Refactor email/contact verification
- Loading branch information
Showing
30 changed files
with
537 additions
and
358 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.