config: simplify mailer configuration again

Move everything to initializers, and replace the email settings
interceptor by a BalancerDeliveryMethod.

It has the advantage that it can be configured entirely from the
`config/environment.rb` file, without an extra file to look at.
This commit is contained in:
Pierre de La Morinerie 2022-01-26 10:33:12 +00:00
parent 27b42fe8ae
commit 847abca122
11 changed files with 205 additions and 109 deletions

View file

@ -0,0 +1,38 @@
# A Mail delivery method that randomly balances the actual delivery between different
# methods.
#
# Usage:
#
# ```ruby
# ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
# config.action_mailer.balancer_settings = {
# smtp: 25,
# sendmail: 75
# }
# config.action_mailer.delivery_method = :balancer
# ```
#
# Be sure to restart your server when you modify this file.
class BalancerDeliveryMethod
# Allows configuring the random number generator used for selecting a delivery method,
# mostly for testing purposes.
mattr_accessor :random, default: Random.new
def initialize(settings)
@delivery_methods = settings
end
def deliver!(mail)
balanced_delivery_method = delivery_method(mail)
ApplicationMailer.wrap_delivery_behavior(mail, balanced_delivery_method)
mail.deliver
end
private
def delivery_method(mail)
@delivery_methods
.flat_map { |delivery_method, weight| [delivery_method] * weight }
.sample(random: self.class.random)
end
end

View file

@ -1,4 +0,0 @@
module Mailtrap
class Smtp < ::Mail::SMTP
end
end

View file

@ -1,4 +0,0 @@
module Sendinblue
class Smtp < ::Mail::SMTP
end
end

View file

@ -75,40 +75,13 @@ Rails.application.configure do
config.assets.raise_runtime_errors = true config.assets.raise_runtime_errors = true
# Action Mailer settings # Action Mailer settings
config.action_mailer.delivery_method = :letter_opener
if ENV['SENDINBLUE_ENABLED'] == 'enabled'
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
user_name: Rails.application.secrets.sendinblue[:username],
password: Rails.application.secrets.sendinblue[:smtp_key],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
else
# https://usehelo.com
if ENV['HELO_ENABLED'] == 'enabled'
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
user_name: 'demarches-simplifiees',
password: '',
address: '127.0.0.1',
domain: '127.0.0.1',
port: ENV.fetch('HELO_PORT', '2525'),
authentication: :plain
}
else
config.action_mailer.delivery_method = :letter_opener_web
end
config.action_mailer.default_url_options = { config.action_mailer.default_url_options = {
host: 'localhost', host: 'localhost',
port: 3000 port: 3000
} }
config.action_mailer.asset_host = "http://" + ENV['APP_HOST'] config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
end
Rails.application.routes.default_url_options = { Rails.application.routes.default_url_options = {
host: 'localhost', host: 'localhost',

View file

@ -1,6 +1,5 @@
require "active_support/core_ext/integer/time" require "active_support/core_ext/integer/time"
require Rails.root.join("app/lib/mailtrap/smtp") require Rails.root.join("app/lib/balancer_delivery_method")
require Rails.root.join("app/lib/sendinblue/smtp")
Rails.application.configure do Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb. # Settings specified here will take precedence over those in config/application.rb.
@ -78,31 +77,20 @@ Rails.application.configure do
# config.action_mailer.raise_delivery_errors = false # config.action_mailer.raise_delivery_errors = false
if ENV['MAILTRAP_ENABLED'] == 'enabled' if ENV['MAILTRAP_ENABLED'] == 'enabled'
ActionMailer::Base.add_delivery_method :mailtrap, Mailtrap::Smtp
config.action_mailer.mailtrap_settings = {
user_name: Rails.application.secrets.mailtrap[:username],
password: Rails.application.secrets.mailtrap[:password],
address: 'smtp.mailtrap.io',
domain: 'smtp.mailtrap.io',
port: '2525',
authentication: :cram_md5
}
config.action_mailer.delivery_method = :mailtrap config.action_mailer.delivery_method = :mailtrap
elsif
if ENV['SENDINBLUE_ENABLED'] == 'enabled'
ActionMailer::Base.add_delivery_method :sendinblue, Sendinblue::Smtp
config.action_mailer.sendinblue_settings = {
user_name: Rails.application.secrets.sendinblue[:username],
password: Rails.application.secrets.sendinblue[:smtp_key],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
end
# Default delivery method elsif ENV['SENDINBLUE_ENABLED'] == 'enabled' && ENV['SENDINBLUE_BALANCING'] == 'enabled'
# (Actual delivery method will be selected at runtime by DynamicSmtpSettingsInterceptor) ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
config.action_mailer.balancer_settings = {
sendinblue: ENV.fetch('SENDINBLUE_BALANCING_VALUE').to_i,
mailjet: 100 - ENV.fetch('SENDINBLUE_BALANCING_VALUE').to_i
}
config.action_mailer.delivery_method = :balancer
elsif ENV['SENDINBLUE_ENABLED'] == 'enabled'
config.action_mailer.delivery_method = :sendinblue
else
config.action_mailer.delivery_method = :mailjet config.action_mailer.delivery_method = :mailjet
end end

View file

@ -1,14 +0,0 @@
# We want to register an interceptor, but we can't make the action idempotent
# (because there's no way to peek at the currently registered interceptors).
#
# To make zeitwerk happy, instead signal that we don't want the
# DynamicSmtpSettingsInterceptor constant to be auto-loaded, by:
# - adding it to a non-autoloaded-path (/lib),
# - requiring it explicitely.
#
# See https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-when-the-application-boots
require 'action_mailer/dynamic_smtp_settings_interceptor'
ActiveSupport.on_load(:action_mailer) do
ActionMailer::Base.register_interceptor DynamicSmtpSettingsInterceptor
end

View file

@ -0,0 +1,17 @@
if ENV['HELO_ENABLED'] == 'enabled'
ActiveSupport.on_load(:action_mailer) do
module Helo
class SMTP < ::Mail::SMTP; end
end
ActionMailer::Base.add_delivery_method :helo, Helo::SMTP
ActionMailer::Base.helo_settings = {
user_name: 'demarches-simplifiees',
password: '',
address: '127.0.0.1',
domain: '127.0.0.1',
port: ENV.fetch('HELO_PORT', '2525'),
authentication: :plain
}
end
end

View file

@ -0,0 +1,17 @@
if ENV.fetch('MAILTRAP_ENABLED') == 'enabled'
ActiveSupport.on_load(:action_mailer) do
module Mailtrap
class SMTP < ::Mail::SMTP; end
end
ActionMailer::Base.add_delivery_method :mailtrap, Mailtrap::SMTP
ActionMailer::Base.mailtrap_settings = {
user_name: Rails.application.secrets.mailtrap[:username],
password: Rails.application.secrets.mailtrap[:password],
address: 'smtp.mailtrap.io',
domain: 'smtp.mailtrap.io',
port: '2525',
authentication: :cram_md5
}
end
end

View file

@ -1,5 +1,23 @@
if ENV.fetch('SENDINBLUE_ENABLED') == 'enabled'
require 'sib-api-v3-sdk' require 'sib-api-v3-sdk'
SibApiV3Sdk.configure do |config| ActiveSupport.on_load(:action_mailer) do
config.api_key['api-key'] = ENV.fetch('SENDINBLUE_API_V3_KEY', '') module Sendinblue
class SMTP < ::Mail::SMTP; end
end
ActionMailer::Base.add_delivery_method :sendinblue, Sendinblue::SMTP
ActionMailer::Base.sendinblue_settings = {
user_name: Rails.application.secrets.sendinblue[:username],
password: Rails.application.secrets.sendinblue[:smtp_key],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
end
SibApiV3Sdk.configure do |config|
config.api_key['api-key'] = Rails.application.secrets.sendinblue[:api_v3_key]
end
end end

View file

@ -1,26 +0,0 @@
# Note: this class is instanciated when being added as an interceptor
# during the app initialization.
#
# If you edit this file in development env, you will need to restart
# the app to see the changes.
class DynamicSmtpSettingsInterceptor
def self.delivering_email(message)
if balance_to_sendinblue?
ApplicationMailer.wrap_delivery_behavior(message, :sendinblue)
end
# fallback to the default delivery method
end
private
def self.balance_to_sendinblue?
if ENV.fetch('SENDINBLUE_ENABLED') != 'enabled'
false
elsif ENV.fetch('SENDINBLUE_BALANCING') == 'enabled'
rand(0..99) < ENV.fetch('SENDINBLUE_BALANCING_VALUE').to_i
else
true
end
end
end

View file

@ -0,0 +1,93 @@
RSpec.describe BalancerDeliveryMethod do
class ExampleMailer < ApplicationMailer
def greet(name)
mail(to: "smtp_to", from: "smtp_from", body: "Hello #{name}")
end
end
class TestMail
def self.deliveries
@deliveries ||= []
end
def self.deliveries=(val)
@deliveries = val
end
attr_accessor :settings
def initialize(values)
@settings = values.dup
end
def deliver!(mail)
Mail::CheckDeliveryParams.check(mail)
self.class.deliveries << mail
end
end
class MockSmtp < TestMail; end
class MockSendmail < TestMail; end
class FixedSequence
def initialize(sequence)
@enumerator = sequence.each
end
def rand(_)
@enumerator.next
end
end
before do
ActionMailer::Base.add_delivery_method :mock_smtp, MockSmtp
ActionMailer::Base.add_delivery_method :mock_sendmail, MockSendmail
ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
ExampleMailer.delivery_method = :balancer
end
context 'when a single delivery method is provided' do
before do
ActionMailer::Base.balancer_settings = { mock_smtp: 10 }
end
it 'sends emails to the selected delivery method' do
mail = ExampleMailer.greet('Joshua').deliver_now
expect(mail).to have_been_delivered_using(MockSmtp)
end
end
context 'when multiple delivery methods are provided' do
before do
ActionMailer::Base.balancer_settings = { mock_smtp: 10, mock_sendmail: 5 }
rng_sequence = [3, 14, 1]
BalancerDeliveryMethod.random = FixedSequence.new(rng_sequence)
end
after do
BalancerDeliveryMethod.random = Random.new
end
it 'sends emails randomly, given the provided weights' do
mail1 = ExampleMailer.greet('Lucia').deliver_now
expect(mail1).to have_been_delivered_using(MockSmtp)
mail2 = ExampleMailer.greet('Damian').deliver_now
expect(mail2).to have_been_delivered_using(MockSendmail)
mail3 = ExampleMailer.greet('Rahwa').deliver_now
expect(mail3).to have_been_delivered_using(MockSmtp)
end
end
# Helpers
def have_been_delivered_using(delivery_class)
satisfy("have been delivered using #{delivery_class}") do |mail|
delivery_class.deliveries.include?(mail)
end
end
end