├── .github
└── workflows
│ └── ruby.yml
├── .gitignore
├── .rspec
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── gemfiles
└── mail_2.7.gemfile
├── lib
├── sendgrid-actionmailer.rb
├── sendgrid_actionmailer.rb
└── sendgrid_actionmailer
│ ├── railtie.rb
│ └── version.rb
├── sendgrid-actionmailer.gemspec
└── spec
├── lib
└── sendgrid_actionmailer_spec.rb
└── spec_helper.rb
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7 |
8 | name: Ruby
9 |
10 | on:
11 | push:
12 | branches: [ main ]
13 | pull_request:
14 | branches: [ main ]
15 |
16 | jobs:
17 | test:
18 |
19 | runs-on: ubuntu-latest
20 | strategy:
21 | matrix:
22 | ruby-version: ['2.6', '2.7', '3.0']
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 | - name: Set up Ruby
27 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28 | # change this to (see https://github.com/ruby/setup-ruby#versioning):
29 | # uses: ruby/setup-ruby@v1
30 | uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
31 | with:
32 | ruby-version: ${{ matrix.ruby-version }}
33 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34 | - name: Run tests
35 | run: bundle exec rake
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | *.bundle
19 | *.so
20 | *.o
21 | *.a
22 | mkmf.log
23 | .DS_Store
24 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require 'spec_helper'
3 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "mail-2.7" do
2 | gem "mail", "2.7.0"
3 | end
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 3.2.0 - 2020-02-16
4 | - #107 Feature/sendgrid api settings, expand settings/options to allow for other SendGrid::Api client settings
5 | - #106 The SendGrid API simple error handling
6 | - #105 Github Actions CI
7 | - #102 Add http_options to configurable in mailer settings
8 | - #101 Railtie should use ActiveSupport.on_load
9 |
10 | ## 3.1.1 - 2020-11-06
11 | - #92 globally configured mail_settings
12 | - #95 add_mail_settings - support global settings
13 |
14 | ## 3.1.0 - 2020-8-03
15 |
16 | ### Changes
17 |
18 | - #81 Fix warnings with ruby 2.7
19 | - #83 Allow string type mail.to
20 | - #84 Content-ID values to fix inline attachments
21 |
22 | ## 3.0.2 - 2020-4-20
23 |
24 | ### Changes
25 |
26 | - perform_send_request setting for testing perposes
27 |
28 | ## 3.0.1 - 2020-4-3
29 |
30 | ### Changes
31 |
32 | - Validate error response body for empty string
33 |
34 | ## 3.0.0 - 2020-3-2
35 |
36 | ### Removed
37 |
38 | - Compatibility with mail gems before version 2.7
39 |
40 | ## 2.6.0 - 2020-1-23
41 |
42 | ### Changes
43 |
44 | - Dont send content types with dynamic templates (#69)
45 |
46 | ## 2.5.0 - 2020-1-21
47 |
48 | ### Changes
49 |
50 | - Add personalizations field (#60)
51 |
52 | ### Fixes
53 |
54 | - Revert "Lazy load ActionMailer::Base" (#64)
55 | - Yank 2.4.1
56 |
57 | ## 2.4.2 - 2020-1-21
58 |
59 | ### Fixes
60 |
61 | - Revert "Lazy load ActionMailer::Base" (#64)
62 | - Yank 2.4.1
63 |
64 | ## 2.4.1 - 2020-1-20
65 |
66 | ### Changed
67 |
68 | - Update Travis CI settings to test on latest Ruby and mail gem version (#55)
69 | - Lazy load ActionMailer::Base (#57)
70 |
71 | ## 2.4.0 - 2019-07-9
72 |
73 | ### Changed
74 |
75 | - Compatibility with `sendgrid-ruby` v6.0.
76 |
77 | ## 2.3.0 - 2019-4-10
78 |
79 | ### Fixes
80 |
81 | - No asm substitutions if template_id present
82 |
83 | ## 2.2.1 - 2019-1-4
84 |
85 | ### Fixes
86 |
87 | - Fix Travis
88 |
89 | ## 2.2.0 - 2018-11-23
90 |
91 | ### Fixes
92 |
93 | - Update Readme
94 |
95 | ## 2.1.0 - 2018-11-20
96 |
97 | ### Fixes
98 |
99 | - Substiutions and dynamic_template_data should be compatible.
100 |
101 |
102 | ## 2.0.0 - 2018-08-15
103 |
104 | ### Changed
105 |
106 | - Compatibility with `sendgrid-ruby` v5.0.
107 |
108 | ## 0.2.1 - 2016-04-26
109 |
110 | ### Fixed
111 |
112 | - Fix handling of multipart/related inline attachments.
113 |
114 | ## 0.2.0 - 2016-04-25
115 |
116 | ### Added
117 |
118 | - Support for `reply_to` and `date` options.
119 |
120 | ### Fixed
121 |
122 | - Fix sending when multiple `X-SMTPAPI` headers are set.
123 |
124 | ## 0.1.1 - 2016-04-25
125 |
126 | ### Fixed
127 |
128 | - Fix file attachments.
129 |
130 | ## 0.1.0 - 2016-04-23
131 |
132 | ### Added
133 |
134 | - Support for `cc` and `bcc` options.
135 | - Support the `X-SMTPAPI` header.
136 |
137 | ### Changed
138 |
139 | - Compatibility with `sendgrid-ruby` v1.0.
140 | - Compatibility with `mail` v2.6.
141 |
142 | ### Fixed
143 |
144 | - Fix `From` addresses with display names.
145 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in sendgrid-actionmailer.gemspec
4 | gemspec
5 |
6 | if RUBY_VERSION < '2'
7 | gem 'public_suffix', '~> 1.4.6'
8 | gem 'mime-types', '~> 2.99'
9 | end
10 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Eddie Zaneski
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SendGrid ActionMailer
2 |
3 | An ActionMailer adapter to send email using SendGrid's HTTPS Web API (instead of SMTP). Compatible with Rails 5 and Sendgrid API v3.
4 |
5 | ## Installation
6 |
7 | Add this line to your application's Gemfile:
8 |
9 | gem 'sendgrid-actionmailer'
10 |
11 | ## Usage
12 |
13 | Create a [SendGrid API Key](https://app.sendgrid.com/settings/api_keys) for your application. Then edit `config/application.rb` or `config/environments/$ENVIRONMENT.rb` and add/change the following to the ActionMailer configuration:
14 |
15 | ```ruby
16 | config.action_mailer.delivery_method = :sendgrid_actionmailer
17 | config.action_mailer.sendgrid_actionmailer_settings = {
18 | api_key: ENV['SENDGRID_API_KEY'],
19 | raise_delivery_errors: true
20 | }
21 | ```
22 |
23 | Normal ActionMailer usage will now transparently be sent using SendGrid's Web API.
24 |
25 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body')```
26 |
27 | ### Mail Settings
28 |
29 | Mail settings, such as sandbox_mode, may be applied globally through the sendgrid_actionmailer_settings configuration.
30 |
31 | ```ruby
32 | config.action_mailer.delivery_method = :sendgrid_actionmailer
33 | config.action_mailer.sendgrid_actionmailer_settings = {
34 | api_key: ENV['SENDGRID_API_KEY'],
35 | mail_settings: { sandbox_mode: { enable: true }}
36 | }
37 | ```
38 |
39 | ### Dynamic API Key
40 |
41 | If you need to send mail for a number of Sendgrid accounts, you can set the API key for these as follows:
42 |
43 |
44 | ```ruby
45 | mail(to: 'example@email.com',
46 | subject: 'email subject',
47 | body: 'email body',
48 | delivery_method_options: {
49 | api_key: 'SENDGRID_API_KEY'
50 | }
51 | )
52 | ```
53 |
54 | ## SendGrid Mail Extensions
55 |
56 | The Mail functionality is extended to include additional attributes provided by the Sendgrid API.
57 |
58 | [Sendgrid v3 API Documentation](https://sendgrid.com/docs/API_Reference/api_v3.html)
59 |
60 | ### template_id (string)
61 | The id of a template that you would like to use. If you use a template that contains a subject, you do not need to specify a subject at the personalizations nor message level. However, because of the way ActionMailer works, a body is required, even if the template contains one. If all your emails use templates with a body, you can add `default body: "not used"` to the top of your mailer.
62 |
63 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', template_id: 'template_1')```
64 |
65 | ### sections (object)
66 | An object of key/value pairs that define block sections of code to be used as substitutions.
67 |
68 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', sections: {'%header%' => "
Header
"})```
69 |
70 | ### headers (object)
71 | An object containing key/value pairs of header names and the value to substitute for them. You must ensure these are properly encoded if they contain unicode characters. Must not be one of the reserved headers.
72 |
73 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', headers: {'X-CUSTOM-HEADER' => "foo"})```
74 |
75 | ### categories (array)
76 | An array of category names for this message. Each category name may not exceed 255 characters.
77 |
78 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', categories: ['marketing', 'sales'])```
79 |
80 | ### custom_args (object)
81 | Values that are specific to the entire send that will be carried along with the email and its activity data. Substitutions will not be made on custom arguments, so any string that is entered into this parameter will be assumed to be the custom argument that you would like to be used. This parameter is overridden by personalizations[x].custom_args if that parameter has been defined. Total custom args size may not exceed 10,000 bytes.
82 |
83 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', custom_args: {campaign: 'welcome'})```
84 |
85 | ### send_at (integer)
86 | A unix timestamp allowing you to specify when you want your email to be delivered. This may be overridden by the personalizations[x].send_at parameter. You can't schedule more than 72 hours in advance. If you have the flexibility, it's better to schedule mail for off-peak times. Most emails are scheduled and sent at the top of the hour or half hour. Scheduling email to avoid those times (for example, scheduling at 10:53) can result in lower deferral rates because it won't be going through our servers at the same times as everyone else's mail.
87 |
88 | ### batch_id (string)
89 | This ID represents a batch of emails to be sent at the same time. Including a batch_id in your request allows you include this email in that batch, and also enables you to cancel or pause the delivery of that batch. For more information, see [cancel_schedule_send](https://sendgrid.com/docs/API_Reference/Web_API_v3/cancel_schedule_send.html)
90 |
91 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', send_at: 1443636842, batch_id: 'batch1')```
92 |
93 | ### asm (object)
94 | An object allowing you to specify how to handle unsubscribes.
95 |
96 | #### group_id (integer) *required
97 | The unsubscribe group to associate with this email.
98 |
99 | #### groups_to_display (array[integer])
100 | An array containing the unsubscribe groups that you would like to be displayed on the unsubscribe preferences page.
101 |
102 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', asm: { group_id: 99, groups_to_display: [4,5,6,7,8] })```
103 |
104 | ### ip_pool_name (string)
105 | The IP Pool that you would like to send this email from.
106 |
107 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', ip_pool_name: 'marketing_ips')```
108 |
109 | ### mail_settings (object)
110 | A collection of different mail settings that you can use to specify how you would like this email to be handled.
111 |
112 | #### bcc (object)
113 | This allows you to have a blind carbon copy automatically sent to the specified email address for every email that is sent.
114 |
115 | ##### enable (boolean)
116 | Indicates if this setting is enabled.
117 |
118 | ##### email (string)
119 | The email address that you would like to receive the BCC.
120 |
121 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', mail_settings: { bcc: { enable: true, email: 'bcc@example.com }})```
122 |
123 | #### bypass_list_management (object)
124 | Allows you to bypass all unsubscribe groups and suppressions to ensure that the email is delivered to every single recipient. This should only be used in emergencies when it is absolutely necessary that every recipient receives your email.
125 |
126 | ###### enable (boolean)
127 | Indicates if this setting is enabled.
128 |
129 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', mail_settings: { bypass_list_management: { enable: true }})```
130 |
131 | #### footer (object)
132 | The default footer that you would like included on every email.
133 |
134 | ##### enable (boolean)
135 | Indicates if this setting is enabled.
136 |
137 | ##### text (string)
138 | The plain text content of your footer.
139 |
140 | ##### html (string)
141 | The HTML content of your footer.
142 |
143 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', mail_settings: { footer: { enable: true, text: 'FOOTER', html: 'FOOTER
' }})```
144 |
145 | #### sandbox_mode (object)
146 | This allows you to send a test email to ensure that your request body is valid and formatted correctly.
147 |
148 | ##### enable (boolean)
149 | Indicates if this setting is enabled.
150 |
151 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', mail_settings: { sandbox_mode: { enable: true }})```
152 |
153 | #### spam_check (object)
154 | This allows you to test the content of your email for spam.
155 |
156 | ##### enable (boolean)
157 | Indicates if this setting is enabled.
158 |
159 | ##### threshold (integer)
160 | The threshold used to determine if your content qualifies as spam on a scale from 1 to 10, with 10 being most strict, or most likely to be considered as spam.
161 |
162 | ##### post_to_url (string)
163 | An Inbound Parse URL that you would like a copy of your email along with the spam report to be sent to.
164 |
165 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', mail_settings: { spam_check: { enable: true, threshold: 1, post_to_url: 'https://spamcatcher.sendgrid.com' }})```
166 |
167 | ### tracking_settings(json)
168 | Settings to determine how you would like to track the metrics of how your recipients interact with your email.
169 |
170 | #### click_tracking(object)
171 | Allows you to track whether a recipient clicked a link in your email.
172 |
173 | ##### enable (boolean)
174 | Indicates if this setting is enabled.
175 |
176 | ##### enable_text (boolean)
177 | Indicates if this setting should be included in the text/plain portion of your email.
178 |
179 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', tracking_settings: { click_tracking: { enable: false, enable_text: false }})```
180 |
181 | #### open_tracking (object)
182 | Allows you to track whether the email was opened or not, but including a single pixel image in the body of the content. When the pixel is loaded, we can log that the email was opened.
183 |
184 | ##### enable (boolean)
185 | Indicates if this setting is enabled.
186 |
187 | ##### substitution_tag (string)
188 | Allows you to specify a substitution tag that you can insert in the body of your email at a location that you desire. This tag will be replaced by the open tracking pixel.
189 |
190 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', tracking_settings: { open_tracking: { enable: true, substitution_tag: 'Optional tag to replace with the open image in the body of the message' }})```
191 |
192 | #### subscription_tracking (object)
193 | Allows you to insert a subscription management link at the bottom of the text and html bodies of your email. If you would like to specify the location of the link within your email, you may use the substitution_tag.
194 |
195 | ##### enable (boolean)
196 | Indicates if this setting is enabled.
197 |
198 | ##### text (string)
199 | Text to be appended to the email, with the subscription tracking link. You may control where the link is by using the tag <% %>
200 |
201 | ##### html (string)
202 | HTML to be appended to the email, with the subscription tracking link. You may control where the link is by using the tag <% %>
203 |
204 | ##### substitution_tag (string)
205 | A tag that will be replaced with the unsubscribe URL. for example: [unsubscribe_url]. If this parameter is used, it will override both the text and html parameters. The URL of the link will be placed at the substitution tag’s location, with no additional formatting.
206 |
207 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', tracking_settings: { subscription_tracking: { enable: true, text: 'text to insert into the text/plain portion of the message', html: 'html to insert into the text/html portion of the message', substitution_tag: 'Optional tag to replace with the open image in the body of the message' }})```
208 |
209 | #### ganalytics (object)
210 | Allows you to enable tracking provided by Google Analytics.
211 |
212 | ##### enable (boolean)
213 | Indicates if this setting is enabled.
214 |
215 | ##### utm_source (string)
216 | Name of the referrer source. (e.g. Google, SomeDomain.com, or Marketing Email)
217 |
218 | ##### utm_medium (string)
219 | Name of the marketing medium. (e.g. Email)
220 |
221 | ##### utm_term (string)
222 | Used to identify any paid keywords.
223 |
224 | ##### utm_content (string)
225 | Used to differentiate your campaign from advertisements.
226 |
227 | ##### utm_campaign (string)
228 | The name of the campaign.
229 |
230 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', tracking_settings: { ganalytics: { enable: true, utm_source: 'some source', utm_medium: 'some medium', utm_term: 'some term', utm_content: 'some content', utm_campaign: 'some campaign' }})```
231 |
232 | ### dynamic_template_data (json)
233 |
234 | Data to provide for feeding the new dynamic templates in Sendgrid with valueable data. This also disables the following Unsubscribe links because of deprecation of substitutions in the new template implementaiton. Variables are available within templates using [{{handlebar syntax}}](https://sendgrid.com/docs/for-developers/sending-email/using-handlebars).
235 |
236 | ```mail(to: 'example@email.com', subject: 'email subject', body: 'email body', dynamic_template_data: { variable_1: 'foo', variable_2: 'bar' })```
237 |
238 | ### personalizations (json)
239 |
240 | Allows providing a customized [personalizations](https://sendgrid.com/docs/for-developers/sending-email/personalizations/) array for the v3 Mail Send endpoint. This allows customizing how an email is sent and also allows sending multiple different emails to different recipients with a single API call.
241 |
242 | The personalizations object supports:
243 |
244 | - "to", "cc", "bcc" - The recipients of your email.
245 | - "subject" - The subject of your email.
246 | - "headers" - Any headers you would like to include in your email.
247 | - "substitutions" - Any substitutions you would like to be made for your email.
248 | - "custom_args" - Any custom arguments you would like to include in your email.
249 | - "send_at" - A specific time that you would like your email to be sent.
250 | - "dynamic_template_data" - data for dynamic templates.
251 |
252 | The following should be noted about these personalization attributes:
253 | - to, cc, or bcc: if either to, cc, or bcc is also set when calling mail, those addresses provided to mail will be inserted as a separate personalization from the ones you provide. However, when using personalizations, you are not required to specify `to` when calling the mail function.
254 | - dynamic_template_data specified in the mail function will be merged with any dynamic_template_data specified in the personalizations object (with the personalizations object keys having priority).
255 | - Other fields set in the personalizations object will override any global parameters defined outside of personalizations.
256 |
257 | Also note that substitutions will not work with dynamic templates.
258 |
259 | Example usage:
260 |
261 | ```
262 | mail(subject: 'default subject', 'email body', personalizations: [
263 | { to: [{ email: 'example@example.com' }]},
264 | { to: [{ email: 'example2@example.com' }]}
265 | ])
266 | ```
267 |
268 | ### Unsubscribe Links
269 |
270 | Sendgrid unfortunately uses <% %> for their default substitution syntax, which makes it incompatible with Rails templates. Their proposed solution is to use Personalization Substitutions with the v3 Mail Send Endpoint. This gem makes that modification to make the following Rails friendly unsubscribe urls.
271 |
272 | * `Unsubscribe`
273 | * `Unsubscribe from List`
274 | * `Manage Email Preferences`
275 |
276 | Note: This feature, and substitutions in general, do not work in combination with dynamic templates.
277 |
278 | ## Testing
279 |
280 | The setting `perform_send_request` is available to disable sending for testing purposes. Setting perform_send_request false and return_response true enables the testing of the JSON API payload.
281 |
282 |
283 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task test: :spec
7 |
8 | task default: :test
9 |
10 |
--------------------------------------------------------------------------------
/gemfiles/mail_2.7.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "mail", "2.7.1"
6 |
7 | gemspec :path => "../"
8 |
--------------------------------------------------------------------------------
/lib/sendgrid-actionmailer.rb:
--------------------------------------------------------------------------------
1 | require "sendgrid_actionmailer"
2 |
--------------------------------------------------------------------------------
/lib/sendgrid_actionmailer.rb:
--------------------------------------------------------------------------------
1 | require 'sendgrid_actionmailer/version'
2 | require 'sendgrid_actionmailer/railtie' if defined? Rails
3 | require 'sendgrid-ruby'
4 |
5 | module SendGridActionMailer
6 | class DeliveryMethod
7 |
8 | # TODO: use custom class to customer excpetion payload
9 | SendgridDeliveryError = Class.new(StandardError)
10 |
11 | include SendGrid
12 |
13 | DEFAULTS = {
14 | raise_delivery_errors: false
15 | }.freeze
16 |
17 | attr_accessor :settings, :options
18 |
19 | def initialize(params = {})
20 | self.settings = DEFAULTS.merge(params)
21 | end
22 |
23 | def deliver!(mail)
24 | self.options = {}
25 | sendgrid_mail = Mail.new.tap do |m|
26 | m.from = to_email(mail.from)
27 | m.reply_to = to_email(mail.reply_to)
28 | m.subject = mail.subject || ""
29 | end
30 |
31 | add_personalizations(sendgrid_mail, mail)
32 | add_options(sendgrid_mail, mail)
33 | add_content(sendgrid_mail, mail)
34 | add_send_options(sendgrid_mail, mail)
35 | add_mail_settings(sendgrid_mail, mail)
36 | add_tracking_settings(sendgrid_mail, mail)
37 |
38 | if (settings[:perform_send_request] == false)
39 | response = sendgrid_mail
40 | else
41 | response = perform_send_request(sendgrid_mail)
42 | end
43 |
44 | settings[:return_response] ? response : self
45 | end
46 |
47 | private
48 |
49 | def client_options
50 | options.dup
51 | .select { |key, value| key.to_s.match(/(api_key|host|request_headers|version|impersonate_subuser)/) }
52 | .merge(http_options: settings.fetch(:http_options, {}))
53 | end
54 |
55 | def client
56 | @client = SendGrid::API.new(**client_options).client
57 | end
58 |
59 | # type should be either :plain or :html
60 | def to_content(type, value)
61 | Content.new(type: "text/#{type}", value: value)
62 | end
63 |
64 | def to_email(input)
65 | to_emails(input).first
66 | end
67 |
68 | def to_emails(input)
69 | if input.is_a?(String)
70 | [Email.new(email: input)]
71 | elsif input.is_a?(::Mail::AddressContainer) && !input.instance_variable_get('@field').nil?
72 | input.instance_variable_get('@field').addrs.map do |addr| # Mail::Address
73 | Email.new(email: addr.address, name: addr.name)
74 | end
75 | elsif input.is_a?(::Mail::AddressContainer)
76 | input.map do |addr|
77 | Email.new(email: addr)
78 | end
79 | elsif input.is_a?(::Mail::StructuredField)
80 | [Email.new(email: input.value)]
81 | elsif input.nil?
82 | []
83 | else
84 | puts "unknown type #{input.class.name}"
85 | end
86 | end
87 |
88 | def setup_personalization(mail, personalization_hash)
89 | personalization = Personalization.new
90 |
91 | personalization_hash = self.class.transform_keys(personalization_hash, &:to_s)
92 |
93 | (personalization_hash['to'] || []).each do |to|
94 | personalization.add_to Email.new(email: to['email'], name: to['name'])
95 | end
96 | (personalization_hash['cc'] || []).each do |cc|
97 | personalization.add_cc Email.new(email: cc['email'], name: cc['name'])
98 | end
99 | (personalization_hash['bcc'] || []).each do |bcc|
100 | personalization.add_bcc Email.new(email: bcc['email'], name: bcc['name'])
101 | end
102 | (personalization_hash['headers'] || []).each do |header_key, header_value|
103 | personalization.add_header Header.new(key: header_key, value: header_value)
104 | end
105 | (personalization_hash['substitutions'] || {}).each do |sub_key, sub_value|
106 | personalization.add_substitution(Substitution.new(key: sub_key, value: sub_value))
107 | end
108 | (personalization_hash['custom_args'] || {}).each do |arg_key, arg_value|
109 | personalization.add_custom_arg(CustomArg.new(key: arg_key, value: arg_value))
110 | end
111 | if personalization_hash['send_at']
112 | personalization.send_at = personalization_hash['send_at']
113 | end
114 | if personalization_hash['subject']
115 | personalization.subject = personalization_hash['subject']
116 | end
117 |
118 | if mail['dynamic_template_data'] || personalization_hash['dynamic_template_data']
119 | if mail['dynamic_template_data']
120 | data = mail['dynamic_template_data'].unparsed_value
121 | data.merge!(personalization_hash['dynamic_template_data'] || {})
122 | else
123 | data = personalization_hash['dynamic_template_data']
124 | end
125 | personalization.add_dynamic_template_data(data)
126 | elsif mail['template_id'].nil?
127 | personalization.add_substitution(Substitution.new(key: "%asm_group_unsubscribe_raw_url%", value: "<%asm_group_unsubscribe_raw_url%>"))
128 | personalization.add_substitution(Substitution.new(key: "%asm_global_unsubscribe_raw_url%", value: "<%asm_global_unsubscribe_raw_url%>"))
129 | personalization.add_substitution(Substitution.new(key: "%asm_preferences_raw_url%", value: "<%asm_preferences_raw_url%>"))
130 | end
131 |
132 | return personalization
133 | end
134 |
135 | def to_attachment(part)
136 | Attachment.new.tap do |a|
137 | a.content = Base64.strict_encode64(part.body.decoded)
138 | a.type = part.mime_type
139 | a.filename = part.filename
140 |
141 | disposition = get_disposition(part)
142 | a.disposition = disposition unless disposition.nil?
143 |
144 | has_content_id = part.header && part.has_content_id?
145 | a.content_id = part.header['content_id'].field.content_id if has_content_id
146 | end
147 | end
148 |
149 | def get_disposition(message)
150 | return if message.header.nil?
151 | content_disp = message.header[:content_disposition]
152 | return unless content_disp.respond_to?(:disposition_type)
153 | content_disp.disposition_type
154 | end
155 |
156 | def add_options(sendgrid_mail, mail)
157 | self.options.merge!(**self.class.transform_keys(self.settings, &:to_sym))
158 | if !!(mail['delivery-method-options'])
159 | self.options.merge!(**self.class.transform_keys(mail['delivery-method-options'].unparsed_value , &:to_sym))
160 | end
161 | end
162 |
163 | def add_attachments(sendgrid_mail, mail)
164 | mail.attachments.each do |part|
165 | sendgrid_mail.add_attachment(to_attachment(part))
166 | end
167 | end
168 |
169 | def add_content(sendgrid_mail, mail)
170 | if mail['template_id']
171 | # We are sending a template, so we don't need to add any content outside
172 | # of attachments
173 | add_attachments(sendgrid_mail, mail)
174 | else
175 | case mail.mime_type
176 | when 'text/plain'
177 | sendgrid_mail.add_content(to_content(:plain, mail.body.decoded))
178 | when 'text/html'
179 | sendgrid_mail.add_content(to_content(:html, mail.body.decoded))
180 | when 'multipart/alternative', 'multipart/mixed', 'multipart/related'
181 | sendgrid_mail.add_content(to_content(:plain, mail.text_part.decoded)) if mail.text_part
182 | sendgrid_mail.add_content(to_content(:html, mail.html_part.decoded)) if mail.html_part
183 |
184 | add_attachments(sendgrid_mail, mail)
185 | end
186 | end
187 | end
188 |
189 | def add_personalizations(sendgrid_mail, mail)
190 | if mail['personalizations']
191 | mail['personalizations'].unparsed_value.each do |p|
192 | sendgrid_mail.add_personalization(setup_personalization(mail, p))
193 | end
194 | end
195 | if (mail.to && !mail.to.empty?) || (mail.cc && !mail.cc.empty?) || (mail.bcc && !mail.bcc.empty?)
196 | personalization = setup_personalization(mail, {})
197 | to_emails(mail.to).each { |to| personalization.add_to(to) }
198 | to_emails(mail.cc).each { |cc| personalization.add_cc(cc) }
199 | to_emails(mail.bcc).each { |bcc| personalization.add_bcc(bcc) }
200 | sendgrid_mail.add_personalization(personalization)
201 | end
202 | end
203 |
204 | def add_send_options(sendgrid_mail, mail)
205 | if mail['template_id']
206 | sendgrid_mail.template_id = mail['template_id'].to_s
207 | end
208 | if mail['sections']
209 | mail['sections'].unparsed_value.each do |key, value|
210 | sendgrid_mail.add_section(Section.new(key: key, value: value))
211 | end
212 | end
213 | if mail['headers']
214 | mail['headers'].unparsed_value.each do |key, value|
215 | sendgrid_mail.add_header(Header.new(key: key, value: value))
216 | end
217 | end
218 | if mail['categories']
219 | mail['categories'].value.split(",").each do |value|
220 | sendgrid_mail.add_category(Category.new(name: value.strip))
221 | end
222 | end
223 | if mail['custom_args']
224 | mail['custom_args'].unparsed_value.each do |key, value|
225 | sendgrid_mail.add_custom_arg(CustomArg.new(key: key, value: value))
226 | end
227 | end
228 | if mail['send_at']
229 | sendgrid_mail.send_at = mail['send_at'].value.to_i
230 | end
231 | if mail['batch_id']
232 | sendgrid_mail.batch_id = mail['batch_id'].to_s
233 | end
234 | if mail['asm']
235 | asm = mail['asm'].unparsed_value
236 | asm = asm.delete_if { |key, value|
237 | !key.to_s.match(/(group_id)|(groups_to_display)/) }
238 | if asm.keys.map(&:to_s).include?('group_id')
239 | sendgrid_mail.asm = ASM.new(**self.class.transform_keys(asm, &:to_sym))
240 | end
241 | end
242 | if mail['ip_pool_name']
243 | sendgrid_mail.ip_pool_name = mail['ip_pool_name'].to_s
244 | end
245 | end
246 |
247 | def add_mail_settings(sendgrid_mail, mail)
248 | local_settings = mail['mail_settings'] && mail['mail_settings'].unparsed_value || {}
249 | global_settings = self.settings[:mail_settings] || {}
250 | settings = global_settings.merge(local_settings)
251 | unless settings.empty?
252 | sendgrid_mail.mail_settings = MailSettings.new.tap do |m|
253 | if settings[:bcc]
254 | m.bcc = BccSettings.new(**settings[:bcc])
255 | end
256 | if settings[:bypass_list_management]
257 | m.bypass_list_management = BypassListManagement.new(**settings[:bypass_list_management])
258 | end
259 | if settings[:footer]
260 | m.footer = Footer.new(**settings[:footer])
261 | end
262 | if settings[:sandbox_mode]
263 | m.sandbox_mode = SandBoxMode.new(**settings[:sandbox_mode])
264 | end
265 | if settings[:spam_check]
266 | m.spam_check = SpamCheck.new(**settings[:spam_check])
267 | end
268 | end
269 | end
270 | end
271 |
272 | def add_tracking_settings(sendgrid_mail, mail)
273 | if mail['tracking_settings']
274 | settings = mail['tracking_settings'].unparsed_value
275 | sendgrid_mail.tracking_settings = TrackingSettings.new.tap do |t|
276 | if settings[:click_tracking]
277 | t.click_tracking = ClickTracking.new(**settings[:click_tracking])
278 | end
279 | if settings[:open_tracking]
280 | t.open_tracking = OpenTracking.new(**settings[:open_tracking])
281 | end
282 | if settings[:subscription_tracking]
283 | t.subscription_tracking = SubscriptionTracking.new(**settings[:subscription_tracking])
284 | end
285 | if settings[:ganalytics]
286 | t.ganalytics = Ganalytics.new(**settings[:ganalytics])
287 | end
288 | end
289 | end
290 | end
291 |
292 | def perform_send_request(email)
293 | result = client.mail._('send').post(request_body: email.to_json) # ლ(ಠ益ಠლ) that API
294 |
295 | if result.status_code && result.status_code.start_with?('4')
296 | full_message = "Sendgrid delivery failed with #{result.status_code}: #{result.body}"
297 | settings[:raise_delivery_errors] ? raise(SendgridDeliveryError, full_message) : warn(full_message)
298 | end
299 |
300 | result
301 | end
302 |
303 | # Recursive key transformation based on Rails deep_transform_values
304 | def self.transform_keys(object, &block)
305 | case object
306 | when Hash
307 | object.map { |key, value| [yield(key), transform_keys(value, &block)] }.to_h
308 | when Array
309 | object.map { |e| transform_keys(e, &block) }
310 | else
311 | object
312 | end
313 | end
314 | end
315 | end
316 |
--------------------------------------------------------------------------------
/lib/sendgrid_actionmailer/railtie.rb:
--------------------------------------------------------------------------------
1 | module SendGridActionMailer
2 | class Railtie < Rails::Railtie
3 | initializer 'sendgrid_actionmailer.add_delivery_method', before: 'action_mailer.set_configs' do
4 | ActiveSupport.on_load(:action_mailer) do
5 | ActionMailer::Base.add_delivery_method(:sendgrid_actionmailer, SendGridActionMailer::DeliveryMethod)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/sendgrid_actionmailer/version.rb:
--------------------------------------------------------------------------------
1 | module SendGridActionMailer
2 | VERSION = '3.2.0'.freeze
3 | end
4 |
--------------------------------------------------------------------------------
/sendgrid-actionmailer.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | lib = File.expand_path('../lib', __FILE__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require 'sendgrid_actionmailer/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'sendgrid-actionmailer'
9 | spec.version = SendGridActionMailer::VERSION
10 | spec.authors = ['Eddie Zaneski', 'Kristján Pétursson', 'Nick Muerdter']
11 | spec.email = ['eddiezane@gmail.com', 'kristjan@gmail.com', 'stuff@nickm.org']
12 | spec.summary = %q{SendGrid support for ActionMailer.}
13 | spec.description = %q{Use ActionMailer with SendGrid's Web API.}
14 | spec.homepage = 'https://github.com/eddiezane/sendgrid-actionmailer'
15 | spec.license = 'MIT'
16 |
17 | spec.files = `git ls-files -z`.split("\x0")
18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20 | spec.require_paths = ['lib']
21 |
22 | spec.add_dependency 'mail', '~> 2.7'
23 | spec.add_dependency 'sendgrid-ruby', '~> 6.4'
24 |
25 | spec.add_development_dependency 'appraisal', '~> 2.1.0'
26 | spec.add_development_dependency 'bundler'
27 | spec.add_development_dependency 'rake'
28 | spec.add_development_dependency 'rspec', '~> 3.2'
29 | spec.add_development_dependency 'webmock'
30 | end
31 |
--------------------------------------------------------------------------------
/spec/lib/sendgrid_actionmailer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'mail'
2 | require 'webmock/rspec'
3 |
4 | module SendGridActionMailer
5 | describe DeliveryMethod do
6 | def transform_keys(object, &block)
7 | SendGridActionMailer::DeliveryMethod.transform_keys(object, &block)
8 | end
9 |
10 | subject(:mailer) do
11 | DeliveryMethod.new(api_key: 'key')
12 | end
13 |
14 | class TestClient
15 | attr_reader :sent_mail
16 |
17 | def send(mail)
18 | @sent_mail = mail
19 | super(mail)
20 | end
21 |
22 | def mail()
23 | self
24 | end
25 |
26 | def _(param)
27 | return self if param == 'send'
28 | raise "Unknown param #{param.inspect}"
29 | end
30 |
31 | def post(request_body:)
32 | @sent_mail = request_body
33 | OpenStruct.new(status_code: '200')
34 | end
35 | end
36 |
37 | describe 'settings' do
38 | it 'has correct api_key' do
39 | m = DeliveryMethod.new(api_key: 'ABCDEFG')
40 | expect(m.settings[:api_key]).to eq('ABCDEFG')
41 | end
42 |
43 | it 'has correct host' do
44 | m = DeliveryMethod.new(host: 'example.com')
45 | expect(m.settings[:host]).to eq('example.com')
46 | end
47 |
48 | it 'default raise_delivery_errors' do
49 | m = DeliveryMethod.new()
50 | expect(m.settings[:raise_delivery_errors]).to eq(false)
51 | end
52 |
53 | it 'sets raise_delivery_errors' do
54 | m = DeliveryMethod.new(raise_delivery_errors: true)
55 | expect(m.settings[:raise_delivery_errors]).to eq(true)
56 | end
57 |
58 | it 'default return_response' do
59 | m = DeliveryMethod.new()
60 | expect(mailer.settings[:return_response]).to eq(nil)
61 | end
62 |
63 | it 'sets return_response' do
64 | m = DeliveryMethod.new(return_response: true)
65 | expect(m.settings[:return_response]).to eq(true)
66 | end
67 |
68 | it 'sets perform_deliveries' do
69 | m = DeliveryMethod.new(perform_send_request: false)
70 | expect(m.settings[:perform_send_request]).to eq(false)
71 | end
72 |
73 | it 'sets http_options' do
74 | m = DeliveryMethod.new(http_options: {open_timeout: 40})
75 | expect(m.settings[:http_options]).to eq({open_timeout: 40})
76 | end
77 | end
78 |
79 | describe '#deliver!' do
80 | let(:client) { TestClient.new }
81 | let(:client_parent) { double(client: client) }
82 |
83 | let(:mail) do
84 | Mail.new(
85 | to: 'test@sendgrid.com',
86 | from: 'taco@cat.limo',
87 | subject: 'Hello, world!',
88 | )
89 | end
90 |
91 | before do
92 | stub_request(:any, 'https://api.sendgrid.com/api/mail.send.json')
93 | .to_return(body: {message: 'success'}.to_json, status: 200, headers: {'X-TEST' => 'yes'})
94 | allow(SendGrid::Client).to receive(:new).and_return(client)
95 | allow(SendGrid::API).to receive(:new).and_return(client_parent)
96 | end
97 |
98 | context 'with dynamic api_key' do
99 | let(:default) do
100 | Mail.new(
101 | to: 'test@sendgrid.com',
102 | from: 'taco@cat.limo',
103 | subject: 'Hello, world!'
104 | )
105 | end
106 |
107 | let(:mail) do
108 | Mail.new(
109 | to: 'test@sendgrid.com',
110 | from: 'taco@cat.limo',
111 | subject: 'Hello, world!',
112 | delivery_method_options: {
113 | api_key: 'test_key'
114 | }
115 | )
116 | end
117 |
118 | it 'sets dynamic api_key, but should revert to default settings api_key' do
119 | expect(SendGrid::API).to receive(:new).with(api_key: 'key', http_options: {})
120 | mailer.deliver!(default)
121 | expect(SendGrid::API).to receive(:new).with(api_key: 'test_key', http_options: {})
122 | mailer.deliver!(mail)
123 | expect(SendGrid::API).to receive(:new).with(api_key: 'key', http_options: {})
124 | mailer.deliver!(default)
125 | end
126 | end
127 |
128 | it 'sets to' do
129 | mailer.deliver!(mail)
130 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com"}]})
131 | end
132 |
133 | it 'returns mailer itself' do
134 | ret = mailer.deliver!(mail)
135 | expect(ret).to eq(mailer)
136 | end
137 |
138 | it 'returns api response' do
139 | m = DeliveryMethod.new(return_response: true, api_key: 'key')
140 | ret = m.deliver!(mail)
141 | expect(ret.status_code).to eq('200')
142 | end
143 |
144 | context 'to with a friendly name' do
145 | before { mail.to = 'Test SendGrid ' }
146 |
147 | it 'sets to' do
148 | mailer.deliver!(mail)
149 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com", "name"=>"Test SendGrid"}]})
150 | end
151 | end
152 |
153 | context 'to with a friendly name (with quotes)' do
154 | before { mail.to = '"Test SendGrid" ' }
155 |
156 | it 'sets to' do
157 | mailer.deliver!(mail)
158 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com", "name"=>"Test SendGrid"}]})
159 | end
160 | end
161 |
162 | context 'there are ccs' do
163 | before { mail.cc = 'burrito@cat.limo' }
164 |
165 | it 'sets cc' do
166 | mailer.deliver!(mail)
167 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com"}], "cc"=>[{"email"=>"burrito@cat.limo"}]})
168 | end
169 | end
170 |
171 | context 'there are bccs' do
172 | before { mail.bcc = 'nachos@cat.limo' }
173 |
174 | it 'sets bcc' do
175 | mailer.deliver!(mail)
176 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com"}], "bcc"=>[{"email"=>"nachos@cat.limo"}]})
177 | end
178 | end
179 |
180 | context 'there are bccs with a friendly name' do
181 | before { mail.bcc = 'Taco Cat ' }
182 |
183 | it 'sets bcc' do
184 | mailer.deliver!(mail)
185 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com"}], "bcc"=>[{"email"=>"nachos@cat.limo", "name"=>"Taco Cat"}]})
186 | end
187 | end
188 |
189 | context 'there are bccs with a friendly name (with quotes)' do
190 | before { mail.bcc = '"Taco Cat" ' }
191 |
192 | it 'sets bcc' do
193 | mailer.deliver!(mail)
194 | expect(client.sent_mail['personalizations'][0]).to include({"to"=>[{"email"=>"test@sendgrid.com"}], "bcc"=>[{"email"=>"nachos@cat.limo", "name"=>"Taco Cat"}]})
195 | end
196 | end
197 |
198 | context 'there is a reply to' do
199 | before { mail.reply_to = 'nachos@cat.limo' }
200 |
201 | it 'sets reply_to' do
202 | mailer.deliver!(mail)
203 | expect(client.sent_mail['reply_to']).to eq({'email' => 'nachos@cat.limo'})
204 | end
205 | end
206 |
207 | context 'there is a reply to with a friendly name' do
208 | before { mail.reply_to = 'Taco Cat ' }
209 |
210 | it 'sets reply_to' do
211 | mailer.deliver!(mail)
212 | expect(client.sent_mail['reply_to']).to eq('email' => 'nachos@cat.limo', 'name' => 'Taco Cat')
213 | end
214 | end
215 |
216 | context 'from contains a friendly name' do
217 | before { mail.from = 'Taco Cat '}
218 |
219 | it 'sets from' do
220 | mailer.deliver!(mail)
221 | expect(client.sent_mail['from']).to eq('email' => 'taco@cat.limo', 'name' => 'Taco Cat')
222 | end
223 | end
224 |
225 | context 'from contains a friendly name (with quotes)' do
226 | before { mail.from = '"Taco Cat" '}
227 |
228 | it 'sets from' do
229 | mailer.deliver!(mail)
230 | expect(client.sent_mail['from']).to eq('email' => 'taco@cat.limo', 'name' => 'Taco Cat')
231 | end
232 | end
233 |
234 | it 'sets subject' do
235 | mailer.deliver!(mail)
236 | expect(client.sent_mail['subject']).to eq('Hello, world!')
237 | end
238 |
239 | it 'sets a text/plain body' do
240 | mail.content_type = 'text/plain'
241 | mail.body = 'I heard you like pineapple.'
242 | mailer.deliver!(mail)
243 | expect(client.sent_mail['content']).to eq([
244 | {
245 | 'type' => 'text/plain',
246 | 'value' => 'I heard you like pineapple.'
247 | }
248 | ])
249 | end
250 |
251 | it 'sets a text/html body' do
252 | mail.content_type = 'text/html'
253 | mail.body = 'I heard you like pineapple.'
254 | mailer.deliver!(mail)
255 |
256 | expect(client.sent_mail['content']).to eq([
257 | {
258 | 'type' => 'text/html',
259 | 'value' => 'I heard you like pineapple.'
260 | }
261 | ])
262 | end
263 |
264 | context 'template_id' do
265 | before do
266 | mail['template_id'] = '1'
267 | end
268 |
269 | it 'sets a template_id' do
270 | mailer.deliver!(mail)
271 | expect(client.sent_mail['template_id']).to eq('1')
272 | end
273 |
274 | it 'does not set unsubscribe substitutions' do
275 | mailer.deliver!(mail)
276 | expect(client.sent_mail['personalizations'].first).to_not have_key('substitutions')
277 | end
278 |
279 | it 'does not set send a content type' do
280 | mailer.deliver!(mail)
281 | expect(client.sent_mail['content']).to eq(nil)
282 | end
283 |
284 | it 'does not set send a content type even if body is given' do
285 | # This matches the default behavior of ActionMail. body must be
286 | # specified and content_type defaults to text/plain.
287 | mail.body = 'I heard you like pineapple.'
288 | mail.content_type = 'text/plain'
289 | mailer.deliver!(mail)
290 | expect(client.sent_mail['content']).to eq(nil)
291 | end
292 | end
293 |
294 | context 'without dynamic template data or a template id' do
295 | it 'sets unsubscribe substitutions' do
296 | mailer.deliver!(mail)
297 | expect(client.sent_mail['personalizations'].first).to have_key('substitutions')
298 | substitutions = client.sent_mail['personalizations'].first['substitutions']
299 | expect(substitutions).to eq({
300 | '%asm_group_unsubscribe_raw_url%' => '<%asm_group_unsubscribe_raw_url%>',
301 | '%asm_global_unsubscribe_raw_url%' => '<%asm_global_unsubscribe_raw_url%>',
302 | '%asm_preferences_raw_url%' => '<%asm_preferences_raw_url%>'
303 | })
304 | end
305 | end
306 |
307 | context 'send options' do
308 |
309 | it 'sets sections' do
310 | mail['sections'] = {'%foo%' => 'bar'}
311 | mailer.deliver!(mail)
312 | expect(client.sent_mail['sections']).to eq({'%foo%' => 'bar'})
313 | end
314 |
315 | it 'sets headers' do
316 | mail['headers'] = {'X-FOO' => 'bar'}
317 | mailer.deliver!(mail)
318 | expect(client.sent_mail['headers']).to eq({'X-FOO' => 'bar'})
319 | end
320 |
321 | it 'sets categories' do
322 | mail['categories'] = ['foo', 'bar']
323 | mailer.deliver!(mail)
324 | expect(client.sent_mail['categories']).to eq(['foo', 'bar'])
325 | end
326 |
327 | it 'sets custom_args' do
328 | mail['custom_args'] = {'campaign' => 'welcome'}
329 | mailer.deliver!(mail)
330 | expect(client.sent_mail['custom_args']).to eq({'campaign' => 'welcome'})
331 | end
332 |
333 | it 'sets send_at and batch_id' do
334 | epoch = Time.now.to_i
335 | mail['send_at'] = epoch
336 | mail['batch_id'] = 3
337 | mailer.deliver!(mail)
338 | expect(client.sent_mail['send_at']).to eq(epoch)
339 | expect(client.sent_mail['batch_id']).to eq('3')
340 | end
341 |
342 | it 'sets asm' do
343 | asm = {group_id: 99, groups_to_display: [4,5,6,7,8]}
344 | mail['asm'] = asm
345 | mailer.deliver!(mail)
346 | expect(client.sent_mail['asm']).to eq(transform_keys(asm, &:to_s))
347 | end
348 |
349 | it 'sets ip_pool_name' do
350 | mail['ip_pool_name'] = 'marketing'
351 | mailer.deliver!(mail)
352 | expect(client.sent_mail['ip_pool_name']).to eq('marketing')
353 | end
354 |
355 | it 'should not change values inside custom args' do
356 | custom_args = { 'text' => 'line with a => in it' }
357 | mail['custom_args'] = custom_args
358 | mailer.deliver!(mail)
359 | expect(client.sent_mail['custom_args']).to eq('text' => 'line with a => in it')
360 | end
361 |
362 | context 'mail_settings' do
363 | it 'sets bcc' do
364 | bcc = { bcc: { enable: true, email: 'test@example.com' }}
365 | mail['mail_settings'] = bcc
366 | mailer.deliver!(mail)
367 | expect(client.sent_mail['mail_settings']).to eq(transform_keys(bcc, &:to_s))
368 | end
369 |
370 | it 'sets bypass_list_management' do
371 | bypass = { bypass_list_management: { enable: true }}
372 | mail['mail_settings'] = bypass
373 | mailer.deliver!(mail)
374 | expect(client.sent_mail['mail_settings']).to eq(transform_keys(bypass, &:to_s))
375 | end
376 |
377 | it 'sets footer' do
378 | footer = {footer: { enable: true, text: 'Footer Text', html: 'Footer Text'}}
379 | mail['mail_settings'] = footer
380 | mailer.deliver!(mail)
381 | expect(client.sent_mail['mail_settings']).to eq(transform_keys(footer, &:to_s))
382 | end
383 |
384 | it 'sets sandbox_mode' do
385 | sandbox = {sandbox_mode: { enable: true }}
386 | mail['mail_settings'] = sandbox
387 | mailer.deliver!(mail)
388 | expect(client.sent_mail['mail_settings']).to eq(transform_keys(sandbox, &:to_s))
389 | end
390 |
391 | it 'sets spam_check' do
392 | spam_check = {spam_check: { enable: true, threshold: 1, post_to_url: 'https://spamcatcher.sendgrid.com'}}
393 | mail['mail_settings'] = spam_check
394 | mailer.deliver!(mail)
395 | expect(client.sent_mail['mail_settings']).to eq(transform_keys(spam_check, &:to_s))
396 | end
397 | end
398 |
399 | context 'tracking_settings' do
400 | it 'sets click_tracking' do
401 | tracking = { click_tracking: { enable: false, enable_text: false }}
402 | mail['tracking_settings'] = tracking.dup
403 | mailer.deliver!(mail)
404 | expect(client.sent_mail['tracking_settings']).to eq(transform_keys(tracking, &:to_s))
405 | end
406 |
407 | it 'sets open_tracking' do
408 | tracking = { open_tracking: { enable: true, substitution_tag: 'Optional tag to replace with the open image in the body of the message' }}
409 | mail['tracking_settings'] = tracking
410 | mailer.deliver!(mail)
411 | expect(client.sent_mail['tracking_settings']).to eq(transform_keys(tracking, &:to_s))
412 | end
413 |
414 | it 'sets subscription_tracking' do
415 | tracking = { subscription_tracking: { enable: true, text: 'text to insert into the text/plain portion of the message', html: 'html to insert into the text/html portion of the message', substitution_tag: 'Optional tag to replace with the open image in the body of the def message' }}
416 | mail['tracking_settings'] = tracking
417 | mailer.deliver!(mail)
418 | expect(client.sent_mail['tracking_settings']).to eq(transform_keys(tracking, &:to_s))
419 | end
420 |
421 | it 'sets ganalytics' do
422 | tracking = { ganalytics: { enable: true, utm_source: 'some source', utm_medium: 'some medium', utm_term: 'some term', utm_content: 'some content', utm_campaign: 'some campaign' }}
423 | mail['tracking_settings'] = tracking
424 | mailer.deliver!(mail)
425 | expect(client.sent_mail['tracking_settings']).to eq(transform_keys(tracking, &:to_s))
426 | end
427 | end
428 |
429 | context 'dynamic template data' do
430 | let(:template_data) do
431 | { variable_1: '1', variable_2: '2' }
432 | end
433 |
434 | before { mail['dynamic_template_data'] = template_data }
435 |
436 | it 'sets dynamic_template_data' do
437 | mailer.deliver!(mail)
438 | expect(client.sent_mail['personalizations'].first['dynamic_template_data']).to eq(template_data)
439 | end
440 |
441 | it 'does not set unsubscribe substitutions' do
442 | mailer.deliver!(mail)
443 | expect(client.sent_mail['personalizations'].first).to_not have_key('substitutions')
444 | end
445 |
446 | context 'containing what looks like hash syntax' do
447 | let(:template_data) do
448 | { hint: 'Just use => instead of :' }
449 | end
450 |
451 | it 'does not change values inside dynamic template data' do
452 | mailer.deliver!(mail)
453 | expect(
454 | client.sent_mail['personalizations'].first['dynamic_template_data']
455 | ).to eq(template_data)
456 | end
457 | end
458 | end
459 |
460 | it 'sets dynamic template data and sandbox_mode' do
461 | mail['mail_settings'] = {}
462 | mailer.deliver!(mail)
463 | expect(client.sent_mail['mail_settings']).to eq(nil)
464 | end
465 | end
466 |
467 | context 'multipart/alternative' do
468 | before do
469 | mail.content_type 'multipart/alternative'
470 | mail.part do |part|
471 | part.text_part = Mail::Part.new do
472 | content_type 'text/plain'
473 | body 'I heard you like pineapple.'
474 | end
475 | part.html_part = Mail::Part.new do
476 | content_type 'text/html'
477 | body 'I heard you like pineapple.'
478 | end
479 | end
480 | end
481 |
482 | it 'sets the text and html body' do
483 | mailer.deliver!(mail)
484 | expect(client.sent_mail['content']).to include({
485 | 'type' => 'text/html',
486 | 'value' => 'I heard you like pineapple.'
487 | })
488 | expect(client.sent_mail['content']).to include({
489 | 'type' => 'text/plain',
490 | 'value' => 'I heard you like pineapple.'
491 | })
492 | end
493 | end
494 |
495 | context 'multipart/mixed' do
496 | before do
497 | mail.content_type 'multipart/mixed'
498 | mail.part do |part|
499 | part.text_part = Mail::Part.new do
500 | content_type 'text/plain'
501 | body 'I heard you like pineapple.'
502 | end
503 | part.html_part = Mail::Part.new do
504 | content_type 'text/html'
505 | body 'I heard you like pineapple.'
506 | end
507 | end
508 | mail.attachments['specs.rb'] = File.read(__FILE__)
509 | end
510 |
511 | it 'sets the text and html body' do
512 | mailer.deliver!(mail)
513 | expect(client.sent_mail['content']).to include({
514 | 'type' => 'text/html',
515 | 'value' => 'I heard you like pineapple.'
516 | })
517 | expect(client.sent_mail['content']).to include({
518 | 'type' => 'text/plain',
519 | 'value' => 'I heard you like pineapple.'
520 | })
521 | end
522 |
523 | it 'adds the attachment' do
524 | expect(mail.attachments.first.read).to include("it 'adds the attachment' do")
525 | mailer.deliver!(mail)
526 | attachment = client.sent_mail['attachments'].first
527 | expect(attachment['filename']).to eq('specs.rb')
528 | expect(attachment['type']).to eq('application/x-ruby')
529 | end
530 | end
531 |
532 | context 'multipart/related' do
533 | before do
534 | mail.content_type 'multipart/related'
535 | mail.part do |part|
536 | part.text_part = Mail::Part.new do
537 | content_type 'text/plain'
538 | body 'I heard you like pineapple.'
539 | end
540 | part.html_part = Mail::Part.new do
541 | content_type 'text/html'
542 | body 'I heard you like pineapple.'
543 | end
544 | end
545 | mail.attachments.inline['specs.rb'] = File.read(__FILE__)
546 | end
547 |
548 | it 'sets the text and html body' do
549 | mailer.deliver!(mail)
550 | expect(client.sent_mail['content']).to include({
551 | 'type' => 'text/html',
552 | 'value' => 'I heard you like pineapple.'
553 | })
554 | expect(client.sent_mail['content']).to include({
555 | 'type' => 'text/plain',
556 | 'value' => 'I heard you like pineapple.'
557 | })
558 | end
559 |
560 | it 'adds the inline attachment' do
561 | expect(mail.attachments.first.read).to include("it 'adds the inline attachment' do")
562 | mailer.deliver!(mail)
563 | content = client.sent_mail['attachments'].first
564 | expect(content['filename']).to eq('specs.rb')
565 | expect(content['type']).to eq('application/x-ruby')
566 | expect(content['content_id'].class).to eq(String)
567 | expect(content['content_id']).to include("@")
568 | expect(content['content_id']).not_to include("<")
569 | expect(content['content_id']).not_to include(">")
570 | end
571 | end
572 |
573 | context 'with personalizations' do
574 | let(:personalizations) do
575 | [
576 | {
577 | 'to' => [
578 | {'email' => 'john1@example.com', 'name' => 'John 1'},
579 | {'email' => 'john2@example.com', 'name' => 'John 2'},
580 | ]
581 | },
582 | {
583 | 'to' => [
584 | {'email' => 'john3@example.com', 'name' => 'John 3'},
585 | {'email' => 'john4@example.com'}
586 | ],
587 | 'cc' => [
588 | {'email' => 'cc@example.com'}
589 | ],
590 | 'bcc' => [
591 | {'email' => 'bcc@example.com'}
592 | ],
593 | 'substitutions' => {
594 | '%fname%' => 'Bob'
595 | },
596 | 'subject' => 'personalized subject',
597 | 'send_at' => 1443636843,
598 | 'custom_args' => {
599 | 'user_id' => '343'
600 | },
601 | 'headers' => {
602 | 'X-Test' => true
603 | }
604 | }
605 | ]
606 | end
607 |
608 | before do
609 | mail.to = nil
610 | mail.cc = nil
611 | mail.bcc = nil
612 | mail['personalizations'] = personalizations
613 | end
614 |
615 | it 'sets the provided to address personalizations' do
616 | mailer.deliver!(mail)
617 | expect(client.sent_mail['personalizations'].length).to eq(2)
618 | expect(client.sent_mail['personalizations'][0]['to']).to eq(personalizations[0]['to'])
619 | expect(client.sent_mail['personalizations'][1]['to']).to eq(personalizations[1]['to'])
620 | end
621 |
622 | it 'sets the provided cc address personalizations' do
623 | mailer.deliver!(mail)
624 | expect(client.sent_mail['personalizations'][0]).to_not have_key('cc')
625 | expect(client.sent_mail['personalizations'][1]['cc']).to eq(personalizations[1]['cc'])
626 | end
627 |
628 | it 'sets the provided bcc address personalizations' do
629 | mailer.deliver!(mail)
630 | expect(client.sent_mail['personalizations'][0]).to_not have_key('bcc')
631 | expect(client.sent_mail['personalizations'][1]['bcc']).to eq(personalizations[1]['bcc'])
632 | end
633 |
634 | it 'sets the provided subject personalizations' do
635 | mailer.deliver!(mail)
636 | expect(client.sent_mail['personalizations'][0]).to_not have_key('subject')
637 | expect(client.sent_mail['personalizations'][1]['subject']).to eq(personalizations[1]['subject'])
638 | end
639 |
640 | it 'sets the provided headers personalizations' do
641 | mailer.deliver!(mail)
642 | expect(client.sent_mail['personalizations'][0]).to_not have_key('headers')
643 | expect(client.sent_mail['personalizations'][1]['headers']).to eq(personalizations[1]['headers'])
644 | end
645 |
646 | it 'sets the provided custom_arg personalizations' do
647 | mailer.deliver!(mail)
648 | expect(client.sent_mail['personalizations'][0]).to_not have_key('custom_args')
649 | expect(client.sent_mail['personalizations'][1]['custom_args']).to eq(personalizations[1]['custom_args'])
650 | end
651 |
652 | it 'sets the provided send_at personalizations' do
653 | mailer.deliver!(mail)
654 | expect(client.sent_mail['personalizations'][0]).to_not have_key('send_at')
655 | expect(client.sent_mail['personalizations'][1]['send_at']).to eq(personalizations[1]['send_at'])
656 | end
657 |
658 | it 'sets the provided substitution personalizations' do
659 | mailer.deliver!(mail)
660 | expect(client.sent_mail['personalizations'][1]['substitutions']).to include(personalizations[1]['substitutions'])
661 | end
662 |
663 | it 'adds to the unsubscribe link substitutions' do
664 | mailer.deliver!(mail)
665 | expect(client.sent_mail['personalizations'][0]['substitutions']).to eq({
666 | '%asm_group_unsubscribe_raw_url%' => '<%asm_group_unsubscribe_raw_url%>',
667 | '%asm_global_unsubscribe_raw_url%' => '<%asm_global_unsubscribe_raw_url%>',
668 | '%asm_preferences_raw_url%' => '<%asm_preferences_raw_url%>'
669 | })
670 | expect(client.sent_mail['personalizations'][1]['substitutions']).to include({
671 | '%asm_group_unsubscribe_raw_url%' => '<%asm_group_unsubscribe_raw_url%>',
672 | '%asm_global_unsubscribe_raw_url%' => '<%asm_global_unsubscribe_raw_url%>',
673 | '%asm_preferences_raw_url%' => '<%asm_preferences_raw_url%>'
674 | })
675 | end
676 |
677 | context 'with symbols used as keys' do
678 | let(:personalizations) do
679 | [
680 | {
681 | to: [
682 | {email: 'sally1@example.com', name: 'Sally 1'},
683 | {email: 'sally2@example.com', name: 'Sally 2'},
684 | ]
685 | }
686 | ]
687 | end
688 |
689 | it 'still works' do
690 | mailer.deliver!(mail)
691 | expect(client.sent_mail['personalizations'].length).to eq(1)
692 | expected_to = personalizations[0][:to].map { |t| transform_keys(t, &:to_s) }
693 | expect(client.sent_mail['personalizations'][0]['to']).to eq(expected_to)
694 | end
695 | end
696 |
697 | context 'dynamic template data passed into a personalizaiton' do
698 | let(:personalization_data) do
699 | {
700 | 'variable_1' => '1', 'variable_2' => '2'
701 | }
702 | end
703 |
704 | let(:personalizations_with_dynamic_data) do
705 | personalizations.tap do |p|
706 | p[1]['dynamic_template_data'] = personalization_data
707 | end
708 | end
709 |
710 | before do
711 | mail['personalizations'] = nil
712 | mail['personalizations'] = personalizations_with_dynamic_data
713 | end
714 |
715 | it 'sets the provided dynamic template data personalizations' do
716 | mailer.deliver!(mail)
717 | expect(client.sent_mail['personalizations'][0]).to_not have_key('dynamic_template_data')
718 | expect(client.sent_mail['personalizations'][1]['dynamic_template_data']).to eq(personalization_data)
719 | end
720 |
721 | context 'dynamic template data is also set on the mail object' do
722 | let(:mail_template_data) do
723 | { 'variable_3' => '1', 'variable_4' => '2' }
724 | end
725 |
726 | before { mail['dynamic_template_data'] = mail_template_data.dup }
727 |
728 | it 'sets dynamic_template_data where not also provided as a personalization' do
729 | mailer.deliver!(mail)
730 | expect(client.sent_mail['personalizations'][0]['dynamic_template_data']).to eq(mail_template_data)
731 | end
732 |
733 | it 'merges the template data with a personalizations dynamic data' do
734 | mailer.deliver!(mail)
735 | expect(client.sent_mail['personalizations'][1]['dynamic_template_data']).to eq(
736 | mail_template_data.merge(personalization_data)
737 | )
738 | end
739 | end
740 | end
741 |
742 | context 'when to is set on mail object' do
743 | before { mail.to = 'test@sendgrid.com' }
744 |
745 | it 'adds that to address as a separate personalization' do
746 | mailer.deliver!(mail)
747 | expect(client.sent_mail['personalizations'].length).to eq(3)
748 | expect(client.sent_mail['personalizations'][0]['to']).to eq(personalizations[0]['to'])
749 | expect(client.sent_mail['personalizations'][1]['to']).to eq(personalizations[1]['to'])
750 | expect(client.sent_mail['personalizations'][2]['to']).to eq([{"email"=>"test@sendgrid.com"}])
751 | end
752 | end
753 |
754 | context 'when cc is set on mail object' do
755 | before { mail.cc = 'test@sendgrid.com' }
756 |
757 | it 'adds that cc address as a separate personalization' do
758 | mailer.deliver!(mail)
759 | expect(client.sent_mail['personalizations'].length).to eq(3)
760 | expect(client.sent_mail['personalizations'][0]['cc']).to eq(personalizations[0]['cc'])
761 | expect(client.sent_mail['personalizations'][1]['cc']).to eq(personalizations[1]['cc'])
762 | expect(client.sent_mail['personalizations'][2]['cc']).to eq([{"email"=>"test@sendgrid.com"}])
763 | end
764 | end
765 |
766 | context 'when bcc is set on mail object' do
767 | before { mail.bcc = 'test@sendgrid.com' }
768 |
769 | it 'adds that bcc address as a separate personalization' do
770 | mailer.deliver!(mail)
771 | expect(client.sent_mail['personalizations'].length).to eq(3)
772 | expect(client.sent_mail['personalizations'][0]['bcc']).to eq(personalizations[0]['bcc'])
773 | expect(client.sent_mail['personalizations'][1]['bcc']).to eq(personalizations[1]['bcc'])
774 | expect(client.sent_mail['personalizations'][2]['bcc']).to eq([{"email"=>"test@sendgrid.com"}])
775 | end
776 | end
777 |
778 | context 'when perform_send_request false' do
779 | it 'should not send and email and return json body' do
780 | m = DeliveryMethod.new(perform_send_request: false, return_response: true, api_key: 'key')
781 | response = m.deliver!(mail)
782 | expect(response).to respond_to(:to_json)
783 | end
784 | end
785 |
786 | context 'when mail_settings are present' do
787 | it 'should apply mail_settings to request body' do
788 | m = DeliveryMethod.new(api_key: 'key', return_response: true, mail_settings: { sandbox_mode: {enable: true }})
789 | m.deliver!(mail)
790 | expect(client.sent_mail['mail_settings']).to eq("sandbox_mode" => {"enable" => true })
791 | end
792 |
793 | context 'when mail has mail_settings set' do
794 | before { mail['mail_settings'] = { spam_check: { enable: true } } }
795 |
796 | it 'should combine local mail_settings with global settings' do
797 | m = DeliveryMethod.new(api_key: 'key', return_response: true, mail_settings: { sandbox_mode: {enable: true }})
798 | m.deliver!(mail)
799 | expect(client.sent_mail['mail_settings']).to eq(
800 | "sandbox_mode" => {"enable" => true },
801 | "spam_check" => {"enable" => true },
802 | )
803 | end
804 | end
805 |
806 | context 'when mail contains the same setting as global settings' do
807 | before do
808 | mail['mail_settings'] = {
809 | sandbox_mode: { enable: false },
810 | spam_check: { enable: true }
811 | }
812 | end
813 |
814 | it 'should apply local mail_settings on top of global settings' do
815 | m = DeliveryMethod.new(api_key: 'key', return_response: true, mail_settings: { sandbox_mode: {enable: true }})
816 | m.deliver!(mail)
817 | expect(client.sent_mail['mail_settings']).to eq(
818 | "sandbox_mode" => {"enable" => false },
819 | "spam_check" => {"enable" => true },
820 | )
821 | end
822 | end
823 | end
824 | end
825 | end
826 | end
827 | end
828 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require_relative '../lib/sendgrid_actionmailer'
2 |
--------------------------------------------------------------------------------