hermes-rails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +32 -0
- data/lib/hermes.rb +16 -0
- data/lib/hermes/deliverer.rb +121 -0
- data/lib/hermes/exceptions.rb +13 -0
- data/lib/hermes/mail_ext.rb +5 -0
- data/lib/hermes/version.rb +3 -0
- data/lib/providers/mailgun/mail_ext.rb +9 -0
- data/lib/providers/mailgun/mailgun_attachment.rb +16 -0
- data/lib/providers/mailgun/mailgun_provider.rb +42 -0
- data/lib/providers/outbound_webhook/outbound_webhook_provider.rb +30 -0
- data/lib/providers/plivo/mail_ext.rb +5 -0
- data/lib/providers/plivo/plivo_provider.rb +33 -0
- data/lib/providers/provider.rb +58 -0
- data/lib/providers/sendgrid/sendgrid_provider.rb +46 -0
- data/lib/providers/twilio/mail_ext.rb +5 -0
- data/lib/providers/twilio/twilio_provider.rb +39 -0
- data/lib/providers/twitter/mail_ext.rb +6 -0
- data/lib/providers/twitter/twitter_provider.rb +21 -0
- data/lib/support/b64y.rb +23 -0
- data/lib/support/email_attachment.rb +16 -0
- data/lib/support/extractors.rb +68 -0
- data/lib/support/phone.rb +235 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f9071b7e4a8721889e58bd6c4c197b19435190a6
|
4
|
+
data.tar.gz: e812e7eb39709a33c11cf6b78f20aced40b5fc58
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 34d64a8a92cdad335b5b4bdb9660f178c4d98163065209102befe8a86299116b14dd59a83254b202b4d7d4ebbf2cb9c7191316cbfd03a04dffdf34775ecdc9e0
|
7
|
+
data.tar.gz: e3fa78be6f7ec3b0c703d06e7e871ca2048c4c584aee63403c5c20f201310e5bae1ecf2a8e07b9365c28d813c3353a3f95c1e2448297bbb8ece0ef97ddc7e0d8
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Dogwood Labs, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'hermes'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
data/lib/hermes.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'action_mailer'
|
2
|
+
require 'json'
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
# all of the Hermes support files
|
6
|
+
Dir[File.dirname(__FILE__) + '/support/*.rb'].each {|file| require file }
|
7
|
+
Dir[File.dirname(__FILE__) + '/hermes/*.rb'].each {|file| require file }
|
8
|
+
|
9
|
+
# all of the generic (abstract) provider support files
|
10
|
+
Dir[File.dirname(__FILE__) + '/providers/*.rb'].each {|file| require file }
|
11
|
+
|
12
|
+
# all of the actual provider support files
|
13
|
+
Dir[File.dirname(__FILE__) + '/providers/**/*.rb'].each {|file| require file }
|
14
|
+
|
15
|
+
module Hermes
|
16
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Hermes
|
4
|
+
class Deliverer
|
5
|
+
include Extractors
|
6
|
+
|
7
|
+
attr_reader :providers, :config
|
8
|
+
|
9
|
+
def initialize(settings)
|
10
|
+
@providers = {}
|
11
|
+
|
12
|
+
@config = settings[:config]
|
13
|
+
|
14
|
+
# this will most likely come back as [:email, :sms, :tweet, :webhook]
|
15
|
+
provider_types = @config.keys.reject{|key| key == :config}
|
16
|
+
|
17
|
+
# loop through each and construct a workable array
|
18
|
+
provider_types.each do |provider_type|
|
19
|
+
@providers[provider_type] ||= []
|
20
|
+
providers = settings[provider_type]
|
21
|
+
next unless providers.try(:any?)
|
22
|
+
|
23
|
+
# go through all of the providers and initialize each
|
24
|
+
providers.each do |provider_name, options|
|
25
|
+
# check to see that the provider class exists
|
26
|
+
provider_proper_name = "#{provider_name}_provider".camelize.to_sym
|
27
|
+
raise(ProviderNotFoundError, "Could not find provider class Hermes::#{provider_proper_name}") unless Hermes.constants.include?(provider_proper_name)
|
28
|
+
|
29
|
+
# initialize the provider with the given weight, defaults, and credentials
|
30
|
+
provider = Hermes.const_get(provider_proper_name).new(self, options)
|
31
|
+
@providers[provider_type] << provider
|
32
|
+
end
|
33
|
+
|
34
|
+
# make sure the provider type has an aggregate weight of more than 1
|
35
|
+
aweight = aggregate_weight_for_type(provider_type)
|
36
|
+
unless aweight > 0
|
37
|
+
raise(InvalidWeightError, "Provider type:#{provider_type} has aggregate weight:#{aweight}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_mode?
|
43
|
+
!!@config[:test]
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_success(provider_name)
|
47
|
+
@config[:stats]
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_failure(provider_name, exception)
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def should_deliver?
|
55
|
+
!self.test_mode? && ActionMailer::Base.perform_deliveries
|
56
|
+
end
|
57
|
+
|
58
|
+
def aggregate_weight_for_type(type)
|
59
|
+
providers = @providers[type]
|
60
|
+
return 0 if providers.empty?
|
61
|
+
|
62
|
+
providers.map(&:weight).inject(0, :+)
|
63
|
+
end
|
64
|
+
|
65
|
+
def weighted_provider_for_type(type)
|
66
|
+
providers = @providers[type]
|
67
|
+
return nil if providers.empty?
|
68
|
+
|
69
|
+
# get the aggregate weight, and do a rand based on it
|
70
|
+
random_index = rand(aggregate_weight_for_type(type))
|
71
|
+
# puts "random_index:#{random_index}"
|
72
|
+
|
73
|
+
# loop through each, exclusive range, and find the one that it falls on
|
74
|
+
running_total = 0
|
75
|
+
providers.each do |provider|
|
76
|
+
# puts "running_total:#{running_total}"
|
77
|
+
left_index = running_total
|
78
|
+
right_index = running_total + provider.weight
|
79
|
+
# puts "left_index:#{left_index} right_index:#{right_index}"
|
80
|
+
|
81
|
+
if (left_index...right_index).include?(random_index)
|
82
|
+
return provider
|
83
|
+
else
|
84
|
+
running_total += provider.weight
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def delivery_type_for(rails_message)
|
90
|
+
to = extract_to(rails_message, format: :address)
|
91
|
+
|
92
|
+
if to.is_a?(Hash) && to[:twitter_username]
|
93
|
+
:tweet
|
94
|
+
elsif rails_message.to.first.include?('@')
|
95
|
+
:email
|
96
|
+
elsif to.is_a?(Phone)
|
97
|
+
:sms
|
98
|
+
elsif to.is_a?(URI)
|
99
|
+
:webhook
|
100
|
+
else
|
101
|
+
raise UnknownDeliveryTypeError, "Cannot determine provider type from provided to:#{to}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def deliver!(rails_message)
|
106
|
+
# figure out what we're delivering
|
107
|
+
delivery_type = delivery_type_for(rails_message)
|
108
|
+
|
109
|
+
# set this on the message so it's available throughout
|
110
|
+
rails_message.hermes_type = delivery_type
|
111
|
+
|
112
|
+
# find a provider, weight matters here
|
113
|
+
provider = weighted_provider_for_type(delivery_type)
|
114
|
+
|
115
|
+
# and then send the message
|
116
|
+
provider.send_message(rails_message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
ActionMailer::Base.add_delivery_method :hermes, Hermes::Deliverer
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Hermes
|
2
|
+
# if provider is listed in settings, but no provider class is available
|
3
|
+
class ProviderNotFoundError < StandardError; end
|
4
|
+
|
5
|
+
# thrown if provider weight goes below 1
|
6
|
+
class InvalidWeightError < StandardError; end
|
7
|
+
|
8
|
+
# thrown if configuration does not provide all required credentials
|
9
|
+
class InsufficientCredentialsError < StandardError; end
|
10
|
+
|
11
|
+
# thrown if deliverer cannot figure out what type of provider to use for provided rails message
|
12
|
+
class UnknownDeliveryTypeError < StandardError; end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Hermes
|
2
|
+
class MailgunAttachment < StringIO
|
3
|
+
attr_reader :original_filename, :content_type, :path
|
4
|
+
|
5
|
+
def initialize (attachment, *rest)
|
6
|
+
@path = ''
|
7
|
+
if rest.detect {|opt| opt[:inline] }
|
8
|
+
basename = @original_filename = attachment.cid
|
9
|
+
else
|
10
|
+
basename = @original_filename = attachment.filename
|
11
|
+
end
|
12
|
+
@content_type = attachment.content_type.split(';')[0]
|
13
|
+
super attachment.body.decoded
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Hermes
|
2
|
+
class MailgunProvider < Provider
|
3
|
+
required_credentials :api_key
|
4
|
+
|
5
|
+
def send_message(rails_message)
|
6
|
+
domain = rails_message.mailgun_domain || self.defaults[:domain]
|
7
|
+
message = self.mailgun_message(rails_message)
|
8
|
+
|
9
|
+
if self.deliverer.should_deliver?
|
10
|
+
self.client.send_message(domain, message)
|
11
|
+
end
|
12
|
+
|
13
|
+
self.message_success(rails_message)
|
14
|
+
end
|
15
|
+
|
16
|
+
def mailgun_message(rails_message)
|
17
|
+
message = Mailgun::MessageBuilder.new
|
18
|
+
|
19
|
+
# basics
|
20
|
+
message.set_from_address(extract_from(rails_message))
|
21
|
+
message.add_recipient(:to, extract_to(rails_message))
|
22
|
+
message.set_subject(rails_message[:subject])
|
23
|
+
message.set_html_body(extract_html(rails_message))
|
24
|
+
message.set_text_body(extract_text(rails_message))
|
25
|
+
message.set_message_id(rails_message.message_id)
|
26
|
+
|
27
|
+
# optionals
|
28
|
+
message.set_from_address('h:reply-to', rails_message[:reply_to].formatted.first) if rails_message[:reply_to]
|
29
|
+
|
30
|
+
# and any attachments
|
31
|
+
rails_message.attachments.try(:each) do |attachment|
|
32
|
+
message.add_attachment(Hermes::EmailAttachment.new(attachment))
|
33
|
+
end
|
34
|
+
|
35
|
+
return message
|
36
|
+
end
|
37
|
+
|
38
|
+
def client
|
39
|
+
Mailgun::Client.new(self.credentials[:api_key])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Hermes
|
2
|
+
class OutboundWebhookProvider < Provider
|
3
|
+
|
4
|
+
def send_message(rails_message)
|
5
|
+
payload = payload(rails_message)
|
6
|
+
outbound_webhook = OutboundWebhook.create!(payload)
|
7
|
+
rails_message[:message_id] = outbound_webhook.id
|
8
|
+
|
9
|
+
byebug
|
10
|
+
|
11
|
+
if self.deliverer.should_deliver?
|
12
|
+
outbound_webhook.deliver_async
|
13
|
+
end
|
14
|
+
|
15
|
+
self.message_success(rails_message)
|
16
|
+
rescue Exception => e
|
17
|
+
self.message_failure(rails_message, e)
|
18
|
+
end
|
19
|
+
|
20
|
+
def payload(rails_message)
|
21
|
+
{
|
22
|
+
:endpoint => extract_to(rails_message),
|
23
|
+
:headers => {
|
24
|
+
'Content-Type' => 'application/json'
|
25
|
+
},
|
26
|
+
:body => extract_text(rails_message)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Hermes
|
2
|
+
class PlivoProvider < Provider
|
3
|
+
required_credentials :auth_id, :auth_token
|
4
|
+
|
5
|
+
def send_message(rails_message)
|
6
|
+
payload = payload(rails_message)
|
7
|
+
|
8
|
+
if self.deliverer.should_deliver?
|
9
|
+
result = self.client.send_message(payload)
|
10
|
+
rails_message[:message_id] = result["api_id"]
|
11
|
+
else
|
12
|
+
# rails message still needs a fake sid as if it succeeded
|
13
|
+
rails_message[:message_id] = SecureRandom.uuid
|
14
|
+
end
|
15
|
+
|
16
|
+
self.message_success(rails_message)
|
17
|
+
end
|
18
|
+
|
19
|
+
def payload(rails_message)
|
20
|
+
{
|
21
|
+
src: extract_from(rails_message).full_number,
|
22
|
+
dst: extract_to(rails_message),
|
23
|
+
text: extract_text(rails_message),
|
24
|
+
type: :sms,
|
25
|
+
url: rails_message.plivo_url || self.defaults[:url]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def client
|
30
|
+
Plivo::RestAPI.new(self.credentials[:auth_id], self.credentials[:auth_token])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hermes
|
2
|
+
class Provider
|
3
|
+
include Extractors
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :_required_credentials
|
7
|
+
|
8
|
+
def required_credentials(*args)
|
9
|
+
self._required_credentials = args.to_a
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :deliverer, :defaults, :credentials, :weight
|
14
|
+
|
15
|
+
def initialize(deliverer, options = {})
|
16
|
+
options.symbolize_keys!
|
17
|
+
|
18
|
+
@deliverer = deliverer
|
19
|
+
@defaults = options[:defaults]
|
20
|
+
@credentials = (options[:credentials] || {}).symbolize_keys
|
21
|
+
@weight = options[:weight].to_i
|
22
|
+
|
23
|
+
if self.class._required_credentials.try(:any?)
|
24
|
+
# provider defines required credentials, let's make sure to check we have everything we need
|
25
|
+
if !((@credentials.keys & self.class._required_credentials) == self.class._required_credentials)
|
26
|
+
# we're missing something, raise here for hard failure
|
27
|
+
raise(InsufficientCredentialsError, "Credentials passed:#{@credentials.keys} do not satisfy all required:#{self.class._required_credentials}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
unless @weight >= 0
|
32
|
+
# provider weights need to be 0 (disabled), or greater than 0 to show as active
|
33
|
+
raise(InvalidWeightError, "Provider name:#{provider_name} has invalid weight:#{@weight}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def provider_name
|
38
|
+
self.class.name.demodulize.underscore.gsub('_provider', '')
|
39
|
+
end
|
40
|
+
|
41
|
+
def send_message(rails_message)
|
42
|
+
raise "this is an abstract method and must be defined in the subclass"
|
43
|
+
end
|
44
|
+
|
45
|
+
def message_success(rails_message)
|
46
|
+
ActionMailer::Base.deliveries << rails_message if self.deliverer.test_mode?
|
47
|
+
self.deliverer.handle_success(self.class.name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def message_failure(rails_message, exception)
|
51
|
+
Utils.log_and_puts "--- MESSAGE SEND FAILURE ---"
|
52
|
+
Utils.log_and_puts exception.message
|
53
|
+
Utils.log_and_puts exception.backtrace.join("\n")
|
54
|
+
Utils.log_and_puts "--- MESSAGE SEND FAILURE ---"
|
55
|
+
self.deliverer.handle_failure(self.class)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Hermes
|
2
|
+
class SendgridProvider < Provider
|
3
|
+
required_credentials :api_user, :api_key
|
4
|
+
|
5
|
+
def send_message(rails_message)
|
6
|
+
payload = payload(rails_message)
|
7
|
+
|
8
|
+
if self.deliverer.should_deliver?
|
9
|
+
client.send(payload)
|
10
|
+
end
|
11
|
+
|
12
|
+
self.message_success(message_success)
|
13
|
+
end
|
14
|
+
|
15
|
+
def payload(rails_message)
|
16
|
+
# requireds
|
17
|
+
message = SendGrid::Mail.new({
|
18
|
+
from: extract_from(rails_message, :address),
|
19
|
+
from_name: extract_from(rails_message, :name),
|
20
|
+
to: extract_to(rails_message, :address),
|
21
|
+
to_name: extract_to(rails_message, :name),
|
22
|
+
subject: rails_message.subject,
|
23
|
+
html: extract_html(rails_message),
|
24
|
+
text: extract_text(rails_message),
|
25
|
+
})
|
26
|
+
|
27
|
+
# optionals
|
28
|
+
message.reply_to = rails_message[:reply_to].formatted.first if rails_message[:reply_to]
|
29
|
+
message.message_id = rails_message[:message_id].value if rails_message.message_id
|
30
|
+
|
31
|
+
# and any attachments
|
32
|
+
rails_message.attachments.try(:each) do |attachment|
|
33
|
+
message.add_attachment_file(Hermes::EmailAttachment.new(attachment))
|
34
|
+
end
|
35
|
+
|
36
|
+
return message
|
37
|
+
end
|
38
|
+
|
39
|
+
def client
|
40
|
+
SendGrid::Client.new({
|
41
|
+
api_user: self.credentials[:api_user],
|
42
|
+
api_key: self.credentials[:api_key]
|
43
|
+
})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Hermes
|
2
|
+
class TwilioProvider < Provider
|
3
|
+
required_credentials :account_sid, :auth_token
|
4
|
+
|
5
|
+
def send_message(rails_message)
|
6
|
+
payload = payload(rails_message)
|
7
|
+
|
8
|
+
if self.deliverer.should_deliver?
|
9
|
+
result = self.client.account.messages.create(payload)
|
10
|
+
|
11
|
+
# set the sid onto the rails message as the message id, used for tracking
|
12
|
+
rails_message[:message_id] = result.sid
|
13
|
+
else
|
14
|
+
# rails message still needs a fake sid as if it succeeded
|
15
|
+
rails_message[:message_id] = SecureRandom.uuid
|
16
|
+
end
|
17
|
+
|
18
|
+
self.message_success(rails_message)
|
19
|
+
rescue Exception => e
|
20
|
+
self.message_failure(rails_message, e)
|
21
|
+
end
|
22
|
+
|
23
|
+
def payload(rails_message)
|
24
|
+
payload = {
|
25
|
+
to: extract_to(rails_message).full_number,
|
26
|
+
from: extract_from(rails_message),
|
27
|
+
body: extract_text(rails_message),
|
28
|
+
}
|
29
|
+
|
30
|
+
payload[:status_callback] = rails_message.twilio_status_callback if rails_message.twilio_status_callback
|
31
|
+
|
32
|
+
return payload
|
33
|
+
end
|
34
|
+
|
35
|
+
def client
|
36
|
+
Twilio::REST::Client.new(self.credentials[:account_sid], self.credentials[:auth_token])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Hermes
|
2
|
+
class TwitterProvider < Provider
|
3
|
+
required_credentials :consumer_key, :consumer_secret
|
4
|
+
|
5
|
+
def send_message(rails_message)
|
6
|
+
self.client(rails_message).update(extract_text(rails_message))
|
7
|
+
end
|
8
|
+
|
9
|
+
def client(rails_message)
|
10
|
+
# this will already be an instance of Twitter::client
|
11
|
+
client = extract_to(rails_message)
|
12
|
+
|
13
|
+
# just need to set the consumer key and secret and
|
14
|
+
# then we'll be ready for liftoff
|
15
|
+
client.consumer_key = self.credentials[:consumer_key]
|
16
|
+
client.consumer_secret = self.credentials[:consumer_secret]
|
17
|
+
|
18
|
+
return client
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/support/b64y.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Hermes
|
2
|
+
class B64Y
|
3
|
+
def self.encode(payload)
|
4
|
+
Base64.strict_encode64(YAML.dump(payload))
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.decode(payload)
|
8
|
+
YAML.load(Base64.strict_decode64(payload))
|
9
|
+
rescue Exception => e
|
10
|
+
Utils.log_and_puts "--- DECODE FAILURE ---"
|
11
|
+
Utils.log_and_puts payload
|
12
|
+
Utils.log_and_puts "--- DECODE FAILURE ---"
|
13
|
+
raise e
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.decodable?(payload)
|
17
|
+
# check to make sure when we decode that it's going to look like a YAML object
|
18
|
+
Base64.strict_decode64(payload)[0..2] == '---'
|
19
|
+
rescue
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Hermes
|
2
|
+
class EmailAttachment < StringIO
|
3
|
+
attr_reader :original_filename, :content_type, :path
|
4
|
+
|
5
|
+
def initialize (attachment, *rest)
|
6
|
+
@path = ''
|
7
|
+
if rest.detect {|opt| opt[:inline] }
|
8
|
+
basename = @original_filename = attachment.cid
|
9
|
+
else
|
10
|
+
basename = @original_filename = attachment.filename
|
11
|
+
end
|
12
|
+
@content_type = attachment.content_type.split(';')[0]
|
13
|
+
super attachment.body.decoded
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Hermes
|
2
|
+
module Extractors
|
3
|
+
# @see http://stackoverflow.com/questions/4868205/rails-mail-getting-the-body-as-plain-text
|
4
|
+
def extract_html(rails_message)
|
5
|
+
if rails_message.html_part
|
6
|
+
rails_message.html_part.body.decoded
|
7
|
+
else
|
8
|
+
rails_message.content_type =~ /text\/html/ ? rails_message.body.decoded : nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def extract_text(rails_message)
|
13
|
+
if rails_message.multipart?
|
14
|
+
rails_message.text_part ? rails_message.text_part.body.decoded.strip : nil
|
15
|
+
else
|
16
|
+
rails_message.content_type =~ /text\/plain/ ? rails_message.body.decoded.strip : nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# format can be full|name|address
|
21
|
+
def extract_from(rails_message, format: :full)
|
22
|
+
from = complex_extract(rails_message.from.first)
|
23
|
+
return from[:value] if from[:decoded]
|
24
|
+
|
25
|
+
case format
|
26
|
+
when :full
|
27
|
+
rails_message[:from].formatted.first
|
28
|
+
when :name
|
29
|
+
rails_message[:from].address_list.addresses.first.name
|
30
|
+
when :address
|
31
|
+
rails_message[:from].address_list.addresses.first.address
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# format can be full|name|address
|
36
|
+
def extract_to(rails_message, format: :full)
|
37
|
+
to = complex_extract(rails_message.to.first)
|
38
|
+
return to[:value] if to[:decoded]
|
39
|
+
|
40
|
+
case format
|
41
|
+
when :full
|
42
|
+
rails_message[:to].formatted.first
|
43
|
+
when :name
|
44
|
+
rails_message[:to].address_list.addresses.first.name
|
45
|
+
when :address
|
46
|
+
rails_message[:to].address_list.addresses.first.address
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# when passing in to/from addresses that are complex objects
|
51
|
+
# like a Hash or Twitter::Client instance, they will be YAMLed
|
52
|
+
# and then Base64ed since Mail::Message really only wants
|
53
|
+
# to play with strings for these fields
|
54
|
+
def complex_extract(address_container)
|
55
|
+
if B64Y.decodable?(address_container)
|
56
|
+
{
|
57
|
+
decoded: true,
|
58
|
+
value: B64Y.decode(address_container)
|
59
|
+
}
|
60
|
+
else
|
61
|
+
{
|
62
|
+
decoded: false,
|
63
|
+
value: address_container
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
module Hermes
|
2
|
+
class Phone
|
3
|
+
|
4
|
+
attr_accessor :country, :number
|
5
|
+
|
6
|
+
@@countries = {
|
7
|
+
:af => ['+93', 'Afghanistan'],
|
8
|
+
:al => ['+355', 'Albania'],
|
9
|
+
:dz => ['+213', 'Algeria'],
|
10
|
+
:as => ['+1684', 'American Samoa', :ws],
|
11
|
+
:ad => ['+376', 'Andorra'],
|
12
|
+
:ao => ['+244', 'Angola'],
|
13
|
+
:ai => ['+1264', 'Anguilla'],
|
14
|
+
:ag => ['+1268', 'Antigua and Barbuda'],
|
15
|
+
:ar => ['+54', 'Argentina'],
|
16
|
+
:am => ['+374', 'Armenia'],
|
17
|
+
:aw => ['+297', 'Aruba'],
|
18
|
+
:au => ['+61', 'Australia/Cocos/Christmas Island'],
|
19
|
+
:at => ['+43', 'Austria'],
|
20
|
+
:az => ['+994', 'Azerbaijan'],
|
21
|
+
:bs => ['+1', 'Bahamas'],
|
22
|
+
:bh => ['+973', 'Bahrain'],
|
23
|
+
:bd => ['+880', 'Bangladesh'],
|
24
|
+
:bb => ['+1246', 'Barbados'],
|
25
|
+
:by => ['+375', 'Belarus'],
|
26
|
+
:be => ['+32', 'Belgium'],
|
27
|
+
:bz => ['+501', 'Belize'],
|
28
|
+
:bj => ['+229', 'Benin'],
|
29
|
+
:bm => ['+1441', 'Bermuda'],
|
30
|
+
:bo => ['+591', 'Bolivia'],
|
31
|
+
:ba => ['+387', 'Bosnia and Herzegovina'],
|
32
|
+
:bw => ['+267', 'Botswana'],
|
33
|
+
:br => ['+55', 'Brazil'],
|
34
|
+
:bn => ['+673', 'Brunei'],
|
35
|
+
:bg => ['+359', 'Bulgaria'],
|
36
|
+
:bf => ['+226', 'Burkina Faso'],
|
37
|
+
:bi => ['+257', 'Burundi'],
|
38
|
+
:kh => ['+855', 'Cambodia'],
|
39
|
+
:cm => ['+237', 'Cameroon'],
|
40
|
+
:ca => ['+1', 'Canada'],
|
41
|
+
:cv => ['+238', 'Cape Verde'],
|
42
|
+
:ky => ['+1345', 'Cayman Islands'],
|
43
|
+
:cf => ['+236', 'Central Africa'],
|
44
|
+
:td => ['+235', 'Chad'],
|
45
|
+
:cl => ['+56', 'Chile'],
|
46
|
+
:cn => ['+86', 'China'],
|
47
|
+
:co => ['+57', 'Colombia'],
|
48
|
+
:km => ['+269', 'Comoros'],
|
49
|
+
:cg => ['+242', 'Congo'],
|
50
|
+
:cd => ['+243', 'Congo, Dem Rep'],
|
51
|
+
:cr => ['+506', 'Costa Rica'],
|
52
|
+
:hr => ['+385', 'Croatia'],
|
53
|
+
:cy => ['+357', 'Cyprus'],
|
54
|
+
:cz => ['+420', 'Czech Republic'],
|
55
|
+
:dk => ['+45', 'Denmark'],
|
56
|
+
:dj => ['+253', 'Djibouti'],
|
57
|
+
:dm => ['+1767', 'Dominica'],
|
58
|
+
:do => ['+1809', 'Dominican Republic'],
|
59
|
+
:eg => ['+20', 'Egypt'],
|
60
|
+
:sv => ['+503', 'El Salvador'],
|
61
|
+
:gq => ['+240', 'Equatorial Guinea'],
|
62
|
+
:ee => ['+372', 'Estonia'],
|
63
|
+
:et => ['+251', 'Ethiopia'],
|
64
|
+
:fo => ['+298', 'Faroe Islands'],
|
65
|
+
:fj => ['+679', 'Fiji'],
|
66
|
+
:fi => ['+358', 'Finland/Aland Islands'],
|
67
|
+
:fr => ['+33', 'France'],
|
68
|
+
:gf => ['+594', 'French Guiana'],
|
69
|
+
:pf => ['+689', 'French Polynesia'],
|
70
|
+
:ga => ['+241', 'Gabon'],
|
71
|
+
:gm => ['+220', 'Gambia'],
|
72
|
+
:ge => ['+995', 'Georgia'],
|
73
|
+
:de => ['+49', 'Germany'],
|
74
|
+
:gh => ['+233', 'Ghana'],
|
75
|
+
:gi => ['+350', 'Gibraltar'],
|
76
|
+
:gr => ['+30', 'Greece'],
|
77
|
+
:gl => ['+299', 'Greenland'],
|
78
|
+
:gd => ['+1473', 'Grenada'],
|
79
|
+
:gp => ['+590', 'Guadeloupe'],
|
80
|
+
:gu => ['+1671', 'Guam'],
|
81
|
+
:gt => ['+502', 'Guatemala'],
|
82
|
+
:gn => ['+224', 'Guinea'],
|
83
|
+
:gy => ['+592', 'Guyana'],
|
84
|
+
:ht => ['+509', 'Haiti'],
|
85
|
+
:hn => ['+504', 'Honduras'],
|
86
|
+
:hk => ['+852', 'Hong Kong'],
|
87
|
+
:hu => ['+36', 'Hungary'],
|
88
|
+
:is => ['+354', 'Iceland'],
|
89
|
+
:in => ['+91', 'India'],
|
90
|
+
:id => ['+62', 'Indonesia'],
|
91
|
+
:ir => ['+98', 'Iran'],
|
92
|
+
:iq => ['+964', 'Iraq'],
|
93
|
+
:ie => ['+353', 'Ireland'],
|
94
|
+
:il => ['+972', 'Israel'],
|
95
|
+
:it => ['+39', 'Italy'],
|
96
|
+
:jm => ['+1876', 'Jamaica'],
|
97
|
+
:jp => ['+81', 'Japan'],
|
98
|
+
:jo => ['+962', 'Jordan'],
|
99
|
+
:ke => ['+254', 'Kenya'],
|
100
|
+
:kr => ['+82', 'Korea, Republic of'],
|
101
|
+
:kw => ['+965', 'Kuwait'],
|
102
|
+
:kg => ['+996', 'Kyrgyzstan'],
|
103
|
+
:la => ['+856', 'Laos'],
|
104
|
+
:lv => ['+371', 'Latvia'],
|
105
|
+
:lb => ['+961', 'Lebanon'],
|
106
|
+
:ls => ['+266', 'Lesotho'],
|
107
|
+
:lr => ['+231', 'Liberia'],
|
108
|
+
:ly => ['+218', 'Libya'],
|
109
|
+
:li => ['+423', 'Liechtenstein'],
|
110
|
+
:lt => ['+370', 'Lithuania'],
|
111
|
+
:lu => ['+352', 'Luxembourg'],
|
112
|
+
:mo => ['+853', 'Macao'],
|
113
|
+
:mk => ['+389', 'Macedonia'],
|
114
|
+
:mg => ['+261', 'Madagascar'],
|
115
|
+
:mw => ['+265', 'Malawi'],
|
116
|
+
:my => ['+60', 'Malaysia'],
|
117
|
+
:mv => ['+960', 'Maldives'],
|
118
|
+
:ml => ['+223', 'Mali'],
|
119
|
+
:mt => ['+356', 'Malta'],
|
120
|
+
:mq => ['+596', 'Martinique'],
|
121
|
+
:mr => ['+222', 'Mauritania'],
|
122
|
+
:mu => ['+230', 'Mauritius'],
|
123
|
+
:mx => ['+52', 'Mexico'],
|
124
|
+
:mc => ['+377', 'Monaco'],
|
125
|
+
:mn => ['+976', 'Mongolia'],
|
126
|
+
:me => ['+382', 'Montenegro'],
|
127
|
+
:ms => ['+1664', 'Montserrat'],
|
128
|
+
:ma => ['+212', 'Morocco/Western Sahara'],
|
129
|
+
:mz => ['+258', 'Mozambique'],
|
130
|
+
:na => ['+264', 'Namibia'],
|
131
|
+
:np => ['+977', 'Nepal'],
|
132
|
+
:nl => ['+31', 'Netherlands'],
|
133
|
+
:nz => ['+64', 'New Zealand'],
|
134
|
+
:ni => ['+505', 'Nicaragua'],
|
135
|
+
:ne => ['+227', 'Niger'],
|
136
|
+
:ng => ['+234', 'Nigeria'],
|
137
|
+
:no => ['+47', 'Norway'],
|
138
|
+
:om => ['+968', 'Oman'],
|
139
|
+
:pk => ['+92', 'Pakistan'],
|
140
|
+
:ps => ['+970', 'Palestinian Territory'],
|
141
|
+
:pa => ['+507', 'Panama'],
|
142
|
+
:py => ['+595', 'Paraguay'],
|
143
|
+
:pe => ['+51', 'Peru'],
|
144
|
+
:ph => ['+63', 'Philippines'],
|
145
|
+
:pl => ['+48', 'Poland'],
|
146
|
+
:pt => ['+351', 'Portugal'],
|
147
|
+
:pr => ['+1787', 'Puerto Rico'],
|
148
|
+
:qa => ['+974', 'Qatar'],
|
149
|
+
:re => ['+262', 'Reunion/Mayotte'],
|
150
|
+
:ro => ['+40', 'Romania'],
|
151
|
+
:ru => ['+7', 'Russia/Kazakhstan'],
|
152
|
+
:rw => ['+250', 'Rwanda'],
|
153
|
+
:ws => ['+685', 'Samoa'],
|
154
|
+
:sm => ['+378', 'San Marino'],
|
155
|
+
:sa => ['+966', 'Saudi Arabia'],
|
156
|
+
:sn => ['+221', 'Senegal'],
|
157
|
+
:rs => ['+381', 'Serbia'],
|
158
|
+
:sc => ['+248', 'Seychelles'],
|
159
|
+
:sl => ['+232', 'Sierra Leone'],
|
160
|
+
:sg => ['+65', 'Singapore'],
|
161
|
+
:sk => ['+421', 'Slovakia'],
|
162
|
+
:si => ['+386', 'Slovenia'],
|
163
|
+
:za => ['+27', 'South Africa'],
|
164
|
+
:es => ['+34', 'Spain'],
|
165
|
+
:lk => ['+94', 'Sri Lanka'],
|
166
|
+
:kn => ['+1869', 'St Kitts and Nevis'],
|
167
|
+
:lc => ['+1758', 'St Lucia'],
|
168
|
+
:vc => ['+1784', 'St Vincent Grenadines'],
|
169
|
+
:sd => ['+249', 'Sudan'],
|
170
|
+
:sr => ['+597', 'Suriname'],
|
171
|
+
:sz => ['+268', 'Swaziland'],
|
172
|
+
:se => ['+46', 'Sweden'],
|
173
|
+
:ch => ['+41', 'Switzerland'],
|
174
|
+
:sy => ['+963', 'Syria'],
|
175
|
+
:tw => ['+886', 'Taiwan'],
|
176
|
+
:tj => ['+992', 'Tajikistan'],
|
177
|
+
:tz => ['+255', 'Tanzania'],
|
178
|
+
:th => ['+66', 'Thailand'],
|
179
|
+
:tg => ['+228', 'Togo'],
|
180
|
+
:to => ['+676', 'Tonga'],
|
181
|
+
:tt => ['+1868', 'Trinidad and Tobago'],
|
182
|
+
:tn => ['+216', 'Tunisia'],
|
183
|
+
:tr => ['+90', 'Turkey'],
|
184
|
+
:tc => ['+1649', 'Turks and Caicos Islands'],
|
185
|
+
:ug => ['+256', 'Uganda'],
|
186
|
+
:ua => ['+380', 'Ukraine'],
|
187
|
+
:ae => ['+971', 'United Arab Emirates'],
|
188
|
+
:gb => ['+44', 'United Kingdom'],
|
189
|
+
:us => ['+1', 'United States'],
|
190
|
+
:uy => ['+598', 'Uruguay'],
|
191
|
+
:uz => ['+998', 'Uzbekistan'],
|
192
|
+
:ve => ['+58', 'Venezuela'],
|
193
|
+
:vn => ['+84', 'Vietnam'],
|
194
|
+
:vg => ['+1284', 'Virgin Islands, British'],
|
195
|
+
:vi => ['+1340', 'Virgin Islands, U.S.'],
|
196
|
+
:ye => ['+967', 'Yemen'],
|
197
|
+
:zm => ['+260', 'Zambia'],
|
198
|
+
:zw => ['+263', 'Zimbabwe']
|
199
|
+
}.with_indifferent_access
|
200
|
+
|
201
|
+
def self.countries
|
202
|
+
@@countries
|
203
|
+
end
|
204
|
+
|
205
|
+
def ==(other_phone)
|
206
|
+
self.country == other_phone.country && self.number == other_phone.number
|
207
|
+
end
|
208
|
+
|
209
|
+
def to_s
|
210
|
+
self.full_number
|
211
|
+
end
|
212
|
+
|
213
|
+
def initialize(country, number)
|
214
|
+
@country = country
|
215
|
+
@number = number
|
216
|
+
end
|
217
|
+
|
218
|
+
def country_prefix
|
219
|
+
self.class.prefix_for_country(@country)
|
220
|
+
end
|
221
|
+
|
222
|
+
def country_name
|
223
|
+
self.class.name_for_country(@country)
|
224
|
+
end
|
225
|
+
|
226
|
+
def full_number
|
227
|
+
self.class.prefix_for_country(self.country) + self.number
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.prefix_for_country(country)
|
231
|
+
return @@countries[country.to_s.downcase][0] unless @@countries[country.to_s.downcase].nil? || @@countries[country.to_s.downcase][0].nil?
|
232
|
+
return nil
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hermes-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Scott Klein
|
8
|
+
- Tyler Davis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-04-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 4.0.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 4.0.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: httparty
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0.12'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.12'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: sqlite3
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
description: Rails Action Mailer adapter for balancing multiple providers across email,
|
57
|
+
SMS, and webhook
|
58
|
+
email:
|
59
|
+
- scott@statuspage.io
|
60
|
+
- tyler@statuspage.io
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- MIT-LICENSE
|
66
|
+
- README.rdoc
|
67
|
+
- Rakefile
|
68
|
+
- lib/hermes.rb
|
69
|
+
- lib/hermes/deliverer.rb
|
70
|
+
- lib/hermes/exceptions.rb
|
71
|
+
- lib/hermes/mail_ext.rb
|
72
|
+
- lib/hermes/version.rb
|
73
|
+
- lib/providers/mailgun/mail_ext.rb
|
74
|
+
- lib/providers/mailgun/mailgun_attachment.rb
|
75
|
+
- lib/providers/mailgun/mailgun_provider.rb
|
76
|
+
- lib/providers/outbound_webhook/outbound_webhook_provider.rb
|
77
|
+
- lib/providers/plivo/mail_ext.rb
|
78
|
+
- lib/providers/plivo/plivo_provider.rb
|
79
|
+
- lib/providers/provider.rb
|
80
|
+
- lib/providers/sendgrid/sendgrid_provider.rb
|
81
|
+
- lib/providers/twilio/mail_ext.rb
|
82
|
+
- lib/providers/twilio/twilio_provider.rb
|
83
|
+
- lib/providers/twitter/mail_ext.rb
|
84
|
+
- lib/providers/twitter/twitter_provider.rb
|
85
|
+
- lib/support/b64y.rb
|
86
|
+
- lib/support/email_attachment.rb
|
87
|
+
- lib/support/extractors.rb
|
88
|
+
- lib/support/phone.rb
|
89
|
+
homepage: https://github.com/StatusPage/hermes
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.4.2
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Rails Action Mailer adapter for balancing multiple providers across email,
|
113
|
+
SMS, and webhook
|
114
|
+
test_files: []
|