I have a 3rd party email integration library that I want to use to send email as one of my users. To send email I make an API call using an access_token that I save for each user.
To still use action mailer, I've created a custom delivery class like this:
module Mail
class CustomMailDelivery
attr_reader :settings
def initialize(settings)
#settings = settings
end
def deliver!(mail)
# use 3rd party client here
end
end
end
I'm configuring this in an initializer:
ActionMailer::Base.add_delivery_method :custom, Mail::CustomMailDelivery, {
app_id: ENV.fetch('3RDPARTY_APP_ID'),
app_secret: ENV.fetch('3RDPARTY_APP_SECRET'),
}
This allows me to set the delivery method on a per-mailer basis:
class LeadMailer < ApplicationMailer
self.delivery_method = :custom
...
end
The problem is, I need to pass the user that is sending this message along, so I can get their access_token.
I don't want to rely on fetching the EmailAccount using the sender's email address because it seems like this could break down the road (it's possible this email address might not be the same as the sending user).
In other words, I'd like to pass it explicitly, so it is easy to understand and I avoid any confusion.
Is there a way to provide per-mail context to a custom action mailer delivery class?
I ended up passing this data with a custom message header, which I later delete when handling the message.
class CustomMailer < ApplicationMailer
self.delivery_method = :custom
attr_reader :sending_account
def mail(params)
raise 'You must call set_sending_account before calling mail.' unless sending_email_account
super(params.merge({
Mail::CustomMailDelivery::ACCOUNT_ID_HEADER => sending_account.id
}))
end
def set_sending_account(account)
#sending_account = account
end
end
This way mailers that need this behavior subclass from this class and are forced to provide the custom data.
In the delivery class I yank this value out of the headers:
module Mail
class CustomMailDelivery
attr_reader :settings
# we'll hijack email headers in order to pass over some required data from the mailer to this class
ACCOUNT_ID_HEADER = '__account_id'
def initialize(settings)
#settings = settings
end
def deliver!(mail)
account = account_for(mail)
client = third_party_api_client(account.access_token)
client.send_message(...)
end
private
def third_party_api_client(access_token)
# ...
end
def account_for(mail)
header_field = mail[ACCOUNT_ID_HEADER]
missing_account_id_header! unless header_field
email_account = Account.find(header_field.value)
# remove the header field so it doesn't show up in the actual email
mail[ACCOUNT_ID_HEADER] = nil
account
end
def missing_account_id_header!
raise "Missing required header: #{ACCOUNT_ID_HEADER}"
end
end
end
This solution is not very elegant, but works.
Thanks for the idea, I put together a shorter version by using register_observer and register_interceptor.
It's basically the same idea, except you don't need to redefine too much delivery stuff. You just hook in the mail workflow.
First, declare the hook:
ActionMailer::Base.register_observer(MailToActionEventObserver)
ActionMailer::Base.register_interceptor(MailToActionEventObserver)
Then, the easy part is that the hooks are static methods inside of the same class:
class MailToActionEventObserver
def self.delivered_email(mail)
# Here you can use #passed_argument because it is called just after
# self.delivering_email
end
def self.delivering_email(mail)
#passed_argument = mail['MY-PERSONAL-HEADER'].to_s
# Now remove the temporary header:
mail['MY-PERSONAL-HEADER'] = nil
end
end
Now, same as your answer #Ben, just need to pass the argument as a header in the mailer:
def my_custom_mail
headers['MY-PERSONAL-HEADER'] = 'whatever-value'
mail(...)
end
Related
I'm building an around_action for my customer_mailer class so that I don't have to wrap begin and rescue around every time I call deliver_now
class CustomerMailer < ApplicationMailer
around_action :rescue_error
def send_email(customer)
...
end
def invite_friend(customer, invitee_email)
...
end
private
def rescue_error
yield
rescue => e
msg = "Caught exception! #{e} | #{action_name}"
puts msg
raise
end
end
So in the rescue, I want to log the message with information such as which action was called, I managed to find the method action_name to show which action was called, but I couldn't find a way to retrieve the parameters that were passed into the action, any ideas?
Thanks!
Before I answer your question: would using Bugsnag or something similar work in your case? Alternatively would rescue_from Exception, with: :exception_handler work for you? (it won't allow you to reraise the exception though)
I dug into Rails source code and it seems that parameters are not stored anywhere. They are just passed as a splat to an instance method defined in your mailer class. However, there is a way to store them (without monkey-patching).
Mailers inherit from AbstractController::Base. Looking at the snippet below:
# Call the action. Override this in a subclass to modify the
# behavior around processing an action. This, and not #process,
# is the intended way to override action dispatching.
#
# Notice that the first argument is the method to be dispatched
# which is *not* necessarily the same as the action name.
def process_action(method_name, *args)
send_action(method_name, *args)
end
# Actually call the method associated with the action. Override
# this method if you wish to change how action methods are called,
# not to add additional behavior around it. For example, you would
# override #send_action if you want to inject arguments into the
# method.
alias send_action send
we can see that we can override #send_action and make it store the arguments. Add the following to your ApplicationMailer:
class ApplicationMailer < ActionMailer::Base
def send_action(method_name, *args)
#action_args = args
super
end
end
The arguments will be available as #action_args in all your mailers.
Just store the parameters with which the action has been called to an instance variable, say #params. Then these parameters will be accessible in rescue_error via #params. As per your example:
class CustomerMailer < ApplicationMailer
around_action :rescue_error
def send_email(customer)
#params = { customer: customer }
...
end
def invite_friend(customer, invitee_email)
#params = { customer: customer, invitee_email: invitee_email }
...
end
private
def rescue_error
begin
yield
rescue => e
msg = "Caught exception! #{e} | #{action_name} | #{#params.inspect}"
puts msg
raise
end
end
end
You can make the assignment to #params a bit cleaner by using hash parameters in your actions, e.g.
def invite_friend(options = {})
#params = params
...
end
Of course, this requires accessing the parameters via options, such as options[:customer] to access customer, and options[:invitee_email] to access invitee_email.
The action name have to be yielded , it depends on the way you use your rescue_error .
Define a variable in the block that will be yielded
or raise specifics errors (maybe your custom exception class )
this way you'll retrieve invormation via "e"
post an exemple use case of rescue_error.
I use a gem to manage certain attributes of a gmail api integration, and I'm pretty happy with the way it works.
I want to add some local methods to act on the Gmail::Message class that is used in that gem.
i.e. I want to do something like this.
models/GmailMessage.rb
class GmailMessage < Gmail::Message
def initialize(gmail)
#create a Gmail::Message instance as a GmailMessage instance
self = gmail
end
def something_clever
#do something clever utilising the Gmail::Message methods
end
end
I don't want to persist it. But obviously I can't define self in that way.
To clarify, I want to take an instance of Gmail::Message and create a GmailMessage instance which is a straight copy of that other message.
I can then run methods like #gmail.subject and #gmail.html, but also run #gmail.something_clever... and save local attributes if necessary.
Am I completely crazy?
You can use concept of mixin, wherein you include a Module in another class to enhance it with additional functions.
Here is how to do it. To create a complete working example, I have created modules that resemble what you may have in your code base.
# Assumed to be present in 3rd party gem, dummy implementation used for demonstration
module Gmail
class Message
def initialize
#some_var = "there"
end
def subject
"Hi"
end
end
end
# Your code
module GmailMessage
# You can code this method assuming as if it is an instance method
# of Gmail::Message. Once we include this module in that class, it
# will be able to call instance methods and access instance variables.
def something_clever
puts "Subject is #{subject} and #some_var = #{#some_var}"
end
end
# Enhance 3rd party class with your code by including your module
Gmail::Message.include(GmailMessage)
# Below gmail object will actually be obtained by reading the user inbox
# Lets create it explicitly for demonstration purposes.
gmail = Gmail::Message.new
# Method can access methods and instance variables of gmail object
p gmail.something_clever
#=> Subject is Hi and #some_var = there
# You can call the methods of original class as well on same object
p gmail.subject
#=> "Hi"
Following should work:
class GmailMessage < Gmail::Message
def initialize(extra)
super
# some additional stuff
#extra = extra
end
def something_clever
#do something clever utilising the Gmail::Message methods
end
end
GmailMessage.new # => will call first the initializer of Gmail::Message class..
Building upon what the other posters have said, you can use built-in class SimpleDelegator in ruby to wrap an existing message:
require 'delegate'
class MyMessage < SimpleDelegator
def my_clever_method
some_method_on_the_original_message + "woohoo"
end
end
class OriginalMessage
def some_method_on_the_original_message
"hey"
end
def another_original_method
"zoink"
end
end
original = OriginalMessage.new
wrapper = MyMessage.new(original)
puts wrapper.my_clever_method
# => "heywoohoo"
puts wrapper.another_original_method
# => "zoink"
As you can see, the wrapper automatically forwards method calls to the wrapped object.
I'm not sure why you can't just have a simple wrapper class...
class GmailMessage
def initialize(message)
#message = message
end
def something_clever
# do something clever here
end
def method_missing(m, *args, &block)
if #message.class.instance_methods.include?(m)
#message.send(m, *args, &block)
else
super
end
end
end
Then you can do...
#my_message = GmailMessage.new(#original_message)
#my_message will correctly respond to all the methods that were supported with #original_message and you can add your own methods to the class.
EDIT - changed thanks to #jeeper's observations in the comments
It's not the prettiest, but it works...
class GmailMessage < Gmail::Message
def initialize(message)
message.instance_variables.each do |variable|
self.instance_variable_set(
variable,
message.instance_variable_get(variable)
)
end
end
def something_clever
# do something clever here
end
end
Thanks for all your help guys.
I'm working on creating a database-backed email auditing system so I can keep track of email messages. The tricky part is I would love to be able to organize these by the mailer classes and also be able to store the name of the mailer method.
It's not difficult to create a mailer interceptor or observer to gather the data from the Mail::Message instance, but I'm curious if there's a way to capture the class and method name that created the instance of that message.
I would prefer not to use callbacks if at all possible.
Any ideas?
Here's what I ended up going with... I would love some feedback about the pros and cons of doing it this way. Feels kind of ugly to me but it was easy. Basically, I included the ability to use callbacks in my mailer, attaching the class and method name metadata to the Mail::Message object so that it would be accessible in my observer. I attached it by setting instance variables on the Mail::Message object, and then sending attr_reader to the Mail::Message class, allowing me to call mail.mailer_klass and mail.mailer_action.
I did it this way because I wanted to record the Mail::Message object after it had been delivered so I could get the exact date it had been sent and know that the logged email should have successfully sent.
The mailer:
class MyMailer < ActionMailer::Base
default from: "noreply#theapp.com"
include AbstractController::Callbacks
# Where I attach the class and method
after_action :attach_metadata
def welcome_note(user)
#user = user
mail(subject: "Thanks for signing up!", to: #user.email)
end
private
def attach_metadata
mailer_klass = self.class.to_s
mailer_action = self.action_name
self.message.instance_variable_set(:#mailer_klass, mailer_klass)
self.message.instance_variable_set(:#mailer_action, mailer_action)
self.message.class.send(:attr_reader, :mailer_klass)
self.message.class.send(:attr_reader, :mailer_action)
end
end
The observer:
class MailAuditor
def self.delivered_email(mail)
if mail.multipart?
body = mail.html_part.decoded
else
body = mail.body.raw_source
end
Email.create!(
sender: mail.from,
recipient: mail.to,
bcc: mail.bcc,
cc: mail.cc,
subject: mail.subject,
body: body,
mailer_klass: mail.mailer_klass,
mailer_action: mail.mailer_action,
sent_at: mail.date
)
end
end
config/initializers/mail.rb
ActionMailer::Base.register_observer(MailAuditor)
Thoughts?
Just to illuminate a simpler answer that is hinted at by Mark Murphy's first comment, I went with a very simple approach like this:
class ApplicationMailer < ActionMailer::Base
default from: "noreply#theapp.com"
after_action :log_email
private
def log_email
mailer_class = self.class.to_s
mailer_action = self.action_name
EmailLog.log("#{mailer_class}##{mailer_action}", message)
end
end
With a simple model EmailLog to save the record
class EmailLog < ApplicationRecord
def self.log(email_type, message)
EmailLog.create(
email_type: email_type,
from: self.comma_separated(message.from),
to: self.comma_separated(message.to),
cc: self.comma_separated(message.cc),
subject: message.subject,
body: message.body)
end
private
def self.comma_separated(arr)
if !arr.nil? && !arr.empty?
arr.join(",")
else
""
end
end
end
If all of your mailers derive from ApplicationMailer, then you're all set.
Not sure exactly what you're asking here ... you want to track when the Mailer is used or where it's used from?
If it's the former, you could hook into method calls with something like: https://gist.github.com/ridiculous/783cf3686c51341ba32f
If it's the latter, then the only way I can think of is using __callee__ to get that info.
Hope that helps!
You can use process_action callback (why not?) to intercept mailer arguments, eg.:
class BaseMailer < ActionMailer::Base
private
# see https://api.rubyonrails.org/classes/AbstractController/Callbacks.html#method-i-process_action
def process_action(action_name, *action_args)
super
track_email_status(action_name, action_args)
end
# move these methods to the concern, copied here for the sake of simplicity!
def track_email_status(action_name, action_args)
email_status = EmailStatus.create!(
user: (action_args.first if action_args.first.is_a?(User)),
email: message.to.first,
mailer_kind: "#{self.class.name}##{action_name}",
mailer_args: tracked_mailer_args(action_name, action_args)
)
message.instance_variable_set(:#email_status, email_status)
end
def tracked_mailer_args(action_name, action_args)
args_map = method(action_name).parameters.map(&:second).zip(action_args).to_h
args_map = self.class.parameter_filter.filter(args_map)
simplify_tracked_arg(args_map.values)
end
def simplify_tracked_arg(argument)
case argument
when Hash then argument.transform_values { |v| simplify_tracked_arg(v) }
when Array then argument.map { |arg| simplify_tracked_arg(arg) }
when ActiveRecord::Base then "#{argument.class.name}##{argument.id}"
else argument
end
end
def self.parameter_filter
#parameter_filter ||= ActionDispatch::Http::ParameterFilter.new(Rails.application.config.filter_parameters)
end
end
This way you can track mailer headers/class/action_name/arguments and build sophisticated emails tracking backend. You can also use an Observer to track when email was sent:
class EmailStatusObserver
def self.delivered_email(mail)
mail.instance_variable_get(:#email_status)&.touch(:sent_at)
end
end
# config/initializers/email_status_observer.rb
ActionMailer::Base.register_observer(EmailStatusObserver)
RSpec test:
describe BaseMailer do
context 'track email status' do
let(:school) { create(:school) }
let(:teacher) { create(:teacher, school: school) }
let(:password) { 'May the Force be with you' }
let(:current_time) { Time.current.change(usec: 0) }
around { |example| travel_to(current_time, &example) }
class TestBaseMailer < BaseMailer
def test_email(user, password)
mail to: user.email, body: password
end
end
subject { TestBaseMailer.test_email(teacher, password) }
it 'creates EmailStatus with tracking data' do
expect { subject.deliver_now }.to change { EmailStatus.count }.by(1)
email_status = EmailStatus.last
expect(email_status.user_id).to eq(teacher.id)
expect(email_status.email).to eq(teacher.email)
expect(email_status.sent_at).to eq(current_time)
expect(email_status.status).to eq(:sent)
expect(email_status.mailer_kind).to eq('TestBaseMailer#test_email')
expect(email_status.mailer_args).to eq(["Teacher##{teacher.id}", '[FILTERED]'])
end
end
end
I'd probably use an alias_method_chain type approach - similar to Ryan's approach, but without using eval.
https://gist.github.com/kenmazaika/cd80b0379655d39690d8
Never been a big fan of observers.
In Rails notifications, I am subscribing to "process_action.action_controller", and would like to add more attributes to the payload. How can I do that?
I have tried using append_info_to_payload, but this seems to do nothing.
module AppendExceptionPayload
module ControllerRuntime
extend ActiveSupport::Concern
protected
def append_info_to_payload(payload)
super
payload[:happy] = "HAPPY"
end
end
end
The subscription and above code is in a Rails engine, so this is where I make the call to add it:
require 'append_exception_payload'
module Instrument
class Engine < ::Rails::Engine
ActiveSupport.on_load :action_controller do
include AppendExceptionPayload::ControllerRuntime
end
end
end
After putting up the bounty, I found a solution myself. Rails handles this really cleanly.
Basically, the append_info_to_payload method is meant exactly for this.
So to include session information and signed_in user information I added this to my application_controller.rb:
def append_info_to_payload(payload)
super
payload[:session] = request.session_options[:id] rescue ""
payload[:user_id] = session[:user_id] rescue "unknown"
end
So i jumped in and had a look at the api for the process_action method (private) and the append_info_to_payload instance method (public) and the proccess action method seems to call append_info_to_payload in its code like so:
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
result = super
payload[:status] = response.status
append_info_to_payload(payload)
result
end
and append_info_to_payload works something like this
def append_info_to_payload(payload) #:nodoc:
payload[:view_runtime] = view_runtime
end
I can suggest trying payload[:view_runtime] instead of payload[:happy] or trying to use payload[:status]
Let me know how you get on and I will try help more, unfortunately there is really no documentation for this stuff.
I want to make an object that will handle all facebook-related issues. For example, I want to say object.is_access_token_valid? or object.get_user_email or object.authenticate_user. There is a lot of complex functionality I want to package in this one object and I want to abstract this from the user of the object. In java, this would be an object. What would this be in ruby/rails?
Here's my specific situation:
I am getting this error: ActiveRecord::StatementInvalid: Could not find table
Here is my code:
class FacebookSession < ActiveRecord::Base
#include OauthHelper
attr_accessor :fb_token, :fb_client, :fb_user_id, :fb_email
def initialize
#fb_client = client # makes new oauth client
#fb_token = OAuth2::AccessToken.new client, fb_token
end
def get_email
self.fb_token.get('/me/interests')
end
def get_fb_user_id
self.fb_token.get('/me/interests')
end
def authenticate
#if this fb_user_id is in the database, log them in otherwise don't
end
def is_valid?
if(try_access_token(self.access_token))
return true
else
return false
end
end
end
If you don't extend from ActiveRecord::Base on the first line, you can get a plain-jane class, which you can use to hold any logic you want.
class FacebookSession
attr_accessor :fb_token, :fb_client, :fb_user_id, :fb_email
def initialize(client, fb_token)
#fb_client = client # makes new oauth client
#fb_token = OAuth2::AccessToken.new client, fb_token
end
def get_interests
#fb_token.get('/me/interests')
end
# etc
end
# somewhere else
session = FacebookSession.new(client, token)
email = session.get_interests