ahoy_email 0.0.1 → 0.0.2

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
  SHA1:
3
- metadata.gz: 099588e69bfa92fd90438816b1a5ef6857fb2f23
4
- data.tar.gz: b9c49ef5f702558434df01c199c8eacd93d80bb3
3
+ metadata.gz: 5c6f895cc438acdeb4982e684a4ba32efc86c05b
4
+ data.tar.gz: 19750f27ae10e0828b4f3e7813c3a236413fa1b1
5
5
  SHA512:
6
- metadata.gz: 27f621400f1b6c2f85408f4e68c43a7cdd420b8b1496ba27b6715190a60c792a105762b1e6e1daa1ea04e31c12ca301618a9d93c4b483ce3eff820f4d7f0e350
7
- data.tar.gz: df563aa16ce94a7197248a449de900f3501349d9b77450a294ed2e2b038e74702e040c7ee49f22e920ba8e9cb95e14b4865a08c7a0ceee6a07a163b0fd26c050
6
+ metadata.gz: 88ba353c711f596a745a8c406a222859c517c1392571791d91c833713d46a56a15f313428eae6af39e042b7c8ffc8aa1b75db969d1f950def5b824bb6b95fe19
7
+ data.tar.gz: 082bba0e14fab78aba984e1d3e1b94ce08841c092ff24e834804bf15478c8cc02c2050df8ab1c15701530158d30954222261f86c7ac09abcbc24bca6ee4a62be
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 [unreleased]
2
+
3
+ - First major release
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. It also adds:
30
+ Ahoy creates an `Ahoy::Message` record when an email is sent.
31
31
 
32
- - open tracking
33
- - click tracking
34
- - UTM parameters
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
- puts "OPENED!"
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
@@ -1,4 +1,7 @@
1
1
  module Ahoy
2
2
  class Message < ActiveRecord::Base
3
+ self.table_name = "ahoy_messages"
4
+
5
+ belongs_to :user, polymorphic: true
3
6
  end
4
7
  end
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
- resources :messages, only: [] do
7
- get :open, on: :collection
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
@@ -1,5 +1,9 @@
1
1
  module AhoyEmail
2
2
  class Engine < ::Rails::Engine
3
- isolate_namespace AhoyEmail
3
+
4
+ initializer "ahoy_email" do |app|
5
+ AhoyEmail.secret_token = app.config.try(:secret_key_base) || app.config.try(:secret_token)
6
+ end
7
+
4
8
  end
5
9
  end
@@ -1,24 +1,11 @@
1
1
  module AhoyEmail
2
2
  class Interceptor
3
- # include ActionView::Helpers::AssetTagHelper
3
+ class << self
4
4
 
5
- def self.delivering_email(message)
6
- # body = (message.html_part || message).body.raw_source
7
- # p AhoyEmail::Engine.routes
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
@@ -1,3 +1,3 @@
1
1
  module AhoyEmail
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
- # message
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.1
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-28 00:00:00.000000000 Z
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