├── .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 | --------------------------------------------------------------------------------