emailbutler 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07e27b9787bb6f85389d9d6628041cec10a2c422c5806b216c1e8a597a6b65f2
4
+ data.tar.gz: 1728e696da05d221df0c990f2248c0bd5e8753f2b8b3569905b054ed624845f5
5
+ SHA512:
6
+ metadata.gz: 34af5717bd9e42499faa83988388cc65b6ad87e33ff7d31912e120a7e69e7d381c92ad4a4b6e926fe8c845505311bc046114caa00fb9debaceba7f2d5ceed1be
7
+ data.tar.gz: f8d56d0c8d13d74f90c32ea95a9edd6eaa3fd064eadb343233f5678389059d4eebcb74e9e1fa3e15c583fccff639371d4389b95511d51875e26c5316667c8618
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Bogdanov Anton
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.md ADDED
@@ -0,0 +1,67 @@
1
+ # Emailbutler
2
+ Simple email tracker for Ruby on Rails applications.
3
+ Emailbutler allows you to track delivery status of emails sent by your app.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+ ```ruby
9
+ gem 'emailbutler'
10
+ ```
11
+
12
+ And then execute:
13
+ ```bash
14
+ $ bundle install
15
+ $ rails g emailbutler:active_record
16
+ $ rails db:migrate
17
+ ```
18
+
19
+ ## Engine configuration
20
+
21
+ ### Initializer
22
+
23
+ Add configuration line to config/initializers/emailbutler.rb:
24
+
25
+ #### ActiveRecord
26
+
27
+ ```ruby
28
+ require 'emailbutler/adapters/active_record'
29
+
30
+ Emailbutler.configure do |config|
31
+ config.adapter = Emailbutler::Adapters::ActiveRecord.new
32
+ end
33
+ ```
34
+
35
+ ### Routes
36
+
37
+ Add this line to config/routes.rb
38
+ ```ruby
39
+ mount Emailbutler::Engine => '/emailbutler'
40
+ ```
41
+
42
+ ### Mailers
43
+
44
+ Update you application mailer
45
+ ```ruby
46
+ class ApplicationMailer < ActionMailer::Base
47
+ include Emailbutler::Mailers::Helpers
48
+ end
49
+ ```
50
+
51
+ ### Email provider webhooks settings
52
+
53
+ #### Sendgrid
54
+
55
+ - go to [Mail settings](https://app.sendgrid.com/settings/mail_settings),
56
+ - turn on Event Webhook,
57
+ - in the HTTP POST URL field, paste the URL to webhook controller of your app,
58
+ - select all deliverability data,
59
+ - save settings.
60
+
61
+ ## Usage
62
+
63
+ 1. Each event with sending email will create new record with message params in database.
64
+ 2. Each webhook event will update status of message in database.
65
+
66
+ ## License
67
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/emailbutler .css
@@ -0,0 +1,15 @@
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
+ */
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ class WebhooksController < ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ def create
8
+ ::Emailbutler::Webhooks::Receiver.call(
9
+ user_agent: request.headers['HTTP_USER_AGENT'],
10
+ payload: receiver_params.to_h
11
+ )
12
+
13
+ head :ok
14
+ end
15
+
16
+ private
17
+
18
+ def receiver_params
19
+ params.permit('_json' => %w[smtp-id event])
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Emailbutler</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <%= stylesheet_link_tag 'emailbutler/application', media: 'all' %>
8
+ </head>
9
+ <body>
10
+ <%= yield %>
11
+ </body>
12
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Emailbutler::Engine.routes.draw do
4
+ post '/webhooks', to: 'webhooks#create'
5
+ end
@@ -0,0 +1,28 @@
1
+ class CreateEmailbutlerTables < ActiveRecord::Migration[7.0]
2
+ def self.up
3
+ enable_extension 'pgcrypto' unless extensions.include?('pgcrypto')
4
+ enable_extension 'uuid-ossp' unless extensions.include?('uuid-ossp')
5
+
6
+ create_table :emailbutler_messages do |t|
7
+ t.uuid :uuid, null: false, default: ''
8
+ t.string :mailer, null: false
9
+ t.string :action, null: false
10
+ t.jsonb :params, null: false, default: {}
11
+ t.string :send_to, array: true
12
+ t.timestamps
13
+ end
14
+ add_index :emailbutler_messages, :uuid, unique: true
15
+
16
+ create_table :emailbutler_events do |t|
17
+ t.integer :emailbutler_message_id, null: false, index: true
18
+ t.integer :status, null: false
19
+ t.datetime :timestamp
20
+ t.timestamps
21
+ end
22
+ end
23
+
24
+ def self.down
25
+ drop_table :emailbutler_messages
26
+ drop_table :emailbutler_events
27
+ end
28
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Emailbutler
6
+ module Adapters
7
+ class ActiveRecord
8
+ # Abstract base class for internal models
9
+ class Model < ::ActiveRecord::Base
10
+ self.abstract_class = true
11
+ end
12
+
13
+ class Emailbutler::Message < Model
14
+ self.table_name = :emailbutler_messages
15
+
16
+ REJECTED = 'rejected'
17
+ PROCESSED = 'processed'
18
+ FAILED = 'failed'
19
+ DELIVERED = 'delivered'
20
+
21
+ has_many :emailbutler_events,
22
+ class_name: 'Emailbutler::Event', dependent: :destroy, foreign_key: :emailbutler_message_id
23
+
24
+ enum status: { REJECTED => 0, PROCESSED => 1, FAILED => 2, DELIVERED => 3 }
25
+
26
+ after_initialize :generate_uuid
27
+
28
+ private
29
+
30
+ def generate_uuid
31
+ self.uuid ||= SecureRandom.uuid
32
+ end
33
+ end
34
+
35
+ class Emailbutler::Event < Model
36
+ self.table_name = :emailbutler_events
37
+
38
+ belongs_to :emailbutler_message, class_name: 'Emailbutler::Message'
39
+ end
40
+
41
+ # Public: The name of the adapter.
42
+ attr_reader :name, :message_class, :event_class
43
+
44
+ # Public: Initialize a new ActiveRecord adapter instance.
45
+ #
46
+ # name - The Symbol name for this adapter. Optional (default :active_record)
47
+ # message_class - The AR class responsible for the messages table.
48
+ # event_class - The AR class responsible for the messages events table.
49
+ def initialize(options={})
50
+ @name = options.fetch(:name, :active_record)
51
+ @message_class = options.fetch(:message_class) { Emailbutler::Message }
52
+ @event_class = options.fetch(:event_class) { Emailbutler::Event }
53
+ end
54
+
55
+ # Public: Builds a message.
56
+ def build_message(args={})
57
+ @message_class.new(args)
58
+ end
59
+
60
+ # Public: Sets attribute with value for the message.
61
+ def set_message_attribute(message, attribute, value)
62
+ message[attribute] = value
63
+ end
64
+
65
+ # Public: Saves the message.
66
+ def save_message(message)
67
+ message.save
68
+ end
69
+
70
+ # Public: Finds message by args.
71
+ def find_message_by(args={})
72
+ @message_class.find_by(args)
73
+ end
74
+
75
+ # Public: Creates a message event.
76
+ def create_message_event(message, args={})
77
+ @event_class.create(args.merge(emailbutler_message_id: message.id))
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ class Configuration
5
+ attr_accessor :adapter
6
+
7
+ def initialize
8
+ @adapter = nil
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ class DSL
5
+ attr_reader :adapter
6
+
7
+ # Public: Returns a new instance of the DSL.
8
+ #
9
+ # adapter - The adapter that this DSL instance should use.
10
+ def initialize(adapter)
11
+ @adapter = adapter
12
+ end
13
+
14
+ # Public: Build a message.
15
+ def build_message(args={})
16
+ adapter.build_message(args)
17
+ end
18
+
19
+ # Public: Sets attribute with value for the message.
20
+ def set_message_attribute(*args)
21
+ adapter.set_message_attribute(*args)
22
+ end
23
+
24
+ # Public: Saves the message.
25
+ def save_message(message)
26
+ adapter.save_message(message)
27
+ end
28
+
29
+ # Public: Finds message by args.
30
+ def find_message_by(args={})
31
+ adapter.find_message_by(args)
32
+ end
33
+
34
+ # Public: Creates a message event.
35
+ def create_message_event(message, args={})
36
+ adapter.create_message_event(message, args)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Emailbutler
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ g.fixture_replacement :factory_bot
10
+ g.factory_bot dir: 'spec/factories'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ module Mailers
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ after_action :save_emailbutler_message
10
+ end
11
+
12
+ private
13
+
14
+ def process_action(*args)
15
+ build_emailbutler_message(args)
16
+
17
+ super
18
+ end
19
+
20
+ def build_emailbutler_message(args)
21
+ @message = Emailbutler.build_message(
22
+ mailer: self.class.to_s,
23
+ action: action_name,
24
+ params: serialize_params(mailer_params: params, action_params: args[1..])
25
+ )
26
+ end
27
+
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
+ def save_emailbutler_message
37
+ Emailbutler.set_message_attribute(@message, :send_to, mail.to)
38
+ Emailbutler.save_message(@message)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ VERSION = '0.2.2'
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ module Webhooks
5
+ module Mappers
6
+ class Sendgrid
7
+ DELIVERABILITY_MAPPER = {
8
+ 'processed' => 'processed',
9
+ 'delivered' => 'delivered',
10
+ 'deferred' => 'failed',
11
+ 'bounce' => 'failed',
12
+ 'dropped' => 'failed'
13
+ }.freeze
14
+
15
+ def self.call(args={})
16
+ new.call(**args)
17
+ end
18
+
19
+ def call(payload:)
20
+ payload['_json'].filter_map { |message|
21
+ message_uuid = message['smtp-id']
22
+ status = DELIVERABILITY_MAPPER[message['event']]
23
+ next unless message_uuid || status
24
+
25
+ {
26
+ message_uuid: message_uuid,
27
+ status: status,
28
+ timestamp: Time.at(message['timestamp']).utc.to_datetime
29
+ }
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'emailbutler/webhooks/mappers/sendgrid'
4
+
5
+ module Emailbutler
6
+ module Webhooks
7
+ class Receiver
8
+ SENDGRID_USER_AGENT = 'SendGrid Event API'
9
+
10
+ def self.call(args={})
11
+ new.call(**args)
12
+ end
13
+
14
+ def call(user_agent:, payload:)
15
+ select_mapper(user_agent)
16
+ .call(payload: payload)
17
+ .each { |event|
18
+ message = Emailbutler.find_message_by(uuid: event.delete(:message_uuid))
19
+ Emailbutler.create_message_event(message, event)
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def select_mapper(user_agent)
26
+ case user_agent
27
+ when SENDGRID_USER_AGENT then Emailbutler::Webhooks::Mappers::Sendgrid
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'emailbutler/version'
6
+ require 'emailbutler/engine'
7
+ require 'emailbutler/configuration'
8
+ require 'emailbutler/dsl'
9
+ require 'emailbutler/webhooks/receiver'
10
+ require 'emailbutler/mailers/helpers'
11
+
12
+ module Emailbutler
13
+ extend self
14
+ extend Forwardable
15
+
16
+ # Public: Given an adapter returns a handy DSL to all the emailbutler goodness.
17
+ def new(adapter)
18
+ DSL.new(adapter)
19
+ end
20
+
21
+ # Public: Configure emailbutler.
22
+ #
23
+ # Emailbutler.configure do |config|
24
+ # config.adapter = Emailbutler::Adapters::ActiveRecord.new
25
+ # end
26
+ #
27
+ def configure
28
+ yield configuration
29
+ end
30
+
31
+ # Public: Returns Emailbutler::Configuration instance.
32
+ def configuration
33
+ @configuration ||= Configuration.new
34
+ end
35
+
36
+ # Public: Default per thread emailbutler instance if configured.
37
+ # Returns Emailbutler::DSL instance.
38
+ def instance
39
+ Thread.current[:emailbutler_instance] ||= new(configuration.adapter)
40
+ end
41
+
42
+ # Public: All the methods delegated to instance. These should match the interface of Emailbutler::DSL.
43
+ def_delegators :instance,
44
+ :adapter, :build_message, :set_message_attribute, :save_message, :find_message_by,
45
+ :create_message_event
46
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+
5
+ module Emailbutler
6
+ module Generators
7
+ class ActiveRecordGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+ desc 'Generates migration for emailbutler tables'
10
+
11
+ source_paths << File.join(File.dirname(__FILE__), 'templates')
12
+
13
+ def self.next_migration_number(dirname)
14
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
15
+ end
16
+
17
+ def self.migration_version
18
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if requires_migration_number?
19
+ end
20
+
21
+ def self.requires_migration_number?
22
+ Rails::VERSION::MAJOR.to_i >= 5
23
+ end
24
+
25
+ def create_migration_file
26
+ options = {
27
+ migration_version: migration_version
28
+ }
29
+ migration_template 'migration.erb', 'db/migrate/create_emailbutler_tables.rb', options
30
+ end
31
+
32
+ def migration_version
33
+ self.class.migration_version
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ class CreateEmailbutlerTables < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ enable_extension 'pgcrypto' unless extensions.include?('pgcrypto')
4
+ enable_extension 'uuid-ossp' unless extensions.include?('uuid-ossp')
5
+
6
+ create_table :emailbutler_messages do |t|
7
+ t.uuid :uuid, null: false, default: ''
8
+ t.string :mailer, null: false
9
+ t.string :action, null: false
10
+ t.jsonb :params, null: false, default: {}
11
+ t.string :send_to, array: true
12
+ t.timestamps
13
+ end
14
+ add_index :emailbutler_messages, :uuid, unique: true
15
+
16
+ create_table :emailbutler_events do |t|
17
+ t.integer :emailbutler_message_id, null: false, index: true
18
+ t.integer :status, null: false
19
+ t.datetime :timestamp
20
+ t.timestamps
21
+ end
22
+ end
23
+
24
+ def self.down
25
+ drop_table :emailbutler_messages
26
+ drop_table :emailbutler_events
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :emailbutler do
5
+ # # Task goes here
6
+ # end
metadata ADDED
@@ -0,0 +1,208 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: emailbutler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Bogdanov Anton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sprockets-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.4.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.4.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: puma
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-performance
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.8'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.8'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.6'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.6'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.0'
153
+ description: Write a longer description or delete this line.
154
+ email:
155
+ - kortirso@gmail.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - MIT-LICENSE
161
+ - README.md
162
+ - Rakefile
163
+ - app/assets/config/emailbutler_manifest.js
164
+ - app/assets/stylesheets/emailbutler/application.css
165
+ - app/controllers/emailbutler/application_controller.rb
166
+ - app/controllers/emailbutler/webhooks_controller.rb
167
+ - app/views/layouts/emailbutler/application.html.erb
168
+ - config/routes.rb
169
+ - db/migrate/20220908180903_create_emailbutler_tables.rb
170
+ - lib/emailbutler.rb
171
+ - lib/emailbutler/adapters/active_record.rb
172
+ - lib/emailbutler/configuration.rb
173
+ - lib/emailbutler/dsl.rb
174
+ - lib/emailbutler/engine.rb
175
+ - lib/emailbutler/mailers/helpers.rb
176
+ - lib/emailbutler/version.rb
177
+ - lib/emailbutler/webhooks/mappers/sendgrid.rb
178
+ - lib/emailbutler/webhooks/receiver.rb
179
+ - lib/generators/emailbutler/active_record_generator.rb
180
+ - lib/generators/emailbutler/templates/migration.erb
181
+ - lib/tasks/emailbutler_tasks.rake
182
+ homepage: https://github.com/kortirso/emailbutler
183
+ licenses:
184
+ - MIT
185
+ metadata:
186
+ homepage_uri: https://github.com/kortirso/emailbutler
187
+ source_code_uri: https://github.com/kortirso/emailbutler
188
+ changelog_uri: https://github.com/kortirso/emailbutler/blob/master/CHANGELOG.md
189
+ post_install_message:
190
+ rdoc_options: []
191
+ require_paths:
192
+ - lib
193
+ required_ruby_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '2.7'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubygems_version: 3.3.7
205
+ signing_key:
206
+ specification_version: 4
207
+ summary: Write a short summary, because RubyGems requires one.
208
+ test_files: []