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.
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