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.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.vscode/settings.json +3 -0
  3. data/ANSWERS_TO_YOUR_QUESTIONS.md +361 -0
  4. data/CHANGELOG.md +14 -0
  5. data/COMPLETE_SETUP_INSTRUCTIONS.md +298 -0
  6. data/CUSTOMIZATION_GUIDE.md +264 -0
  7. data/FAQ.md +133 -0
  8. data/LICENSE.txt +21 -0
  9. data/QUICK_START.md +62 -0
  10. data/README.md +253 -0
  11. data/REPOSITORY_READY.md +146 -0
  12. data/RUBYGEMS_PUBLISHING.md +272 -0
  13. data/SETUP_SEPARATE_GEM_REPO.md +131 -0
  14. data/email_digest.gemspec +52 -0
  15. data/lib/email_digest/concerns/digestable.rb +57 -0
  16. data/lib/email_digest/configuration.rb +40 -0
  17. data/lib/email_digest/generators/email_digest/install/install_generator.rb +50 -0
  18. data/lib/email_digest/generators/email_digest/install/templates/README +16 -0
  19. data/lib/email_digest/generators/email_digest/install/templates/create_email_digest_tables.rb +77 -0
  20. data/lib/email_digest/generators/email_digest/install/templates/email_digest.rb +25 -0
  21. data/lib/email_digest/models/digest_item.rb +49 -0
  22. data/lib/email_digest/models/digest_preference.rb +106 -0
  23. data/lib/email_digest/railtie.rb +9 -0
  24. data/lib/email_digest/services/digest_collector.rb +38 -0
  25. data/lib/email_digest/services/digest_scheduler.rb +55 -0
  26. data/lib/email_digest/services/digest_summarizer.rb +135 -0
  27. data/lib/email_digest/version.rb +5 -0
  28. data/lib/email_digest/workers/digest_processor_worker.rb +64 -0
  29. data/lib/email_digest/workers/digest_scheduler_worker.rb +26 -0
  30. data/lib/email_digest/workers/digest_sender_worker.rb +48 -0
  31. data/lib/email_digest.rb +37 -0
  32. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ VERSION = '1.0.0'
5
+ 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
@@ -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: []