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,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class SendController < ApplicationController
|
|
5
|
+
MAX_RECIPIENTS = 50
|
|
6
|
+
|
|
7
|
+
def new
|
|
8
|
+
@templates = Sincerely::Templates::NotificationTemplate.order(:name)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
recipients = parse_recipients(params[:recipients])
|
|
13
|
+
|
|
14
|
+
if recipients.empty?
|
|
15
|
+
render json: { success: false, error: 'No valid recipients provided' }, status: :unprocessable_entity
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if recipients.size > MAX_RECIPIENTS
|
|
20
|
+
render json: { success: false, error: "Maximum #{MAX_RECIPIENTS} recipients allowed" },
|
|
21
|
+
status: :unprocessable_entity
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
template = Sincerely::Templates::NotificationTemplate.find_by(id: params[:template_id])
|
|
26
|
+
unless template
|
|
27
|
+
render json: { success: false, error: 'Template not found' }, status: :unprocessable_entity
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
template_data = params[:template_data]&.to_unsafe_h || {}
|
|
32
|
+
|
|
33
|
+
results = { sent: 0, failed: 0, errors: [] }
|
|
34
|
+
|
|
35
|
+
recipients.each do |recipient|
|
|
36
|
+
notification = notification_model.new(
|
|
37
|
+
recipient:,
|
|
38
|
+
notification_type: 'email',
|
|
39
|
+
template_id: template.id,
|
|
40
|
+
delivery_options: { template_data: }
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if notification.save
|
|
44
|
+
begin
|
|
45
|
+
notification.deliver
|
|
46
|
+
results[:sent] += 1
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
results[:failed] += 1
|
|
49
|
+
results[:errors] << { recipient:, error: e.message }
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
results[:failed] += 1
|
|
53
|
+
results[:errors] << { recipient:, error: notification.errors.full_messages.join(', ') }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
render json: {
|
|
58
|
+
success: true,
|
|
59
|
+
sent: results[:sent],
|
|
60
|
+
failed: results[:failed],
|
|
61
|
+
errors: results[:errors].first(5) # Limit error details
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def template_variables
|
|
66
|
+
template = Sincerely::Templates::NotificationTemplate.find_by(id: params[:id])
|
|
67
|
+
|
|
68
|
+
unless template
|
|
69
|
+
render json: { variables: [] }
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
variables = extract_liquid_variables(template)
|
|
74
|
+
render json: { variables: variables.uniq.sort }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def parse_recipients(input)
|
|
80
|
+
return [] if input.blank?
|
|
81
|
+
|
|
82
|
+
# Split by comma, semicolon, space, or newline
|
|
83
|
+
input.split(/[\s,;\n]+/)
|
|
84
|
+
.map(&:strip)
|
|
85
|
+
.compact_blank
|
|
86
|
+
.grep(/\A[^@\s]+@[^@\s]+\z/)
|
|
87
|
+
.uniq
|
|
88
|
+
.first(MAX_RECIPIENTS)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_liquid_variables(template)
|
|
92
|
+
content = [
|
|
93
|
+
template.subject,
|
|
94
|
+
template.html_content,
|
|
95
|
+
template.text_content
|
|
96
|
+
].compact.join(' ')
|
|
97
|
+
|
|
98
|
+
# Extract {{ variable }} and {{ variable.property }} patterns
|
|
99
|
+
content.scan(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*\}\}/)
|
|
100
|
+
.flatten
|
|
101
|
+
.map { |v| v.split('.').first } # Get root variable name
|
|
102
|
+
.uniq
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class TemplatesController < ApplicationController
|
|
5
|
+
before_action :set_template, only: %i[show edit update preview]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@pagination = paginate(Sincerely::Templates::NotificationTemplate.order(created_at: :desc))
|
|
9
|
+
@templates = @pagination[:records]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
@notification_count = notification_model.where(template_id: @template.id).count
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new
|
|
17
|
+
@template = Sincerely::Templates::EmailLiquidTemplate.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def edit; end
|
|
21
|
+
|
|
22
|
+
def create
|
|
23
|
+
@template = Sincerely::Templates::EmailLiquidTemplate.new(template_params)
|
|
24
|
+
|
|
25
|
+
if @template.save
|
|
26
|
+
redirect_to template_path(@template), notice: 'Template created successfully'
|
|
27
|
+
else
|
|
28
|
+
render :new, status: :unprocessable_entity
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
if @template.update(template_params)
|
|
34
|
+
redirect_to template_path(@template), notice: 'Template updated successfully'
|
|
35
|
+
else
|
|
36
|
+
render :edit, status: :unprocessable_entity
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def preview
|
|
41
|
+
sample_data = params[:sample_data].present? ? JSON.parse(params[:sample_data]) : {}
|
|
42
|
+
@rendered_subject = @template.renderer.render(@template.subject || '', { 'template_data' => sample_data })
|
|
43
|
+
@rendered_html = @template.render(:html, { 'template_data' => sample_data })
|
|
44
|
+
@rendered_text = @template.render(:text, { 'template_data' => sample_data })
|
|
45
|
+
|
|
46
|
+
render layout: false
|
|
47
|
+
rescue JSON::ParserError
|
|
48
|
+
render plain: 'Invalid JSON in sample data', status: :bad_request
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def set_template
|
|
54
|
+
@template = Sincerely::Templates::NotificationTemplate.find(params[:id])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def template_params
|
|
58
|
+
params.require(:template).permit(:name, :subject, :sender, :html_content, :text_content)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
module ApplicationHelper
|
|
5
|
+
def state_badge_class(state)
|
|
6
|
+
case state.to_s
|
|
7
|
+
when 'draft' then 'badge-draft'
|
|
8
|
+
when 'accepted' then 'badge-accepted'
|
|
9
|
+
when 'delivered' then 'badge-delivered'
|
|
10
|
+
when 'opened' then 'badge-opened'
|
|
11
|
+
when 'clicked' then 'badge-clicked'
|
|
12
|
+
when 'bounced' then 'badge-bounced'
|
|
13
|
+
when 'complained' then 'badge-complained'
|
|
14
|
+
when 'rejected' then 'badge-rejected'
|
|
15
|
+
when 'delayed' then 'badge-delayed'
|
|
16
|
+
else 'badge-default'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def format_timestamp(time)
|
|
21
|
+
return '-' unless time
|
|
22
|
+
|
|
23
|
+
time.strftime('%b %d, %Y %H:%M:%S')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def event_type_badge_class(event_type)
|
|
27
|
+
case event_type.to_s.downcase
|
|
28
|
+
when 'delivery', 'send' then 'badge-delivered'
|
|
29
|
+
when 'open' then 'badge-opened'
|
|
30
|
+
when 'click' then 'badge-clicked'
|
|
31
|
+
when 'bounce', 'hard_bounce', 'soft_bounce' then 'badge-bounced'
|
|
32
|
+
when 'complaint', 'spam_complaint' then 'badge-complained'
|
|
33
|
+
when 'reject' then 'badge-rejected'
|
|
34
|
+
when 'delay' then 'badge-delayed'
|
|
35
|
+
else 'badge-default'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|