mail-ses 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a729c8707418afffdc7f0c411a4f92b66d236db4ed0d1e41940c1ab00cecdb6c
4
+ data.tar.gz: 45fd00d6204d3dd2764b78f5ee250f4cbf17f01a4dd6cf8671d9d55530eb9f68
5
+ SHA512:
6
+ metadata.gz: aaddce5f599c511aa774e9d8daba34e2142b88ebcee3048cbcac32c5e5c09ea653e627fc0087ea76f5086e3a6de40b881a7de46b79336b523ff7a466674faffa
7
+ data.tar.gz: 89973fc7a007b2058e5c33f819519b06e8962e93439880799d74454ed6b52ca86dafef7adfffccd745c8fe3b70f4fcb05a66dec5b763f2c12783e1c032cbe017
@@ -0,0 +1,4 @@
1
+ # 0.1.0
2
+
3
+ - Initial release of gem.
4
+ - Support for sending ActionMailer mails via AWS SDK v3.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2018 TableCheck Inc.
2
+
3
+ AWS::SES originally copyright (c) 2011 Drew V. Blas <drew.blas@gmail.com>
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.
@@ -0,0 +1,121 @@
1
+ [![Gem Version](https://badge.fury.io/rb/mail-ses.svg)](http://badge.fury.io/rb/mail-ses)
2
+ [![Travis Status](https://travis-ci.org/tablecheck/mail-ses.svg?branch=master)](https://travis-ci.org/tablecheck/mail-ses)
3
+
4
+ # Mail::SES
5
+
6
+ Mail::SES is a mail delivery method handler for Amazon SES (Simple Email Service) which can be used with Rails' [Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html).
7
+
8
+ This gem is inspired by [Drew Blas' AWS::SES gem](https://github.com/drewblas/aws-ses),
9
+ but uses the official [AWS SDK v3 for SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-using-sdk-ruby.html) under-the-hood.
10
+ By passing parameters through to the SDK, this gem supports greater flexibility with less code (including IAM instance profiles, retry parameters, etc.)
11
+
12
+ ### Compatibility
13
+
14
+ * TBD
15
+
16
+ ### Shameless Plug
17
+
18
+ ActionMailer::SES is sponsored by [TableCheck](http://corp.tablecheck.com/), Japan's leading restaurant management app. If **you** are a ninja-level Javascript/Ruby coder, designer, project manager, etc. and are eager to work in Tokyo with other ninjas, Japan in a dynamic environment, please get in touch at [careers@tablecheck.com](mailto:careers@tablecheck.com).
19
+
20
+ ## Getting Started
21
+
22
+ In your `Gemfile`:
23
+
24
+ ```ruby
25
+ gem 'mail-ses'
26
+ ```
27
+
28
+ Next, make a new initializer at `config/initializers/mail_ses.rb`:
29
+
30
+ ```ruby
31
+ ActionMailer::Base.add_delivery_method :ses, Mail::SES,
32
+ region: 'us-east-1',
33
+ access_key_id: 'abc',
34
+ secret_access_key: '123'
35
+ ```
36
+
37
+ Finally, in the appropriate `config/environments/*.rb`:
38
+
39
+ ```ruby
40
+ config.action_mailer.delivery_method = :ses
41
+ ```
42
+
43
+ ## Advanced Usage
44
+
45
+ ### AWS SES Client Options
46
+
47
+ Any options supported by the `Aws::SES::Client` class can be passed into the initializer, for example:
48
+
49
+ ```ruby
50
+ ActionMailer::Base.add_delivery_method :ses, Mail::SES,
51
+ region: 'us-east-1',
52
+ session_token: 'foobar',
53
+ retry_limit: 5,
54
+ retry_max_delay: 10
55
+ ```
56
+
57
+ In addition, the shortcut option `:use_iam_profile (Boolean)` which activates the IAM instance profile.
58
+
59
+ ```ruby
60
+ ActionMailer::Base.add_delivery_method :ses, Mail::SES,
61
+ region: 'us-east-1',
62
+ use_iam_profile: true
63
+ ```
64
+
65
+ ### Default Mail Options
66
+
67
+ In the initializer you can set `:mail_options (Hash)` which are default options to pass-through to each mail sent:
68
+
69
+ ```ruby
70
+ ActionMailer::Base.add_delivery_method :ses, Mail::SES,
71
+ # ...
72
+ mail_options: {
73
+ source_arn: 'arn:aws:ses:us-east-1:123456789012:identity/example.com',
74
+ tags: [
75
+ { name: 'MessageTagName', value: 'MessageTagValue' },
76
+ ],
77
+ }
78
+ ```
79
+
80
+ ### AWS Error Handling
81
+
82
+ To handle errors from AWS API, in the initializer you can set `:error_handler (Proc)` which takes two args:
83
+ the error which was raised, and the raw_email options hash. This is useful for notifying your bug tracking service.
84
+ Setting `:error_handler` causes the error to be swallowed unless it is raised again in the handler itself.
85
+
86
+ ```ruby
87
+ ActionMailer::Base.add_delivery_method :ses, Mail::SES,
88
+ # ...
89
+ error_handler: ->(error, raw_email) do
90
+ Bugsnag.notify(error){|r| r.add_tab('email', { email: raw_email })}
91
+ raise error
92
+ end
93
+ ```
94
+
95
+ ### Send Email as a Standalone
96
+
97
+ You can send one-off mails using the `Mail::SES` object and `#deliver` method.
98
+
99
+ ```ruby
100
+ mail = Mail.new(args)
101
+
102
+ ses = Mail::SES.new(region: 'us-east-1',
103
+ access_key_id: 'abc',
104
+ secret_access_key: '123')
105
+
106
+ options = { source_arn: 'arn:aws:ses:us-east-1:123456789012:identity/example.com' }
107
+
108
+ ses.deliver!(mail, options) #=> returns AWS API response
109
+
110
+ mail.message_id #=> "00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa0680-000000@email.amazonses.com"
111
+ ```
112
+
113
+ Please also see the [AWS SDK v3 for SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-using-sdk-ruby.html) for alternate approaches.
114
+
115
+ ## Statistics, Verified Addresses, Bounce Rate, etc.
116
+
117
+ Please use the official [AWS SDK v3 for SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-using-sdk-ruby.html).
118
+
119
+ ## Copyright
120
+
121
+ Copyright (c) 2018 TableCheck Inc. See LICENSE for further details.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require 'aws-sdk-ses'
3
+ require 'mail'
4
+ require 'mail/ses'
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ module Mail
3
+ # Mail delivery method handler for AWS SES
4
+ class SES
5
+ VERSION = File.read(File.join(File.dirname(__FILE__), '../../VERSION')).strip.freeze
6
+
7
+ RAW_EMAIL_ATTRS = %i[ source
8
+ source_arn
9
+ from_arn
10
+ return_path_arn
11
+ tags
12
+ configuration_set_name ].freeze
13
+
14
+ attr_reader :client
15
+
16
+ # Initializes the Mail::SES object.
17
+ #
18
+ # options - The Hash options (optional, default: {}):
19
+ # :mail_options - (Hash) Default AWS options to set on each mail object.
20
+ # :error_handler - (Proc<Error, Hash>) Handler for AWS API errors.
21
+ # :use_iam_profile - Shortcut to use AWS IAM instance profile.
22
+ # All other options are passed-thru to Aws::SES::Client.
23
+ def initialize(options = {})
24
+ @mail_options = options.delete(:mail_options) || {}
25
+ @error_handler = options.delete(:error_handler)
26
+ self.class.validate_error_handler(@error_handler)
27
+ options = self.class.build_client_options(options)
28
+ @client = Aws::SES::Client.new(options)
29
+ end
30
+
31
+ # Delivers a Mail object via SES.
32
+ #
33
+ # mail - The Mail object to deliver (required).
34
+ # options - The Hash options which override any defaults set in :mail_options
35
+ # in the initializer (optional, default: {}). Refer to
36
+ # Aws::SES::Client#send_raw_email
37
+ def deliver!(mail, options = {})
38
+ self.class.validate_mail(mail)
39
+ options = @mail_options.merge(options || {})
40
+ raw_email_options = self.class.build_raw_email_options(mail, options)
41
+ begin
42
+ response = client.send_raw_email(raw_email_options)
43
+ mail.message_id = "#{response.to_h[:message_id]}@email.amazonses.com"
44
+ response
45
+ rescue StandardError => e
46
+ @error_handler ? @error_handler.call(e, raw_email_options.dup) : raise(e)
47
+ end
48
+ end
49
+
50
+ class << self
51
+ def validate_error_handler(error_handler)
52
+ raise ArgumentError.new(':error_handler must be a Proc') if error_handler && !error_handler.is_a?(Proc)
53
+ end
54
+
55
+ def validate_mail(mail)
56
+ unless mail.is_a?(Mail::Message)
57
+ raise ArgumentError.new('mail must be an instance of Mail::Message class')
58
+ end
59
+
60
+ Mail::CheckDeliveryParams.check(mail)
61
+
62
+ if mail.has_attachments? && mail.text_part.nil? && mail.html_part.nil?
63
+ raise ArgumentError.new('Attachment provided without message body')
64
+ end
65
+ end
66
+
67
+ def build_client_options(options)
68
+ options[:credentials] = Aws::InstanceProfileCredentials.new if options.delete(:use_iam_profile)
69
+ options
70
+ end
71
+
72
+ def build_raw_email_options(message, options = {})
73
+ output = slice_hash(options, *RAW_EMAIL_ATTRS)
74
+ output[:source] ||= message.from.first
75
+ output[:destinations] = [message.to, message.cc, message.bcc].flatten.compact
76
+ output[:raw_message] = { data: Base64.encode64(message.to_s) }
77
+ output
78
+ end
79
+
80
+ protected
81
+
82
+ def slice_hash(hash, *keys)
83
+ keys.each_with_object({}) { |k, h| h[k] = hash[k] if hash.key?(k) }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe Mail::SES do
5
+ let(:ses_options) { { stub_responses: true } }
6
+
7
+ let(:ses) do
8
+ described_class.new(ses_options)
9
+ end
10
+
11
+ let(:mail) do
12
+ Mail.new do
13
+ from 'from@abc.com'
14
+ to %w[to1@def.com to2@xyz.com]
15
+ cc %w[cc1@xyz.com cc2@def.com]
16
+ bcc %w[bcc1@abc.com bcc2@def.com]
17
+ body 'This is the body'
18
+ end
19
+ end
20
+
21
+ describe '::VERSION' do
22
+ it { expect(described_class::VERSION).to match(/\A\d+\.\d+\.\d+/) }
23
+ end
24
+
25
+ describe '#initialize' do
26
+ it 'accepts valid :error_handler' do
27
+ expect(described_class.new(ses_options)).to be_a(Mail::SES)
28
+ end
29
+
30
+ it 'accepts valid :error_handler' do
31
+ expect(described_class.new(ses_options.merge(error_handler: ->(a, b) {}))).to be_a(Mail::SES)
32
+ end
33
+
34
+ it 'rejects invalid :error_handler' do
35
+ expect { described_class.new(ses_options.merge(error_handler: 'foobar')) }.to raise_error(ArgumentError, ':error_handler must be a Proc')
36
+ end
37
+
38
+ it 'handles :use_iam_profile option' do
39
+ allow_any_instance_of(Aws::InstanceProfileCredentials).to receive(:get_credentials).and_return('{}')
40
+ ses = described_class.new(ses_options.merge(use_iam_profile: true))
41
+ expect(ses.client.config.credentials).to be_a(Aws::InstanceProfileCredentials)
42
+ end
43
+
44
+ it 'passes through options to AWS' do
45
+ ses = described_class.new(ses_options.merge(log_level: :debug, retry_limit: 5))
46
+ expect(ses.client.config.log_level).to eq :debug
47
+ expect(ses.client.config.retry_limit).to eq 5
48
+ end
49
+ end
50
+
51
+ describe '#deliver!' do
52
+ it 'validates that mail is a Mail' do
53
+ expect { ses.deliver!(foo: :bar) }.to raise_error(ArgumentError, 'mail must be an instance of Mail::Message class')
54
+ end
55
+
56
+ it 'validates integrity of Mail' do
57
+ expect { ses.deliver!(Mail.new) }.to raise_error(ArgumentError, 'SMTP From address may not be blank: nil')
58
+ expect { ses.deliver!(Mail.new { from 'foo@bar.com' }) }.to raise_error(ArgumentError, 'SMTP To address may not be blank: []')
59
+ end
60
+
61
+ it 'validates attachment without body' do
62
+ mail.body = nil
63
+ mail.add_file __FILE__
64
+ expect { ses.deliver!(mail) }.to raise_error(ArgumentError, 'Attachment provided without message body')
65
+ end
66
+
67
+ context 'when options set' do
68
+ before { allow_any_instance_of(Mail::Message).to receive(:to_s).and_return('Fixed message body') }
69
+ let(:ses_options) { { stub_responses: true, mail_options: { source: 'foo@bar.com', source_arn: 'sa1' } } }
70
+
71
+ let(:exp) do
72
+ {
73
+ source: 'foo@bar.com',
74
+ source_arn: 'sa2',
75
+ destinations: %w[to1@def.com to2@xyz.com cc1@xyz.com cc2@def.com bcc1@abc.com bcc2@def.com],
76
+ raw_message: {
77
+ data: "Rml4ZWQgbWVzc2FnZSBib2R5\n"
78
+ }
79
+ }
80
+ end
81
+
82
+ it 'allows pass-thru and override of default options' do
83
+ expect(ses.client).to receive(:send_raw_email).with(exp)
84
+ ses.deliver!(mail, source_arn: 'sa2')
85
+ end
86
+ end
87
+
88
+ it 'sets mail.message_id' do
89
+ ses.deliver!(mail)
90
+ expect(mail.message_id).to eq('MessageId@email.amazonses.com')
91
+ end
92
+
93
+ it 'returns the AWS response' do
94
+ expect(ses.deliver!(mail)).to be_a(Seahorse::Client::Response)
95
+ end
96
+
97
+ context 'error handling' do
98
+ before { allow_any_instance_of(Aws::SES::Client).to receive(:send_raw_email).and_raise(RuntimeError.new('test')) }
99
+
100
+ context 'when :error_handler not set' do
101
+ it 'raises the error' do
102
+ expect { ses.deliver!(mail) }.to raise_error(RuntimeError, 'test')
103
+ end
104
+ end
105
+
106
+ context 'when :error_handler set' do
107
+ let(:ses_options) { { stub_responses: true, error_handler: ->(a, b) {} } }
108
+
109
+ it 'calls the error handler' do
110
+ expect(ses_options[:error_handler]).to receive(:call).and_call_original
111
+ ses.deliver!(mail)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ describe '::build_raw_email_options' do
118
+ let(:options) { {} }
119
+ subject { described_class.build_raw_email_options(mail, options) }
120
+ before { allow_any_instance_of(Mail::Message).to receive(:to_s).and_return('Fixed message body') }
121
+
122
+ context 'without options' do
123
+ let(:exp) do
124
+ {
125
+ source: 'from@abc.com',
126
+ destinations: %w[to1@def.com to2@xyz.com cc1@xyz.com cc2@def.com bcc1@abc.com bcc2@def.com],
127
+ raw_message: {
128
+ data: "Rml4ZWQgbWVzc2FnZSBib2R5\n"
129
+ }
130
+ }
131
+ end
132
+
133
+ it { expect(subject).to eq(exp) }
134
+ end
135
+
136
+ context 'with options' do
137
+ let(:options) do
138
+ { source: 'source@source.com',
139
+ source_arn: 'source_arn',
140
+ from_arn: 'from_arn',
141
+ return_path_arn: 'return_path_arn',
142
+ tags: [{ name: 'Name', value: 'Value' }],
143
+ configuration_set_name: 'configuration_set_name',
144
+ other: 'other' }
145
+ end
146
+
147
+ let(:exp) do
148
+ {
149
+ source: 'source@source.com',
150
+ source_arn: 'source_arn',
151
+ from_arn: 'from_arn',
152
+ return_path_arn: 'return_path_arn',
153
+ tags: [
154
+ { name: 'Name', value: 'Value' }
155
+ ],
156
+ configuration_set_name: 'configuration_set_name',
157
+ destinations: %w[to1@def.com to2@xyz.com cc1@xyz.com cc2@def.com bcc1@abc.com bcc2@def.com],
158
+ raw_message: {
159
+ data: "Rml4ZWQgbWVzc2FnZSBib2R5\n"
160
+ }
161
+ }
162
+ end
163
+
164
+ it { expect(subject).to eq(exp) }
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ require 'rspec'
5
+
6
+ Bundler.setup
7
+
8
+ require 'mail-ses'
9
+
10
+ RSpec.configure do |config|
11
+ config.disable_monkey_patching!
12
+
13
+ config.default_formatter = 'doc' if config.files_to_run.one?
14
+
15
+ config.order = :random
16
+
17
+ Kernel.srand config.seed
18
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mail-ses
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Johnny Shields
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-ses
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mail
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 2.2.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.57.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.57.0
83
+ description: Ruby Mail delivery method handler for Amazon SES
84
+ email: info@tablecheck.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - LICENSE
91
+ - README.md
92
+ - VERSION
93
+ - lib/mail-ses.rb
94
+ - lib/mail/ses.rb
95
+ - spec/mail_ses_spec.rb
96
+ - spec/spec_helper.rb
97
+ homepage: https://github.com/tablecheck/mail-ses
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.7.3
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Ruby Mail delivery method handler for Amazon SES
121
+ test_files:
122
+ - spec/mail_ses_spec.rb
123
+ - spec/spec_helper.rb