ActionMailer Callbacks: In the Spirit of ActionController Filters
One of the most useful features of
ActionController is the ability to add filters before, after, or around actions. This tool is made even more powerful by the ability to chain filters together. Allowing an AOP approach is indispensable for addressing cross cutting concerns (or simply separating concerns), and is one of the things which makes a framework valuable to a developer (Spring is an excellent example of this).
A while back, I had a requirement to persist a record of which email addresses were sent an email through the system. I expected to find callback support for
ActionMailer, but was surprised to find that it didn't exist.
I had three options: put the logic inline in each
ActionMailer method which is not DRY and muddles concerns, put the logic in the
ActionContoller as filters which break encapsulation in terrible ways (and is at the wrong layer), or extend
ActionMailler to allow callback methods.
I choose the latter...
I created a simple plugin which allows you to add before and after deliver callbacks to
ActionMailer, and constrain them to certain methods using
:except options ala
You can add the plugin by executing the following in your rails app directory:
script/plugin install git://github.com/AnthonyCaliendo/action_mailer_callbacks.git
Here is a snipet from the readme:
There are 2 main ways to define a callback. In each case, the callback method/block is passed the mail object as the only argument. You may define a callback using a block: class FooMailer < ActionMailer::Base after_deliver do |mail| ... end end You may also define a callback using a symbol/string for a method name: class FooMailer < ActionMailer::Base before_deliver :append_advertisement def append_advertisement(mail) ... end end Callbacks take options which can be used to define which mail types (i.e. methods) they will be applied to. These options take the format of *only* and *except*. - An *only* callback will only be run for methods which match the passed method names. - An *except* callback will be called for all methods EXCEPT those that match the passed method names The options can take either an array of strings/symbols, or a single string/symbol. class FooMailer < ActionMailer::Base before_deliver :append_disclaimer, :only => [:email_friend, :announce_something] after_deliver :notify_user, :except => :invite_user ... end == What About Halting the Chain? You can halt the chain in either a before or after callback. In order to do this, just call +halt_callback_chain+ in the block (or +self.class.halt_callback_chain+ in an instance method). If the chain is halted in a before callback, the email will *NOT* be delivered and no other callbacks will be invoked (either any after callbacks or any remaining before callbacks). If the chain is halted in an after callback, the email will have already been sent and all before callbacks would have run, but any remaining after callbacks will not be invoked. class FooMailer < ActionMailer::Base before_deliver do |mail| halt_callback_chain if invalid_mail?(mail) end after_deliver :abort def abort(mail) self.class.halt_callback_chain end end
I wanted to keep usage similar to how filters are handled in
ActionController, so I decided against using the return value of the callback to halt the chain. Instead, you explicitly halt the chain similar to how rendering or redirecting halts a
before_filter chain in a controller.
I am hosting the code on github at http://github.com/AnthonyCaliendo/action_mailer_callbacks/tree/master. I was time constrained when I wrote this, but I will be cleaning up the code once I get a chance (I promise!).
I wrote this code months ago (just now got around to blogging about it and releasing it to github), but while researching this blog post I saw that someone else came up with a similar plugin at http://github.com/kelyar/mailer_callbacks/tree/master. Kelyar's plugin seems more limited than the one I am providing (plus, there are no tests... tsk tsk!), but it is cleaner and simpler. It may be worth taking a look at that as well.