ses-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. data/app/public/bootstrap/css/bootstrap-responsive.css +1092 -0
  2. data/app/public/bootstrap/css/bootstrap-responsive.min.css +9 -0
  3. data/app/public/bootstrap/css/bootstrap.css +6039 -0
  4. data/app/public/bootstrap/css/bootstrap.min.css +9 -0
  5. data/app/public/bootstrap/img/glyphicons-halflings-white.png +0 -0
  6. data/app/public/bootstrap/img/glyphicons-halflings.png +0 -0
  7. data/app/public/bootstrap/js/bootstrap.js +2159 -0
  8. data/app/public/bootstrap/js/bootstrap.min.js +6 -0
  9. data/app/public/css/application.css +16 -0
  10. data/app/public/datepicker/css/datepicker.css +7 -0
  11. data/app/public/datepicker/js/bootstrap-datepicker.js +454 -0
  12. data/app/public/datepicker/less/datepicker.less +119 -0
  13. data/app/public/highcharts/adapters/mootools-adapter.js +13 -0
  14. data/app/public/highcharts/adapters/mootools-adapter.src.js +327 -0
  15. data/app/public/highcharts/adapters/prototype-adapter.js +16 -0
  16. data/app/public/highcharts/adapters/prototype-adapter.src.js +385 -0
  17. data/app/public/highcharts/highcharts-more.js +35 -0
  18. data/app/public/highcharts/highcharts.js +246 -0
  19. data/app/public/highcharts/highcharts.src.js +15111 -0
  20. data/app/public/highcharts/modules/canvas-tools.js +133 -0
  21. data/app/public/highcharts/modules/canvas-tools.src.js +3113 -0
  22. data/app/public/highcharts/modules/data.js +11 -0
  23. data/app/public/highcharts/modules/data.src.js +277 -0
  24. data/app/public/highcharts/modules/exporting.js +23 -0
  25. data/app/public/highcharts/modules/exporting.src.js +736 -0
  26. data/app/public/highcharts/themes/dark-blue.js +263 -0
  27. data/app/public/highcharts/themes/dark-green.js +263 -0
  28. data/app/public/highcharts/themes/gray.js +262 -0
  29. data/app/public/highcharts/themes/grid.js +95 -0
  30. data/app/public/highcharts/themes/skies.js +89 -0
  31. data/app/public/images/loader.gif +0 -0
  32. data/app/public/js/application.js +81 -0
  33. data/app/views/_chart.haml +2 -0
  34. data/app/views/_search_form.haml +23 -0
  35. data/app/views/bounces.haml +23 -0
  36. data/app/views/kaminari/_first_page.html.erb +3 -0
  37. data/app/views/kaminari/_gap.html.erb +3 -0
  38. data/app/views/kaminari/_last_page.html.erb +3 -0
  39. data/app/views/kaminari/_next_page.html.erb +3 -0
  40. data/app/views/kaminari/_page.html.erb +3 -0
  41. data/app/views/kaminari/_paginator.html.erb +17 -0
  42. data/app/views/kaminari/_prev_page.html.erb +3 -0
  43. data/app/views/layout.haml +25 -0
  44. data/app/views/mails.haml +26 -0
  45. data/app/web_panel.rb +149 -0
  46. data/bin/ses_proxy +20 -0
  47. data/lib/ses_proxy/conf.rb +9 -0
  48. data/lib/ses_proxy/main_command.rb +154 -0
  49. data/lib/ses_proxy/models/bounce.rb +14 -0
  50. data/lib/ses_proxy/models/complaint.rb +13 -0
  51. data/lib/ses_proxy/models/email.rb +15 -0
  52. data/lib/ses_proxy/smtp_server.rb +122 -0
  53. data/lib/ses_proxy/sns_endpoint.rb +199 -0
  54. data/ses_proxy.rb +10 -0
  55. data/template/mongoid.yml +12 -0
  56. data/template/ses-proxy.yml +19 -0
  57. metadata +294 -0
@@ -0,0 +1,14 @@
1
+ require 'mongoid'
2
+
3
+ module SesProxy
4
+ class Bounce
5
+ include Mongoid::Document
6
+ store_in collection: "bounced"
7
+
8
+ field :email, type: String
9
+ field :type, type: String
10
+ field :desc, type: String
11
+ field :created_at, type: DateTime
12
+ field :updated_at, type: DateTime
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ require 'mongoid'
2
+
3
+ module SesProxy
4
+ class Complaint
5
+ include Mongoid::Document
6
+ store_in collection: "complained"
7
+
8
+ field :email, type: String
9
+ field :type, type: String
10
+ field :created_at, type: DateTime
11
+ field :updated_at, type: DateTime
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ require 'mongoid'
2
+
3
+ module SesProxy
4
+ class Email
5
+ include Mongoid::Document
6
+
7
+ field :sender, type: String
8
+ field :recipients, type: String
9
+ field :subject, type: String
10
+ field :body, type: String
11
+ field :system, type: String
12
+ field :created_at, type: DateTime
13
+ field :updated_at, type: DateTime
14
+ end
15
+ end
@@ -0,0 +1,122 @@
1
+ require 'eventmachine'
2
+ require 'mail'
3
+ require 'aws-sdk'
4
+
5
+ module SesProxy
6
+ class SmtpServer < EM::P::SmtpServer
7
+
8
+ def get_server_domain
9
+ @host || "mock.smtp.server.local"
10
+ end
11
+
12
+ def get_server_greeting
13
+ "smtp ses greetings"
14
+ end
15
+
16
+ def receive_plain_auth(user, password)
17
+ @verified = SesProxy::Conf.get[:smtp_auth][:user].eql?(user) and SesProxy::Conf.get[:smtp_auth][:password].eql?(password)
18
+ end
19
+
20
+ def receive_sender(sender)
21
+ sender = sender.gsub(/[<>]/,'')
22
+ @sender = sender
23
+ domains = ses.identities.domains.map(&:identity)
24
+ email_addresses = ses.identities.email_addresses.map(&:identity)
25
+ identity = nil
26
+ if email_addresses.include?(sender)
27
+ identity = ses.identities[sender]
28
+ elsif domains.include?(sender.split('@').last)
29
+ identity = ses.identities[sender.split('@').last]
30
+ end
31
+ identity&&identity.verified?
32
+ end
33
+
34
+ def receive_recipient(recipient)
35
+ recipients << recipient.gsub(/[<>]/,'')
36
+ true
37
+ end
38
+
39
+ def receive_data_chunk(data)
40
+ @message = "#{@message}\n#{data.join("\n")}"
41
+ true
42
+ end
43
+
44
+ def recipients
45
+ @recipients ||= []
46
+ end
47
+
48
+ def sender
49
+ @sender || ""
50
+ end
51
+
52
+ def message
53
+ @message || ""
54
+ end
55
+
56
+ def verified
57
+ @verified || false
58
+ end
59
+
60
+ def ses
61
+ @ses ||= AWS::SimpleEmailService.new(SesProxy::Conf.get[:aws])
62
+ end
63
+
64
+ def receive_message
65
+ return false unless verified
66
+ bounced = Bounce.where({:email=>{"$in"=>recipients}}).map(&:email)
67
+ mail = Mail.read_from_string(message)
68
+ #TODO: Define policy for retry when bounce is not permanent
69
+ actual_recipients = recipients - bounced
70
+ actual_cc_addrs = mail.cc_addrs - bounced
71
+ actual_bcc_addrs = mail.bcc_addrs - bounced
72
+ if actual_recipients.any?
73
+ mail.to = actual_recipients.uniq.join(",")
74
+ mail.cc = actual_cc_addrs.uniq.join(",")
75
+ mail.bcc = actual_bcc_addrs.uniq.join(",")
76
+ record = Email.new({
77
+ :sender => sender,
78
+ :recipients => actual_recipients.uniq.join(","),
79
+ :subject => mail.subject,
80
+ :body => mail.body.decoded,
81
+ :system => mail['X-Sender-System']||"Unknown",
82
+ :created_at => Time.now,
83
+ :updated_at => Time.now
84
+ })
85
+ record.save!
86
+ begin
87
+ ses.send_raw_email(mail.to_s)
88
+ true
89
+ rescue Exception => e
90
+ print "Error! "
91
+ puts e.message
92
+ false
93
+ end
94
+ else
95
+ puts "No valid recipients!"
96
+ true
97
+ end
98
+ end
99
+
100
+ def receive_ehlo_domain(domain)
101
+ @ehlo_domain = domain
102
+ true
103
+ end
104
+
105
+ def self.start(host, port)
106
+ trap(:QUIT) { stop }
107
+ @host = host
108
+ @server = EM.start_server host, port, self
109
+ end
110
+
111
+ def self.stop
112
+ if @server
113
+ EM.stop_server @server
114
+ @server = nil
115
+ end
116
+ end
117
+
118
+ def self.running?
119
+ !!@server
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,199 @@
1
+ require 'rack/request'
2
+ require 'json'
3
+ require 'aws-sdk'
4
+ require 'net/http'
5
+ require 'base64'
6
+ require 'openssl'
7
+
8
+ module SesProxy
9
+ class SnsEndpoint
10
+
11
+ CERTS_URI = {
12
+ "us-east-1" => "http://sns.us-east-1.amazonaws.com/SimpleNotificationService.pem",
13
+ "us-west-1" => "http://sns.us-west-1.amazonaws.com/SimpleNotificationService.pem",
14
+ "eu-west-1" => "http://sns.eu-west-1.amazonaws.com/SimpleNotificationService.pem",
15
+ "ap-southeast-1" => "http://sns.ap-southeast-1.amazonaws.com/SimpleNotificationService.pem"
16
+ }
17
+
18
+ def call(env)
19
+ req = Rack::Request.new(env)
20
+ res = []
21
+
22
+ if not req.post? or not check_message_type(env) or not check_topic(env)
23
+ return make_error
24
+ end
25
+
26
+ json_string = req.body.read
27
+ sns_obj = nil
28
+
29
+ begin
30
+ if json_string and not json_string.strip.eql?""
31
+ sns_obj = JSON.parse json_string
32
+ end
33
+ if sns_obj
34
+ unless check_message_signature(sns_obj)
35
+ puts "Error in message signature"
36
+ return make_error
37
+ end
38
+ if env["HTTP_X_AMZ_SNS_MESSAGE_TYPE"].eql?"Notification"
39
+ message = JSON.parse sns_obj["Message"]
40
+ unless message
41
+ return make_error
42
+ end
43
+ if message["notificationType"].eql? "Bounce"
44
+ message["bounce"]["bouncedRecipients"].each do |recipient|
45
+ if record = Bounce.where(:email => recipient["emailAddress"]).first
46
+ record.updated_at = Time.now
47
+ record.save!
48
+ else
49
+ record = Bounce.new({
50
+ :email => recipient["emailAddress"],
51
+ :type => message["bounce"]["bounceType"],
52
+ :desc => recipient["diagnosticCode"],
53
+ :created_at => Time.now,
54
+ :updated_at => Time.now
55
+ })
56
+ record.save!
57
+ end
58
+ end
59
+ elsif message["notificationType"].eql? "Complaint"
60
+ message["complaint"]["complainedRecipients"].each do |recipient|
61
+ if record = Complaint.where(:email => recipient["emailAddress"]).first
62
+ record.updated_at = Time.now
63
+ record.save!
64
+ else
65
+ record = Complaint.new({
66
+ :email=>recipient["emailAddress"],
67
+ :type=>message["complaint"]["complaintFeedbackType"],
68
+ :created_at=>Time.now,
69
+ :updated_at=>Time.now
70
+ })
71
+ record.save!
72
+ end
73
+ end
74
+ end
75
+ elsif env["HTTP_X_AMZ_SNS_MESSAGE_TYPE"].eql?"SubscriptionConfirmation" and sns_obj["Type"].eql? "SubscriptionConfirmation"
76
+ sns.confirm_subscription :topic_arn=>sns_obj["TopicArn"], :token=>sns_obj["Token"], :authenticate_on_unsubscribe=>"true"
77
+ end
78
+ [200, {'Content-Type' => 'text/html'}, res]
79
+ else
80
+ puts "SNS Object is nil"
81
+ return make_error
82
+ end
83
+ rescue Exception => e
84
+ print "Error! "
85
+ puts e.message
86
+ return make_error
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def check_message_signature(sns_obj)
93
+ if sns_obj["Type"].eql? "Notification"
94
+ string = get_notification_canonical_string(sns_obj)
95
+ else
96
+ string = get_subscription_confirmation_canonical_string(sns_obj)
97
+ end
98
+ region = sns_obj["TopicArn"].split(":")[3]
99
+ signature = sns_obj["Signature"]
100
+ #openssl x509 -in CERT -pubkey -noout > pub_key
101
+ pub_key = OpenSSL::X509::Certificate.new(File.read(get_cert(region))).public_key
102
+ #base64 -i -d signature > plain_signature
103
+ plain_signature = Base64.decode64(signature)
104
+ #openssl dgst -sha1 -verify pub -signature sigraw MESS
105
+ pub_key.verify(OpenSSL::Digest::SHA1.new, plain_signature, string)
106
+ end
107
+
108
+ def get_cert(region)
109
+ unless File.exists? File.join("/","tmp","#{region}_sns_cert.pem")
110
+ res = fetch CERTS_URI[region]
111
+ puts res.inspect
112
+ if res and not res.eql?""
113
+ open(File.join("/","tmp","#{region}_sns_cert.pem"), "wb") do |file|
114
+ file.write(res)
115
+ end
116
+ else
117
+ raise Exception, "Unable to get SNS Certificate!"
118
+ end
119
+ end
120
+ File.join("/","tmp","#{region}_sns_cert.pem")
121
+ end
122
+
123
+ def get_notification_canonical_string(sns_obj)
124
+ string = "Message\n"
125
+ string = "#{string}#{sns_obj["Message"]}\n"
126
+ string = "#{string}MessageId\n"
127
+ string = "#{string}#{sns_obj["MessageId"]}\n"
128
+ if sns_obj["Subject"]
129
+ string = "#{string}Subject\n"
130
+ string = "#{string}#{sns_obj["Subject"]}\n"
131
+ end
132
+ string = "#{string}Timestamp\n"
133
+ string = "#{string}#{sns_obj["Timestamp"]}\n"
134
+ string = "#{string}TopicArn\n"
135
+ string = "#{string}#{sns_obj["TopicArn"]}\n"
136
+ string = "#{string}Type\n"
137
+ string = "#{string}#{sns_obj["Type"]}\n"
138
+ string.force_encoding("UTF-8")
139
+ end
140
+
141
+ def get_subscription_confirmation_canonical_string(sns_obj)
142
+ string = "Message\n"
143
+ string = "#{string}#{sns_obj["Message"]}\n"
144
+ string = "#{string}MessageId\n"
145
+ string = "#{string}#{sns_obj["MessageId"]}\n"
146
+ string = "#{string}SubscribeURL\n"
147
+ string = "#{string}#{sns_obj["SubscribeURL"]}\n"
148
+ string = "#{string}Timestamp\n"
149
+ string = "#{string}#{sns_obj["Timestamp"]}\n"
150
+ string = "#{string}Token\n"
151
+ string = "#{string}#{sns_obj["Token"]}\n"
152
+ string = "#{string}TopicArn\n"
153
+ string = "#{string}#{sns_obj["TopicArn"]}\n"
154
+ string = "#{string}Type\n"
155
+ string = "#{string}#{sns_obj["Type"]}\n"
156
+ string.force_encoding("UTF-8")
157
+ end
158
+
159
+ def fetch(uri_str, limit = 10)
160
+ raise Exception, 'too many HTTP redirects' if limit == 0
161
+
162
+ response = Net::HTTP.get_response(URI(uri_str))
163
+
164
+ case response
165
+ when Net::HTTPSuccess then
166
+ response.body
167
+ when Net::HTTPRedirection then
168
+ puts response.inspect
169
+ location = response['location']
170
+ url = URI.parse(location)
171
+ location = "#{URI(uri_str).scheme}://#{URI(uri_str).host}#{location}" unless url.host
172
+ warn "redirected to #{location}"
173
+ fetch(location, limit - 1)
174
+ else
175
+ response.value
176
+ end
177
+ end
178
+
179
+ def make_error
180
+ [422, {'Content-Type' => 'text/html'}, ["Wrong request!"]]
181
+ end
182
+
183
+ def sns
184
+ @sns ||= AWS::SNS::Client.new(SesProxy::Conf.get[:aws])
185
+ end
186
+
187
+ def check_topic(env)
188
+ topic_arn = env["HTTP_X_AMZ_SNS_TOPIC_ARN"]
189
+ allowed_topic_arns = SesProxy::Conf.get[:aws][:allowed_topic_arns]
190
+ topic_arn && allowed_topic_arns.include?(topic_arn)
191
+ end
192
+
193
+ def check_message_type(env)
194
+ message_type = env["HTTP_X_AMZ_SNS_MESSAGE_TYPE"]
195
+ message_type && ["SubscriptionConfirmation","Notification","UnsubscribeConfirmation"].include?(message_type)
196
+ end
197
+
198
+ end
199
+ end
@@ -0,0 +1,10 @@
1
+ module SesProxy
2
+ ROOT = File.expand_path(File.join(File.dirname(__FILE__)))
3
+ autoload :MainCommand, 'ses_proxy/main_command'
4
+ autoload :SnsEndpoint, 'ses_proxy/sns_endpoint'
5
+ autoload :SmtpServer, 'ses_proxy/smtp_server'
6
+ autoload :Conf, 'ses_proxy/conf'
7
+ autoload :Bounce, 'ses_proxy/models/bounce'
8
+ autoload :Complaint, 'ses_proxy/models/complaint'
9
+ autoload :Email, 'ses_proxy/models/email'
10
+ end
@@ -0,0 +1,12 @@
1
+ development:
2
+ sessions:
3
+ default:
4
+ database: ses-proxy-development
5
+ hosts:
6
+ - localhost:27017
7
+ production:
8
+ sessions:
9
+ default:
10
+ database: ses-proxy-production
11
+ hosts:
12
+ - localhost:27017
@@ -0,0 +1,19 @@
1
+ :aws:
2
+ :access_key_id: your_access_key_id
3
+ :secret_access_key: your_secret_access_key
4
+ :account_id: 000000000000
5
+ :allowed_topic_arns: ["arn:aws:sns:us-east-1:123456789012:MyTopic"]
6
+ :smtp_auth:
7
+ :user: smtp_user
8
+ :password: smtp_pass
9
+ :test:
10
+ :from: test@example.com
11
+ :to: ["test@example.com"]
12
+ :topic_arn: "arn:aws:sns:us-east-1:123456789012:MyTopic"
13
+ :subscription_arn: "arn:aws:sns:us-east-1:123456789012:MyTopic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55"
14
+ #:smtp:
15
+ # :port: 1025
16
+ # :host: 0.0.0.0
17
+ #:http:
18
+ # :port: 9292
19
+ # :host: 0.0.0.0
metadata ADDED
@@ -0,0 +1,294 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ses-proxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Massimo Maino
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: clamp
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rack
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: thin
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: aws-sdk
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: mail
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: eventmachine
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: mongoid
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: haml
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: sinatra
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :runtime
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: kaminari
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :runtime
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ - !ruby/object:Gem::Dependency
191
+ name: padrino-helpers
192
+ requirement: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :runtime
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ description:
207
+ email: maintux@gmail.com
208
+ executables:
209
+ - ses_proxy
210
+ extensions: []
211
+ extra_rdoc_files: []
212
+ files:
213
+ - ses_proxy.rb
214
+ - lib/ses_proxy/sns_endpoint.rb
215
+ - lib/ses_proxy/models/email.rb
216
+ - lib/ses_proxy/models/complaint.rb
217
+ - lib/ses_proxy/models/bounce.rb
218
+ - lib/ses_proxy/conf.rb
219
+ - lib/ses_proxy/smtp_server.rb
220
+ - lib/ses_proxy/main_command.rb
221
+ - bin/ses_proxy
222
+ - app/web_panel.rb
223
+ - app/views/_search_form.haml
224
+ - app/views/bounces.haml
225
+ - app/views/kaminari/_gap.html.erb
226
+ - app/views/kaminari/_prev_page.html.erb
227
+ - app/views/kaminari/_page.html.erb
228
+ - app/views/kaminari/_next_page.html.erb
229
+ - app/views/kaminari/_paginator.html.erb
230
+ - app/views/kaminari/_first_page.html.erb
231
+ - app/views/kaminari/_last_page.html.erb
232
+ - app/views/layout.haml
233
+ - app/views/mails.haml
234
+ - app/views/_chart.haml
235
+ - app/public/datepicker/less/datepicker.less
236
+ - app/public/datepicker/css/datepicker.css
237
+ - app/public/datepicker/js/bootstrap-datepicker.js
238
+ - app/public/images/loader.gif
239
+ - app/public/css/application.css
240
+ - app/public/js/application.js
241
+ - app/public/highcharts/modules/canvas-tools.js
242
+ - app/public/highcharts/modules/exporting.js
243
+ - app/public/highcharts/modules/data.src.js
244
+ - app/public/highcharts/modules/data.js
245
+ - app/public/highcharts/modules/exporting.src.js
246
+ - app/public/highcharts/modules/canvas-tools.src.js
247
+ - app/public/highcharts/highcharts.src.js
248
+ - app/public/highcharts/themes/gray.js
249
+ - app/public/highcharts/themes/skies.js
250
+ - app/public/highcharts/themes/dark-blue.js
251
+ - app/public/highcharts/themes/grid.js
252
+ - app/public/highcharts/themes/dark-green.js
253
+ - app/public/highcharts/adapters/prototype-adapter.js
254
+ - app/public/highcharts/adapters/prototype-adapter.src.js
255
+ - app/public/highcharts/adapters/mootools-adapter.src.js
256
+ - app/public/highcharts/adapters/mootools-adapter.js
257
+ - app/public/highcharts/highcharts-more.js
258
+ - app/public/highcharts/highcharts.js
259
+ - app/public/bootstrap/img/glyphicons-halflings-white.png
260
+ - app/public/bootstrap/img/glyphicons-halflings.png
261
+ - app/public/bootstrap/css/bootstrap.min.css
262
+ - app/public/bootstrap/css/bootstrap-responsive.css
263
+ - app/public/bootstrap/css/bootstrap-responsive.min.css
264
+ - app/public/bootstrap/css/bootstrap.css
265
+ - app/public/bootstrap/js/bootstrap.min.js
266
+ - app/public/bootstrap/js/bootstrap.js
267
+ - template/ses-proxy.yml
268
+ - template/mongoid.yml
269
+ homepage: https://github.com/maintux/ses-proxy
270
+ licenses:
271
+ - MIT
272
+ post_install_message:
273
+ rdoc_options: []
274
+ require_paths:
275
+ - lib
276
+ required_ruby_version: !ruby/object:Gem::Requirement
277
+ none: false
278
+ requirements:
279
+ - - ! '>='
280
+ - !ruby/object:Gem::Version
281
+ version: '0'
282
+ required_rubygems_version: !ruby/object:Gem::Requirement
283
+ none: false
284
+ requirements:
285
+ - - ! '>='
286
+ - !ruby/object:Gem::Version
287
+ version: '0'
288
+ requirements: []
289
+ rubyforge_project:
290
+ rubygems_version: 1.8.24
291
+ signing_key:
292
+ specification_version: 3
293
+ summary: SMTP Proxy for Amazon Simple Email Service with bounce and complaints support
294
+ test_files: []