ahoy_email 0.0.1 → 0.0.2
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 +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +92 -5
- data/ahoy_email.gemspec +2 -0
- data/app/controllers/ahoy/messages_controller.rb +26 -1
- data/app/models/ahoy/message.rb +3 -0
- data/config/routes.rb +5 -2
- data/lib/ahoy_email.rb +47 -0
- data/lib/ahoy_email/engine.rb +5 -1
- data/lib/ahoy_email/interceptor.rb +5 -18
- data/lib/ahoy_email/processor.rb +143 -0
- data/lib/ahoy_email/version.rb +1 -1
- data/lib/generators/ahoy_email/templates/install.rb +4 -1
- metadata +32 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c6f895cc438acdeb4982e684a4ba32efc86c05b
|
|
4
|
+
data.tar.gz: 19750f27ae10e0828b4f3e7813c3a236413fa1b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88ba353c711f596a745a8c406a222859c517c1392571791d91c833713d46a56a15f313428eae6af39e042b7c8ffc8aa1b75db969d1f950def5b824bb6b95fe19
|
|
7
|
+
data.tar.gz: 082bba0e14fab78aba984e1d3e1b94ce08841c092ff24e834804bf15478c8cc02c2050df8ab1c15701530158d30954222261f86c7ac09abcbc24bca6ee4a62be
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
|
@@ -27,15 +27,102 @@ rake db:migrate
|
|
|
27
27
|
|
|
28
28
|
## How It Works
|
|
29
29
|
|
|
30
|
-
Ahoy creates an `Ahoy::Message` record when an email is sent.
|
|
30
|
+
Ahoy creates an `Ahoy::Message` record when an email is sent.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
### Open
|
|
33
|
+
|
|
34
|
+
An invisible pixel is added right before the closing `</body>` tag to HTML emails.
|
|
35
|
+
|
|
36
|
+
If a recipient has images enabled in his / her email client, the pixel is loaded and an open is recorded.
|
|
37
|
+
|
|
38
|
+
### Click
|
|
39
|
+
|
|
40
|
+
Links in HTML emails are rewritten to pass through your server.
|
|
41
|
+
|
|
42
|
+
````
|
|
43
|
+
http://chartkick.com
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
becomes
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
http://yoursite.com/ahoy/messages/rAnDoMtOken/click?url=http%3A%2F%2Fchartkick.com&signature=...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A signature is added to prevent [open redirects](https://www.owasp.org/index.php/Open_redirect).
|
|
53
|
+
|
|
54
|
+
Keep specific links from being tracked with `<a data-disable-tracking="true" href="..."></a>`.
|
|
55
|
+
|
|
56
|
+
### UTM Parameters
|
|
57
|
+
|
|
58
|
+
UTM parameters are added to each link if they don’t already exist.
|
|
59
|
+
|
|
60
|
+
By default, `utm_medium` is set to `email`.
|
|
61
|
+
|
|
62
|
+
### User
|
|
63
|
+
|
|
64
|
+
To specify the user, use:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class UserMailer < ActionMailer::Base
|
|
68
|
+
def welcome_email(user)
|
|
69
|
+
# ...
|
|
70
|
+
ahoy user: user
|
|
71
|
+
mail to: user.email
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
User is [polymorphic](http://railscasts.com/episodes/154-polymorphic-association), so use it with any model.
|
|
77
|
+
|
|
78
|
+
## Customize
|
|
79
|
+
|
|
80
|
+
There are 3 places to set options.
|
|
81
|
+
|
|
82
|
+
### Global
|
|
83
|
+
|
|
84
|
+
The defaults are listed below.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
AhoyEmail.options = {
|
|
88
|
+
create_message: true,
|
|
89
|
+
track_open: true,
|
|
90
|
+
track_click: true,
|
|
91
|
+
utm_source: nil,
|
|
92
|
+
utm_medium: "email",
|
|
93
|
+
utm_term: nil,
|
|
94
|
+
utm_content: nil,
|
|
95
|
+
utm_campaign: nil
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Mailers
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class UserMailer < ActionMailer::Base
|
|
103
|
+
ahoy utm_campaign: "boom"
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Action
|
|
108
|
+
|
|
109
|
+
``` ruby
|
|
110
|
+
class UserMailer < ActionMailer::Base
|
|
111
|
+
def welcome_email(user)
|
|
112
|
+
# ...
|
|
113
|
+
ahoy user: user
|
|
114
|
+
mail to: user.email
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
35
118
|
|
|
36
119
|
## TODO
|
|
37
120
|
|
|
38
|
-
- Subscription management (lists, opt-outs)
|
|
121
|
+
- Subscription management (lists, opt-outs) [separate gem]
|
|
122
|
+
|
|
123
|
+
## History
|
|
124
|
+
|
|
125
|
+
View the [changelog](https://github.com/ankane/ahoy_email/blob/master/CHANGELOG.md)
|
|
39
126
|
|
|
40
127
|
## Contributing
|
|
41
128
|
|
data/ahoy_email.gemspec
CHANGED
|
@@ -19,6 +19,8 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
21
|
spec.add_dependency "actionmailer"
|
|
22
|
+
spec.add_dependency "addressable"
|
|
23
|
+
spec.add_dependency "nokogiri"
|
|
22
24
|
|
|
23
25
|
spec.add_development_dependency "bundler", "~> 1.6"
|
|
24
26
|
spec.add_development_dependency "rake"
|
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
module Ahoy
|
|
2
2
|
class MessagesController < ActionController::Base
|
|
3
|
+
before_filter :set_message
|
|
3
4
|
|
|
4
5
|
def open
|
|
5
|
-
|
|
6
|
+
if @message and !@message.opened_at
|
|
7
|
+
@message.opened_at = Time.now
|
|
8
|
+
@message.save!
|
|
9
|
+
end
|
|
10
|
+
send_data Base64.decode64("R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="), type: "image/gif", disposition: "inline"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def click
|
|
14
|
+
if @message and !@message.clicked_at
|
|
15
|
+
@message.clicked_at = Time.now
|
|
16
|
+
@message.save!
|
|
17
|
+
end
|
|
18
|
+
url = params[:url]
|
|
19
|
+
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new("sha1"), AhoyEmail.secret_token, url)
|
|
20
|
+
if params[:signature] == signature
|
|
21
|
+
redirect_to url
|
|
22
|
+
else
|
|
23
|
+
redirect_to main_app.root_url
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def set_message
|
|
30
|
+
@message = Ahoy::Message.where(token: params[:id]).first
|
|
6
31
|
end
|
|
7
32
|
|
|
8
33
|
end
|
data/app/models/ahoy/message.rb
CHANGED
data/config/routes.rb
CHANGED
|
@@ -3,7 +3,10 @@ Rails.application.routes.draw do
|
|
|
3
3
|
end
|
|
4
4
|
|
|
5
5
|
AhoyEmail::Engine.routes.draw do
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
scope module: "ahoy" do
|
|
7
|
+
resources :messages, only: [] do
|
|
8
|
+
get :open, on: :member
|
|
9
|
+
get :click, on: :member
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
end
|
data/lib/ahoy_email.rb
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
require "ahoy_email/version"
|
|
2
2
|
require "action_mailer"
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "addressable/uri"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "ahoy_email/processor"
|
|
3
7
|
require "ahoy_email/interceptor"
|
|
4
8
|
require "ahoy_email/engine"
|
|
5
9
|
|
|
6
10
|
ActionMailer::Base.register_interceptor AhoyEmail::Interceptor
|
|
11
|
+
|
|
12
|
+
module AhoyEmail
|
|
13
|
+
mattr_accessor :secret_token, :options
|
|
14
|
+
|
|
15
|
+
self.options = {
|
|
16
|
+
create_message: true,
|
|
17
|
+
track_open: true,
|
|
18
|
+
track_click: true,
|
|
19
|
+
utm_source: nil,
|
|
20
|
+
utm_medium: "email",
|
|
21
|
+
utm_term: nil,
|
|
22
|
+
utm_content: nil,
|
|
23
|
+
utm_campaign: nil
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module ActionMailer
|
|
28
|
+
class Base
|
|
29
|
+
class_attribute :ahoy_options
|
|
30
|
+
self.ahoy_options = {}
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def ahoy(options)
|
|
34
|
+
self.ahoy_options = ahoy_options.merge(options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ahoy(options)
|
|
39
|
+
@ahoy_options = (@ahoy_options || {}).merge(options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mail_with_ahoy(headers = {}, &block)
|
|
43
|
+
message = mail_without_ahoy(headers, &block)
|
|
44
|
+
|
|
45
|
+
options = AhoyEmail.options.merge(self.class.ahoy_options).merge(@ahoy_options || {})
|
|
46
|
+
AhoyEmail::Processor.new(message, options).process!
|
|
47
|
+
|
|
48
|
+
message
|
|
49
|
+
end
|
|
50
|
+
alias_method_chain :mail, :ahoy
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/ahoy_email/engine.rb
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
module AhoyEmail
|
|
2
2
|
class Interceptor
|
|
3
|
-
|
|
3
|
+
class << self
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# if body
|
|
9
|
-
# regex = /<\/body>/i
|
|
10
|
-
# pixel = image_tag(AhoyEmail::Engine.routes.url_helpers.url_for(controller: "messages", action: "open"))
|
|
11
|
-
# if body.match(regex)
|
|
12
|
-
# body.gsub!(regex, "#{pixel}\\0")
|
|
13
|
-
# else
|
|
14
|
-
# body << pixel
|
|
15
|
-
# end
|
|
16
|
-
# end
|
|
17
|
-
Ahoy::Message.create!(
|
|
18
|
-
subject: message.subject,
|
|
19
|
-
content: message.to_s
|
|
20
|
-
)
|
|
21
|
-
end
|
|
5
|
+
def delivering_email(message)
|
|
6
|
+
AhoyEmail::Processor.new(message).mark_sent!
|
|
7
|
+
end
|
|
22
8
|
|
|
9
|
+
end
|
|
23
10
|
end
|
|
24
11
|
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module AhoyEmail
|
|
2
|
+
class Processor
|
|
3
|
+
include ActionView::Helpers::AssetTagHelper
|
|
4
|
+
|
|
5
|
+
attr_reader :message, :options, :ahoy_message
|
|
6
|
+
|
|
7
|
+
def initialize(message, options = {})
|
|
8
|
+
@message = message
|
|
9
|
+
@options = options
|
|
10
|
+
@ahoy_message = Ahoy::Message.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def process!
|
|
14
|
+
if options[:create_message]
|
|
15
|
+
ahoy_message.token = generate_token
|
|
16
|
+
ahoy_message.user = options[:user]
|
|
17
|
+
|
|
18
|
+
track_utm_parameters!
|
|
19
|
+
track_open! if options[:track_open]
|
|
20
|
+
track_click! if options[:track_click]
|
|
21
|
+
|
|
22
|
+
# save
|
|
23
|
+
ahoy_message.subject = message.subject if ahoy_message.respond_to?(:subject=)
|
|
24
|
+
ahoy_message.content = message.to_s if ahoy_message.respond_to?(:content=)
|
|
25
|
+
ahoy_message.sent_at = Time.now
|
|
26
|
+
ahoy_message.save
|
|
27
|
+
end
|
|
28
|
+
rescue => e
|
|
29
|
+
report_error(e)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mark_sent!
|
|
33
|
+
if (message_id = message["Ahoy-Message-Id"])
|
|
34
|
+
ahoy_message = Ahoy::Message.where(id: message_id).first
|
|
35
|
+
if ahoy_message
|
|
36
|
+
ahoy_message.sent_at = Time.now
|
|
37
|
+
ahoy_message.save
|
|
38
|
+
end
|
|
39
|
+
message["Ahoy-Message-Id"] = nil
|
|
40
|
+
end
|
|
41
|
+
rescue => e
|
|
42
|
+
report_error(e)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
def generate_token
|
|
48
|
+
SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def track_utm_parameters!
|
|
52
|
+
if html_part?
|
|
53
|
+
body = (message.html_part || message).body
|
|
54
|
+
|
|
55
|
+
doc = Nokogiri::HTML(body.raw_source)
|
|
56
|
+
doc.css("a").each do |link|
|
|
57
|
+
uri = Addressable::URI.parse(link["href"])
|
|
58
|
+
params = uri.query_values || {}
|
|
59
|
+
%w[utm_source utm_medium utm_term utm_content utm_campaign].each do |key|
|
|
60
|
+
params[key] ||= options[key.to_sym] if options[key.to_sym]
|
|
61
|
+
end
|
|
62
|
+
uri.query_values = params
|
|
63
|
+
link["href"] = uri.to_s
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# hacky
|
|
67
|
+
body.raw_source.sub!(body.raw_source, doc.to_s)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def track_open!
|
|
72
|
+
if html_part?
|
|
73
|
+
raw_source = (message.html_part || message).body.raw_source
|
|
74
|
+
regex = /<\/body>/i
|
|
75
|
+
url =
|
|
76
|
+
AhoyEmail::Engine.routes.url_helpers.url_for(
|
|
77
|
+
Rails.application.config.action_mailer.default_url_options.merge(
|
|
78
|
+
controller: "ahoy/messages",
|
|
79
|
+
action: "open",
|
|
80
|
+
id: ahoy_message.token,
|
|
81
|
+
format: "gif"
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
pixel = image_tag(url, size: "1x1", alt: nil)
|
|
85
|
+
|
|
86
|
+
# try to add before body tag
|
|
87
|
+
if raw_source.match(regex)
|
|
88
|
+
raw_source.gsub!(regex, "#{pixel}\\0")
|
|
89
|
+
else
|
|
90
|
+
raw_source << pixel
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def track_click!
|
|
96
|
+
if html_part?
|
|
97
|
+
body = (message.html_part || message).body
|
|
98
|
+
|
|
99
|
+
doc = Nokogiri::HTML(body.raw_source)
|
|
100
|
+
doc.css("a").each do |link|
|
|
101
|
+
key = "data-disable-tracking"
|
|
102
|
+
if link[key]
|
|
103
|
+
# remove attribute
|
|
104
|
+
link.remove_attribute(key)
|
|
105
|
+
else
|
|
106
|
+
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new("sha1"), AhoyEmail.secret_token, link["href"])
|
|
107
|
+
url =
|
|
108
|
+
AhoyEmail::Engine.routes.url_helpers.url_for(
|
|
109
|
+
Rails.application.config.action_mailer.default_url_options.merge(
|
|
110
|
+
controller: "ahoy/messages",
|
|
111
|
+
action: "click",
|
|
112
|
+
id: ahoy_message.token,
|
|
113
|
+
url: link["href"],
|
|
114
|
+
signature: signature
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
link["href"] = url
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# hacky
|
|
123
|
+
body.raw_source.sub!(body.raw_source, doc.to_s)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def html_part?
|
|
128
|
+
(message.html_part || message).content_type =~ /html/
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# not a fan of quiet errors
|
|
132
|
+
# but tracking should *not* break
|
|
133
|
+
# email delivery in production
|
|
134
|
+
def report_error(e)
|
|
135
|
+
if Rails.env.production?
|
|
136
|
+
$stderr.puts e
|
|
137
|
+
else
|
|
138
|
+
raise e
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/ahoy_email/version.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration
|
|
2
2
|
def change
|
|
3
3
|
create_table :ahoy_messages do |t|
|
|
4
|
+
t.string :token
|
|
5
|
+
|
|
4
6
|
# user
|
|
5
7
|
t.integer :user_id
|
|
6
8
|
t.string :user_type
|
|
7
9
|
|
|
8
|
-
#
|
|
10
|
+
# optional
|
|
9
11
|
t.text :subject
|
|
10
12
|
t.text :content
|
|
11
13
|
|
|
@@ -15,6 +17,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
|
|
|
15
17
|
t.timestamp :clicked_at
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
add_index :ahoy_messages, [:token]
|
|
18
21
|
add_index :ahoy_messages, [:user_id, :user_type]
|
|
19
22
|
end
|
|
20
23
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ahoy_email
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2014-04-
|
|
11
|
+
date: 2014-04-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionmailer
|
|
@@ -24,6 +24,34 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: addressable
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: nokogiri
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
27
55
|
- !ruby/object:Gem::Dependency
|
|
28
56
|
name: bundler
|
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -60,6 +88,7 @@ extensions: []
|
|
|
60
88
|
extra_rdoc_files: []
|
|
61
89
|
files:
|
|
62
90
|
- ".gitignore"
|
|
91
|
+
- CHANGELOG.md
|
|
63
92
|
- Gemfile
|
|
64
93
|
- LICENSE.txt
|
|
65
94
|
- README.md
|
|
@@ -71,6 +100,7 @@ files:
|
|
|
71
100
|
- lib/ahoy_email.rb
|
|
72
101
|
- lib/ahoy_email/engine.rb
|
|
73
102
|
- lib/ahoy_email/interceptor.rb
|
|
103
|
+
- lib/ahoy_email/processor.rb
|
|
74
104
|
- lib/ahoy_email/version.rb
|
|
75
105
|
- lib/generators/ahoy_email/install_generator.rb
|
|
76
106
|
- lib/generators/ahoy_email/templates/install.rb
|