hermes-rails 0.0.1
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.
- 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: []
|