action_mailer_pixel 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20847b2dc90ebaac48117f71451ab0bfd3534de66f4dbd4327b6401e774292a9
4
+ data.tar.gz: f140fe438ffb84b1badfcda8360ac7dacffad20889167a5b191bea6df55d062e
5
+ SHA512:
6
+ metadata.gz: '0294b8df34fc4857d5dde7bb50d224b7a263e747bccf7c68bd6c0d065bc8e12cf88e5a07332ce5b696c4cf834e81adaaa69f3ee043ede99b3cdb6ca8f7eb9dd3'
7
+ data.tar.gz: da4cd791942b75581e0ebb929c6b986328c472494e59170ff156b339a2288400929c2feb5ccdded43cfbf3926304b1c46280c1fecf7fc89e68080e04f04057b9
@@ -0,0 +1,20 @@
1
+ Copyright 2018 Veselin Stoyanov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,51 @@
1
+ # ActionMailerPixel
2
+ ActionMailerPixel allows you to easily track your mailers' open rate.
3
+
4
+ ## How it works
5
+ When you enable the open tracking pixel on a mailer, a simple **img** tag will be inserted right before the closing body tag upon delivery. The source of the image tag leads to the /open.png path wherever you mounted the engine in your application. The response is a 1x1px transparent PNG image. When the email is opened by an email client, the pixel might get rendered (depending on the security/privacy settings) and the opening will be saved. Open rate is tracked by user and by email campaign. This way you can see exactly who opened a specific email.
6
+
7
+ ## Usage
8
+ **1. Default (convention over configuration)** - pixel is created with the first available of the following:
9
+ - *@user* -> *params[:user]* -> *recipient_class.find_by(email: message.to.first)*
10
+ - *@campaign* -> *params[:campaign]*
11
+ ```ruby
12
+ class PixelMailer < ApplicationMailer
13
+ open_tracking_pixel
14
+
15
+ def sample
16
+ mail(to: 'recipient@example.com')
17
+ end
18
+ end
19
+ ```
20
+
21
+ **2. Explicit use** - pixel is created with the provided data
22
+ ```ruby
23
+ class PixelMailer < ApplicationMailer
24
+ open_tracking_pixel recipient: -> { params[:user] },
25
+ campaign: -> { params[:campaign] }
26
+
27
+ def sample
28
+ mail(to: 'recipient@example.com')
29
+ end
30
+ end
31
+ ```
32
+
33
+ ## Installation
34
+ Add this line to your application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'action_mailer_pixel'
38
+ ```
39
+
40
+ And then execute:
41
+ ```bash
42
+ $ bundle
43
+ $ bundle exec rails action_mailer_pixel:install:migrations
44
+ $ bundle exec rails action_mailer_pixel:install
45
+ ```
46
+
47
+ ## Contributing
48
+ Contributions are welcome.
49
+
50
+ ## License
51
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ActionMailerPixel'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
@@ -0,0 +1,5 @@
1
+ module ActionMailerPixel
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,28 @@
1
+ module ActionMailerPixel
2
+ class PixelController < ApplicationController
3
+ attr_reader :email_view
4
+
5
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found
6
+
7
+ def open
8
+ respond_to do |format|
9
+ format.png do
10
+ email_view.increment_views!
11
+ send_data Pixel.file_data,
12
+ type: Pixel::FILE_TYPE,
13
+ disposition: 'inline'
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def email_view
21
+ @email_view = EmailView.find_by!(token: params[:token])
22
+ end
23
+
24
+ def handle_record_not_found
25
+ head :not_found
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ module ActionMailerPixel
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module ActionMailerPixel
2
+ class EmailView < ApplicationRecord
3
+ has_secure_token
4
+ belongs_to :recipient, class_name: ActionMailerPixel.recipient_class.to_s
5
+ belongs_to :campaign, class_name: ActionMailerPixel.campaign_class.to_s
6
+
7
+ def increment_views!
8
+ increment!(:views)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module ActionMailerPixel
2
+ class Pixel
3
+ BASE64_PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=
4
+ '.freeze
5
+ FILE_TYPE = 'image/png'.freeze
6
+
7
+ def self.file_data
8
+ Base64.decode64(BASE64_PNG_PIXEL)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ ActionMailerPixel::Engine.routes.draw do
2
+ get :open, to: 'pixel#open'
3
+ end
@@ -0,0 +1,19 @@
1
+ class CreateEmailViews < ActiveRecord::Migration[5.2]
2
+ def up
3
+ create_table :action_mailer_pixel_email_views do |t|
4
+ t.string :token, unique: true
5
+ t.integer :recipient_id
6
+ t.integer :campaign_id
7
+ t.integer :views, default: 0
8
+ t.timestamps
9
+
10
+ t.index [:recipient_id, :campaign_id],
11
+ unique: true,
12
+ name: 'index_email_views_on_recipient_id_and_campaign_id'
13
+ end
14
+ end
15
+
16
+ def down
17
+ drop_table :action_mailer_pixel_email_views
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ require 'action_mailer_pixel/engine'
2
+
3
+ module ActionMailerPixel
4
+ class << self
5
+ attr_accessor :configuration
6
+
7
+ def configure
8
+ @configuration ||= Configuration.new
9
+ yield @configuration
10
+ end
11
+
12
+ def recipient_class
13
+ @configuration.recipient_class.constantize
14
+ end
15
+
16
+ def campaign_class
17
+ @configuration.campaign_class.constantize
18
+ end
19
+ end
20
+
21
+ class Configuration
22
+ attr_accessor :recipient_class, :campaign_class
23
+
24
+ def initialize
25
+ @recipient_class = 'User'
26
+ @campaign_class = 'Campaign'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,62 @@
1
+ module ActionMailerPixel
2
+ module DSL
3
+ module TrackingPixel
4
+ extend ActiveSupport::Concern
5
+
6
+ class Configuration
7
+ def initialize(recipient, campaign)
8
+ @recipient = recipient
9
+ @campaign = campaign
10
+ end
11
+
12
+ def recipient
13
+ @recipient.respond_to?(:call) ? @recipient.call : @recipient
14
+ end
15
+
16
+ def campaign
17
+ @campaign.respond_to?(:call) ? @campaign.call : @campaign
18
+ end
19
+ end
20
+
21
+ class_methods do
22
+ def open_tracking_pixel(recipient: nil, campaign: nil)
23
+ define_method :open_pixel_configuration do
24
+ @open_pixel_configuration ||= Configuration.new(
25
+ recipient.nil? ? send(:open_pixel_recipient) : recipient,
26
+ campaign.nil? ? send(:open_pixel_campaign) : campaign
27
+ )
28
+ end
29
+
30
+ define_method :open_pixel_recipient do
31
+ -> do
32
+ begin
33
+ @user ||
34
+ params[:user] ||
35
+ ActionMailerPixel.recipient_class.find_by(email: message.to.first)
36
+ rescue NameError
37
+ nil
38
+ end
39
+ end
40
+ end
41
+
42
+ define_method :open_pixel_campaign do
43
+ -> do
44
+ begin
45
+ @campaign || params[:campaign]
46
+ rescue NameError
47
+ nil
48
+ end
49
+ end
50
+ end
51
+
52
+ define_method :append_open_tracking_pixel do
53
+ interceptor = ActionMailerPixel::Interceptor.new self
54
+ interceptor.setup_pixel
55
+ end
56
+
57
+ after_action :append_open_tracking_pixel
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,18 @@
1
+ require 'action_mailer_pixel/dsl/tracking_pixel'
2
+ require 'action_mailer_pixel/interceptor'
3
+
4
+ module ActionMailerPixel
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace ActionMailerPixel
7
+
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ g.fixture_replacement :factory_bot
11
+ g.factory_bot dir: 'spec/factories'
12
+ end
13
+
14
+ config.to_prepare do
15
+ ::ActionMailer::Base.include ActionMailerPixel::DSL::TrackingPixel
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module ActionMailerPixel
2
+ class Interceptor
3
+ def initialize(mailer)
4
+ @mailer = mailer
5
+ end
6
+
7
+ def setup_pixel
8
+ if html?
9
+ @email_view = EmailView.create(recipient: recipient, campaign: campaign)
10
+
11
+ raw_source = (message.html_part || message).body.raw_source
12
+ regex = /<\/body>/i
13
+
14
+ if raw_source.match(regex)
15
+ raw_source.gsub!(regex, "#{pixel_html}\\0")
16
+ else
17
+ raw_source << pixel_html
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def message
25
+ @mailer.message
26
+ end
27
+
28
+ def configuration
29
+ @mailer.open_pixel_configuration
30
+ end
31
+
32
+ def recipient
33
+ configuration.recipient
34
+ end
35
+
36
+ def campaign
37
+ configuration.campaign
38
+ end
39
+
40
+ def pixel_url
41
+ options = (ActionMailer::Base.default_url_options || {})
42
+ .merge({
43
+ controller: 'action_mailer_pixel/pixel',
44
+ action: 'open',
45
+ token: @email_view.token,
46
+ format: 'png'
47
+ })
48
+ ActionMailerPixel::Engine.routes.url_helpers.url_for(options)
49
+ end
50
+
51
+ def pixel_html
52
+ ActionController::Base.helpers.image_tag(pixel_url, size: '1x1', alt: '')
53
+ end
54
+
55
+ def html?
56
+ (message.html_part || message).content_type.include? 'text/html'
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module ActionMailerPixel
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,15 @@
1
+ module ActionMailerPixel
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../../templates', __FILE__)
5
+ desc 'Creates ActionMailerPixel initializer for your application'
6
+
7
+ def copy_initializer
8
+ template 'action_mailer_pixel_initializer.rb',
9
+ 'config/initializers/action_mailer_pixel.rb'
10
+
11
+ puts 'ActionMailerPixel initializer created successfully.'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ ActionMailerPixel.configure do |config|
2
+ # Provide a model class name which is going to be used in order to
3
+ # associate the recipient. Default: 'User'
4
+ config.recipient_class = 'User'
5
+
6
+ # Provide a model class name which is going to be used in order to
7
+ # associate the specific email delivery. Default: 'Campaign'
8
+ config.campaign_class = 'Campaign'
9
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :action_mailer_pixel do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: action_mailer_pixel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Veselin Stoyanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: factory_bot_rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.8'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ description: " Tracks email open rate by including a tracking pixel in the message
70
+ body.\n"
71
+ email:
72
+ - stoyanov.veseline@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - MIT-LICENSE
78
+ - README.md
79
+ - Rakefile
80
+ - app/controllers/action_mailer_pixel/application_controller.rb
81
+ - app/controllers/action_mailer_pixel/pixel_controller.rb
82
+ - app/models/action_mailer_pixel/application_record.rb
83
+ - app/models/action_mailer_pixel/email_view.rb
84
+ - app/models/action_mailer_pixel/pixel.rb
85
+ - config/routes.rb
86
+ - db/migrate/20181018105200_create_email_views.rb
87
+ - lib/action_mailer_pixel.rb
88
+ - lib/action_mailer_pixel/dsl/tracking_pixel.rb
89
+ - lib/action_mailer_pixel/engine.rb
90
+ - lib/action_mailer_pixel/interceptor.rb
91
+ - lib/action_mailer_pixel/version.rb
92
+ - lib/generators/action_mailer_pixel/install_generator.rb
93
+ - lib/generators/templates/action_mailer_pixel_initializer.rb
94
+ - lib/tasks/action_mailer_pixel_tasks.rake
95
+ homepage: https://github.com/veskoy/action_mailer_pixel
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.7.6
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Simple ActionMailer open tracking pixel.
119
+ test_files: []