I am trying to integrate Sendgrid using their Github documentation.
In their examples they suggest that you create a Mail Helper class but give very little guidance on how to actually do this.
I have a scheduled Rake task running using Heroku Scheduler that I would like to send an email when the task is complete and was hoping to use Sendgrid for this.
Currently I have the following code in /lib/tasks/scheduler.rake
require 'sendgrid-ruby'
include SendGrid
desc "Testing Email Rake"
task :test_sendgrid => :environment do
puts 'Starting Sendgrid Email Test'
send_task_complete_email()
puts 'Sendgrid Email Test Complete'
end
def send_task_complete_email
from = Email.new(email: 'test#example.com')
to = Email.new(email: 'test#example.com')
subject = 'Sending with SendGrid is Fun'
content = Content.new(type: 'text/plain', value: 'and easy to do anywhere, even with Ruby')
mail = Mail.new(from, subject, to, content)
sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
response = sg.client.mail._('send').post(request_body: mail.to_json)
puts response.status_code
puts response.body
puts response.headers
end
I don't have the helper classes added anywhere as I am not sure which bits to add or where to put them. At the moment when I run this task I receive a 400 Bad request error back from Sendgrid and I believe it's because I don't have these helpers in place.
Any advice to fix this would be much appreciated as when I try to integrate without using the helpers and instead writing out the JSON I can successfully send the email but receive a TypeError: Mail is not a module when I try to deploy to Heroku.
UPDATE: ERROR RECEIVED USING ANSWER BELOW
400
{"errors":[{"message":"Invalid type. Expected: object, given: string.","field":"(root)","help":"http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#-Request-Body-Parameters"}]}
{"server"=>["nginx"], "date"=>["Thu, 07 Jun 2018 09:02:42 GMT"], "content-type"=>["application/json"], "content-length"=>["191"], "connection"=>["close"], "access-control-allow-origin"=>["https://sendgrid.api-docs.io"], "access-control-allow-methods"=>["POST"], "access-control-allow-headers"=>["Authorization, Content-Type, On-behalf-of, x-sg-elas-acl"], "access-control-max-age"=>["600"], "x-no-cors-reason"=>["https://sendgrid.com/docs/Classroom/Basics/API/cors.html"]}
You need to use Action Mailer:
First create a mailer class to add your mail details (i.e. UserMailer):
$ bin/rails generate mailer UserMailer
it will create the following files:
create app/mailers/user_mailer.rb
create app/mailers/application_mailer.rb
invoke erb
create app/views/user_mailer
create app/views/layouts/mailer.text.erb
create app/views/layouts/mailer.html.erb
invoke test_unit
create test/mailers/user_mailer_test.rb
create test/mailers/previews/user_mailer_preview.rb
Now edit the file app/mailers/user_mailer.rb:
require 'sendgrid-ruby'
include SendGrid
class UserMailer < ApplicationMailer
def send_task_complete_email
from = Email.new(email: 'test#example.com')
to = Email.new(email: 'test#example.com')
subject = 'Sending with SendGrid is Fun'
content = Content.new(type: 'text/plain', value: 'and easy to do anywhere, even with Ruby')
mail = Mail.new(from, subject, to, content)
sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
response = sg.client.mail._('send').post(request_body: mail.to_json)
puts response.status_code
puts response.body
puts response.headers
end
end
Then you can simply send emails using this code:
UserMailer.send_task_complete_email.deliver_now
Or from rake task:
desc "Testing Email Rake"
task :test_sendgrid => :environment do
puts 'Starting Sendgrid Email Test'
UserMailer.send_task_complete_email.deliver_now
puts 'Sendgrid Email Test Complete'
end
Update:
Because there is Mail module in Rails, you need to specify the correct SendGrid Mail module by changing:
mail = Mail.new(from, subject, to, content)
to
mail = SendGrid::Mail.new(from, subject, to, content)
Related
I have a Sidekiq worker that reaches out to an external API to get some data back. I am trying to write tests to make sure that this worker is designed and functioning correctly. The worker grabs a local model instance and examines two fields on the model. If one of the fields is nil, it will send the other field to the remote API.
Here's the worker code:
class TokenizeAndVectorizeWorker
include Sidekiq::Worker
sidekiq_options queue: 'tokenizer_vectorizer', retry: true, backtrace: true
def perform(article_id)
article = Article.find(article_id)
tokenizer_url = ENV['TOKENIZER_URL']
if article.content.nil?
send_content = article.abstract
else
send_content = article.content
end
# configure Faraday
conn = Faraday.new(tokenizer_url) do |c|
c.use Faraday::Response::RaiseError
c.headers['Content-Type'] = 'application/x-www-form-urlencoded'
end
# get the response from the tokenizer
resp = conn.post '/tokenize', "content=#{URI.encode(send_content)}"
# the response's body contains the JSON for the tokenized and vectorized article content
article.token_vector = resp.body
article.save
end
end
I want to write a test to ensure that if the article content is nil that the article abstract is what is sent to be encoded.
My assumption is that the "right" way to do this would be to mock responses with Faraday such that I expect a specific response to a specific input. By creating an article with nil content and an abstract x I can mock a response to sending x to the remote API, and mock a response to sending nil to the remote API. I can also create an article with x as the abstract and z as the content and mock responses for z.
I have written a test that generically mocks Faraday:
it "should fetch the token vector on ingest" do
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
# stub Faraday to return something without making a real request
allow_any_instance_of(Faraday::Connection).to receive(:post).and_return(
double('response', status: 200, body: "some data")
)
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: Faker::Lorem.paragraphs(number: 5).join("<br>"),
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should've saved "some data" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("some data")
end
But this mocks 100% of uses of Faraday with :post. In my particular case, I have no earthly idea how to mock a response of :post with a specific body...
It's also possible that I'm going about testing this all wrong. I could be instead testing that we are sending the right content (the test should check what is being sent with Faraday) and completely ignoring the right response.
What is the correct way to test that this worker does the right thing (sends content, or sends abstract if content is nil)? Is it to test what's being sent, or test what we are getting back as a reflection of what's being sent?
If I should be testing what's coming back as a reflection of what's being sent, how do I mock different responses from Faraday depending on the value of something being sent to it/
** note added later **
I did some more digging and thought, OK, let me test that I'm sending the request I expect, and that I'm processing the response correctly. So, I tried to use webmock.
it "should fetch token vector for article content when content is not nil" do
require 'webmock/rspec'
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url)
.with(body: 'content=y')
.to_return(body: 'y')
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: "y",
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should have sent a request with content=y
expect(stub).to have_been_requested
# we should've saved "y" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("y")
end
But I think that webmock isn't getting picked up inside the sidekiq job, because I get this:
1) Article tokenization and vectorization should fetch token vector for article content when content is not nil
Failure/Error: expect(stub).to have_been_requested
The request POST https://zzzzz/tokenize with body "content=y" was expected to execute 1 time but it executed 0 times
The following requests were made:
No requests were made.
============================================================
If I try to include webmock/rspec in any of the other places, for example, at the beginning of my file, random things start to explode. For example, if I have these lines in the beginning of this spec file:
require 'spec_helper'
require 'rails_helper'
require 'sidekiq/testing'
require 'webmock/rspec'
Then I get:
root#c18df30d6d22:/usr/src/app# bundle exec rspec spec/models/article_spec.rb:174
database: test
Run options: include {:locations=>{"./spec/models/article_spec.rb"=>[174]}}
There was an error creating the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
There was an error removing the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
Which I am guessing is because the test suite is trying to initialize stuff, but webmock is interfering...
I ended up abandoning Faraday and a more complicated test as an approach. I decomposed the worker into both a Service class and a worker. The worker simply invokes the Service class. This allows me to test the service class directly, and then just validate that the worker calls the service class correctly, and that the model calls the worker correctly.
Here's the much simpler service class:
require 'excon'
# this class is used to call out to the tokenizer service to retrieve
# a tokenized and vectorized JSON to store in an article model instance
class TokenizerVectorizerService
def self.tokenize(content)
tokenizer_url = ENV['TOKENIZER_URL']
response = Excon.post("#{tokenizer_url}/tokenize",
body: URI.encode_www_form(content: content),
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
expects: [200])
# the response's body contains the JSON for the tokenized and vectorized
# article content
response.body
end
end
Here's the test to see that we are calling the right destination:
require 'rails_helper'
require 'spec_helper'
require 'webmock/rspec'
RSpec.describe TokenizerVectorizerService, type: :service do
describe "tokenize" do
it "should send the content passed in" do
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url).
with(
body: {"content"=>"y"},
headers: {
'Content-Type'=>'application/x-www-form-urlencoded',
}).
to_return(status: 200, body: "y", headers: {})
TokenizerVectorizerService.tokenize("y")
expect(stub).to have_been_requested
end
end
end
I'm working with SendGrid support to determine why categories stopped working on my multipart email campaigns (the text-only one is fine). If I intentionally set the content-type of an HTML email as "text/plain" the email displays the header data, text and raw html all on a single email, but will get its category. Otherwise the email looks correct, but there's no category.
SendGrid has asked me to send them a copy of the payload and I'm not sure what that is or how to find it. They said "If you are familiar with running a telnet test then that is what we are looking for." I'm not familiar with telnet tests. This is the info from the screenshot they provided as an example of what they're looking for:
220 Hi! This is Rob's hMailServer!
ehlo panoply-tech.com
250-SAGE013963
250-SIZE 20480000
250 AUTH LOGIN PLAIN
AUTH LOGIN
334 VXN1ea5bbVUG
YT3TQBHbhM9WBHKTDGUjeD65WQ20=
235 authenticated.
MAIL FROM: mayes#panoply-tech.com
250 OK
RCPT TO: cstickings#demosagecrm.com
250 OK
DATA
354 OK, send.
Subject: This is a test email
Hi Clemence,
Just sending you a test email.
.
250 Queued <25.927 seconds>
I went to .rvm/gems/ruby-2.3.3/gems/actionmailer-4.2.8/lib/action_mailer/base.rb and found a method called "set_payload_for_mail" but what that produces does seem to be like their example:
{"mailer":"B2c::B2cSendGridMailer",
"message_id":"5d0b979767c26_16f2c3fc04043f9c84968e#Domain-Person.local.mail",
"subject":"TEST: 26_txt","to":["person#domain.com"],
"from":["info#another.com"],"date":"2019-06-20T09:26:31.000-05:00",
"mail":"Date: Thu, 20 Jun 2019 09:26:31 -0500\r\nFrom: info#another.com\r\nTo: person#domain.com\r\nMessage-ID: \u003c5d0b979767c26_16f2c3fc04043f9c84968e#Domain-Person.local.mail\u003e\r\nSubject: TEST: 26_txt\r\nMime-Version: 1.0\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n
X-SMTPAPI: {\"category\":[\"html_false\"]}\r\n
X-SMTPAPI: {\"filters\": {\"ganalytics\": {\"settings\": {\"enable\":1}}}}
\r\n\r\nHi there, but text\r\n"}
I know in the Google inbox, you can click "Show Original" for an email and see the header info, etc. I've sent that to them but that didn't have what they needed.
def b2c_tester(html=false, content)
e_domain = 'careinhomes.com'
#mailer_path = "app/views/b2c/b2c_send_grid_mailer"
#from = "info#careinhomes.com"
#recipients = ['gina#pipelinesuccess.com']
#subject = html ? "#{DateTime.now.minute.to_s}_html" :
"#{DateTime.now.minute.to_s}_txt"
header_category = {"category": ["html_#{html}"]}
headers['X-SMTPAPI'] = header_category.to_json
if html
msg = tester_mail_with_opts({domain: e_domain}, content)
else
msg = tester_mail_plain_text_with_opts(
"b2c_tester",{domain: e_domain})
end
msg
end
#content ex: 'text/plain', 'text/html', 'multipart/alternative', etc
def tester_mail_with_opts(delivery_options={}, content=nil)
mail_opts = set_mail_opts(delivery_options)
unless content.nil?
mail_opts[:content_type] = content
end
mail mail_opts
end
def set_mail_opts(delivery_options={})
#subject = "TEST: #{#subject}" unless Rails.env.production?
# Required
mail_opts = {
to: #recipients,
from: #from,
subject: #subject,
}
mail_opts[:template_path] = #template_path if #template_path
mail_opts[:content_type] = #content_type if #content_type
# Do delivery options
mail_opts[:delivery_method_options] = DELIVERY_OPTIONS
mail_opts[:delivery_method_options] =
mail_opts[:delivery_method_options].merge(delivery_options)
unless delivery_options.blank?
mail_opts
end
In ActionMailer's base model is a method called deliver_mail that extracts the payload and you can capture it that way. It appears that, for my problem, the payload is an empty hash.
This is what a healthy payload should look like from ActionMailer:
{"mailer":"B2c::B2cSendGridMailer","message_id":"5d10dacb26dc2_17fb93ff52483b9c8952bf#Domain-Person.local.mail","subject":"TEST: 14_txt","to":["person#domain.com"],"from":["info#another.com"],"date":"2019-06-24T09:14:35.000-05:00","mail":"Date: Mon, 24 Jun 2019 09:14:35 -0500\r\nFrom: info#another.com\r\nTo: person#domain.com\r\nMessage-ID: \u003c5d10dacb26dc2_17fb93ff52483b9c8952bf#Domain-Person.local.mail\u003e\r\nSubject: TEST: 14_txt\r\nMime-Version: 1.0\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\nX-SMTPAPI: {\"category\":[\"html_false\"]}\r\nX-SMTPAPI: {\"filters\": {\"ganalytics\": {\"settings\": {\"enable\":1}}}}\r\n\r\nHi there, but text\r\n"}
i'm want to generate CSV data and send it via mail to some email-address. For the generation of the CSV i'm using FasterCSV with the following code:
csv_data = FasterCSV.generate(:col_sep => ";") do |csv|
csv << ["timestamp", "staff_firstname", "staff_lastname", "message"]
log.each do |log_entry|
csv << [log_entry.timestamp, log_entry.staff_firstname, log_entry.staff_lastname, log_entry.message]
end
end
The csv_data i want to send via a ActionMailer method and therefore i'm using the following code:
def log_csv_export(log_csv, email)
mail.attachments["log.csv"] = log_csv
mail(:to => email, :subject => 'Export Log' )
end
To call the ActionMailer method i'm using:
AccountMailer.log_csv_export(csv_data, email).deliver
If I test it, the mail was send to the transmitted email address, but without an attachment. The csv-data is shown as plain text in the email, but not as attachment to save.
This problem only occurs if i send the mail via heroku mailgun. If i'm testing it with
ActionMailer::Base.delivery_method = :sendmail in the config, then it works.
Did someone knows what the issue is or what i need to change that it works?
Thank you.
Email from Ruby code is sending without any issue. But when I try to send it from Rails console, it gives the Missing required header 'From' error even From is specified in the email (Notifier), see below:
def send_email(user)
recipients "#{user.email}"
from %("Name" "<name#domain.com>")
subject "Subject Line"
content_type "text/html"
end
Here is email sending code from rails console:
ActionMailer::Base.delivery_method = :amazon_ses
ActionMailer::Base.custom_amazon_ses_mailer = AWS::SES::Base.new(:secret_access_key => 'abc', :access_key_id => '123')
Notifier.deliver_send_email
Am I missing something?
GEMS used: rails (2.3.5), aws-ses (0.4.4), mail (2.3.0)
Here is how it was fixed:
def send_email(user)
recipients "#{user.email}"
from "'First Last' <name#domain.com>" # changing the FROM format fixed it.
subject "Subject Line"
content_type "text/html"
end
I am using PDFKit on Heroku (and also locally for dev) and am having difficulty emailing the generating PDF from my cron task.
This is the code in my cron job:
kit = PDFKit.new(html_file)
file = kit.to_pdf
OutboundMailer.deliver_pdf_email(file)
#kit.render_file("pdf_from_cron.pdf")
This is the code in my Mailer:
def pdf_email(pdf)
subject "This is your batch of letters"
recipients "helloworld#gmail.com"
from "Batch Email <hello#batch.com>"
sent_on Date.today
body "This is the body of pdf"
attachment "application/pdf" do |a|
a.filename = "batch-letters.pdf"
a.body = pdf
end
end
I setup PDFKit on Heroku once and followed: http://blog.mattgornick.com/using-pdfkit-on-heroku. You may need to include the binary.