Rails before_action for ActionMailer that would use mailer arguments
Asked Answered
A

3

16

Suppose I have a mailer that sends different emails, but is expected to be called with the same parameters. I want to process those parameters for all mailer actions. So, calling a before_action that would read the parameters sent to the mailer method

/mailers/my_mailer.rb
class MyMailer < ApplicationMailer
    before_filter do |c|
      # c.prepare_mail # Will fail, because I need to pass `same_param` arguments
      # # I want to send the original arguments
      # c.prepare_mail(same_param) # How do I get `same_param` here ?
    end

    def action1(same_param)
      # email view is going to use @to, @from, @context    
      method_only_specific_to_action1
    end

    def action2(same_param)
      # email view is going to use @to, @from, @context
      method_only_specific_to_action2
    end

    private
      def prepare_mail(same_params)
        @to = same_params.recipient
        @from = same_params.initiator
        @context = same_params.context
      end
    end

Then in my controller/service I do somewhere

MyMailer.actionx(*mailer_params).deliver_now

How can I access the same_param arguments list inside the before_action block ?

EDIT :

I want to refactor from

/mailers/my_mailer.rb
class MyMailer < ApplicationMailer

    def action1(same_param)
      @to = same_params.recipient
      @from = same_params.initiator
      @context = same_params.context   
      method_only_specific_to_action1
    end

    def action2(same_param)
      @to = same_params.recipient
      @from = same_params.initiator
      @context = same_params.context   
      method_only_specific_to_action2
    end

    def actionx
      ... 
    end
  end

And this refactoring

/mailers/my_mailer.rb
class MyMailer < ApplicationMailer

    def action1(same_param)
      prepare_mail(same_params)   
      method_only_specific_to_action1
    end

    def action2(same_param)
      prepare_mail(same_params)   
      method_only_specific_to_action2
    end

    def actionx
      ... 
    end

    private
      def prepare_mail(same_params)
        @to = same_params.recipient
        @from = same_params.initiator
        @context = same_params.context
      end
    end

Feels non-optimal (prepare_mail(same_params) duplicated in every action)

Hence what was suggested above

Antipope answered 20/2, 2015 at 17:19 Comment(10)
just a thought - have you thought of just using net::STMP directly to send emails ruby-doc.org/stdlib-2.0.0/libdoc/net/smtp/rdoc/Net/SMTP.html. It will be more customizable than Action Mailer.Sladen
From a software engineering point of view, ActionMailer is an adapter, and you can configure several email delivery methods (I'm currently using 3-4), I'm afraid SMTP isn't all there is in the world, so I want to use ActionMailer.Antipope
I think you are moving logic part from the controller into the layer. Its best to add a service layer/model class in between the mailer class and controller to achieve this rather than overriding the default mailer to achieve what you want.Carrara
@spickermann Sorry forgot the inheritance, adding it now. You can consider ApplicationMailer is like ActionMailer::Base. In my previous comment I was just explaining I'm not just sending mails directly via SMTP but also configuring to use :file, :cache, :letter_opener, etc delivery adapters which are wrapped nicely by ActionMailerAntipope
@sairam I disagree, you can see in the prepare_mail I am just setting instance variables that will be directly used in an ActionMailer's view. I would have to pass too many parameters otherwiseAntipope
@spickermann No not at all sorry if the question isn't clear, but the previous comment mentionned completely cutting off ActionMailer. I just want to understand if in my before_action filter I can do something like c.prepare_mail(*arguments_sent_to_mailer_action). I just edited, hope it's clearerAntipope
before_action is defined in AbstractController::Callbacks. This method doesn't exist in the context of a Mailer. If you really want to use callbacks in the context of an Mailer than you need to implement the logic to use them. You might want to have a look into ActiveSupport::Callbacks.Muirhead
@Muirhead I'm sorry, but Callbacks in Action Mailer are implemented using AbstractController::Callbacks (at least in Rails 4-5, yes I know it's weird they didn't refactor that name) and before_action DOES work. api.rubyonrails.org/classes/ActionMailer/Base.htmlAntipope
You problem seems to be solved by parameterized mailers that are part of Rails 5.1 (see PR)Muirhead
Hey @Muirhead that sounds cool ! Please add an answer with that when it is released, I'll definitely use it :DAntipope
M
15

ActionMailer uses the AbstractController::Callbacks module. I tried it and it seems to work for me.

The code

class MyMailer < ApplicationMailer
  def process_action(*args)
    # process the args here
    puts args
    super
  end

  def some_mail(*args)
  end
end

MyMailer.some_mail(1, 2) #=> prints ['some_mail', 1, 2]

The documentation


UPDATE

If you're using Rails 5.1, you can have a look at ActionMailer::Parameterized

Mountfort answered 19/1, 2017 at 8:46 Comment(4)
how would you do this using before_action? I tried, but there were no argsFord
I don't know neither. Actually if you're using Rails 5.1, you can have a look at ActionMailer::ParameterizedMountfort
api.rubyonrails.org/v5.1/classes/ActionMailer/… is actually quite nice and probably where things are going, but it actually broke most existing tests and at the time of writing, it is actually not supported very well with RSpec and will involve quite a bit of code rewriting. We've tried to use this in different mailers, but at the time of writing, it wasn't as easy to use as process_actionAntipope
@Ford you could assign this to an instance variable ex @mailer_arguments = args, then in the before action access @mailer_arguments and use them as needed.Meaganmeager
C
1

Solution1:

I would suggest you use this if you do not care about the format

MyMailer.generic("actionx", *mailer_params).deliver_now

def generic(actiontype, *mailer_params)
  # custom logic goes here to construct the to, from, etc.,
  # new_options from custom logic
  self.send(actiontype, new_options)
end

alternative solution below using method_missing from the parent controller

Its not right to put your logic there, but if you still want to do it, you can use the method_missing to put your logic there and skip the action1 and action2 methods.

Original method_missing from action_mailer which can be used as a reference:

def method_missing(method_name, *args)
  if action_methods.include?(method_name.to_s)
    MessageDelivery.new(self, method_name, *args)
  else
    super
  end
end

https://github.com/rails/rails/blob/c8a18aaf44a84f1ef0df007aa3f8446752dc327d/actionmailer/lib/action_mailer/base.rb#L561-L567

Carrara answered 15/1, 2017 at 17:34 Comment(4)
The problem is there's a lot of not-really logic that consists in filling the email headers (and they are the same for every action), but each individual mailer_action remains specific and I might want to do extra stuff in there (like attaching a specific file, etc.)Antipope
see the first solution. self.send(actiontype, options)Carrara
Oh right that actually makes sense ! Sorry didn't realize what it was doing at first.Antipope
@Sairam, did you actually try your first suggestion? It's appealing, but under rails 4.2, I get ActionView::MissingTemplate: Missing template my_mailer/generic with "mailer".Injunction
A
0

Based on Sairam's answer I though of the following but that feels a bit weird, can it not be done with before_action callback ??

class MyMailer < ApplicationMailer

    # Simulation of before_action callback that would support passing the *args to the callback method
    def self.method_missing(method_name, *args)
      method_name = :"#{method_name.to_s}_headers_prefilled"
      if action_methods.include?(method_name)
        mailer = MyMailer.generic(*args) # The before_action callback that passes *args
        mailer.send(method_name, *args) # The action itself
      else
        super
      end
    end

    def generic(*mailer_params)
      # custom logic goes here to construct the headers to, from, etc.,
    end

    def action1_headers_prefilled(mailer_params)
      # Logic only relevant for action1
    end

Also I lose all the cool stuff from before_action (passing an only or except array, etc.)

Antipope answered 15/1, 2017 at 17:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.