emailbutler 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86c998b3d9e29ae064f2b2881b392661d785534883a28f40cb7fd5e846e5cf69
4
- data.tar.gz: 4952983e5fc06ea2e90f2d2504d66d6c7ea083281796d30b7e71154096235e1d
3
+ metadata.gz: 2cb350207d1f0d4174a10f2589ee604cc3ddb71438ed6c63b057ef9975a93ed4
4
+ data.tar.gz: a0dae6c1a23c417827d03abf56ae9386696ca175f5de107eb81473925d3010b4
5
5
  SHA512:
6
- metadata.gz: eedddd11e3d27324ceeb712c3f9821ede81fe3a02970166c6d7c67b546333ffdd8ce5ffc9cf885879a8020c966233c6aa51088d683546dda472645156f77c3d2
7
- data.tar.gz: da1e97498ff6853525d05a8cfe988aef95cccd3b82e39803c966218945904a45c2830b17bd3a5eea5a728b02eb0e47faf0e4d3b9fdd2e503c87423f3d816df6d
6
+ metadata.gz: 5c69ecaab954896b0378320e4c535dcfb152356117181fc55c3f5fb479afa73ddceee39bfbd10707c6b488710b07e8b281fa96dcc723c114fa76ed78a849aba2
7
+ data.tar.gz: 8362e1119289ec26d18b29d7575d629a334265ae9365a052bc84f0e731694ad6372e314f976b22d118a31389691cc06c3194650dcc8b22f994bdfd8e05ab1282
data/README.md CHANGED
@@ -29,6 +29,9 @@ require 'emailbutler/adapters/active_record'
29
29
 
30
30
  Emailbutler.configure do |config|
31
31
  config.adapter = Emailbutler::Adapters::ActiveRecord.new
32
+ config.ui_username = 'username'
33
+ config.ui_password = 'password'
34
+ config.ui_secured_environments = ['production']
32
35
  end
33
36
  ```
34
37
 
@@ -63,5 +66,10 @@ end
63
66
  1. Each event with sending email will create new record with message params in database.
64
67
  2. Each webhook event will update status of message in database.
65
68
 
69
+ ### UI
70
+
71
+ Emailbutler provides UI with rendering email tracking statistics - /emailbutler/ui/dashboard.
72
+ And with opportunity to resend emails.
73
+
66
74
  ## License
67
75
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1 +1 @@
1
- //= link_directory ../stylesheets/emailbutler .css
1
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,42 @@
1
+ @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600&display=swap&subset=cyrillic');
2
+
3
+ #emailbutler, #emailbutler body {
4
+ margin: 0;
5
+ padding: 0;
6
+ min-height: 100vh;
7
+ height: 100%;
8
+ }
9
+
10
+ #emailbutler {
11
+ font-family: "Source Sans Pro", "Helvetica", "Open Sans", sans-serif;
12
+ box-sizing: border-box;
13
+
14
+ body {
15
+ position: relative;
16
+ display: flex;
17
+ flex-direction: column;
18
+
19
+ * {
20
+ box-sizing: border-box;
21
+
22
+ &:focus {
23
+ outline: none;
24
+ }
25
+ }
26
+
27
+ p {
28
+ margin: 0;
29
+ }
30
+
31
+ .content {
32
+ flex: 1;
33
+ width: 100%;
34
+ max-width: 90rem;
35
+ margin: 0 auto;
36
+ }
37
+
38
+ h1, h2, h3, h4, h5, h6 {
39
+ margin: 0;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,10 @@
1
+ footer {
2
+ border-top: 1px solid #e5e7eb;
3
+
4
+ .footer-wrapper {
5
+ width: 100%;
6
+ max-width: 90rem;
7
+ margin: 0 auto;
8
+ padding: .5rem 1rem;
9
+ }
10
+ }
@@ -0,0 +1,26 @@
1
+ header {
2
+ border-bottom: 1px solid #e5e7eb;
3
+
4
+ .header-wrapper {
5
+ display: flex;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ width: 100%;
9
+ max-width: 90rem;
10
+ margin: 0 auto;
11
+ padding: .5rem 0;
12
+
13
+ a {
14
+ text-decoration: none;
15
+ color: #000;
16
+ }
17
+ }
18
+ }
19
+
20
+ @media screen and (max-width: 48rem) {
21
+ header {
22
+ .header-wrapper {
23
+ flex-direction: column;
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,97 @@
1
+ #emailbutler {
2
+ #dashboard {
3
+ padding-top: 1rem;
4
+
5
+ .summary {
6
+ list-style: none;
7
+ display: flex;
8
+ flex-direction: row;
9
+ padding: .75rem 1rem;
10
+ background: #e5e7eb;
11
+ border-radius: .125rem;
12
+ margin: 0;
13
+
14
+ li {
15
+ flex: 1;
16
+ text-align: center;
17
+
18
+ a {
19
+ text-decoration: none;
20
+ }
21
+ }
22
+
23
+ .status-summary {
24
+ display: flex;
25
+ flex-direction: column;
26
+ color: #000;
27
+ padding: .5rem 0;
28
+
29
+ &:hover {
30
+ background: #d1d5db;
31
+ }
32
+
33
+ span:nth-of-type(1) {
34
+ margin-bottom: .25rem;
35
+ font-weight: 600;
36
+ }
37
+ }
38
+ }
39
+
40
+ .messages {
41
+ width: 100%;
42
+
43
+ thead {
44
+ tr {
45
+ th {
46
+ text-align: left;
47
+ padding: .125rem .25rem;
48
+ }
49
+ }
50
+ }
51
+
52
+ tbody {
53
+ tr {
54
+ &:nth-of-type(odd) {
55
+ background: #e5e7eb;
56
+ }
57
+
58
+ &:hover {
59
+ background: #d1d5db;
60
+ }
61
+
62
+ td {
63
+ padding: .125rem .25rem;
64
+
65
+ &.actions {
66
+ display: flex;
67
+
68
+ button {
69
+ box-shadow: none;
70
+ border: none;
71
+ cursor: pointer;
72
+ padding: .25rem;
73
+
74
+ &.resend {
75
+ background: #bbf7d0;
76
+ margin-right: .5rem;
77
+
78
+ &:hover {
79
+ background: #86efac;
80
+ }
81
+ }
82
+
83
+ &.destroy {
84
+ background: #fecaca;
85
+
86
+ &:hover {
87
+ background: #fca5a5;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,4 @@
1
+ @import 'emailbutler/core/base';
2
+ @import 'emailbutler/shared/header';
3
+ @import 'emailbutler/shared/footer';
4
+ @import 'emailbutler/views/dashboard';
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ module Ui
5
+ class DashboardController < Emailbutler::UiController
6
+ def index
7
+ @summary = Emailbutler.count_messages_by_status
8
+ end
9
+
10
+ def show
11
+ @messages = Emailbutler.find_messages_by(status: params[:id])
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ module Ui
5
+ class MessagesController < Emailbutler::UiController
6
+ before_action :find_message
7
+
8
+ def update
9
+ Emailbutler.resend_message(@message)
10
+ redirect_to ui_dashboard_index_path
11
+ end
12
+
13
+ def destroy
14
+ Emailbutler.destroy_message(@message)
15
+ redirect_to ui_dashboard_index_path
16
+ end
17
+
18
+ private
19
+
20
+ def find_message
21
+ @message = Emailbutler.find_message_by(uuid: params[:id])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ class UiController < Emailbutler::ApplicationController
5
+ http_basic_authenticate_with name: Emailbutler.configuration.ui_username,
6
+ password: Emailbutler.configuration.ui_password,
7
+ if: -> { basic_auth_enabled? }
8
+
9
+ private
10
+
11
+ def basic_auth_enabled?
12
+ configuration = Emailbutler.configuration
13
+
14
+ return false if configuration.ui_username.blank?
15
+ return false if configuration.ui_password.blank?
16
+
17
+ configuration.ui_secured_environments.include?(Rails.env)
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emailbutler
4
- class WebhooksController < ApplicationController
4
+ class WebhooksController < Emailbutler::ApplicationController
5
5
  skip_before_action :verify_authenticity_token
6
6
 
7
7
  def create
@@ -0,0 +1,16 @@
1
+ <div id="dashboard">
2
+ <ul class="summary">
3
+ <li class="status-summary">
4
+ <span><%= @summary.values.sum %></span>
5
+ <span>Total</span>
6
+ </li>
7
+ <% Emailbutler.adapter.message_class.statuses.each_key do |status| %>
8
+ <li>
9
+ <%= link_to ui_dashboard_path(status), class: 'status-summary' do %>
10
+ <span><%= @summary[status].to_i %></span>
11
+ <span><%= status.capitalize %></span>
12
+ <% end %>
13
+ </li>
14
+ <% end %>
15
+ </ul>
16
+ </div>
@@ -0,0 +1,35 @@
1
+ <div id="dashboard">
2
+ <table class="messages" cellspacing="0">
3
+ <thead>
4
+ <tr>
5
+ <th>ID</th>
6
+ <th>UUID</th>
7
+ <th>Mailer</th>
8
+ <th>Action</th>
9
+ <th>Send to</th>
10
+ <th>Params</th>
11
+ <th></th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <% @messages.each do |message| %>
16
+ <tr>
17
+ <td><%= message.id %></td>
18
+ <td><%= message.uuid %></td>
19
+ <td><%= message.mailer %></td>
20
+ <td><%= message.action %></td>
21
+ <td>
22
+ <% message.send_to.each do |receiver| %>
23
+ <p><%= receiver %></p>
24
+ <% end %>
25
+ </td>
26
+ <td><%= message.params %></td>
27
+ <td class="actions">
28
+ <%= button_to 'Resend', ui_message_path(message.uuid), method: :patch, class: 'resend' %>
29
+ <%= button_to 'Destroy', ui_message_path(message.uuid), method: :delete, class: 'destroy' %>
30
+ </td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ </div>
@@ -0,0 +1,8 @@
1
+ <footer>
2
+ <div class="footer-wrapper flex flex-row justify-between">
3
+ <div>
4
+ </div>
5
+ <div>
6
+ </div>
7
+ </div>
8
+ </footer>
@@ -0,0 +1,9 @@
1
+ <header>
2
+ <div class="header-wrapper flex justify-between items-center">
3
+ <%= link_to ui_dashboard_index_path do %>
4
+ <h1>Emailbutler</h1>
5
+ <% end %>
6
+ <nav>
7
+ </nav>
8
+ </div>
9
+ </header>
@@ -1,12 +1,16 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html id="emailbutler">
3
3
  <head>
4
4
  <title>Emailbutler</title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
- <%= stylesheet_link_tag 'emailbutler/application', media: 'all' %>
7
+ <%= stylesheet_link_tag 'emailbutler', media: 'all' %>
8
8
  </head>
9
- <body>
10
- <%= yield %>
9
+ <body id="emailbutler-body">
10
+ <%= render 'emailbutler/ui/shared/header' %>
11
+ <div class="content">
12
+ <%= yield %>
13
+ </div>
14
+ <%= render 'emailbutler/ui/shared/footer' %>
11
15
  </body>
12
16
  </html>
data/config/routes.rb CHANGED
@@ -2,4 +2,9 @@
2
2
 
3
3
  Emailbutler::Engine.routes.draw do
4
4
  post '/webhooks', to: 'webhooks#create'
5
+
6
+ namespace :ui do
7
+ resources :dashboard, only: %i[index show]
8
+ resources :messages, only: %i[update destroy]
9
+ end
5
10
  end
@@ -5,6 +5,8 @@ require 'active_record'
5
5
  module Emailbutler
6
6
  module Adapters
7
7
  class ActiveRecord
8
+ include Emailbutler::Helpers
9
+
8
10
  # Abstract base class for internal models
9
11
  class Model < ::ActiveRecord::Base
10
12
  self.abstract_class = true
@@ -68,6 +70,29 @@ module Emailbutler
68
70
  rescue ::ActiveRecord::StaleObjectError
69
71
  update_message(message.reload, args)
70
72
  end
73
+
74
+ # Public: Groups messages by status and count them.
75
+ def count_messages_by_status
76
+ @message_class.group(:status).count
77
+ end
78
+
79
+ # Public: Finds messages by args.
80
+ def find_messages_by(args={})
81
+ @message_class.where(args).order(created_at: :desc)
82
+ end
83
+
84
+ # Public: Resends the message.
85
+ def resend_message(message)
86
+ ::ActiveRecord::Base.transaction do
87
+ message.destroy
88
+ resend_message_with_mailer(message)
89
+ end
90
+ end
91
+
92
+ # Public: Destroy the message.
93
+ def destroy_message(message)
94
+ message.destroy
95
+ end
71
96
  end
72
97
  end
73
98
  end
@@ -2,10 +2,17 @@
2
2
 
3
3
  module Emailbutler
4
4
  class Configuration
5
- attr_accessor :adapter
5
+ attr_accessor :adapter, :ui_username, :ui_password, :ui_secured_environments
6
6
 
7
7
  def initialize
8
8
  @adapter = nil
9
+
10
+ # It's required to specify these 3 variables to enable basic auth to UI
11
+ @ui_username = ''
12
+ @ui_password = ''
13
+
14
+ # Secured environments variable must directly contains environment names
15
+ @ui_secured_environments = []
9
16
  end
10
17
  end
11
18
  end
@@ -35,5 +35,25 @@ module Emailbutler
35
35
  def update_message(message, args={})
36
36
  adapter.update_message(message, args)
37
37
  end
38
+
39
+ # Public: Groups messages by status and count them.
40
+ def count_messages_by_status
41
+ adapter.count_messages_by_status
42
+ end
43
+
44
+ # Public: Finds messages by args.
45
+ def find_messages_by(args={})
46
+ adapter.find_messages_by(args)
47
+ end
48
+
49
+ # Public: Resends the message.
50
+ def resend_message(message)
51
+ adapter.resend_message(message)
52
+ end
53
+
54
+ # Public: Destroys the message.
55
+ def destroy_message(message)
56
+ adapter.destroy_message(message)
57
+ end
38
58
  end
39
59
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ module Helpers
5
+ private
6
+
7
+ def resend_message_with_mailer(message)
8
+ params = message.params
9
+
10
+ mailer = message.mailer.constantize
11
+ mailer = mailer.with(serialize(params['mailer_params'])) if params['mailer_params'].present?
12
+ mailer = mailer.method(message.action)
13
+ mailer = params['action_params'] ? mailer.call(*serialize(params['action_params'])) : mailer.call
14
+ mailer.deliver_now
15
+ end
16
+
17
+ def serialize(value, reverse=true)
18
+ return value.map { |element| serialize(element, reverse) } if value.is_a?(Array)
19
+ return value.transform_values { |element| serialize(element, reverse) } if value.is_a?(Hash)
20
+
21
+ reverse ? deserialize_value(value) : serialize_value(value)
22
+ end
23
+
24
+ def serialize_value(value)
25
+ return value.to_global_id.to_s if value.respond_to?(:to_global_id)
26
+
27
+ value
28
+ end
29
+
30
+ def deserialize_value(value)
31
+ return GlobalID::Locator.locate(value) if value.is_a?(String) && value.starts_with?('gid://')
32
+
33
+ value
34
+ end
35
+ end
36
+ end
@@ -4,6 +4,7 @@ module Emailbutler
4
4
  module Mailers
5
5
  module Helpers
6
6
  extend ActiveSupport::Concern
7
+ include Emailbutler::Helpers
7
8
 
8
9
  included do
9
10
  after_action :save_emailbutler_message
@@ -21,18 +22,10 @@ module Emailbutler
21
22
  @message = Emailbutler.build_message(
22
23
  mailer: self.class.to_s,
23
24
  action: action_name,
24
- params: serialize_params(mailer_params: params, action_params: args[1..])
25
+ params: serialize({ mailer_params: params, action_params: args[1..] }, false)
25
26
  )
26
27
  end
27
28
 
28
- def serialize_params(value)
29
- return value.map { |element| serialize_params(element) } if value.is_a?(Array)
30
- return value.transform_values { |element| serialize_params(element) } if value.is_a?(Hash)
31
- return value.to_global_id.to_s if value.respond_to?(:to_global_id)
32
-
33
- value
34
- end
35
-
36
29
  def save_emailbutler_message
37
30
  Emailbutler.set_message_attribute(@message, :send_to, mail.to)
38
31
  Emailbutler.save_message(@message)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emailbutler
4
- VERSION = '0.2.3'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/emailbutler.rb CHANGED
@@ -7,6 +7,7 @@ require 'emailbutler/engine'
7
7
  require 'emailbutler/configuration'
8
8
  require 'emailbutler/dsl'
9
9
  require 'emailbutler/webhooks/receiver'
10
+ require 'emailbutler/helpers'
10
11
  require 'emailbutler/mailers/helpers'
11
12
 
12
13
  module Emailbutler
@@ -42,5 +43,5 @@ module Emailbutler
42
43
  # Public: All the methods delegated to instance. These should match the interface of Emailbutler::DSL.
43
44
  def_delegators :instance,
44
45
  :adapter, :build_message, :set_message_attribute, :save_message, :find_message_by,
45
- :update_message
46
+ :update_message, :count_messages_by_status, :find_messages_by, :resend_message, :destroy_message
46
47
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emailbutler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdanov Anton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-16 00:00:00.000000000 Z
11
+ date: 2022-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 6.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: sprockets-rails
28
+ name: sass-rails
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 3.4.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 3.4.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -161,9 +161,20 @@ files:
161
161
  - README.md
162
162
  - Rakefile
163
163
  - app/assets/config/emailbutler_manifest.js
164
- - app/assets/stylesheets/emailbutler/application.css
164
+ - app/assets/stylesheets/emailbutler.scss
165
+ - app/assets/stylesheets/emailbutler/core/base.scss
166
+ - app/assets/stylesheets/emailbutler/shared/footer.scss
167
+ - app/assets/stylesheets/emailbutler/shared/header.scss
168
+ - app/assets/stylesheets/emailbutler/views/dashboard.scss
165
169
  - app/controllers/emailbutler/application_controller.rb
170
+ - app/controllers/emailbutler/ui/dashboard_controller.rb
171
+ - app/controllers/emailbutler/ui/messages_controller.rb
172
+ - app/controllers/emailbutler/ui_controller.rb
166
173
  - app/controllers/emailbutler/webhooks_controller.rb
174
+ - app/views/emailbutler/ui/dashboard/index.html.erb
175
+ - app/views/emailbutler/ui/dashboard/show.html.erb
176
+ - app/views/emailbutler/ui/shared/_footer.html.erb
177
+ - app/views/emailbutler/ui/shared/_header.html.erb
167
178
  - app/views/layouts/emailbutler/application.html.erb
168
179
  - config/routes.rb
169
180
  - db/migrate/20220916162720_create_emailbutler_tables.rb
@@ -172,6 +183,7 @@ files:
172
183
  - lib/emailbutler/configuration.rb
173
184
  - lib/emailbutler/dsl.rb
174
185
  - lib/emailbutler/engine.rb
186
+ - lib/emailbutler/helpers.rb
175
187
  - lib/emailbutler/mailers/helpers.rb
176
188
  - lib/emailbutler/version.rb
177
189
  - lib/emailbutler/webhooks/mappers/sendgrid.rb
@@ -1,15 +0,0 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */