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).
Related
I have a before action in a user mailer file, which is supposed to stop mailers sending if a column on user is set to true or false. However current user is currently unavailable. I understand why, but was wondering if there was a way to do this.
I want to avoid adding the check_if_users_can_receive_mailers at the top of each mailer method.
before_action :check_if_users_can_receive_mailers
#methods that send mailers
private
def check_if_users_can_receive_mailers
current_user.send_mailers?
end
You have to make the current user available as a attribute or class variable. The most straight forward method is something like this:
class MailerBase < ActionMailer::Base
before_action :check_if_users_can_receive_mailers
attr_accessor :user
def initialize(user)
#user = user
end
private
def check_if_users_can_receive_mailers
user.send_mailers?
end
end
class SomeMailerClass < MailerBase
end
In Rails only your controller and views are request aware. Mailers and models and other classes in your application are not and they cannot get the current user since they can't access the session nor the method current_user which is a helper method mixed into your controller (and the view context).
If your mailers need to know about the current user the most logical approach is to pass that information into the mailer:
class UserMailer < ApplicationMailer
def intialize(user)
#user = user
end
end
However a mailer should only have one job - to send emails and it shouldn't be questioning if it should do the job or not. Determining if you should send an email to the user should be done elsewhere. You can place this logic in the controller or even better in a service object:
# app/notifiers/user_notifier.rb
class UserNotifier
def initialize(user, event:)
#user = user
#event = event
end
def notify
if #user.wants_email?
spam_user!
end
send_in_app_notification
end
def self.notify(user, event:)
new(user, event:)
end
private
def spam_user!
# ...
end
def send_in_app_notification
# ...
end
end
class ThingsController
def create
#thing = Thing.new
if #thing.save
UserNotifier.notify(current_user, event: :thing_created)
redirect_to #thing
else
render :new
end
end
end
I have a simple mailer
class ApplyMailer < ActionMailer::Base
def inform_teacher
end
def inform_division
end
def inform_everyone
inform_teacher.deliver
inform_division.deliver
end
end
Calling inform_teacher and inform_division everything works well. But when I try to call inform_everyone just one blank email arrives.
Is it possible to combine multiple email method though one method?
Found solution to this:
class ApplyMailer < ActionMailer::Base
def inform_teacher
end
def inform_division
end
def self.inform_everyone
ApplyMailer.inform_teacher.deliver
ApplyMailer.inform_division.deliver
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
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
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.