mail-ses 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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