Merge pull request #6821 from betagouv/improve-email-balancing

Amélioration de la configuration des fournisseurs d'email
This commit is contained in:
Pierre de La Morinerie 2022-02-08 12:49:09 +01:00 committed by GitHub
commit a3a2bab89b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 220 additions and 100 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

@ -70,28 +70,33 @@ MATOMO_ENABLED="disabled"
MATOMO_ID=""
MATOMO_HOST="matomo.organisme.fr"
# SMTP Provider: Send In Blue
# Default SMTP Provider: Mailjet
MAILJET_API_KEY=""
MAILJET_SECRET_KEY=""
# Alternate SMTP Provider: SendInBlue
SENDINBLUE_ENABLED="disabled"
SENDINBLUE_BALANCING=""
SENDINBLUE_BALANCING_VALUE=""
SENDINBLUE_CLIENT_KEY=""
SENDINBLUE_SMTP_KEY=""
SENDINBLUE_USER_NAME=""
# SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc"
# SMTP Provider: Mailjet
MAILJET_API_KEY=""
MAILJET_SECRET_KEY=""
# Ratio of emails sent using SendInBlue
# When enabled, N % of emails will be sent using SendInBlue
# (and the others using the default SMTP provider)
SENDINBLUE_BALANCING="disabled"
SENDINBLUE_BALANCING_VALUE="50"
# Alternate SMTP Provider: Mailtrap (mail catcher for staging environments)
# When enabled, all emails will be sent using this provided
MAILTRAP_ENABLED="disabled"
MAILTRAP_USERNAME=""
MAILTRAP_PASSWORD=""
# External service: live chat for admins (specific to démarches-simplifiées.fr)
CRISP_ENABLED="disabled"
CRISP_CLIENT_KEY=""
# External service: mail catcher for staging environments (specific to démarches-simplifiées.fr)
MAILTRAP_ENABLED="disabled"
MAILTRAP_USERNAME=""
MAILTRAP_PASSWORD=""
# API Entreprise credentials
# https://api.gouv.fr/api/api-entreprise.html
API_ENTREPRISE_KEY=""

View file

@ -75,40 +75,13 @@ Rails.application.configure do
config.assets.raise_runtime_errors = true
# 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 = {
host: 'localhost',
port: 3000
}
config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
end
config.action_mailer.default_url_options = {
host: 'localhost',
port: 3000
}
config.action_mailer.asset_host = "http://" + ENV['APP_HOST']
Rails.application.routes.default_url_options = {
host: 'localhost',

View file

@ -1,4 +1,5 @@
require "active_support/core_ext/integer/time"
require Rails.root.join("app/lib/balancer_delivery_method")
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@ -76,25 +77,19 @@ Rails.application.configure do
# config.action_mailer.raise_delivery_errors = false
if ENV['MAILTRAP_ENABLED'] == 'enabled'
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_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
elsif ENV['SENDINBLUE_ENABLED'] == 'enabled' && ENV['SENDINBLUE_BALANCING'] == 'enabled'
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 = :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
}
config.action_mailer.delivery_method = :sendinblue
else
config.action_mailer.delivery_method = :mailjet
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 @@
require 'sib-api-v3-sdk'
if ENV.fetch('SENDINBLUE_ENABLED') == 'enabled'
require 'sib-api-v3-sdk'
SibApiV3Sdk.configure do |config|
config.api_key['api-key'] = ENV.fetch('SENDINBLUE_API_V3_KEY', '')
ActiveSupport.on_load(:action_mailer) do
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

View file

@ -1,22 +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 ENV['SENDINBLUE_BALANCING'] == 'enabled'
if rand(0..99) < ENV['SENDINBLUE_BALANCING_VALUE'].to_i
message.delivery_method.settings = {
user_name: ENV['SENDINBLUE_USER_NAME'],
password: ENV['SENDINBLUE_SMTP_KEY'],
address: 'smtp-relay.sendinblue.com',
domain: 'smtp-relay.sendinblue.com',
port: '587',
authentication: :cram_md5
}
end
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