mailkick 0.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -18
  3. data/LICENSE.txt +1 -1
  4. data/README.md +153 -85
  5. data/app/controllers/mailkick/subscriptions_controller.rb +38 -20
  6. data/app/models/mailkick/opt_out.rb +1 -1
  7. data/app/models/mailkick/subscription.rb +9 -0
  8. data/app/views/mailkick/subscriptions/show.html.erb +1 -1
  9. data/lib/generators/mailkick/install_generator.rb +4 -21
  10. data/lib/generators/mailkick/templates/install.rb.tt +11 -0
  11. data/lib/mailkick.rb +23 -69
  12. data/lib/mailkick/engine.rb +0 -4
  13. data/lib/mailkick/legacy.rb +70 -0
  14. data/lib/mailkick/model.rb +11 -21
  15. data/lib/mailkick/service.rb +1 -16
  16. data/lib/mailkick/service/aws_ses.rb +47 -0
  17. data/lib/mailkick/service/postmark.rb +41 -0
  18. data/lib/mailkick/service/sendgrid.rb +4 -1
  19. data/lib/mailkick/service/sendgrid_v2.rb +52 -0
  20. data/lib/mailkick/url_helper.rb +8 -0
  21. data/lib/mailkick/version.rb +1 -1
  22. metadata +20 -184
  23. data/.gitignore +0 -25
  24. data/.travis.yml +0 -16
  25. data/Gemfile +0 -6
  26. data/Rakefile +0 -9
  27. data/app/helpers/mailkick/url_helper.rb +0 -15
  28. data/lib/generators/mailkick/templates/install.rb +0 -14
  29. data/mailkick.gemspec +0 -32
  30. data/test/gemfiles/actionmailer42.gemfile +0 -6
  31. data/test/gemfiles/actionmailer50.gemfile +0 -6
  32. data/test/gemfiles/actionmailer51.gemfile +0 -6
  33. data/test/internal/app/mailers/user_mailer.rb +0 -7
  34. data/test/internal/app/models/user.rb +0 -3
  35. data/test/internal/app/views/user_mailer/welcome.html.erb +0 -1
  36. data/test/internal/app/views/user_mailer/welcome.text.erb +0 -1
  37. data/test/internal/config/database.yml +0 -3
  38. data/test/internal/db/schema.rb +0 -16
  39. data/test/mailkick_test.rb +0 -29
  40. 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 :set_email
5
+ before_action :set_subscription
6
6
 
7
7
  def show
8
8
  end
9
9
 
10
10
  def unsubscribe
11
- Mailkick.opt_out(@options)
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
- Mailkick.opt_in(@options)
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 set_email
23
- @email, user_id, user_type, @list = Mailkick.message_verifier.verify(params[:id])
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
- if Rails::VERSION::MAJOR >= 5
35
- render plain: "Subscription not found", status: :bad_request
36
- else
37
- render text: "Subscription not found", status: :bad_request
38
- end
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
- Mailkick.opted_out?(@options)
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, ActiveRecord::VERSION::MAJOR >= 5 ? {polymorphic: true, optional: true} : {polymorphic: true}
5
+ belongs_to :user, polymorphic: true, optional: true
6
6
  end
7
7
  end
@@ -0,0 +1,9 @@
1
+ module Mailkick
2
+ class Subscription < ActiveRecord::Base
3
+ self.table_name = "mailkick_subscriptions"
4
+
5
+ belongs_to :subscriber, polymorphic: true
6
+
7
+ validates :list, presence: true
8
+ end
9
+ end
@@ -24,7 +24,7 @@
24
24
  </head>
25
25
  <body>
26
26
  <div class="container">
27
- <% if opted_out? %>
27
+ <% if !subscribed? %>
28
28
  <p>You are unsubscribed.</p>
29
29
  <p><%= link_to "Resubscribe", subscribe_url %></p>
30
30
  <% else %>
@@ -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 Rails::Generators::Migration
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/install_mailkick.rb", migration_version: migration_version
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
- if ActiveRecord::VERSION::MAJOR >= 5
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
- require "set"
1
+ # dependencies
2
2
  require "active_support"
3
3
 
4
- require "mailkick/engine" if defined?(Rails)
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, :user_method, :secret_token, :mount
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(email, user: nil, list: nil)
95
- raise ArgumentError, "Missing email" unless email
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
- user ||= Mailkick.user_method.call(email) if Mailkick.user_method
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
@@ -17,10 +17,6 @@ module Mailkick
17
17
 
18
18
  creds.respond_to?(:secret_key_base) ? creds.secret_key_base : creds.secret_token
19
19
  end
20
-
21
- ActiveSupport.on_load :action_mailer do
22
- helper Mailkick::UrlHelper
23
- end
24
20
  end
25
21
  end
26
22
  end
@@ -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
@@ -1,32 +1,22 @@
1
1
  module Mailkick
2
2
  module Model
3
- def mailkick_user(opts = {})
4
- email_key = opts[:email_key] || :email
3
+ def has_subscriptions
5
4
  class_eval do
6
- scope :opted_out, proc {|options = {}|
7
- binds = [self.class.name, true]
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 opted_out?(options = {})
21
- Mailkick.opted_out?({email: email, user: self}.merge(options))
8
+ def subscribe(list)
9
+ mailkick_subscriptions.where(list: list).first_or_create!
10
+ nil
22
11
  end
23
12
 
24
- def opt_out(options = {})
25
- Mailkick.opt_out({email: email, user: self}.merge(options))
13
+ def unsubscribe(list)
14
+ mailkick_subscriptions.where(list: list).delete_all
15
+ nil
26
16
  end
27
17
 
28
- def opt_in(options = {})
29
- Mailkick.opt_in({email: email, user: self}.merge(options))
18
+ def subscribed?(list)
19
+ mailkick_subscriptions.where(list: list).exists?
30
20
  end
31
21
  end
32
22
  end
@@ -1,22 +1,7 @@
1
1
  module Mailkick
2
2
  class Service
3
3
  def fetch_opt_outs
4
- opt_outs.each do |api_data|
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