Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto ForceDelete on email bounces/verification fails #1874

3 changes: 3 additions & 0 deletions app/interactions/domains/force_delete/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class Base < ActiveInteraction::Base
string :reason,
default: nil,
description: 'Which mail template to use explicitly'
string :email,
default: nil,
description: 'Possible invalid email to notify on'

validates :type, inclusion: { in: %i[fast_track soft] }
end
Expand Down
12 changes: 12 additions & 0 deletions app/interactions/domains/force_delete/notify_registrar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@ module Domains
module ForceDelete
class NotifyRegistrar < Base
def execute
email.present? ? notify_with_email : notify_without_email
end

def notify_without_email
domain.registrar.notifications.create!(text: I18n.t('force_delete_set_on_domain',
domain_name: domain.name,
outzone_date: domain.outzone_date,
purge_date: domain.purge_date))
end

def notify_with_email
domain.registrar.notifications.create!(text: I18n.t('force_delete_auto_email',
domain_name: domain.name,
outzone_date: domain.outzone_date,
purge_date: domain.purge_date,
email: email))
end
end
end
end
27 changes: 27 additions & 0 deletions app/interactions/domains/force_delete_email/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Domains
module ForceDeleteEmail
class Base < ActiveInteraction::Base
string :email,
description: 'Bounced email to set ForceDelete from'

def execute
domain_contacts = Contact.where(email: email).map(&:domain_contacts).flatten
registrant_ids = Registrant.where(email: email).pluck(:id)

domains = domain_contacts.map(&:domain).flatten +
Domain.where(registrant_id: registrant_ids)

domains.each { |domain| process_force_delete(domain) unless domain.force_delete_scheduled? }
end

private

def process_force_delete(domain)
domain.schedule_force_delete(type: :soft,
notify_by_email: true,
reason: 'invalid_email',
email: email)
end
end
end
end
9 changes: 7 additions & 2 deletions app/mailers/domain_expire_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ def registrar_presenter(registrar:)

# Needed because there are invalid emails in the database, which have been imported from legacy app
def filter_invalid_emails(emails:, domain:)
emails.select do |email|
valid = EmailValidator.new(email).valid?
old_validation_type = Truemail.configure.default_validation_type
Truemail.configure.default_validation_type = :regex

results = emails.select do |email|
valid = Truemail.valid?(email)

unless valid
logger.info("Unable to send DomainExpireMailer#expired email for domain #{domain.name} (##{domain.id})" \
Expand All @@ -53,5 +56,7 @@ def filter_invalid_emails(emails:, domain:)

valid
end
Truemail.configure.default_validation_type = old_validation_type
results
end
end
5 changes: 5 additions & 0 deletions app/models/bounced_mail_address.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class BouncedMailAddress < ApplicationRecord
validates :email, :message_id, :bounce_type, :bounce_subtype, :action, :status, presence: true
after_destroy :destroy_aws_suppression
after_create :force_delete_from_bounce

def bounce_reason
"#{action} (#{status} #{diagnostic})"
Expand Down Expand Up @@ -42,4 +43,8 @@ def self.ses_configured?
rescue Aws::Errors::MissingRegionError
false
end

def force_delete_from_bounce
Domains::ForceDeleteEmail::Base.run(email: email)
end
end
4 changes: 2 additions & 2 deletions app/models/concerns/domain/force_delete.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ def force_delete_scheduled?
statuses.include?(DomainStatus::FORCE_DELETE)
end

def schedule_force_delete(type: :fast_track, notify_by_email: false, reason: nil)
def schedule_force_delete(type: :fast_track, notify_by_email: false, reason: nil, email: nil)
Domains::ForceDelete::SetForceDelete.run(domain: self, type: type, reason: reason,
notify_by_email: notify_by_email)
notify_by_email: notify_by_email, email: email)
end

def cancel_force_delete
Expand Down
7 changes: 7 additions & 0 deletions app/models/email_address_verification.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class EmailAddressVerification < ApplicationRecord
RECENTLY_VERIFIED_PERIOD = 1.month
after_save :check_force_delete

scope :not_verified_recently, lambda {
where('verified_at IS NULL or verified_at < ?', verification_period)
Expand Down Expand Up @@ -40,6 +41,12 @@ def verified?
success
end

def check_force_delete
return unless failed?

Domains::ForceDeleteEmail::Base.run(email: email)
end

def verify
validation_request = Truemail.validate(email)

Expand Down
2 changes: 1 addition & 1 deletion app/presenters/domain_presenter.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class DomainPresenter
delegate :name, :transfer_code, :registrant, :registrant_id, to: :domain
delegate :name, :transfer_code, :registrant, :registrant_id, :id, to: :domain

def initialize(domain:, view:)
@domain = domain
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ en:
created_at_until: 'Created at until'
is_registrant: 'Is registrant'
force_delete_set_on_domain: 'Force delete set on domain %{domain_name}. Outzone date: %{outzone_date}. Purge date: %{purge_date}'
force_delete_auto_email: 'Force delete set on domain %{domain_name}. Outzone date: %{outzone_date}. Purge date: %{purge_date}. Invalid email: %{email}'
grace_period_started_domain: 'For domain %{domain_name} started 45-days redemption grace period, ForceDelete will be in effect from %{date}'
force_delete_cancelled: 'Force delete is cancelled on domain %{domain_name}'
contact_is_not_valid: 'Contact %{value} is not valid, please fix the invalid contact'
Expand Down
14 changes: 14 additions & 0 deletions lib/tasks/email_bounce_test.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace :email_bounce do
desc 'Creates a dummy email bounce by email address'
task :create_test, [:email] => [:environment] do |_t, args|
bounced_mail = BouncedMailAddress.new
bounced_mail.email = args[:email]
bounced_mail.message_id = '010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000'
bounced_mail.bounce_type = 'Permanent'
bounced_mail.bounce_subtype = 'General'
bounced_mail.action = 'failed'
bounced_mail.status = '5.1.1'
bounced_mail.diagnostic = 'smtp; 550 5.1.1 user unknown'
bounced_mail.save!
end
end
15 changes: 0 additions & 15 deletions lib/validators/email_validator.rb

This file was deleted.

25 changes: 25 additions & 0 deletions test/mailers/domain_expire_mailer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,29 @@ def test_delivers_domain_expiration_soft_email
assert_equal I18n.t("domain_expire_mailer.expired_soft.subject", domain_name: domain.name),
email.subject
end

def test_delivers_domain_expiration_soft_email_if_auto_fd
domain = domains(:shop)
assert_not domain.force_delete_scheduled?
travel_to Time.zone.parse('2010-07-05')
email = 'some@[email protected]'

Truemail.configure.default_validation_type = :regex

contact = domain.admin_contacts.first
contact.update_attribute(:email, email)
contact.email_verification.verify

assert contact.email_verification_failed?

domain.reload

assert domain.force_delete_scheduled?

email = DomainExpireMailer.expired_soft(domain: domain, registrar: domain.registrar).deliver_now

assert_emails 1
assert_equal I18n.t("domain_expire_mailer.expired_soft.subject", domain_name: domain.name),
email.subject
end
end
46 changes: 46 additions & 0 deletions test/models/bounced_mail_address_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,52 @@ def setup
@bounced_mail.action = 'failed'
@bounced_mail.status = '5.1.1'
@bounced_mail.diagnostic = 'smtp; 550 5.1.1 user unknown'

@contact_email = "[email protected]"
end

def test_soft_force_delete_related_domains
domain_contacts = Contact.where(email: @contact_email).map(&:domain_contacts).flatten

domain_contacts.each do |domain_contact|
domain_contact.domain.update(valid_to: Time.zone.now + 5.years)
assert_not domain_contact.domain.statuses.include? DomainStatus::FORCE_DELETE
assert_not domain_contact.domain.statuses.include? DomainStatus::SERVER_RENEW_PROHIBITED
assert_not domain_contact.domain.statuses.include? DomainStatus::SERVER_TRANSFER_PROHIBITED
end

@bounced_mail.email = @contact_email
@bounced_mail.save

domain_contacts.each do |domain_contact|
domain_contact.reload
assert_equal 'soft', domain_contact.domain.force_delete_type
assert domain_contact.domain.force_delete_scheduled?
assert domain_contact.domain.statuses.include? DomainStatus::FORCE_DELETE
assert domain_contact.domain.statuses.include? DomainStatus::SERVER_RENEW_PROHIBITED
assert domain_contact.domain.statuses.include? DomainStatus::SERVER_TRANSFER_PROHIBITED
end
end

def test_soft_force_delete_if_domain_has_force_delete_status
domain_contacts = Contact.where(email: @contact_email).map(&:domain_contacts).flatten
perform_enqueued_jobs do
domain_contacts.each do |domain_contact|
domain_contact.domain.update(valid_to: Time.zone.now + 5.years)
domain_contact.domain.schedule_force_delete(type: :soft, notify_by_email: false, reason: 'test')
end
end
force_delete_date = domain_contacts.map(&:domain).each.pluck(:force_delete_date).sample
assert_not_nil force_delete_date

@bounced_mail.email = @contact_email
@bounced_mail.save

domain_contacts.all? do |domain_contact|
assert_equal force_delete_date, domain_contact.domain.force_delete_date
assert_equal 'soft', domain_contact.domain.force_delete_type
assert domain_contact.domain.force_delete_scheduled?
end
end

def test_email_is_required
Expand Down
80 changes: 80 additions & 0 deletions test/models/domain/force_delete_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ class ForceDeleteTest < ActionMailer::TestCase
@domain = domains(:shop)
Setting.redemption_grace_period = 30
ActionMailer::Base.deliveries.clear
@old_validation_type = Truemail.configure.default_validation_type
end

teardown do
Truemail.configure.default_validation_type = @old_validation_type
end

def test_schedules_force_delete_fast_track
Expand Down Expand Up @@ -315,4 +320,79 @@ def test_force_delete_does_not_affect_registrant_update_confirmable
assert @domain.force_delete_scheduled?
assert @domain.registrant_update_confirmable?(@domain.registrant_verification_token)
end

def test_schedules_force_delete_after_bounce
@domain.update(valid_to: Time.zone.parse('2012-08-05'))
assert_not @domain.force_delete_scheduled?
travel_to Time.zone.parse('2010-07-05')
email = @domain.admin_contacts.first.email
asserted_text = "Invalid email: #{email}"

prepare_bounced_email_address(email)

@domain.reload

assert @domain.force_delete_scheduled?
assert_equal 'invalid_email', @domain.template_name
assert_equal Date.parse('2010-09-19'), @domain.force_delete_date.to_date
assert_equal Date.parse('2010-08-05'), @domain.force_delete_start.to_date
notification = @domain.registrar.notifications.last
assert notification.text.include? asserted_text
end

def test_schedules_force_delete_after_registrant_bounce
@domain.update(valid_to: Time.zone.parse('2012-08-05'))
assert_not @domain.force_delete_scheduled?
travel_to Time.zone.parse('2010-07-05')
email = @domain.registrant.email
asserted_text = "Invalid email: #{email}"

prepare_bounced_email_address(email)

@domain.reload

assert @domain.force_delete_scheduled?
assert_equal 'invalid_email', @domain.template_name
assert_equal Date.parse('2010-09-19'), @domain.force_delete_date.to_date
assert_equal Date.parse('2010-08-05'), @domain.force_delete_start.to_date
notification = @domain.registrar.notifications.last
assert notification.text.include? asserted_text
end

def test_schedules_force_delete_invalid_contact
@domain.update(valid_to: Time.zone.parse('2012-08-05'))
assert_not @domain.force_delete_scheduled?
travel_to Time.zone.parse('2010-07-05')
email = 'some@[email protected]'
asserted_text = "Invalid email: #{email}"

Truemail.configure.default_validation_type = :regex

contact = @domain.admin_contacts.first
contact.update_attribute(:email, email)
contact.email_verification.verify

assert contact.email_verification_failed?

@domain.reload

assert @domain.force_delete_scheduled?
assert_equal 'invalid_email', @domain.template_name
assert_equal Date.parse('2010-09-19'), @domain.force_delete_date.to_date
assert_equal Date.parse('2010-08-05'), @domain.force_delete_start.to_date
notification = @domain.registrar.notifications.last
assert notification.text.include? asserted_text
end

def prepare_bounced_email_address(email)
@bounced_mail = BouncedMailAddress.new
@bounced_mail.email = email
@bounced_mail.message_id = '010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000'
@bounced_mail.bounce_type = 'Permanent'
@bounced_mail.bounce_subtype = 'General'
@bounced_mail.action = 'failed'
@bounced_mail.status = '5.1.1'
@bounced_mail.diagnostic = 'smtp; 550 5.1.1 user unknown'
@bounced_mail.save!
end
end