mailkick 0.3.1 → 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 +41 -18
- data/LICENSE.txt +1 -1
- data/README.md +153 -85
- data/app/controllers/mailkick/subscriptions_controller.rb +38 -20
- data/app/models/mailkick/opt_out.rb +1 -1
- data/app/models/mailkick/subscription.rb +9 -0
- data/app/views/mailkick/subscriptions/show.html.erb +1 -1
- data/lib/generators/mailkick/install_generator.rb +4 -21
- data/lib/generators/mailkick/templates/install.rb.tt +11 -0
- data/lib/mailkick.rb +23 -69
- data/lib/mailkick/engine.rb +0 -4
- data/lib/mailkick/legacy.rb +70 -0
- data/lib/mailkick/model.rb +11 -21
- data/lib/mailkick/service.rb +1 -16
- data/lib/mailkick/service/aws_ses.rb +47 -0
- data/lib/mailkick/service/postmark.rb +41 -0
- data/lib/mailkick/service/sendgrid.rb +4 -1
- data/lib/mailkick/service/sendgrid_v2.rb +52 -0
- data/lib/mailkick/url_helper.rb +8 -0
- data/lib/mailkick/version.rb +1 -1
- metadata +20 -184
- data/.gitignore +0 -25
- data/.travis.yml +0 -16
- data/Gemfile +0 -6
- data/Rakefile +0 -9
- data/app/helpers/mailkick/url_helper.rb +0 -15
- data/lib/generators/mailkick/templates/install.rb +0 -14
- data/mailkick.gemspec +0 -32
- data/test/gemfiles/actionmailer42.gemfile +0 -6
- data/test/gemfiles/actionmailer50.gemfile +0 -6
- data/test/gemfiles/actionmailer51.gemfile +0 -6
- data/test/internal/app/mailers/user_mailer.rb +0 -7
- data/test/internal/app/models/user.rb +0 -3
- data/test/internal/app/views/user_mailer/welcome.html.erb +0 -1
- data/test/internal/app/views/user_mailer/welcome.text.erb +0 -1
- data/test/internal/config/database.yml +0 -3
- data/test/internal/db/schema.rb +0 -16
- data/test/mailkick_test.rb +0 -29
- data/test/test_helper.rb +0 -18
@@ -2,44 +2,50 @@ module Mailkick
|
|
2
2
|
class SubscriptionsController < ActionController::Base
|
3
3
|
protect_from_forgery with: :exception
|
4
4
|
|
5
|
-
before_action :
|
5
|
+
before_action :set_subscription
|
6
6
|
|
7
7
|
def show
|
8
8
|
end
|
9
9
|
|
10
10
|
def unsubscribe
|
11
|
-
|
11
|
+
subscription.delete_all
|
12
|
+
|
13
|
+
Mailkick::Legacy.opt_out(legacy_options) if Mailkick::Legacy.opt_outs?
|
14
|
+
|
12
15
|
redirect_to subscription_path(params[:id])
|
13
16
|
end
|
14
17
|
|
15
18
|
def subscribe
|
16
|
-
|
19
|
+
subscription.first_or_create!
|
20
|
+
|
21
|
+
Mailkick::Legacy.opt_in(legacy_options) if Mailkick::Legacy.opt_outs?
|
22
|
+
|
17
23
|
redirect_to subscription_path(params[:id])
|
18
24
|
end
|
19
25
|
|
20
26
|
protected
|
21
27
|
|
22
|
-
def
|
23
|
-
@email,
|
24
|
-
if user_type
|
25
|
-
# on the unprobabilistic chance user_type is compromised, not much damage
|
26
|
-
@user = user_type.constantize.find(user_id)
|
27
|
-
end
|
28
|
-
@options = {
|
29
|
-
email: @email,
|
30
|
-
user: @user,
|
31
|
-
list: @list
|
32
|
-
}
|
28
|
+
def set_subscription
|
29
|
+
@email, @subscriber_id, @subscriber_type, @list = Mailkick.message_verifier.verify(params[:id])
|
33
30
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
render plain: "Subscription not found", status: :bad_request
|
32
|
+
end
|
33
|
+
|
34
|
+
def subscription
|
35
|
+
Mailkick::Subscription.where(
|
36
|
+
subscriber_id: @subscriber_id,
|
37
|
+
subscriber_type: @subscriber_type,
|
38
|
+
list: @list
|
39
|
+
)
|
39
40
|
end
|
40
41
|
|
42
|
+
def subscribed?
|
43
|
+
subscription.exists?
|
44
|
+
end
|
45
|
+
helper_method :subscribed?
|
46
|
+
|
41
47
|
def opted_out?
|
42
|
-
|
48
|
+
!subscribed?
|
43
49
|
end
|
44
50
|
helper_method :opted_out?
|
45
51
|
|
@@ -52,5 +58,17 @@ module Mailkick
|
|
52
58
|
unsubscribe_subscription_path(params[:id])
|
53
59
|
end
|
54
60
|
helper_method :unsubscribe_url
|
61
|
+
|
62
|
+
def legacy_options
|
63
|
+
if @subscriber_type
|
64
|
+
# on the unprobabilistic chance subscriber_type is compromised, not much damage
|
65
|
+
user = @subscriber_type.constantize.find(@subscriber_id)
|
66
|
+
end
|
67
|
+
{
|
68
|
+
email: @email,
|
69
|
+
user: user,
|
70
|
+
list: @list
|
71
|
+
}
|
72
|
+
end
|
55
73
|
end
|
56
74
|
end
|
@@ -2,6 +2,6 @@ module Mailkick
|
|
2
2
|
class OptOut < ActiveRecord::Base
|
3
3
|
self.table_name = "mailkick_opt_outs"
|
4
4
|
|
5
|
-
belongs_to :user,
|
5
|
+
belongs_to :user, polymorphic: true, optional: true
|
6
6
|
end
|
7
7
|
end
|
@@ -1,34 +1,17 @@
|
|
1
|
-
# taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb
|
2
|
-
require "rails/generators"
|
3
|
-
require "rails/generators/migration"
|
4
|
-
require "active_record"
|
5
1
|
require "rails/generators/active_record"
|
6
2
|
|
7
3
|
module Mailkick
|
8
4
|
module Generators
|
9
5
|
class InstallGenerator < Rails::Generators::Base
|
10
|
-
include
|
11
|
-
|
12
|
-
source_root File.expand_path("../templates", __FILE__)
|
13
|
-
|
14
|
-
# Implement the required interface for Rails::Generators::Migration.
|
15
|
-
def self.next_migration_number(dirname) #:nodoc:
|
16
|
-
next_migration_number = current_migration_number(dirname) + 1
|
17
|
-
if ActiveRecord::Base.timestamped_migrations
|
18
|
-
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
19
|
-
else
|
20
|
-
"%.3d" % next_migration_number
|
21
|
-
end
|
22
|
-
end
|
6
|
+
include ActiveRecord::Generators::Migration
|
7
|
+
source_root File.join(__dir__, "templates")
|
23
8
|
|
24
9
|
def copy_migration
|
25
|
-
migration_template "install.rb", "db/migrate/
|
10
|
+
migration_template "install.rb", "db/migrate/create_mailkick_subscriptions.rb", migration_version: migration_version
|
26
11
|
end
|
27
12
|
|
28
13
|
def migration_version
|
29
|
-
|
30
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
31
|
-
end
|
14
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
32
15
|
end
|
33
16
|
end
|
34
17
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :mailkick_subscriptions do |t|
|
4
|
+
t.references :subscriber, polymorphic: true, index: false
|
5
|
+
t.string :list
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :mailkick_subscriptions, [:subscriber_type, :subscriber_id, :list], unique: true, name: "index_mailkick_subscriptions_on_subscriber_and_list"
|
10
|
+
end
|
11
|
+
end
|
data/lib/mailkick.rb
CHANGED
@@ -1,20 +1,31 @@
|
|
1
|
-
|
1
|
+
# dependencies
|
2
2
|
require "active_support"
|
3
3
|
|
4
|
-
|
4
|
+
# stdlib
|
5
|
+
require "set"
|
6
|
+
|
7
|
+
# modules
|
8
|
+
require "mailkick/legacy"
|
5
9
|
require "mailkick/model"
|
6
10
|
require "mailkick/service"
|
11
|
+
require "mailkick/service/aws_ses"
|
7
12
|
require "mailkick/service/mailchimp"
|
8
13
|
require "mailkick/service/mailgun"
|
9
14
|
require "mailkick/service/mandrill"
|
10
15
|
require "mailkick/service/sendgrid"
|
16
|
+
require "mailkick/service/sendgrid_v2"
|
17
|
+
require "mailkick/service/postmark"
|
18
|
+
require "mailkick/url_helper"
|
11
19
|
require "mailkick/version"
|
12
20
|
|
21
|
+
# integrations
|
22
|
+
require "mailkick/engine" if defined?(Rails)
|
23
|
+
|
13
24
|
module Mailkick
|
14
|
-
mattr_accessor :services, :
|
25
|
+
mattr_accessor :services, :secret_token, :mount, :process_opt_outs_method
|
15
26
|
self.services = []
|
16
|
-
self.user_method = ->(email) { User.where(email: email).first rescue nil }
|
17
27
|
self.mount = true
|
28
|
+
self.process_opt_outs_method = -> { raise "process_opt_outs_method not defined" }
|
18
29
|
|
19
30
|
def self.fetch_opt_outs
|
20
31
|
services.each(&:fetch_opt_outs)
|
@@ -26,79 +37,22 @@ module Mailkick
|
|
26
37
|
end
|
27
38
|
end
|
28
39
|
|
29
|
-
def self.opted_out?(options)
|
30
|
-
opt_outs(options).any?
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.opt_out(options)
|
34
|
-
unless opted_out?(options)
|
35
|
-
time = options[:time] || Time.now
|
36
|
-
Mailkick::OptOut.create! do |o|
|
37
|
-
o.email = options[:email]
|
38
|
-
o.user = options[:user]
|
39
|
-
o.reason = options[:reason] || "unsubscribe"
|
40
|
-
o.list = options[:list]
|
41
|
-
o.created_at = time
|
42
|
-
o.updated_at = time
|
43
|
-
end
|
44
|
-
end
|
45
|
-
true
|
46
|
-
end
|
47
|
-
|
48
|
-
def self.opt_in(options)
|
49
|
-
opt_outs(options).each do |opt_out|
|
50
|
-
opt_out.active = false
|
51
|
-
opt_out.save!
|
52
|
-
end
|
53
|
-
true
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.opt_outs(options = {})
|
57
|
-
relation = Mailkick::OptOut.where(active: true)
|
58
|
-
|
59
|
-
parts = []
|
60
|
-
binds = []
|
61
|
-
if (email = options[:email])
|
62
|
-
parts << "email = ?"
|
63
|
-
binds << email
|
64
|
-
end
|
65
|
-
if (user = options[:user])
|
66
|
-
parts << "(user_id = ? and user_type = ?)"
|
67
|
-
binds.concat [user.id, user.class.name]
|
68
|
-
end
|
69
|
-
relation = relation.where(parts.join(" OR "), *binds) if parts.any?
|
70
|
-
|
71
|
-
relation =
|
72
|
-
if options[:list]
|
73
|
-
relation.where("list IS NULL OR list = ?", options[:list])
|
74
|
-
else
|
75
|
-
relation.where("list IS NULL")
|
76
|
-
end
|
77
|
-
|
78
|
-
relation
|
79
|
-
end
|
80
|
-
|
81
|
-
def self.opted_out_emails(options = {})
|
82
|
-
Set.new(opt_outs(options).where("email IS NOT NULL").uniq.pluck(:email))
|
83
|
-
end
|
84
|
-
|
85
|
-
# does not take into account emails
|
86
|
-
def self.opted_out_users(options = {})
|
87
|
-
Set.new(opt_outs(options).where("user_id IS NOT NULL").map(&:user))
|
88
|
-
end
|
89
|
-
|
90
40
|
def self.message_verifier
|
91
41
|
@message_verifier ||= ActiveSupport::MessageVerifier.new(Mailkick.secret_token)
|
92
42
|
end
|
93
43
|
|
94
|
-
def self.generate_token(
|
95
|
-
raise ArgumentError, "Missing
|
44
|
+
def self.generate_token(subscriber, list)
|
45
|
+
raise ArgumentError, "Missing subscriber" unless subscriber
|
46
|
+
raise ArgumentError, "Missing list" unless list.present?
|
96
47
|
|
97
|
-
|
98
|
-
message_verifier.generate([email, user.try(:id), user.try(:class).try(:name), list])
|
48
|
+
message_verifier.generate([nil, subscriber.id, subscriber.class.name, list])
|
99
49
|
end
|
100
50
|
end
|
101
51
|
|
52
|
+
ActiveSupport.on_load :action_mailer do
|
53
|
+
helper Mailkick::UrlHelper
|
54
|
+
end
|
55
|
+
|
102
56
|
ActiveSupport.on_load(:active_record) do
|
103
57
|
extend Mailkick::Model
|
104
58
|
end
|
data/lib/mailkick/engine.rb
CHANGED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Mailkick
|
2
|
+
module Legacy
|
3
|
+
# checks for table as long as it exists
|
4
|
+
def self.opt_outs?
|
5
|
+
unless defined?(@opt_outs) && @opt_outs == false
|
6
|
+
@opt_outs = ActiveRecord::Base.connection.table_exists?("mailkick_opt_outs")
|
7
|
+
end
|
8
|
+
@opt_outs
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.opted_out?(options)
|
12
|
+
opt_outs(options).any?
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.opt_out(options)
|
16
|
+
unless opted_out?(options)
|
17
|
+
time = options[:time] || Time.now
|
18
|
+
Mailkick::OptOut.create! do |o|
|
19
|
+
o.email = options[:email]
|
20
|
+
o.user = options[:user]
|
21
|
+
o.reason = options[:reason] || "unsubscribe"
|
22
|
+
o.list = options[:list]
|
23
|
+
o.created_at = time
|
24
|
+
o.updated_at = time
|
25
|
+
end
|
26
|
+
end
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.opt_in(options)
|
31
|
+
opt_outs(options).each do |opt_out|
|
32
|
+
opt_out.active = false
|
33
|
+
opt_out.save!
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.opt_outs(options = {})
|
39
|
+
relation = Mailkick::OptOut.where(active: true)
|
40
|
+
|
41
|
+
contact_relation = Mailkick::OptOut.none
|
42
|
+
if (email = options[:email])
|
43
|
+
contact_relation = contact_relation.or(Mailkick::OptOut.where(email: email))
|
44
|
+
end
|
45
|
+
if (user = options[:user])
|
46
|
+
contact_relation = contact_relation.or(
|
47
|
+
Mailkick::OptOut.where("user_id = ? AND user_type = ?", user.id, user.class.name)
|
48
|
+
)
|
49
|
+
end
|
50
|
+
relation = relation.merge(contact_relation) if email || user
|
51
|
+
|
52
|
+
relation =
|
53
|
+
if options[:list]
|
54
|
+
relation.where("list IS NULL OR list = ?", options[:list])
|
55
|
+
else
|
56
|
+
relation.where("list IS NULL")
|
57
|
+
end
|
58
|
+
|
59
|
+
relation
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.opted_out_emails(options = {})
|
63
|
+
Set.new(opt_outs(options).where.not(email: nil).distinct.pluck(:email))
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.opted_out_users(options = {})
|
67
|
+
Set.new(opt_outs(options).where.not(user_id: nil).map(&:user))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/mailkick/model.rb
CHANGED
@@ -1,32 +1,22 @@
|
|
1
1
|
module Mailkick
|
2
2
|
module Model
|
3
|
-
def
|
4
|
-
email_key = opts[:email_key] || :email
|
3
|
+
def has_subscriptions
|
5
4
|
class_eval do
|
6
|
-
|
7
|
-
|
8
|
-
if options[:list]
|
9
|
-
query = "(mailkick_opt_outs.list IS NULL OR mailkick_opt_outs.list = ?)"
|
10
|
-
binds << options[:list]
|
11
|
-
else
|
12
|
-
query = "mailkick_opt_outs.list IS NULL"
|
13
|
-
end
|
14
|
-
where("#{options[:not] ? 'NOT ' : ''}EXISTS(SELECT * FROM mailkick_opt_outs WHERE (#{table_name}.#{email_key} = mailkick_opt_outs.email OR (#{table_name}.#{primary_key} = mailkick_opt_outs.user_id AND mailkick_opt_outs.user_type = ?)) AND mailkick_opt_outs.active = ? AND #{query})", *binds)
|
15
|
-
}
|
16
|
-
scope :not_opted_out, proc {|options = {}|
|
17
|
-
opted_out(options.merge(not: true))
|
18
|
-
}
|
5
|
+
has_many :mailkick_subscriptions, class_name: "Mailkick::Subscription", as: :subscriber
|
6
|
+
scope :subscribed, -> (list) { joins(:mailkick_subscriptions).where(mailkick_subscriptions: {list: list}) }
|
19
7
|
|
20
|
-
def
|
21
|
-
|
8
|
+
def subscribe(list)
|
9
|
+
mailkick_subscriptions.where(list: list).first_or_create!
|
10
|
+
nil
|
22
11
|
end
|
23
12
|
|
24
|
-
def
|
25
|
-
|
13
|
+
def unsubscribe(list)
|
14
|
+
mailkick_subscriptions.where(list: list).delete_all
|
15
|
+
nil
|
26
16
|
end
|
27
17
|
|
28
|
-
def
|
29
|
-
|
18
|
+
def subscribed?(list)
|
19
|
+
mailkick_subscriptions.where(list: list).exists?
|
30
20
|
end
|
31
21
|
end
|
32
22
|
end
|
data/lib/mailkick/service.rb
CHANGED
@@ -1,22 +1,7 @@
|
|
1
1
|
module Mailkick
|
2
2
|
class Service
|
3
3
|
def fetch_opt_outs
|
4
|
-
opt_outs
|
5
|
-
email = api_data[:email]
|
6
|
-
time = api_data[:time]
|
7
|
-
|
8
|
-
opt_out = Mailkick::OptOut.where(email: email).order("updated_at desc").first
|
9
|
-
if !opt_out || (time > opt_out.updated_at && !opt_out.active)
|
10
|
-
Mailkick.opt_out(
|
11
|
-
email: email,
|
12
|
-
user: Mailkick.user_method ? Mailkick.user_method.call(email) : nil,
|
13
|
-
reason: api_data[:reason],
|
14
|
-
time: time
|
15
|
-
)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
true
|
4
|
+
Mailkick.process_opt_outs_method.call(opt_outs)
|
20
5
|
end
|
21
6
|
end
|
22
7
|
end
|