diff --git a/.gitignore b/.gitignore index 166267f4..9269680f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,3 @@ Gemfile.lock # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc - diff --git a/README.md b/README.md index f9ae89c9..38a0f824 100644 --- a/README.md +++ b/README.md @@ -106,11 +106,11 @@ params = { :reply_to, :date, :smtpapi, - :attachments + :attachments, + :template } ``` - #### Setting Params Params can be set in the usual Ruby ways, including a block or a hash. @@ -187,9 +187,67 @@ mail = SendGrid::Mail.new mail.html = 'Stuff in here, yo!' ``` +#### :template -## Using SendGrid's X-SMTPAPI Header +The **:template** param allows us to specify a template object for this email to use. The initialized `Template` will automatically be included in the `smtpapi` header and passed to SendGrid. + +```ruby +template = SendGrid::Template.new('MY_TEMPLATE_ID') +mail.template = template +``` + +## Working with Templates + +Another easy way to use the [SendGrid Templating](https://sendgrid.com/docs/API_Reference/Web_API_v3/Template_Engine/index.html) system is with the `Recipient`, `Mail`, `Template`, and `TemplateMailer` objects. + +Create some `Recipients` + +```ruby +users = User.where(email: ['first@gmail.com', 'second@gmail.com']) + +recipients = [] + +users.each do |user| + recipient = SendGrid::Recipient.new(user.email) + recipient.add_substitution('first_name', user.first_name) + recipient.add_substitution('city', user.city) + recipients << recipient +end +``` + +Create a `Template` + +```ruby +template = SendGrid::Template.new('MY_TEMPLATE_ID') +``` + +Create a `Client` + +```ruby +client = SendGrid::Client.new(api_user: my_user, api_key: my_key) +``` + +Initialize mail defaults and create the `TemplateMailer` + +```ruby +mail_defaults = { + from: 'admin@email.com', + html: '

I like email

', + text: 'I like email' + subject: 'Email is great', +} + +mailer = TemplateMailer.new(client, template, recipients) +``` + +Mail it! + +```ruby +mailer.mail(mail_defaults) +``` + +## Using SendGrid's X-SMTPAPI Header
To utilize the X-SMTPAPI header, we have directly integrated the SendGridJP/smtpapi-ruby gem. @@ -233,3 +291,4 @@ mail.smtpapi = header 5. Create a new Pull Request ***Hit up [@rbin](http://twitter.com/rbin) or [@sendgrid](http://twitter.com/sendgrid) on Twitter with any issues.*** + diff --git a/lib/sendgrid-ruby.rb b/lib/sendgrid-ruby.rb index 2dfd8b90..17153b5c 100644 --- a/lib/sendgrid-ruby.rb +++ b/lib/sendgrid-ruby.rb @@ -1,5 +1,8 @@ require_relative 'sendgrid/client' require_relative 'sendgrid/exceptions' +require_relative 'sendgrid/recipient' +require_relative 'sendgrid/template' +require_relative 'sendgrid/template_mailer' require_relative 'sendgrid/mail' require_relative 'sendgrid/response' require_relative 'sendgrid/version' diff --git a/lib/sendgrid/client.rb b/lib/sendgrid/client.rb index 0abac194..6c59299b 100644 --- a/lib/sendgrid/client.rb +++ b/lib/sendgrid/client.rb @@ -3,7 +3,7 @@ module SendGrid class Client attr_accessor :api_user, :api_key, :protocol, :host, :port, :url, :endpoint, - :user_agent + :user_agent, :template attr_writer :adapter, :conn, :raise_exceptions def initialize(params = {}) @@ -25,7 +25,7 @@ def send(mail) res = conn.post do |req| payload = mail.to_h req.url(endpoint) - + # Check if using username + password or API key if api_user # Username + password @@ -34,12 +34,12 @@ def send(mail) # API key req.headers['Authorization'] = "Bearer #{api_key}" end - + req.body = payload end - + fail SendGrid::Exception, res.body if raise_exceptions? && res.status != 200 - + SendGrid::Response.new(code: res.status, headers: res.headers, body: res.body) end diff --git a/lib/sendgrid/mail.rb b/lib/sendgrid/mail.rb index eedb7f12..58a5393a 100644 --- a/lib/sendgrid/mail.rb +++ b/lib/sendgrid/mail.rb @@ -5,12 +5,13 @@ module SendGrid class Mail attr_accessor :to, :to_name, :from, :from_name, :subject, :text, :html, :cc, - :bcc, :reply_to, :date, :smtpapi, :attachments, :content + :bcc, :reply_to, :date, :smtpapi, :attachments, :content, :template def initialize(params = {}) params.each do |k, v| send(:"#{k}=", v) unless v.nil? end + yield self if block_given? end @@ -103,11 +104,11 @@ def headers def attachments @attachments ||= [] end - + def contents @contents ||= [] end - + def add_content(path, cid) mime_type = MimeMagic.by_path(path) file = Faraday::UploadIO.new(path, mime_type) @@ -119,6 +120,14 @@ def smtpapi @smtpapi ||= Smtpapi::Header.new end + def smtpapi_json + if !template.nil? && template.is_a?(Template) + template.add_to_smtpapi(smtpapi) + end + + smtpapi.to_json + end + # rubocop:disable Style/HashSyntax def to_h payload = { @@ -133,7 +142,7 @@ def to_h :bcc => bcc, :text => text, :html => html, - :'x-smtpapi' => smtpapi.to_json, + :'x-smtpapi' => smtpapi_json, :content => ({":default"=>"0"} unless contents.empty?), :files => ({":default"=>"0"} unless attachments.empty? and contents.empty?) # If I don't define a default value, I get a Nil error when @@ -144,7 +153,7 @@ def to_h payload.delete(:'x-smtpapi') if payload[:'x-smtpapi'] == '{}' payload[:to] = payload[:from] if payload[:to].nil? and not smtpapi.to.empty? - + unless attachments.empty? attachments.each do |file| payload[:files][file[:name]] = file[:file] @@ -153,7 +162,7 @@ def to_h payload[:files].delete(":default") end end - + unless contents.empty? contents.each do |content| payload[:content][content[:name]] = content[:cid] @@ -163,7 +172,7 @@ def to_h payload[:content].delete(":default") end end - + payload end # rubocop:enable Style/HashSyntax diff --git a/lib/sendgrid/recipient.rb b/lib/sendgrid/recipient.rb new file mode 100644 index 00000000..181eff3f --- /dev/null +++ b/lib/sendgrid/recipient.rb @@ -0,0 +1,29 @@ +require 'smtpapi' + +module SendGrid + class Recipient + class NoAddress < StandardError; end + + attr_reader :address, :substitutions + + def initialize(address) + @address = address + @substitutions = {} + + raise NoAddress, 'Recipient address cannot be nil' if @address.nil? + end + + def add_substitution(key, value) + substitutions[key] = value + end + + def add_to_smtpapi(smtpapi) + smtpapi.add_to(@address) + + @substitutions.each do |key, value| + existing = smtpapi.sub[key] || [] + smtpapi.add_substitution(key, existing + [value]) + end + end + end +end diff --git a/lib/sendgrid/template.rb b/lib/sendgrid/template.rb new file mode 100644 index 00000000..867b3451 --- /dev/null +++ b/lib/sendgrid/template.rb @@ -0,0 +1,26 @@ +require 'smtpapi' + +module SendGrid + class Template + attr_reader :id, :recipients + + def initialize(id) + @id = id + @recipients = [] + end + + def add_recipient(recipient) + recipients << recipient + end + + def add_to_smtpapi(smtpapi) + return if smtpapi.nil? + + smtpapi.tap do |api| + api.add_filter(:templates, :enable, 1) + api.add_filter(:templates, :template_id, id) + recipients.each { |r| r.add_to_smtpapi(smtpapi) } + end + end + end +end diff --git a/lib/sendgrid/template_mailer.rb b/lib/sendgrid/template_mailer.rb new file mode 100644 index 00000000..724963bc --- /dev/null +++ b/lib/sendgrid/template_mailer.rb @@ -0,0 +1,59 @@ +module SendGrid + class InvalidClient < StandardError; end + class InvalidTemplate < StandardError; end + class InvalidRecipients < StandardError; end + + class TemplateMailer + + # This class is responsible for coordinating the responsibilities + # of various models in the gem. + # It makes use of the Recipient, Template and Mail models to create + # a single work flow, an example might look like: + # + # users = User.where(email: ['first@gmail.com', 'second@gmail.com']) + # + # recipients = [] + # + # users.each do |user| + # recipient = SendGrid::Recipient.new(user.email) + # recipient.add_substitution('first_name', user.first_name) + # recipient.add_substitution('city', user.city) + # + # recipients << recipient + # end + # + # template = SendGrid::Template.new('MY_TEMPLATE_ID') + # + # client = SendGrid::Client.new(api_user: my_user, api_key: my_key) + # + # mail_defaults = { + # from: 'admin@email.com', + # html: '

I like email

', + # text: 'I like email' + # subject: 'Email is great', + # } + # + # mailer = TemplateMailer.new(client, template, recipients) + # mailer.mail(mail_defaults) + def initialize(client, template, recipients = []) + @client = client + @template = template + @recipients = recipients + + raise InvalidClient, 'Client must be present' if @client.nil? + raise InvalidTemplate, 'Template must be present' if @template.nil? + raise InvalidRecipients, 'Recipients may not be empty' if @recipients.empty? + + @recipients.each do |recipient| + @template.add_recipient(recipient) + end + end + + def mail(params = {}) + mail = Mail.new(params) + + mail.template = @template + @client.send(mail.to_h) + end + end +end diff --git a/spec/lib/sendgrid/client_spec.rb b/spec/lib/sendgrid/client_spec.rb index 6600c13e..2a7979e4 100644 --- a/spec/lib/sendgrid/client_spec.rb +++ b/spec/lib/sendgrid/client_spec.rb @@ -23,6 +23,10 @@ expect(SendGrid::Client.new.endpoint).to eq('/api/mail.send.json') end + it 'accepts a block' do + expect { |b| SendGrid::Client.new(&b) }.to yield_control + end + describe ':send' do it 'should make a request to sendgrid' do stub_request(:any, 'https://api.sendgrid.com/api/mail.send.json') diff --git a/spec/lib/sendgrid/mail_spec.rb b/spec/lib/sendgrid/mail_spec.rb index 4ce828ca..950714b4 100644 --- a/spec/lib/sendgrid/mail_spec.rb +++ b/spec/lib/sendgrid/mail_spec.rb @@ -120,4 +120,32 @@ expect(@mail.reply_to).to eq('foo@example.com') end end + + describe 'smtpapi_json' do + before do + @mail.template = template + end + + context 'a template has been set' do + let(:template) { SendGrid::Template.new(anything) } + + it 'adds the template to the smtpapi header' do + expect(@mail.template).to receive(:add_to_smtpapi).with(@mail.smtpapi) + expect(@mail.smtpapi).to receive(:to_json) + + @mail.to_h + end + end + + context 'no template has been set' do + let(:template) { nil } + + it 'does not add anything to the smtpapi header' do + expect_any_instance_of(SendGrid::Template).to_not receive(:add_to_smtpapi) + expect(@mail.smtpapi).to receive(:to_json) + + @mail.to_h + end + end + end end diff --git a/spec/lib/sendgrid/recipient_spec.rb b/spec/lib/sendgrid/recipient_spec.rb new file mode 100644 index 00000000..db9b1304 --- /dev/null +++ b/spec/lib/sendgrid/recipient_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../../lib/sendgrid/recipient' + +module SendGrid + describe Recipient do + subject { described_class.new(anything) } + + describe '#initialize' do + it 'sets the address instance var' do + expect(subject.instance_variable_get(:@address)).to_not be_nil + end + + it 'sets substitutions to an empty hash' do + expect(subject.instance_variable_get(:@substitutions)).to eq({}) + end + + context 'initialized with nil' do + it 'raises an error' do + expect do + described_class.new(nil) + end.to raise_error(Recipient::NoAddress, 'Recipient address cannot be nil') + end + end + end + + describe '#add_substitution' do + it 'adds the key and value to the substitutions hash' do + subject.add_substitution(:foo, :bar) + expect(subject.substitutions).to have_key(:foo) + expect(subject.substitutions[:foo]).to eq(:bar) + end + + context 'the same substiution key already exists' do + before do + subject.add_substitution(:foo, :bar) + end + + it 'replaces the value' do + subject.add_substitution(:foo, :baz) + expect(subject.substitutions).to have_key(:foo) + expect(subject.substitutions[:foo]).to eq(:baz) + end + end + end + + describe '#add_to_smtpapi' do + let(:substitutions) { { foo: :bar, baz: :qux } } + let(:smtp_api) { Smtpapi::Header.new } + before do + substitutions.each do |key, value| + subject.add_substitution(key, value) + end + end + + it 'adds the address' do + expect(smtp_api).to receive(:add_to) + subject.add_to_smtpapi(smtp_api) + end + + it 'calls add_substitution as many times as there are substitution keys' do + substitutions.each do |key, value| + expect(smtp_api).to receive(:add_substitution).with(key, [value]) + end + + subject.add_to_smtpapi(smtp_api) + end + + context 'a substitution for the same key already exists' do + let(:substitutions) { { foo: :bar } } + let(:added_value) { [:bar, :rab] } + + before do + smtp_api.add_substitution(:foo, [:rab]) + end + + it 'adds to it' do + expect(smtp_api).to receive(:add_substitution).with(:foo, array_including(added_value)) + subject.add_to_smtpapi(smtp_api) + end + end + + context 'substitutions is empty' do + let(:substitutions) { {} } + + it 'does nothing' do + expect(smtp_api).to_not receive(:add_substitution) + subject.add_to_smtpapi(smtp_api) + end + end + end + end +end diff --git a/spec/lib/sendgrid/template_mailer_spec.rb b/spec/lib/sendgrid/template_mailer_spec.rb new file mode 100644 index 00000000..1105baf1 --- /dev/null +++ b/spec/lib/sendgrid/template_mailer_spec.rb @@ -0,0 +1,86 @@ +require_relative '../../../lib/sendgrid/template_mailer' + +module SendGrid + describe TemplateMailer do + let(:client) { anything } + let(:template) { Template.new(anything) } + let(:recipients) { [Recipient.new(anything)] } + + describe '#initialize' do + let(:client) { anything } + let(:template) { Template.new(anything) } + let(:recipients) { [anything] } + + subject { described_class.new(client, template, recipients) } + + it 'sets the instance variables' do + expect(subject.instance_variable_get(:@client)).to_not be_nil + expect(subject.instance_variable_get(:@template)).to_not be_nil + expect(subject.instance_variable_get(:@recipients)).to_not be_nil + end + + context 'nil variables' do + context 'template is nil' do + let(:template) { nil } + + it 'raises error' do + expect do + subject + end.to raise_error(InvalidTemplate, 'Template must be present') + end + end + + context 'client is nil' do + let(:client) { nil } + + it 'raises error' do + expect do + subject + end.to raise_error(InvalidClient, 'Client must be present') + end + end + end + + context 'recipients' do + let(:first_recipient) { Recipient.new('someone@anything.com') } + let(:second_recipient) { Recipient.new('test@test.com') } + let(:recipients) { [first_recipient, second_recipient] } + + it 'adds them to the template' do + expect(template).to receive(:add_recipient).with(first_recipient) + expect(template).to receive(:add_recipient).with(second_recipient) + + subject + end + end + end + + describe '#mail' do + subject { described_class.new(client, template, recipients) } + + let(:mail_params) { {} } + let(:mail_to_h) { '' } + + before do + allow(subject).to receive(:mail_params) { mail_params } + allow_any_instance_of(Mail).to receive(:to_h) { mail_to_h } + allow(client).to receive(:send) + end + + it 'creates a new mail object' do + expect(Mail).to receive(:new).with(mail_params).and_call_original + subject.mail + end + + it 'adds the template to the mail object' do + expect_any_instance_of(Mail).to receive(:template=).with(template) + subject.mail + end + + it 'calls send on the client with the mail object' do + expect(client).to receive(:send).with(mail_to_h) + subject.mail + end + end + end +end diff --git a/spec/lib/sendgrid/template_spec.rb b/spec/lib/sendgrid/template_spec.rb new file mode 100644 index 00000000..da78ebd0 --- /dev/null +++ b/spec/lib/sendgrid/template_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../../lib/sendgrid/template' + +module SendGrid + describe Template do + let(:id) { anything } + subject { described_class.new(id) } + + describe '#initialize' do + it 'sets the id instance var' do + expect(subject.instance_variable_get(:@id)).to_not be_nil + end + end + + describe '#add_to_smtpapi' do + let(:id) { rand(8999) } + let(:smtpapi) { Smtpapi::Header.new } + + it 'adds enabled and the templates id' do + expect(smtpapi).to receive(:add_filter).with(:templates, :enable, 1) + expect(smtpapi).to receive(:add_filter).with(:templates, :template_id, id) + subject.add_to_smtpapi(smtpapi) + end + + context 'smtpapi is nil' do + it 'does not error' do + expect do + subject.add_to_smtpapi(nil) + end.to_not raise_error + end + end + + context 'with recipients' do + let(:substitution_key) { :foo } + let(:substitution_value) { :bar } + let(:recipients) do + [].tap do |recipients| + 3.times.each do + r = Recipient.new("test+#{ rand(100) }@example.com") + r.add_substitution(substitution_key, substitution_value) + recipients << r + end + end + end + + before do + recipients.each do |r| + subject.add_recipient(r) + end + end + + it 'calls the recipients call to add to smtpapi' do + recipients.each do |recipient| + expect(recipient).to receive(:add_to_smtpapi).with(smtpapi) + end + + subject.add_to_smtpapi(smtpapi) + end + end + end + end +end