Here is some code in a recent Railscast:
class UserMailer < ActionMailer::Base
default from: "from#example.com"
def password_reset(user)
#user = user
mail :to => user.email, :subject => "Password Reset"
end
end
and this is in a controller
def create
user = User.find_by_email(params[:email])
UserMailer.password_reset(user).deliver
redirect_to :root, :notice => "Email sent with password reset instructions."
end
The password_reset method looks like an instance method to me, yet it looks like it's being called like a class method. Is it an instance or a class method, or is there something special about this UserMailer class?
Looking in the source (https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/base.rb), Rails uses method_missing to create a new instance of the ActionMailer. Here's the relevant part from the source:
def method_missing(method_name, *args) # :nodoc:
if respond_to?(method_name)
new(method_name, *args).message
else
super
end
end
Related
There is a before_action callback method in my ActionMailer object which is responsible for setting some instance variables.
class TestMailer < ApplicationMailer
before_action :set_params
def send_test_mail
mail(to: #email, subject: subject)
end
def set_params
#account = account.email
#date = some_action(account.updated_at)
end
end
The question is How one can test these variables in a rspec test?
some thing like:
describe TestMailer do
describe '#set_params' do
described_class.with(account: account, subject: subject).send_test_mail.deliver_now
expect(#date).to eq(Date.today)
end
end
any clue would be highly appreciated.
I think that instead of testing the instance variables, it would be better to test the email body, for example:
expect(mail.body.encoded).to include(account.updated_at)
you could setup a spy inside a mock method instance_variable_set, then validate that spy
class TestMailer < ApplicationMailer
attr_accessor :day
# ...
end
describe TestMailer do
let(:freeze_today) { Time.now.utc }
it '#set_params' do
# freeze today
allow_any_instance_of(TestMailer).to receive(:some_action)
.with(account.updated_at)
.and_return(freeze_today)
# spy
#spy = nil
allow_any_instance_of(TestMailer).to receive(:day=) do |time|
#spy = time
end
described_class.with(account: account, subject: subject)
.send_test_mail
.deliver_now
expect(#spy).to eq(freeze_today)
# or just simple like this
expect_any_instance_of(TestMailer).to receive(:day=).with(freeze_today)
end
end
I have this weird thing going on in my rails4 app:
I created event.rb in the lib folder.
In there, I call a mailer:
def whatever
puts 'here'
UserMailer.welcome(user)
puts 'there'
end
which is calling
class UserMailer < ActionMailer::Base
def welcome(user)
#user = user
mail(to: #user.mailer, subject: 'Welcome to my app').deliver
end
end
The weird thing is that the method welcome is never called, while whatever is called, without raising any error (the logs are there).
But if I call UserMailer.welcome(User.first) in the console, it is sent.
What am I doing wrong? Is it that it is not possible to send an email from a module? I should move this code to a model? That would be weird.
Thanks in advance
IMO mailer should look like this:
class UserMailer < ActionMailer::Base
def welcome(user)
#user = user
mail(to: #user.mailer, subject: 'Welcome to my app') #.deliver removed
end
end
and should be invoked with this manner:
def whatever
puts 'here'
UserMailer.welcome(user).deliver_now # and added here
puts 'there'
end
I've got a Rails 3 mailer that works fine.
class Notifier < ActionMailer::Base
def cool_email(user_id)
#user = User.find_by_id(user_id)
mail(to: #user.email,
from: 'admin#example.com',
subject: 'hi'
)
end
end
The view for this will render the #user instance variable correctly and the email is sent without any problem.
However, when I namespace the mailer, everything breaks. With the mailer structured like this.
class Foo::Notifier < ::ActionMailer::Base
def cool_email(user_id)
#user = User.find_by_id(user_id)
mail(to: #user.email,
from: 'admin#example.com',
subject: 'hi'
)
end
end
And the view inside app/view/foo, Rails is unable to find the html template. The email sends, but there is nothing inside the body.
What am I doing wrong?
The views should be stored in app/view/foo/notifier, specifically app/view/foo/notifier/cool_email.EXTENSION.
FYI, it's always a good practice to append Mailer to the name of a mailer.
class Foo::NotifierMailer < ::ActionMailer::Base
or
class Foo::NotificationMailer < ::ActionMailer::Base
It prevents conflicts and makes possible to immediately understand the scope of the class.
I'm trying to access request.host (well, ideally host_with_port) from a Mailer in Rails. The actually call to request.host is in a Helper:
#/app/helpers/confirmations_helper
module ConfirmationsHelper
def email_confirm_url(token)
"http://#{request.host_with_port}/confirm/#{token}" # failure: undefined method
end
end
#/app/mailers/user_mailer
class UserMailer < ActionMailer::Base
default from: "email#domain.com"
add_template_helper(ConfirmationsHelper) #get access to helpers/confirmations_helper.rb
def email_confirmation(user)
#user = user
#url = "http://www.domain.com/"
mail(to: user.email, subject: "Email Confirmation")
end
end
#config/environments/development.rb
...
config.action_mailer.default_url_options = { :host => "localhost:3000" }
Error I'm getting is:
ActionView::Template::Error:
undefined method `host' for nil:NilClass
Use
ActionMailer::Base.default_url_options[:host]
in your Mailer to access the configured host in in config/environments/
I believe, this is the easiest way.
ActionView::Template::Error:
undefined method `host' for nil:NilClass
This is telling you that request is nil. This is because outside of the scope of your controller (ie. in a class extending ActionMailer::Base) request doesn't exist.
You need to pass the request object or just the part you need (request.host_with_port) to the mailer like you do other data like user in your email_confirmation.
So you have a create method with something like this
def create
#user = User.new
#user.assign_attributes(params[:user])
#user.save
#user.send_email_confirmation
end
Inside your User model you have a send_email_confirmation method like this
class User < ActiveRecord::Base
def send_email_confirmation
UserMailer.email_confirmation(self).deliver
end
Your mailer's email_confirmation looks like
def email_confirmation(user)
#user = user
#url = "http://www.domain.com/"
mail(to: user.email, subject: "Email Confirmation")
end
Making the request to the mailer from your model is not the best idea; you should keep a cleaner separation of concerns. This is part of your problem and why you are finding unwanted complexity when trying to pass something like request from your controller action into the mailer template.
What I might suggest is creating a worker class. Here I explain how to setup classes in lib/ - the same concept can be applied to something like a lib/your_app/workers/user.rb.
You could have the following in this class
module YourApp
module Workers
module User
extend self
def create!(params, options{})
options.reverse_merge! host: ""
user = User.new
user.assign_attributes(params)
user.save
UserMailer.email_confirmation(user, host).deliver
user
end
end
end
end
Your controller action could then simply be
def create
#user = ::YourApp::Worker::User.create!(params[:user], host: request.host_with_port)
end
Your mailer method can now look like
def email_confirmation(user, host)
#user = user
token = "" # define token somehow
#url = "#{host}/confirm/#{token}"
mail(to: user.email, subject: "Email Confirmation")
end
Finally, you can remove send_email_confirmation from your model as well as the email_confirm_url method from your helper since they're no longer used. Two things to note
my example above doesn't include anything in the way of validations/error-checks
my example makes an assumtion about where token is being defined and used
As you can see, by introducing this 'worker' class, there is a clean separation of functionality without duplication.
In my Rails application I want to temporarily stop sending email for specific users (e.g. when I get bounces due to quota) until the user confirms he is able to receive email again.
I have a common superclass for all mailer classes. There I always call a method setup_email before sending a mail.
Where is the best place to call #user.mail_suspended??
Here is some simplified sample app, I use Rails 2.3:
# Common super class for all Mailers
class ApplicationMailer < ActionMailer::Base
protected
def setup_mail(user)
#recipients = user.email
#from = ...
end
end
# Specific Mailer for User model
class UserMailer < ApplicationMailer
def message(user, message)
setup_mail(user)
#subject = "You got new message"
#body[:message] = message
end
end
# Use the UserMailer to deliver some message
def MessagesController < ApplicationController
def create
#message = Message.new(params[:message])
#message.save
UserMailer.deliver_message(#message.user, #message)
redirect_to ...
end
end
I solved this by setting the ActionMailer::Base.perform_deliveries to false:
def setup_mail(user)
email = user.default_email
if email.paused?
ActionMailer::Base.perform_deliveries = false
logger.info "INFO: suspended mail for #{user.login} to #{email.email})"
else
ActionMailer::Base.perform_deliveries = true
end
# other stuff here
end
I wouldn't set perform_deliveries universally, just per message, e.g.
after_filter :do_not_send_if_old_email
def do_not_send_if_old_email
message.perform_deliveries = false if email.paused?
true
end
I tried many ways, but no one could help me except this one.
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
Make a class inherited with standardError to raise exception.
Check the condition, if false then raise exception.
Handle that exception with the empty lambda.
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).