caffeinate 0.2.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3241ca7ca0c6e31220511ad04172105d97058fa84f1de1e6ac442052823a91e0
4
- data.tar.gz: 5b0bda6f08ae89f79cbd64df4c75f48fcb0dacf7471155aae779290b6a1b2e41
3
+ metadata.gz: 3095db5083c76905bf72f39bb96264c4fc90cbe93ef2740614c2275a3a95c87c
4
+ data.tar.gz: 76b7ffad86369d443d44a1bce50a9498547981522c6192d11cb35b300a4ce94e
5
5
  SHA512:
6
- metadata.gz: d43a0030851c7fa107fed83fcc05c9cfa8596be068a8327d01f597a0a45fc84aa403d1a95990e8a4c45013fb44bc899215778d36ce8a8f5cc770db50b33cd0df
7
- data.tar.gz: 6f9e399cc7e0a00bbb544b668d80e0438f4482013f3163a819b9f2ab8ea502f3c8535aacb91d57eb80efc5f12c22bd03a2bdfd36c2216544bb9aa1fd6928e797
6
+ metadata.gz: 0c605b54079ebe483d5ade7afa85298ae4d0983863b498732c137f13cf1bf10621338ab2e0d5887996a7d8fa1b84f4c8bedffdf291efc54931f6093abc650646
7
+ data.tar.gz: 2673a9957170a65695487facc9ba68fdcdd01b7fc441b140a7573f7e69248954548cbe5309751783b7c224073b8b424823c912e57eb9e2887cc6e7cffaa53d3f
data/README.md CHANGED
@@ -6,6 +6,8 @@ Caffeinate tries to make creating and managing timed and scheduled email sequenc
6
6
  and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
7
7
  already familiar with: Ruby on Rails.
8
8
 
9
+ ![Caffeinate logo](logo.png)
10
+
9
11
  ## Usage
10
12
 
11
13
  You can probably imagine seeing a Mailer like this:
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  class CampaignSubscriptionsController < ApplicationController
5
- layout 'caffeinate'
5
+ layout '_caffeinate'
6
6
 
7
7
  helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
8
8
 
@@ -65,15 +65,15 @@ module Caffeinate
65
65
  end
66
66
 
67
67
  # Updates `ended_at` and runs `on_complete` callbacks
68
- def end!
69
- update!(ended_at: ::Caffeinate.config.time_now)
68
+ def end!(reason = nil)
69
+ update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
70
70
 
71
71
  caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
72
72
  end
73
73
 
74
74
  # Updates `unsubscribed_at` and runs `on_subscribe` callbacks
75
- def unsubscribe!
76
- update!(unsubscribed_at: ::Caffeinate.config.time_now)
75
+ def unsubscribe!(reason = nil)
76
+ update!(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
77
77
 
78
78
  caffeinate_campaign.to_dripper.run_callbacks(:on_unsubscribe, self)
79
79
  end
@@ -30,6 +30,13 @@ module Caffeinate
30
30
  scope :skipped, -> { where.not(skipped_at: nil) }
31
31
  scope :unskipped, -> { where(skipped_at: nil) }
32
32
 
33
+ def initialize_dup(args)
34
+ super
35
+ self.send_at = nil
36
+ self.sent_at = nil
37
+ self.skipped_at = nil
38
+ end
39
+
33
40
  # Checks if the Mailing is not skipped and not sent
34
41
  def pending?
35
42
  unskipped? && unsent?
@@ -59,13 +66,13 @@ module Caffeinate
59
66
  def skip!
60
67
  update!(skipped_at: Caffeinate.config.time_now)
61
68
 
62
- caffeinate_campaign.to_dripper.run_callbacks(:on_skip, caffeinate_campaign_subscription, self)
69
+ caffeinate_campaign.to_dripper.run_callbacks(:on_skip, self)
63
70
  end
64
71
 
65
72
  # The associated drip
66
73
  # @todo This can be optimized with a better cache
67
74
  def drip
68
- @drip ||= caffeinate_campaign.to_dripper.drip_collection[mailer_action]
75
+ @drip ||= caffeinate_campaign.to_dripper.drip_collection.for(mailer_action)
69
76
  end
70
77
 
71
78
  # The associated Subscriber from `::Caffeinate::CampaignSubscription`
@@ -80,7 +87,7 @@ module Caffeinate
80
87
 
81
88
  # Assigns attributes to the Mailing from the Drip
82
89
  def from_drip(drip)
83
- self.send_at = drip.send_at
90
+ self.send_at = drip.send_at(self)
84
91
  self.mailer_class = drip.options[:mailer_class]
85
92
  self.mailer_action = drip.action
86
93
  self
@@ -6,6 +6,7 @@ class CreateCaffeinateCampaigns < ActiveRecord::Migration[6.0]
6
6
  create_table :caffeinate_campaigns do |t|
7
7
  t.string :name, null: false
8
8
  t.string :slug, null: false
9
+ t.boolean :active, default: true, null: false
9
10
 
10
11
  t.timestamps
11
12
  end
@@ -12,8 +12,10 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
12
12
  t.string :user_id
13
13
  t.string :token, null: false
14
14
  t.datetime :ended_at
15
+ t.string :ended_reason
15
16
  t.datetime :resubscribed_at
16
17
  t.datetime :unsubscribed_at
18
+ t.string :unsubscribe_reason
17
19
 
18
20
  t.timestamps
19
21
  end
@@ -14,6 +14,9 @@ class CreateCaffeinateMailings < ActiveRecord::Migration[6.0]
14
14
 
15
15
  t.timestamps
16
16
  end
17
- add_index :caffeinate_mailings, %i[campaign_subscription_id mailer_class mailer_action sent_at send_at skipped_at], name: :index_caffeinate_mailings
17
+ add_index :caffeinate_mailings, :sent_at
18
+ add_index :caffeinate_mailings, :send_at
19
+ add_index :caffeinate_mailings, :skipped_at
20
+ add_index :caffeinate_mailings, %i[caffeinate_campaign_subscription_id mailer_class mailer_action], name: :index_caffeinate_mailings
18
21
  end
19
22
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/all'
4
+ require 'caffeinate/mail_ext'
4
5
  require 'caffeinate/engine'
5
6
  require 'caffeinate/drip'
6
7
  require 'caffeinate/url_helpers'
@@ -28,14 +29,4 @@ module Caffeinate
28
29
  def self.setup
29
30
  yield config
30
31
  end
31
-
32
- # The current mailing
33
- def self.current_mailing=(val)
34
- Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY] = val
35
- end
36
-
37
- # The current mailing
38
- def self.current_mailing
39
- Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
40
- end
41
32
  end
@@ -7,7 +7,7 @@ module Caffeinate
7
7
  module Extension
8
8
  def self.included(klass)
9
9
  klass.before_action do
10
- @mailing = Caffeinate.current_mailing if Caffeinate.current_mailing
10
+ @mailing = params[:mailing] if params
11
11
  end
12
12
 
13
13
  klass.helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
@@ -7,10 +7,10 @@ module Caffeinate
7
7
  class Interceptor
8
8
  # Handles `before_send` callbacks for a `Caffeinate::Dripper`
9
9
  def self.delivering_email(message)
10
- mailing = Caffeinate.current_mailing
10
+ mailing = message.caffeinate_mailing
11
11
  return unless mailing
12
12
 
13
- mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing.caffeinate_campaign_subscription, mailing, message)
13
+ mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing, message)
14
14
  drip = mailing.drip
15
15
  message.perform_deliveries = drip.enabled?(mailing)
16
16
  end
@@ -2,15 +2,15 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
- # Handles updating the Caffeinate::Message if it's available in Caffeinate.current_mailing
5
+ # Handles updating the Caffeinate::Message if it's available in Mail::Message.caffeinate_mailing
6
6
  # and runs any associated callbacks
7
7
  class Observer
8
8
  def self.delivered_email(message)
9
- mailing = Caffeinate.current_mailing
9
+ mailing = message.caffeinate_mailing
10
10
  return unless mailing
11
11
 
12
12
  mailing.update!(sent_at: Caffeinate.config.time_now, skipped_at: nil) if message.perform_deliveries
13
- mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing.caffeinate_campaign_subscription, mailing, message)
13
+ mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing, message)
14
14
  end
15
15
  end
16
16
  end
@@ -3,13 +3,14 @@
3
3
  module Caffeinate
4
4
  # Global configuration
5
5
  class Configuration
6
- attr_accessor :now, :async_delivery, :mailing_job, :batch_size
6
+ attr_accessor :now, :async_delivery, :mailing_job, :batch_size, :drippers_path
7
7
 
8
8
  def initialize
9
9
  @now = -> { Time.current }
10
10
  @async_delivery = false
11
11
  @mailing_job = nil
12
12
  @batch_size = 1_000
13
+ @drippers_path = "app/drippers"
13
14
  end
14
15
 
15
16
  def now=(val)
@@ -19,8 +19,20 @@ module Caffeinate
19
19
  options[:using] == :parameterized
20
20
  end
21
21
 
22
- def send_at
23
- options[:delay].from_now
22
+ def send_at(mailing = nil)
23
+ if periodical?
24
+ start = mailing.instance_exec(&options[:start])
25
+ if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count > 0
26
+ start += options[:every]
27
+ end
28
+ start.from_now
29
+ else
30
+ options[:delay].from_now
31
+ end
32
+ end
33
+
34
+ def periodical?
35
+ options[:every].present?
24
36
  end
25
37
 
26
38
  # Checks if the drip is enabled
@@ -8,6 +8,7 @@ require 'caffeinate/dripper/delivery'
8
8
  require 'caffeinate/dripper/drip'
9
9
  require 'caffeinate/dripper/inferences'
10
10
  require 'caffeinate/dripper/perform'
11
+ require 'caffeinate/dripper/periodical'
11
12
  require 'caffeinate/dripper/subscriber'
12
13
 
13
14
  module Caffeinate
@@ -22,7 +23,10 @@ module Caffeinate
22
23
  include Drip
23
24
  include Inferences
24
25
  include Perform
26
+ include Periodical
25
27
  include Subscriber
26
28
  end
27
29
  end
28
30
  end
31
+
32
+ ActiveSupport.run_load_hooks :caffeinate, Caffeinate::Dripper::Base
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- # Includes batch support for setting the batch size for Perform
5
- module Batching
6
- def self.included(klass)
7
- klass.extend ClassMethods
8
- end
9
-
10
- module ClassMethods
11
- def batch_size(num)
12
- @_batch_size = num
4
+ module Dripper
5
+ # Includes batch support for setting the batch size for Perform
6
+ module Batching
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
13
9
  end
14
10
 
15
- def _batch_size
16
- @_batch_size || ::Caffeinate.config.batch_size
11
+ module ClassMethods
12
+ def batch_size=(num)
13
+ @batch_size = num
14
+ end
15
+
16
+ def batch_size
17
+ @batch_size || ::Caffeinate.config.batch_size
18
+ end
17
19
  end
18
20
  end
19
21
  end
@@ -94,9 +94,8 @@ module Caffeinate
94
94
  # Slack.notify(:caffeinate, "#{drip.action_name} is starting")
95
95
  # end
96
96
  #
97
- # @yield Caffeinate::CampaignSubscription
98
- # @yield Caffeinate::Mailing
99
97
  # @yield Caffeinate::Drip current drip
98
+ # @yield Caffeinate::Mailing
100
99
  def before_drip(&block)
101
100
  before_drip_blocks << block
102
101
  end
@@ -112,7 +111,6 @@ module Caffeinate
112
111
  # Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
113
112
  # end
114
113
  #
115
- # @yield Caffeinate::CampaignSubscription
116
114
  # @yield Caffeinate::Mailing
117
115
  # @yield Mail::Message
118
116
  def before_send(&block)
@@ -130,7 +128,6 @@ module Caffeinate
130
128
  # Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
131
129
  # end
132
130
  #
133
- # @yield Caffeinate::CampaignSubscription
134
131
  # @yield Caffeinate::Mailing
135
132
  # @yield Mail::Message
136
133
  def after_send(&block)
@@ -180,7 +177,6 @@ module Caffeinate
180
177
  # Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
181
178
  # end
182
179
  #
183
- # @yield `Caffeinate::CampaignSubscription`
184
180
  # @yield `Caffeinate::Mailing`
185
181
  def on_skip(&block)
186
182
  on_skip_blocks << block
@@ -28,7 +28,7 @@ module Caffeinate
28
28
  # self.name.delete_suffix("Campaign").underscore
29
29
  #
30
30
  # @param [Symbol] slug The slug of a persisted `Caffeinate::Campaign`.
31
- def campaign(slug)
31
+ def campaign=(slug)
32
32
  @caffeinate_campaign = nil
33
33
  @_campaign_slug = slug.to_sym
34
34
  Caffeinate.register_dripper(@_campaign_slug, name)
@@ -40,6 +40,7 @@ module Caffeinate
40
40
 
41
41
  @caffeinate_campaign = ::Caffeinate::Campaign[campaign_slug]
42
42
  end
43
+ alias campaign caffeinate_campaign
43
44
 
44
45
  # The defined slug or the inferred slug
45
46
  def campaign_slug
@@ -24,6 +24,7 @@ module Caffeinate
24
24
  # @param [Hash] options The options to set defaults with
25
25
  # @option options [String] :mailer_class The mailer class
26
26
  def default(options = {})
27
+ options.symbolize_keys!
27
28
  options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
28
29
  @defaults = options
29
30
  end
@@ -14,13 +14,13 @@ module Caffeinate
14
14
  #
15
15
  # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
16
  def deliver!(mailing)
17
- Caffeinate.current_mailing = mailing
18
-
19
- if mailing.drip.parameterized?
20
- mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action).deliver
21
- else
22
- mailing.mailer_class.constantize.send(mailing.mailer_action, mailing).deliver
23
- end
17
+ message = if mailing.drip.parameterized?
18
+ mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action)
19
+ else
20
+ mailing.mailer_class.constantize.send(mailing.mailer_action, mailing)
21
+ end
22
+ message.caffeinate_mailing = mailing
23
+ message.deliver
24
24
  end
25
25
  end
26
26
  end
@@ -13,9 +13,15 @@ module Caffeinate
13
13
  @drips = {}
14
14
  end
15
15
 
16
+ def for(action)
17
+ @drips[action.to_sym]
18
+ end
19
+
16
20
  # Register the drip
17
21
  def register(action, options, &block)
18
- @drips[action.to_s] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
22
+ options = validate_drip_options(action, options)
23
+
24
+ @drips[action.to_sym] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
19
25
  end
20
26
 
21
27
  def each(&block)
@@ -33,6 +39,26 @@ module Caffeinate
33
39
  def [](val)
34
40
  @drips[val]
35
41
  end
42
+
43
+ private
44
+
45
+ def validate_drip_options(action, options)
46
+ options.symbolize_keys!
47
+ options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer)
48
+ options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
49
+ options[:using] ||= @dripper.defaults[:using]
50
+ options[:step] ||= @dripper.drips.size + 1
51
+
52
+ if options[:mailer_class].nil?
53
+ raise ArgumentError, "You must define :mailer_class or :mailer in the options for #{action.inspect} on #{@dripper.inspect}"
54
+ end
55
+
56
+ if options[:every].nil? && options[:delay].nil?
57
+ raise ArgumentError, "You must define :delay in the options for #{action.inspect} on #{@dripper.inspect}"
58
+ end
59
+
60
+ options
61
+ end
36
62
  end
37
63
 
38
64
  # :nodoc:
@@ -60,17 +86,8 @@ module Caffeinate
60
86
  # @option options [String] :mailer_class The mailer_class
61
87
  # @option options [Integer] :step The order in which the drip is executed
62
88
  # @option options [ActiveSupport::Duration] :delay When the drip should be ran
89
+ # @option options [Symbol] :using set to :parameters if the mailer action uses ActionMailer::Parameters
63
90
  def drip(action_name, options = {}, &block)
64
- options.assert_valid_keys(:mailer_class, :step, :delay, :using, :mailer)
65
- options[:mailer_class] ||= options[:mailer] || defaults[:mailer_class]
66
- options[:using] ||= defaults[:using]
67
- options[:step] ||= drips.size + 1
68
-
69
- if options[:mailer_class].nil?
70
- raise ArgumentError, "You must define :mailer_class or :mailer in the options for :#{action_name}"
71
- end
72
- raise ArgumentError, "You must define :delay in the options for :#{action_name}" if options[:delay].nil?
73
-
74
91
  drip_collection.register(action_name, options, &block)
75
92
  end
76
93
  end
@@ -9,19 +9,31 @@ module Caffeinate
9
9
  klass.extend ClassMethods
10
10
  end
11
11
 
12
- # Delivers the next_caffeinate_mailer for the campaign's subscribers.
13
- #
14
- # Handles with batches based on batch_size.
12
+ # Delivers the next_caffeinate_mailer for the campaign's subscribers. Handles with batches based on `batch_size`.
15
13
  #
16
14
  # OrderDripper.new.perform!
17
15
  #
18
16
  # @return nil
19
17
  def perform!
18
+ includes = [:next_caffeinate_mailing]
19
+ preloads = []
20
+ if ::Caffeinate.config.async_delivery?
21
+ # nothing
22
+ else
23
+ preloads += %i[subscriber user]
24
+ end
25
+
20
26
  run_callbacks(:before_process, self)
21
- campaign.caffeinate_campaign_subscriptions.active.in_batches(of: self.class._batch_size).each do |batch|
27
+ campaign.caffeinate_campaign_subscriptions
28
+ .active
29
+ .joins(:next_caffeinate_mailing)
30
+ .preload(*preloads)
31
+ .includes(*includes)
32
+ .in_batches(of: self.class.batch_size)
33
+ .each do |batch|
22
34
  run_callbacks(:on_process, self, batch)
23
35
  batch.each do |subscriber|
24
- subscriber.next_caffeinate_mailing&.process!
36
+ subscriber.next_caffeinate_mailing.process!
25
37
  end
26
38
  end
27
39
  run_callbacks(:after_process, self)
@@ -0,0 +1,24 @@
1
+ module Caffeinate
2
+ module Dripper
3
+ module Periodical
4
+ def self.included(klass)
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def periodical(action_name, every:, start: -> { ::Caffeinate.config.time_now }, **options, &block)
10
+ options[:start] = start
11
+ options[:every] = every
12
+ drip(action_name, options, &block)
13
+ after_send do |mailing, _message|
14
+ if mailing.drip.action == action_name
15
+ next_mailing = mailing.dup
16
+ next_mailing.send_at = mailing.drip.send_at(mailing)
17
+ next_mailing.save!
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -10,6 +10,12 @@ module Caffeinate
10
10
  isolate_namespace Caffeinate
11
11
  config.eager_load_namespaces << Caffeinate
12
12
 
13
+ config.to_prepare do
14
+ Dir.glob(Rails.root.join(Caffeinate.config.drippers_path, "**", "*.rb")).each do |dripper|
15
+ require dripper
16
+ end
17
+ end
18
+
13
19
  ActiveSupport.on_load(:action_mailer) do
14
20
  include ::Caffeinate::ActionMailer::Extension
15
21
  ::ActionMailer::Base.register_interceptor(::Caffeinate::ActionMailer::Interceptor)
@@ -21,7 +27,7 @@ module Caffeinate
21
27
  end
22
28
 
23
29
  ActiveSupport.on_load(:action_view) do
24
- ApplicationHelper.include ::Caffeinate::Helpers
30
+ ActionView::Base.include ::Caffeinate::Helpers
25
31
  end
26
32
  end
27
33
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ # Extend Mail::Message to account for a Caffeinate::Mailing
5
+ class Message
6
+ attr_accessor :caffeinate_mailing
7
+
8
+ def caffeinate?
9
+ caffeinate_mailing.present?
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.2.1'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -20,7 +20,7 @@ module Caffeinate
20
20
  end
21
21
 
22
22
  def install_routes
23
- inject_into_file 'config/routes.rb', "\n mount ::Caffeinate::Engine => '/caffeinate", after: /Rails.application.routes.draw do/
23
+ inject_into_file 'config/routes.rb', "\n mount ::Caffeinate::Engine => '/caffeinate'", after: /Rails.application.routes.draw do/
24
24
  end
25
25
 
26
26
  # :nodoc:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caffeinate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Brody
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-28 00:00:00.000000000 Z
11
+ date: 2020-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -126,7 +126,7 @@ files:
126
126
  - app/models/caffeinate/mailing.rb
127
127
  - app/views/caffeinate/campaign_subscriptions/subscribe.html.erb
128
128
  - app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb
129
- - app/views/layouts/caffeinate.html.erb
129
+ - app/views/layouts/_caffeinate.html.erb
130
130
  - config/locales/en.yml
131
131
  - config/routes.rb
132
132
  - db/migrate/20201124183102_create_caffeinate_campaigns.rb
@@ -151,9 +151,11 @@ files:
151
151
  - lib/caffeinate/dripper/drip.rb
152
152
  - lib/caffeinate/dripper/inferences.rb
153
153
  - lib/caffeinate/dripper/perform.rb
154
+ - lib/caffeinate/dripper/periodical.rb
154
155
  - lib/caffeinate/dripper/subscriber.rb
155
156
  - lib/caffeinate/engine.rb
156
157
  - lib/caffeinate/helpers.rb
158
+ - lib/caffeinate/mail_ext.rb
157
159
  - lib/caffeinate/url_helpers.rb
158
160
  - lib/caffeinate/version.rb
159
161
  - lib/generators/caffeinate/install_generator.rb