ses_api-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 412077631ec736027d7d9b14e46fcfca42e2d193
4
+ data.tar.gz: 01081ff399c203622a4514cddf6089cca15c4c58
5
+ SHA512:
6
+ metadata.gz: fddfc7096b9b62780bded4527d1c57f291921a3e1ee63a5559a3e65c08bac04aa0beb00ea8acdf2ee4185001bf421c1abd15218f95194fac3086cb339d0d3409
7
+ data.tar.gz: a0c64ff863393372b9a736fd72682a6664ffec491806c56b0b30e9d95c807d207e6b9ae263a63c9f0d9f2836ef0ae88662ec58cc7550200c070194f36bcac043
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ses_api-rails.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Chris Ickes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # SES API for Rails
2
+ Basic SES API integration with Rails specifically for transaction based emails - contact emails, user emails, etc.
3
+
4
+ ## Install the SES API Rails gem
5
+ ```
6
+ # Gemfile
7
+ gem 'ses_api-rails', git: 'https://github.com/cickes/ses_api-rails.git'
8
+ ```
9
+ `bundle install`
10
+
11
+ ## High Level Integration Details
12
+ 1. Create an initializer file defining your AWS credentials as constants
13
+ ```
14
+ # config/initializers/ses_api-rails.rb
15
+ SesApi::Rails.configure do |config|
16
+ config.secret_access_key = ENV['SECRET_ACCESS_KEY']
17
+ config.access_key_id = Figaro.env.access_key_id
18
+ config.aws_region = "us-east-1"
19
+ config.ses_endpoint = "email.us-east-1.amazonaws.com"
20
+ end
21
+ ```
22
+ There are many ways to accomplish this. The above shows 3 different methods. Personally I recommend the [Figaro Gem](https://github.com/laserlemon/figaro).
23
+
24
+ 2. Subclass the SesApi::Rails::Mailer class in your CustomMailer or in your ApplicationMailer
25
+ ```
26
+ # app/mailers/custom_mailer.rb
27
+ class CustomMailer < SesApi::Rails::Mailer
28
+ ...
29
+ def contact
30
+ mail to: "you@example.com", subject: "Via Ses"
31
+ end
32
+ end
33
+ ```
34
+
35
+ 3. Instantiate the mailer where appropriate.
36
+ ```
37
+ # app/controllers/contacts_controller.rb
38
+ def create
39
+ @contact = Contact.new(contact_params)
40
+ if @contact.valid?
41
+ CustomMailer.contact(@contact).deliver_now!
42
+ redirect_to contact_success_path, notice: "Thank you for your contact request."
43
+ else
44
+ render :new
45
+ end
46
+ end
47
+ ```
48
+
49
+ ## A Step By Step Example
50
+ 1. (OPTIONAL) After the ses_api-rails gem is installed, add your Amazon AWS / SES credentials to environment variables.
51
+ NOTE: There are many ways to set environment variables. This example uses the Figaro gem.
52
+ ```
53
+ # Gemfile
54
+ gem 'figaro'
55
+ ```
56
+ ```
57
+ `bundle install`
58
+ `figaro install`
59
+ ```
60
+ ```
61
+ # config/application.yml
62
+ SECRET_ACCESS_KEY: "secret_access_key"
63
+ AWS_ACCESS_KEY_ID: "aws_access_key_id"
64
+ AWS_REGION: "us-east-1"
65
+ SES_ENDPOINT: "email.us-east-1.amazonaws.com"
66
+ ```
67
+
68
+ 2. Create an initializer that assigns your AWS credentials to constants. There are multiple ways to accomplish this including simply hardcoding a string value. The example below uses Figaro environment variables.
69
+ ```
70
+ # config/initializers/ses_api-rails.rb
71
+ SesApi::Rails.configure do |config|
72
+ config.secret_access_key = Figaro.env.secret_access_key
73
+ config.access_key_id = Figaro.env.access_key_id
74
+ config.aws_region = Figaro.env.aws_region
75
+ config.ses_endpoint = Figaro.env.ses_endpoint
76
+ end
77
+ ```
78
+
79
+ 3. Create a Mailer that subclasses the SesApi::Rails::Mailer
80
+ `rails g mailer ContactMailer`
81
+ If you are only sending email from the Amazon Ses Api, you can subclass the ApplicationMailer. Otherwise subclass the Mailer that will use the Ses delivery method.
82
+ * Using the Amazon Ses API as the only delivery method application-wide
83
+ ```
84
+ # app/mailers/application_mailer.rb
85
+ class ApplicationMailer < SesApi::Rails::Mailer
86
+ default from: "from@example.com"
87
+ layout 'mailer'
88
+ end
89
+ ```
90
+ ```
91
+ # app/mailers/contact_mailer.rb
92
+ class ContactMailer < ApplicationMailer
93
+ ...
94
+ def contact
95
+ mail to: "you@example.com", subject: "Via Ses"
96
+ end
97
+ end
98
+ ```
99
+ Create a mailer view(s)
100
+ ```
101
+ # app/views/contact_mailer/contact.html.erb
102
+ <h1>Hello new contact</h1>
103
+ <p>Include instance variables as you like using @contact.attribute</p>
104
+ ```
105
+ ```
106
+ # app/views/contact_mailer/contact.text.erb
107
+ This is a text version
108
+ Hello new contact
109
+ ```
110
+
111
+ 4. Instantiate the mailer where appropriate.
112
+ NOTE: This assumes that you have a form, model, etc. & is not covered in the installation guide of this gem.
113
+ ```
114
+ # app/controllers/contacts_controller.rb
115
+ ...
116
+ def create
117
+ @contact = Contact.new(contact_params)
118
+ if @contact.valid?
119
+ ContactMailer.contact(@contact).deliver_now!
120
+ redirect_to contact_success_path, notice: "Thank you for your contact request."
121
+ else
122
+ render :new
123
+ end
124
+ end
125
+ ...
126
+ ```
127
+
128
+ ## Troubleshooting
129
+ Is the library included? One way to do so is to autoload all library sub-directories:
130
+ `config.autoload_paths += Dir["#{config.root}/lib/**/"]`
131
+
132
+ Is the time correct? Recommended to use ntp or another time snychronization method
133
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ses_api/rails"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,115 @@
1
+ module SesApi
2
+ module Rails
3
+
4
+ # defines Mail::delivery_method
5
+ class Api < Mail::SMTP
6
+ # constants
7
+ ALGO = "AWS4-HMAC-SHA256"
8
+ SERVICE = "ses"
9
+ TERM_STR = "aws4_request"
10
+
11
+ class_attribute :conn, :mail
12
+
13
+ def initialize(values)
14
+ self.settings = {
15
+ }.merge!(values)
16
+
17
+ self.conn = Faraday.new(:url => "https://#{SesApi::Rails.configuration.ses_endpoint}") do |faraday|
18
+ faraday.request :url_encoded # form-encode POST params
19
+ faraday.response :logger # log requests to STDOUT
20
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
21
+ end
22
+ end
23
+
24
+ def settings
25
+ {}
26
+ end
27
+
28
+ def deliver!(mail)
29
+ self.mail = mail
30
+ dt = create_datetime
31
+ credential_scope = create_credential_scope(dt)
32
+
33
+ response = conn.post do |req|
34
+ req.body = create_payload
35
+ hashed_payload = Digest::SHA256.hexdigest(req.body) #.downcase
36
+ headers = { :'X-Amz-Date' => dt, Host: "#{SesApi::Rails.configuration.ses_endpoint}", :'X-Amz-Content-Sha256' => hashed_payload }
37
+ headers.each do |addtl_header, value|
38
+ req.headers[addtl_header.to_s.camelize] = value
39
+ end
40
+ req.headers['Authorization'] = create_auth_header(dt, credential_scope, hashed_payload, headers)
41
+ end
42
+
43
+ raise SesApi::Rails::SesError if response.status != 200
44
+ end
45
+
46
+ def create_datetime
47
+ # returns ISO8601 Basic format YYYYMMDD'T'HHMMSS'Z'
48
+ timestamp = Time.now.getutc
49
+ timestamp.strftime('%Y%m%dT%H%M%SZ')
50
+ end
51
+
52
+ def create_credential_scope(request_datetime)
53
+ "#{request_datetime[0,8]}/#{SesApi::Rails.configuration.aws_region}/#{SERVICE}/#{TERM_STR}"
54
+ end
55
+
56
+ def create_payload
57
+ to = mail.to.each_with_index.map { |email, i| "&Destination.ToAddresses.member.#{i+1}=#{CGI::escape(email)}"}.join
58
+ cc = mail.cc.each_with_index.map { |email, i| "&Destination.CCAddresses.member.#{i+1}=#{CGI::escape(email)}"}.join if mail.cc.present?
59
+ bcc = mail.bcc.each_with_index.map { |email, i| "&Destination.BCCAddresses.member.#{i+1}=#{CGI::escape(email)}"}.join if mail.bcc.present?
60
+ from = "&Source=#{CGI::escape(mail.from[0])}"
61
+ subject = "&Message.Subject.Data=#{CGI::escape(mail.subject)}"
62
+
63
+ if mail.body.raw_source.present?
64
+ body = "&Message.Body.Html.Data=#{CGI::escape(mail.body.raw_source)}&Message.Body.Html.Charset=UTF-8"
65
+ body << "&Message.Body.Text.Data=#{CGI::escape(mail.body.raw_source)}&Message.Body.Text.Charset=UTF-8"
66
+ else
67
+ body = "&Message.Body.Html.Data=#{CGI::escape(mail.html_part.body.raw_source)}&Message.Body.Html.Charset=UTF-8"
68
+ body << "&Message.Body.Text.Data=#{CGI::escape(mail.text_part.body.raw_source)}&Message.Body.Text.Charset=UTF-8"
69
+ end
70
+
71
+ payload = "AWSAccessKeyId=#{SesApi::Rails.configuration.access_key_id}&Action=SendEmail#{to}#{cc}#{bcc}#{body}#{subject}#{from}"
72
+ end
73
+
74
+ def create_canonical_request(headers, hashed_payload, signed_headers)
75
+ http_request_method = "POST"
76
+ canonical_uri = "/"
77
+ canonical_query_str = ""
78
+ canonical_headers = headers.sort.map { |k,v| "#{k.downcase}:#{v.strip}\n"}.join
79
+ canonical_request = [http_request_method, canonical_uri, canonical_query_str, canonical_headers, signed_headers, hashed_payload].join("\n")
80
+ end
81
+
82
+ def create_auth_header(request_datetime, credential_scope, hashed_payload, headers)
83
+ signing_key = create_signing_key(request_datetime)
84
+ signed_headers = headers.sort.map { |k,v| "#{k.downcase}" }.join(";")
85
+ string_to_sign = create_str_to_sign(request_datetime, credential_scope, headers, hashed_payload, signed_headers)
86
+ signing_signature = create_signature(signing_key, string_to_sign)
87
+ return "#{ALGO} Credential=#{SesApi::Rails.configuration.access_key_id}/#{credential_scope}, SignedHeaders=#{signed_headers}, Signature=#{signing_signature}"
88
+ end
89
+
90
+ def create_signing_key(request_datetime)
91
+ key_date = hmac("AWS4" + SesApi::Rails.configuration.secret_access_key, request_datetime[0,8])
92
+ key_region = hmac(key_date, SesApi::Rails.configuration.aws_region)
93
+ key_service = hmac(key_region, SERVICE)
94
+ key_credentials = hmac(key_service, TERM_STR)
95
+ end
96
+
97
+ def create_str_to_sign(request_datetime, credential_scope, headers, hashed_payload, signed_headers)
98
+ canonical_request = create_canonical_request(headers, hashed_payload, signed_headers)
99
+ return "#{ALGO}\n#{request_datetime}\n#{credential_scope}\n#{Digest::SHA256.hexdigest(canonical_request)}"
100
+ end
101
+
102
+ def create_signature(signing_key, string_to_sign)
103
+ hexhmac(signing_key, string_to_sign)
104
+ end
105
+
106
+ def hmac(key, value)
107
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
108
+ end
109
+
110
+ def hexhmac(key, value)
111
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,26 @@
1
+ module SesApi
2
+ module Rails
3
+ class << self
4
+ attr_accessor :configuration
5
+ end
6
+
7
+ def self.configure
8
+ self.configuration ||= Configuration.new
9
+ yield configuration
10
+ end
11
+
12
+ class Configuration
13
+ attr_accessor :aws_region
14
+ attr_accessor :ses_endpoint
15
+ attr_accessor :access_key_id
16
+ attr_accessor :secret_access_key
17
+
18
+ def initialize
19
+ @aws_region = "us-east-1"
20
+ @ses_endpoint = "email.us-east-1.amazonaws.com"
21
+ @access_key_id = nil
22
+ @secret_access_key = nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ require 'ses_api/rails/api'
2
+ module SesApi
3
+ module Rails
4
+ class Railtie < ::Rails::Railtie
5
+ config.after_initialize do
6
+ ActionMailer::Base.add_delivery_method :ses, SesApi::Rails::Api, {}
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module SesApi
2
+ module Rails
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ require 'mail'
2
+ require 'faraday'
3
+ require 'ses_api/rails/version'
4
+ require 'ses_api/rails/configuration'
5
+ require 'ses_api/rails/railtie'
6
+
7
+ module SesApi
8
+ module Rails
9
+
10
+ class Mailer < ActionMailer::Base
11
+ self.delivery_method = :ses
12
+ end
13
+
14
+ class SesError < StandardError; end
15
+
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ses_api/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ses_api-rails"
8
+ spec.version = SesApi::Rails::VERSION
9
+ spec.authors = ["Chris Ickes"]
10
+ spec.email = ["chris@ickessoftware.com"]
11
+
12
+ spec.summary = %q{Basic Rails / Amazon SES API Integration.}
13
+ spec.description = %q{Rails / Amazon SES API integration for basic transactional email sending.}
14
+ spec.homepage = "https://github.com/cickes/ses_api-rails.git"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_dependency 'mail'
25
+ spec.add_dependency 'faraday'
26
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ses_api-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Ickes
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mail
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Rails / Amazon SES API integration for basic transactional email sending.
70
+ email:
71
+ - chris@ickessoftware.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/console
83
+ - bin/setup
84
+ - lib/ses_api/rails.rb
85
+ - lib/ses_api/rails/api.rb
86
+ - lib/ses_api/rails/configuration.rb
87
+ - lib/ses_api/rails/railtie.rb
88
+ - lib/ses_api/rails/version.rb
89
+ - ses_api-rails.gemspec
90
+ homepage: https://github.com/cickes/ses_api-rails.git
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.4.5
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Basic Rails / Amazon SES API Integration.
114
+ test_files: []