ses-dashboard 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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/Dockerfile +8 -0
  3. data/README.md +238 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/ses_dashboard/application.js +126 -0
  6. data/app/assets/stylesheets/ses_dashboard/application.css +226 -0
  7. data/app/controllers/ses_dashboard/application_controller.rb +50 -0
  8. data/app/controllers/ses_dashboard/dashboard_controller.rb +26 -0
  9. data/app/controllers/ses_dashboard/emails_controller.rb +93 -0
  10. data/app/controllers/ses_dashboard/projects_controller.rb +67 -0
  11. data/app/controllers/ses_dashboard/test_emails_controller.rb +38 -0
  12. data/app/controllers/ses_dashboard/webhooks_controller.rb +39 -0
  13. data/app/helpers/ses_dashboard/application_helper.rb +47 -0
  14. data/app/models/ses_dashboard/application_record.rb +5 -0
  15. data/app/models/ses_dashboard/email.rb +48 -0
  16. data/app/models/ses_dashboard/email_event.rb +18 -0
  17. data/app/models/ses_dashboard/project.rb +20 -0
  18. data/app/services/ses_dashboard/webhook_event_persistor.rb +69 -0
  19. data/app/views/layouts/ses_dashboard/application.html.erb +25 -0
  20. data/app/views/ses_dashboard/dashboard/index.html.erb +56 -0
  21. data/app/views/ses_dashboard/emails/index.html.erb +72 -0
  22. data/app/views/ses_dashboard/emails/show.html.erb +60 -0
  23. data/app/views/ses_dashboard/projects/_form.html.erb +24 -0
  24. data/app/views/ses_dashboard/projects/edit.html.erb +6 -0
  25. data/app/views/ses_dashboard/projects/index.html.erb +42 -0
  26. data/app/views/ses_dashboard/projects/new.html.erb +6 -0
  27. data/app/views/ses_dashboard/projects/show.html.erb +47 -0
  28. data/app/views/ses_dashboard/shared/_flash.html.erb +3 -0
  29. data/app/views/ses_dashboard/shared/_pagination.html.erb +15 -0
  30. data/app/views/ses_dashboard/shared/_stat_card.html.erb +4 -0
  31. data/app/views/ses_dashboard/test_emails/new.html.erb +38 -0
  32. data/config/routes.rb +16 -0
  33. data/db/migrate/20240101000001_create_ses_dashboard_projects.rb +13 -0
  34. data/db/migrate/20240101000002_create_ses_dashboard_emails.rb +21 -0
  35. data/db/migrate/20240101000003_create_ses_dashboard_email_events.rb +15 -0
  36. data/docker-compose.yml +45 -0
  37. data/lib/ses_dashboard/auth/base.rb +31 -0
  38. data/lib/ses_dashboard/auth/cloudflare_adapter.rb +106 -0
  39. data/lib/ses_dashboard/auth/devise_adapter.rb +22 -0
  40. data/lib/ses_dashboard/client.rb +95 -0
  41. data/lib/ses_dashboard/engine.rb +39 -0
  42. data/lib/ses_dashboard/paginatable.rb +30 -0
  43. data/lib/ses_dashboard/stats_aggregator.rb +107 -0
  44. data/lib/ses_dashboard/version.rb +3 -0
  45. data/lib/ses_dashboard/webhook_processor.rb +116 -0
  46. data/lib/ses_dashboard.rb +66 -0
  47. metadata +369 -0
@@ -0,0 +1,107 @@
1
+ module SesDashboard
2
+ # Aggregates email statistics for the dashboard.
3
+ #
4
+ # Usage:
5
+ # agg = StatsAggregator.new(project_id: project.id, from: 14.days.ago, to: Time.current)
6
+ # agg.counters # => { sent: 100, delivered: 90, bounced: 3, ... }
7
+ # agg.time_series # => { labels: ["2024-01-01", ...], data: [5, 12, ...] }
8
+ # agg.total_opens # => 45
9
+ # agg.total_clicks # => 12
10
+ #
11
+ class StatsAggregator
12
+ def initialize(project_id: nil, from: nil, to: nil)
13
+ @project_id = project_id
14
+ @from = from
15
+ @to = to
16
+ end
17
+
18
+ # Returns counts grouped by status.
19
+ # "not_delivered" is derived as the sum of bounced + complained + rejected.
20
+ def counters
21
+ scope = base_scope
22
+ counts = scope.group(:status).count
23
+
24
+ not_delivered = (counts["bounced"] || 0) +
25
+ (counts["complained"] || 0) +
26
+ (counts["rejected"] || 0)
27
+
28
+ {
29
+ total: scope.count,
30
+ sent: counts["sent"] || 0,
31
+ delivered: counts["delivered"] || 0,
32
+ bounced: counts["bounced"] || 0,
33
+ complained: counts["complained"] || 0,
34
+ rejected: counts["rejected"] || 0,
35
+ failed: counts["failed"] || 0,
36
+ not_delivered: not_delivered
37
+ }
38
+ end
39
+
40
+ def total_opens
41
+ base_scope.sum(:opens)
42
+ end
43
+
44
+ def total_clicks
45
+ base_scope.sum(:clicks)
46
+ end
47
+
48
+ # Returns a Chart.js-compatible hash: { labels: [...], data: [...] }
49
+ # Groups by calendar day over the from..to range, filling gaps with zero.
50
+ def time_series
51
+ from = effective_from
52
+ to = effective_to
53
+
54
+ expr = Arel.sql(date_group_expr)
55
+ raw = base_scope
56
+ .where(sent_at: from..to)
57
+ .group(expr)
58
+ .order(expr)
59
+ .count
60
+
61
+ all_days = date_range_days(from, to)
62
+ labels = all_days.map { |d| d.strftime("%Y-%m-%d") }
63
+ data = all_days.map { |d| raw[d.strftime("%Y-%m-%d")] || 0 }
64
+
65
+ { labels: labels, data: data }
66
+ end
67
+
68
+ private
69
+
70
+ def base_scope
71
+ scope = SesDashboard::Email.all
72
+ scope = scope.where(project_id: @project_id) if @project_id
73
+ scope = scope.where(sent_at: @from..@to) if @from && @to
74
+ scope
75
+ end
76
+
77
+ def date_group_expr
78
+ case connection.adapter_name.downcase
79
+ when "sqlite"
80
+ "strftime('%Y-%m-%d', sent_at)"
81
+ when "postgresql"
82
+ "TO_CHAR(sent_at AT TIME ZONE 'UTC', 'YYYY-MM-DD')"
83
+ else
84
+ # MySQL / MariaDB
85
+ "DATE_FORMAT(sent_at, '%Y-%m-%d')"
86
+ end
87
+ end
88
+
89
+ def connection
90
+ ActiveRecord::Base.connection
91
+ end
92
+
93
+ def effective_from
94
+ @from || 30.days.ago.beginning_of_day
95
+ end
96
+
97
+ def effective_to
98
+ @to || Time.current.end_of_day
99
+ end
100
+
101
+ def date_range_days(from, to)
102
+ from_d = from.to_date
103
+ to_d = to.to_date
104
+ (from_d..to_d).to_a
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,3 @@
1
+ module SesDashboard
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,116 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module SesDashboard
6
+ # Parses a raw SNS HTTP POST body and returns a normalized Result.
7
+ #
8
+ # Usage:
9
+ # result = WebhookProcessor.new(request.body.read).process
10
+ # case result.action
11
+ # when :confirm_subscription
12
+ # Net::HTTP.get(URI(result.subscribe_url))
13
+ # when :process_event
14
+ # WebhookEventPersistor.new(project, result).persist
15
+ # end
16
+ #
17
+ class WebhookProcessor
18
+ Result = Struct.new(
19
+ :action, # :confirm_subscription | :process_event | :unknown
20
+ :subscribe_url, # present when action == :confirm_subscription
21
+ :event_type, # "send" | "delivery" | "bounce" | "complaint" | "open" | "click" | "reject" | "rendering_failure"
22
+ :message_id, # SES messageId
23
+ :destination, # Array of recipient addresses
24
+ :source, # From: address
25
+ :subject, # Email subject (may be nil)
26
+ :occurred_at, # Time
27
+ :raw_payload, # The full parsed inner SES event Hash for storage
28
+ keyword_init: true
29
+ )
30
+
31
+ def initialize(raw_body)
32
+ @raw_body = raw_body
33
+ end
34
+
35
+ def process
36
+ sns = parse_json(@raw_body)
37
+ return unknown_result unless sns
38
+
39
+ case sns["Type"]
40
+ when "SubscriptionConfirmation"
41
+ Result.new(action: :confirm_subscription, subscribe_url: sns["SubscribeURL"])
42
+ when "Notification"
43
+ process_notification(sns)
44
+ else
45
+ unknown_result
46
+ end
47
+ rescue => e
48
+ Rails.logger.error("[SesDashboard] WebhookProcessor error: #{e.message}") if defined?(Rails)
49
+ unknown_result
50
+ end
51
+
52
+ private
53
+
54
+ def process_notification(sns)
55
+ message = parse_json(sns["Message"])
56
+ return unknown_result unless message
57
+
58
+ # SES supports two notification formats:
59
+ # - Event Publishing (newer): uses "eventType" key
60
+ # - Feedback Notifications (legacy): uses "notificationType" key
61
+ raw_event_type = (message["eventType"] || message["notificationType"] || "").downcase
62
+ event_type = normalize_event_type(raw_event_type)
63
+
64
+ mail = message["mail"] || {}
65
+ timestamp = parse_time(mail["timestamp"] || sns["Timestamp"])
66
+
67
+ Result.new(
68
+ action: :process_event,
69
+ event_type: event_type,
70
+ message_id: mail["messageId"],
71
+ destination: Array(mail["destination"]),
72
+ source: mail["source"],
73
+ subject: extract_subject(mail),
74
+ occurred_at: timestamp,
75
+ raw_payload: message
76
+ )
77
+ end
78
+
79
+ def normalize_event_type(raw)
80
+ case raw
81
+ when "send" then "send"
82
+ when "delivery" then "delivery"
83
+ when "bounce" then "bounce"
84
+ when "complaint" then "complaint"
85
+ when "open" then "open"
86
+ when "click" then "click"
87
+ when "reject" then "reject"
88
+ when "renderingfailure" then "rendering_failure"
89
+ else raw
90
+ end
91
+ end
92
+
93
+ def extract_subject(mail)
94
+ headers = Array(mail["headers"])
95
+ subj = headers.find { |h| h["name"]&.casecmp("subject")&.zero? }
96
+ subj ? subj["value"] : (mail["commonHeaders"] || {})["subject"]
97
+ end
98
+
99
+ def parse_time(str)
100
+ return Time.now.utc unless str
101
+ Time.parse(str).utc
102
+ rescue ArgumentError
103
+ Time.now.utc
104
+ end
105
+
106
+ def parse_json(str)
107
+ JSON.parse(str)
108
+ rescue JSON::ParserError, TypeError
109
+ nil
110
+ end
111
+
112
+ def unknown_result
113
+ Result.new(action: :unknown)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,66 @@
1
+ require_relative "ses_dashboard/version"
2
+ require_relative "ses_dashboard/client"
3
+ require_relative "ses_dashboard/webhook_processor"
4
+ require_relative "ses_dashboard/stats_aggregator"
5
+ require_relative "ses_dashboard/paginatable"
6
+ require_relative "ses_dashboard/auth/base"
7
+ require_relative "ses_dashboard/auth/devise_adapter"
8
+ require_relative "ses_dashboard/auth/cloudflare_adapter"
9
+
10
+ module SesDashboard
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def self.configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration) if block_given?
20
+ end
21
+
22
+ def self.reset_configuration!
23
+ self.configuration = Configuration.new
24
+ end
25
+
26
+ class Configuration
27
+ # AWS — credentials are optional. The SDK credential chain (SSO, IAM roles, instance
28
+ # profiles, env vars) is used when these are not explicitly set.
29
+ attr_accessor :aws_region, :aws_access_key_id, :aws_secret_access_key, :endpoint
30
+
31
+ # Auth — :none, :devise, :cloudflare, or any object responding to #authenticate(request)
32
+ attr_accessor :authentication_adapter
33
+
34
+ # Caching for SES API calls (quota, statistics, etc.)
35
+ attr_accessor :cache_enabled
36
+
37
+ # Dashboard behaviour
38
+ attr_accessor :per_page # rows per page in activity log (default 25)
39
+ attr_accessor :time_zone # time zone for chart grouping (default "UTC")
40
+ attr_accessor :test_email_from # From: address used when sending test emails
41
+
42
+ # Security — set to true in production to validate SNS message signatures
43
+ attr_accessor :verify_sns_signature
44
+
45
+ # Cloudflare Zero Trust
46
+ attr_accessor :cloudflare_team_domain # e.g. "myteam.cloudflareaccess.com"
47
+ attr_accessor :cloudflare_aud # JWT audience (your CF application AUD)
48
+
49
+ def initialize
50
+ @aws_region = ENV.fetch("AWS_REGION", "us-east-1")
51
+ @aws_access_key_id = nil
52
+ @aws_secret_access_key = nil
53
+ @endpoint = ENV["AWS_ENDPOINT_URL"] # LocalStack in dev/test
54
+ @authentication_adapter = :none
55
+ @cache_enabled = true
56
+ @per_page = 25
57
+ @time_zone = "UTC"
58
+ @test_email_from = nil
59
+ @verify_sns_signature = false
60
+ @cloudflare_team_domain = nil
61
+ @cloudflare_aud = nil
62
+ end
63
+ end
64
+
65
+ autoload :Engine, "ses_dashboard/engine"
66
+ end
metadata ADDED
@@ -0,0 +1,369 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ses-dashboard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - antodoms
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: actionpack
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: actionview
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '7.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '7.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: aws-sdk-ses
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rexml
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: csv
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: bundler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '2.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '2.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rake
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rspec
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rspec-rails
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '6.0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '6.0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: rack-test
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ - !ruby/object:Gem::Dependency
181
+ name: rails
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '7.0'
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '7.0'
194
+ - !ruby/object:Gem::Dependency
195
+ name: sqlite3
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '2.1'
201
+ type: :development
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '2.1'
208
+ - !ruby/object:Gem::Dependency
209
+ name: factory_bot_rails
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ type: :development
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ - !ruby/object:Gem::Dependency
223
+ name: capybara
224
+ requirement: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ type: :development
230
+ prerelease: false
231
+ version_requirements: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ - !ruby/object:Gem::Dependency
237
+ name: rails-controller-testing
238
+ requirement: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0'
243
+ type: :development
244
+ prerelease: false
245
+ version_requirements: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ - !ruby/object:Gem::Dependency
251
+ name: puma
252
+ requirement: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - ">="
255
+ - !ruby/object:Gem::Version
256
+ version: '0'
257
+ type: :development
258
+ prerelease: false
259
+ version_requirements: !ruby/object:Gem::Requirement
260
+ requirements:
261
+ - - ">="
262
+ - !ruby/object:Gem::Version
263
+ version: '0'
264
+ - !ruby/object:Gem::Dependency
265
+ name: selenium-webdriver
266
+ requirement: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0'
271
+ type: :development
272
+ prerelease: false
273
+ version_requirements: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0'
278
+ - !ruby/object:Gem::Dependency
279
+ name: database_cleaner-active_record
280
+ requirement: !ruby/object:Gem::Requirement
281
+ requirements:
282
+ - - ">="
283
+ - !ruby/object:Gem::Version
284
+ version: '0'
285
+ type: :development
286
+ prerelease: false
287
+ version_requirements: !ruby/object:Gem::Requirement
288
+ requirements:
289
+ - - ">="
290
+ - !ruby/object:Gem::Version
291
+ version: '0'
292
+ description: A mountable Rails engine that provides a real-time dashboard for Amazon
293
+ SES, tracking email delivery, bounces, complaints, opens, and clicks via SNS webhooks.
294
+ email:
295
+ - antodoms@outlook.com
296
+ executables: []
297
+ extensions: []
298
+ extra_rdoc_files: []
299
+ files:
300
+ - Dockerfile
301
+ - README.md
302
+ - Rakefile
303
+ - app/assets/javascripts/ses_dashboard/application.js
304
+ - app/assets/stylesheets/ses_dashboard/application.css
305
+ - app/controllers/ses_dashboard/application_controller.rb
306
+ - app/controllers/ses_dashboard/dashboard_controller.rb
307
+ - app/controllers/ses_dashboard/emails_controller.rb
308
+ - app/controllers/ses_dashboard/projects_controller.rb
309
+ - app/controllers/ses_dashboard/test_emails_controller.rb
310
+ - app/controllers/ses_dashboard/webhooks_controller.rb
311
+ - app/helpers/ses_dashboard/application_helper.rb
312
+ - app/models/ses_dashboard/application_record.rb
313
+ - app/models/ses_dashboard/email.rb
314
+ - app/models/ses_dashboard/email_event.rb
315
+ - app/models/ses_dashboard/project.rb
316
+ - app/services/ses_dashboard/webhook_event_persistor.rb
317
+ - app/views/layouts/ses_dashboard/application.html.erb
318
+ - app/views/ses_dashboard/dashboard/index.html.erb
319
+ - app/views/ses_dashboard/emails/index.html.erb
320
+ - app/views/ses_dashboard/emails/show.html.erb
321
+ - app/views/ses_dashboard/projects/_form.html.erb
322
+ - app/views/ses_dashboard/projects/edit.html.erb
323
+ - app/views/ses_dashboard/projects/index.html.erb
324
+ - app/views/ses_dashboard/projects/new.html.erb
325
+ - app/views/ses_dashboard/projects/show.html.erb
326
+ - app/views/ses_dashboard/shared/_flash.html.erb
327
+ - app/views/ses_dashboard/shared/_pagination.html.erb
328
+ - app/views/ses_dashboard/shared/_stat_card.html.erb
329
+ - app/views/ses_dashboard/test_emails/new.html.erb
330
+ - config/routes.rb
331
+ - db/migrate/20240101000001_create_ses_dashboard_projects.rb
332
+ - db/migrate/20240101000002_create_ses_dashboard_emails.rb
333
+ - db/migrate/20240101000003_create_ses_dashboard_email_events.rb
334
+ - docker-compose.yml
335
+ - lib/ses_dashboard.rb
336
+ - lib/ses_dashboard/auth/base.rb
337
+ - lib/ses_dashboard/auth/cloudflare_adapter.rb
338
+ - lib/ses_dashboard/auth/devise_adapter.rb
339
+ - lib/ses_dashboard/client.rb
340
+ - lib/ses_dashboard/engine.rb
341
+ - lib/ses_dashboard/paginatable.rb
342
+ - lib/ses_dashboard/stats_aggregator.rb
343
+ - lib/ses_dashboard/version.rb
344
+ - lib/ses_dashboard/webhook_processor.rb
345
+ homepage: https://github.com/antodoms/ses_dashboard
346
+ licenses:
347
+ - MIT
348
+ metadata:
349
+ homepage_uri: https://github.com/antodoms/ses_dashboard
350
+ source_code_uri: https://github.com/antodoms/ses_dashboard
351
+ changelog_uri: https://github.com/antodoms/ses_dashboard/CHANGELOG.md
352
+ rdoc_options: []
353
+ require_paths:
354
+ - lib
355
+ required_ruby_version: !ruby/object:Gem::Requirement
356
+ requirements:
357
+ - - ">="
358
+ - !ruby/object:Gem::Version
359
+ version: '3.0'
360
+ required_rubygems_version: !ruby/object:Gem::Requirement
361
+ requirements:
362
+ - - ">="
363
+ - !ruby/object:Gem::Version
364
+ version: '0'
365
+ requirements: []
366
+ rubygems_version: 4.0.6
367
+ specification_version: 4
368
+ summary: SES dashboard gem with pluggable authentication and AWS SES data fetching.
369
+ test_files: []