From 0a7b754c4ce6c21b62afe421e6bb8fc7e07a4b35 Mon Sep 17 00:00:00 2001 From: Alex Sherman Date: Mon, 13 Jul 2020 13:48:08 +0500 Subject: [PATCH] Add email description to epp responce on contact creation --- app/models/concerns/email_verifable.rb | 89 +++++++++++++++++++ app/models/epp/contact.rb | 5 +- config/initializers/truemail.rb | 77 ++++++++++++++++ config/locales/contacts.en.yml | 3 + .../epp/contact/create/base_test.rb | 35 ++++++++ test/models/contact_test.rb | 20 ++++- test/models/registrar_test.rb | 61 ++++++++++++- 7 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 app/models/concerns/email_verifable.rb create mode 100644 config/initializers/truemail.rb diff --git a/app/models/concerns/email_verifable.rb b/app/models/concerns/email_verifable.rb new file mode 100644 index 000000000..f573b5e44 --- /dev/null +++ b/app/models/concerns/email_verifable.rb @@ -0,0 +1,89 @@ +module Concerns + module EmailVerifable + extend ActiveSupport::Concern + + def email_verification + @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') + + @billing_email_verification ||= EmailAddressVerification + .find_or_create_by(email: unicode_billing_email, + domain: domain(billing_email)) + 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 + 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) + end + + def correct_email_format + return if email.blank? + + result = email_verification.verify + process_result(result: result, field: :email) + end + + def correct_billing_email_format + return if email.blank? + + result = billing_email_verification.verify + process_result(result: result, field: :billing_email) + end + + 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 + end + end +end diff --git a/app/models/epp/contact.rb b/app/models/epp/contact.rb index 3fb87a1f0..82dd57aa4 100644 --- a/app/models/epp/contact.rb +++ b/app/models/epp/contact.rb @@ -77,7 +77,10 @@ class Epp::Contact < Contact [:email, :invalid], [:country_code, :invalid], [:code, :invalid], - [:code, :too_long_contact_code] + [:code, :too_long_contact_code], + [:email, :email_smtp_check_error], + [:email, :email_mx_check_error], + [:email, :email_regex_check_error] ], '2302' => [ # Object exists [:code, :epp_id_taken] diff --git a/config/initializers/truemail.rb b/config/initializers/truemail.rb new file mode 100644 index 000000000..26e4e0dc5 --- /dev/null +++ b/config/initializers/truemail.rb @@ -0,0 +1,77 @@ +require 'truemail' + +Truemail.configure do |config| + # Required parameter. Must be an existing email on behalf of which verification will be performed + config.verifier_email = ENV['action_mailer_default_from'] + + # Optional parameter. Must be an existing domain on behalf of which verification will be performed. + # By default verifier domain based on verifier email + # config.verifier_domain = 'internet.ee' + + # Optional parameter. You can override default regex pattern + # config.email_pattern = /regex_pattern/ + + # Optional parameter. You can override default regex pattern + # config.smtp_error_body_pattern = /regex_pattern/ + + # Optional parameter. Connection timeout is equal to 2 ms by default. + # config.connection_timeout = 1 + + # Optional parameter. A SMTP server response timeout is equal to 2 ms by default. + # config.response_timeout = 1 + + # Optional parameter. Total of connection attempts. It is equal to 2 by default. + # This parameter uses in mx lookup timeout error and smtp request (for cases when + # there is one mx server). + config.connection_attempts = 3 + + # Optional parameter. You can predefine default validation type for + # Truemail.validate('email@email.com') call without with-parameter + # Available validation types: :regex, :mx, :smtp + if Rails.env.production? + config.default_validation_type = :smtp + elsif Rails.env.test? + config.default_validation_type = :regex + else + config.default_validation_type = :mx + end + + # Optional parameter. You can predefine which type of validation will be used for domains. + # Also you can skip validation by domain. Available validation types: :regex, :mx, :smtp + # This configuration will be used over current or default validation type parameter + # All of validations for 'somedomain.com' will be processed with regex validation only. + # And all of validations for 'otherdomain.com' will be processed with mx validation only. + # It is equal to empty hash by default. + # config.validation_type_for = { 'somedomain.com' => :regex, 'otherdomain.com' => :mx } + + # Optional parameter. Validation of email which contains whitelisted domain always will + # return true. Other validations will not processed even if it was defined in validation_type_for + # It is equal to empty array by default. + # config.whitelisted_domains = [] + + # Optional parameter. With this option Truemail will validate email which contains whitelisted + # domain only, i.e. if domain whitelisted, validation will passed to Regex, MX or SMTP validators. + # Validation of email which not contains whitelisted domain always will return false. + # It is equal false by default. + #config.whitelist_validation = true + + # Optional parameter. Validation of email which contains blacklisted domain always will + # return false. Other validations will not processed even if it was defined in validation_type_for + # It is equal to empty array by default. + #config.blacklisted_domains = [] + + # Optional parameter. This option will provide to use not RFC MX lookup flow. + # It means that MX and Null MX records will be cheked on the DNS validation layer only. + # By default this option is disabled. + # config.not_rfc_mx_lookup_flow = true + + # Optional parameter. This option will be parse bodies of SMTP errors. It will be helpful + # if SMTP server does not return an exact answer that the email does not exist + # By default this option is disabled, available for SMTP validation only. + # config.smtp_safe_check = true + + # Optional parameter. This option will enable tracking events. You can print tracking events to + # stdout, write to file or both of these. Tracking event by default is :error + # Available tracking event: :all, :unrecognized_error, :recognized_error, :error + # config.logger = { tracking_event: :all, stdout: true, log_absolute_path: '/home/app/log/truemail.log' } +end diff --git a/config/locales/contacts.en.yml b/config/locales/contacts.en.yml index cdfe2277d..906bde193 100644 --- a/config/locales/contacts.en.yml +++ b/config/locales/contacts.en.yml @@ -20,6 +20,9 @@ en: email: blank: "Required parameter missing - email" invalid: "Email is invalid" + email_smtp_check_error: SMTP check error + email_mx_check_error: Mail domain not found + email_regex_check_error: Invalid format domains: exist: 'Object association prohibits operation' statuses: diff --git a/test/integration/epp/contact/create/base_test.rb b/test/integration/epp/contact/create/base_test.rb index 0a14f1f4f..e9a59b8d2 100644 --- a/test/integration/epp/contact/create/base_test.rb +++ b/test/integration/epp/contact/create/base_test.rb @@ -41,6 +41,41 @@ class EppContactCreateBaseTest < EppTestCase assert_not_empty contact.code end + def test_responces_error_with_email_error + name = 'new' + email = 'new@registrar@test' + phone = '+1.2' + + request_xml = <<-XML + + + + + + + #{name} + + #{phone} + #{email} + + + + + any + + + + + XML + + assert_no_difference 'Contact.count' do + post epp_create_path, params: { frame: request_xml }, + headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + end + + assert_epp_response :parameter_value_syntax_error + end + def test_respects_custom_code name = 'new' code = 'custom-id' diff --git a/test/models/contact_test.rb b/test/models/contact_test.rb index 8847c8c49..f71546088 100644 --- a/test/models/contact_test.rb +++ b/test/models/contact_test.rb @@ -66,9 +66,25 @@ class ContactTest < ActiveSupport::TestCase contact.email = 'invalid' assert contact.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_smtp_check_error'), contact.errors.messages[:email].first + end - contact.email = 'valid@registrar.test' - assert contact.valid? + def test_email_verification_mx_error + Truemail.configure.default_validation_type = :mx + + contact = valid_contact + contact.email = 'somecrude31337joke@somestrange31337domain.ee' + assert contact.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_mx_check_error'), contact.errors.messages[:email].first + end + + def test_email_verification_regex_error + Truemail.configure.default_validation_type = :regex + + contact = valid_contact + contact.email = 'some@strangesentence@internet.ee' + assert contact.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_regex_check_error'), contact.errors.messages[:email].first end def test_invalid_without_phone diff --git a/test/models/registrar_test.rb b/test/models/registrar_test.rb index c5d832922..2536bbdd8 100644 --- a/test/models/registrar_test.rb +++ b/test/models/registrar_test.rb @@ -43,15 +43,25 @@ class RegistrarTest < ActiveSupport::TestCase registrar.email = 'invalid' assert registrar.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_smtp_check_error'), registrar.errors.messages[:email].first + end - registrar.email = 'valid@email.test' - assert registrar.valid? + def test_email_verification_mx_error + Truemail.configure.default_validation_type = :mx + + registrar = valid_registrar + registrar.email = 'somecrude31337joke@somestrange31337domain.ee' + registrar.billing_email = nil + + assert registrar.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_mx_check_error'), registrar.errors.messages[:email].first end def test_invalid_without_accounting_customer_code registrar = valid_registrar registrar.accounting_customer_code = '' assert registrar.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_regex_check_error'), registrar.errors.messages[:email].first end def test_optional_billing_email @@ -65,8 +75,53 @@ class RegistrarTest < ActiveSupport::TestCase registrar.billing_email = 'invalid' assert registrar.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_smtp_check_error'), registrar.errors.messages[:billing_email].first + end - registrar.billing_email = 'valid@email.test' + def test_billing_email_verification_mx_error + Truemail.configure.default_validation_type = :mx + + registrar = valid_registrar + registrar.billing_email = 'somecrude31337joke@somestrange31337domain.ee' + + assert registrar.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_mx_check_error'), registrar.errors.messages[:billing_email].first + end + + def test_billing_email_verification_regex_error + Truemail.configure.default_validation_type = :regex + + registrar = valid_registrar + registrar.billing_email = 'some@strangesentence@internet.ee' + + assert registrar.invalid? + assert_equal I18n.t('activerecord.errors.models.contact.attributes.email.email_regex_check_error'), registrar.errors.messages[:billing_email].first + end + + def test_creates_email_verification_in_unicode + unicode_email = 'suur@äri.ee' + punycode_email = Registrar.unicode_to_punycode(unicode_email) + unicode_billing_email = 'billing@äri.ee' + punycode_billing_email = Registrar.unicode_to_punycode(unicode_billing_email) + + registrar = valid_registrar + registrar.email = punycode_email + registrar.billing_email = punycode_billing_email + registrar.save + + assert_equal registrar.email_verification.email, unicode_email + assert_equal registrar.billing_email_verification.email, unicode_billing_email + end + + def test_invalid_without_accounting_customer_code + registrar = valid_registrar + registrar.accounting_customer_code = '' + assert registrar.invalid? + end + + def test_optional_billing_email + registrar = valid_registrar + registrar.billing_email = '' assert registrar.valid? end