Skip to content

Commit 83220e8

Browse files
committed
move scheduler, add bounced email check
1 parent 91d4471 commit 83220e8

9 files changed

Lines changed: 138 additions & 45 deletions

File tree

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ gem 'sprockets-rails'
6666
gem 'uuid', '~> 2.3', '>= 2.3.9'
6767
gem 'uuidtools', '~> 2.1', '>= 2.1.5'
6868
gem 'vuejs-rails', '~> 2.3.2'
69-
gem 'whenever'
69+
gem 'rufus-scheduler', '~> 3.9'
7070
gem "altcha", "~> 1.0"
7171

7272
group :development do

app/mailers/bounce_interceptor.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
class BounceInterceptor
4+
def self.delivering_email(message)
5+
return unless defined?(BouncedEmail)
6+
7+
original_to = Array(message.to)
8+
filtered_to = original_to.reject { |email| BouncedEmail.bounced?(email) }
9+
10+
if filtered_to.empty?
11+
message.perform_deliveries = false
12+
Rails.logger.info("[BounceInterceptor] Blocked email to bounced address(es): #{original_to.join(', ')}")
13+
elsif filtered_to.size < original_to.size
14+
blocked = original_to - filtered_to
15+
Rails.logger.info("[BounceInterceptor] Removed bounced address(es) from recipients: #{blocked.join(', ')}")
16+
message.to = filtered_to
17+
end
18+
end
19+
end

app/models/bounced_email.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class BouncedEmail < ApplicationRecord
2+
validates :email, presence: true, uniqueness: { case_sensitive: false }
3+
4+
PERMANENTLY_BLOCKED = %w[deactivated_account@tosdr.org].freeze
5+
6+
def self.bounced?(email)
7+
return true if PERMANENTLY_BLOCKED.include?(email.downcase)
8+
9+
where('lower(email) = ?', email.downcase).exists?
10+
end
11+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
Rails.application.config.after_initialize do
4+
ActionMailer::Base.register_interceptor(BounceInterceptor)
5+
end

config/initializers/scheduler.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
# In-process scheduler using rufus-scheduler.
4+
# Replaces system cron (which doesn't work reliably in Docker).
5+
# All times are UTC.
6+
7+
require 'rufus-scheduler'
8+
9+
return unless defined?(Rails::Server) || ENV['RUN_SCHEDULER'] == 'true'
10+
11+
scheduler = Rufus::Scheduler.singleton
12+
13+
# service:perform_rating — Mon, Wed, Fri at 3:00 AM
14+
scheduler.cron '0 3 * * 1,3,5' do
15+
Rails.logger.info('[Scheduler] Running service:perform_rating')
16+
Rake::Task['service:perform_rating'].reenable
17+
Rake::Task['service:perform_rating'].invoke
18+
end
19+
20+
# spam:clean_spam — Tue, Thu at 3:00 AM
21+
scheduler.cron '0 3 * * 2,4' do
22+
Rails.logger.info('[Scheduler] Running spam:clean_spam')
23+
Rake::Task['spam:clean_spam'].reenable
24+
Rake::Task['spam:clean_spam'].invoke
25+
end
26+
27+
# bounced_emails:fetch — Daily at 4:00 AM
28+
scheduler.cron '0 4 * * *' do
29+
Rails.logger.info('[Scheduler] Running bounced_emails:fetch')
30+
Rake::Task['bounced_emails:fetch'].reenable
31+
Rake::Task['bounced_emails:fetch'].invoke
32+
end

config/schedule.rb

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,8 @@
1-
# Use this file to easily define all of your cron jobs.
2-
#
3-
# It's helpful, but not entirely necessary to understand cron before proceeding.
4-
# http://en.wikipedia.org/wiki/Cron
5-
6-
# Example:
7-
#
8-
# set :output, "/path/to/my/cron_log.log"
9-
#
10-
# every 2.hours do
11-
# command "/usr/bin/some_great_command"
12-
# runner "MyModel.some_method"
13-
# rake "some:great:rake:task"
14-
# end
15-
#
16-
# every 4.days do
17-
# runner "AnotherModel.prune_old_records"
18-
# end
19-
20-
# Learn more: http://github.com/javan/whenever
21-
22-
every :monday, at: '3:00 am' do
23-
rake 'service:perform_rating'
24-
end
25-
26-
every :wednesday, at: '3:00 am' do
27-
rake 'service:perform_rating'
28-
end
29-
30-
every :friday, at: '3:00 am' do
31-
rake 'service:perform_rating'
32-
end
33-
34-
every :tuesday, at: '3:00 am' do
35-
rake 'spam:clean_spam'
36-
end
37-
38-
every :thursday, at: '3:00 am' do
39-
rake 'spam:clean_spam'
40-
end
1+
# DEPRECATED: This file was used by the `whenever` gem for system cron.
2+
# Scheduled tasks are now managed by rufus-scheduler in
3+
# config/initializers/scheduler.rb (runs in-process, Docker-friendly).
4+
#
5+
# Schedule overview:
6+
# Mon, Wed, Fri at 3:00 AM — service:perform_rating
7+
# Tue, Thu at 3:00 AM — spam:clean_spam
8+
# Daily at 4:00 AM — bounced_emails:fetch
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class CreateBouncedEmails < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :bounced_emails do |t|
4+
t.string :email, null: false
5+
t.string :bounce_type
6+
t.datetime :bounced_at
7+
t.timestamps
8+
end
9+
10+
add_index :bounced_emails, :email, unique: true
11+
end
12+
end

docker-entrypoint.sh

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ if [ -v INIT_SETUP ]; then
1616
bundle exec rake db:seed
1717
fi
1818

19-
# Start cron in the background
20-
service cron start
21-
whenever --update-crontab
22-
2319
# Starting the Rails server
2420
# Option 1: Using puma
2521
exec bundle exec puma -C config/puma.rb

lib/tasks/bounced_emails.rake

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
namespace :bounced_emails do
4+
desc 'Fetch bounced emails from Mailtrap and store them'
5+
task fetch: :environment do
6+
account_id = ENV['MAILTRAP_ACCOUNT_ID']
7+
api_token = ENV['SMTP_PASSWORD']
8+
9+
if account_id.blank?
10+
puts 'MAILTRAP_ACCOUNT_ID not set, skipping bounce fetch.'
11+
next
12+
end
13+
14+
if api_token.blank?
15+
puts 'SMTP_PASSWORD not set, skipping bounce fetch.'
16+
next
17+
end
18+
19+
url = "https://mailtrap.io/api/accounts/#{account_id}/email_logs"
20+
params = {
21+
'filters[events][operator]' => 'include_event',
22+
'filters[events][value][]' => %w[bounce soft_bounce]
23+
}
24+
25+
response = HTTParty.get(url, query: params, headers: { 'Api-Token' => api_token })
26+
27+
unless response.success?
28+
puts "Failed to fetch bounced emails: #{response.code} #{response.message}"
29+
next
30+
end
31+
32+
messages = response.parsed_response['messages'] || []
33+
count = 0
34+
35+
messages.each do |msg|
36+
email = msg['to']&.downcase
37+
next if email.blank?
38+
39+
bounced = BouncedEmail.find_or_initialize_by(email: email)
40+
bounced.bounce_type ||= 'bounce'
41+
bounced.bounced_at ||= msg['sent_at']
42+
if bounced.new_record?
43+
bounced.save!
44+
count += 1
45+
end
46+
end
47+
48+
puts "Fetched #{messages.size} bounced messages, added #{count} new bounced emails."
49+
end
50+
end

0 commit comments

Comments
 (0)