Register a Mailer Interceptor in Rails on just a single mailer - ruby-on-rails

I'm trying to add an interceptor to one of several mailers in my Rails application. Even though I'm trying to register it on only a single mailer, it seems to be intercepting the emails from both. Here is some sample code of how I'm trying to register just a single Mailer.
class Mailer1 < ActionMailer::Base; end
class Mailer2 < ActionMailer::Base; end
Mailer1.register_interceptor(MailInterceptor)
Is it possible to follow just a single mailer? Thanks for the help

What you can do is apply the interceptor only on specific Mailers:
class SpecificMailerInterceptor
FILTERED_MAILERS = %w(SpecificMailer)
def self.delivering_email(message)
return if deliver?(message)
message.perform_deliveries = false
end
def self.deliver?(message)
# If you are in the other mailers always deliver message
return true unless FILTERED_MAILERS.include?(message.delivery_handler.to_s)
# Apply interceptor logic here
end
end

Related

Pass extra info to custom action mailer delivery class

I have a 3rd party email integration library that I want to use to send email as one of my users. To send email I make an API call using an access_token that I save for each user.
To still use action mailer, I've created a custom delivery class like this:
module Mail
class CustomMailDelivery
attr_reader :settings
def initialize(settings)
#settings = settings
end
def deliver!(mail)
# use 3rd party client here
end
end
end
I'm configuring this in an initializer:
ActionMailer::Base.add_delivery_method :custom, Mail::CustomMailDelivery, {
app_id: ENV.fetch('3RDPARTY_APP_ID'),
app_secret: ENV.fetch('3RDPARTY_APP_SECRET'),
}
This allows me to set the delivery method on a per-mailer basis:
class LeadMailer < ApplicationMailer
self.delivery_method = :custom
...
end
The problem is, I need to pass the user that is sending this message along, so I can get their access_token.
I don't want to rely on fetching the EmailAccount using the sender's email address because it seems like this could break down the road (it's possible this email address might not be the same as the sending user).
In other words, I'd like to pass it explicitly, so it is easy to understand and I avoid any confusion.
Is there a way to provide per-mail context to a custom action mailer delivery class?
I ended up passing this data with a custom message header, which I later delete when handling the message.
class CustomMailer < ApplicationMailer
self.delivery_method = :custom
attr_reader :sending_account
def mail(params)
raise 'You must call set_sending_account before calling mail.' unless sending_email_account
super(params.merge({
Mail::CustomMailDelivery::ACCOUNT_ID_HEADER => sending_account.id
}))
end
def set_sending_account(account)
#sending_account = account
end
end
This way mailers that need this behavior subclass from this class and are forced to provide the custom data.
In the delivery class I yank this value out of the headers:
module Mail
class CustomMailDelivery
attr_reader :settings
# we'll hijack email headers in order to pass over some required data from the mailer to this class
ACCOUNT_ID_HEADER = '__account_id'
def initialize(settings)
#settings = settings
end
def deliver!(mail)
account = account_for(mail)
client = third_party_api_client(account.access_token)
client.send_message(...)
end
private
def third_party_api_client(access_token)
# ...
end
def account_for(mail)
header_field = mail[ACCOUNT_ID_HEADER]
missing_account_id_header! unless header_field
email_account = Account.find(header_field.value)
# remove the header field so it doesn't show up in the actual email
mail[ACCOUNT_ID_HEADER] = nil
account
end
def missing_account_id_header!
raise "Missing required header: #{ACCOUNT_ID_HEADER}"
end
end
end
This solution is not very elegant, but works.
Thanks for the idea, I put together a shorter version by using register_observer and register_interceptor.
It's basically the same idea, except you don't need to redefine too much delivery stuff. You just hook in the mail workflow.
First, declare the hook:
ActionMailer::Base.register_observer(MailToActionEventObserver)
ActionMailer::Base.register_interceptor(MailToActionEventObserver)
Then, the easy part is that the hooks are static methods inside of the same class:
class MailToActionEventObserver
def self.delivered_email(mail)
# Here you can use #passed_argument because it is called just after
# self.delivering_email
end
def self.delivering_email(mail)
#passed_argument = mail['MY-PERSONAL-HEADER'].to_s
# Now remove the temporary header:
mail['MY-PERSONAL-HEADER'] = nil
end
end
Now, same as your answer #Ben, just need to pass the argument as a header in the mailer:
def my_custom_mail
headers['MY-PERSONAL-HEADER'] = 'whatever-value'
mail(...)
end

Why Rails instance method can be used as class method in rspec

I found a snippet in an article about sending mails in a Rails application:
class ExampleMailerPreview < ActionMailer::Preview
def sample_mail_preview
ExampleMailer.sample_email(User.first)
end
end
in this link: http://www.gotealeaf.com/blog/handling-emails-in-rails.
I do not know why the method: sample_email(), which in my mind should be an instance method, can be accessed like class method here as ExampleMailer.sample_email(). Can anyone explain?
It's not an rspec thing, it's an ActionMailer thing. Looking at:
https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/base.rb
Take a look at the comments in lines 135-146:
# = Sending mail
#
# Once a mailer action and template are defined, you can deliver your message or defer its creation and
# delivery for later:
#
# NotifierMailer.welcome(User.first).deliver_now # sends the email
# mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object
# mail.deliver_now # generates and sends the email now
#
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call
# your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>,
# you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object.
The functionality is implemented by defining a method_missing method on the ActionMailer::Base class that looks like:
def method_missing(method_name, *args) # :nodoc:
if action_methods.include?(method_name.to_s)
MessageDelivery.new(self, method_name, *args)
else
super
end
end
Essentially, defining a method on an ActionMailer instance (NotifierMailer in the comment example) and then calling it on the class creates a new MessageDelivery instance which delegates to a new instance of the ActionMailer class.

Passing params into MailView or ActionMailer::Preview in Ruby on Rails

Is it possible when using the MailView gem or Rails 4.1 mail previews to pass parameters into the MailView? I would love to be able to use query string parameters in the preview URLs and access them in the MailView to dynamically choose which record to show in the preview.
I stumbled upon the same issue and as far as I understand from reading the Rails code it's not possible to access request params from mailer preview.
Crucial is line 22 in Rails::PreviewsController (email is name of the mailer method)
#email = #preview.call(email)
Still a relevant question and still very few solutions to be found on the web (especially elegant ones). I hacked my way through this one today and came up with this solution and blog post on extending ActionMailer.
# config/initializers/mailer_injection.rb
# This allows `request` to be accessed from ActionMailer Previews
# And #request to be accessed from rendered view templates
# Easy to inject any other variables like current_user here as well
module MailerInjection
def inject(hash)
hash.keys.each do |key|
define_method key.to_sym do
eval " ##{key} = hash[key] "
end
end
end
end
class ActionMailer::Preview
extend MailerInjection
end
class ActionMailer::Base
extend MailerInjection
end
class ActionController::Base
before_filter :inject_request
def inject_request
ActionMailer::Preview.inject({ request: request })
ActionMailer::Base.inject({ request: request })
end
end
Since Rails 5.2, mailer previews now have a params attr reader available to use inside your previews.
Injecting requests into your mailers is not ideal as it might lead to thread safety issues and also means your mailers won't work with ActiveJob & co

Rails ActionMailer Message Determine Mailer That Created It

Is there a way for a mail message created by ActionMailer via mail() to determine which mailer method created it? Assume that an interceptor wants to determine which mailer and which mailer method it was sent from and to modify something based on this.
Assume this mailer:
class ApplicationMailer < ActionMailer::Base
def some_mail
mail
end
end
And this interceptor:
class MailInterceptor
def self.delivering_email(message)
# `delivery_handler` exists, and answers question of which Mailer was used
logger.debug "Mailer: #{message.delivery_handler}" # "ApplicationMailer"
# NOTE: `method_name` doesn't exist, it shows the desired functionality
logger.debug "Message Method: #{message.mailer.method_name}" # "some_mail"
case message.delivery_handler
when ApplicationMailer
message.bcc.append "bccmetoo#test.com" if message.mailer.method_name == "some_mail"
when OtherMailer
message.bcc.append "bccthisperson#foo.com"
end
end
end
Are there such methods, or some way of determining this?
UPDATE
Changed the question to reflect how the mailer can be determined by the existing delivery_handler method, as answered in the question of which this question was marked a duplicate, but that the exact mailer method used is still unknown.

Is it good practice to send in multiple mail methods in an ActionMailer Model action, or should that logic be kept outside of the Mailer action?

Which would be the preferred?
class Mailer < ActionMailer::Base
# this?
def bid_end_notify_failed_bidders(job)
#job = job
bidders = #job.bidders
bidders.delete(#job.winner)
bidders.each do |t|
mail(:to => t.email, ....)
end
end
# or this?
def notify_failed_bid(bidder)
mail(:to => bidder ...)
end
end
I suggest method #2, this is because you want to be agile in the way you send the email.
The most regular way of using it is:
declare an instance
declare any relations you need from the instance
pass the instances to the Mailer::Base method
Deliver
Example:
user = User.first
bets_won = user.bets_won
Mailer.send_congrats(users, bets_won).deliver!
Now it will only use the data it's being served so no logic has to be done by the Mailer class
also this approach will allow you to reuse the method easily.

Resources