pass instance variable while using observer pattern in rails - ruby-on-rails

I'm using observer pattern in rails. The "delivered_email" method in TestObserver will be called after notification email been sent out. How could I pass the instance variable in "notification" to the "delivered_email"? I could add it either in the header or subject. But it could pose security issue since user who received email could also see the variable. Is there any better way to solve it?
class GeneralMailer < ActionMailer::Base
def notification(data)
#emails = data[:emails]
subject = "#{#sender.to_s}"
mail(:to => #emails, :subject => subject)
end
end
class TestObserver
def self.delivered_email(message)
begin
# do something here
puts #emails
rescue => ex
# do something here
end
end
ActionMailer::Base.register_observer(TestObserver)

If you do want to send extended information to a mail observer – as I myself did – it can be done by monkey-patching your mailer a bit to use a custom class extending from Mail::Message.
Note that this requires setting an internal, undocumented attribute. In a future Rails version, it might not continue to work.
class ExtendedMessage < Mail::Message
attr_accessor :extra_args
end
class ApplicationMailer
before_action :patch_message
def patch_message
# Patch the internal attribute #_message to use our overridden class
# that you can store extra attributes inside.
# NOTE: If a future Rails version decides to change the internals, this could break.
#_message = ExtendedMessage.new
end
end
class MailObserver
def self.delivered_email(mail)
do_something_with mail.extra_args
end
end

Related

Pass instance variable from Devise controller to custom mailer?

I use Devise for authentication in my Rails app.
In my registrations_controller I set an instance variable like this:
class RegistrationsController < Devise::RegistrationsController
def create
#foo = "bar"
super
end
end
In my customized mailer I then try to access the #foo instance variable, but it just returns nil:
class CustomMailer < Devise::Mailer
helper :application
include Devise::Controllers::UrlHelpers
def confirmation_instructions(record, token, opts={})
Rails.logger.error #foo.inspect # => nil
super
end
end
Anyone who could help?
I have looked through the posts How do I add instance variables to Devise email templates?, How to pass additional data to devise mailer?, How to pass instance variable to devise custom mailer in Rails?. But none of them seem to deal with this exact problem.
First let me explain you have instance variables work in ruby classes.
In ruby, instance variable(in your case #foo) in a class(in your case RegistrationsController) do not pass down to other classes(in your case CustomMailer) even if inheritance is ON. For Example consider:
class Abc
#first = "First"
puts "Abc: #{#first}"
end
class Def < Abc
puts "Def: #{#first}"
end
# => Abc: First instance variable
# => Def:
As you can see the class Def cannot have access to #first instance variable. Thus, instance variables don't get passed down to other classes automatically.
If you want these variables to pass down the class, you should consider using Class Instance Variables which starts with ##. Ex:
class Abc
##first = "First instance variable"
puts "Abc: #{##first}"
end
class Def < Abc
puts "Def: #{##first}"
end
# => Abc: First instance variable
# => Def: First instance variable
Now ##first will pass down to inherited class automatically.
Now, relate the above senario to your question. So you're creating a instance variable #foo in RegistrationsController and it will not be passed to other inherited classes. Under the hood both Devise::RegistrationsController and DeviseMailer are inherited from Devise.parent_controller.
So, better way to work with it is to send the #foo as a parameter, for example:
class RegistrationsController < Devise::RegistrationsController
def create
#foo = "bar"
CustomMailer.confirmation_instructions(user, token, opts={foo: #foo})
...
end
end
Then you can access this in your CustomMailer:
def confirmation_instructions(record, token, opts={})
puts opts[:foo]
super
end

Set an instance variable app wide in Rails

In my Rails application I have a class that I want to initialize and then access it throughout my controllers. So the idea is that I set it via the application controller if it's not already been defined:
class ApplicationController < ActionController::Base
before_action :set_custom_class
# create an instance of customclass if doesn't exist
def set_custom_class
#custom_class ||= CustomClass.new
end
end
An example of the class:
class CustomClass
def initialize; end
def custom_method
#custom_method
end
def custom_method=(content)
#custom_method = content
end
end
If I then have a controller like:
class MyController < ApplicationController
def method_1
# set the custom_method value on my instance
#custom_class.custom_method('Some content')
# return the value I set above
#variable = #custom_class.custom_method
redirect_to :method_2
end
def method_2
# I should be able to retrieve the same value from that same instance
#variable = #custom_class.custom_method
end
end
What I'm finding is that when calling method_1 the #variable will return my content fine, but when calling method_2 AFTER method_1 (so the custom_method for the app wide #custom_class has been set) it's returning nil.
Why isn't the instance being retained? The #custom_class shouldn't be creating a new instance as it's already been set. So I can't understand why the value I have set gets lost when requesting it.
You witnessing such behaviour, because state of a controller is not preserved between requests. For example, imagine that current_user method sets #current_user for one request and returns the same user for another one.
Please, consider an option of using cookies or database for sharing state between requests.
Otherwise, a workaround would be setting a class variable of CustomClass, but I don't recommend to do it.
Looks like your before_action will re-instantiate the new object on every request. That means that since you aren't passing anything through to the class in Method2, it will come out as NULL.
Since you said app-wide, why not make it app-wide?
In config/application.rb,
module App
class Application < Rails::Application
def custom_class
#custom_class ||= CustomClass.new
end
end
end
in your application code,
Rails.application.custom_class

How would I access url parameters in Mailer Preview

I have the following working Preview class:
class UserMailerPreview < ActionMailer::Preview
def invite
USerMailer.invite
end
end
I'm trying to pass paramaters to the method like so:
localhost:3000/rails/mailers/user_mailer/invite?key1=some_value
The server seems to receive them:
Parameters: {"key1"=>"some_value", "path"=>"user_mailer/invite"}
But when trying to access them with the hash params, I get an error.
Can I access these parameters in a Preview method and if so - how?
I dug into the code behind the mailer preview system and discovered that, unfortunately, none of the request parameters are passed to the preview class, and are thus inaccessible to the preview.
The relevant controller action is in railties: Rails::MailersControlller#preview. Here, you can see it calling ActionMailer::Preview#call and just passing the name of the "email" (ie: the appropriate method in the preview).
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

Using Observer to send email from rails app

I am trying to send an email from my app. The email gets sent when I don't use observer. When I use observer i get the following error :
undefined local variable or method ` UserMailer' for #<UserObserver:0x7f5730c07400>
Here's my UserMailer
class UserMailer < ActionMailer::Base
default :from => "from#me.com"
def welcome_email(user)
#user = user
#url = "website.com/home"
mail(:to => user.email, :subject => "Welcome to My Awesome Site")
end
end
The observer code
require "#{Rails.root}/app/mailers/user_mailer.rb"
class UserObserver < ActiveRecord::Observer
observe :user
def after_save(user)
 UserMailer.welcome_email(user).deliver
end
end
Any help will be appreciated. I am a nuby to ruby on rails. TIA
Observers have been removed in rails 4.
Which means, even though you can still use them with a gem, they are a deprecated and you should not use them.
The main reason for it is that observers are making your application a mess to read for new developers.
I would suggest you to use services (take a look at this post, which mentions how you can refactor your AR models properly), and send your email in one of them.
Do you have the below line setup in your application.rb file?
config.active_record.observers = :user_observer

rails 3: how to abort delivery method in actionmailer?

In my mailer controller, under certain conditions (missing data) we abort sending the email.
How do I exit the controller method without still rendering a view in that case?
return if #some_email_data.nil?
Doesn't do the trick since the view is still rendered (throwing an error every place I try to use #some_email_data unless I add a lot of nil checks)
And even if I do the nil checks, it complains there's no 'sender' (because I supposed did a 'return' before getting to the line where I set the sender and subject.
Neither does render ... return
Basically, RETURN DOESN'T RETURN inside a mailer method!
A much simpler solution than the accepted answer would be something like:
class SomeMailer < ActionMailer::Base
def some_method
if #some_email_data.nil?
self.message.perform_deliveries = false
else
mail(...)
end
end
end
If you're using Rails 3.2.9 (or later things even better) - there you can finally conditionally call mail(). Here's the related GitHub thread. Now the code can be reworked like this:
class SomeMailer < ActionMailer::Base
def some_method
unless #some_email_data.nil?
mail(...)
end
end
end
I just encountered same thing here.
My solution was following:
module BulletproofMailer
class BlackholeMailMessage < Mail::Message
def self.deliver
false
end
end
class AbortDeliveryError < StandardError
end
class Base < ActionMailer::Base
def abort_delivery
raise AbortDeliveryError
end
def process(*args)
begin
super *args
rescue AbortDeliveryError
self.message = BulletproofMailer::BlackholeMailMessage
end
end
end
end
Using these wrapper mailer would look like this:
class EventMailer < BulletproofMailer::Base
include Resque::Mailer
def event_created(event_id)
begin
#event = CalendarEvent.find(event_id)
rescue ActiveRecord::RecordNotFound
abort_delivery
end
end
end
It is also posted in my blog.
I've found this method that seems the least-invasive, as it works across all mailer methods without requiring you to remember to catch an error. In our case, we just want a setting to completely disable mailers for certain environments. Tested in Rails 6, although I'm sure it'll work just fine in Rails 5 as well, maybe lower.
class ApplicationMailer < ActionMailer::Base
class AbortDeliveryError < StandardError; end
before_action :ensure_notifications_enabled
rescue_from AbortDeliveryError, with: -> {}
def ensure_notifications_enabled
raise AbortDeliveryError.new unless <your_condition>
end
...
end
The empty lambda causes Rails 6 to just return an ActionMailer::Base::NullMail instance, which doesn't get delivered (same as if your mailer method didn't call mail, or returned prematurely).
Setting self.message.perform_deliveries = false did not work for me.
I used a similar approach as some of the other answers - using error handling to control the flow and prevent the mail from being sent.
The example below is aborting mail from being sent in non-Production ENVs to non-whitelisted emails, but the helper method logic can be whatever you need for your scenario.
class BaseMailer < ActionMailer::Base
class AbortedMailer < StandardError; end
def mail(**args)
whitelist_mail_delivery(args[:to])
super(args)
rescue AbortedMailer
Rails.logger.info "Mail aborted! We do not send emails to external email accounts outside of Production ENV"
end
private
def whitelist_mail_delivery(to_email)
return if Rails.env.production?
raise AbortedMailer.new unless internal_email?(to_email)
end
def internal_email?(to_email)
to_email.include?('#widgetbusiness.com')
end
end
I just clear the #to field and return, so deliver aborts when it doesn't have anything there. (Or just return before setting #to).
I haven't spent much time with rails 3 but you could try using
redirect_to some_other_route
alternatively, if you're really just checking for missing data you could do a js validation of the form fields and only submit if it passes.

Resources