ahoy_email 0.5.2 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CONTRIBUTING.md +47 -0
- data/LICENSE.txt +1 -1
- data/README.md +135 -99
- data/app/controllers/ahoy/messages_controller.rb +21 -19
- data/app/models/ahoy/message.rb +1 -1
- data/config/routes.rb +1 -3
- data/lib/ahoy_email.rb +43 -19
- data/lib/ahoy_email/engine.rb +0 -2
- data/lib/ahoy_email/interceptor.rb +1 -1
- data/lib/ahoy_email/mailer.rb +27 -21
- data/lib/ahoy_email/processor.rb +40 -60
- data/lib/ahoy_email/tracker.rb +21 -0
- data/lib/ahoy_email/version.rb +1 -1
- data/lib/generators/ahoy_email/templates/install.rb.tt +11 -0
- metadata +25 -59
- data/.gitignore +0 -24
- data/.travis.yml +0 -16
- data/Gemfile +0 -6
- data/Rakefile +0 -9
- data/ahoy_email.gemspec +0 -35
- data/lib/generators/ahoy_email/templates/install.rb +0 -30
- data/test/gemfiles/actionmailer42.gemfile +0 -6
- data/test/gemfiles/actionmailer50.gemfile +0 -6
- data/test/gemfiles/actionmailer51.gemfile +0 -6
- data/test/internal/config/database.yml +0 -3
- data/test/internal/config/routes.rb +0 -3
- data/test/internal/db/schema.rb +0 -30
- data/test/mailer_test.rb +0 -153
- data/test/test_helper.rb +0 -16
data/app/models/ahoy/message.rb
CHANGED
data/config/routes.rb
CHANGED
data/lib/ahoy_email.rb
CHANGED
@@ -1,45 +1,69 @@
|
|
1
|
+
# dependencies
|
1
2
|
require "active_support"
|
2
|
-
require "nokogiri"
|
3
3
|
require "addressable/uri"
|
4
|
+
require "nokogiri"
|
4
5
|
require "openssl"
|
5
6
|
require "safely/core"
|
7
|
+
|
8
|
+
# modules
|
6
9
|
require "ahoy_email/processor"
|
10
|
+
require "ahoy_email/tracker"
|
7
11
|
require "ahoy_email/interceptor"
|
8
12
|
require "ahoy_email/mailer"
|
9
|
-
require "ahoy_email/engine"
|
10
13
|
require "ahoy_email/version"
|
14
|
+
require "ahoy_email/engine" if defined?(Rails)
|
11
15
|
|
12
16
|
module AhoyEmail
|
13
|
-
mattr_accessor :secret_token, :
|
17
|
+
mattr_accessor :secret_token, :default_options, :subscribers, :invalid_redirect_url, :track_method, :api, :preserve_callbacks
|
18
|
+
mattr_writer :message_model
|
14
19
|
|
15
|
-
self.
|
20
|
+
self.api = false
|
21
|
+
|
22
|
+
self.default_options = {
|
16
23
|
message: true,
|
17
|
-
open:
|
18
|
-
click:
|
19
|
-
utm_params:
|
20
|
-
utm_source: ->
|
24
|
+
open: false,
|
25
|
+
click: false,
|
26
|
+
utm_params: false,
|
27
|
+
utm_source: -> { mailer_name },
|
21
28
|
utm_medium: "email",
|
22
29
|
utm_term: nil,
|
23
30
|
utm_content: nil,
|
24
|
-
utm_campaign: ->
|
25
|
-
user: ->(
|
26
|
-
mailer: ->
|
31
|
+
utm_campaign: -> { action_name },
|
32
|
+
user: -> { @user || (respond_to?(:params) && params && params[:user]) || (message.to.size == 1 ? (User.find_by(email: message.to.first) rescue nil) : nil) },
|
33
|
+
mailer: -> { "#{self.class.name}##{action_name}" },
|
27
34
|
url_options: {},
|
28
|
-
|
35
|
+
extra: {},
|
36
|
+
unsubscribe_links: false
|
29
37
|
}
|
30
38
|
|
31
|
-
self.
|
39
|
+
self.track_method = lambda do |data|
|
40
|
+
message = data[:message]
|
32
41
|
|
33
|
-
|
42
|
+
ahoy_message = AhoyEmail.message_model.new
|
43
|
+
ahoy_message.to = Array(message.to).join(", ") if ahoy_message.respond_to?(:to=)
|
44
|
+
ahoy_message.user_type = data[:user_type]
|
45
|
+
ahoy_message.user_id = data[:user_id]
|
34
46
|
|
35
|
-
|
36
|
-
|
37
|
-
|
47
|
+
ahoy_message.mailer = data[:mailer] if ahoy_message.respond_to?(:mailer=)
|
48
|
+
ahoy_message.subject = message.subject if ahoy_message.respond_to?(:subject=)
|
49
|
+
ahoy_message.content = message.encoded if ahoy_message.respond_to?(:content=)
|
38
50
|
|
39
|
-
|
40
|
-
|
51
|
+
AhoyEmail::Processor::UTM_PARAMETERS.each do |k|
|
52
|
+
ahoy_message.send("#{k}=", data[k.to_sym]) if ahoy_message.respond_to?("#{k}=")
|
53
|
+
end
|
54
|
+
|
55
|
+
ahoy_message.token = data[:token] if ahoy_message.respond_to?(:token=)
|
56
|
+
|
57
|
+
ahoy_message.assign_attributes(data[:extra] || {})
|
58
|
+
|
59
|
+
ahoy_message.sent_at = Time.now
|
60
|
+
ahoy_message.save!
|
41
61
|
end
|
42
62
|
|
63
|
+
self.subscribers = []
|
64
|
+
|
65
|
+
self.preserve_callbacks = []
|
66
|
+
|
43
67
|
def self.message_model
|
44
68
|
model = (defined?(@message_model) && @message_model) || ::Ahoy::Message
|
45
69
|
model = model.call if model.respond_to?(:call)
|
data/lib/ahoy_email/engine.rb
CHANGED
data/lib/ahoy_email/mailer.rb
CHANGED
@@ -1,33 +1,39 @@
|
|
1
1
|
module AhoyEmail
|
2
2
|
module Mailer
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
class_attribute :ahoy_options
|
9
|
-
self.ahoy_options = {}
|
10
|
-
end
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attr_writer :ahoy_options
|
7
|
+
after_action :save_ahoy_options
|
11
8
|
end
|
12
9
|
|
13
|
-
|
14
|
-
def track(options
|
15
|
-
|
10
|
+
class_methods do
|
11
|
+
def track(**options)
|
12
|
+
before_action(options.slice(:only, :except)) do
|
13
|
+
self.ahoy_options = ahoy_options.merge(message: true).merge(options.except(:only, :except))
|
14
|
+
end
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
def track(**options)
|
19
|
+
self.ahoy_options = ahoy_options.merge(message: true).merge(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def ahoy_options
|
23
|
+
@ahoy_options ||= AhoyEmail.default_options
|
24
|
+
end
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
def save_ahoy_options
|
27
|
+
if ahoy_options[:message]
|
28
|
+
Safely.safely do
|
29
|
+
options = {}
|
30
|
+
ahoy_options.each do |k, v|
|
31
|
+
# execute options in mailer content
|
32
|
+
options[k] = v.respond_to?(:call) ? instance_exec(&v) : v
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
message
|
35
|
+
AhoyEmail::Processor.new(self, options).perform
|
36
|
+
end
|
31
37
|
end
|
32
38
|
end
|
33
39
|
end
|
data/lib/ahoy_email/processor.rb
CHANGED
@@ -1,74 +1,57 @@
|
|
1
1
|
module AhoyEmail
|
2
2
|
class Processor
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :mailer, :options
|
4
4
|
|
5
5
|
UTM_PARAMETERS = %w(utm_source utm_medium utm_term utm_content utm_campaign)
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@message = message
|
7
|
+
def initialize(mailer, options)
|
9
8
|
@mailer = mailer
|
10
|
-
|
11
|
-
|
12
|
-
def process
|
13
|
-
Safely.safely do
|
14
|
-
action_name = mailer.action_name.to_sym
|
15
|
-
if options[:message] && (!options[:only] || options[:only].include?(action_name)) && !options[:except].to_a.include?(action_name)
|
16
|
-
@ahoy_message = AhoyEmail.message_model.new
|
17
|
-
ahoy_message.token = generate_token
|
18
|
-
ahoy_message.to = Array(message.to).join(", ") if ahoy_message.respond_to?(:to=)
|
19
|
-
ahoy_message.user = options[:user]
|
9
|
+
@options = options
|
20
10
|
|
21
|
-
|
22
|
-
|
11
|
+
unknown_keywords = options.keys - AhoyEmail.default_options.keys
|
12
|
+
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
13
|
+
end
|
23
14
|
|
24
|
-
|
25
|
-
|
26
|
-
|
15
|
+
def perform
|
16
|
+
track_open if options[:open]
|
17
|
+
track_links if options[:utm_params] || options[:click]
|
18
|
+
track_message
|
19
|
+
end
|
27
20
|
|
28
|
-
|
29
|
-
ahoy_message.send("#{k}=", options[k.to_sym]) if ahoy_message.respond_to?("#{k}=")
|
30
|
-
end
|
21
|
+
protected
|
31
22
|
|
32
|
-
|
23
|
+
def message
|
24
|
+
mailer.message
|
25
|
+
end
|
33
26
|
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
end
|
27
|
+
def token
|
28
|
+
@token ||= SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32)
|
38
29
|
end
|
39
30
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
31
|
+
def track_message
|
32
|
+
data = {
|
33
|
+
mailer: options[:mailer],
|
34
|
+
extra: options[:extra]
|
35
|
+
}
|
36
|
+
|
37
|
+
user = options[:user]
|
38
|
+
if user
|
39
|
+
data[:user_type] = user.model_name.name
|
40
|
+
id = user.id
|
41
|
+
data[:user_id] = id.is_a?(Integer) ? id : id.to_s
|
50
42
|
end
|
51
|
-
end
|
52
43
|
|
53
|
-
|
44
|
+
if options[:open] || options[:click]
|
45
|
+
data[:token] = token
|
46
|
+
end
|
54
47
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
if mailer.ahoy_options
|
59
|
-
options = options.except(:only, :except).merge(mailer.ahoy_options)
|
48
|
+
if options[:utm_params]
|
49
|
+
UTM_PARAMETERS.each do |k|
|
50
|
+
data[k] = options[k.to_sym] if options[k.to_sym]
|
60
51
|
end
|
61
|
-
options.each do |k, v|
|
62
|
-
if v.respond_to?(:call)
|
63
|
-
options[k] = v.call(message, mailer)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
options
|
67
52
|
end
|
68
|
-
end
|
69
53
|
|
70
|
-
|
71
|
-
SecureRandom.urlsafe_base64(32).gsub(/[\-_]/, "").first(32)
|
54
|
+
mailer.message["Ahoy-Message"] = data.to_json
|
72
55
|
end
|
73
56
|
|
74
57
|
def track_open
|
@@ -79,7 +62,7 @@ module AhoyEmail
|
|
79
62
|
url_for(
|
80
63
|
controller: "ahoy/messages",
|
81
64
|
action: "open",
|
82
|
-
id:
|
65
|
+
id: token,
|
83
66
|
format: "gif"
|
84
67
|
)
|
85
68
|
pixel = ActionController::Base.helpers.image_tag(url, size: "1x1", alt: "")
|
@@ -113,12 +96,13 @@ module AhoyEmail
|
|
113
96
|
end
|
114
97
|
|
115
98
|
if options[:click] && !skip_attribute?(link, "click")
|
116
|
-
|
99
|
+
# TODO sign more than just url and transition to HMAC-SHA256
|
100
|
+
signature = OpenSSL::HMAC.hexdigest("SHA1", AhoyEmail.secret_token, link["href"])
|
117
101
|
link["href"] =
|
118
102
|
url_for(
|
119
103
|
controller: "ahoy/messages",
|
120
104
|
action: "click",
|
121
|
-
id:
|
105
|
+
id: token,
|
122
106
|
url: link["href"],
|
123
107
|
signature: signature
|
124
108
|
)
|
@@ -157,11 +141,7 @@ module AhoyEmail
|
|
157
141
|
# Return uri if valid, nil otherwise
|
158
142
|
def parse_uri(href)
|
159
143
|
# to_s prevent to return nil from this method
|
160
|
-
|
161
|
-
Addressable::URI.heuristic_parse(href.to_s) rescue nil
|
162
|
-
else
|
163
|
-
Addressable::URI.parse(href.to_s) rescue nil
|
164
|
-
end
|
144
|
+
Addressable::URI.heuristic_parse(href.to_s) rescue nil
|
165
145
|
end
|
166
146
|
|
167
147
|
def url_for(opt)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module AhoyEmail
|
2
|
+
class Tracker
|
3
|
+
attr_reader :message
|
4
|
+
|
5
|
+
def initialize(message)
|
6
|
+
@message = message
|
7
|
+
end
|
8
|
+
|
9
|
+
def perform
|
10
|
+
if message.perform_deliveries && (data_header = message["Ahoy-Message"])
|
11
|
+
Safely.safely do
|
12
|
+
data = JSON.parse(data_header.to_s).symbolize_keys
|
13
|
+
data[:message] = message
|
14
|
+
AhoyEmail.track_method.call(data)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
ensure
|
18
|
+
message["Ahoy-Message"] = nil if message["Ahoy-Message"]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/ahoy_email/version.rb
CHANGED
@@ -0,0 +1,11 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :ahoy_messages do |t|
|
4
|
+
t.references :user, polymorphic: true
|
5
|
+
t.text :to
|
6
|
+
t.string :mailer
|
7
|
+
t.text :subject
|
8
|
+
t.timestamp :sent_at
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
metadata
CHANGED
@@ -1,45 +1,45 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ahoy_email
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-09-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: actionmailer
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '4.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '4.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: addressable
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 2.3.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 2.3.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: nokogiri
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,27 +53,27 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: safely_block
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 0.1.1
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 0.1.1
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: bundler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
|
-
type: :
|
76
|
+
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
@@ -81,21 +81,7 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: 0.1.1
|
90
|
-
type: :runtime
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 0.1.1
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: bundler
|
84
|
+
name: rake
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
100
86
|
requirements:
|
101
87
|
- - ">="
|
@@ -109,7 +95,7 @@ dependencies:
|
|
109
95
|
- !ruby/object:Gem::Version
|
110
96
|
version: '0'
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
98
|
+
name: minitest
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
101
|
- - ">="
|
@@ -123,7 +109,7 @@ dependencies:
|
|
123
109
|
- !ruby/object:Gem::Version
|
124
110
|
version: '0'
|
125
111
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
112
|
+
name: activerecord
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
128
114
|
requirements:
|
129
115
|
- - ">="
|
@@ -179,20 +165,15 @@ dependencies:
|
|
179
165
|
- !ruby/object:Gem::Version
|
180
166
|
version: '0'
|
181
167
|
description:
|
182
|
-
email:
|
183
|
-
- andrew@chartkick.com
|
168
|
+
email: andrew@chartkick.com
|
184
169
|
executables: []
|
185
170
|
extensions: []
|
186
171
|
extra_rdoc_files: []
|
187
172
|
files:
|
188
|
-
- ".gitignore"
|
189
|
-
- ".travis.yml"
|
190
173
|
- CHANGELOG.md
|
191
|
-
-
|
174
|
+
- CONTRIBUTING.md
|
192
175
|
- LICENSE.txt
|
193
176
|
- README.md
|
194
|
-
- Rakefile
|
195
|
-
- ahoy_email.gemspec
|
196
177
|
- app/controllers/ahoy/messages_controller.rb
|
197
178
|
- app/models/ahoy/message.rb
|
198
179
|
- config/routes.rb
|
@@ -201,17 +182,10 @@ files:
|
|
201
182
|
- lib/ahoy_email/interceptor.rb
|
202
183
|
- lib/ahoy_email/mailer.rb
|
203
184
|
- lib/ahoy_email/processor.rb
|
185
|
+
- lib/ahoy_email/tracker.rb
|
204
186
|
- lib/ahoy_email/version.rb
|
205
187
|
- lib/generators/ahoy_email/install_generator.rb
|
206
|
-
- lib/generators/ahoy_email/templates/install.rb
|
207
|
-
- test/gemfiles/actionmailer42.gemfile
|
208
|
-
- test/gemfiles/actionmailer50.gemfile
|
209
|
-
- test/gemfiles/actionmailer51.gemfile
|
210
|
-
- test/internal/config/database.yml
|
211
|
-
- test/internal/config/routes.rb
|
212
|
-
- test/internal/db/schema.rb
|
213
|
-
- test/mailer_test.rb
|
214
|
-
- test/test_helper.rb
|
188
|
+
- lib/generators/ahoy_email/templates/install.rb.tt
|
215
189
|
homepage: https://github.com/ankane/ahoy_email
|
216
190
|
licenses:
|
217
191
|
- MIT
|
@@ -224,7 +198,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
224
198
|
requirements:
|
225
199
|
- - ">="
|
226
200
|
- !ruby/object:Gem::Version
|
227
|
-
version: 2.2
|
201
|
+
version: '2.2'
|
228
202
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
229
203
|
requirements:
|
230
204
|
- - ">="
|
@@ -232,16 +206,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
232
206
|
version: '0'
|
233
207
|
requirements: []
|
234
208
|
rubyforge_project:
|
235
|
-
rubygems_version: 2.7.
|
209
|
+
rubygems_version: 2.7.7
|
236
210
|
signing_key:
|
237
211
|
specification_version: 4
|
238
|
-
summary:
|
239
|
-
test_files:
|
240
|
-
- test/gemfiles/actionmailer42.gemfile
|
241
|
-
- test/gemfiles/actionmailer50.gemfile
|
242
|
-
- test/gemfiles/actionmailer51.gemfile
|
243
|
-
- test/internal/config/database.yml
|
244
|
-
- test/internal/config/routes.rb
|
245
|
-
- test/internal/db/schema.rb
|
246
|
-
- test/mailer_test.rb
|
247
|
-
- test/test_helper.rb
|
212
|
+
summary: Email analytics for Rails
|
213
|
+
test_files: []
|