emailbutler 0.7.6 → 0.8.1

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: d7a1a7e4cc9dad6d9958abcb9e832e85d6c74a2c44a71c63bc42e7e3be59debc
4
- data.tar.gz: 490337b5946df84317357c9f22d9d189d0434ff215726533b3dc934479b99eb3
3
+ metadata.gz: e06b545b530403687a7844d31538ea50de73cffbc8f225ab5a849fe13d7678b0
4
+ data.tar.gz: c6fdc2b0fcf1afb430c0b1c80b2b1b89f9273007a54f97b3c96ddbad44a1bef2
5
5
  SHA512:
6
- metadata.gz: 53580acc1672780cc1b5eca024fc68868cf8ab3f3d2cadc6a02f4edb70787ba1c87b583a04703a1bca718ecc9ef80254d6f6a5d288824cb7a05247ef38c30a50
7
- data.tar.gz: 7565afd9b680a42d2dee0b500b39773e696b2e80a53d7eb7e1f8ddb3f686dca7ef65af0548471416d18cbe2cb4bfd68e6b07128007439341a6e611a29d2e73a8
6
+ metadata.gz: 71919a2533d61eb98ea21ef74266c261f1569acaf8ae2193fa30eef083b2ae06c659ec38af299d86d67d5b09f06ed9391b7fd63ba143687bd2592d071bbac916
7
+ data.tar.gz: ea46055aa238b71c0c237138caf69e2ed8b4580d0956e95274e3762071b7ae72e273ddf6fc43147634d0723d8b5966b31b8a25d89b8ed71901937fa7c1f9e12a
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Emailbutler
2
2
  Simple email tracker for Ruby on Rails applications.
3
- Emailbutler allows you to track delivery status of emails sent by your app through Sendgrid and/or SMTP2GO.
3
+ Emailbutler allows you to track delivery status of emails sent by your app through Sendgrid, SMTP2GO, Resend.
4
4
 
5
5
  There are situations when you need to check whether a certain letter or certain type of letters was successfully sent from the application, and through the UI of some providers you can try to find such a letter by the recipient or the subject of the letter, but sometimes it's not enough.
6
6
 
@@ -27,16 +27,17 @@ $ rails db:migrate
27
27
 
28
28
  Add configuration line to config/initializers/emailbutler.rb:
29
29
 
30
- #### ActiveRecord
30
+ #### For ActiveRecord
31
31
 
32
32
  ```ruby
33
33
  require 'emailbutler/adapters/active_record'
34
34
 
35
35
  Emailbutler.configure do |config|
36
- config.adapter = Emailbutler::Adapters::ActiveRecord.new
37
- config.ui_username = 'username'
38
- config.ui_password = 'password'
39
- config.ui_secured_environments = ['production']
36
+ config.adapter = Emailbutler::Adapters::ActiveRecord.new # required
37
+ config.providers = %w[sendgrid smtp2go resend] # optional
38
+ config.ui_username = 'username' # optional
39
+ config.ui_password = 'password' # optional
40
+ config.ui_secured_environments = ['production'] # optional
40
41
  end
41
42
  ```
42
43
 
@@ -59,9 +60,9 @@ class SendgridController < ApplicationController
59
60
  def create
60
61
  ... you can add some logic here
61
62
 
62
- ::Emailbutler::Webhooks::Receiver.call(
63
- user_agent: request.headers['HTTP_USER_AGENT'],
64
- payload: receiver_params.to_h
63
+ Emailbutler::Container.resolve(:webhooks_receiver).call(
64
+ mapper: Emailbutler::Container.resolve(:sendgrid_mapper),
65
+ payload: receiver_params
65
66
  )
66
67
 
67
68
  head :ok
@@ -70,10 +71,7 @@ class SendgridController < ApplicationController
70
71
  private
71
72
 
72
73
  def receiver_params
73
- params.permit(
74
- 'event', 'sendtime', 'message-id',
75
- '_json' => %w[event timestamp smtp-id sg_message_id]
76
- )
74
+ params.permit('event', 'sendtime', 'message-id').to_h
77
75
  end
78
76
  end
79
77
  ```
@@ -106,7 +104,7 @@ end
106
104
 
107
105
  - go to [Mail settings](https://app.sendgrid.com/settings/mail_settings),
108
106
  - turn on Event Webhook,
109
- - in the HTTP POST URL field, paste the URL to webhook controller of your app,
107
+ - in the HTTP POST URL field, paste the URL to webhook controller of your app (host/emailbutler/webhooks/sendgrid),
110
108
  - select all deliverability data,
111
109
  - save settings.
112
110
 
@@ -114,7 +112,15 @@ end
114
112
 
115
113
  - go to [Mail settings](https://app-eu.smtp2go.com/settings/webhooks),
116
114
  - turn on Webhooks,
117
- - in the HTTP POST URL field, paste the URL to webhook controller of your app,
115
+ - in the HTTP POST URL field, paste the URL to webhook controller of your app (host/emailbutler/webhooks/smtp2go),
116
+ - select all deliverability data,
117
+ - save settings.
118
+
119
+ #### Resend
120
+
121
+ - go to [Mail settings](https://resend.com/webhooks),
122
+ - add Webhook,
123
+ - in the Endpoint URL field, paste the URL to webhook controller of your app (host/emailbutler/webhooks/resend),
118
124
  - select all deliverability data,
119
125
  - save settings.
120
126
 
@@ -2,12 +2,26 @@
2
2
 
3
3
  module Emailbutler
4
4
  class WebhooksController < Emailbutler::ApplicationController
5
+ # deprecated constants
6
+ SENDGRID_USER_AGENT = 'SendGrid Event API'
7
+ SMTP2GO_USER_AGENT = 'Go-http-client/1.1'
8
+
5
9
  skip_before_action :verify_authenticity_token
10
+ before_action :validate_provider, only: %i[create]
11
+
12
+ def create_deprecated
13
+ Emailbutler::Container.resolve(:webhooks_receiver).call(
14
+ mapper: receiver_mapper_deprecated(request.headers['HTTP_USER_AGENT']),
15
+ payload: mapper_params_deprecated.to_h
16
+ )
17
+
18
+ head :ok
19
+ end
6
20
 
7
21
  def create
8
- ::Emailbutler::Webhooks::Receiver.call(
9
- user_agent: request.headers['HTTP_USER_AGENT'],
10
- payload: receiver_params.to_h
22
+ Emailbutler::Container.resolve(:webhooks_receiver).call(
23
+ mapper: receiver_mapper,
24
+ payload: mapper_params.to_h
11
25
  )
12
26
 
13
27
  head :ok
@@ -15,7 +29,46 @@ module Emailbutler
15
29
 
16
30
  private
17
31
 
18
- def receiver_params
32
+ def validate_provider
33
+ head :ok if Emailbutler.configuration.providers.exclude?(params[:provider])
34
+ end
35
+
36
+ def receiver_mapper
37
+ case params[:provider]
38
+ when 'sendgrid' then Emailbutler::Container.resolve(:sendgrid_mapper)
39
+ when 'smtp2go' then Emailbutler::Container.resolve(:smtp2go_mapper)
40
+ when 'resend' then Emailbutler::Container.resolve(:resend_mapper)
41
+ end
42
+ end
43
+
44
+ def mapper_params
45
+ case params[:provider]
46
+ when 'sendgrid' then sendgrid_params
47
+ when 'smtp2go' then smtp2go_params
48
+ when 'resend' then resend_params
49
+ end
50
+ end
51
+
52
+ def sendgrid_params
53
+ params.permit('_json' => %w[smtp-id event timestamp sg_message_id])
54
+ end
55
+
56
+ def smtp2go_params
57
+ params.permit('event', 'sendtime', 'message-id')
58
+ end
59
+
60
+ def resend_params
61
+ params.permit('type', 'created_at', 'data' => %w[email_id])
62
+ end
63
+
64
+ def receiver_mapper_deprecated(user_agent)
65
+ case user_agent
66
+ when SENDGRID_USER_AGENT then Emailbutler::Container.resolve(:sendgrid_mapper)
67
+ when SMTP2GO_USER_AGENT then Emailbutler::Container.resolve(:smtp2go_mapper)
68
+ end
69
+ end
70
+
71
+ def mapper_params_deprecated
19
72
  params.permit(
20
73
  'event', 'sendtime', 'message-id',
21
74
  '_json' => %w[event timestamp smtp-id sg_message_id]
data/config/routes.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Emailbutler::Engine.routes.draw do
4
- post '/webhooks', to: 'webhooks#create'
4
+ post '/webhooks', to: 'webhooks#create_deprecated'
5
+ post '/webhooks/:provider', to: 'webhooks#create'
5
6
 
6
7
  resources :ui, only: %i[index show]
7
8
  namespace :ui do
@@ -2,16 +2,65 @@
2
2
 
3
3
  module Emailbutler
4
4
  class Configuration
5
- attr_accessor :adapter, :ui_username, :ui_password, :ui_secured_environments
5
+ InitializeError = Class.new(StandardError)
6
+
7
+ AVAILABLE_ADAPTERS = %w[
8
+ Emailbutler::Adapters::ActiveRecord
9
+ ].freeze
10
+ AVAILABLE_PROVIDERS = %w[sendgrid smtp2go resend].freeze
11
+
12
+ attr_accessor :adapter, :providers, :ui_username, :ui_password, :ui_secured_environments
6
13
 
7
14
  def initialize
8
15
  @adapter = nil
16
+ @providers = []
9
17
 
10
18
  # It's required to specify these 3 variables to enable basic auth to UI
11
- @ui_username = ''
12
- @ui_password = ''
19
+ @ui_username = nil
20
+ @ui_password = nil
13
21
  # Secured environments variable must directly contains environment names
14
22
  @ui_secured_environments = []
15
23
  end
24
+
25
+ def validate
26
+ validate_adapter
27
+ validate_providers
28
+ validate_username
29
+ validate_password
30
+ validate_secured_environments
31
+ end
32
+
33
+ private
34
+
35
+ def validate_adapter
36
+ raise InitializeError, 'Adapter must be present' if adapter.nil?
37
+ raise InitializeError, 'Invalid adapter' if AVAILABLE_ADAPTERS.exclude?(adapter.class.name)
38
+ end
39
+
40
+ def validate_providers
41
+ raise InitializeError, 'Providers list must be array' unless providers.is_a?(Array)
42
+
43
+ return unless providers.any? { |provider| AVAILABLE_PROVIDERS.exclude?(provider) }
44
+
45
+ raise InitializeError, 'Providers list contain invalid element'
46
+ end
47
+
48
+ def validate_username
49
+ return if ui_username.blank?
50
+ return if ui_username.is_a?(String)
51
+
52
+ raise InitializeError, 'Username must be nil or filled string'
53
+ end
54
+
55
+ def validate_password
56
+ return if ui_password.blank?
57
+ return if ui_password.is_a?(String)
58
+
59
+ raise InitializeError, 'Password must be nil or filled string'
60
+ end
61
+
62
+ def validate_secured_environments
63
+ raise InitializeError, 'Environments list must be array' unless ui_secured_environments.is_a?(Array)
64
+ end
16
65
  end
17
66
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/container'
4
+ require 'emailbutler/webhooks/mappers/sendgrid'
5
+ require 'emailbutler/webhooks/mappers/smtp2go'
6
+ require 'emailbutler/webhooks/mappers/resend'
7
+ require 'emailbutler/webhooks/receiver'
8
+
9
+ module Emailbutler
10
+ class Container
11
+ extend Dry::Container::Mixin
12
+
13
+ DEFAULT_OPTIONS = { memoize: true }.freeze
14
+
15
+ class << self
16
+ def register(key)
17
+ super(key, DEFAULT_OPTIONS)
18
+ end
19
+ end
20
+
21
+ # webhook mappers
22
+ register(:sendgrid_mapper) { Emailbutler::Webhooks::Mappers::Sendgrid.new }
23
+ register(:smtp2go_mapper) { Emailbutler::Webhooks::Mappers::Smtp2Go.new }
24
+ register(:resend_mapper) { Emailbutler::Webhooks::Mappers::Resend.new }
25
+
26
+ # webhook receiver
27
+ register(:webhooks_receiver) { Emailbutler::Webhooks::Receiver.new }
28
+ end
29
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Emailbutler
4
- VERSION = '0.7.6'
4
+ VERSION = '0.8.1'
5
5
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emailbutler
4
+ module Webhooks
5
+ module Mappers
6
+ class Resend
7
+ DELIVERABILITY_MAPPER = {
8
+ 'email.sent' => 'processed',
9
+ 'email.delivered' => 'delivered',
10
+ 'email.opened' => 'delivered',
11
+ 'email.clicked' => 'delivered',
12
+ 'email.delivered_delayed' => 'failed',
13
+ 'email.bounced' => 'failed',
14
+ 'email.complained' => 'failed'
15
+ }.freeze
16
+
17
+ def call(payload:)
18
+ payload.stringify_keys!
19
+ # message-id contains data like <uuid>
20
+ message_uuid = payload.dig('data', 'email_id')
21
+ status = DELIVERABILITY_MAPPER[payload['type']]
22
+ return [] if message_uuid.nil? || status.nil?
23
+
24
+ [
25
+ {
26
+ message_uuid: message_uuid,
27
+ status: status,
28
+ timestamp: payload['created_at'] ? DateTime.parse(payload['created_at']).utc : nil
29
+ }
30
+ ]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -15,10 +15,6 @@ module Emailbutler
15
15
  'spamreport' => 'failed'
16
16
  }.freeze
17
17
 
18
- def self.call(...)
19
- new.call(...)
20
- end
21
-
22
18
  def call(payload:)
23
19
  payload['_json'].filter_map { |message|
24
20
  message.stringify_keys!
@@ -14,10 +14,6 @@ module Emailbutler
14
14
  'spam' => 'failed'
15
15
  }.freeze
16
16
 
17
- def self.call(...)
18
- new.call(...)
19
- end
20
-
21
17
  def call(payload:)
22
18
  payload.stringify_keys!
23
19
  # message-id contains data like <uuid>
@@ -1,25 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'emailbutler/webhooks/mappers/sendgrid'
4
- require 'emailbutler/webhooks/mappers/smtp2go'
5
-
6
3
  module Emailbutler
7
4
  module Webhooks
8
5
  class Receiver
9
- SENDGRID_USER_AGENT = 'SendGrid Event API'
10
- SMTP2GO_USER_AGENT = 'Go-http-client/1.1'
11
-
12
- RECEIVERS_MAPPER = {
13
- 'SendGrid Event API' => Emailbutler::Webhooks::Mappers::Sendgrid,
14
- 'Go-http-client/1.1' => Emailbutler::Webhooks::Mappers::Smtp2Go
15
- }.freeze
16
-
17
- def self.call(...)
18
- new.call(...)
19
- end
20
-
21
- def call(user_agent:, payload:)
22
- mapper = RECEIVERS_MAPPER[user_agent]
6
+ def call(mapper:, payload:)
23
7
  return unless mapper
24
8
 
25
9
  mapper
data/lib/emailbutler.rb CHANGED
@@ -6,38 +6,49 @@ require 'emailbutler/version'
6
6
  require 'emailbutler/engine'
7
7
  require 'emailbutler/configuration'
8
8
  require 'emailbutler/dsl'
9
- require 'emailbutler/webhooks/receiver'
10
9
  require 'emailbutler/helpers'
11
10
  require 'emailbutler/mailers/helpers'
11
+ require 'emailbutler/container'
12
12
 
13
13
  module Emailbutler
14
14
  extend self
15
15
  extend Forwardable
16
16
 
17
- # Public: Given an adapter returns a handy DSL to all the emailbutler goodness.
18
- def new(adapter)
19
- DSL.new(adapter)
20
- end
21
-
22
17
  # Public: Configure emailbutler.
23
18
  #
19
+ # require 'emailbutler/adapters/active_record'
20
+ #
24
21
  # Emailbutler.configure do |config|
25
22
  # config.adapter = Emailbutler::Adapters::ActiveRecord.new
23
+ # config.providers = %i[sendgrid]
26
24
  # end
27
25
  #
28
26
  def configure
27
+ ActiveSupport::Deprecation.new.warn(
28
+ 'Webhook endpoint should receive provider name in params. ' \
29
+ 'Please update webhook url in settings of your SMTP provider.' \
30
+ 'Webhook without provider in params will be removed soon.'
31
+ )
32
+
29
33
  yield configuration
34
+
35
+ configuration.validate
30
36
  end
31
37
 
32
38
  # Public: Returns Emailbutler::Configuration instance.
33
39
  def configuration
34
- @configuration ||= Configuration.new
40
+ return Emailbutler::Container.resolve(:configuration) if Emailbutler::Container.key?(:configuration)
41
+
42
+ Emailbutler::Container.register(:configuration) { Configuration.new }
43
+ Emailbutler::Container.resolve(:configuration)
35
44
  end
36
45
 
37
- # Public: Default per thread emailbutler instance if configured.
38
- # Returns Emailbutler::DSL instance.
46
+ # Public: Returns Emailbutler::DSL instance.
39
47
  def instance
40
- Thread.current[:emailbutler_instance] ||= new(configuration.adapter)
48
+ return Emailbutler::Container.resolve(:instance) if Emailbutler::Container.key?(:instance)
49
+
50
+ Emailbutler::Container.register(:instance) { DSL.new(configuration.adapter) }
51
+ Emailbutler::Container.resolve(:instance)
41
52
  end
42
53
 
43
54
  # Public: All the methods delegated to instance. These should match the interface of Emailbutler::DSL.
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emailbutler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.6
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdanov Anton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-19 00:00:00.000000000 Z
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-container
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.11.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.11.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: pagy
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -196,11 +210,13 @@ files:
196
210
  - lib/emailbutler.rb
197
211
  - lib/emailbutler/adapters/active_record.rb
198
212
  - lib/emailbutler/configuration.rb
213
+ - lib/emailbutler/container.rb
199
214
  - lib/emailbutler/dsl.rb
200
215
  - lib/emailbutler/engine.rb
201
216
  - lib/emailbutler/helpers.rb
202
217
  - lib/emailbutler/mailers/helpers.rb
203
218
  - lib/emailbutler/version.rb
219
+ - lib/emailbutler/webhooks/mappers/resend.rb
204
220
  - lib/emailbutler/webhooks/mappers/sendgrid.rb
205
221
  - lib/emailbutler/webhooks/mappers/smtp2go.rb
206
222
  - lib/emailbutler/webhooks/receiver.rb