griddler-amazon_ses 2.0.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: 5477ab34b51905078ad94e973d364db93f67e33b
4
+ data.tar.gz: f5b18a45a5c65013916e88cd1ef460ff19b838e9
5
+ SHA512:
6
+ metadata.gz: 4703977f6155fc5a066eaf11c08f5378127089c86d38267bcf022f07874df751792955c9adcc7b6b9bb9d92519ddbaf43ca57963f6c8808086ac5ef7e858d910
7
+ data.tar.gz: c3fbab952d3090166379e0c28ed97976ce0fec0c988a6f8534ee5445d8ac6591b01b2bf8aa84482d10d7c7f7f66f2ec1ecf9fa398b5aa230e650885e7b04cda4
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .idea/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Coupa Software Inc
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,38 @@
1
+ # Griddler::AmazonSES
2
+
3
+ This is a [Griddler](https://github.com/thoughtbot/griddler) adapter that allows you to parse email replies when used with Amazon SES.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add these lines to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'griddler'
12
+ gem 'griddler-amazon_ses'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+
20
+ ## Usage
21
+
22
+ 1. Setup Amazon SES to receive emails -- see http://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-setting-up.html
23
+
24
+ 2. From AWS SES -> Rule Sets, choose to "Create Receipt Rule" and choose to use SNS. Add an action with a new SNS topic that *ends in `_griddler`*, with a Base64 encoding.
25
+
26
+ 3. Go to AWS SNS (Simple Notification Service) -> Topics. Click on your `_griddler` topic. Add a subscription that points to your configured griddler endpoint (the default route is `/email_processor`). You're server must be already running to confirm the subscription request!
27
+
28
+ 4. In your griddler handler, be sure to handle/ignore empty reply emails (ie. check for `email.headers.empty?`) to account for the fact that some hooks are just SNS's subscription handling.
29
+
30
+ ## Contributing
31
+
32
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ccallebs/griddler-amazon_ses.
33
+
34
+
35
+ ## License
36
+
37
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
38
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/circle.yml ADDED
@@ -0,0 +1,4 @@
1
+ machine:
2
+ ruby:
3
+ version:
4
+ 2.2.4
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'griddler/amazon_ses/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "griddler-amazon_ses"
8
+ spec.version = Griddler::AmazonSES::VERSION
9
+ spec.authors = ["Chuck Callebs", "Kent Mewhort @ Coupa"]
10
+ spec.email = ["chuck@callebs.io", "kent.mewhort@coupa.com"]
11
+
12
+ spec.summary = %q{Griddler adapter for AWS SES (handle incoming email replies through SES)}
13
+ spec.homepage = "https://github.com/ccallebs/griddler-amazon_ses"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'griddler'
22
+ spec.add_runtime_dependency 'mail'
23
+ spec.add_runtime_dependency 'httparty'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.11"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ end
@@ -0,0 +1,156 @@
1
+ # Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+
15
+ # Forked from https://github.com/elibri/sns_endpoint
16
+
17
+ require 'base64'
18
+ require 'json'
19
+ require 'openssl'
20
+ require 'httparty'
21
+
22
+ module AWS
23
+ class MessageWasNotAuthenticError < StandardError
24
+ end
25
+
26
+ class SnsMessage
27
+ attr_accessor :origin, :raw
28
+
29
+ def initialize sns
30
+ if sns.is_a? String
31
+ @raw = parse_from sns
32
+ else
33
+ @raw = sns
34
+ end
35
+ @origin = :sns
36
+ end
37
+
38
+ def [] key
39
+ @raw[key]
40
+ end
41
+
42
+ def authentic?
43
+ begin
44
+ decoded_from_base64 = decode signature
45
+ public_key = get_public_key_from signing_cert_url
46
+ public_key.verify OpenSSL::Digest::SHA1.new, decoded_from_base64, canonical_string
47
+ rescue MessageWasNotAuthenticError
48
+ false
49
+ end
50
+ end
51
+
52
+ def type
53
+ case when @raw['Type'] =~ /SubscriptionConfirmation/i
54
+ then :SubscriptionConfirmation
55
+ when @raw['Type'] =~ /Notification/i
56
+ then :Notification
57
+ when @raw['Type'] =~ /UnsubscribeConfirmation/i
58
+ then :UnsubscribeConfirmation
59
+ else
60
+ :unknown
61
+ end
62
+ end
63
+
64
+ def message_id
65
+ @raw['MessageId']
66
+ end
67
+
68
+ def topic_arn
69
+ @raw['TopicArn']
70
+ end
71
+
72
+ def subject
73
+ @raw['Subject']
74
+ end
75
+
76
+ def message
77
+ @raw['Message']
78
+ end
79
+
80
+ def timestamp
81
+ @raw['Timestamp']
82
+ end
83
+
84
+ def signature
85
+ @raw['Signature']
86
+ end
87
+
88
+ def signature_version
89
+ @raw['SignatureVersion']
90
+ end
91
+
92
+ def signing_cert_url
93
+ @raw['SigningCertURL']
94
+ end
95
+
96
+ def unsubscribe_url
97
+ @raw['UnsubscribeURL']
98
+ end
99
+
100
+ def subscribe_url
101
+ @raw['SubscribeURL']
102
+ end
103
+
104
+ def token
105
+ @raw['Token']
106
+ end
107
+
108
+ def parse_from json
109
+ JSON.parse json
110
+ end
111
+
112
+ protected
113
+ def decode raw
114
+ Base64.decode64 raw
115
+ end
116
+
117
+ def get_public_key_from(x509_pem_url)
118
+ cert_pem = download x509_pem_url
119
+ x509 = OpenSSL::X509::Certificate.new(cert_pem)
120
+ OpenSSL::PKey::RSA.new(x509.public_key)
121
+ end
122
+
123
+ def canonical_string
124
+ if type == :SubscriptionConfirmation
125
+ text = "Message\n#{message}\n"
126
+ text += "MessageId\n#{message_id}\n"
127
+ text += "SubscribeURL\n#{subscribe_url}\n"
128
+ text += "Timestamp\n#{timestamp}\n"
129
+ text += "Token\n#{token}\n"
130
+ text += "TopicArn\n#{topic_arn}\n"
131
+ text += "Type\n#{type}\n"
132
+ else
133
+ text = "Message\n#{message}\n"
134
+ text += "MessageId\n#{message_id}\n"
135
+ text += "Subject\n#{subject}\n" unless subject.nil? or subject.empty?
136
+ text += "Timestamp\n#{timestamp}\n"
137
+ text += "TopicArn\n#{topic_arn}\n"
138
+ text += "Type\n#{type}\n"
139
+ end
140
+ text
141
+ end
142
+
143
+ def download url
144
+ raise MessageWasNotAuthenticError, "cert is not hosted at AWS URL (https): #{url}" unless url =~ /^https.*amazonaws\.com\/.*$/i
145
+ tries = 0
146
+ begin
147
+ response = HTTParty.get url
148
+ response.body
149
+ rescue => msg
150
+ tries += 1
151
+ retry if tries <= 3
152
+ raise StandardError, "SNS signing cert could not be retrieved after #{tries} tries.\n#{msg}"
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,13 @@
1
+ require 'aws/sns_message'
2
+ require 'griddler'
3
+ require 'griddler/amazon_ses/version'
4
+ require 'griddler/amazon_ses/adapter'
5
+ require 'griddler/amazon_ses/middleware'
6
+ require 'griddler/amazon_ses/railtie'
7
+
8
+ module Griddler
9
+ module AmazonSES
10
+ end
11
+ end
12
+
13
+ Griddler.adapter_registry.register(:amazon_ses, Griddler::AmazonSES::Adapter)
@@ -0,0 +1,134 @@
1
+ require 'mail'
2
+ require 'net/http'
3
+
4
+ module Griddler
5
+ module AmazonSES
6
+ class Adapter
7
+ attr_reader :sns_json
8
+
9
+ def initialize(params)
10
+ @sns_json = params
11
+ end
12
+
13
+ def self.normalize_params(params)
14
+ adapter = new(params)
15
+ adapter.normalize_params
16
+ end
17
+
18
+ def normalize_params
19
+ sns_msg = AWS::SnsMessage.new sns_json
20
+ raise "Invalid SNS message" unless sns_msg.authentic? && sns_msg.topic_arn.end_with?('griddler')
21
+
22
+ case sns_msg.type
23
+ when :SubscriptionConfirmation
24
+ confirm_sns_subscription_request
25
+ # this is not an actual email reply (and griddler has no way to bail at this point), so return empty parameters
26
+ {}
27
+ when :Notification
28
+ ensure_valid_notification_type!
29
+ sns_json.merge(
30
+ to: recipients,
31
+ from: sender,
32
+ cc: cc,
33
+ bcc: bcc,
34
+ subject: subject,
35
+ text: text_part,
36
+ html: html_part,
37
+ headers: raw_headers,
38
+ attachments: attachment_files
39
+ )
40
+ else
41
+ raise "Invalid SNS message type"
42
+ end
43
+ end
44
+
45
+ private
46
+ def email_json
47
+ @email_json ||= JSON.parse(sns_json['Message'])
48
+ end
49
+
50
+ def notification_type
51
+ email_json['notificationType']
52
+ end
53
+
54
+ def recipients
55
+ email_json['mail']['commonHeaders']['to']
56
+ end
57
+
58
+ def sender
59
+ email_json['mail']['commonHeaders']['from'].first
60
+ end
61
+
62
+ def cc
63
+ email_json['mail']['commonHeaders']['cc'] || []
64
+ end
65
+
66
+ def bcc
67
+ email_json['mail']['commonHeaders']['bcc'] || []
68
+ end
69
+
70
+ def subject
71
+ email_json['mail']['commonHeaders']['subject']
72
+ end
73
+
74
+ def header_array
75
+ email_json['mail']['headers']
76
+ end
77
+
78
+ def message
79
+ @message ||= Mail.read_from_string(Base64.decode64(email_json['content']))
80
+ end
81
+
82
+ def multipart?
83
+ message.parts.count > 0
84
+ end
85
+
86
+ def text_part
87
+ multipart? ? message.text_part.body.to_s : message.body.to_s
88
+ end
89
+
90
+ def html_part
91
+ multipart? ? message.html_part.body.to_s : nil
92
+ end
93
+
94
+ def raw_headers
95
+ # SNS gives us an array of hashes with name value, which we need to convert back to raw headers;
96
+ # based on griddler-sparkpost (https://github.com/PrestoDoctor/griddler-sparkpost, MIT license)
97
+ header_array.inject([]) { |raw_headers, sns_hash|
98
+ raw_headers.push("#{sns_hash['name']}: #{sns_hash['value']}")
99
+ }.join("\r\n")
100
+ end
101
+
102
+ def attachment_files
103
+ # also based on griddler-sparkpost (https://github.com/PrestoDoctor/griddler-sparkpost, MIT license);
104
+ # AWS doesn't presently support sending the attachments from the message through SNS, but ready if they do!
105
+ message.attachments.map do |attachment|
106
+ ActionDispatch::Http::UploadedFile.new({
107
+ type: attachment.mime_type,
108
+ filename: attachment.filename,
109
+ tempfile: tempfile_for_attachment(attachment)
110
+ })
111
+ end
112
+ end
113
+
114
+ def tempfile_for_attachment(attachment)
115
+ filename = attachment.filename.gsub(/\/|\\/, '_')
116
+ tempfile = Tempfile.new(filename, Dir::tmpdir, encoding: 'ascii-8bit')
117
+ content = attachment.body.decoded
118
+ tempfile.write(content)
119
+ tempfile.rewind
120
+ tempfile
121
+ end
122
+
123
+ def ensure_valid_notification_type!
124
+ raise "Invalid SNS notification type (\"#{notification_type}\", expecting Received" unless notification_type == 'Received'
125
+ end
126
+
127
+ def confirm_sns_subscription_request
128
+ confirmation_endpoint = URI.parse(sns_json['SubscribeURL'])
129
+ Net::HTTP.get confirmation_endpoint
130
+ end
131
+ end
132
+ end
133
+ end
134
+
@@ -0,0 +1,34 @@
1
+ module Griddler
2
+ module AmazonSES
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ # a bug on the AWS side doesn't set the content type to application/json type properly,
10
+ # so we have to intercept and do this in order for Griddler's controller to correctly
11
+ # parse the parameters (see https://forums.aws.amazon.com/thread.jspa?messageID=418160)
12
+ if is_griddler_request?(env) && is_aws_sns_request?(env)
13
+ env['CONTENT_TYPE'] = 'application/json; charset=UTF-8'
14
+ end
15
+
16
+ @app.call(env)
17
+ end
18
+
19
+ private
20
+ def griddler_path
21
+ @griddler_path ||= Rails.application.routes.url_helpers.url_for(controller: 'griddler/emails', action: 'create', only_path: true)
22
+ end
23
+
24
+ def is_griddler_request?(request)
25
+ # Fix for servers that do not include 'request_path' in headers
26
+ request['REQUEST_PATH'] == griddler_path || request['REQUEST_URI'] == griddler_path
27
+ end
28
+
29
+ def is_aws_sns_request?(request)
30
+ request['HTTP_X_AMZ_SNS_MESSAGE_TYPE'].present?
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ require 'rails/version'
2
+
3
+ module Griddler
4
+ module AmazonSES
5
+ class Railtie < Rails::Railtie
6
+ if Rails::VERSION::MAJOR < 5
7
+ middleware = ActionDispatch::ParamsParser
8
+ else
9
+ middleware = Rack::Head
10
+ end
11
+
12
+ initializer "griddler_ses.configure_rails_initialization" do |app|
13
+ Rails.application.middleware.insert_before middleware, Griddler::Ses::Middleware
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module Griddler
2
+ module AmazonSES
3
+ VERSION = "2.0.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: griddler-amazon_ses
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Chuck Callebs
8
+ - Kent Mewhort @ Coupa
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2018-08-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: griddler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: mail
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: httparty
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundler
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.11'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.11'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '10.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '3.0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.0'
98
+ description:
99
+ email:
100
+ - chuck@callebs.io
101
+ - kent.mewhort@coupa.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - circle.yml
113
+ - griddler-amazon_ses.gemspec
114
+ - lib/aws/sns_message.rb
115
+ - lib/griddler/amazon_ses.rb
116
+ - lib/griddler/amazon_ses/adapter.rb
117
+ - lib/griddler/amazon_ses/middleware.rb
118
+ - lib/griddler/amazon_ses/railtie.rb
119
+ - lib/griddler/amazon_ses/version.rb
120
+ homepage: https://github.com/ccallebs/griddler-amazon_ses
121
+ licenses:
122
+ - MIT
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubyforge_project:
140
+ rubygems_version: 2.6.12
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: Griddler adapter for AWS SES (handle incoming email replies through SES)
144
+ test_files: []