ahoy_email 0.5.2 → 1.0.0

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