ahoy_email 0.5.2 → 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.
@@ -2,6 +2,6 @@ module Ahoy
2
2
  class Message < ActiveRecord::Base
3
3
  self.table_name = "ahoy_messages"
4
4
 
5
- belongs_to :user, AhoyEmail.belongs_to.merge(polymorphic: true)
5
+ belongs_to :user, (ActiveRecord::VERSION::MAJOR >= 5 ? {optional: true} : {}).merge(polymorphic: true)
6
6
  end
7
7
  end
data/config/routes.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  Rails.application.routes.draw do
2
- unless respond_to?(:has_named_route?) && has_named_route?("ahoy_email_engine")
3
- mount AhoyEmail::Engine => "/ahoy"
4
- end
2
+ mount AhoyEmail::Engine => "/ahoy" if AhoyEmail.api
5
3
  end
6
4
 
7
5
  AhoyEmail::Engine.routes.draw do
data/lib/ahoy_email.rb CHANGED
@@ -1,45 +1,69 @@
1
+ # dependencies
1
2
  require "active_support"
2
- require "nokogiri"
3
3
  require "addressable/uri"
4
+ require "nokogiri"
4
5
  require "openssl"
5
6
  require "safely/core"
7
+
8
+ # modules
6
9
  require "ahoy_email/processor"
10
+ require "ahoy_email/tracker"
7
11
  require "ahoy_email/interceptor"
8
12
  require "ahoy_email/mailer"
9
- require "ahoy_email/engine"
10
13
  require "ahoy_email/version"
14
+ require "ahoy_email/engine" if defined?(Rails)
11
15
 
12
16
  module AhoyEmail
13
- mattr_accessor :secret_token, :options, :subscribers, :belongs_to, :invalid_redirect_url
17
+ mattr_accessor :secret_token, :default_options, :subscribers, :invalid_redirect_url, :track_method, :api, :preserve_callbacks
18
+ mattr_writer :message_model
14
19
 
15
- self.options = {
20
+ self.api = false
21
+
22
+ self.default_options = {
16
23
  message: true,
17
- open: true,
18
- click: true,
19
- utm_params: true,
20
- utm_source: ->(message, mailer) { mailer.mailer_name },
24
+ open: false,
25
+ click: false,
26
+ utm_params: false,
27
+ utm_source: -> { mailer_name },
21
28
  utm_medium: "email",
22
29
  utm_term: nil,
23
30
  utm_content: nil,
24
- utm_campaign: ->(message, mailer) { mailer.action_name },
25
- user: ->(message, mailer) { (message.to.size == 1 ? User.where(email: message.to.first).first : nil) rescue nil },
26
- mailer: ->(message, mailer) { "#{mailer.class.name}##{mailer.action_name}" },
31
+ utm_campaign: -> { action_name },
32
+ user: -> { @user || (respond_to?(:params) && params && params[:user]) || (message.to.size == 1 ? (User.find_by(email: message.to.first) rescue nil) : nil) },
33
+ mailer: -> { "#{self.class.name}##{action_name}" },
27
34
  url_options: {},
28
- heuristic_parse: false
35
+ extra: {},
36
+ unsubscribe_links: false
29
37
  }
30
38
 
31
- self.subscribers = []
39
+ self.track_method = lambda do |data|
40
+ message = data[:message]
32
41
 
33
- self.belongs_to = {}
42
+ ahoy_message = AhoyEmail.message_model.new
43
+ ahoy_message.to = Array(message.to).join(", ") if ahoy_message.respond_to?(:to=)
44
+ ahoy_message.user_type = data[:user_type]
45
+ ahoy_message.user_id = data[:user_id]
34
46
 
35
- def self.track(options)
36
- self.options = self.options.merge(options)
37
- end
47
+ ahoy_message.mailer = data[:mailer] if ahoy_message.respond_to?(:mailer=)
48
+ ahoy_message.subject = message.subject if ahoy_message.respond_to?(:subject=)
49
+ ahoy_message.content = message.encoded if ahoy_message.respond_to?(:content=)
38
50
 
39
- class << self
40
- attr_writer :message_model
51
+ AhoyEmail::Processor::UTM_PARAMETERS.each do |k|
52
+ ahoy_message.send("#{k}=", data[k.to_sym]) if ahoy_message.respond_to?("#{k}=")
53
+ end
54
+
55
+ ahoy_message.token = data[:token] if ahoy_message.respond_to?(:token=)
56
+
57
+ ahoy_message.assign_attributes(data[:extra] || {})
58
+
59
+ ahoy_message.sent_at = Time.now
60
+ ahoy_message.save!
41
61
  end
42
62
 
63
+ self.subscribers = []
64
+
65
+ self.preserve_callbacks = []
66
+
43
67
  def self.message_model
44
68
  model = (defined?(@message_model) && @message_model) || ::Ahoy::Message
45
69
  model = model.call if model.respond_to?(:call)
@@ -15,8 +15,6 @@ module AhoyEmail
15
15
 
16
16
  creds.respond_to?(:secret_key_base) ? creds.secret_key_base : creds.secret_token
17
17
  end
18
-
19
- AhoyEmail.belongs_to = {optional: true} if Rails::VERSION::MAJOR >= 5
20
18
  end
21
19
  end
22
20
  end
@@ -2,7 +2,7 @@ module AhoyEmail
2
2
  class Interceptor
3
3
  class << self
4
4
  def delivering_email(message)
5
- AhoyEmail::Processor.new(message).track_send
5
+ AhoyEmail::Tracker.new(message).perform
6
6
  end
7
7
  end
8
8
  end
@@ -1,33 +1,39 @@
1
1
  module AhoyEmail
2
2
  module Mailer
3
- def self.included(base)
4
- base.extend ClassMethods
5
- base.prepend InstanceMethods
6
- base.class_eval do
7
- attr_accessor :ahoy_options
8
- class_attribute :ahoy_options
9
- self.ahoy_options = {}
10
- end
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_writer :ahoy_options
7
+ after_action :save_ahoy_options
11
8
  end
12
9
 
13
- module ClassMethods
14
- def track(options = {})
15
- self.ahoy_options = ahoy_options.merge(message: true).merge(options)
10
+ class_methods do
11
+ def track(**options)
12
+ before_action(options.slice(:only, :except)) do
13
+ self.ahoy_options = ahoy_options.merge(message: true).merge(options.except(:only, :except))
14
+ end
16
15
  end
17
16
  end
18
17
 
19
- module InstanceMethods
20
- def track(options = {})
21
- self.ahoy_options = (ahoy_options || {}).merge(message: true).merge(options)
22
- end
18
+ def track(**options)
19
+ self.ahoy_options = ahoy_options.merge(message: true).merge(options)
20
+ end
21
+
22
+ def ahoy_options
23
+ @ahoy_options ||= AhoyEmail.default_options
24
+ end
23
25
 
24
- def mail(headers = {}, &block)
25
- # this mimics what original method does
26
- return message if @_mail_was_called && headers.blank? && !block
26
+ def save_ahoy_options
27
+ if ahoy_options[:message]
28
+ Safely.safely do
29
+ options = {}
30
+ ahoy_options.each do |k, v|
31
+ # execute options in mailer content
32
+ options[k] = v.respond_to?(:call) ? instance_exec(&v) : v
33
+ end
27
34
 
28
- message = super
29
- AhoyEmail::Processor.new(message, self).process
30
- message
35
+ AhoyEmail::Processor.new(self, options).perform
36
+ end
31
37
  end
32
38
  end
33
39
  end
@@ -1,74 +1,57 @@
1
1
  module AhoyEmail
2
2
  class Processor
3
- attr_reader :message, :mailer, :ahoy_message
3
+ attr_reader :mailer, :options
4
4
 
5
5
  UTM_PARAMETERS = %w(utm_source utm_medium utm_term utm_content utm_campaign)
6
6
 
7
- def initialize(message, mailer = nil)
8
- @message = message
7
+ def initialize(mailer, options)
9
8
  @mailer = mailer
10
- end
11
-
12
- def process
13
- Safely.safely do
14
- action_name = mailer.action_name.to_sym
15
- if options[:message] && (!options[:only] || options[:only].include?(action_name)) && !options[:except].to_a.include?(action_name)
16
- @ahoy_message = AhoyEmail.message_model.new
17
- ahoy_message.token = generate_token
18
- ahoy_message.to = Array(message.to).join(", ") if ahoy_message.respond_to?(:to=)
19
- ahoy_message.user = options[:user]
9
+ @options = options
20
10
 
21
- track_open if options[:open]
22
- track_links if options[:utm_params] || options[:click]
11
+ unknown_keywords = options.keys - AhoyEmail.default_options.keys
12
+ raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
13
+ end
23
14
 
24
- ahoy_message.mailer = options[:mailer] if ahoy_message.respond_to?(:mailer=)
25
- ahoy_message.subject = message.subject if ahoy_message.respond_to?(:subject=)
26
- ahoy_message.content = message.to_s if ahoy_message.respond_to?(:content=)
15
+ def perform
16
+ track_open if options[:open]
17
+ track_links if options[:utm_params] || options[:click]
18
+ track_message
19
+ end
27
20
 
28
- UTM_PARAMETERS.each do |k|
29
- ahoy_message.send("#{k}=", options[k.to_sym]) if ahoy_message.respond_to?("#{k}=")
30
- end
21
+ protected
31
22
 
32
- ahoy_message.assign_attributes(options[:extra] || {})
23
+ def message
24
+ mailer.message
25
+ end
33
26
 
34
- ahoy_message.save!
35
- message["Ahoy-Message-Id"] = ahoy_message.id.to_s
36
- end
37
- end
27
+ def token
28
+ @token ||= SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32)
38
29
  end
39
30
 
40
- def track_send
41
- Safely.safely do
42
- if (message_id = message["Ahoy-Message-Id"]) && message.perform_deliveries
43
- ahoy_message = AhoyEmail.message_model.where(id: message_id.to_s).first
44
- if ahoy_message
45
- ahoy_message.sent_at = Time.now
46
- ahoy_message.save
47
- end
48
- message["Ahoy-Message-Id"] = nil
49
- end
31
+ def track_message
32
+ data = {
33
+ mailer: options[:mailer],
34
+ extra: options[:extra]
35
+ }
36
+
37
+ user = options[:user]
38
+ if user
39
+ data[:user_type] = user.model_name.name
40
+ id = user.id
41
+ data[:user_id] = id.is_a?(Integer) ? id : id.to_s
50
42
  end
51
- end
52
43
 
53
- protected
44
+ if options[:open] || options[:click]
45
+ data[:token] = token
46
+ end
54
47
 
55
- def options
56
- @options ||= begin
57
- options = AhoyEmail.options.merge(mailer.class.ahoy_options)
58
- if mailer.ahoy_options
59
- options = options.except(:only, :except).merge(mailer.ahoy_options)
48
+ if options[:utm_params]
49
+ UTM_PARAMETERS.each do |k|
50
+ data[k] = options[k.to_sym] if options[k.to_sym]
60
51
  end
61
- options.each do |k, v|
62
- if v.respond_to?(:call)
63
- options[k] = v.call(message, mailer)
64
- end
65
- end
66
- options
67
52
  end
68
- end
69
53
 
70
- def generate_token
71
- SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32)
54
+ mailer.message["Ahoy-Message"] = data.to_json
72
55
  end
73
56
 
74
57
  def track_open
@@ -79,7 +62,7 @@ module AhoyEmail
79
62
  url_for(
80
63
  controller: "ahoy/messages",
81
64
  action: "open",
82
- id: ahoy_message.token,
65
+ id: token,
83
66
  format: "gif"
84
67
  )
85
68
  pixel = ActionController::Base.helpers.image_tag(url, size: "1x1", alt: "")
@@ -113,12 +96,13 @@ module AhoyEmail
113
96
  end
114
97
 
115
98
  if options[:click] && !skip_attribute?(link, "click")
116
- signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha1"), AhoyEmail.secret_token, link["href"])
99
+ # TODO sign more than just url and transition to HMAC-SHA256
100
+ signature = OpenSSL::HMAC.hexdigest("SHA1", AhoyEmail.secret_token, link["href"])
117
101
  link["href"] =
118
102
  url_for(
119
103
  controller: "ahoy/messages",
120
104
  action: "click",
121
- id: ahoy_message.token,
105
+ id: token,
122
106
  url: link["href"],
123
107
  signature: signature
124
108
  )
@@ -157,11 +141,7 @@ module AhoyEmail
157
141
  # Return uri if valid, nil otherwise
158
142
  def parse_uri(href)
159
143
  # to_s prevent to return nil from this method
160
- if options[:heuristic_parse]
161
- Addressable::URI.heuristic_parse(href.to_s) rescue nil
162
- else
163
- Addressable::URI.parse(href.to_s) rescue nil
164
- end
144
+ Addressable::URI.heuristic_parse(href.to_s) rescue nil
165
145
  end
166
146
 
167
147
  def url_for(opt)
@@ -0,0 +1,21 @@
1
+ module AhoyEmail
2
+ class Tracker
3
+ attr_reader :message
4
+
5
+ def initialize(message)
6
+ @message = message
7
+ end
8
+
9
+ def perform
10
+ if message.perform_deliveries && (data_header = message["Ahoy-Message"])
11
+ Safely.safely do
12
+ data = JSON.parse(data_header.to_s).symbolize_keys
13
+ data[:message] = message
14
+ AhoyEmail.track_method.call(data)
15
+ end
16
+ end
17
+ ensure
18
+ message["Ahoy-Message"] = nil if message["Ahoy-Message"]
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module AhoyEmail
2
- VERSION = "0.5.2"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :ahoy_messages do |t|
4
+ t.references :user, polymorphic: true
5
+ t.text :to
6
+ t.string :mailer
7
+ t.text :subject
8
+ t.timestamp :sent_at
9
+ end
10
+ end
11
+ end
metadata CHANGED
@@ -1,45 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ahoy_email
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-26 00:00:00.000000000 Z
11
+ date: 2018-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: railties
14
+ name: actionmailer
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '4.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '4.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: actionmailer
28
+ name: addressable
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 2.3.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 2.3.2
41
41
  - !ruby/object:Gem::Dependency
42
- name: activerecord
42
+ name: nokogiri
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,27 +53,27 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: addressable
56
+ name: safely_block
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 2.3.2
61
+ version: 0.1.1
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 2.3.2
68
+ version: 0.1.1
69
69
  - !ruby/object:Gem::Dependency
70
- name: nokogiri
70
+ name: bundler
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
- type: :runtime
76
+ type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
@@ -81,21 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: safely_block
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: 0.1.1
90
- type: :runtime
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: 0.1.1
97
- - !ruby/object:Gem::Dependency
98
- name: bundler
84
+ name: rake
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
101
87
  - - ">="
@@ -109,7 +95,7 @@ dependencies:
109
95
  - !ruby/object:Gem::Version
110
96
  version: '0'
111
97
  - !ruby/object:Gem::Dependency
112
- name: rake
98
+ name: minitest
113
99
  requirement: !ruby/object:Gem::Requirement
114
100
  requirements:
115
101
  - - ">="
@@ -123,7 +109,7 @@ dependencies:
123
109
  - !ruby/object:Gem::Version
124
110
  version: '0'
125
111
  - !ruby/object:Gem::Dependency
126
- name: minitest
112
+ name: activerecord
127
113
  requirement: !ruby/object:Gem::Requirement
128
114
  requirements:
129
115
  - - ">="
@@ -179,20 +165,15 @@ dependencies:
179
165
  - !ruby/object:Gem::Version
180
166
  version: '0'
181
167
  description:
182
- email:
183
- - andrew@chartkick.com
168
+ email: andrew@chartkick.com
184
169
  executables: []
185
170
  extensions: []
186
171
  extra_rdoc_files: []
187
172
  files:
188
- - ".gitignore"
189
- - ".travis.yml"
190
173
  - CHANGELOG.md
191
- - Gemfile
174
+ - CONTRIBUTING.md
192
175
  - LICENSE.txt
193
176
  - README.md
194
- - Rakefile
195
- - ahoy_email.gemspec
196
177
  - app/controllers/ahoy/messages_controller.rb
197
178
  - app/models/ahoy/message.rb
198
179
  - config/routes.rb
@@ -201,17 +182,10 @@ files:
201
182
  - lib/ahoy_email/interceptor.rb
202
183
  - lib/ahoy_email/mailer.rb
203
184
  - lib/ahoy_email/processor.rb
185
+ - lib/ahoy_email/tracker.rb
204
186
  - lib/ahoy_email/version.rb
205
187
  - lib/generators/ahoy_email/install_generator.rb
206
- - lib/generators/ahoy_email/templates/install.rb
207
- - test/gemfiles/actionmailer42.gemfile
208
- - test/gemfiles/actionmailer50.gemfile
209
- - test/gemfiles/actionmailer51.gemfile
210
- - test/internal/config/database.yml
211
- - test/internal/config/routes.rb
212
- - test/internal/db/schema.rb
213
- - test/mailer_test.rb
214
- - test/test_helper.rb
188
+ - lib/generators/ahoy_email/templates/install.rb.tt
215
189
  homepage: https://github.com/ankane/ahoy_email
216
190
  licenses:
217
191
  - MIT
@@ -224,7 +198,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
224
198
  requirements:
225
199
  - - ">="
226
200
  - !ruby/object:Gem::Version
227
- version: 2.2.0
201
+ version: '2.2'
228
202
  required_rubygems_version: !ruby/object:Gem::Requirement
229
203
  requirements:
230
204
  - - ">="
@@ -232,16 +206,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
232
206
  version: '0'
233
207
  requirements: []
234
208
  rubyforge_project:
235
- rubygems_version: 2.7.6
209
+ rubygems_version: 2.7.7
236
210
  signing_key:
237
211
  specification_version: 4
238
- summary: Simple, powerful email tracking for Rails
239
- test_files:
240
- - test/gemfiles/actionmailer42.gemfile
241
- - test/gemfiles/actionmailer50.gemfile
242
- - test/gemfiles/actionmailer51.gemfile
243
- - test/internal/config/database.yml
244
- - test/internal/config/routes.rb
245
- - test/internal/db/schema.rb
246
- - test/mailer_test.rb
247
- - test/test_helper.rb
212
+ summary: Email analytics for Rails
213
+ test_files: []