email_digest 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/.vscode/settings.json +3 -0
- data/ANSWERS_TO_YOUR_QUESTIONS.md +361 -0
- data/CHANGELOG.md +14 -0
- data/COMPLETE_SETUP_INSTRUCTIONS.md +298 -0
- data/CUSTOMIZATION_GUIDE.md +264 -0
- data/FAQ.md +133 -0
- data/LICENSE.txt +21 -0
- data/QUICK_START.md +62 -0
- data/README.md +253 -0
- data/REPOSITORY_READY.md +146 -0
- data/RUBYGEMS_PUBLISHING.md +272 -0
- data/SETUP_SEPARATE_GEM_REPO.md +131 -0
- data/email_digest.gemspec +52 -0
- data/lib/email_digest/concerns/digestable.rb +57 -0
- data/lib/email_digest/configuration.rb +40 -0
- data/lib/email_digest/generators/email_digest/install/install_generator.rb +50 -0
- data/lib/email_digest/generators/email_digest/install/templates/README +16 -0
- data/lib/email_digest/generators/email_digest/install/templates/create_email_digest_tables.rb +77 -0
- data/lib/email_digest/generators/email_digest/install/templates/email_digest.rb +25 -0
- data/lib/email_digest/models/digest_item.rb +49 -0
- data/lib/email_digest/models/digest_preference.rb +106 -0
- data/lib/email_digest/railtie.rb +9 -0
- data/lib/email_digest/services/digest_collector.rb +38 -0
- data/lib/email_digest/services/digest_scheduler.rb +55 -0
- data/lib/email_digest/services/digest_summarizer.rb +135 -0
- data/lib/email_digest/version.rb +5 -0
- data/lib/email_digest/workers/digest_processor_worker.rb +64 -0
- data/lib/email_digest/workers/digest_scheduler_worker.rb +26 -0
- data/lib/email_digest/workers/digest_sender_worker.rb +48 -0
- data/lib/email_digest.rb +37 -0
- metadata +241 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EmailDigest
|
|
4
|
+
module Services
|
|
5
|
+
class DigestScheduler
|
|
6
|
+
def self.process_all_ready(organization = nil)
|
|
7
|
+
preferences = Models::DigestPreference.ready_to_send(organization)
|
|
8
|
+
preferences.each do |preference|
|
|
9
|
+
process_preference(preference)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.process_preference(preference)
|
|
14
|
+
return unless preference.enabled
|
|
15
|
+
|
|
16
|
+
# Get user from MongoDB - we need to load it using the configured user class
|
|
17
|
+
user_class = EmailDigest.configuration.user_class.constantize
|
|
18
|
+
user = user_class.find(preference.user_id)
|
|
19
|
+
return unless user
|
|
20
|
+
|
|
21
|
+
# Get organization from MongoDB
|
|
22
|
+
org_class = EmailDigest.configuration.organization_class.constantize
|
|
23
|
+
organization = org_class.find(preference.organization_id)
|
|
24
|
+
return unless organization
|
|
25
|
+
|
|
26
|
+
# Scope queries by organization_id (replaces Mongoid multitenancy)
|
|
27
|
+
# Use custom collector if configured, otherwise use default
|
|
28
|
+
digest_config = EmailDigest.configuration.digest_types[preference.digest_type.to_sym]
|
|
29
|
+
collector_class = if digest_config&.dig(:collector)
|
|
30
|
+
digest_config[:collector].constantize
|
|
31
|
+
else
|
|
32
|
+
EmailDigest::Services::DigestCollector
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
collector = collector_class.new(user, preference.digest_type, organization)
|
|
36
|
+
items = collector.collect_items
|
|
37
|
+
|
|
38
|
+
# Trigger if there are any items (1 or more)
|
|
39
|
+
if items.any?
|
|
40
|
+
Workers::DigestProcessorWorker.perform_async(
|
|
41
|
+
preference.id.to_s,
|
|
42
|
+
items.map(&:id).map(&:to_s)
|
|
43
|
+
)
|
|
44
|
+
else
|
|
45
|
+
# No items, but update the schedule
|
|
46
|
+
preference.update_last_sent
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Rails.logger.error("EmailDigest: Error processing preference #{preference.id}: #{e.message}")
|
|
50
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
51
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EmailDigest
|
|
4
|
+
module Services
|
|
5
|
+
class DigestSummarizer
|
|
6
|
+
attr_reader :items, :digest_type
|
|
7
|
+
|
|
8
|
+
def initialize(items, digest_type)
|
|
9
|
+
@items = items
|
|
10
|
+
@digest_type = digest_type.to_sym
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def summarize
|
|
14
|
+
return empty_summary if items.empty?
|
|
15
|
+
return single_item_summary if items.size == 1
|
|
16
|
+
return grouped_summary if should_group?
|
|
17
|
+
return priority_summary if has_high_priority_items?
|
|
18
|
+
|
|
19
|
+
default_summary
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def empty_summary
|
|
25
|
+
{
|
|
26
|
+
subject: "Your #{digest_type.to_s.humanize} Digest",
|
|
27
|
+
summary: "No new notifications.",
|
|
28
|
+
items: [],
|
|
29
|
+
grouped_items: {},
|
|
30
|
+
total_count: 0
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def single_item_summary
|
|
35
|
+
item = items.first
|
|
36
|
+
item_hash = enrich_item_hash(item_to_hash(item), item)
|
|
37
|
+
{
|
|
38
|
+
subject: item.subject || "New #{item.notification_type.humanize}",
|
|
39
|
+
summary: item.body,
|
|
40
|
+
items: [item_hash],
|
|
41
|
+
grouped_items: { item.notification_type => [item_hash] },
|
|
42
|
+
total_count: 1
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def should_group?
|
|
47
|
+
items.size > 3 && items.map(&:notification_type).uniq.size < items.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def grouped_summary
|
|
51
|
+
grouped = items.group_by(&:notification_type)
|
|
52
|
+
notification_counts = grouped.map { |type, items| "#{items.size} #{type.humanize.pluralize(items.size)}" }
|
|
53
|
+
items_hash = items.map { |item| enrich_item_hash(item_to_hash(item), item) }
|
|
54
|
+
grouped_hash = grouped.transform_values { |items| items.map { |item| enrich_item_hash(item_to_hash(item), item) } }
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
subject: "Your #{digest_type.to_s.humanize} Digest - #{items.size} New Items",
|
|
58
|
+
summary: "You have #{items.size} new notifications: #{notification_counts.join(', ')}.",
|
|
59
|
+
items: items_hash,
|
|
60
|
+
grouped_items: grouped_hash,
|
|
61
|
+
total_count: items.size,
|
|
62
|
+
grouped: true
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def has_high_priority_items?
|
|
67
|
+
items.any? { |item| item.priority > 5 }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def priority_summary
|
|
71
|
+
high_priority = items.select { |item| item.priority > 5 }
|
|
72
|
+
regular = items.reject { |item| item.priority > 5 }
|
|
73
|
+
|
|
74
|
+
summary_parts = []
|
|
75
|
+
summary_parts << "#{high_priority.size} high priority notification#{'s' if high_priority.size != 1}" if high_priority.any?
|
|
76
|
+
summary_parts << "#{regular.size} other notification#{'s' if regular.size != 1}" if regular.any?
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
subject: "Your #{digest_type.to_s.humanize} Digest - #{items.size} New Items",
|
|
80
|
+
summary: summary_parts.join(' and ') + ".",
|
|
81
|
+
items: items.map { |item| enrich_item_hash(item_to_hash(item), item) },
|
|
82
|
+
grouped_items: {
|
|
83
|
+
high_priority: high_priority.map { |item| enrich_item_hash(item_to_hash(item), item) },
|
|
84
|
+
regular: regular.map { |item| enrich_item_hash(item_to_hash(item), item) }
|
|
85
|
+
},
|
|
86
|
+
total_count: items.size,
|
|
87
|
+
priority_based: true
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_summary
|
|
92
|
+
items_hash = items.map { |item| enrich_item_hash(item_to_hash(item), item) }
|
|
93
|
+
{
|
|
94
|
+
subject: "Your #{digest_type.to_s.humanize} Digest - #{items.size} New Items",
|
|
95
|
+
summary: "You have #{items.size} new notification#{'s' if items.size != 1}.",
|
|
96
|
+
items: items_hash,
|
|
97
|
+
grouped_items: { all: items_hash },
|
|
98
|
+
total_count: items.size
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def item_to_hash(item)
|
|
103
|
+
{
|
|
104
|
+
id: item.id.to_s,
|
|
105
|
+
notification_type: item.notification_type,
|
|
106
|
+
subject: item.subject,
|
|
107
|
+
body: item.body,
|
|
108
|
+
metadata: item.metadata || {},
|
|
109
|
+
source_id: item.source_id,
|
|
110
|
+
source_type: item.source_type,
|
|
111
|
+
priority: item.priority,
|
|
112
|
+
created_at: item.created_at
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Override this method in custom summarizers to include additional context
|
|
117
|
+
# This method is called for each item and can be extended to include
|
|
118
|
+
# course, section, groupTemplate, group, or any other metadata
|
|
119
|
+
#
|
|
120
|
+
# Example:
|
|
121
|
+
# def enrich_item_hash(item_hash, item)
|
|
122
|
+
# item_hash.merge(
|
|
123
|
+
# course: load_course_from_metadata(item.metadata),
|
|
124
|
+
# section: load_section_from_metadata(item.metadata),
|
|
125
|
+
# group_template: load_group_template_from_metadata(item.metadata)
|
|
126
|
+
# )
|
|
127
|
+
# end
|
|
128
|
+
def enrich_item_hash(item_hash, item)
|
|
129
|
+
# Base implementation returns item_hash as-is
|
|
130
|
+
# Override in custom summarizers to add additional fields
|
|
131
|
+
item_hash
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EmailDigest
|
|
4
|
+
module Workers
|
|
5
|
+
class DigestProcessorWorker
|
|
6
|
+
include Sidekiq::Worker
|
|
7
|
+
|
|
8
|
+
sidekiq_options retry: 3, queue: :email_digest
|
|
9
|
+
|
|
10
|
+
def perform(preference_id, item_ids)
|
|
11
|
+
preference = Models::DigestPreference.find(preference_id)
|
|
12
|
+
return unless preference&.enabled
|
|
13
|
+
|
|
14
|
+
# Get user and organization from MongoDB
|
|
15
|
+
user_class = EmailDigest.configuration.user_class.constantize
|
|
16
|
+
user = user_class.find(preference.user_id)
|
|
17
|
+
return unless user
|
|
18
|
+
|
|
19
|
+
org_class = EmailDigest.configuration.organization_class.constantize
|
|
20
|
+
organization = org_class.find(preference.organization_id)
|
|
21
|
+
return unless organization
|
|
22
|
+
|
|
23
|
+
# Scope queries by organization_id (replaces Mongoid multitenancy)
|
|
24
|
+
items = Models::DigestItem.where(id: item_ids)
|
|
25
|
+
.where(organization_id: organization.id.to_s)
|
|
26
|
+
.where(processed: false)
|
|
27
|
+
return if items.empty?
|
|
28
|
+
|
|
29
|
+
# Convert to array for summarizer
|
|
30
|
+
items_array = items.to_a
|
|
31
|
+
|
|
32
|
+
# Use custom summarizer if configured, otherwise use default
|
|
33
|
+
digest_config = EmailDigest.configuration.digest_types[preference.digest_type.to_sym]
|
|
34
|
+
summarizer_class = if digest_config&.dig(:summarizer)
|
|
35
|
+
digest_config[:summarizer].constantize
|
|
36
|
+
else
|
|
37
|
+
Services::DigestSummarizer
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Summarize items
|
|
41
|
+
summarizer = summarizer_class.new(items_array, preference.digest_type)
|
|
42
|
+
summary = summarizer.summarize
|
|
43
|
+
|
|
44
|
+
# Enqueue email sending
|
|
45
|
+
Workers::DigestSenderWorker.perform_async(
|
|
46
|
+
preference.id.to_s,
|
|
47
|
+
summary.to_json
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Mark items as processed atomically to prevent race conditions
|
|
51
|
+
Models::DigestItem.where(id: item_ids, processed: false, organization_id: organization.id.to_s)
|
|
52
|
+
.update_all(processed: true, processed_at: Time.current)
|
|
53
|
+
|
|
54
|
+
# Update preference
|
|
55
|
+
preference.update_last_sent
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
Rails.logger.error("EmailDigest: Error processing digest #{preference_id}: #{e.message}")
|
|
58
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
59
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EmailDigest
|
|
4
|
+
module Workers
|
|
5
|
+
class DigestSchedulerWorker
|
|
6
|
+
include Sidekiq::Worker
|
|
7
|
+
|
|
8
|
+
sidekiq_options retry: 2, queue: :email_digest
|
|
9
|
+
|
|
10
|
+
def perform(organization_id = nil)
|
|
11
|
+
organization = if organization_id
|
|
12
|
+
org_class = EmailDigest.configuration.organization_class.constantize
|
|
13
|
+
org_class.find(organization_id)
|
|
14
|
+
else
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
Services::DigestScheduler.process_all_ready(organization)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Rails.logger.error("EmailDigest: Error in scheduler worker: #{e.message}")
|
|
20
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
21
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EmailDigest
|
|
4
|
+
module Workers
|
|
5
|
+
class DigestSenderWorker
|
|
6
|
+
include Sidekiq::Worker
|
|
7
|
+
|
|
8
|
+
sidekiq_options retry: 2, queue: :email_digest
|
|
9
|
+
|
|
10
|
+
def perform(preference_id, summary_json)
|
|
11
|
+
preference = Models::DigestPreference.find(preference_id)
|
|
12
|
+
return unless preference
|
|
13
|
+
|
|
14
|
+
# Get user and organization from MongoDB
|
|
15
|
+
user_class = EmailDigest.configuration.user_class.constantize
|
|
16
|
+
user = user_class.find(preference.user_id)
|
|
17
|
+
return unless user
|
|
18
|
+
|
|
19
|
+
org_class = EmailDigest.configuration.organization_class.constantize
|
|
20
|
+
organization = org_class.find(preference.organization_id)
|
|
21
|
+
return unless organization
|
|
22
|
+
|
|
23
|
+
summary = JSON.parse(summary_json).with_indifferent_access
|
|
24
|
+
mailer_class = EmailDigest.configuration.mailer_class.constantize
|
|
25
|
+
|
|
26
|
+
# Get digest type specific mailer method or use default
|
|
27
|
+
digest_config = EmailDigest.configuration.digest_types[preference.digest_type.to_sym]
|
|
28
|
+
mailer_method = digest_config&.dig(:mailer_method) ||
|
|
29
|
+
EmailDigest.configuration.mailer_method
|
|
30
|
+
|
|
31
|
+
# Send email
|
|
32
|
+
mailer_class.send(
|
|
33
|
+
mailer_method,
|
|
34
|
+
user,
|
|
35
|
+
summary,
|
|
36
|
+
preference.digest_type
|
|
37
|
+
).deliver
|
|
38
|
+
|
|
39
|
+
Rails.logger.info("EmailDigest: Sent digest to user #{user.id} for type #{preference.digest_type}")
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
Rails.logger.error("EmailDigest: Error sending digest #{preference_id}: #{e.message}")
|
|
42
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
43
|
+
Airbrake.notify(e) if defined?(Airbrake)
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/email_digest.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'sidekiq'
|
|
5
|
+
require 'sidekiq-cron'
|
|
6
|
+
|
|
7
|
+
require_relative 'email_digest/version'
|
|
8
|
+
require_relative 'email_digest/configuration'
|
|
9
|
+
require_relative 'email_digest/models/digest_preference'
|
|
10
|
+
require_relative 'email_digest/models/digest_item'
|
|
11
|
+
require_relative 'email_digest/services/digest_collector'
|
|
12
|
+
require_relative 'email_digest/services/digest_summarizer'
|
|
13
|
+
require_relative 'email_digest/services/digest_scheduler'
|
|
14
|
+
require_relative 'email_digest/workers/digest_processor_worker'
|
|
15
|
+
require_relative 'email_digest/workers/digest_sender_worker'
|
|
16
|
+
require_relative 'email_digest/workers/digest_scheduler_worker'
|
|
17
|
+
require_relative 'email_digest/concerns/digestable'
|
|
18
|
+
|
|
19
|
+
# Load generators if Rails is available
|
|
20
|
+
if defined?(Rails)
|
|
21
|
+
require 'email_digest/railtie'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module EmailDigest
|
|
25
|
+
class << self
|
|
26
|
+
attr_accessor :configuration
|
|
27
|
+
|
|
28
|
+
def configure
|
|
29
|
+
self.configuration ||= Configuration.new
|
|
30
|
+
yield(configuration) if block_given?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configuration
|
|
34
|
+
@configuration ||= Configuration.new
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: email_digest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rohit sharma
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-29 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'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '6.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: sidekiq
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: sidekiq-cron
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
type: :runtime
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.0'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: pg
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.0'
|
|
68
|
+
type: :runtime
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.0'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: bundler
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.0'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '2.0'
|
|
89
|
+
- !ruby/object:Gem::Dependency
|
|
90
|
+
name: rake
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '13.0'
|
|
96
|
+
type: :development
|
|
97
|
+
prerelease: false
|
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '13.0'
|
|
103
|
+
- !ruby/object:Gem::Dependency
|
|
104
|
+
name: rspec
|
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.0'
|
|
110
|
+
type: :development
|
|
111
|
+
prerelease: false
|
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.0'
|
|
117
|
+
- !ruby/object:Gem::Dependency
|
|
118
|
+
name: rspec-rails
|
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '6.0'
|
|
124
|
+
type: :development
|
|
125
|
+
prerelease: false
|
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '6.0'
|
|
131
|
+
- !ruby/object:Gem::Dependency
|
|
132
|
+
name: rubocop
|
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '1.0'
|
|
138
|
+
type: :development
|
|
139
|
+
prerelease: false
|
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '1.0'
|
|
145
|
+
- !ruby/object:Gem::Dependency
|
|
146
|
+
name: rubocop-rails
|
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '2.0'
|
|
152
|
+
type: :development
|
|
153
|
+
prerelease: false
|
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.0'
|
|
159
|
+
- !ruby/object:Gem::Dependency
|
|
160
|
+
name: simplecov
|
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0.21'
|
|
166
|
+
type: :development
|
|
167
|
+
prerelease: false
|
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - "~>"
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '0.21'
|
|
173
|
+
description: |
|
|
174
|
+
EmailDigest is a comprehensive email digest system that allows you to collect,
|
|
175
|
+
summarize, and send batched email notifications to users. It supports multiple
|
|
176
|
+
digest types, customizable schedules, priority-based processing, and multi-tenant
|
|
177
|
+
architectures. Built with PostgreSQL and Sidekiq for reliability and scalability.
|
|
178
|
+
email:
|
|
179
|
+
- rohitsharma170801@gmail.com
|
|
180
|
+
executables: []
|
|
181
|
+
extensions: []
|
|
182
|
+
extra_rdoc_files: []
|
|
183
|
+
files:
|
|
184
|
+
- ".vscode/settings.json"
|
|
185
|
+
- ANSWERS_TO_YOUR_QUESTIONS.md
|
|
186
|
+
- CHANGELOG.md
|
|
187
|
+
- COMPLETE_SETUP_INSTRUCTIONS.md
|
|
188
|
+
- CUSTOMIZATION_GUIDE.md
|
|
189
|
+
- FAQ.md
|
|
190
|
+
- LICENSE.txt
|
|
191
|
+
- QUICK_START.md
|
|
192
|
+
- README.md
|
|
193
|
+
- REPOSITORY_READY.md
|
|
194
|
+
- RUBYGEMS_PUBLISHING.md
|
|
195
|
+
- SETUP_SEPARATE_GEM_REPO.md
|
|
196
|
+
- email_digest.gemspec
|
|
197
|
+
- lib/email_digest.rb
|
|
198
|
+
- lib/email_digest/concerns/digestable.rb
|
|
199
|
+
- lib/email_digest/configuration.rb
|
|
200
|
+
- lib/email_digest/generators/email_digest/install/install_generator.rb
|
|
201
|
+
- lib/email_digest/generators/email_digest/install/templates/README
|
|
202
|
+
- lib/email_digest/generators/email_digest/install/templates/create_email_digest_tables.rb
|
|
203
|
+
- lib/email_digest/generators/email_digest/install/templates/email_digest.rb
|
|
204
|
+
- lib/email_digest/models/digest_item.rb
|
|
205
|
+
- lib/email_digest/models/digest_preference.rb
|
|
206
|
+
- lib/email_digest/railtie.rb
|
|
207
|
+
- lib/email_digest/services/digest_collector.rb
|
|
208
|
+
- lib/email_digest/services/digest_scheduler.rb
|
|
209
|
+
- lib/email_digest/services/digest_summarizer.rb
|
|
210
|
+
- lib/email_digest/version.rb
|
|
211
|
+
- lib/email_digest/workers/digest_processor_worker.rb
|
|
212
|
+
- lib/email_digest/workers/digest_scheduler_worker.rb
|
|
213
|
+
- lib/email_digest/workers/digest_sender_worker.rb
|
|
214
|
+
homepage: https://github.com/rohitsharma170801/email_digest
|
|
215
|
+
licenses:
|
|
216
|
+
- MIT
|
|
217
|
+
metadata:
|
|
218
|
+
homepage_uri: https://github.com/rohitsharma170801/email_digest
|
|
219
|
+
source_code_uri: https://github.com/rohitsharma170801/email_digest
|
|
220
|
+
changelog_uri: https://github.com/rohitsharma170801/email_digest/blob/main/CHANGELOG.md
|
|
221
|
+
rubygems_mfa_required: 'true'
|
|
222
|
+
post_install_message:
|
|
223
|
+
rdoc_options: []
|
|
224
|
+
require_paths:
|
|
225
|
+
- lib
|
|
226
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
227
|
+
requirements:
|
|
228
|
+
- - ">="
|
|
229
|
+
- !ruby/object:Gem::Version
|
|
230
|
+
version: '0'
|
|
231
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
232
|
+
requirements:
|
|
233
|
+
- - ">="
|
|
234
|
+
- !ruby/object:Gem::Version
|
|
235
|
+
version: '0'
|
|
236
|
+
requirements: []
|
|
237
|
+
rubygems_version: 3.0.3.1
|
|
238
|
+
signing_key:
|
|
239
|
+
specification_version: 4
|
|
240
|
+
summary: A flexible, production-ready email digest system for Rails applications
|
|
241
|
+
test_files: []
|