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 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