Passing additional context to devise mailers?

1,770 views
Skip to first unread message

Daniel

unread,
Nov 24, 2011, 10:36:26 PM11/24/11
to Devise
Howdy all,

I use devise on a small website I run and it's absolutely fantastic.
Thankfully my site has been growing and I'm looking at launching
similar sites that do the same thing just in different niches (think
kind of like how stack exchange works, but for local business
listings.)

I'm in the process of converting the system to support multiple sites
that are based off of the accessed hostname, and user accounts are
shared across all sites and the major problem I'm having is figuring
out how to get devise mailers to have the context of which site the
user is on when sending emails.

With all of my custom mailers I can simply pass a site_id to the
mailer method so it can look it up and brand the email based on that
site the user is access, but I am not quite sure how to make devise do
this. Basically, if someone tries to reset their password on
site1.com, that sites context needs to make it down to the email
layout so it can use the right logo/name.

Has anyone tried to do anything like this and can offer suggestions?

Thanks in advance!

José Valim

unread,
Nov 25, 2011, 2:48:39 AM11/25/11
to Devise
There is a config.mailer in your Devise initializer. So what you can
do is to inherit from the Devise::Mailer and change Devise to use your
new mailer. Inside the new mailer you will have access to the user
object and then you can do whatever you want.

Here is the devise mailer source code:

https://github.com/plataformatec/devise/blob/master/app/mailers/devise/mailer.rb

And here are the helpers used in it:

https://github.com/plataformatec/devise/blob/master/lib/devise/mailers/helpers.rb

I also think we have more information on the wiki regarding
configuring mailers.

Daniel

unread,
Nov 25, 2011, 11:37:26 AM11/25/11
to Devise
Yeah I found that and it's very useful, I use it to solve other
problems, the difference here is that unfortunately the user does
object does not carry a specific site context with it since it can
access any of the sites in the system. At the time the user is
resetting their password on the site they happen to be accessing at
the time, only the controller has the necessary site context. I solved
this in all my other controllers be simply passing the controllers
current_site_id (a custom helper) to the mailer, but since I do not
control the call to the mailers in devise controllers I'm kind of
stuck.

Ultimately this may be something where I need to change my
architecture so that if accounts are shared, users get a generic email
from a parent site, but if I can get away with not doing that I'd be
very happy.

Thanks for such a quick reply and a great piece of code.

On Nov 25, 2:48 am, José Valim <jose.va...@gmail.com> wrote:
> There is a config.mailer in your Devise initializer. So what you can
> do is to inherit from the Devise::Mailer and change Devise to use your
> new mailer. Inside the new mailer you will have access to the user
> object and then you can do whatever you want.
>
> Here is the devise mailer source code:
>

> https://github.com/plataformatec/devise/blob/master/app/mailers/devis...


>
> And here are the helpers used in it:
>

> https://github.com/plataformatec/devise/blob/master/lib/devise/mailer...

Daniel

unread,
Nov 27, 2011, 5:36:43 PM11/27/11
to Devise
So, I was able to finally do this but I don't like what I had to do:

1) Add a non-persisted attribute (in my case contextual_site_id) to my
User model.
2) Override create actions in confirmation and password controllers,
add the contextual data to the params[resource_name] hash, and call
super
3) Override send_X_instructions class methods on my User model and set
the contextual_site_id attribute from the parameter added to the
attributes hash
4) Create a custom UserMailer that sets the necessary template
variables from the user object's contextual_site_id attribute.

This was the cleanest way I could find to do what I needed to do and I
don't like it at all which makes me think there may be a need for some
sort of hooking system built into devise to do these sorts of things.
I really have no idea how far my use case is from the norm.

Hopefully this will help anyone else having this issue.

Danial Pearce

unread,
Jan 1, 2013, 7:11:29 AM1/1/13
to Daniel, plataforma...@googlegroups.com
Hi Daniel,

Interestingly I am looking at the exact same problem. A single rails
app that can be accessed from many different hostnames and the
hostname it is accessed from is needed in the views to determine
layouts/branding. This branding of course is required for mailers as
well as normal views.

As you already discovered, the mailer call for resetting a password is
deep inside of devise itself. It in fact calls a method on the User
object itself, but as you mentioned, the User object does not have any
information about the request.

At first I was thinking, the User model will probably need to know the
hostname context at all times. So perhaps overload the instantiate
(note: not initialize since ActiveRecord doesn't use that in all
situations) method so that anything that ever tries to "find" or build
a User object always has to pass though the site context. But this
would prove extremely difficult, as lots of third party stuff expects
to be able to fetch a model by just ID alone.

Next thought was to setup a cattr_accessor in the User model so that
ApplicationController can set the User.site_context at the beginning
of a request, and at any time from then on, user.site_context will
always contain that. This class variable is "process safe", so a
mailer would be able to access it just fine. However, this is of
course not threadsafe, and whilst I do run on unicorn (process based
so I should be fine), i'd rather stay threadsafe for future releases.

I then jumped on #rubyonrails on freenode and was pointed to
https://github.com/steveklabnik/request_store which looks like it
might be promosing, but it's still the general idea of a "site context
global variable" that is pretty scary. I hear the words global
variable and shudder.

So that's how I ended up here. I'll investigate your suggestion, but
you can of course point the passwords route to your own
PasswordController that inherits from Devise's one, and overload the
create method to set some instance variables on the resource before
calling super, or pass extra variables wherever you need if you clone
the whole method, but I don't like doing that.

The crux of it is that rails mail views in general are lacking. Rails
does let you set config.action_mailer.default_url_options, but of
course in situations like ours, there is no default hostname because
in the situation of devise you almost always want the hostname to
reflect that of the request. Exceptions of course being if you are
generating the mails from outside a request, such as from the console
manually or via cron.

If anyone has any thoughts, I'd love to hear them.

regards,
Danial


On Nov 25 2011, 2:36 pm, Daniel <dpehr...@gmail.com> wrote:
> Howdy all,
>
> I use devise on a small website I run and it's absolutely fantastic.
> Thankfully my site has been growing and I'm looking at launching
> similar sites that do the same thing just in different niches (think
> kind of like how stack exchange works, but for local business
> listings.)
>
> I'm in the process of converting the system to support multiple sites
> that are based off of the accessedhostname, and user accounts are
Reply all
Reply to author
Forward
0 new messages