Sending emails in Rails using HTTP protocol(MailGun API) - ruby-on-rails

I am trying to sending emails using MailGun's batch sending API using MailGun ruby sdk(https://github.com/mailgun/mailgun-ruby/blob/master/docs/MessageBuilder.md). As of now I have this method inside a class which inherits from ActionMailer.
class BatchMailer < ApplicationMailer
def send_batch_email(mail, recipients)
# First, instantiate the Mailgun Client with your API key
mg_client = Mailgun::Client.new("your-api-key")
# Create a Batch Message object, pass in the client and your domain.
mb_obj = Mailgun::BatchMessage.new(mg_client, "example.com")
# Define the from address.
mb_obj.from("me#example.com", {"first" => "Ruby", "last" => "SDK"});
# Define the subject.
mb_obj.subject("A message from the Ruby SDK using Message Builder!");
# Define the body of the message.
mb_obj.body_text("This is the text body of the message!");
# Loop through all of your recipients
mb_obj.add_recipient(:to, "john.doe#example.com", {"first" => "John", "last" => "Doe"});
mb_obj.add_recipient(:to, "jane.doe#example.com", {"first" => "Jane", "last" => "Doe"});
mb_obj.add_recipient(:to, "bob.doe#example.com", {"first" => "Bob", "last" => "Doe"});
...
mb_obj.add_recipient(:to, "sally.doe#example.com", {"first" => "Sally", "last" => "Doe"});
# Call finalize to get a list of message ids and totals.
message_ids = mb_obj.finalize
# {'id1234#example.com' => 1000, 'id5678#example.com' => 15}
end
end
Is is a correct way to keep the method that doesn't use actionmailer to send emails inside mailer?
ActionMailer method returns mail object but when trying to write spec for the method that uses API to send emails I can't able to get response as there won't be a mail object(ActionMailer message object). Where to keep this method and how it can be tested?

Is this a correct way to keep the method that doesn't use actionmailer to send emails inside mailer?
There is no reason to use a Mailer in this case. Simply use a service object (a plain-old ruby object or PORO). It might look something like:
class BatchMailerService
attr_accessor *%w(
mail
recipients
recipient
).freeze
delegate *%w(
from
subject
body_text
add_recipient
finalize
), to: :mb_obj
delegate *%w(
address
first_name
last_name
), to: :recipient, prefix: true
class << self
def call(mail, recipients)
new(mail, recipients).call
end
end # Class Methods
#==============================================================================================
# Instance Methods
#==============================================================================================
def initialize(mail, recipients)
#mail, #recipients = mail, recipients
end
def call
setup_mail
add_recipients
message_ids = finalize
end
private
def mg_client
#mg_client ||= Mailgun::Client.new(ENV["your-api-key"])
end
def mb_obj
#mb_obj ||= Mailgun::BatchMessage.new(mg_client, "example.com")
end
def setup_mail
from("me#example.com", {"first" => "Ruby", "last" => "SDK"})
subject("A message from the Ruby SDK using Message Builder!")
body_text("This is the text body of the message!")
end
def add_recipients
recipients.each do |recipient|
#recipient = recipient
add_recipient(
:to,
recipient_address,
{
first: recipient_first_name,
last: recipient_last_name
}
)
end
end
end
Which you would use something like:
BatchMailerService.call(mail, recipients)
(assuming, naturally, that you have variables called mail and recipients).
Where to keep this method?
You might place that file in app/services/batch_mailer_service.rb.
How can it be tested?
What do you mean? How you test the service depends on what your criteria for success are. You could test that mb_obj receives the finalize call (maybe using something like expect().to receive). You could test message_ids contains the correct information (maybe using something like expect().to include). It sort of depends.

Related

Rails stub method on initialized ActiveRecord object

I want to stub set_user_tokens which is executed on the initialized (not saved) ActiveRecord object. This method assigns a token to the login object.
class AwareLogin < Authenticatable
def authenticate!
login = Login.find_or_initialize_for_authentication(params[:login][:email], 'aware')
class << login
include AwareAuth
end
if login.valid_password?(password) || (set_token = login.id.nil? && aware_response.success?)
login.set_user_tokens(aware_response) if set_token
success!(login)
else
aware_response.success? ? fail!(:aware_auth) : raise(ActiveRecord::Rollback)
end
end
end
end
So I want to stub setu_user_tokens method:
login.set_user_tokens(aware_response) if set_token
to receive login ActiveRecord object with attributes of oauth_tokens like below:
login.oauth_token
=> {"access_token" => return_token,"refresh_token" => nil,"token_expiration" => 1200 }
I've tried:
allow_any_instance_of(Login).to receive(:set_user_tokens).with(status: 200, body: { access_token: return_token }.to_json, success?: true).and_return(
oauth_tokens: {
"access_token" => return_token,
"refresh_token" => nil,
"token_expiration" => 1200 },
)
But I'm getting an error:
Login does not implement #set_user_tokens
I would be willing to bet your issue is set_user_tokens is part of AwareAuth.
Since you are only including this module in the eigenclass of the instance (login) as part of the AwareLogin#authenticate! method, the Login class does not implement that method at any point in time.
Is there a reason you are doing it this way rather than just including AwareAuth in the Login class in the first place?
Either way, while your question appears to lack context for the test itself, if I understand correctly, we should be able to resolve these issues as follows:
it 'sets user tokens' do
login = Login
.find_or_initialize_for_authentication('some_email#example.com', 'aware')
.tap {|l| l.singleton_class.send(:include, AwareAuth) }
allow(Login).to receive(:find_or_initialize_for_authentication).and_return(login)
allow(login).to receive(:set_user_tokens).and_return(
oauth_tokens: {
"access_token" => return_token,
"refresh_token" => nil,
"token_expiration" => 1200 }
)
#perform your action and expectations here
end
By using partial doubles you can stub the specific methods you need to without impacting any other functionality of the object itself.

bulk email using send grid in rails

Context:
I need to send bulk-email using send grid in a rails app.
I will be sending emails to maybe around 300 subscribers.
I have read that it can be accomplished using
headers["X-SMTPAPI"] = { :to => array_of_recipients }.to_json
I have tried following that.
The following is my ActionMailer:
class NewJobMailer < ActionMailer::Base
default from: "from#example.com"
def new_job_post(subscribers)
#greeting = "Hi"
headers['X-SMTPAPI'] = { :to => subscribers.to_a }.to_json
mail(
:to => "this.will#be.ignored.com",
:subject => "New Job Posted!"
)
end
end
I call this mailer method from a controller
..
#subscribers = Subscriber.where(activated: true)
NewJobMailer.new_job_post(#subscribers).deliver
..
The config for send-grid is specified in the config/production.rb file and is correct, since I am able to send out account activation emails.
Problem:
The app works fine without crashing anywhere, but the emails are not being sent out.
I am guessing the headers config is not being passed along ?
How can I correct this ?
UPDATE:
I checked for email activity in the send grid dashboard.
Here is a snapshot of one of the dropped emails:
You are grabbing an array of ActiveRecord objects with
#subscribers = Subscriber.where(activated: true)
and passing that into the smtpapi header. You need to pull out the email addresses of those ActiveRecord objects.
Depending on what you called the email field, this can be done with
headers['X-SMTPAPI'] = { :to => subscribers.map(&:email) }.to_json

Rails delayed_job argument error with mailer

I'm getting a "wrong number of arguments (2 for 1)" error when I try send a mailer to delayed_job. Everything works perfect if I don't try to push it to the background. Below is my controller with delayed_job:
def export
#user = User.where("id = ?", params[:user_id])
#logs = VehicleMileage.export_logs(params, #user)
if #logs['log_count'] > 0
ExportLogsMailer.delay.email_logs(#user, #logs['sending_to'])
end
respond_to do |format|
formats # index.html.erb
format.json { render json: #logs }
end
end
When I ExportLogsMailer.email_logs(#user, #logs['sending_to']).deliver the mailer works fine. I'm using the gem 'delayed_job_active_record' and gem 'rails', '3.2.13'.
Here is what ExportLogsMailer looks like:
class ExportLogsMailer < ActionMailer::Base
default :from => "support#myconsultantapp.com"
def email_logs(user, emails)
# make sure emails are unique
emails.uniq
first_name = user[0].first_name
last_name = user[0].last_name
# encoded_content = Base64.strict_encode64(File.read("#{Rails.root}/tmp/test.xls"))
# puts encoded_content
# attachments['test.xls'] = { :content => encoded_content,
# :encoding => 'Base64'
# }
# Name of template in your Mandrill template area
headers['X-MC-Template'] = 'Export Logs'
# Tags help classify your messages
headers['X-MC-Tags'] = 'mileage logs'
# Enable open or click-tracking for the message.
# Only can track to at a time. Possibilies: opens, clicks, clicks_htmlonly, clicks_textonly
headers['X-MC-Track'] = 'opens, clicks'
# Automatically generate a plain-text version of the email from the HTML content.
headers['X-MC-Autotext'] = 'true'
# Add dynamic data to replace mergetags that appear in your message content. Should be a JSON-formatted
# object and flat, so if you more than one recipient then add another X-MC-MergeVars to the header. The
# below example will change anywhere where *|FNAME|* or *|LNAME|* to the respective value.
mergeVars =
{
"fname" => first_name,
"lname" => last_name
}
headers['X-MC-MergeVars'] = mergeVars.to_json
# Add Google Analytics tracking to links in your email for the specified domains.
headers['X-MC-GoogleAnalytics'] = 'http://www.myconsultantapp.com/'
# Add an optional value to be used for the utm_campaign parameter in Google Analytics tracked links.
# headers['X-MC-GoogleAnalyticsCampaign'] = ''
# Information about any custom fields or data you want to append to the message.
# Up to 200 bytes of JSON-encoded data as an object. The object should be flat; nested object structures are not supported.
# headers['X-MC-Metadata'] = ''
# Whether to strip querystrings from links for reporting. "true" or "false"
# headers['X-MC-URLStripQS'] = 'true'
# Whether to show recipients of the email other recipients, such as those in the "cc" field. "true" or "false"
headers['X-MC-PreserveRecipients'] = 'false'
message = prepare_message :subject => "1 myConsultant logs",
:to => emails,
:content_type => "multipart/mixed"
message.alternative_content_types_with_attachment(
:text => 'text',
:html => 'html'
) do |i|
i.inline['test.xls'] = File.read("#{Rails.root}/tmp/test.xls")
end
message
end
end
Any help would be greatly appreciated. Thanks you!
Are you trying to retrieve just one user? Then do:
#user = User.find(params[:user_id])
Otherwise, if you're trying to pass an array of objects to delayed_job, I think you need to add all at the end:
#objects = Model.where(...).all

Ruby - Ignore protected attributes

How can I tell Ruby (Rails) to ignore protected variables which are present when mass-assigning?
class MyClass < ActiveRecord::Base
attr_accessible :name, :age
end
Now I will mass-assign a hash to create a new MyClass.
MyClass.create!({:name => "John", :age => 25, :id => 2})
This will give me an exception:
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: id
I want it to create a new MyClass with the specified (unprotected) attributes and ignore the id attribute.
On the side note: How can I also ignore unknown attributes. For example, MyClass doesn't have a location attribute. If I try to mass-assign it, just ignore it.
Use Hash#slice to only select the keys you're actually interested in assigning:
# Pass only :name and :age to create!
MyClass.create!(params.slice(:name, :age))
Typically, I'll add wrapper method for params to my controller which filters it down to only the fields that I know I want assigned:
class MyController
# ...
def create
#my_instance = MyClass.create!(create_params)
end
protected
def create_params
params.slice(:name, :age)
end
end
Setting mass_assignment_sanitizer to :logger solved the issue in development and test.
config.active_record.mass_assignment_sanitizer = :logger
You can use strong_parameters gem, that will be in rails 4.
See the documentation here.
This way you can specify the params you want by action or role, for example.
If you want to get down and dirty with it, and dynamically let only a model's attributes through, without disabling ActiveModel::MassAssignmentSecurity::Errors globally:
params = {:name => "John", :age => 25, :id => 2}
MyClass.create!(params.slice(*MyClass.new.attributes.symbolize_keys.keys)
The .symbolize_keys is required if you are using symbols in your hash, like in this situation, but you might not need that.
Personally, I like to keep things in the model by overriding assign_attributes.
def assign_attributes(new_attributes, options = {})
if options[:safe_assign]
authorizer = mass_assignment_authorizer(options[:as])
new_attributes = new_attributes.reject { |key|
!has_attribute?(key) || authorizer.deny?(key)
}
end
super(new_attributes, options)
end
Use it similarly to :without_protection, but for when you want to ignore unknown or protected attributes:
MyModel.create!(
{ :asdf => "invalid", :admin_field => "protected", :actual_data => 'hello world!' },
:safe_assign => true
)
# => #<MyModel actual_data: "hello world!">

How to convert a Ruby object to JSON

I would like to do something like this:
require 'json'
class Person
attr_accessor :fname, :lname
end
p = Person.new
p.fname = "Mike"
p.lname = "Smith"
p.to_json
Is it possible?
Yes, you can do it with to_json.
You may need to require 'json' if you're not running Rails.
To make your Ruby class JSON-friendly without touching Rails, you'd define two methods:
to_json, which returns a JSON object
as_json, which returns a hash representation of the object
When your object responds properly to both to_json and as_json, it can behave properly even when it is nested deep inside other standard classes like Array and/or Hash:
#!/usr/bin/env ruby
require 'json'
class Person
attr_accessor :fname, :lname
def as_json(options={})
{
fname: #fname,
lname: #lname
}
end
def to_json(*options)
as_json(*options).to_json(*options)
end
end
p = Person.new
p.fname = "Mike"
p.lname = "Smith"
# case 1
puts p.to_json # output: {"fname":"Mike","lname":"Smith"}
# case 2
puts [p].to_json # output: [{"fname":"Mike","lname":"Smith"}]
# case 3
h = {:some_key => p}
puts h.to_json # output: {"some_key":{"fname":"Mike","lname":"Smith"}}
puts JSON.pretty_generate(h) # output
# {
# "some_key": {
# "fname": "Mike",
# "lname": "Smith"
# }
# }
Also see "Using custom to_json method in nested objects".
Try it. If you're using Ruby on Rails (and the tags say you are), I think this exact code should work already, without requiring anything.
Rails supports JSON output from controllers, so it already pulls in all of the JSON serialization code that you will ever need. If you're planning to output this data through a controller, you might be able to save time by just writing
render :json => #person

Resources