sincerely 1.0.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/CHANGELOG.md +12 -0
- data/README.md +202 -0
- data/Rakefile +12 -0
- data/app/controllers/sincerely/application_controller.rb +81 -0
- data/app/controllers/sincerely/dashboard_controller.rb +112 -0
- data/app/controllers/sincerely/delivery_events_controller.rb +31 -0
- data/app/controllers/sincerely/engagement_events_controller.rb +32 -0
- data/app/controllers/sincerely/notifications_controller.rb +69 -0
- data/app/controllers/sincerely/send_controller.rb +105 -0
- data/app/controllers/sincerely/templates_controller.rb +61 -0
- data/app/helpers/sincerely/application_helper.rb +39 -0
- data/app/views/layouts/sincerely/application.html.erb +593 -0
- data/app/views/sincerely/dashboard/index.html.erb +382 -0
- data/app/views/sincerely/delivery_events/index.html.erb +97 -0
- data/app/views/sincerely/engagement_events/index.html.erb +97 -0
- data/app/views/sincerely/notifications/index.html.erb +91 -0
- data/app/views/sincerely/notifications/show.html.erb +98 -0
- data/app/views/sincerely/send/new.html.erb +592 -0
- data/app/views/sincerely/shared/_pagination.html.erb +19 -0
- data/app/views/sincerely/templates/_form.html.erb +226 -0
- data/app/views/sincerely/templates/edit.html.erb +11 -0
- data/app/views/sincerely/templates/index.html.erb +59 -0
- data/app/views/sincerely/templates/new.html.erb +11 -0
- data/app/views/sincerely/templates/preview.html.erb +48 -0
- data/app/views/sincerely/templates/show.html.erb +69 -0
- data/config/routes.rb +21 -0
- data/lib/config/application_config.rb +18 -0
- data/lib/config/sincerely_config.rb +31 -0
- data/lib/generators/sincerely/aws_ses_webhook_controller_generator.rb +31 -0
- data/lib/generators/sincerely/events_generator.rb +45 -0
- data/lib/generators/sincerely/install_generator.rb +18 -0
- data/lib/generators/sincerely/migration_generator.rb +65 -0
- data/lib/generators/templates/aws_ses_webhook_controller.rb.erb +3 -0
- data/lib/generators/templates/events/delivery_event_model.rb.erb +5 -0
- data/lib/generators/templates/events/delivery_events_create.rb.erb +20 -0
- data/lib/generators/templates/events/engagement_event_model.rb.erb +5 -0
- data/lib/generators/templates/events/engagement_events_create.rb.erb +21 -0
- data/lib/generators/templates/notification_model.rb.erb +3 -0
- data/lib/generators/templates/notifications_create.rb.erb +21 -0
- data/lib/generators/templates/notifications_update.rb.erb +16 -0
- data/lib/generators/templates/sincerely.yml +21 -0
- data/lib/generators/templates/templates_create.rb.erb +15 -0
- data/lib/sincerely/delivery_systems/email_aws_ses.rb +69 -0
- data/lib/sincerely/engine.rb +7 -0
- data/lib/sincerely/mixins/notification_model.rb +94 -0
- data/lib/sincerely/mixins/webhooks/aws_ses_events.rb +74 -0
- data/lib/sincerely/renderers/liquid.rb +14 -0
- data/lib/sincerely/services/events/aws_ses_bounce_event.rb +26 -0
- data/lib/sincerely/services/events/aws_ses_click_event.rb +25 -0
- data/lib/sincerely/services/events/aws_ses_complaint_event.rb +25 -0
- data/lib/sincerely/services/events/aws_ses_delivery_event.rb +17 -0
- data/lib/sincerely/services/events/aws_ses_event.rb +56 -0
- data/lib/sincerely/services/events/aws_ses_open_event.rb +25 -0
- data/lib/sincerely/services/process_delivery_event.rb +72 -0
- data/lib/sincerely/templates/email_liquid_template.rb +13 -0
- data/lib/sincerely/templates/notification_template.rb +22 -0
- data/lib/sincerely/version.rb +5 -0
- data/lib/sincerely.rb +20 -0
- data/sincerely.gemspec +37 -0
- metadata +187 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class CreateSincerelyDeliveryEvents < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :sincerely_delivery_events do |t|
|
|
4
|
+
t.string :message_id
|
|
5
|
+
t.string :delivery_system
|
|
6
|
+
t.string :event_type
|
|
7
|
+
t.string :recipient
|
|
8
|
+
t.string :delivery_event_type
|
|
9
|
+
t.string :delivery_event_subtype
|
|
10
|
+
t.text :options
|
|
11
|
+
t.datetime :timestamp
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :sincerely_delivery_events, :message_id
|
|
17
|
+
add_index :sincerely_delivery_events, :delivery_system
|
|
18
|
+
add_index :sincerely_delivery_events, :created_at
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreateSincerelyEngagementEvents < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :sincerely_engagement_events do |t|
|
|
4
|
+
t.string :message_id
|
|
5
|
+
t.string :delivery_system
|
|
6
|
+
t.string :event_type
|
|
7
|
+
t.string :recipient
|
|
8
|
+
t.string :ip_address
|
|
9
|
+
t.string :user_agent
|
|
10
|
+
t.string :link
|
|
11
|
+
t.string :feedback_type
|
|
12
|
+
t.text :options
|
|
13
|
+
t.datetime :timestamp
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :sincerely_engagement_events, :message_id
|
|
19
|
+
add_index :sincerely_engagement_events, :created_at
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class Create<%= class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %> do |t|
|
|
4
|
+
t.string :recipient
|
|
5
|
+
t.string :notification_type
|
|
6
|
+
t.text :delivery_options
|
|
7
|
+
t.string :delivery_system
|
|
8
|
+
t.string :delivery_state
|
|
9
|
+
t.string :template_id
|
|
10
|
+
t.datetime :sent_at
|
|
11
|
+
t.datetime :scheduled_at
|
|
12
|
+
t.integer :delivery_attempts
|
|
13
|
+
t.string :message_id
|
|
14
|
+
t.string :error_message
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :<%= table_name %>, :recipient
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class Update<%= class_name.pluralize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
add_column :<%= table_name %>, :recipient, :string
|
|
4
|
+
add_column :<%= table_name %>, :notification_type, :string
|
|
5
|
+
add_column :<%= table_name %>, :delivery_options, :text
|
|
6
|
+
add_column :<%= table_name %>, :delivery_system, :string
|
|
7
|
+
add_column :<%= table_name %>, :delivery_state, :string
|
|
8
|
+
add_column :<%= table_name %>, :sent_at, :datetime
|
|
9
|
+
add_column :<%= table_name %>, :scheduled_at, :datetime
|
|
10
|
+
add_column :<%= table_name %>, :delivery_attempts, :integer
|
|
11
|
+
add_column :<%= table_name %>, :message_id, :string
|
|
12
|
+
add_column :<%= table_name %>, :error_message, :string
|
|
13
|
+
|
|
14
|
+
add_index :<%= table_name %>, :recipient
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
defaults: &defaults
|
|
2
|
+
notification_model_name: Notification
|
|
3
|
+
delivery_methods:
|
|
4
|
+
email:
|
|
5
|
+
delivery_system: Sincerely::DeliverySystems::EmailAwsSes
|
|
6
|
+
options:
|
|
7
|
+
region: <%%= ENV['AWS_REGION'] %>
|
|
8
|
+
access_key_id: <%%= ENV['AWS_ACCESS_KEY_ID'] %>
|
|
9
|
+
secret_access_key: <%%= ENV['AWS_SECRET_ACCESS_KEY'] %>
|
|
10
|
+
configuration_set_name: config_set
|
|
11
|
+
test:
|
|
12
|
+
<<: *defaults
|
|
13
|
+
|
|
14
|
+
development:
|
|
15
|
+
<<: *defaults
|
|
16
|
+
|
|
17
|
+
staging:
|
|
18
|
+
<<: *defaults
|
|
19
|
+
|
|
20
|
+
production:
|
|
21
|
+
<<: *defaults
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class Create<%= class_name %>Templates < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :notification_templates do |t|
|
|
4
|
+
t.integer :template_id
|
|
5
|
+
t.string :name
|
|
6
|
+
t.string :subject
|
|
7
|
+
t.string :sender
|
|
8
|
+
t.string :html_content
|
|
9
|
+
t.string :text_content
|
|
10
|
+
t.string :type
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aws-sdk-sesv2'
|
|
4
|
+
|
|
5
|
+
module Sincerely
|
|
6
|
+
module DeliverySystems
|
|
7
|
+
class EmailAwsSes
|
|
8
|
+
DELIVERY_SYSTEM = :aws_ses2
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def call(notification:, options: {})
|
|
12
|
+
new(notification:, options:).deliver
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(notification:, options:)
|
|
17
|
+
@notification = notification
|
|
18
|
+
@template = notification.template
|
|
19
|
+
@options = options.symbolize_keys
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deliver
|
|
23
|
+
response = client.send_email(email_options)
|
|
24
|
+
update_notification(response)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :notification, :template, :options
|
|
30
|
+
|
|
31
|
+
def client
|
|
32
|
+
client_options = options.slice(:region, :access_key_id, :secret_access_key)
|
|
33
|
+
@client ||= Aws::SESV2::Client.new(**client_options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def email_options
|
|
37
|
+
opts = {
|
|
38
|
+
from_email_address: template.sender,
|
|
39
|
+
destination: { to_addresses: [notification.recipient] },
|
|
40
|
+
content: {
|
|
41
|
+
simple: {
|
|
42
|
+
subject: { data: subject },
|
|
43
|
+
body: {
|
|
44
|
+
html: {
|
|
45
|
+
data: notification.render_content(:html)
|
|
46
|
+
},
|
|
47
|
+
text: {
|
|
48
|
+
data: notification.render_content(:text)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
config_set = options[:configuration_set_name]
|
|
55
|
+
opts = opts.merge(configuration_set_name: config_set) if config_set.present?
|
|
56
|
+
opts
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def subject
|
|
60
|
+
raw_subject = notification.delivery_options&.fetch('subject', nil) || template.subject
|
|
61
|
+
template.renderer.render(raw_subject, notification.delivery_options&.stringify_keys)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update_notification(response)
|
|
65
|
+
notification.update(message_id: response.message_id, delivery_system: DELIVERY_SYSTEM, sent_at: Time.current)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aasm'
|
|
4
|
+
require 'active_support/concern'
|
|
5
|
+
|
|
6
|
+
module Sincerely
|
|
7
|
+
module Mixins
|
|
8
|
+
module NotificationModel
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do # rubocop:disable Metrics/BlockLength
|
|
12
|
+
include AASM
|
|
13
|
+
|
|
14
|
+
serialize :delivery_options, coder: JSON
|
|
15
|
+
|
|
16
|
+
belongs_to :template, class_name: 'Sincerely::Templates::NotificationTemplate'
|
|
17
|
+
|
|
18
|
+
validates :recipient, presence: true
|
|
19
|
+
validates :notification_type, inclusion: { in: %w[email sms push] }
|
|
20
|
+
|
|
21
|
+
aasm column: :delivery_state do # rubocop:disable Metrics/BlockLength
|
|
22
|
+
state :draft, initial: true
|
|
23
|
+
state :accepted
|
|
24
|
+
state :rejected
|
|
25
|
+
state :delivered
|
|
26
|
+
state :bounced
|
|
27
|
+
state :complained
|
|
28
|
+
state :delayed
|
|
29
|
+
state :opened
|
|
30
|
+
state :clicked
|
|
31
|
+
|
|
32
|
+
# Terminal states: bounced, complained, rejected - no transitions allowed from these
|
|
33
|
+
# Flow: draft -> accepted -> delivered -> opened -> clicked
|
|
34
|
+
# \-> bounced (terminal)
|
|
35
|
+
# \-> complained (terminal from delivered/opened/clicked)
|
|
36
|
+
|
|
37
|
+
event :set_accepted do
|
|
38
|
+
transitions to: :accepted, from: [:draft]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
event :set_rejected do
|
|
42
|
+
transitions to: :rejected, from: %i[draft accepted]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
event :set_delivered do
|
|
46
|
+
transitions to: :delivered, from: %i[draft accepted delayed]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
event :set_bounced do
|
|
50
|
+
transitions to: :bounced, from: %i[draft accepted delivered delayed]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
event :set_complained do
|
|
54
|
+
transitions to: :complained, from: %i[delivered opened clicked]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
event :set_delayed do
|
|
58
|
+
transitions to: :delayed, from: %i[draft accepted]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
event :set_opened do
|
|
62
|
+
transitions to: :opened, from: [:delivered]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
event :set_clicked do
|
|
66
|
+
transitions to: :clicked, from: %i[delivered opened]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_content(content_type)
|
|
71
|
+
template.render(content_type, delivery_options)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def deliver
|
|
75
|
+
raise StandardError, "Delivery method not configured for #{notification_type}." if delivery_method.blank?
|
|
76
|
+
|
|
77
|
+
call_delivery_method
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def delivery_method
|
|
83
|
+
@delivery_method ||= Sincerely.config.delivery_methods&.fetch(notification_type, {})
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def call_delivery_method
|
|
87
|
+
class_name, options = delivery_method.values_at('delivery_system', 'options')
|
|
88
|
+
klass = class_name.constantize
|
|
89
|
+
klass.call(notification: self, options:)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require 'aws-sdk-sns'
|
|
5
|
+
|
|
6
|
+
require 'sincerely/services/process_delivery_event'
|
|
7
|
+
require 'sincerely/services/events/aws_ses_event'
|
|
8
|
+
|
|
9
|
+
module Sincerely
|
|
10
|
+
module Mixins
|
|
11
|
+
module Webhooks
|
|
12
|
+
module AwsSesEvents
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
included do # rubocop:disable Metrics/BlockLength
|
|
16
|
+
skip_before_action :verify_authenticity_token, only: [:create]
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
verifier.authenticate!(request.raw_post)
|
|
20
|
+
|
|
21
|
+
case sns_message_type
|
|
22
|
+
when 'SubscriptionConfirmation'
|
|
23
|
+
confirm_subscription
|
|
24
|
+
when 'Notification'
|
|
25
|
+
logger&.info event_payload # temporary log
|
|
26
|
+
event = Sincerely::Services::Events::AwsSesEvent.event_for(event_payload)
|
|
27
|
+
Sincerely::Services::ProcessDeliveryEvent.call(event:)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
render json: { message: 'ok' }, status: :ok
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def verifier
|
|
36
|
+
@verifier ||= Aws::SNS::MessageVerifier.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def posted_message_body
|
|
40
|
+
@posted_message_body ||= JSON.parse(request.raw_post)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def sns_message_type
|
|
44
|
+
posted_message_body['Type']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def topic_arn
|
|
48
|
+
posted_message_body['TopicArn']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def token
|
|
52
|
+
posted_message_body['Token']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def event_payload
|
|
56
|
+
message = posted_message_body['Message']
|
|
57
|
+
JSON.parse(message) if message.present?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def delivery_options
|
|
61
|
+
Sincerely.config.delivery_methods.dig('email', 'options').symbolize_keys
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def confirm_subscription
|
|
65
|
+
client_options = delivery_options.slice(:region, :access_key_id, :secret_access_key)
|
|
66
|
+
client = Aws::SNS::Client.new(**client_options)
|
|
67
|
+
|
|
68
|
+
client.confirm_subscription(topic_arn:, token:)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'liquid'
|
|
4
|
+
|
|
5
|
+
module Sincerely
|
|
6
|
+
module Renderers
|
|
7
|
+
class Liquid
|
|
8
|
+
def self.render(content, options = {})
|
|
9
|
+
template = ::Liquid::Template.parse(content)
|
|
10
|
+
template.render(options&.dig('template_data')&.stringify_keys)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module Services
|
|
5
|
+
module Events
|
|
6
|
+
class AwsSesBounceEvent < AwsSesEvent
|
|
7
|
+
def delivery_event_type
|
|
8
|
+
event.dig('bounce', 'bounceType')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def delivery_event_subtype
|
|
12
|
+
event.dig('bounce', 'bounceSubType')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def options
|
|
16
|
+
bounced_recipient = event.dig('bounce', 'bouncedRecipients')[0]
|
|
17
|
+
{
|
|
18
|
+
action: bounced_recipient&.dig('action'),
|
|
19
|
+
status: bounced_recipient&.dig('status'),
|
|
20
|
+
diagnostic_code: bounced_recipient&.dig('diagnosticCode')
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module Services
|
|
5
|
+
module Events
|
|
6
|
+
class AwsSesClickEvent < AwsSesEvent
|
|
7
|
+
def ip_address
|
|
8
|
+
event.dig('click', 'ipAddress')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def user_agent
|
|
12
|
+
event.dig('click', 'userAgent')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def link
|
|
16
|
+
event.dig('click', 'link')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def feedback_type
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module Services
|
|
5
|
+
module Events
|
|
6
|
+
class AwsSesComplaintEvent < AwsSesEvent
|
|
7
|
+
def ip_address
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def user_agent
|
|
12
|
+
event.dig('complaint', 'userAgent')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def link
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def feedback_type
|
|
20
|
+
event.dig('complaint', 'complaintFeedbackType')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module Services
|
|
5
|
+
module Events
|
|
6
|
+
class AwsSesEvent
|
|
7
|
+
attr_reader :event
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def event_for(event_payload)
|
|
11
|
+
case event_type(event_payload)
|
|
12
|
+
when 'bounce' then AwsSesBounceEvent.new(event_payload)
|
|
13
|
+
when 'delivery' then AwsSesDeliveryEvent.new(event_payload)
|
|
14
|
+
when 'click' then AwsSesClickEvent.new(event_payload)
|
|
15
|
+
when 'open' then AwsSesOpenEvent.new(event_payload)
|
|
16
|
+
when 'complaint' then AwsSesComplaintEvent.new(event_payload)
|
|
17
|
+
else new(event_payload)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def event_type(event_payload)
|
|
22
|
+
event_payload&.dig('eventType')&.downcase
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(event_payload)
|
|
27
|
+
@event = event_payload
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def event_type
|
|
31
|
+
self.class.event_type(event)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def message_id
|
|
35
|
+
event.dig('mail', 'messageId')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def recipient
|
|
39
|
+
event.dig('mail', 'destination')[0]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def timestamp
|
|
43
|
+
Time.parse(event.dig(event_type, 'timestamp') || event.dig('mail', 'timestamp')) # rubocop:disable Rails/TimeZone
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def options
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delivery_system
|
|
51
|
+
Sincerely::DeliverySystems::EmailAwsSes::DELIVERY_SYSTEM
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module Services
|
|
5
|
+
module Events
|
|
6
|
+
class AwsSesOpenEvent < AwsSesEvent
|
|
7
|
+
def ip_address
|
|
8
|
+
event.dig('open', 'ipAddress')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def user_agent
|
|
12
|
+
event.dig('open', 'userAgent')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def link
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def feedback_type
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module Services
|
|
5
|
+
class ProcessDeliveryEvent
|
|
6
|
+
delegate :event_type, :message_id, :recipient, :timestamp, :options, :delivery_system,
|
|
7
|
+
to: :event
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(event:)
|
|
11
|
+
new(event:).call
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(event:)
|
|
16
|
+
@event = event
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
return if notification.blank?
|
|
21
|
+
|
|
22
|
+
case event_type.to_sym
|
|
23
|
+
when :bounce
|
|
24
|
+
notification.set_bounced!
|
|
25
|
+
create_event
|
|
26
|
+
when :complaint
|
|
27
|
+
notification.set_complained!
|
|
28
|
+
create_event
|
|
29
|
+
when :delivery
|
|
30
|
+
notification.set_delivered!
|
|
31
|
+
create_event
|
|
32
|
+
when :send
|
|
33
|
+
notification.set_accepted! if notification.may_set_accepted?
|
|
34
|
+
when :reject
|
|
35
|
+
notification.set_rejected!
|
|
36
|
+
notification.update(error_message: event.rejection_reason)
|
|
37
|
+
when :open
|
|
38
|
+
notification.set_opened!
|
|
39
|
+
create_event
|
|
40
|
+
when :click
|
|
41
|
+
notification.set_clicked!
|
|
42
|
+
create_event
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :event
|
|
49
|
+
|
|
50
|
+
def create_event
|
|
51
|
+
case event_type.to_sym
|
|
52
|
+
when :bounce, :delivery
|
|
53
|
+
Sincerely::DeliveryEvent.create(
|
|
54
|
+
message_id:, delivery_system:, event_type:, recipient:, timestamp:, options:,
|
|
55
|
+
delivery_event_type: event.delivery_event_type, delivery_event_subtype: event.delivery_event_subtype
|
|
56
|
+
)
|
|
57
|
+
else
|
|
58
|
+
Sincerely::EngagementEvent.create(
|
|
59
|
+
message_id:, delivery_system:, event_type:, recipient:, timestamp:, options:,
|
|
60
|
+
ip_address: event.ip_address, user_agent: event.user_agent, link: event.link,
|
|
61
|
+
feedback_type: event.feedback_type
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def notification
|
|
67
|
+
model = Sincerely.config.notification_model_name.constantize
|
|
68
|
+
@notification ||= model.find_by(message_id:)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|