caffeinate 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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