pax_global_header00006660000000000000000000000064134327113720014515gustar00rootroot0000000000000052 comment=81898f33d2727b3e912cf3953c0c551750240736 ahoy_email-1.0.3/000077500000000000000000000000001343271137200136255ustar00rootroot00000000000000ahoy_email-1.0.3/.github/000077500000000000000000000000001343271137200151655ustar00rootroot00000000000000ahoy_email-1.0.3/.github/ISSUE_TEMPLATE.md000066400000000000000000000002231343271137200176670ustar00rootroot00000000000000Hi, Before creating an issue, please check out the Contributing Guide: https://github.com/ankane/ahoy_email/blob/master/CONTRIBUTING.md Thanks! ahoy_email-1.0.3/.gitignore000066400000000000000000000003021343271137200156100ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc *.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp *.bundle *.so *.o *.a mkmf.log *.log *.sqlite ahoy_email-1.0.3/.travis.yml000066400000000000000000000007151343271137200157410ustar00rootroot00000000000000language: ruby rvm: 2.5.1 sudo: false script: bundle exec rake test before_script: - gem install bundler notifications: email: on_success: never on_failure: change gemfile: - Gemfile - test/gemfiles/actionmailer51.gemfile matrix: include: - gemfile: test/gemfiles/actionmailer42.gemfile rvm: 2.3.7 - gemfile: test/gemfiles/actionmailer50.gemfile rvm: 2.4.4 - gemfile: test/gemfiles/mongoid6.gemfile services: - mongodb ahoy_email-1.0.3/CHANGELOG.md000066400000000000000000000041051343271137200154360ustar00rootroot00000000000000## 1.0.3 - Fixed custom message model - Fixed `message` option with proc ## 1.0.2 - Fixed error with Ruby < 2.5 - Fixed UTM parameters storage on model ## 1.0.1 - Use observer instead of interceptor ## 1.0.0 - Removed support for Rails < 4.2 Breaking changes - UTM tagging, open tracking, and click tracking are no longer enabled by default - Only sent emails are recorded - Proc options are now executed in the context of the mailer and take no arguments - Invalid options now throw an `ArgumentError` - `AhoyEmail.track` was removed in favor of `AhoyEmail.default_options` - The `heuristic_parse` option was removed and is now the default ## 0.5.2 - Fixed secret token for Rails 5.2 - Added `heuristic_parse` option ## 0.5.1 - Fixed deprecation warning in Rails 5.2 - Added `unsubscribe_links` option - Allow `message_model` to accept a proc - Use `references` in migration ## 0.5.0 - Added support for Rails 5.1 - Added `invalid_redirect_url` ## 0.4.0 - Fixed `belongs_to` error in Rails 5 - Include `safely_block` gem without polluting global namespace ## 0.3.2 - Fixed deprecation warning for Rails 5 - Do not track content by default on fresh installations ## 0.3.1 - Fixed deprecation warnings - Fixed `stack level too deep` error ## 0.3.0 - Added safely for error reporting - Fixed error with `to` - Prevent duplicate records when mail called multiple times ## 0.2.4 - Added `extra` option for extra attributes ## 0.2.3 - Save utm parameters - Added `url_options` - Skip tracking for `mailto` links - Only set secret token if not already set ## 0.2.2 - Fixed secret token for Rails 4.1 - Fixed links with href - Fixed message id for Rails 3.1 ## 0.2.1 - Added `only` and `except` options ## 0.2.0 - Enable tracking when track is called by default ## 0.1.5 - Rails 3 fix ## 0.1.4 - Try not to rewrite unsubscribe links ## 0.1.3 - Added `to` and `mailer` fields - Added subscribers for open and click events ## 0.1.2 - Added `AhoyEmail.track` (fix) ## 0.1.1 - Use secure compare for signature verification - Fixed deprecation warnings ## 0.1.0 - First major release ahoy_email-1.0.3/CONTRIBUTING.md000066400000000000000000000026671343271137200160710ustar00rootroot00000000000000# Contributing First, thanks for wanting to contribute. You’re awesome! :heart: ## Help We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/). All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist. ## Bugs Think you’ve discovered a bug? 1. Search existing issues to see if it’s been reported. 2. Try the `master` branch to make sure it hasn’t been fixed. ```rb gem "ahoy_email", github: "ankane/ahoy_email" ``` If the above steps don’t help, create an issue. Include: - Detailed steps to reproduce - Complete backtraces for exceptions ## New Features If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`. ## Pull Requests Fork the project and create a pull request. A few tips: - Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests. - Follow the existing style. The code should read like it’s written by a single person. - Add one or more tests if possible. Make sure existing tests pass with: ```sh bundle exec rake test ``` Feel free to open an issue to get feedback on your idea before spending too much time on it. --- This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution. ahoy_email-1.0.3/Gemfile000066400000000000000000000002361343271137200151210ustar00rootroot00000000000000source "https://rubygems.org" # Specify your gem's dependencies in ahoy_email.gemspec gemspec gem "actionmailer", "~> 5.2.0" gem "activerecord", "~> 5.2.0" ahoy_email-1.0.3/LICENSE.txt000066400000000000000000000020611343271137200154470ustar00rootroot00000000000000Copyright (c) 2014-2019 Andrew Kane MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ahoy_email-1.0.3/README.md000066400000000000000000000160601343271137200151070ustar00rootroot00000000000000# Ahoy Email :postbox: Email analytics for Rails You get: - A history of emails sent to each user - Easy UTM tagging - Optional open and click tracking **Ahoy Email 1.0 was recently released!** See [how to upgrade](#upgrading) :bullettrain_side: To manage unsubscribes, check out [Mailkick](https://github.com/ankane/mailkick) :fire: To track visits and events, check out [Ahoy](https://github.com/ankane/ahoy) [![Build Status](https://travis-ci.org/ankane/ahoy_email.svg?branch=master)](https://travis-ci.org/ankane/ahoy_email) ## Installation Add this line to your application’s Gemfile: ```ruby gem 'ahoy_email' ``` And run the generator. This creates a model to store messages. ```sh rails generate ahoy_email:install rails db:migrate ``` ## How It Works ### Message History Ahoy creates an `Ahoy::Message` record for each email sent by default. You can disable history for a mailer: ```ruby class CouponMailer < ApplicationMailer track message: false # use only/except to limit actions end ``` Or by default: ```ruby AhoyEmail.default_options[:message] = false ``` ### Users Ahoy records the user a message is sent to - not just the email address. This gives you a full history of messages for each user, even if he or she changes addresses. By default, Ahoy tries `@user` then `params[:user]` then `User.find_by(email: message.to.first)` to find the user. You can pass a specific user with: ```ruby class CouponMailer < ApplicationMailer track user: -> { params[:some_user] } end ``` The user association is [polymorphic](https://railscasts.com/episodes/154-polymorphic-association), so use it with any model. To get all messages sent to a user, add an association: ```ruby class User < ApplicationRecord has_many :messages, class_name: "Ahoy::Message", as: :user end ``` And run: ```ruby user.messages ``` ### Extra Attributes Record extra attributes on the `Ahoy::Message` model. Create a migration to add extra attributes to the `ahoy_messages` table. For example: ```ruby class AddCouponIdToAhoyMessages < ActiveRecord::Migration[5.2] def change add_column :ahoy_messages, :coupon_id, :integer end end ``` Then use: ```ruby class CouponMailer < ApplicationMailer track extra: {coupon_id: 1} end ``` You can use a proc as well. ```ruby class CouponMailer < ApplicationMailer track extra: -> { {coupon_id: params[:coupon].id} } end ``` ### UTM Tagging Automatically add UTM parameters to links. ```ruby class CouponMailer < ApplicationMailer track utm_params: true # use only/except to limit actions end ``` The defaults are: - `utm_medium` - `email` - `utm_source` - the mailer name like `coupon_mailer` - `utm_campaign` - the mailer action like `offer` You can customize them with: ```ruby class CouponMailer < ApplicationMailer track utm_params: true, utm_campaign: -> { "coupon#{params[:coupon].id}" } end ``` Skip specific links with: ```erb <%= link_to "Go", some_url, data: {skip_utm_params: true} %> ``` ### Opens & Clicks #### Setup Additional setup is required to track opens and clicks. Create a migration with: ```ruby class AddTokenToAhoyMessages < ActiveRecord::Migration[5.2] def change add_column :ahoy_messages, :token, :string add_column :ahoy_messages, :opened_at, :timestamp add_column :ahoy_messages, :clicked_at, :timestamp add_index :ahoy_messages, :token end end ``` Create an initializer `config/initializers/ahoy_email.rb` with: ```ruby AhoyEmail.api = true ``` And add to mailers you want to track: ```ruby class CouponMailer < ApplicationMailer track open: true, click: true # use only/except to limit actions end ``` #### How It Works For opens, an invisible pixel is added right before the `` tag in HTML emails. If the recipient has images enabled in their email client, the pixel is loaded and the open time recorded. For clicks, a redirect is added to links to track clicks in HTML emails. ``` https://chartkick.com ``` becomes ``` https://yoursite.com/ahoy/messages/rAnDoMtOkEn/click?url=https%3A%2F%2Fchartkick.com&signature=... ``` A signature is added to prevent [open redirects](https://www.owasp.org/index.php/Open_redirect). Skip specific links with: ```erb <%= link_to "Go", some_url, data: {skip_click: true} %> ``` By default, unsubscribe links are excluded. To change this, use: ```ruby AhoyEmail.default_options[:unsubscribe_links] = true ``` You can specify the domain to use with: ```ruby AhoyEmail.default_options[:url_options] = {host: "mydomain.com"} ``` #### Events Subscribe to open and click events by adding to the initializer: ```ruby class EmailSubscriber def open(event) # your code end def click(event) # your code end end AhoyEmail.subscribers << EmailSubscriber.new ``` Here’s an example if you use [Ahoy](https://github.com/ankane/ahoy) to track visits and events: ```ruby class EmailSubscriber def open(event) event[:controller].ahoy.track "Email opened", message_id: event[:message].id end def click(event) event[:controller].ahoy.track "Email clicked", message_id: event[:message].id, url: event[:url] end end AhoyEmail.subscribers << EmailSubscriber.new ``` ## Reference Set global options ```ruby AhoyEmail.default_options[:user] = -> { params[:admin] } ``` Use a different model ```ruby AhoyEmail.message_model = -> { UserMessage } ``` Or fully customize how messages are tracked ```ruby AhoyEmail.track_method = lambda do |data| # your code end ``` ## Mongoid If you prefer to use Mongoid instead of ActiveRecord, create `app/models/ahoy/message.rb` with: ```ruby class Ahoy::Message include Mongoid::Document belongs_to :user, polymorphic: true, optional: true, index: true field :to, type: String field :mailer, type: String field :subject, type: String field :sent_at, type: Time end ``` ## Upgrading ### 1.0 Breaking changes - UTM tagging, open tracking, and click tracking are no longer enabled by default. To enable, create an initializer with: ```ruby AhoyEmail.api = true AhoyEmail.default_options[:open] = true AhoyEmail.default_options[:click] = true AhoyEmail.default_options[:utm_params] = true ``` - Only sent emails are recorded - Proc options are now executed in the context of the mailer and take no arguments ```ruby # old user: ->(mailer, message) { User.find_by(email: message.to.first) } # new user: -> { User.find_by(email: message.to.first) } ``` - Invalid options now throw an `ArgumentError` - `AhoyEmail.track` was removed in favor of `AhoyEmail.default_options` - The `heuristic_parse` option was removed and is now the default ## History View the [changelog](https://github.com/ankane/ahoy_email/blob/master/CHANGELOG.md) ## Contributing Everyone is encouraged to help improve this project. Here are a few ways you can help: - [Report bugs](https://github.com/ankane/ahoy_email/issues) - Fix bugs and [submit pull requests](https://github.com/ankane/ahoy_email/pulls) - Write, clarify, or fix documentation - Suggest or add new features To get started with development and testing: ```sh git clone https://github.com/ankane/ahoy_email.git cd ahoy_email bundle install rake test ``` ahoy_email-1.0.3/Rakefile000066400000000000000000000002601343271137200152700ustar00rootroot00000000000000require "bundler/gem_tasks" require "rake/testtask" task default: :test Rake::TestTask.new do |t| t.libs << "test" t.pattern = "test/**/*_test.rb" t.warning = false end ahoy_email-1.0.3/ahoy_email.gemspec000066400000000000000000000021271343271137200173030ustar00rootroot00000000000000 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "ahoy_email/version" Gem::Specification.new do |spec| spec.name = "ahoy_email" spec.version = AhoyEmail::VERSION spec.summary = "Email analytics for Rails" spec.homepage = "https://github.com/ankane/ahoy_email" spec.license = "MIT" spec.author = "Andrew Kane" spec.email = "andrew@chartkick.com" spec.files = Dir["*.{md,txt}", "{app,config,lib}/**/*"] spec.require_path = "lib" spec.required_ruby_version = ">= 2.2" spec.add_dependency "actionmailer", ">= 4.2" spec.add_dependency "addressable", ">= 2.3.2" spec.add_dependency "nokogiri" spec.add_dependency "safely_block", ">= 0.1.1" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "minitest" spec.add_development_dependency "activerecord" spec.add_development_dependency "combustion" spec.add_development_dependency "rails" spec.add_development_dependency "sqlite3", "~> 1.3.0" end ahoy_email-1.0.3/app/000077500000000000000000000000001343271137200144055ustar00rootroot00000000000000ahoy_email-1.0.3/app/controllers/000077500000000000000000000000001343271137200167535ustar00rootroot00000000000000ahoy_email-1.0.3/app/controllers/ahoy/000077500000000000000000000000001343271137200177135ustar00rootroot00000000000000ahoy_email-1.0.3/app/controllers/ahoy/messages_controller.rb000066400000000000000000000033571343271137200243220ustar00rootroot00000000000000module Ahoy class MessagesController < ApplicationController filters = _process_action_callbacks.map(&:filter) - AhoyEmail.preserve_callbacks if Rails::VERSION::MAJOR >= 5 skip_before_action(*filters, raise: false) skip_after_action(*filters, raise: false) skip_around_action(*filters, raise: false) else skip_action_callback *filters end before_action :set_message def open if @message && !@message.opened_at @message.opened_at = Time.now @message.save! end publish :open send_data Base64.decode64("R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="), type: "image/gif", disposition: "inline" end def click if @message && !@message.clicked_at @message.clicked_at = Time.now @message.opened_at ||= @message.clicked_at @message.save! end user_signature = params[:signature].to_s url = params[:url].to_s # TODO sign more than just url and transition to HMAC-SHA256 digest = "SHA1" signature = OpenSSL::HMAC.hexdigest(digest, AhoyEmail.secret_token, url) if ActiveSupport::SecurityUtils.secure_compare(user_signature, signature) publish :click, url: params[:url] redirect_to url else redirect_to AhoyEmail.invalid_redirect_url || main_app.root_url end end protected def set_message @message = AhoyEmail.message_model.where(token: params[:id]).first end def publish(name, event = {}) AhoyEmail.subscribers.each do |subscriber| if subscriber.respond_to?(name) event[:message] = @message event[:controller] = self subscriber.send name, event end end end end end ahoy_email-1.0.3/app/models/000077500000000000000000000000001343271137200156705ustar00rootroot00000000000000ahoy_email-1.0.3/app/models/ahoy/000077500000000000000000000000001343271137200166305ustar00rootroot00000000000000ahoy_email-1.0.3/app/models/ahoy/message.rb000066400000000000000000000003151343271137200206000ustar00rootroot00000000000000module Ahoy class Message < ActiveRecord::Base self.table_name = "ahoy_messages" belongs_to :user, (ActiveRecord::VERSION::MAJOR >= 5 ? {optional: true} : {}).merge(polymorphic: true) end end ahoy_email-1.0.3/config/000077500000000000000000000000001343271137200150725ustar00rootroot00000000000000ahoy_email-1.0.3/config/routes.rb000066400000000000000000000004111343271137200167340ustar00rootroot00000000000000Rails.application.routes.draw do mount AhoyEmail::Engine => "/ahoy" if AhoyEmail.api end AhoyEmail::Engine.routes.draw do scope module: "ahoy" do resources :messages, only: [] do get :open, on: :member get :click, on: :member end end end ahoy_email-1.0.3/lib/000077500000000000000000000000001343271137200143735ustar00rootroot00000000000000ahoy_email-1.0.3/lib/ahoy_email.rb000066400000000000000000000045151343271137200170340ustar00rootroot00000000000000# dependencies require "active_support" require "addressable/uri" require "nokogiri" require "openssl" require "safely/core" # modules require "ahoy_email/processor" require "ahoy_email/tracker" require "ahoy_email/observer" require "ahoy_email/mailer" require "ahoy_email/version" require "ahoy_email/engine" if defined?(Rails) module AhoyEmail mattr_accessor :secret_token, :default_options, :subscribers, :invalid_redirect_url, :track_method, :api, :preserve_callbacks mattr_writer :message_model self.api = false self.default_options = { message: true, open: false, click: false, utm_params: false, utm_source: -> { mailer_name }, utm_medium: "email", utm_term: nil, utm_content: nil, utm_campaign: -> { action_name }, user: -> { @user || (respond_to?(:params) && params && params[:user]) || (message.to.try(:size) == 1 ? (User.find_by(email: message.to.first) rescue nil) : nil) }, mailer: -> { "#{self.class.name}##{action_name}" }, url_options: {}, extra: {}, unsubscribe_links: false } self.track_method = lambda do |data| message = data[:message] ahoy_message = AhoyEmail.message_model.new ahoy_message.to = Array(message.to).join(", ") if ahoy_message.respond_to?(:to=) ahoy_message.user = data[:user] if ahoy_message.respond_to?(:user=) ahoy_message.mailer = data[:mailer] if ahoy_message.respond_to?(:mailer=) ahoy_message.subject = message.subject if ahoy_message.respond_to?(:subject=) ahoy_message.content = message.encoded if ahoy_message.respond_to?(:content=) AhoyEmail::Processor::UTM_PARAMETERS.each do |k| ahoy_message.send("#{k}=", data[k.to_sym]) if ahoy_message.respond_to?("#{k}=") end ahoy_message.token = data[:token] if ahoy_message.respond_to?(:token=) ahoy_message.assign_attributes(data[:extra] || {}) ahoy_message.sent_at = Time.now ahoy_message.save! ahoy_message end self.subscribers = [] self.preserve_callbacks = [] self.message_model = -> { ::Ahoy::Message } def self.message_model model = defined?(@@message_model) && @@message_model model = model.call if model.respond_to?(:call) model end end ActiveSupport.on_load(:action_mailer) do include AhoyEmail::Mailer register_observer AhoyEmail::Observer Mail::Message.send(:attr_accessor, :ahoy_data, :ahoy_message) end ahoy_email-1.0.3/lib/ahoy_email/000077500000000000000000000000001343271137200165025ustar00rootroot00000000000000ahoy_email-1.0.3/lib/ahoy_email/engine.rb000066400000000000000000000007751343271137200203050ustar00rootroot00000000000000require "rails/engine" module AhoyEmail class Engine < ::Rails::Engine initializer "ahoy_email" do |app| AhoyEmail.secret_token ||= begin creds = if app.respond_to?(:credentials) && app.credentials.secret_key_base app.credentials elsif app.respond_to?(:secrets) app.secrets else app.config end creds.respond_to?(:secret_key_base) ? creds.secret_key_base : creds.secret_token end end end end ahoy_email-1.0.3/lib/ahoy_email/mailer.rb000066400000000000000000000021541343271137200203020ustar00rootroot00000000000000module AhoyEmail module Mailer extend ActiveSupport::Concern included do attr_writer :ahoy_options after_action :save_ahoy_options end class_methods do def track(**options) before_action(options.slice(:only, :except)) do self.ahoy_options = ahoy_options.merge(message: true).merge(options.except(:only, :except)) end end end def track(**options) self.ahoy_options = ahoy_options.merge(message: true).merge(options) end def ahoy_options @ahoy_options ||= AhoyEmail.default_options end def save_ahoy_options Safely.safely do # do message first for performance message = ahoy_options[:message] message = message.respond_to?(:call) ? instance_exec(&message) : message if message options = {} ahoy_options.except(:message).each do |k, v| # execute options in mailer content options[k] = v.respond_to?(:call) ? instance_exec(&v) : v end AhoyEmail::Processor.new(self, options).perform end end end end end ahoy_email-1.0.3/lib/ahoy_email/observer.rb000066400000000000000000000002101343271137200206470ustar00rootroot00000000000000module AhoyEmail class Observer def self.delivered_email(message) AhoyEmail::Tracker.new(message).perform end end end ahoy_email-1.0.3/lib/ahoy_email/processor.rb000066400000000000000000000102401343271137200210430ustar00rootroot00000000000000module AhoyEmail class Processor attr_reader :mailer, :options UTM_PARAMETERS = %w(utm_source utm_medium utm_term utm_content utm_campaign) def initialize(mailer, options) @mailer = mailer @options = options unknown_keywords = options.keys - AhoyEmail.default_options.keys raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any? end def perform track_open if options[:open] track_links if options[:utm_params] || options[:click] track_message end protected def message mailer.message end def token @token ||= SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32) end def track_message data = { mailer: options[:mailer], extra: options[:extra], user: options[:user] } # legacy, remove in next major version user = options[:user] if user data[:user_type] = user.model_name.name id = user.id data[:user_id] = id.is_a?(Integer) ? id : id.to_s end if options[:open] || options[:click] data[:token] = token end if options[:utm_params] UTM_PARAMETERS.map(&:to_sym).each do |k| data[k] = options[k] if options[k] end end mailer.message.ahoy_data = data end def track_open if html_part? raw_source = (message.html_part || message).body.raw_source regex = /<\/body>/i url = url_for( controller: "ahoy/messages", action: "open", id: token, format: "gif" ) pixel = ActionController::Base.helpers.image_tag(url, size: "1x1", alt: "") # try to add before body tag if raw_source.match(regex) raw_source.gsub!(regex, "#{pixel}\\0") else raw_source << pixel end end end def track_links if html_part? body = (message.html_part || message).body doc = Nokogiri::HTML(body.raw_source) doc.css("a[href]").each do |link| uri = parse_uri(link["href"]) next unless trackable?(uri) # utm params first if options[:utm_params] && !skip_attribute?(link, "utm-params") params = uri.query_values(Array) || [] UTM_PARAMETERS.each do |key| next if params.any? { |k, _v| k == key } || !options[key.to_sym] params << [key, options[key.to_sym]] end uri.query_values = params link["href"] = uri.to_s end if options[:click] && !skip_attribute?(link, "click") # TODO sign more than just url and transition to HMAC-SHA256 signature = OpenSSL::HMAC.hexdigest("SHA1", AhoyEmail.secret_token, link["href"]) link["href"] = url_for( controller: "ahoy/messages", action: "click", id: token, url: link["href"], signature: signature ) end end # hacky body.raw_source.sub!(body.raw_source, doc.to_s) end end def html_part? (message.html_part || message).content_type =~ /html/ end def skip_attribute?(link, suffix) attribute = "data-skip-#{suffix}" if link[attribute] # remove it link.remove_attribute(attribute) true elsif link["href"].to_s =~ /unsubscribe/i && !options[:unsubscribe_links] # try to avoid unsubscribe links true else false end end # Filter trackable URIs, i.e. absolute one with http def trackable?(uri) uri && uri.absolute? && %w(http https).include?(uri.scheme) end # Parse href attribute # Return uri if valid, nil otherwise def parse_uri(href) # to_s prevent to return nil from this method Addressable::URI.heuristic_parse(href.to_s) rescue nil end def url_for(opt) opt = (ActionMailer::Base.default_url_options || {}) .merge(options[:url_options]) .merge(opt) AhoyEmail::Engine.routes.url_helpers.url_for(opt) end end end ahoy_email-1.0.3/lib/ahoy_email/tracker.rb000066400000000000000000000006751343271137200204720ustar00rootroot00000000000000module AhoyEmail class Tracker attr_reader :message def initialize(message) @message = message end def perform Safely.safely do # perform_deliveries check still needed in observer if message.perform_deliveries && message.ahoy_data data = message.ahoy_data.merge(message: message) message.ahoy_message = AhoyEmail.track_method.call(data) end end end end end ahoy_email-1.0.3/lib/ahoy_email/version.rb000066400000000000000000000000511343271137200205100ustar00rootroot00000000000000module AhoyEmail VERSION = "1.0.3" end ahoy_email-1.0.3/lib/generators/000077500000000000000000000000001343271137200165445ustar00rootroot00000000000000ahoy_email-1.0.3/lib/generators/ahoy_email/000077500000000000000000000000001343271137200206535ustar00rootroot00000000000000ahoy_email-1.0.3/lib/generators/ahoy_email/install_generator.rb000066400000000000000000000022551343271137200247200ustar00rootroot00000000000000# taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb require "rails/generators" require "rails/generators/migration" require "active_record" require "rails/generators/active_record" module AhoyEmail module Generators class InstallGenerator < Rails::Generators::Base include Rails::Generators::Migration source_root File.expand_path("../templates", __FILE__) # Implement the required interface for Rails::Generators::Migration. def self.next_migration_number(dirname) #:nodoc: next_migration_number = current_migration_number(dirname) + 1 if ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max else "%.3d" % next_migration_number end end def copy_migration migration_template "install.rb", "db/migrate/create_ahoy_messages.rb", migration_version: migration_version end def migration_version if ActiveRecord::VERSION::MAJOR >= 5 "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" end end end end end ahoy_email-1.0.3/lib/generators/ahoy_email/templates/000077500000000000000000000000001343271137200226515ustar00rootroot00000000000000ahoy_email-1.0.3/lib/generators/ahoy_email/templates/install.rb.tt000066400000000000000000000004371343271137200252760ustar00rootroot00000000000000class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %> def change create_table :ahoy_messages do |t| t.references :user, polymorphic: true t.text :to t.string :mailer t.text :subject t.timestamp :sent_at end end end ahoy_email-1.0.3/test/000077500000000000000000000000001343271137200146045ustar00rootroot00000000000000ahoy_email-1.0.3/test/click_test.rb000066400000000000000000000012021343271137200172500ustar00rootroot00000000000000require_relative "test_helper" class ClickTest < Minitest::Test def test_default message = ClickMailer.welcome.deliver_now refute_body "click", message end def test_basic message = ClickMailer.basic.deliver_now assert_body "click", message end def test_mailto message = ClickMailer.mailto.deliver_now assert_body '', message end def test_app message = ClickMailer.app.deliver_now assert_body '', message end def test_schemeless message = ClickMailer.schemeless.deliver_now assert_body "click", message end end ahoy_email-1.0.3/test/extra_test.rb000066400000000000000000000005711343271137200173160ustar00rootroot00000000000000require_relative "test_helper" class ExtraTest < Minitest::Test def test_default ExtraMailer.welcome.deliver_now assert_nil ahoy_message.coupon_id end def test_basic ExtraMailer.basic.deliver_now assert_equal 1, ahoy_message.coupon_id end def test_dynamic ExtraMailer.dynamic(2).deliver_now assert_equal 2, ahoy_message.coupon_id end end ahoy_email-1.0.3/test/gemfiles/000077500000000000000000000000001343271137200163775ustar00rootroot00000000000000ahoy_email-1.0.3/test/gemfiles/actionmailer42.gemfile000066400000000000000000000002551343271137200225500ustar00rootroot00000000000000source "https://rubygems.org" # Specify your gem's dependencies in ahoy_email.gemspec gemspec path: "../../" gem "actionmailer", "~> 4.2.0" gem "activerecord", "~> 4.2.0" ahoy_email-1.0.3/test/gemfiles/actionmailer50.gemfile000066400000000000000000000002551343271137200225470ustar00rootroot00000000000000source "https://rubygems.org" # Specify your gem's dependencies in ahoy_email.gemspec gemspec path: "../../" gem "actionmailer", "~> 5.0.0" gem "activerecord", "~> 5.0.0" ahoy_email-1.0.3/test/gemfiles/actionmailer51.gemfile000066400000000000000000000002551343271137200225500ustar00rootroot00000000000000source "https://rubygems.org" # Specify your gem's dependencies in ahoy_email.gemspec gemspec path: "../../" gem "actionmailer", "~> 5.1.0" gem "activerecord", "~> 5.1.0" ahoy_email-1.0.3/test/gemfiles/mongoid6.gemfile000066400000000000000000000002111343271137200214450ustar00rootroot00000000000000source "https://rubygems.org" # Specify your gem's dependencies in ahoy_email.gemspec gemspec path: "../../" gem "mongoid", "~> 6.0.0" ahoy_email-1.0.3/test/internal/000077500000000000000000000000001343271137200164205ustar00rootroot00000000000000ahoy_email-1.0.3/test/internal/app/000077500000000000000000000000001343271137200172005ustar00rootroot00000000000000ahoy_email-1.0.3/test/internal/app/mailers/000077500000000000000000000000001343271137200206345ustar00rootroot00000000000000ahoy_email-1.0.3/test/internal/app/mailers/application_mailer.rb000066400000000000000000000004141343271137200250140ustar00rootroot00000000000000class ApplicationMailer < ActionMailer::Base default from: "from@example.org", to: "to@example.org", subject: "Hello", body: "World" def mail_html(html) mail do |format| format.html { render plain: html } end end end ahoy_email-1.0.3/test/internal/app/mailers/click_mailer.rb000066400000000000000000000007161343271137200236030ustar00rootroot00000000000000class ClickMailer < ApplicationMailer track click: true, except: [:welcome] def welcome mail_html('Test') end def basic mail_html('Test') end def mailto mail_html('Test') end def app mail_html('Test') end def schemeless mail_html('Test') end end ahoy_email-1.0.3/test/internal/app/mailers/extra_mailer.rb000066400000000000000000000004261343271137200236370ustar00rootroot00000000000000class ExtraMailer < ApplicationMailer track extra: {coupon_id: 1}, only: [:basic] track extra: -> { {coupon_id: @coupon_id} }, only: [:dynamic] def welcome mail end def basic mail end def dynamic(coupon_id) @coupon_id = coupon_id mail end end ahoy_email-1.0.3/test/internal/app/mailers/message_mailer.rb000066400000000000000000000006231343271137200241370ustar00rootroot00000000000000class MessageMailer < ApplicationMailer track message: false, only: [:other] track message: true, only: [:other2] after_action :prevent_delivery def welcome mail end def other mail end def other2 mail end def no_deliver @prevent_delivery = true mail end private def prevent_delivery mail.perform_deliveries = false if @prevent_delivery end end ahoy_email-1.0.3/test/internal/app/mailers/open_mailer.rb000066400000000000000000000002341343271137200234520ustar00rootroot00000000000000class OpenMailer < ApplicationMailer track open: true, only: [:basic] def welcome mail_html('Hi') end def basic mail_html('Hi') end end ahoy_email-1.0.3/test/internal/app/mailers/unmailed_mailer.rb000066400000000000000000000001751343271137200243130ustar00rootroot00000000000000# extend ActionMailer::Base directly # to prevent default to class UnmailedMailer < ActionMailer::Base def hello end end ahoy_email-1.0.3/test/internal/app/mailers/user_mailer.rb000066400000000000000000000004351343271137200234720ustar00rootroot00000000000000class UserMailer < ApplicationMailer track user: -> { @some_user }, only: [:dynamic] def welcome mail end def user_var(user) @user = user mail end def to_field(to) mail to: to end def dynamic(some_user) @some_user = some_user mail end end ahoy_email-1.0.3/test/internal/app/mailers/utm_params_mailer.rb000066400000000000000000000005261343271137200246650ustar00rootroot00000000000000class UtmParamsMailer < ApplicationMailer track utm_params: true, except: [:welcome] def welcome mail_html('Test') end def basic mail_html('Test') end def array_params mail_html('Hi') end end ahoy_email-1.0.3/test/internal/app/models/000077500000000000000000000000001343271137200204635ustar00rootroot00000000000000ahoy_email-1.0.3/test/internal/app/models/user.rb000066400000000000000000000000441343271137200217640ustar00rootroot00000000000000class User < ActiveRecord::Base end ahoy_email-1.0.3/test/internal/config/000077500000000000000000000000001343271137200176655ustar00rootroot00000000000000ahoy_email-1.0.3/test/internal/config/database.yml000066400000000000000000000001001343271137200221430ustar00rootroot00000000000000test: adapter: sqlite3 database: db/combustion_test.sqlite ahoy_email-1.0.3/test/internal/config/routes.rb000066400000000000000000000000511343271137200215270ustar00rootroot00000000000000Rails.application.routes.draw do # end ahoy_email-1.0.3/test/internal/db/000077500000000000000000000000001343271137200170055ustar00rootroot00000000000000ahoy_email-1.0.3/test/internal/db/schema.rb000066400000000000000000000007411343271137200205740ustar00rootroot00000000000000ActiveRecord::Schema.define do create_table :ahoy_messages, force: true do |t| t.references :user, polymorphic: true t.text :to t.string :mailer t.text :subject t.timestamp :sent_at # extra t.integer :coupon_id # legacy t.text :content t.string :utm_source t.string :utm_medium t.string :utm_term t.string :utm_content t.string :utm_campaign end create_table :users, force: true do |t| t.string :email end end ahoy_email-1.0.3/test/message_test.rb000066400000000000000000000016231343271137200176160ustar00rootroot00000000000000require_relative "test_helper" class MessageTest < Minitest::Test def test_default MessageMailer.welcome.deliver_now assert ahoy_message assert_equal "Hello", ahoy_message.subject assert_match "Hello", ahoy_message.content assert_match "World", ahoy_message.content end def test_false MessageMailer.other.deliver_now assert_nil ahoy_message end def test_prevent_delivery MessageMailer.no_deliver.deliver_now assert_nil ahoy_message end def test_default_false with_default(message: false) do MessageMailer.welcome.deliver_now assert_nil ahoy_message end end def test_default_false_track with_default(message: false) do MessageMailer.other2.deliver_now assert ahoy_message end end def test_ahoy_message message = MessageMailer.welcome.deliver_now assert_equal message.ahoy_message, ahoy_message end end ahoy_email-1.0.3/test/open_test.rb000066400000000000000000000004251343271137200171320ustar00rootroot00000000000000require_relative "test_helper" class OpenTest < Minitest::Test def test_default message = OpenMailer.welcome.deliver_now refute_body "open.gif", message end def test_basic message = OpenMailer.basic.deliver_now assert_body "open.gif", message end end ahoy_email-1.0.3/test/support/000077500000000000000000000000001343271137200163205ustar00rootroot00000000000000ahoy_email-1.0.3/test/support/mongoid.rb000066400000000000000000000013551343271137200203050ustar00rootroot00000000000000Mongoid.logger.level = Logger::INFO Mongo::Logger.logger.level = Logger::INFO if defined?(Mongo::Logger) Mongoid.configure do |config| config.connect_to "ahoy_email_test" end class User include Mongoid::Document field :email, type: String end class Ahoy::Message include Mongoid::Document belongs_to :user, polymorphic: true, optional: true, index: true field :to, type: String field :mailer, type: String field :subject, type: String field :sent_at, type: Time # extra field :coupon_id, type: Integer # legacy field :content, type: String field :utm_source, type: String field :utm_campaign, type: String field :utm_term, type: String field :utm_medium, type: String field :utm_content, type: String end ahoy_email-1.0.3/test/test_helper.rb000066400000000000000000000020251343271137200174460ustar00rootroot00000000000000require "bundler/setup" require "combustion" Bundler.require(:default) require "minitest/autorun" require "minitest/pride" Combustion.path = "test/internal" Combustion.initialize! :active_record, :action_mailer do if config.active_record.sqlite3.respond_to?(:represent_boolean_as_integer) config.active_record.sqlite3.represent_boolean_as_integer = true end end require_relative "support/mongoid" if defined?(Mongoid) ActionMailer::Base.delivery_method = :test class Minitest::Test def setup Ahoy::Message.delete_all end def ahoy_message Ahoy::Message.last end def refute_body(str, message) refute_match str, message.body.decoded end def assert_body(str, message) assert_match str, message.body.decoded end def params_supported? Rails.version > "5.1.0" end def with_default(options) previous_options = AhoyEmail.default_options.dup begin AhoyEmail.default_options.merge!(options) yield ensure AhoyEmail.default_options = previous_options end end end ahoy_email-1.0.3/test/unmailed_test.rb000066400000000000000000000002431343271137200177650ustar00rootroot00000000000000require_relative "test_helper" class UnmailedTest < Minitest::Test def test_unmailed UnmailedMailer.hello.deliver_now assert_nil ahoy_message end end ahoy_email-1.0.3/test/user_test.rb000066400000000000000000000014611343271137200171500ustar00rootroot00000000000000require_relative "test_helper" class UserTest < Minitest::Test def setup super User.delete_all end def test_no_user UserMailer.welcome.deliver_now assert_nil ahoy_message.user end def test_user_var user = User.create! UserMailer.user_var(user).deliver_now assert_equal user, ahoy_message.user end def test_user_param skip unless params_supported? user = User.create! UserMailer.with(user: user).welcome.deliver_now assert_equal user, ahoy_message.user end def test_to user = User.create!(email: "test@example.org") UserMailer.to_field(user.email).deliver_now assert_equal user, ahoy_message.user end def test_dynamic user = User.create! UserMailer.dynamic(user).deliver_now assert_equal user, ahoy_message.user end end ahoy_email-1.0.3/test/utm_params_test.rb000066400000000000000000000014651343271137200203460ustar00rootroot00000000000000require_relative "test_helper" class UtmParamsTest < Minitest::Test def test_default message = UtmParamsMailer.welcome.deliver_now refute_body "utm", message assert_nil ahoy_message.utm_campaign assert_nil ahoy_message.utm_medium assert_nil ahoy_message.utm_source end def test_basic message = UtmParamsMailer.basic.deliver_now assert_body "utm_campaign=basic", message assert_body "utm_medium=email", message assert_body "utm_source=utm_params_mailer", message assert_equal "basic", ahoy_message.utm_campaign assert_equal "email", ahoy_message.utm_medium assert_equal "utm_params_mailer", ahoy_message.utm_source end def test_array_params message = UtmParamsMailer.array_params.deliver_now assert_body "baz%5B%5D=1&baz%5B%5D=2", message end end